Bezier-rs: Add curvature and rotate functions to subpath (#1081)
* Add rotate and curvature * Fix comments * Fix case where curve is linear * Address comments * Fixed breakages caused by UI updates * Scootch rotation point to center in frame * Code review * Visualize t value point when segment is linear --------- Co-authored-by: Rob Nadal <robnadal44@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
3f8adbafba
commit
d0ac86b713
|
|
@ -283,7 +283,7 @@ impl Bezier {
|
||||||
let line_directional_vector = other.end - other.start;
|
let line_directional_vector = other.end - other.start;
|
||||||
let angle = line_directional_vector.angle_between(DVec2::new(1., 0.));
|
let angle = line_directional_vector.angle_between(DVec2::new(1., 0.));
|
||||||
let rotation_matrix = DMat2::from_angle(angle);
|
let rotation_matrix = DMat2::from_angle(angle);
|
||||||
let rotated_bezier = self.apply_transformation(&|point| rotation_matrix.mul_vec2(point));
|
let rotated_bezier = self.apply_transformation(|point| rotation_matrix.mul_vec2(point));
|
||||||
let rotated_line = [rotation_matrix.mul_vec2(other.start), rotation_matrix.mul_vec2(other.end)];
|
let rotated_line = [rotation_matrix.mul_vec2(other.start), rotation_matrix.mul_vec2(other.end)];
|
||||||
|
|
||||||
// Translate the bezier such that the line becomes aligned on top of the x-axis
|
// Translate the bezier such that the line becomes aligned on top of the x-axis
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||||
/// Return the min and max corners that represent the bounding box of the subpath, after a given affine transform.
|
/// Return the min and max corners that represent the bounding box of the subpath, after a given affine transform.
|
||||||
pub fn bounding_box_with_transform(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
pub fn bounding_box_with_transform(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||||
self.iter()
|
self.iter()
|
||||||
.map(|bezier| bezier.apply_transformation(&|v| transform.transform_point2(v)).bounding_box())
|
.map(|bezier| bezier.apply_transformation(|v| transform.transform_point2(v)).bounding_box())
|
||||||
.reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])])
|
.reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,6 +222,14 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||||
|
|
||||||
[ManipulatorGroup::new_anchor(left + translation), ManipulatorGroup::new_anchor(right + translation)]
|
[ManipulatorGroup::new_anchor(left + translation), ManipulatorGroup::new_anchor(right + translation)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the curvature, a scalar value for the derivative at the point `t` along the subpath.
|
||||||
|
/// Curvature is 1 over the radius of a circle with an equivalent derivative.
|
||||||
|
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/curvature/solo" title="Curvature Demo"></iframe>
|
||||||
|
pub fn curvature(&self, t: SubpathTValue) -> f64 {
|
||||||
|
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||||
|
self.get_segment(segment_index).unwrap().curvature(TValue::Parametric(t))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -333,6 +333,29 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||||
Some((clipped_subpath1, clipped_subpath2.unwrap()))
|
Some((clipped_subpath1, clipped_subpath2.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a subpath that results from rotating this subpath around the origin by the given angle (in radians).
|
||||||
|
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#subpath/rotate/solo" title="Rotate Demo"></iframe>
|
||||||
|
pub fn rotate(&self, angle: f64) -> Subpath<ManipulatorGroupId> {
|
||||||
|
let mut rotated_subpath = self.clone();
|
||||||
|
|
||||||
|
let affine_transform: DAffine2 = DAffine2::from_angle(angle);
|
||||||
|
rotated_subpath.apply_transform(affine_transform);
|
||||||
|
|
||||||
|
rotated_subpath
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a subpath that results from rotating this subpath around the provided point by the given angle (in radians).
|
||||||
|
pub fn rotate_about_point(&self, angle: f64, pivot: DVec2) -> Subpath<ManipulatorGroupId> {
|
||||||
|
// Translate before and after the rotation to account for the pivot
|
||||||
|
let translate: DAffine2 = DAffine2::from_translation(pivot);
|
||||||
|
let rotate: DAffine2 = DAffine2::from_angle(angle);
|
||||||
|
let translate_inverse = translate.inverse();
|
||||||
|
|
||||||
|
let mut rotated_subpath = self.clone();
|
||||||
|
rotated_subpath.apply_transform(translate * rotate * translate_inverse);
|
||||||
|
rotated_subpath
|
||||||
|
}
|
||||||
|
|
||||||
/// Reduces the segments of the subpath into simple subcurves, then scales each subcurve a set `distance` away.
|
/// Reduces the segments of the subpath into simple subcurves, then scales each subcurve a set `distance` away.
|
||||||
/// The intersections of segments of the subpath are joined using the method specified by the `join` argument.
|
/// The intersections of segments of the subpath are joined using the method specified by the `join` argument.
|
||||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/offset/solo" title="Offset Demo"></iframe>
|
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/offset/solo" title="Offset Demo"></iframe>
|
||||||
|
|
|
||||||
|
|
@ -467,7 +467,7 @@ const bezierFeatures = {
|
||||||
},
|
},
|
||||||
rotate: {
|
rotate: {
|
||||||
name: "Rotate",
|
name: "Rotate",
|
||||||
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.rotate(options.angle * Math.PI, 100, 100),
|
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.rotate(options.angle * Math.PI, 125, 100),
|
||||||
demoOptions: {
|
demoOptions: {
|
||||||
Quadratic: {
|
Quadratic: {
|
||||||
inputOptions: [
|
inputOptions: [
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,11 @@ const subpathFeatures = {
|
||||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.self_intersections(options.error, options.minimum_separation),
|
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.self_intersections(options.error, options.minimum_separation),
|
||||||
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
|
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
|
||||||
},
|
},
|
||||||
|
curvature: {
|
||||||
|
name: "Curvature",
|
||||||
|
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.curvature(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
|
||||||
|
inputOptions: [subpathTValueVariantOptions, { ...tSliderOptions, default: 0.2 }],
|
||||||
|
},
|
||||||
split: {
|
split: {
|
||||||
name: "Split",
|
name: "Split",
|
||||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.split(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
|
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.split(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
|
||||||
|
|
@ -134,6 +139,20 @@ const subpathFeatures = {
|
||||||
{ ...capOptions, isDisabledForClosed: true },
|
{ ...capOptions, isDisabledForClosed: true },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
rotate: {
|
||||||
|
name: "Rotate",
|
||||||
|
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.rotate(options.angle * Math.PI, 125, 100),
|
||||||
|
inputOptions: [
|
||||||
|
{
|
||||||
|
variable: "angle",
|
||||||
|
min: 0,
|
||||||
|
max: 2,
|
||||||
|
step: 1 / 50,
|
||||||
|
default: 0.12,
|
||||||
|
unit: "π",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SubpathFeatureKey = keyof typeof subpathFeatures;
|
export type SubpathFeatureKey = keyof typeof subpathFeatures;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export type WasmSubpathInstance = InstanceType<WasmRawInstance["WasmSubpath"]>;
|
||||||
export type WasmSubpathManipulatorKey = "set_anchor" | "set_in_handle" | "set_out_handle";
|
export type WasmSubpathManipulatorKey = "set_anchor" | "set_in_handle" | "set_out_handle";
|
||||||
|
|
||||||
export const BEZIER_CURVE_TYPE = ["Linear", "Quadratic", "Cubic"] as const;
|
export const BEZIER_CURVE_TYPE = ["Linear", "Quadratic", "Cubic"] as const;
|
||||||
export type BezierCurveType = (typeof BEZIER_CURVE_TYPE)[number];
|
export type BezierCurveType = typeof BEZIER_CURVE_TYPE[number];
|
||||||
|
|
||||||
export type BezierCallback = (bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: [number, number]) => string;
|
export type BezierCallback = (bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: [number, number]) => string;
|
||||||
export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record<string, number>, mouseLocation?: [number, number]) => string;
|
export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record<string, number>, mouseLocation?: [number, number]) => string;
|
||||||
|
|
|
||||||
|
|
@ -221,25 +221,27 @@ impl WasmBezier {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn curvature(&self, raw_t: f64, t_variant: String) -> String {
|
pub fn curvature(&self, raw_t: f64, t_variant: String) -> String {
|
||||||
let mut content = self.get_bezier_path();
|
let bezier = self.get_bezier_path();
|
||||||
let t = parse_t_variant(&t_variant, raw_t);
|
let t = parse_t_variant(&t_variant, raw_t);
|
||||||
|
|
||||||
|
let intersection_point = self.0.evaluate(t);
|
||||||
|
let normal_point = self.0.normal(t);
|
||||||
let curvature = self.0.curvature(t);
|
let curvature = self.0.curvature(t);
|
||||||
if curvature > 0. {
|
let content = if curvature < 0.000001 {
|
||||||
let radius = 1. / self.0.curvature(t);
|
// Linear curve segment: the radius is infinite so we don't draw it
|
||||||
let normal_point = self.0.normal(t);
|
format!("{bezier}{}", draw_circle(intersection_point, 3., RED, 1., WHITE))
|
||||||
let intersection_point = self.0.evaluate(t);
|
} else {
|
||||||
|
let radius = 1. / curvature;
|
||||||
let curvature_center = intersection_point + normal_point * radius;
|
let curvature_center = intersection_point + normal_point * radius;
|
||||||
|
|
||||||
content = format!(
|
format!(
|
||||||
"{content}{}{}{}{}",
|
"{bezier}{}{}{}{}",
|
||||||
draw_circle(curvature_center, radius.abs(), RED, 1., NONE),
|
draw_circle(curvature_center, radius.abs(), RED, 1., NONE),
|
||||||
draw_line(intersection_point.x, intersection_point.y, curvature_center.x, curvature_center.y, RED, 1.),
|
draw_line(intersection_point.x, intersection_point.y, curvature_center.x, curvature_center.y, RED, 1.),
|
||||||
draw_circle(intersection_point, 3., RED, 1., WHITE),
|
draw_circle(intersection_point, 3., RED, 1., WHITE),
|
||||||
draw_circle(curvature_center, 3., RED, 1., WHITE),
|
draw_circle(curvature_center, 3., RED, 1., WHITE),
|
||||||
);
|
)
|
||||||
}
|
};
|
||||||
wrap_svg_tag(content)
|
wrap_svg_tag(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -386,32 +388,18 @@ impl WasmBezier {
|
||||||
let pivot = draw_circle(DVec2::new(pivot_x, pivot_y), 3., GRAY, 1.5, WHITE);
|
let pivot = draw_circle(DVec2::new(pivot_x, pivot_y), 3., GRAY, 1.5, WHITE);
|
||||||
|
|
||||||
// Line between pivot and start point on curve
|
// Line between pivot and start point on curve
|
||||||
let original_dashed_line_start = format!(
|
let original_dashed_line = format!(
|
||||||
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
|
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
|
||||||
self.0.start().x,
|
self.0.start().x,
|
||||||
self.0.start().y
|
self.0.start().y
|
||||||
);
|
);
|
||||||
let rotated_dashed_line_start = format!(
|
let rotated_dashed_line = format!(
|
||||||
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
|
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
|
||||||
rotated_bezier.start().x,
|
rotated_bezier.start().x,
|
||||||
rotated_bezier.start().y
|
rotated_bezier.start().y
|
||||||
);
|
);
|
||||||
|
|
||||||
// Line between pivot and end point on curve
|
wrap_svg_tag(format!("{original_bezier_svg}{rotated_bezier_svg}{pivot}{original_dashed_line}{rotated_dashed_line}"))
|
||||||
let original_dashed_line_end = format!(
|
|
||||||
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{PINK}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
|
|
||||||
self.0.end().x,
|
|
||||||
self.0.end().y
|
|
||||||
);
|
|
||||||
let rotated_dashed_line_end = format!(
|
|
||||||
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{PINK}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
|
|
||||||
rotated_bezier.end().x,
|
|
||||||
rotated_bezier.end().y
|
|
||||||
);
|
|
||||||
|
|
||||||
wrap_svg_tag(format!(
|
|
||||||
"{original_bezier_svg}{rotated_bezier_svg}{pivot}{original_dashed_line_start}{rotated_dashed_line_start}{original_dashed_line_end}{rotated_dashed_line_end}"
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn intersect(&self, curve: &Bezier, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<f64> {
|
fn intersect(&self, curve: &Bezier, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<f64> {
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,28 @@ impl WasmSubpath {
|
||||||
wrap_svg_tag(content)
|
wrap_svg_tag(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn rotate(&self, angle: f64, pivot_x: f64, pivot_y: f64) -> String {
|
||||||
|
let subpath_svg = self.to_default_svg();
|
||||||
|
let rotated_subpath = self.0.rotate_about_point(angle, DVec2::new(pivot_x, pivot_y));
|
||||||
|
let mut rotated_subpath_svg = String::new();
|
||||||
|
rotated_subpath.to_svg(&mut rotated_subpath_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||||
|
let pivot = draw_circle(DVec2::new(pivot_x, pivot_y), 3., GRAY, 1.5, WHITE);
|
||||||
|
|
||||||
|
// Line between pivot and start point on curve
|
||||||
|
let original_dashed_line = format!(
|
||||||
|
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
|
||||||
|
self.0.iter().nth(0).unwrap().start().x,
|
||||||
|
self.0.iter().nth(0).unwrap().start().y
|
||||||
|
);
|
||||||
|
let rotated_dashed_line = format!(
|
||||||
|
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
|
||||||
|
rotated_subpath.iter().nth(0).unwrap().start().x,
|
||||||
|
rotated_subpath.iter().nth(0).unwrap().start().y
|
||||||
|
);
|
||||||
|
|
||||||
|
wrap_svg_tag(format!("{subpath_svg}{rotated_subpath_svg}{pivot}{original_dashed_line}{rotated_dashed_line}"))
|
||||||
|
}
|
||||||
|
|
||||||
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 });
|
||||||
|
|
@ -304,6 +326,31 @@ impl WasmSubpath {
|
||||||
wrap_svg_tag(format!("{subpath_svg}{self_intersections_svg}"))
|
wrap_svg_tag(format!("{subpath_svg}{self_intersections_svg}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn curvature(&self, t: f64, t_variant: String) -> String {
|
||||||
|
let subpath = self.to_default_svg();
|
||||||
|
let t = parse_t_variant(&t_variant, t);
|
||||||
|
|
||||||
|
let intersection_point = self.0.evaluate(t);
|
||||||
|
let normal_point = self.0.normal(t);
|
||||||
|
let curvature = self.0.curvature(t);
|
||||||
|
let content = if curvature < 0.000001 {
|
||||||
|
// Linear curve segment: the radius is infinite so we don't draw it
|
||||||
|
format!("{subpath}{}", draw_circle(intersection_point, 3., RED, 1., WHITE))
|
||||||
|
} else {
|
||||||
|
let radius = 1. / curvature;
|
||||||
|
let curvature_center = intersection_point + normal_point * radius;
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{subpath}{}{}{}{}",
|
||||||
|
draw_circle(curvature_center, radius.abs(), RED, 1., NONE),
|
||||||
|
draw_line(intersection_point.x, intersection_point.y, curvature_center.x, curvature_center.y, RED, 1.),
|
||||||
|
draw_circle(intersection_point, 3., RED, 1., WHITE),
|
||||||
|
draw_circle(curvature_center, 3., RED, 1., WHITE),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
wrap_svg_tag(content)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn split(&self, t: f64, t_variant: String) -> String {
|
pub fn split(&self, t: f64, t_variant: String) -> String {
|
||||||
let t = parse_t_variant(&t_variant, t);
|
let t = parse_t_variant(&t_variant, t);
|
||||||
let (main_subpath, optional_subpath) = self.0.split(t);
|
let (main_subpath, optional_subpath) = self.0.split(t);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue