mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 21:37:59 +00:00
Move bezier-rs into libraries folder and deploy its interactive docs
This commit is contained in:
parent
78a3644c45
commit
4412b983cd
44 changed files with 352 additions and 544 deletions
14
libraries/bezier-rs/Cargo.toml
Normal file
14
libraries/bezier-rs/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "bezier-rs"
|
||||
version = "0.1.0"
|
||||
rust-version = "1.62.0"
|
||||
edition = "2021"
|
||||
authors = ["Graphite Authors <contact@graphite.rs>"]
|
||||
description = "A wide assortment of useful math functions for Bezier segments and shapes."
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "./README.md"
|
||||
homepage = "https://graphite.rs/libraries/bezier-rs"
|
||||
repository = "https://github.com/GraphiteEditor/Graphite/libraries/bezier-rs"
|
||||
|
||||
[dependencies]
|
||||
glam = { version = "0.17", features = ["serde"] }
|
201
libraries/bezier-rs/LICENSE-APACHE
Normal file
201
libraries/bezier-rs/LICENSE-APACHE
Normal file
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
17
libraries/bezier-rs/LICENSE-MIT
Normal file
17
libraries/bezier-rs/LICENSE-MIT
Normal file
|
@ -0,0 +1,17 @@
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
3
libraries/bezier-rs/README.md
Normal file
3
libraries/bezier-rs/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Bezier-rs
|
||||
|
||||
A wide assortment of useful math functions for Bezier segments and shapes.
|
28
libraries/bezier-rs/src/consts.rs
Normal file
28
libraries/bezier-rs/src/consts.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Implementation constants
|
||||
|
||||
/// Constant used to determine if `f64`s are equivalent.
|
||||
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
|
||||
/// A stricter constant used to determine if `f64`s are equivalent.
|
||||
pub const STRICT_MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-6;
|
||||
/// Number of distances used in search algorithm for `project`.
|
||||
pub const NUM_DISTANCES: usize = 5;
|
||||
/// Maximum allowed angle that the normal of the `start` or `end` point can make with the normal of the corresponding handle for a curve to be considered scalable/simple.
|
||||
pub const SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE: f64 = std::f64::consts::PI / 3.;
|
||||
|
||||
// Method argument defaults
|
||||
|
||||
/// Default `t` value used for the `curve_through_points` functions.
|
||||
pub const DEFAULT_T_VALUE: f64 = 0.5;
|
||||
/// Default LUT step size in `compute_lookup_table` function.
|
||||
pub const DEFAULT_LUT_STEP_SIZE: usize = 10;
|
||||
/// Default number of subdivisions used in `length` calculation.
|
||||
pub const DEFAULT_LENGTH_SUBDIVISIONS: usize = 1000;
|
||||
/// Default step size for `reduce` function.
|
||||
pub const DEFAULT_REDUCE_STEP_SIZE: f64 = 0.01;
|
||||
|
||||
// SVG constants
|
||||
pub const SVG_ARG_CUBIC: &str = "C";
|
||||
pub const SVG_ARG_LINEAR: &str = "L";
|
||||
pub const SVG_ARG_MOVE: &str = "M";
|
||||
pub const SVG_ARG_QUADRATIC: &str = "Q";
|
||||
pub const SVG_ARG_CLOSED: &str = "Z";
|
1447
libraries/bezier-rs/src/lib.rs
Normal file
1447
libraries/bezier-rs/src/lib.rs
Normal file
File diff suppressed because it is too large
Load diff
95
libraries/bezier-rs/src/structs.rs
Normal file
95
libraries/bezier-rs/src/structs.rs
Normal file
|
@ -0,0 +1,95 @@
|
|||
use glam::DVec2;
|
||||
use std::fmt::{Debug, Formatter, Result};
|
||||
|
||||
/// Struct to represent optional parameters that can be passed to the `project` function.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct ProjectionOptions {
|
||||
/// Size of the lookup table for the initial passthrough. The default value is `20`.
|
||||
pub lut_size: usize,
|
||||
/// Difference used between floating point numbers to be considered as equal. The default value is `0.0001`
|
||||
pub convergence_epsilon: f64,
|
||||
/// Controls the number of iterations needed to consider that minimum distance to have converged. The default value is `3`.
|
||||
pub convergence_limit: usize,
|
||||
/// Controls the maximum total number of iterations to be used. The default value is `10`.
|
||||
pub iteration_limit: usize,
|
||||
}
|
||||
|
||||
impl Default for ProjectionOptions {
|
||||
fn default() -> Self {
|
||||
ProjectionOptions {
|
||||
lut_size: 20,
|
||||
convergence_epsilon: 1e-4,
|
||||
convergence_limit: 3,
|
||||
iteration_limit: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct used to represent the different strategies for generating arc approximations.
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum ArcStrategy {
|
||||
/// Start with the greedy strategy of maximizing arc approximations and automatically switch to the divide-and-conquer when the greedy approximations no longer fall within the error bound.
|
||||
Automatic,
|
||||
/// Use the greedy strategy to maximize approximated arcs, despite potentially erroneous arcs.
|
||||
FavorLargerArcs,
|
||||
/// Use the divide-and-conquer strategy that prioritizes correctness over maximal arcs.
|
||||
FavorCorrectness,
|
||||
}
|
||||
|
||||
/// Struct to represent optional parameters that can be passed to the `arcs` function.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct ArcsOptions {
|
||||
/// Determines how the approximated arcs are computed.
|
||||
/// When maximizing the arcs, the algorithm may return incorrect arcs when the curve contains any small loops or segments that look like a very thin "U".
|
||||
/// The enum options behave as follows:
|
||||
/// - `Automatic`: Maximize arcs until an erroneous approximation is found. Compute the arcs of the rest of the curve by first splitting on extremas to ensure no more erroneous cases are encountered.
|
||||
/// - `FavorLargerArcs`: Maximize arcs using the original algorithm from the [Approximating a Bezier curve with circular arcs](https://pomax.github.io/bezierinfo/#arcapproximation) section of Pomax's bezier curve primer. Erroneous arcs are possible.
|
||||
/// - `FavorCorrectness`: Prioritize correctness by first spliting the curve by its extremas and determine the arc approximation of each segment instead.
|
||||
///
|
||||
/// The default value is `Automatic`.
|
||||
pub strategy: ArcStrategy,
|
||||
/// The error used for approximating the arc's fit. The default is `0.5`.
|
||||
pub error: f64,
|
||||
/// The maximum number of segment iterations used as attempts for arc approximations. The default is `100`.
|
||||
pub max_iterations: usize,
|
||||
}
|
||||
|
||||
impl Default for ArcsOptions {
|
||||
fn default() -> Self {
|
||||
ArcsOptions {
|
||||
strategy: ArcStrategy::Automatic,
|
||||
error: 0.5,
|
||||
max_iterations: 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct to represent the circular arc approximation used in the `arcs` bezier function.
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub struct CircleArc {
|
||||
/// The center point of the circle.
|
||||
pub center: DVec2,
|
||||
/// The radius of the circle.
|
||||
pub radius: f64,
|
||||
/// The start angle of the circle sector in rad.
|
||||
pub start_angle: f64,
|
||||
/// The end angle of the circle sector in rad.
|
||||
pub end_angle: f64,
|
||||
}
|
||||
|
||||
impl Debug for CircleArc {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
write!(f, "Center: {}, radius: {}, start to end angles: {} to {}", self.center, self.radius, self.start_angle, self.end_angle)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CircleArc {
|
||||
fn default() -> Self {
|
||||
CircleArc {
|
||||
center: DVec2::ZERO,
|
||||
radius: 0.,
|
||||
start_angle: 0.,
|
||||
end_angle: 0.,
|
||||
}
|
||||
}
|
||||
}
|
88
libraries/bezier-rs/src/subpath/core.rs
Normal file
88
libraries/bezier-rs/src/subpath/core.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
use super::*;
|
||||
use crate::consts::*;
|
||||
|
||||
/// Functionality relating to core `Subpath` operations, such as constructors and `iter`.
|
||||
impl Subpath {
|
||||
/// Create a new `Subpath` using a list of [ManipulatorGroup]s.
|
||||
/// A `Subpath` with less than 2 [ManipulatorGroup]s may not be closed.
|
||||
pub fn new(manipulator_groups: Vec<ManipulatorGroup>, closed: bool) -> Self {
|
||||
assert!(!closed || manipulator_groups.len() > 1, "A closed Subpath must contain more than 1 ManipulatorGroup.");
|
||||
Self { manipulator_groups, closed }
|
||||
}
|
||||
|
||||
/// Create a `Subpath` consisting of 2 manipulator groups from a `Bezier`.
|
||||
pub fn from_bezier(bezier: Bezier) -> Self {
|
||||
Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
anchor: bezier.start(),
|
||||
in_handle: None,
|
||||
out_handle: bezier.handle_start(),
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: bezier.end(),
|
||||
in_handle: bezier.handle_end(),
|
||||
out_handle: None,
|
||||
},
|
||||
],
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns true if the `Subpath` contains no [ManipulatorGroup].
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.manipulator_groups.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the number of [ManipulatorGroup]s contained within the `Subpath`.
|
||||
pub fn len(&self) -> usize {
|
||||
self.manipulator_groups.len()
|
||||
}
|
||||
|
||||
/// Returns an iterator of the [Bezier]s along the `Subpath`.
|
||||
pub fn iter(&self) -> SubpathIter {
|
||||
SubpathIter { sub_path: self, index: 0 }
|
||||
}
|
||||
|
||||
/// Returns an SVG representation of the `Subpath`.
|
||||
pub fn to_svg(&self, options: ToSVGOptions) -> String {
|
||||
if self.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let curve_start_argument = format!("{SVG_ARG_MOVE}{} {}", self[0].anchor.x, self[0].anchor.y);
|
||||
let mut curve_arguments: Vec<String> = self.iter().map(|bezier| bezier.svg_curve_argument()).collect();
|
||||
if self.closed {
|
||||
curve_arguments.push(String::from(SVG_ARG_CLOSED));
|
||||
}
|
||||
|
||||
let anchor_arguments = options.formatted_anchor_arguments();
|
||||
let anchor_circles = self
|
||||
.manipulator_groups
|
||||
.iter()
|
||||
.map(|point| format!(r#"<circle cx="{}" cy="{}" {}/>"#, point.anchor.x, point.anchor.y, anchor_arguments))
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let handle_point_arguments = options.formatted_handle_point_arguments();
|
||||
let handle_circles: Vec<String> = self
|
||||
.manipulator_groups
|
||||
.iter()
|
||||
.flat_map(|group| [group.in_handle, group.out_handle])
|
||||
.flatten()
|
||||
.map(|handle| format!(r#"<circle cx="{}" cy="{}" {}/>"#, handle.x, handle.y, handle_point_arguments))
|
||||
.collect();
|
||||
|
||||
let handle_pieces: Vec<String> = self.iter().filter_map(|bezier| bezier.svg_handle_line_argument()).collect();
|
||||
|
||||
format!(
|
||||
r#"<path d="{} {}" {}/><path d="{}" {}/>{}{}"#,
|
||||
curve_start_argument,
|
||||
curve_arguments.join(" "),
|
||||
options.formatted_curve_arguments(),
|
||||
handle_pieces.join(" "),
|
||||
options.formatted_handle_line_arguments(),
|
||||
handle_circles.join(""),
|
||||
anchor_circles.join(""),
|
||||
)
|
||||
}
|
||||
}
|
95
libraries/bezier-rs/src/subpath/lookup.rs
Normal file
95
libraries/bezier-rs/src/subpath/lookup.rs
Normal file
|
@ -0,0 +1,95 @@
|
|||
use super::*;
|
||||
|
||||
/// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`.
|
||||
impl Subpath {
|
||||
/// Return the sum of the approximation of the length of each `Bezier` curve along the `Subpath`.
|
||||
/// - `num_subdivisions` - Number of subdivisions used to approximate the curve. The default value is `1000`.
|
||||
pub fn length(&self, num_subdivisions: Option<usize>) -> f64 {
|
||||
self.iter().fold(0., |accumulator, bezier| accumulator + bezier.length(num_subdivisions))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Bezier;
|
||||
use glam::DVec2;
|
||||
|
||||
#[test]
|
||||
fn length_quadratic() {
|
||||
let start = DVec2::new(20., 30.);
|
||||
let middle = DVec2::new(80., 90.);
|
||||
let end = DVec2::new(60., 45.);
|
||||
let handle1 = DVec2::new(75., 85.);
|
||||
let handle2 = DVec2::new(40., 30.);
|
||||
let handle3 = DVec2::new(10., 10.);
|
||||
|
||||
let bezier1 = Bezier::from_quadratic_dvec2(start, handle1, middle);
|
||||
let bezier2 = Bezier::from_quadratic_dvec2(middle, handle2, end);
|
||||
let bezier3 = Bezier::from_quadratic_dvec2(end, handle3, start);
|
||||
|
||||
let mut subpath = Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
anchor: start,
|
||||
in_handle: None,
|
||||
out_handle: Some(handle1),
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: middle,
|
||||
in_handle: None,
|
||||
out_handle: Some(handle2),
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: end,
|
||||
in_handle: None,
|
||||
out_handle: Some(handle3),
|
||||
},
|
||||
],
|
||||
false,
|
||||
);
|
||||
assert_eq!(subpath.length(None), bezier1.length(None) + bezier2.length(None));
|
||||
|
||||
subpath.closed = true;
|
||||
assert_eq!(subpath.length(None), bezier1.length(None) + bezier2.length(None) + bezier3.length(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn length_mixed() {
|
||||
let start = DVec2::new(20., 30.);
|
||||
let middle = DVec2::new(70., 70.);
|
||||
let end = DVec2::new(60., 45.);
|
||||
let handle1 = DVec2::new(75., 85.);
|
||||
let handle2 = DVec2::new(40., 30.);
|
||||
let handle3 = DVec2::new(10., 10.);
|
||||
|
||||
let linear_bezier = Bezier::from_linear_dvec2(start, middle);
|
||||
let quadratic_bezier = Bezier::from_quadratic_dvec2(middle, handle1, end);
|
||||
let cubic_bezier = Bezier::from_cubic_dvec2(end, handle2, handle3, start);
|
||||
|
||||
let mut subpath = Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
anchor: start,
|
||||
in_handle: Some(handle3),
|
||||
out_handle: None,
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: middle,
|
||||
in_handle: None,
|
||||
out_handle: Some(handle1),
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: end,
|
||||
in_handle: None,
|
||||
out_handle: Some(handle2),
|
||||
},
|
||||
],
|
||||
false,
|
||||
);
|
||||
assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None));
|
||||
|
||||
subpath.closed = true;
|
||||
assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None) + cubic_bezier.length(None));
|
||||
}
|
||||
}
|
68
libraries/bezier-rs/src/subpath/mod.rs
Normal file
68
libraries/bezier-rs/src/subpath/mod.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
mod core;
|
||||
mod lookup;
|
||||
mod structs;
|
||||
pub use structs::*;
|
||||
|
||||
use crate::Bezier;
|
||||
|
||||
use std::ops::{Index, IndexMut};
|
||||
|
||||
/// Structure used to represent a path composed of [Bezier] curves.
|
||||
pub struct Subpath {
|
||||
manipulator_groups: Vec<ManipulatorGroup>,
|
||||
closed: bool,
|
||||
}
|
||||
|
||||
/// Iteration structure for iterating across each curve of a `Subpath`, using an intermediate `Bezier` representation.
|
||||
pub struct SubpathIter<'a> {
|
||||
index: usize,
|
||||
sub_path: &'a Subpath,
|
||||
}
|
||||
|
||||
impl Index<usize> for Subpath {
|
||||
type Output = ManipulatorGroup;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
assert!(index < self.len(), "Index out of bounds in trait Index of SubPath.");
|
||||
&self.manipulator_groups[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<usize> for Subpath {
|
||||
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
|
||||
assert!(index < self.len(), "Index out of bounds in trait IndexMut of SubPath.");
|
||||
&mut self.manipulator_groups[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SubpathIter<'_> {
|
||||
type Item = Bezier;
|
||||
|
||||
// Returns the Bezier representation of each `Subpath` segment, defined between a pair of adjacent manipulator points.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let len = self.sub_path.len() - 1
|
||||
+ match self.sub_path.closed {
|
||||
true => 1,
|
||||
false => 0,
|
||||
};
|
||||
if self.index >= len {
|
||||
return None;
|
||||
}
|
||||
let start_index = self.index;
|
||||
let end_index = (self.index + 1) % self.sub_path.len();
|
||||
self.index += 1;
|
||||
|
||||
let start = self.sub_path[start_index].anchor;
|
||||
let end = self.sub_path[end_index].anchor;
|
||||
let out_handle = self.sub_path[start_index].out_handle;
|
||||
let in_handle = self.sub_path[end_index].in_handle;
|
||||
|
||||
if let (Some(handle1), Some(handle2)) = (out_handle, in_handle) {
|
||||
Some(Bezier::from_cubic_dvec2(start, handle1, handle2, end))
|
||||
} else if let Some(handle) = out_handle.or(in_handle) {
|
||||
Some(Bezier::from_quadratic_dvec2(start, handle, end))
|
||||
} else {
|
||||
Some(Bezier::from_linear_dvec2(start, end))
|
||||
}
|
||||
}
|
||||
}
|
83
libraries/bezier-rs/src/subpath/structs.rs
Normal file
83
libraries/bezier-rs/src/subpath/structs.rs
Normal file
|
@ -0,0 +1,83 @@
|
|||
use glam::DVec2;
|
||||
|
||||
/// Structure used to represent a single anchor with up to two optional associated handles along a `Subpath`
|
||||
pub struct ManipulatorGroup {
|
||||
pub anchor: DVec2,
|
||||
pub in_handle: Option<DVec2>,
|
||||
pub out_handle: Option<DVec2>,
|
||||
}
|
||||
|
||||
/// Structure to represent optional parameters that can be passed to the `into_svg` function.
|
||||
pub struct ToSVGOptions {
|
||||
/// Color of the line segments along the `Subpath`. Defaulted to `black`.
|
||||
pub curve_stroke_color: String,
|
||||
/// Width of the line segments along the `Subpath`. Defaulted to `2.`.
|
||||
pub curve_stroke_width: f64,
|
||||
/// Stroke color outlining circles marking anchors on the `Subpath`. Defaulted to `black`.
|
||||
pub anchor_stroke_color: String,
|
||||
/// Stroke width outlining circles marking anchors on the `Subpath`. Defaulted to `2.`.
|
||||
pub anchor_stroke_width: f64,
|
||||
/// Radius of the circles marking anchors on the `Subpath`. Defaulted to `4.`.
|
||||
pub anchor_radius: f64,
|
||||
/// Fill color of the circles marking anchors on the `Subpath`. Defaulted to `white`.
|
||||
pub anchor_fill: String,
|
||||
/// Color of the line segments connecting anchors to handle points. Defaulted to `gray`.
|
||||
pub handle_line_stroke_color: String,
|
||||
/// Width of the line segments connecting anchors to handle points. Defaulted to `1.`.
|
||||
pub handle_line_stroke_width: f64,
|
||||
/// Stroke color outlining circles marking the handles of `Subpath`. Defaulted to `gray`.
|
||||
pub handle_point_stroke_color: String,
|
||||
/// Stroke color outlining circles marking the handles of `Subpath`. Defaulted to `1.5`.
|
||||
pub handle_point_stroke_width: f64,
|
||||
/// Radius of the circles marking the handles of `Subpath`. Defaulted to `3.`.
|
||||
pub handle_point_radius: f64,
|
||||
/// Fill color of the circles marking the handles of `Subpath`. Defaulted to `white`.
|
||||
pub handle_point_fill: String,
|
||||
}
|
||||
|
||||
impl ToSVGOptions {
|
||||
/// Combine and format curve styling options for an SVG path.
|
||||
pub(crate) fn formatted_curve_arguments(&self) -> String {
|
||||
format!(r#"stroke="{}" stroke-width="{}" fill="none""#, self.curve_stroke_color, self.curve_stroke_width)
|
||||
}
|
||||
|
||||
/// Combine and format anchor styling options an SVG circle.
|
||||
pub(crate) fn formatted_anchor_arguments(&self) -> String {
|
||||
format!(
|
||||
r#"r="{}", stroke="{}" stroke-width="{}" fill="{}""#,
|
||||
self.anchor_radius, self.anchor_stroke_color, self.anchor_stroke_width, self.anchor_fill
|
||||
)
|
||||
}
|
||||
|
||||
/// Combine and format handle point styling options for an SVG circle.
|
||||
pub(crate) fn formatted_handle_point_arguments(&self) -> String {
|
||||
format!(
|
||||
r#"r="{}", stroke="{}" stroke-width="{}" fill="{}""#,
|
||||
self.handle_point_radius, self.handle_point_stroke_color, self.handle_point_stroke_width, self.handle_point_fill
|
||||
)
|
||||
}
|
||||
|
||||
/// Combine and format handle line styling options an SVG path.
|
||||
pub(crate) fn formatted_handle_line_arguments(&self) -> String {
|
||||
format!(r#"stroke="{}" stroke-width="{}" fill="none""#, self.handle_line_stroke_color, self.handle_line_stroke_width)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ToSVGOptions {
|
||||
fn default() -> Self {
|
||||
ToSVGOptions {
|
||||
curve_stroke_color: String::from("black"),
|
||||
curve_stroke_width: 2.,
|
||||
anchor_stroke_color: String::from("black"),
|
||||
anchor_stroke_width: 2.,
|
||||
anchor_radius: 4.,
|
||||
anchor_fill: String::from("white"),
|
||||
handle_line_stroke_color: String::from("gray"),
|
||||
handle_line_stroke_width: 1.,
|
||||
handle_point_stroke_color: String::from("gray"),
|
||||
handle_point_stroke_width: 1.5,
|
||||
handle_point_radius: 3.,
|
||||
handle_point_fill: String::from("white"),
|
||||
}
|
||||
}
|
||||
}
|
333
libraries/bezier-rs/src/utils.rs
Normal file
333
libraries/bezier-rs/src/utils.rs
Normal file
|
@ -0,0 +1,333 @@
|
|||
use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE};
|
||||
|
||||
use glam::{BVec2, DMat2, DVec2};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
/// Helper to perform the computation of a and c, where b is the provided point on the curve.
|
||||
/// Given the correct power of `t` and `(1-t)`, the computation is the same for quadratic and cubic cases.
|
||||
/// Relevant derivation and the definitions of a, b, and c can be found in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer.
|
||||
fn compute_abc_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t_to_nth_power: f64, nth_power_of_one_minus_t: f64) -> [DVec2; 3] {
|
||||
let point_c_ratio = nth_power_of_one_minus_t / (t_to_nth_power + nth_power_of_one_minus_t);
|
||||
let c = point_c_ratio * start_point + (1. - point_c_ratio) * end_point;
|
||||
let ab_bc_ratio = (t_to_nth_power + nth_power_of_one_minus_t - 1.).abs() / (t_to_nth_power + nth_power_of_one_minus_t);
|
||||
let a = point_on_curve + (point_on_curve - c) / ab_bc_ratio;
|
||||
[a, point_on_curve, c]
|
||||
}
|
||||
|
||||
/// Compute `a`, `b`, and `c` for a quadratic curve that fits the start, end and point on curve at `t`.
|
||||
/// The definition for the `a`, `b`, `c` points are defined in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer.
|
||||
pub fn compute_abc_for_quadratic_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t: f64) -> [DVec2; 3] {
|
||||
let t_squared = t * t;
|
||||
let one_minus_t = 1. - t;
|
||||
let squared_one_minus_t = one_minus_t * one_minus_t;
|
||||
compute_abc_through_points(start_point, point_on_curve, end_point, t_squared, squared_one_minus_t)
|
||||
}
|
||||
|
||||
/// Compute `a`, `b`, and `c` for a cubic curve that fits the start, end and point on curve at `t`.
|
||||
/// The definition for the `a`, `b`, `c` points are defined in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer.
|
||||
pub fn compute_abc_for_cubic_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t: f64) -> [DVec2; 3] {
|
||||
let t_cubed = t * t * t;
|
||||
let one_minus_t = 1. - t;
|
||||
let cubed_one_minus_t = one_minus_t * one_minus_t * one_minus_t;
|
||||
|
||||
compute_abc_through_points(start_point, point_on_curve, end_point, t_cubed, cubed_one_minus_t)
|
||||
}
|
||||
|
||||
/// Return the index and the value of the closest point in the LUT compared to the provided point.
|
||||
pub fn get_closest_point_in_lut(lut: &[DVec2], point: DVec2) -> (usize, f64) {
|
||||
lut.iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| (i, point.distance_squared(*p)))
|
||||
.min_by(|x, y| (&(x.1)).partial_cmp(&(y.1)).unwrap())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// TODO: Use an `Option` return type instead of a `Vec`
|
||||
/// Find the roots of the linear equation `ax + b`.
|
||||
pub fn solve_linear(a: f64, b: f64) -> Vec<f64> {
|
||||
let mut roots = Vec::new();
|
||||
// There exist roots when `a` is not 0
|
||||
if a.abs() > MAX_ABSOLUTE_DIFFERENCE {
|
||||
roots.push(-b / a);
|
||||
}
|
||||
roots
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Find the roots of the linear equation `ax^2 + bx + c`.
|
||||
/// Precompute the `discriminant` (`b^2 - 4ac`) and `two_times_a` arguments prior to calling this function for efficiency purposes.
|
||||
pub fn solve_quadratic(discriminant: f64, two_times_a: f64, b: f64, c: f64) -> Vec<f64> {
|
||||
let mut roots = Vec::new();
|
||||
if two_times_a != 0. {
|
||||
if discriminant > 0. {
|
||||
let root_discriminant = discriminant.sqrt();
|
||||
roots.push((-b + root_discriminant) / (two_times_a));
|
||||
roots.push((-b - root_discriminant) / (two_times_a));
|
||||
} else if discriminant == 0. {
|
||||
roots.push(-b / (two_times_a));
|
||||
}
|
||||
} else {
|
||||
roots = solve_linear(b, c);
|
||||
}
|
||||
roots
|
||||
}
|
||||
|
||||
/// Compute the cube root of a number.
|
||||
fn cube_root(f: f64) -> f64 {
|
||||
if f < 0. {
|
||||
-(-f).powf(1. / 3.)
|
||||
} else {
|
||||
f.powf(1. / 3.)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Solve a cubic of the form `x^3 + px + q`, derivation from: <https://trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm>.
|
||||
pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec<f64> {
|
||||
let mut roots = Vec::new();
|
||||
if p.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
// Handle when p is approximately 0
|
||||
roots.push(cube_root(-q));
|
||||
} else if q.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
// Handle when q is approximately 0
|
||||
if p < 0. {
|
||||
roots.push((-p).powf(1. / 2.));
|
||||
}
|
||||
} else if discriminant.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
// When discriminant is 0 (check for approximation because of floating point errors), all roots are real, and 2 are repeated
|
||||
let q_divided_by_2 = q / 2.;
|
||||
let a_divided_by_3 = a / 3.;
|
||||
|
||||
roots.push(2. * cube_root(-q_divided_by_2) - a_divided_by_3);
|
||||
roots.push(cube_root(q_divided_by_2) - a_divided_by_3);
|
||||
} else if discriminant > 0. {
|
||||
// When discriminant > 0, there is one real and two imaginary roots
|
||||
let q_divided_by_2 = q / 2.;
|
||||
let square_root_discriminant = discriminant.powf(1. / 2.);
|
||||
|
||||
roots.push(cube_root(-q_divided_by_2 + square_root_discriminant) - cube_root(q_divided_by_2 + square_root_discriminant) - a / 3.);
|
||||
} else {
|
||||
// Otherwise, discriminant < 0 and there are three real roots
|
||||
let p_divided_by_3 = p / 3.;
|
||||
let a_divided_by_3 = a / 3.;
|
||||
let cube_root_r = (-p_divided_by_3).powf(1. / 2.);
|
||||
let phi = (-q / (2. * cube_root_r.powi(3))).acos();
|
||||
|
||||
let two_times_cube_root_r = 2. * cube_root_r;
|
||||
roots.push(two_times_cube_root_r * (phi / 3.).cos() - a_divided_by_3);
|
||||
roots.push(two_times_cube_root_r * ((phi + 2. * PI) / 3.).cos() - a_divided_by_3);
|
||||
roots.push(two_times_cube_root_r * ((phi + 4. * PI) / 3.).cos() - a_divided_by_3);
|
||||
}
|
||||
roots
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Solve a cubic of the form `ax^3 + bx^2 + ct + d`.
|
||||
pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
|
||||
if a.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
if b.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
// If both a and b are approximately 0, treat as a linear problem
|
||||
solve_linear(c, d)
|
||||
} else {
|
||||
// If a is approximately 0, treat as a quadratic problem
|
||||
let discriminant = c * c - 4. * b * d;
|
||||
solve_quadratic(discriminant, 2. * b, c, d)
|
||||
}
|
||||
} else {
|
||||
let new_a = b / a;
|
||||
let new_b = c / a;
|
||||
let new_c = d / a;
|
||||
|
||||
// Refactor cubic to be of the form: a(t^3 + pt + q), derivation from: https://trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm
|
||||
let p = (3. * new_b - new_a * new_a) / 3.;
|
||||
let q = (2. * new_a.powi(3) - 9. * new_a * new_b + 27. * new_c) / 27.;
|
||||
let discriminant = (p / 3.).powi(3) + (q / 2.).powi(2);
|
||||
solve_reformatted_cubic(discriminant, new_a, p, q)
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if two rectangles have any overlap. The rectangles are represented by a pair of coordinates that designate the top left and bottom right corners (in a graphical coordinate system).
|
||||
pub fn do_rectangles_overlap(rectangle1: [DVec2; 2], rectangle2: [DVec2; 2]) -> bool {
|
||||
let [bottom_left1, top_right1] = rectangle1;
|
||||
let [bottom_left2, top_right2] = rectangle2;
|
||||
|
||||
top_right1.x >= bottom_left2.x && top_right2.x >= bottom_left1.x && top_right2.y >= bottom_left1.y && top_right1.y >= bottom_left2.y
|
||||
}
|
||||
|
||||
/// Returns the intersection of two lines. The lines are given by a point on the line and its slope (represented by a vector).
|
||||
pub fn line_intersection(point1: DVec2, point1_slope_vector: DVec2, point2: DVec2, point2_slope_vector: DVec2) -> DVec2 {
|
||||
assert!(point1_slope_vector.normalize() != point2_slope_vector.normalize());
|
||||
|
||||
// Find the intersection when the first line is vertical
|
||||
if f64_compare(point1_slope_vector.x, 0., MAX_ABSOLUTE_DIFFERENCE) {
|
||||
let m2 = point2_slope_vector.y / point2_slope_vector.x;
|
||||
let b2 = point2.y - m2 * point2.x;
|
||||
DVec2::new(point1.x, point1.x * m2 + b2)
|
||||
}
|
||||
// Find the intersection when the second line is vertical
|
||||
else if f64_compare(point2_slope_vector.x, 0., MAX_ABSOLUTE_DIFFERENCE) {
|
||||
let m1 = point1_slope_vector.y / point1_slope_vector.x;
|
||||
let b1 = point1.y - m1 * point1.x;
|
||||
DVec2::new(point2.x, point2.x * m1 + b1)
|
||||
}
|
||||
// Find the intersection where neither line is vertical
|
||||
else {
|
||||
let m1 = point1_slope_vector.y / point1_slope_vector.x;
|
||||
let b1 = point1.y - m1 * point1.x;
|
||||
let m2 = point2_slope_vector.y / point2_slope_vector.x;
|
||||
let b2 = point2.y - m2 * point2.x;
|
||||
let intersection_x = (b2 - b1) / (m1 - m2);
|
||||
DVec2::new(intersection_x, intersection_x * m1 + b1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if 3 points are collinear.
|
||||
pub fn are_points_collinear(p1: DVec2, p2: DVec2, p3: DVec2) -> bool {
|
||||
let matrix = DMat2::from_cols(p1 - p2, p2 - p3);
|
||||
f64_compare(matrix.determinant() / 2., 0., MAX_ABSOLUTE_DIFFERENCE)
|
||||
}
|
||||
|
||||
/// Compute the center of the circle that passes through all three provided points. The provided points cannot be collinear.
|
||||
pub fn compute_circle_center_from_points(p1: DVec2, p2: DVec2, p3: DVec2) -> Option<DVec2> {
|
||||
if are_points_collinear(p1, p2, p3) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let midpoint_a = p1.lerp(p2, 0.5);
|
||||
let midpoint_b = p2.lerp(p3, 0.5);
|
||||
let midpoint_c = p3.lerp(p1, 0.5);
|
||||
|
||||
let tangent_a = (p1 - p2).perp();
|
||||
let tangent_b = (p2 - p3).perp();
|
||||
let tangent_c = (p3 - p1).perp();
|
||||
|
||||
let intersect_a_b = line_intersection(midpoint_a, tangent_a, midpoint_b, tangent_b);
|
||||
let intersect_b_c = line_intersection(midpoint_b, tangent_b, midpoint_c, tangent_c);
|
||||
let intersect_c_a = line_intersection(midpoint_c, tangent_c, midpoint_a, tangent_a);
|
||||
|
||||
Some((intersect_a_b + intersect_b_c + intersect_c_a) / 3.)
|
||||
}
|
||||
|
||||
/// Compare two `f64` numbers with a provided max absolute value difference.
|
||||
pub fn f64_compare(f1: f64, f2: f64, max_abs_diff: f64) -> bool {
|
||||
(f1 - f2).abs() < max_abs_diff
|
||||
}
|
||||
|
||||
/// Determine if an `f64` number is within a given range by using a max absolute value difference comparison.
|
||||
pub fn f64_approximately_in_range(value: f64, min: f64, max: f64, max_abs_diff: f64) -> bool {
|
||||
(min..=max).contains(&value) || f64_compare(value, min, max_abs_diff) || f64_compare(value, max, max_abs_diff)
|
||||
}
|
||||
|
||||
/// Compare the two values in a `DVec2` independently with a provided max absolute value difference.
|
||||
pub fn dvec2_compare(dv1: DVec2, dv2: DVec2, max_abs_diff: f64) -> BVec2 {
|
||||
BVec2::new((dv1.x - dv2.x).abs() < max_abs_diff, (dv1.y - dv2.y).abs() < max_abs_diff)
|
||||
}
|
||||
|
||||
/// Determine if the values in a `DVec2` are within a given range independently by using a max absolute value difference comparison.
|
||||
pub fn dvec2_approximately_in_range(point: DVec2, min: DVec2, max: DVec2, max_abs_diff: f64) -> BVec2 {
|
||||
(point.cmpge(min) & point.cmple(max)) | dvec2_compare(point, min, max_abs_diff) | dvec2_compare(point, max, max_abs_diff)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
|
||||
/// Compare vectors of `f64`s with a provided max absolute value difference.
|
||||
fn f64_compare_vector(vec1: Vec<f64>, vec2: Vec<f64>, max_abs_diff: f64) -> bool {
|
||||
vec1.len() == vec2.len() && vec1.into_iter().zip(vec2.into_iter()).all(|(a, b)| f64_compare(a, b, max_abs_diff))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_solve_linear() {
|
||||
// Line that is on the x-axis
|
||||
assert!(solve_linear(0., 0.).is_empty());
|
||||
// Line that is parallel to but not on the x-axis
|
||||
assert!(solve_linear(0., 1.).is_empty());
|
||||
// Line with a non-zero slope
|
||||
assert!(solve_linear(2., -8.) == vec![4.]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_solve_cubic() {
|
||||
// discriminant == 0
|
||||
let roots1 = solve_cubic(1., 0., 0., 0.);
|
||||
assert!(roots1 == vec![0.]);
|
||||
|
||||
let roots2 = solve_cubic(1., 3., 0., -4.);
|
||||
assert!(roots2 == vec![1., -2.]);
|
||||
|
||||
// p == 0
|
||||
let roots3 = solve_cubic(1., 0., 0., -1.);
|
||||
assert!(roots3 == vec![1.]);
|
||||
|
||||
// discriminant > 0
|
||||
let roots4 = solve_cubic(1., 3., 0., 2.);
|
||||
assert!(f64_compare_vector(roots4, vec![-3.196], MAX_ABSOLUTE_DIFFERENCE));
|
||||
|
||||
// discriminant < 0
|
||||
let roots5 = solve_cubic(1., 3., 0., -1.);
|
||||
assert!(f64_compare_vector(roots5, vec![0.532, -2.879, -0.653], MAX_ABSOLUTE_DIFFERENCE));
|
||||
|
||||
// quadratic
|
||||
let roots6 = solve_cubic(0., 3., 0., -3.);
|
||||
assert!(roots6 == vec![1., -1.]);
|
||||
|
||||
// linear
|
||||
let roots7 = solve_cubic(0., 0., 1., -1.);
|
||||
assert!(roots7 == vec![1.]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_do_rectangles_overlap() {
|
||||
// Rectangles overlap
|
||||
assert!(do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(20., 20.)], [DVec2::new(10., 10.), DVec2::new(30., 20.)]));
|
||||
// Rectangles share a side
|
||||
assert!(do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(10., 10.), DVec2::new(30., 30.)]));
|
||||
// Rectangle inside the other
|
||||
assert!(do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(2., 2.), DVec2::new(6., 4.)]));
|
||||
// No overlap, rectangles are beside each other
|
||||
assert!(!do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(20., 0.), DVec2::new(30., 10.)]));
|
||||
// No overlap, rectangles are above and below each other
|
||||
assert!(!do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(0., 20.), DVec2::new(20., 30.)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_intersection() {
|
||||
// y = 2x + 10
|
||||
// y = 5x + 4
|
||||
// intersect at (2, 14)
|
||||
|
||||
let start1 = DVec2::new(0., 10.);
|
||||
let end1 = DVec2::new(0., 4.);
|
||||
let start_direction1 = DVec2::new(1., 2.);
|
||||
let end_direction1 = DVec2::new(1., 5.);
|
||||
assert!(line_intersection(start1, start_direction1, end1, end_direction1) == DVec2::new(2., 14.));
|
||||
|
||||
// y = x
|
||||
// y = -x + 8
|
||||
// intersect at (4, 4)
|
||||
|
||||
let start2 = DVec2::new(0., 0.);
|
||||
let end2 = DVec2::new(8., 0.);
|
||||
let start_direction2 = DVec2::new(1., 1.);
|
||||
let end_direction2 = DVec2::new(1., -1.);
|
||||
assert!(line_intersection(start2, start_direction2, end2, end_direction2) == DVec2::new(4., 4.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_are_points_collinear() {
|
||||
assert!(are_points_collinear(DVec2::new(2., 4.), DVec2::new(6., 8.), DVec2::new(4., 6.)));
|
||||
assert!(!are_points_collinear(DVec2::new(1., 4.), DVec2::new(6., 8.), DVec2::new(4., 6.)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_circle_center_from_points() {
|
||||
// 3/4 of unit circle
|
||||
let center1 = compute_circle_center_from_points(DVec2::new(0., 1.), DVec2::new(-1., 0.), DVec2::new(1., 0.));
|
||||
assert_eq!(center1.unwrap(), DVec2::new(0., 0.));
|
||||
// 1/4 of unit circle
|
||||
let center2 = compute_circle_center_from_points(DVec2::new(-1., 0.), DVec2::new(0., 1.), DVec2::new(1., 0.));
|
||||
assert_eq!(center2.unwrap(), DVec2::new(0., 0.));
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue