From f2d35f50de69f0787908273c944c63afca193ff0 Mon Sep 17 00:00:00 2001 From: Thomas Cheng <35661641+Androxium@users.noreply.github.com> Date: Tue, 21 Feb 2023 17:08:55 -0500 Subject: [PATCH] Bezier-rs: Add self_intersection to subpath (#1035) * add self-intersection to subpath, rebased old work onto master * fix interactive website after rebase * fix rustdoc iframe placement * remove double comment * address comments * revert evaluate change * address comment + fix assert statement bug * update function comments --- libraries/bezier-rs/src/bezier/solvers.rs | 8 +- libraries/bezier-rs/src/subpath/lookup.rs | 2 +- libraries/bezier-rs/src/subpath/solvers.rs | 99 +++++++++++++------ .../src/features/subpath-features.ts | 62 ++++++++---- .../bezier-rs-demos/src/utils/options.ts | 8 ++ .../other/bezier-rs-demos/wasm/src/subpath.rs | 51 +++++++--- 6 files changed, 164 insertions(+), 66 deletions(-) diff --git a/libraries/bezier-rs/src/bezier/solvers.rs b/libraries/bezier-rs/src/bezier/solvers.rs index 2d32003a..4100ae3b 100644 --- a/libraries/bezier-rs/src/bezier/solvers.rs +++ b/libraries/bezier-rs/src/bezier/solvers.rs @@ -359,13 +359,17 @@ impl Bezier { let (self2, self2_t_values) = (self1.clone(), self1_t_values.clone()); let num_curves = self1.len(); + // Adjacent reduced curves cannot intersect + if num_curves <= 2 { + return vec![]; + } + // Create iterators that combine a subcurve with the `t` value pair that it was trimmed with let combined_iterator1 = self1.into_iter().zip(self1_t_values.windows(2).map(|t_pair| Range { start: t_pair[0], end: t_pair[1] })); // Second one needs to be a list because Iterator does not implement copy let combined_list2: Vec<(Bezier, Range)> = self2.into_iter().zip(self2_t_values.windows(2).map(|t_pair| Range { start: t_pair[0], end: t_pair[1] })).collect(); - // Adjacent reduced curves cannot intersect - // So for each curve, look for intersections with every curve that is at least 2 indices away + // For each curve, look for intersections with every curve that is at least 2 indices away combined_iterator1 .take(num_curves - 2) .enumerate() diff --git a/libraries/bezier-rs/src/subpath/lookup.rs b/libraries/bezier-rs/src/subpath/lookup.rs index 889c803a..3c181742 100644 --- a/libraries/bezier-rs/src/subpath/lookup.rs +++ b/libraries/bezier-rs/src/subpath/lookup.rs @@ -37,7 +37,7 @@ impl Subpath { match t { SubpathTValue::Parametric { segment_index, t } => { assert!((0.0..=1.).contains(&t)); - assert!((0..self.len_segments() - 1).contains(&segment_index)); + assert!((0..self.len_segments()).contains(&segment_index)); (segment_index, t) } SubpathTValue::GlobalParametric(global_t) => { diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 4b3b290f..a7bbe0f9 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -1,5 +1,5 @@ use super::*; -use crate::consts::MIN_SEPERATION_VALUE; +use crate::consts::MAX_ABSOLUTE_DIFFERENCE; use crate::utils::SubpathTValue; use crate::TValue; @@ -14,34 +14,23 @@ impl Subpath { self.get_segment(segment_index).unwrap().evaluate(TValue::Parametric(t)) } - /// Calculates the intersection points the subpath has with a given curve and returns a list of parameteric `t`-values. + /// Calculates the intersection points the subpath has with a given curve and returns a list of `(usize, f64)` tuples, + /// where the `usize` represents the index of the curve in the subpath, and the `f64` represents the `t`-value local to + /// that curve where the intersection occured. /// This function expects the following: - /// - other: a [Bezier] curve to check intersections against - /// - error: an optional f64 value to provide an error bound + /// - `other`: a [Bezier] curve to check intersections against + /// - `error`: an optional f64 value to provide an error bound + /// - `minimum_seperation`: 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. /// - pub fn intersections(&self, other: &Bezier, error: Option, minimum_seperation: Option) -> Vec { + pub fn intersections(&self, other: &Bezier, error: Option, minimum_seperation: Option) -> Vec<(usize, f64)> { // TODO: account for either euclidean or parametric type - let number_of_curves = self.len_segments() as f64; - let intersection_t_values: Vec = self + let intersection_t_values: Vec<(usize, f64)> = self .iter() .enumerate() - .flat_map(|(index, bezier)| { - bezier - .intersections(other, error, minimum_seperation) - .into_iter() - .map(|t| ((index as f64) + t) / number_of_curves) - .collect::>() - }) + .flat_map(|(index, bezier)| bezier.intersections(other, error, minimum_seperation).into_iter().map(|t| (index, t)).collect::>()) .collect(); - intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| { - if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_seperation.unwrap_or(MIN_SEPERATION_VALUE) { - accumulator.pop(); - } - accumulator.push(*t); - accumulator - }); - intersection_t_values } @@ -52,6 +41,32 @@ impl Subpath { self.get_segment(segment_index).unwrap().tangent(TValue::Parametric(t)) } + /// Returns a list of `t` values that correspond to the self intersection points of the subpath. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point. + /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. + /// - `minimum_seperation`: 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 + /// + /// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out. + /// + pub fn self_intersections(&self, error: Option, minimum_seperation: Option) -> Vec<(usize, f64)> { + let mut intersections_vec = Vec::new(); + let err = error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE); + // TODO: optimization opportunity - this for-loop currently compares all intersections with all curve-segments in the subpath collection + self.iter().enumerate().for_each(|(i, other)| { + intersections_vec.extend(other.self_intersections(error).iter().map(|value| (i, value[0]))); + self.iter().enumerate().skip(i + 1).for_each(|(j, curve)| { + intersections_vec.extend( + curve + .intersections(&other, error, minimum_seperation) + .iter() + .filter(|&value| value > &err && (1. - value) > err) + .map(|value| (j, *value)), + ); + }); + }); + intersections_vec + } + /// Returns a normalized unit vector representing the direction of the normal on the subpath based on the parametric `t`-value provided. /// pub fn normal(&self, t: SubpathTValue) -> DVec2 { @@ -298,21 +313,30 @@ mod tests { assert!(utils::dvec2_compare( cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), - subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[0])), + subpath.evaluate(SubpathTValue::Parametric { + segment_index: subpath_intersections[0].0, + t: subpath_intersections[0].1 + }), MAX_ABSOLUTE_DIFFERENCE ) .all()); assert!(utils::dvec2_compare( quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), - subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[1])), + subpath.evaluate(SubpathTValue::Parametric { + segment_index: subpath_intersections[1].0, + t: subpath_intersections[1].1 + }), MAX_ABSOLUTE_DIFFERENCE ) .all()); assert!(utils::dvec2_compare( quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])), - subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[2])), + subpath.evaluate(SubpathTValue::Parametric { + segment_index: subpath_intersections[2].0, + t: subpath_intersections[2].1 + }), MAX_ABSOLUTE_DIFFERENCE ) .all()); @@ -365,14 +389,20 @@ mod tests { assert!(utils::dvec2_compare( cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), - subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[0])), + subpath.evaluate(SubpathTValue::Parametric { + segment_index: subpath_intersections[0].0, + t: subpath_intersections[0].1 + }), MAX_ABSOLUTE_DIFFERENCE ) .all()); assert!(utils::dvec2_compare( quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), - subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[1])), + subpath.evaluate(SubpathTValue::Parametric { + segment_index: subpath_intersections[1].0, + t: subpath_intersections[1].1 + }), MAX_ABSOLUTE_DIFFERENCE ) .all()); @@ -424,21 +454,30 @@ mod tests { assert!(utils::dvec2_compare( cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), - subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[0])), + subpath.evaluate(SubpathTValue::Parametric { + segment_index: subpath_intersections[0].0, + t: subpath_intersections[0].1 + }), MAX_ABSOLUTE_DIFFERENCE ) .all()); assert!(utils::dvec2_compare( quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), - subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[1])), + subpath.evaluate(SubpathTValue::Parametric { + segment_index: subpath_intersections[1].0, + t: subpath_intersections[1].1 + }), MAX_ABSOLUTE_DIFFERENCE ) .all()); assert!(utils::dvec2_compare( quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])), - subpath.evaluate(SubpathTValue::GlobalParametric(subpath_intersections[2])), + subpath.evaluate(SubpathTValue::Parametric { + segment_index: subpath_intersections[2].0, + t: subpath_intersections[2].1 + }), MAX_ABSOLUTE_DIFFERENCE ) .all()); diff --git a/website/other/bezier-rs-demos/src/features/subpath-features.ts b/website/other/bezier-rs-demos/src/features/subpath-features.ts index 8eac1e5c..9384843a 100644 --- a/website/other/bezier-rs-demos/src/features/subpath-features.ts +++ b/website/other/bezier-rs-demos/src/features/subpath-features.ts @@ -1,4 +1,4 @@ -import { tSliderOptions } from "@/utils/options"; +import { tSliderOptions, intersectionErrorOptions, minimumSeparationOptions } from "@/utils/options"; import { TVariant, SliderOption, SubpathCallback, WasmSubpathInstance } from "@/utils/types"; const subpathFeatures = { @@ -54,30 +54,50 @@ const subpathFeatures = { }, "intersect-linear": { name: "Intersect (Line Segment)", - callback: (subpath: WasmSubpathInstance): string => - subpath.intersect_line_segment([ - [150, 150], - [20, 20], - ]), + callback: (subpath: WasmSubpathInstance, options: Record): string => + subpath.intersect_line_segment( + [ + [150, 150], + [20, 20], + ], + options.error, + options.minimum_seperation + ), + sliderOptions: [intersectionErrorOptions, minimumSeparationOptions], }, "intersect-quadratic": { - name: "Intersect (Quadratic segment)", - callback: (subpath: WasmSubpathInstance): string => - subpath.intersect_quadratic_segment([ - [20, 80], - [180, 10], - [90, 120], - ]), + name: "Intersect (Quadratic Segment)", + callback: (subpath: WasmSubpathInstance, options: Record): string => + subpath.intersect_quadratic_segment( + [ + [20, 80], + [180, 10], + [90, 120], + ], + options.error, + options.minimum_seperation + ), + sliderOptions: [intersectionErrorOptions, minimumSeparationOptions], }, "intersect-cubic": { - name: "Intersect (Cubic segment)", - callback: (subpath: WasmSubpathInstance): string => - subpath.intersect_cubic_segment([ - [40, 20], - [100, 40], - [40, 120], - [175, 140], - ]), + name: "Intersect (Cubic Segment)", + callback: (subpath: WasmSubpathInstance, options: Record): string => + subpath.intersect_cubic_segment( + [ + [40, 20], + [100, 40], + [40, 120], + [175, 140], + ], + options.error, + options.minimum_seperation + ), + sliderOptions: [intersectionErrorOptions, minimumSeparationOptions], + }, + "self-intersect": { + name: "Self Intersect", + callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.self_intersections(options.error, options.minimum_seperation), + sliderOptions: [intersectionErrorOptions, minimumSeparationOptions], }, split: { name: "Split", diff --git a/website/other/bezier-rs-demos/src/utils/options.ts b/website/other/bezier-rs-demos/src/utils/options.ts index 14de87c3..9cf1ccfc 100644 --- a/website/other/bezier-rs-demos/src/utils/options.ts +++ b/website/other/bezier-rs-demos/src/utils/options.ts @@ -21,3 +21,11 @@ export const minimumSeparationOptions = { step: 0.001, default: 0.05, }; + +export const intersectionErrorOptions = { + variable: "error", + min: 0.001, + max: 0.525, + step: 0.0025, + default: 0.02, +}; diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index 344cd3e8..b6f96828 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -179,7 +179,7 @@ impl WasmSubpath { wrap_svg_tag(content) } - pub fn intersect_line_segment(&self, js_points: JsValue) -> String { + pub fn intersect_line_segment(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String { let points: [DVec2; 2] = serde_wasm_bindgen::from_value(js_points).unwrap(); let line = Bezier::from_linear_dvec2(points[0], points[1]); @@ -197,10 +197,13 @@ impl WasmSubpath { let intersections_svg = self .0 - .intersections(&line, None, None) + .intersections(&line, Some(error), Some(minimum_separation)) .iter() - .map(|intersection_t| { - let point = self.0.evaluate(SubpathTValue::GlobalParametric(*intersection_t)); + .map(|(segment_index, intersection_t)| { + let point = self.0.evaluate(SubpathTValue::Parametric { + segment_index: *segment_index, + t: *intersection_t, + }); draw_circle(point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); @@ -208,7 +211,7 @@ impl WasmSubpath { wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}")) } - pub fn intersect_quadratic_segment(&self, js_points: JsValue) -> String { + pub fn intersect_quadratic_segment(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String { let points: [DVec2; 3] = serde_wasm_bindgen::from_value(js_points).unwrap(); let line = Bezier::from_quadratic_dvec2(points[0], points[1], points[2]); @@ -226,10 +229,13 @@ impl WasmSubpath { let intersections_svg = self .0 - .intersections(&line, None, None) + .intersections(&line, Some(error), Some(minimum_separation)) .iter() - .map(|intersection_t| { - let point = self.0.evaluate(SubpathTValue::GlobalParametric(*intersection_t)); + .map(|(segment_index, intersection_t)| { + let point = self.0.evaluate(SubpathTValue::Parametric { + segment_index: *segment_index, + t: *intersection_t, + }); draw_circle(point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); @@ -237,7 +243,7 @@ impl WasmSubpath { wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}")) } - pub fn intersect_cubic_segment(&self, js_points: JsValue) -> String { + pub fn intersect_cubic_segment(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String { let points: [DVec2; 4] = serde_wasm_bindgen::from_value(js_points).unwrap(); let line = Bezier::from_cubic_dvec2(points[0], points[1], points[2], points[3]); @@ -255,10 +261,13 @@ impl WasmSubpath { let intersections_svg = self .0 - .intersections(&line, None, None) + .intersections(&line, Some(error), Some(minimum_separation)) .iter() - .map(|intersection_t| { - let point = self.0.evaluate(SubpathTValue::GlobalParametric(*intersection_t)); + .map(|(segment_index, intersection_t)| { + let point = self.0.evaluate(SubpathTValue::Parametric { + segment_index: *segment_index, + t: *intersection_t, + }); draw_circle(point, 4., RED, 1.5, WHITE) }) .fold(String::new(), |acc, item| format!("{acc}{item}")); @@ -266,6 +275,24 @@ impl WasmSubpath { wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}")) } + pub fn self_intersections(&self, error: f64, minimum_separation: f64) -> String { + let subpath_svg = self.to_default_svg(); + let self_intersections_svg = self + .0 + .self_intersections(Some(error), Some(minimum_separation)) + .iter() + .map(|(segment_index, intersection_t)| { + let point = self.0.evaluate(SubpathTValue::Parametric { + segment_index: *segment_index, + t: *intersection_t, + }); + draw_circle(point, 4., RED, 1.5, WHITE) + }) + .fold(String::new(), |acc, item| format!("{acc}{item}")); + + wrap_svg_tag(format!("{subpath_svg}{self_intersections_svg}")) + } + pub fn split(&self, t: f64, t_variant: String) -> String { let t = parse_t_variant(&t_variant, t); let (main_subpath, optional_subpath) = self.0.split(t);