mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 07:38:12 +08:00
Bezier-rs: Implement miter-limit (#1096)
* Implement miter-limit approximation * Refactor to use Join enum and address other comments * Rustdocs improvements * Tweaks --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
3733804d18
commit
97be83c404
@@ -241,7 +241,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
}
|
||||
|
||||
/// Construct a cubic spline from a list of points.
|
||||
/// Based on https://mathworld.wolfram.com/CubicSpline.html
|
||||
/// Based on <https://mathworld.wolfram.com/CubicSpline.html>.
|
||||
pub fn new_cubic_spline(points: Vec<DVec2>) -> Self {
|
||||
// Number of points = number of points to find handles for
|
||||
let len_points = points.len();
|
||||
|
||||
@@ -8,7 +8,7 @@ use glam::DVec2;
|
||||
impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
/// Return a selection of equidistant points on the bezier curve.
|
||||
/// If no value is provided for `steps`, then the function will default `steps` to be 10.
|
||||
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#subpath/lookup-table/solo" title="Lookup-Table Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/bezier-rs-demos#subpath/lookup-table/solo" title="Lookup-Table Demo"></iframe>
|
||||
pub fn compute_lookup_table(&self, steps: Option<usize>, tvalue_type: Option<TValueType>) -> Vec<DVec2> {
|
||||
let steps = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE);
|
||||
let tvalue_type = tvalue_type.unwrap_or(TValueType::Parametric);
|
||||
@@ -26,7 +26,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
|
||||
/// 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`.
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/length/solo" title="Length Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/bezier-rs-demos#subpath/length/solo" title="Length Demo"></iframe>
|
||||
pub fn length(&self, num_subdivisions: Option<usize>) -> f64 {
|
||||
self.iter().fold(0., |accumulator, bezier| accumulator + bezier.length(num_subdivisions))
|
||||
}
|
||||
@@ -97,7 +97,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
|
||||
/// Returns the segment index and `t` value that corresponds to the closest point on the curve to the provided point.
|
||||
/// Uses a searching algorithm akin to binary search that can be customized using the [ProjectionOptions] structure.
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/project/solo" title="Project Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/bezier-rs-demos#subpath/project/solo" title="Project Demo"></iframe>
|
||||
pub fn project(&self, point: DVec2, options: Option<ProjectionOptions>) -> Option<(usize, f64)> {
|
||||
if self.is_empty() {
|
||||
return None;
|
||||
|
||||
@@ -14,17 +14,17 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
self.closed = new_closed;
|
||||
}
|
||||
|
||||
/// Access a [ManipulatorGroup] from a [ManipulatorGroupId].
|
||||
/// Access a [ManipulatorGroup] from a ManipulatorGroupId.
|
||||
pub fn manipulator_from_id(&self, id: ManipulatorGroupId) -> Option<&ManipulatorGroup<ManipulatorGroupId>> {
|
||||
self.manipulator_groups.iter().find(|manipulator_group| manipulator_group.id == id)
|
||||
}
|
||||
|
||||
/// Access a mutable [ManipulatorGroup] from a [ManipulatorGroupId].
|
||||
/// Access a mutable [ManipulatorGroup] from a ManipulatorGroupId.
|
||||
pub fn manipulator_mut_from_id(&mut self, id: ManipulatorGroupId) -> Option<&mut ManipulatorGroup<ManipulatorGroupId>> {
|
||||
self.manipulator_groups.iter_mut().find(|manipulator_group| manipulator_group.id == id)
|
||||
}
|
||||
|
||||
/// Access the index of a [ManipulatorGroup] from a [ManipulatorGroupId].
|
||||
/// Access the index of a [ManipulatorGroup] from a ManipulatorGroupId.
|
||||
pub fn manipulator_index_from_id(&self, id: ManipulatorGroupId) -> Option<usize> {
|
||||
self.manipulator_groups.iter().position(|manipulator_group| manipulator_group.id == id)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::f64::consts::PI;
|
||||
impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
/// Calculate the point on the subpath based on the parametric `t`-value provided.
|
||||
/// Expects `t` to be within the inclusive range `[0, 1]`.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/evaluate/solo" title="Evaluate Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/bezier-rs-demos#subpath/evaluate/solo" title="Evaluate Demo"></iframe>
|
||||
pub fn evaluate(&self, t: SubpathTValue) -> DVec2 {
|
||||
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||
self.get_segment(segment_index).unwrap().evaluate(TValue::Parametric(t))
|
||||
@@ -23,7 +23,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
/// - `error`: an optional f64 value to provide an error bound
|
||||
/// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
|
||||
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
|
||||
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
self.iter()
|
||||
.enumerate()
|
||||
@@ -35,7 +35,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
/// This function expects the following:
|
||||
/// - other: a [Bezier] curve to check intersections against
|
||||
/// - error: an optional f64 value to provide an error bound
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
|
||||
pub fn subpath_intersections(&self, other: &Subpath<ManipulatorGroupId>, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
let mut intersection_t_values: Vec<(usize, f64)> = other.iter().flat_map(|bezier| self.intersections(&bezier, error, minimum_separation)).collect();
|
||||
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
@@ -48,7 +48,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two
|
||||
///
|
||||
/// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out.
|
||||
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/bezier-rs-demos#subpath/self-intersect/solo" title="Self-Intersection Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#subpath/self-intersect/solo" title="Self-Intersection Demo"></iframe>
|
||||
pub fn self_intersections(&self, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
let mut intersections_vec = Vec::new();
|
||||
let err = error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE);
|
||||
@@ -69,14 +69,14 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
}
|
||||
|
||||
/// Returns a normalized unit vector representing the tangent on the subpath based on the parametric `t`-value provided.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/tangent/solo" title="Tangent Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/bezier-rs-demos#subpath/tangent/solo" title="Tangent Demo"></iframe>
|
||||
pub fn tangent(&self, t: SubpathTValue) -> DVec2 {
|
||||
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||
self.get_segment(segment_index).unwrap().tangent(TValue::Parametric(t))
|
||||
}
|
||||
|
||||
/// Returns a normalized unit vector representing the direction of the normal on the subpath based on the parametric `t`-value provided.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/normal/solo" title="Normal Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/bezier-rs-demos#subpath/normal/solo" title="Normal Demo"></iframe>
|
||||
pub fn normal(&self, t: SubpathTValue) -> DVec2 {
|
||||
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||
self.get_segment(segment_index).unwrap().normal(TValue::Parametric(t))
|
||||
@@ -84,7 +84,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
|
||||
/// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric subpaths respectively.
|
||||
/// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`.
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/local-extrema/solo" title="Local Extrema Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/bezier-rs-demos#subpath/local-extrema/solo" title="Local Extrema Demo"></iframe>
|
||||
pub fn local_extrema(&self) -> [Vec<f64>; 2] {
|
||||
let number_of_curves = self.len_segments() as f64;
|
||||
|
||||
@@ -99,7 +99,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
}
|
||||
|
||||
/// Return the min and max corners that represent the bounding box of the subpath.
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/bounding-box/solo" title="Bounding Box Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/bezier-rs-demos#subpath/bounding-box/solo" title="Bounding Box Demo"></iframe>
|
||||
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
self.iter().map(|bezier| bezier.bounding_box()).reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])])
|
||||
}
|
||||
@@ -138,7 +138,15 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
}
|
||||
|
||||
/// Returns the manipulator point that is needed for a miter join if it is possible.
|
||||
pub(crate) fn miter_line_join(&self, other: &Subpath<ManipulatorGroupId>) -> Option<ManipulatorGroup<ManipulatorGroupId>> {
|
||||
/// - `miter_limit`: Defines a limit for the ratio between the miter length and the stroke width.
|
||||
/// Alternatively, this can be interpreted as limiting the angle that the miter can form.
|
||||
/// When the limit is exceeded, no manipulator group will be returned.
|
||||
/// This value should be at least 1. If not, the default of 4 will be used.
|
||||
pub(crate) fn miter_line_join(&self, other: &Subpath<ManipulatorGroupId>, miter_limit: Option<f64>) -> Option<ManipulatorGroup<ManipulatorGroupId>> {
|
||||
let miter_limit = match miter_limit {
|
||||
Some(miter_limit) if miter_limit >= 1. => miter_limit,
|
||||
_ => 4.,
|
||||
};
|
||||
let in_segment = self.get_segment(self.len_segments() - 1).unwrap();
|
||||
let out_segment = other.get_segment(0).unwrap();
|
||||
let in_tangent = in_segment.tangent(TValue::Parametric(1.));
|
||||
@@ -151,9 +159,13 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
if !normalized_in_tangent.abs_diff_eq(normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) && !normalized_in_tangent.abs_diff_eq(-normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) {
|
||||
let intersection = line_intersection(in_segment.end(), in_tangent, out_segment.start(), out_tangent);
|
||||
|
||||
let start_to_intersection = intersection - in_segment.end();
|
||||
let intersection_to_end = out_segment.start() - intersection;
|
||||
|
||||
// Draw the miter join if the intersection occurs in the correct direction with respect to the path
|
||||
if (intersection - in_segment.end()).normalize().abs_diff_eq(in_tangent, MAX_ABSOLUTE_DIFFERENCE)
|
||||
&& (out_segment.start() - intersection).normalize().abs_diff_eq(out_tangent, MAX_ABSOLUTE_DIFFERENCE)
|
||||
if start_to_intersection.normalize().abs_diff_eq(in_tangent, MAX_ABSOLUTE_DIFFERENCE)
|
||||
&& intersection_to_end.normalize().abs_diff_eq(out_tangent, MAX_ABSOLUTE_DIFFERENCE)
|
||||
&& miter_limit >= 1. / (start_to_intersection.angle_between(-intersection_to_end).abs() / 2.).sin()
|
||||
{
|
||||
return Some(ManipulatorGroup {
|
||||
anchor: intersection,
|
||||
@@ -225,7 +237,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
|
||||
/// Returns the curvature, a scalar value for the derivative at the point `t` along the subpath.
|
||||
/// Curvature is 1 over the radius of a circle with an equivalent derivative.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/curvature/solo" title="Curvature Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/bezier-rs-demos#subpath/curvature/solo" title="Curvature Demo"></iframe>
|
||||
pub fn curvature(&self, t: SubpathTValue) -> f64 {
|
||||
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||
self.get_segment(segment_index).unwrap().curvature(TValue::Parametric(t))
|
||||
|
||||
@@ -22,7 +22,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
/// Returns either one or two Subpaths that result from splitting the original Subpath at the point corresponding to `t`.
|
||||
/// If the original Subpath was closed, a single open Subpath will be returned.
|
||||
/// If the original Subpath was open, two open Subpaths will be returned.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/split/solo" title="Split Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/bezier-rs-demos#subpath/split/solo" title="Split Demo"></iframe>
|
||||
pub fn split(&self, t: SubpathTValue) -> (Subpath<ManipulatorGroupId>, Option<Subpath<ManipulatorGroupId>>) {
|
||||
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||
let curve = self.get_segment(segment_index).unwrap();
|
||||
@@ -126,7 +126,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
/// The resulting Subpath will wind from the given `t1` to `t2`.
|
||||
/// That means, if the value of `t1` > `t2`, it will cross the break between endpoints from `t1` to `t = 1 = 0` to `t2`.
|
||||
/// If a path winding in the reverse direction is desired, call `trim` on the `Subpath` returned from `Subpath::reverse`.
|
||||
/// <iframe frameBorder="0" width="100%" height="450px" src="https://graphite.rs/bezier-rs-demos#subpath/trim/solo" title="Trim Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/trim/solo" title="Trim Demo"></iframe>
|
||||
pub fn trim(&self, t1: SubpathTValue, t2: SubpathTValue) -> Subpath<ManipulatorGroupId> {
|
||||
// Return a clone of the Subpath if it is not long enough to be a valid Bezier
|
||||
if self.manipulator_groups.is_empty() {
|
||||
@@ -334,7 +334,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
}
|
||||
|
||||
/// Returns a subpath that results from rotating this subpath around the origin by the given angle (in radians).
|
||||
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#subpath/rotate/solo" title="Rotate Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/rotate/solo" title="Rotate Demo"></iframe>
|
||||
pub fn rotate(&self, angle: f64) -> Subpath<ManipulatorGroupId> {
|
||||
let mut rotated_subpath = self.clone();
|
||||
|
||||
@@ -411,8 +411,8 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
drop_common_point[j] = false;
|
||||
match join {
|
||||
Join::Bevel => {}
|
||||
Join::Miter => {
|
||||
let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j]);
|
||||
Join::Miter(miter_limit) => {
|
||||
let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j], miter_limit);
|
||||
if let Some(miter_manipulator_group) = miter_manipulator_group {
|
||||
subpaths[i].manipulator_groups.push(miter_manipulator_group);
|
||||
}
|
||||
@@ -448,9 +448,9 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
drop_common_point[0] = false;
|
||||
match join {
|
||||
Join::Bevel => {}
|
||||
Join::Miter => {
|
||||
Join::Miter(miter_limit) => {
|
||||
let last_subpath_index = subpaths.len() - 1;
|
||||
let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0]);
|
||||
let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0], miter_limit);
|
||||
if let Some(miter_manipulator_group) = miter_manipulator_group {
|
||||
subpaths[last_subpath_index].manipulator_groups.push(miter_manipulator_group);
|
||||
}
|
||||
@@ -526,7 +526,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
/// an approximate outline around the subpath at a specified distance from the curve. Outline takes the following parameters:
|
||||
/// - `distance` - The outline's distance from the curve.
|
||||
/// - `join` - The join type used to cap the endpoints of open bezier curves, and join successive subpath segments.
|
||||
/// <iframe frameBorder="0" width="100%" height="450px" src="https://graphite.rs/bezier-rs-demos#subpath/outline/solo" title="Outline Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="425px" src="https://graphite.rs/bezier-rs-demos#subpath/outline/solo" title="Outline Demo"></iframe>
|
||||
pub fn outline(&self, distance: f64, join: Join, cap: Cap) -> (Subpath<ManipulatorGroupId>, Option<Subpath<ManipulatorGroupId>>) {
|
||||
let is_point = self.is_point();
|
||||
let (pos_offset, neg_offset) = if is_point {
|
||||
|
||||
Reference in New Issue
Block a user