diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 22244e59..4b3b290f 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -58,6 +58,49 @@ impl Subpath { let (segment_index, t) = self.t_value_to_parametric(t); self.get_segment(segment_index).unwrap().normal(TValue::Parametric(t)) } + + /// 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]`. + /// + pub fn local_extrema(&self) -> [Vec; 2] { + let number_of_curves = self.len_segments() as f64; + + // TODO: Consider the shared point between adjacent beziers. + self.iter().enumerate().fold([Vec::new(), Vec::new()], |mut acc, elem| { + let extremas = elem.1.local_extrema(); + // Convert t-values of bezier curve to t-values of subpath + acc[0].extend(extremas[0].iter().map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::>()); + acc[1].extend(extremas[1].iter().map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::>()); + acc + }) + } + + /// Return the min and max corners that represent the bounding box of the subpath. + /// + 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])]) + } + + /// Returns list of `t`-values representing the inflection points of the subpath. + /// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`. + /// + pub fn inflections(&self) -> Vec { + let number_of_curves = self.len_segments() as f64; + let inflection_t_values: Vec = self + .iter() + .enumerate() + .flat_map(|(index, bezier)| { + bezier + .inflections() + .into_iter() + // Convert t-values of bezier curve to t-values of subpath + .map(move |t| ((index as f64) + t) / number_of_curves) + }) + .collect(); + + // TODO: Consider the shared point between adjacent beziers. + inflection_t_values + } } #[cfg(test)] 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 92eb6bc3..8eac1e5c 100644 --- a/website/other/bezier-rs-demos/src/features/subpath-features.ts +++ b/website/other/bezier-rs-demos/src/features/subpath-features.ts @@ -40,6 +40,18 @@ const subpathFeatures = { sliderOptions: [tSliderOptions], chooseTVariant: true, }, + "local-extrema": { + name: "Local Extrema", + callback: (subpath: WasmSubpathInstance): string => subpath.local_extrema(), + }, + "bounding-box": { + name: "Bounding Box", + callback: (subpath: WasmSubpathInstance): string => subpath.bounding_box(), + }, + inflections: { + name: "Inflections", + callback: (subpath: WasmSubpathInstance): string => subpath.inflections(), + }, "intersect-linear": { name: "Intersect (Line Segment)", callback: (subpath: WasmSubpathInstance): string => diff --git a/website/other/bezier-rs-demos/src/style.css b/website/other/bezier-rs-demos/src/style.css index 3296ba5d..56828f63 100644 --- a/website/other/bezier-rs-demos/src/style.css +++ b/website/other/bezier-rs-demos/src/style.css @@ -73,3 +73,8 @@ body > h2 { margin-bottom: 20px; border: solid 1px black; } + +svg text { + pointer-events: none; + user-select: none; +} diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index 244944aa..344cd3e8 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -114,6 +114,62 @@ impl WasmSubpath { wrap_svg_tag(format!("{}{}{}{}", self.to_default_svg(), point_text, line_text, normal_end_point)) } + pub fn local_extrema(&self) -> String { + let local_extrema: [Vec; 2] = self.0.local_extrema(); + + let bezier = self.to_default_svg(); + let circles: String = local_extrema + .iter() + .zip([RED, GREEN]) + .flat_map(|(t_value_list, color)| { + t_value_list.iter().map(|&t_value| { + let point = self.0.evaluate(SubpathTValue::GlobalParametric(t_value)); + draw_circle(point, 3., color, 1.5, WHITE) + }) + }) + .fold("".to_string(), |acc, circle| acc + &circle); + + let content = format!( + "{bezier}{circles}{}{}", + draw_text("X extrema".to_string(), TEXT_OFFSET_X, TEXT_OFFSET_Y - 20., RED), + draw_text("Y extrema".to_string(), TEXT_OFFSET_X, TEXT_OFFSET_Y, GREEN), + ); + wrap_svg_tag(content) + } + + pub fn bounding_box(&self) -> String { + let subpath_svg = self.to_default_svg(); + let bounding_box = self.0.bounding_box(); + match bounding_box { + None => wrap_svg_tag(subpath_svg), + Some(bounding_box) => { + let content = format!( + "{subpath_svg}", + bounding_box[0].x, + bounding_box[0].y, + bounding_box[1].x - bounding_box[0].x, + bounding_box[1].y - bounding_box[0].y, + ); + wrap_svg_tag(content) + } + } + } + + pub fn inflections(&self) -> String { + let inflections: Vec = self.0.inflections(); + + let bezier = self.to_default_svg(); + let circles: String = inflections + .iter() + .map(|&t_value| { + let point = self.0.evaluate(SubpathTValue::GlobalParametric(t_value)); + draw_circle(point, 3., RED, 1.5, WHITE) + }) + .fold("".to_string(), |acc, circle| acc + &circle); + let content = format!("{bezier}{circles}"); + wrap_svg_tag(content) + } + pub fn project(&self, x: f64, y: f64) -> String { let (segment_index, projected_t) = self.0.project(DVec2::new(x, y), ProjectionOptions::default()).unwrap(); let projected_point = self.0.evaluate(SubpathTValue::Parametric { segment_index, t: projected_t });