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
This commit is contained in:
parent
4797aed05b
commit
f2d35f50de
|
|
@ -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<f64>)> = 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()
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
|
||||
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_seperation: Option<f64>) -> Vec<f64> {
|
||||
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_seperation: Option<f64>) -> 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<f64> = 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::<Vec<f64>>()
|
||||
})
|
||||
.flat_map(|(index, bezier)| bezier.intersections(other, error, minimum_seperation).into_iter().map(|t| (index, t)).collect::<Vec<(usize, f64)>>())
|
||||
.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.
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/self-intersect/solo" title="Self-Intersection Demo"></iframe>
|
||||
pub fn self_intersections(&self, error: Option<f64>, minimum_seperation: Option<f64>) -> 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.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/normal/solo" title="Normal Demo"></iframe>
|
||||
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());
|
||||
|
|
|
|||
|
|
@ -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, number>): 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, number>): 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, number>): 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, number>): string => subpath.self_intersections(options.error, options.minimum_seperation),
|
||||
sliderOptions: [intersectionErrorOptions, minimumSeparationOptions],
|
||||
},
|
||||
split: {
|
||||
name: "Split",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue