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 <keavon@keavon.com>
This commit is contained in:
parent
0d703e857b
commit
72cd204c64
|
|
@ -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<usize>) -> 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() {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,13 @@
|
|||
|
||||
<h2>Subpaths</h2>
|
||||
<div v-for="(feature, index) in subpathFeatures" :key="index">
|
||||
<SubpathExamplePane :name="feature.name" :callback="feature.callback" :sliderOptions="feature.sliderOptions" :chooseComputeType="feature.chooseComputeType" />
|
||||
<SubpathExamplePane
|
||||
:name="feature.name"
|
||||
:callback="feature.callback"
|
||||
:sliderOptions="feature.sliderOptions"
|
||||
:triggerOnMouseMove="feature.triggerOnMouseMove"
|
||||
:chooseComputeType="feature.chooseComputeType"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -605,6 +611,12 @@ export default defineComponent({
|
|||
sliderOptions: [{ ...tSliderOptions, variable: "computeArgument" }],
|
||||
chooseComputeType: true,
|
||||
},
|
||||
{
|
||||
name: "Project",
|
||||
callback: (subpath: WasmSubpathInstance, _: Record<string, number>, mouseLocation?: [number, number]): string =>
|
||||
mouseLocation ? subpath.project(mouseLocation[0], mouseLocation[1]) : subpath.to_svg(),
|
||||
triggerOnMouseMove: true,
|
||||
},
|
||||
{
|
||||
name: "Intersect (Line Segment)",
|
||||
callback: (subpath: WasmSubpathInstance): string =>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export default defineComponent({
|
|||
closed: { type: Boolean as PropType<boolean>, default: false },
|
||||
callback: { type: Function as PropType<SubpathCallback>, required: true },
|
||||
sliderOptions: { type: Object as PropType<Array<SliderOption>>, default: () => ({}) },
|
||||
triggerOnMouseMove: { type: Boolean as PropType<boolean>, default: false },
|
||||
computeType: { type: String as PropType<ComputeType>, 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),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,15 @@
|
|||
</div>
|
||||
<div class="example-row">
|
||||
<div v-for="(example, index) in examples" :key="index">
|
||||
<SubpathExample :title="example.title" :triples="example.triples" :closed="example.closed" :callback="callback" :sliderOptions="sliderOptions" :computeType="computeTypeChoice" />
|
||||
<SubpathExample
|
||||
:title="example.title"
|
||||
:triples="example.triples"
|
||||
:closed="example.closed"
|
||||
:callback="callback"
|
||||
:sliderOptions="sliderOptions"
|
||||
:triggerOnMouseMove="triggerOnMouseMove"
|
||||
:computeType="computeTypeChoice"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -32,6 +40,7 @@ export default defineComponent({
|
|||
name: { type: String as PropType<string>, required: true },
|
||||
callback: { type: Function as PropType<SubpathCallback>, required: true },
|
||||
sliderOptions: { type: Array as PropType<Array<SliderOption>>, default: () => [] },
|
||||
triggerOnMouseMove: { type: Boolean as PropType<boolean>, default: false },
|
||||
chooseComputeType: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
Loading…
Reference in New Issue