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 });