From 72cd204c648c3adabf47f0ee75f56a649f84c4a6 Mon Sep 17 00:00:00 2001 From: Rob Nadal Date: Thu, 29 Dec 2022 18:32:22 -0500 Subject: [PATCH] Bezier-rs: Add project function for Subpaths (#914) * Added subpath project function * Set appropriate project default in single manipulator group case * Lint * Return optional from subpath project + stylistic changes per review Co-authored-by: Keavon Chambers --- libraries/bezier-rs/src/subpath/lookup.rs | 25 +++++++++++++++++-- website/other/bezier-rs-demos/src/App.vue | 14 ++++++++++- .../src/components/SubpathExample.vue | 3 +++ .../src/components/SubpathExamplePane.vue | 11 +++++++- .../other/bezier-rs-demos/wasm/src/subpath.rs | 11 +++++++- 5 files changed, 59 insertions(+), 5 deletions(-) diff --git a/libraries/bezier-rs/src/subpath/lookup.rs b/libraries/bezier-rs/src/subpath/lookup.rs index 66f58b4c..f70d6dfa 100644 --- a/libraries/bezier-rs/src/subpath/lookup.rs +++ b/libraries/bezier-rs/src/subpath/lookup.rs @@ -1,4 +1,6 @@ use super::*; +use crate::{ComputeType, ProjectionOptions}; +use glam::DVec2; /// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`. impl Subpath { @@ -7,13 +9,32 @@ impl Subpath { pub fn length(&self, num_subdivisions: Option) -> f64 { self.iter().fold(0., |accumulator, bezier| accumulator + bezier.length(num_subdivisions)) } + + /// 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. + pub fn project(&self, point: DVec2, options: ProjectionOptions) -> Option<(usize, f64)> { + if self.is_empty() { + return None; + } + + // TODO: Optimization opportunity: Filter out segments which are *definitely* not the closest to the given point + let (index, (_, project_t)) = self + .iter() + .map(|bezier| { + let project_t = bezier.project(point, options); + (bezier.evaluate(ComputeType::Parametric(project_t)).distance(point), project_t) + }) + .enumerate() + .min_by(|(_, (distance1, _)), (_, (distance2, _))| distance1.total_cmp(distance2)) + .unwrap_or((0, (0., 0.))); // If the Subpath contains only a single manipulator group, returns (0, 0.) + + Some((index, project_t)) + } } #[cfg(test)] mod tests { use super::*; - use crate::Bezier; - use glam::DVec2; #[test] fn length_quadratic() { diff --git a/website/other/bezier-rs-demos/src/App.vue b/website/other/bezier-rs-demos/src/App.vue index 00880b3a..76760398 100644 --- a/website/other/bezier-rs-demos/src/App.vue +++ b/website/other/bezier-rs-demos/src/App.vue @@ -19,7 +19,13 @@

Subpaths

- +
@@ -605,6 +611,12 @@ export default defineComponent({ sliderOptions: [{ ...tSliderOptions, variable: "computeArgument" }], chooseComputeType: true, }, + { + name: "Project", + callback: (subpath: WasmSubpathInstance, _: Record, mouseLocation?: [number, number]): string => + mouseLocation ? subpath.project(mouseLocation[0], mouseLocation[1]) : subpath.to_svg(), + triggerOnMouseMove: true, + }, { name: "Intersect (Line Segment)", callback: (subpath: WasmSubpathInstance): string => diff --git a/website/other/bezier-rs-demos/src/components/SubpathExample.vue b/website/other/bezier-rs-demos/src/components/SubpathExample.vue index ad82a49a..6372a08b 100644 --- a/website/other/bezier-rs-demos/src/components/SubpathExample.vue +++ b/website/other/bezier-rs-demos/src/components/SubpathExample.vue @@ -27,6 +27,7 @@ export default defineComponent({ closed: { type: Boolean as PropType, default: false }, callback: { type: Function as PropType, required: true }, sliderOptions: { type: Object as PropType>, default: () => ({}) }, + triggerOnMouseMove: { type: Boolean as PropType, default: false }, computeType: { type: String as PropType, default: "Parametric" }, }, data() { @@ -68,6 +69,8 @@ export default defineComponent({ this.subpath[POINT_INDEX_TO_MANIPULATOR[this.activeIndex[1]]](this.activeIndex[0], mx, my); this.mutableTriples[this.activeIndex[0]][this.activeIndex[1]] = [mx, my]; this.subpathSVG = this.callback(this.subpath, this.sliderData, [mx, my], this.computeType); + } else if (this.triggerOnMouseMove) { + this.subpathSVG = this.callback(this.subpath, this.sliderData, [mx, my], this.computeType); } }, getSliderValue: (sliderValue: number, sliderUnit?: string | string[]) => (Array.isArray(sliderUnit) ? sliderUnit[sliderValue] : sliderUnit), diff --git a/website/other/bezier-rs-demos/src/components/SubpathExamplePane.vue b/website/other/bezier-rs-demos/src/components/SubpathExamplePane.vue index 7c57535a..aef447f9 100644 --- a/website/other/bezier-rs-demos/src/components/SubpathExamplePane.vue +++ b/website/other/bezier-rs-demos/src/components/SubpathExamplePane.vue @@ -12,7 +12,15 @@
- +
@@ -32,6 +40,7 @@ export default defineComponent({ name: { type: String as PropType, required: true }, callback: { type: Function as PropType, required: true }, sliderOptions: { type: Array as PropType>, default: () => [] }, + triggerOnMouseMove: { type: Boolean as PropType, default: false }, chooseComputeType: { type: Boolean as PropType, default: false }, }, data() { diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index cec62e9c..7b104a9f 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -1,6 +1,6 @@ use crate::svg_drawing::*; -use bezier_rs::{Bezier, ComputeType, ManipulatorGroup, Subpath}; +use bezier_rs::{Bezier, ComputeType, ManipulatorGroup, ProjectionOptions, Subpath}; use glam::DVec2; use wasm_bindgen::prelude::*; @@ -68,6 +68,15 @@ impl WasmSubpath { wrap_svg_tag(format!("{}{}", self.to_default_svg(), point_text)) } + 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(ComputeType::Parametric((segment_index as f64 + projected_t) / (self.0.len_segments() as f64))); + + let subpath_svg = self.to_default_svg(); + let content = format!("{subpath_svg}{}", draw_line(projected_point.x, projected_point.y, x, y, RED, 1.),); + wrap_svg_tag(content) + } + pub fn intersect_line_segment(&self, js_points: JsValue) -> String { let points: [DVec2; 2] = serde_wasm_bindgen::from_value(js_points).unwrap(); let line = Bezier::from_linear_dvec2(points[0], points[1]);