mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-01 06:58:11 +08:00
Bezier-rs: Add bounding box, extrema, and inflection functions for subpath (#953)
* Create bbox function for subpath * Create extrema and inflection function for subpath * Address comments * Prevent selecting text in SVG demo boxes * Address Keavon's comments --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
4bb4e2c22e
commit
37775eb9e9
@@ -58,6 +58,49 @@ impl Subpath {
|
|||||||
let (segment_index, t) = self.t_value_to_parametric(t);
|
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||||
self.get_segment(segment_index).unwrap().normal(TValue::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]`.
|
||||||
|
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/local-extrema/solo" title="Local Extrema Demo"></iframe>
|
||||||
|
pub fn local_extrema(&self) -> [Vec<f64>; 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::<Vec<f64>>());
|
||||||
|
acc[1].extend(extremas[1].iter().map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::<Vec<f64>>());
|
||||||
|
acc
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the min and max corners that represent the bounding box of the subpath.
|
||||||
|
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/bounding-box/solo" title="Bounding Box Demo"></iframe>
|
||||||
|
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]`.
|
||||||
|
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/inflections/solo" title="Inflections Demo"></iframe>
|
||||||
|
pub fn inflections(&self) -> Vec<f64> {
|
||||||
|
let number_of_curves = self.len_segments() as f64;
|
||||||
|
let inflection_t_values: Vec<f64> = 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)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -40,6 +40,18 @@ const subpathFeatures = {
|
|||||||
sliderOptions: [tSliderOptions],
|
sliderOptions: [tSliderOptions],
|
||||||
chooseTVariant: true,
|
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": {
|
"intersect-linear": {
|
||||||
name: "Intersect (Line Segment)",
|
name: "Intersect (Line Segment)",
|
||||||
callback: (subpath: WasmSubpathInstance): string =>
|
callback: (subpath: WasmSubpathInstance): string =>
|
||||||
|
|||||||
@@ -73,3 +73,8 @@ body > h2 {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
border: solid 1px black;
|
border: solid 1px black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svg text {
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -114,6 +114,62 @@ impl WasmSubpath {
|
|||||||
wrap_svg_tag(format!("{}{}{}{}", self.to_default_svg(), point_text, line_text, normal_end_point))
|
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<f64>; 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}<rect x={} y ={} width=\"{}\" height=\"{}\" style=\"fill:{NONE};stroke:{RED};stroke-width:1\" />",
|
||||||
|
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<f64> = 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 {
|
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 (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 });
|
let projected_point = self.0.evaluate(SubpathTValue::Parametric { segment_index, t: projected_t });
|
||||||
|
|||||||
Reference in New Issue
Block a user