diff --git a/libraries/bezier-rs/src/bezier/solvers.rs b/libraries/bezier-rs/src/bezier/solvers.rs index 18be83ec..29e711b5 100644 --- a/libraries/bezier-rs/src/bezier/solvers.rs +++ b/libraries/bezier-rs/src/bezier/solvers.rs @@ -283,7 +283,7 @@ impl Bezier { let line_directional_vector = other.end - other.start; let angle = line_directional_vector.angle_between(DVec2::new(1., 0.)); 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)]; // Translate the bezier such that the line becomes aligned on top of the x-axis diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 166a5e1f..72730ffe 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -107,7 +107,7 @@ impl Subpath { /// 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]> { 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])]) } @@ -222,6 +222,14 @@ impl Subpath { [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. + /// + 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)] diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index 0087bce0..d47507d1 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -333,6 +333,29 @@ impl Subpath { Some((clipped_subpath1, clipped_subpath2.unwrap())) } + /// Returns a subpath that results from rotating this subpath around the origin by the given angle (in radians). + /// + pub fn rotate(&self, angle: f64) -> Subpath { + 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 { + // 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. /// The intersections of segments of the subpath are joined using the method specified by the `join` argument. /// diff --git a/website/other/bezier-rs-demos/src/features/bezier-features.ts b/website/other/bezier-rs-demos/src/features/bezier-features.ts index eac221e7..602414fd 100644 --- a/website/other/bezier-rs-demos/src/features/bezier-features.ts +++ b/website/other/bezier-rs-demos/src/features/bezier-features.ts @@ -467,7 +467,7 @@ const bezierFeatures = { }, rotate: { name: "Rotate", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.rotate(options.angle * Math.PI, 100, 100), + callback: (bezier: WasmBezierInstance, options: Record): string => bezier.rotate(options.angle * Math.PI, 125, 100), demoOptions: { Quadratic: { inputOptions: [ diff --git a/website/other/bezier-rs-demos/src/features/subpath-features.ts b/website/other/bezier-rs-demos/src/features/subpath-features.ts index 75f3edb1..aab782ca 100644 --- a/website/other/bezier-rs-demos/src/features/subpath-features.ts +++ b/website/other/bezier-rs-demos/src/features/subpath-features.ts @@ -95,6 +95,11 @@ const subpathFeatures = { callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.self_intersections(options.error, options.minimum_separation), inputOptions: [intersectionErrorOptions, minimumSeparationOptions], }, + curvature: { + name: "Curvature", + callback: (subpath: WasmSubpathInstance, options: Record, _: 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, _: 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 => 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; diff --git a/website/other/bezier-rs-demos/src/utils/types.ts b/website/other/bezier-rs-demos/src/utils/types.ts index 4dc2eb6b..b1c5fb7f 100644 --- a/website/other/bezier-rs-demos/src/utils/types.ts +++ b/website/other/bezier-rs-demos/src/utils/types.ts @@ -9,7 +9,7 @@ export type WasmSubpathInstance = InstanceType; 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, mouseLocation?: [number, number]) => string; export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record, mouseLocation?: [number, number]) => string; diff --git a/website/other/bezier-rs-demos/wasm/src/bezier.rs b/website/other/bezier-rs-demos/wasm/src/bezier.rs index e57a5cc6..6cb49b86 100644 --- a/website/other/bezier-rs-demos/wasm/src/bezier.rs +++ b/website/other/bezier-rs-demos/wasm/src/bezier.rs @@ -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#""#, self.0.start().x, self.0.start().y ); - let rotated_dashed_line_start = format!( + let rotated_dashed_line = format!( r#""#, rotated_bezier.start().x, rotated_bezier.start().y ); - // Line between pivot and end point on curve - let original_dashed_line_end = format!( - r#""#, - self.0.end().x, - self.0.end().y - ); - let rotated_dashed_line_end = format!( - r#""#, - 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, minimum_separation: Option) -> Vec { diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index d8d713a3..237c9979 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -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#""#, + self.0.iter().nth(0).unwrap().start().x, + self.0.iter().nth(0).unwrap().start().y + ); + let rotated_dashed_line = format!( + r#""#, + 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);