From 8bc290fde9173ea457128dc42d5d1b8433c3fbb8 Mon Sep 17 00:00:00 2001 From: Rob Nadal Date: Tue, 28 Feb 2023 18:59:06 -0500 Subject: [PATCH] Bezier-rs: Add function to smoothly join bezier curves (#1037) * Added bezier join * Stylistic changes per review --- libraries/bezier-rs/src/bezier/solvers.rs | 23 +++++++-- .../src/features/bezier-features.ts | 50 +++++++++++++++++++ .../other/bezier-rs-demos/wasm/src/bezier.rs | 40 +++++++++++++++ 3 files changed, 108 insertions(+), 5 deletions(-) diff --git a/libraries/bezier-rs/src/bezier/solvers.rs b/libraries/bezier-rs/src/bezier/solvers.rs index c64ad02d..09898be9 100644 --- a/libraries/bezier-rs/src/bezier/solvers.rs +++ b/libraries/bezier-rs/src/bezier/solvers.rs @@ -52,15 +52,19 @@ impl Bezier { } } - /// Returns a normalized unit vector representing the tangent at the point `t` along the curve. - /// - pub fn tangent(&self, t: TValue) -> DVec2 { - let t = self.t_value_to_parametric(t); + /// Returns the non-normalized vector representing the tangent at the point `t` along the curve. + fn non_normalized_tangent(&self, t: f64) -> DVec2 { match self.handles { BezierHandles::Linear => self.end - self.start, _ => self.derivative().unwrap().evaluate(TValue::Parametric(t)), } - .normalize() + } + + /// Returns a normalized unit vector representing the tangent at the point `t` along the curve. + /// + pub fn tangent(&self, t: TValue) -> DVec2 { + let t = self.t_value_to_parametric(t); + self.non_normalized_tangent(t).normalize() } /// Returns a normalized unit vector representing the direction of the normal at the point `t` along the curve. @@ -390,6 +394,15 @@ impl Bezier { .flat_map(|bezier| self.intersections(bezier, None, None)) .collect() } + + /// Returns a cubic bezier which joins this with the provided bezier curve. + /// The resulting path formed by the Bezier curves is continuous up to the first derivative. + /// + pub fn join(&self, other: &Bezier) -> Bezier { + let handle1 = self.non_normalized_tangent(1.) / 3. + self.end; + let handle2 = other.start - other.non_normalized_tangent(0.) / 3.; + Bezier::from_cubic_dvec2(self.end, handle1, handle2, other.start) + } } #[cfg(test)] 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 d25d2ff0..9c4b333e 100644 --- a/website/other/bezier-rs-demos/src/features/bezier-features.ts +++ b/website/other/bezier-rs-demos/src/features/bezier-features.ts @@ -483,6 +483,56 @@ const bezierFeatures = { }, chooseTVariant: true, }, + join: { + name: "Join", + callback: (bezier: WasmBezierInstance): string => { + const points = JSON.parse(bezier.get_points()); + let examplePoints = []; + if (points.length === 2) { + examplePoints = [ + [120, 155], + [40, 155], + ]; + } else if (points.length === 3) { + examplePoints = [ + [40, 150], + [95, 195], + [155, 145], + ]; + } else { + examplePoints = [ + [140, 150], + [85, 110], + [65, 180], + [30, 140], + ]; + } + return bezier.join(examplePoints); + }, + demoOptions: { + Linear: { + customPoints: [ + [45, 40], + [130, 90], + ], + }, + Quadratic: { + customPoints: [ + [153, 40], + [40, 20], + [75, 85], + ], + }, + Cubic: { + customPoints: [ + [20, 80], + [40, 20], + [90, 100], + [130, 55], + ], + }, + }, + }, }; export type BezierFeatureKey = keyof typeof bezierFeatures; diff --git a/website/other/bezier-rs-demos/wasm/src/bezier.rs b/website/other/bezier-rs-demos/wasm/src/bezier.rs index 6f63ca1c..e11999a9 100644 --- a/website/other/bezier-rs-demos/wasm/src/bezier.rs +++ b/website/other/bezier-rs-demos/wasm/src/bezier.rs @@ -621,4 +621,44 @@ impl WasmBezier { .fold(original_curve_svg, |acc, item| format!("{acc}{item}")); wrap_svg_tag(arcs_svg) } + + pub fn join(&self, js_points: &JsValue) -> String { + let other_bezier: Bezier = match self.0.get_points().count() { + 2 => { + let points: [DVec2; 2] = serde_wasm_bindgen::from_value(js_points.into()).unwrap(); + Bezier::from_linear_dvec2(points[0], points[1]) + } + 3 => { + let points: [DVec2; 3] = serde_wasm_bindgen::from_value(js_points.into()).unwrap(); + Bezier::from_quadratic_dvec2(points[0], points[1], points[2]) + } + 4 => { + let points: [DVec2; 4] = serde_wasm_bindgen::from_value(js_points.into()).unwrap(); + Bezier::from_cubic_dvec2(points[0], points[1], points[2], points[3]) + } + _ => unreachable!(), + }; + + let mut other_bezier_svg = String::new(); + other_bezier.to_svg( + &mut other_bezier_svg, + CURVE_ATTRIBUTES.to_string().replace(BLACK, GRAY), + ANCHOR_ATTRIBUTES.to_string().replace(BLACK, GRAY), + String::new(), + String::new(), + ); + + let joining_bezier: Bezier = self.0.join(&other_bezier); + let mut joining_bezier_svg = String::new(); + joining_bezier.to_svg( + &mut joining_bezier_svg, + CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), + ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED), + String::new(), + String::new(), + ); + + let bezier_svg = self.get_bezier_path(); + wrap_svg_tag(format!("{bezier_svg}{joining_bezier_svg}{other_bezier_svg}")) + } }