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:
Linda Zheng
2023-03-27 19:35:57 -04:00
committed by Keavon Chambers
co-authored by Rob Nadal Keavon Chambers
parent 3f8adbafba
commit d0ac86b713
8 changed files with 116 additions and 31 deletions
@@ -467,7 +467,7 @@ const bezierFeatures = {
},
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: {
Quadratic: {
inputOptions: [
@@ -95,6 +95,11 @@ const subpathFeatures = {
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.self_intersections(options.error, options.minimum_separation),
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: {
name: "Split",
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 },
],
},
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;
@@ -9,7 +9,7 @@ export type WasmSubpathInstance = InstanceType<WasmRawInstance["WasmSubpath"]>;
export type WasmSubpathManipulatorKey = "set_anchor" | "set_in_handle" | "set_out_handle";
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 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 {
let mut content = self.get_bezier_path();
let bezier = self.get_bezier_path();
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);
if curvature > 0. {
let radius = 1. / self.0.curvature(t);
let normal_point = self.0.normal(t);
let intersection_point = self.0.evaluate(t);
let content = if curvature < 0.000001 {
// Linear curve segment: the radius is infinite so we don't draw it
format!("{bezier}{}", draw_circle(intersection_point, 3., RED, 1., WHITE))
} else {
let radius = 1. / curvature;
let curvature_center = intersection_point + normal_point * radius;
content = format!(
"{content}{}{}{}{}",
format!(
"{bezier}{}{}{}{}",
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)
}
@@ -386,32 +388,18 @@ impl WasmBezier {
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_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"/>"#,
self.0.start().x,
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"/>"#,
rotated_bezier.start().x,
rotated_bezier.start().y
);
// Line between pivot and end point on curve
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}"
))
wrap_svg_tag(format!("{original_bezier_svg}{rotated_bezier_svg}{pivot}{original_dashed_line}{rotated_dashed_line}"))
}
fn intersect(&self, curve: &Bezier, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<f64> {
@@ -181,6 +181,28 @@ impl WasmSubpath {
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 {
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 });
@@ -304,6 +326,31 @@ impl WasmSubpath {
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 {
let t = parse_t_variant(&t_variant, t);
let (main_subpath, optional_subpath) = self.0.split(t);