From 5fcbfcc7db5101fa44c6e8d4aada75b619a650ad Mon Sep 17 00:00:00 2001 From: Hannah Li Date: Sat, 4 Mar 2023 00:51:08 -0500 Subject: [PATCH 01/11] Intial work --- libraries/bezier-rs/src/subpath/solvers.rs | 24 ++++++++++++++++++- libraries/bezier-rs/src/subpath/transform.rs | 23 ++++++++++++++++++ .../src/features/subpath-features.ts | 20 ++++++++++++++-- .../other/bezier-rs-demos/wasm/src/subpath.rs | 21 ++++++++++++---- 4 files changed, 80 insertions(+), 8 deletions(-) diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 7b93fd0b1ba..142d5efb892 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -1,6 +1,6 @@ use super::*; use crate::consts::MAX_ABSOLUTE_DIFFERENCE; -use crate::utils::SubpathTValue; +use crate::utils::{line_intersection, SubpathTValue}; use crate::TValue; use glam::DVec2; @@ -135,6 +135,28 @@ impl Subpath { pub fn contains_point(&self, target_point: DVec2) -> bool { self.iter().map(|bezier| bezier.winding(target_point)).sum::() != 0 } + + pub(crate) fn miter_line_join(&self, other: &Subpath) -> Option> { + let in_segment = self.get_segment(self.len_segments() - 1).unwrap(); + let out_segment = other.get_segment(0).unwrap(); + let in_tangent = in_segment.tangent(TValue::Parametric(1.)); + let out_tangent = out_segment.tangent(TValue::Parametric(0.)); + + let intersection = line_intersection(in_segment.end(), in_tangent, out_segment.start(), out_tangent); + + // Draw the miter join if the intersection occurs in the correct direction with respect to the path + if (intersection - in_segment.end()).normalize().abs_diff_eq(in_tangent, MAX_ABSOLUTE_DIFFERENCE) + && (out_segment.start() - intersection).normalize().abs_diff_eq(out_tangent, MAX_ABSOLUTE_DIFFERENCE) + { + return Some(ManipulatorGroup { + anchor: intersection, + in_handle: None, + out_handle: None, + id: ManipulatorGroupId::new(), + }); + } + None + } } #[cfg(test)] diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index 2233b75685d..5bda10e38e9 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -376,8 +376,18 @@ impl Subpath { Joint::Bevel => { drop_common_point[j] = false; } + Joint::Miter => { + let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j]); + if let Some(miter_manipulator_group) = miter_manipulator_group { + subpaths[i].manipulator_groups.push(miter_manipulator_group); + } + drop_common_point[j] = false; + } _ => unimplemented!(), } + } else { + // Otherwise, default to the bevel join + drop_common_point[j] = false; } } @@ -402,6 +412,14 @@ impl Subpath { Joint::Bevel => { drop_common_point[0] = false; } + Joint::Miter => { + let last_subpath_index = subpaths.len() - 1; + let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0]); + if let Some(miter_manipulator_group) = miter_manipulator_group { + subpaths[last_subpath_index].manipulator_groups.push(miter_manipulator_group); + } + drop_common_point[0] = false; + } _ => unimplemented!(), } } @@ -448,6 +466,11 @@ impl Subpath { pos_offset.closed = true; (pos_offset, None) } + Joint::Miter => { + pos_offset.manipulator_groups.append(&mut neg_offset.manipulator_groups); + pos_offset.closed = true; + (pos_offset, None) + } _ => unimplemented!(), } } 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 570cda07c82..239e2d50634 100644 --- a/website/other/bezier-rs-demos/src/features/subpath-features.ts +++ b/website/other/bezier-rs-demos/src/features/subpath-features.ts @@ -107,7 +107,7 @@ const subpathFeatures = { }, offset: { name: "Offset", - callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.offset(options.distance), + callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.offset(options.distance, options.joint), inputOptions: [ { variable: "distance", @@ -116,11 +116,19 @@ const subpathFeatures = { step: 1, default: 10, }, + { + variable: "joint", + min: 0, + max: 2, + step: 1, + default: 0, + unit: [": Bevel", ": Miter", ": Round"], + }, ], }, outline: { name: "Outline", - callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.outline(options.distance), + callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.outline(options.distance, options.joint), inputOptions: [ { variable: "distance", @@ -129,6 +137,14 @@ const subpathFeatures = { step: 1, default: 10, }, + { + variable: "joint", + min: 0, + max: 2, + step: 1, + default: 0, + unit: [": Bevel", ": Miter", ": Round"], + }, ], }, }; diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index 4c176914a62..cb897e87982 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -1,6 +1,6 @@ use crate::svg_drawing::*; -use bezier_rs::{Bezier, ManipulatorGroup, ProjectionOptions, Subpath, SubpathTValue}; +use bezier_rs::{Bezier, Joint, ManipulatorGroup, ProjectionOptions, Subpath, SubpathTValue}; use glam::DVec2; use std::fmt::Write; @@ -29,6 +29,15 @@ fn parse_t_variant(t_variant: &String, t: f64) -> SubpathTValue { } } +fn parse_joint(joint: i32) -> Joint { + match joint { + 0 => Joint::Bevel, + 1 => Joint::Miter, + 2 => Joint::Round, + _ => panic!("Unexpected Joint string: '{}'", joint), + } +} + #[wasm_bindgen] impl WasmSubpath { /// Expects js_points to be an unbounded list of triples, where each item is a tuple of floats. @@ -377,8 +386,9 @@ impl WasmSubpath { wrap_svg_tag(format!("{}{}", self.to_default_svg(), trimmed_subpath_svg)) } - pub fn offset(&self, distance: f64) -> String { - let offset_subpath = self.0.offset(distance, bezier_rs::Joint::Bevel); + pub fn offset(&self, distance: f64, joint: i32) -> String { + let joint = parse_joint(joint); + let offset_subpath = self.0.offset(distance, joint); let mut offset_svg = String::new(); offset_subpath.to_svg(&mut offset_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); @@ -386,8 +396,9 @@ impl WasmSubpath { wrap_svg_tag(format!("{}{offset_svg}", self.to_default_svg())) } - pub fn outline(&self, distance: f64) -> String { - let (outline_piece1, outline_piece2) = self.0.outline(distance, bezier_rs::Joint::Bevel); + pub fn outline(&self, distance: f64, joint: i32) -> String { + let joint = parse_joint(joint); + let (outline_piece1, outline_piece2) = self.0.outline(distance, joint); let mut outline_piece1_svg = String::new(); outline_piece1.to_svg(&mut outline_piece1_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); From 4a6dfcc1048306e1fc2f1333a0197143053f1238 Mon Sep 17 00:00:00 2001 From: Hannah Li Date: Mon, 6 Mar 2023 12:01:43 -0500 Subject: [PATCH 02/11] Improve miter and add round join --- libraries/bezier-rs/src/bezier/transform.rs | 40 +++++---- libraries/bezier-rs/src/subpath/solvers.rs | 58 ++++++++++--- libraries/bezier-rs/src/subpath/transform.rs | 83 +++++++++++++++---- .../src/features/bezier-features.ts | 31 ++++++- .../other/bezier-rs-demos/wasm/src/bezier.rs | 17 ++-- website/other/bezier-rs-demos/wasm/src/lib.rs | 1 + .../other/bezier-rs-demos/wasm/src/subpath.rs | 12 +-- .../other/bezier-rs-demos/wasm/src/utils.rs | 10 +++ 8 files changed, 184 insertions(+), 68 deletions(-) create mode 100644 website/other/bezier-rs-demos/wasm/src/utils.rs diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs index 5e2555879ef..dea2bc5276e 100644 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ b/libraries/bezier-rs/src/bezier/transform.rs @@ -1,8 +1,8 @@ use super::*; use crate::compare::compare_points; -use crate::utils::{f64_compare, TValue}; -use crate::{AppendType, ManipulatorGroup, Subpath}; +use crate::utils::{f64_compare, Joint, TValue}; +use crate::{AppendType, Subpath}; use glam::DMat2; use std::f64::consts::PI; @@ -405,7 +405,7 @@ impl Bezier { /// Outline takes the following parameter: /// - `distance` - The outline's distance from the curve. /// - pub fn outline(&self, distance: f64) -> Subpath { + pub fn outline(&self, distance: f64, joint: Joint) -> Subpath { let first_segment = self.offset(distance); let third_segment = self.reverse().offset(distance); @@ -413,23 +413,25 @@ impl Bezier { return Subpath::new(vec![], false); } - let mut result_manipulator_groups: Vec> = vec![]; - result_manipulator_groups.extend_from_slice(first_segment.manipulator_groups()); - // TODO: Handle other caps here - result_manipulator_groups.extend_from_slice(third_segment.manipulator_groups()); - Subpath::new(result_manipulator_groups, true) + // Handle join between the two offsets + let joint_to_use = match joint { + // Miter join would result in the same as bevel, so don't bother doing miter calculation + Joint::Miter => Joint::Bevel, + _ => joint, + }; + first_segment.combine_outline(&third_segment, joint_to_use, self.start(), self.end()) } /// Version of the `outline` function which draws the outline at the specified distances away from the curve. /// The outline begins `start_distance` away, and gradually move to being `end_distance` away. /// - pub fn graduated_outline(&self, start_distance: f64, end_distance: f64) -> Subpath { - self.skewed_outline(start_distance, end_distance, end_distance, start_distance) + pub fn graduated_outline(&self, start_distance: f64, end_distance: f64, joint: Joint) -> Subpath { + self.skewed_outline(start_distance, end_distance, end_distance, start_distance, joint) } /// Version of the `graduated_outline` function that allows for the 4 corners of the outline to be different distances away from the curve. /// - pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64) -> Subpath { + pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, joint: Joint) -> Subpath { let first_segment = self.graduated_offset(distance1, distance2); let third_segment = self.reverse().graduated_offset(distance3, distance4); @@ -437,11 +439,13 @@ impl Bezier { return Subpath::new(vec![], false); } - let mut result_manipulator_groups: Vec> = vec![]; - result_manipulator_groups.extend_from_slice(first_segment.manipulator_groups()); - // TODO: Handle other caps here - result_manipulator_groups.extend_from_slice(third_segment.manipulator_groups()); - Subpath::new(result_manipulator_groups, true) + // Handle join between the two offsets + let joint_to_use = match joint { + // Miter join would result in the same as bevel, so don't bother doing miter calculation + Joint::Miter => Joint::Bevel, + _ => joint, + }; + first_segment.combine_outline(&third_segment, joint_to_use, self.start(), self.end()) } /// Approximate a bezier curve with circular arcs. @@ -597,7 +601,7 @@ impl Bezier { mod tests { use super::*; use crate::compare::{compare_arcs, compare_points, compare_vec_of_points}; - use crate::utils::TValue; + use crate::utils::{Joint, TValue}; use crate::EmptyId; #[test] @@ -858,7 +862,7 @@ mod tests { let p1 = DVec2::new(30., 50.); let p2 = DVec2::new(140., 30.); let line = Bezier::from_linear_dvec2(p1, p2); - let outline = line.outline::(10.); + let outline = line.outline::(10., Joint::Bevel); assert_eq!(outline.len(), 4); diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 142d5efb892..27e4e83c268 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -3,7 +3,7 @@ use crate::consts::MAX_ABSOLUTE_DIFFERENCE; use crate::utils::{line_intersection, SubpathTValue}; use crate::TValue; -use glam::DVec2; +use glam::{DMat2, DVec2}; impl Subpath { /// Calculate the point on the subpath based on the parametric `t`-value provided. @@ -136,27 +136,59 @@ impl Subpath { self.iter().map(|bezier| bezier.winding(target_point)).sum::() != 0 } + /// Returns the manipulator point that is needed for a miter join if it is possible. pub(crate) fn miter_line_join(&self, other: &Subpath) -> Option> { let in_segment = self.get_segment(self.len_segments() - 1).unwrap(); let out_segment = other.get_segment(0).unwrap(); let in_tangent = in_segment.tangent(TValue::Parametric(1.)); let out_tangent = out_segment.tangent(TValue::Parametric(0.)); - let intersection = line_intersection(in_segment.end(), in_tangent, out_segment.start(), out_tangent); - - // Draw the miter join if the intersection occurs in the correct direction with respect to the path - if (intersection - in_segment.end()).normalize().abs_diff_eq(in_tangent, MAX_ABSOLUTE_DIFFERENCE) - && (out_segment.start() - intersection).normalize().abs_diff_eq(out_tangent, MAX_ABSOLUTE_DIFFERENCE) - { - return Some(ManipulatorGroup { - anchor: intersection, - in_handle: None, - out_handle: None, - id: ManipulatorGroupId::new(), - }); + let normalized_in_tangent = in_tangent.normalize(); + let normalized_out_tangent = out_tangent.normalize(); + + // The tangents must not be parallel for the miter join + if !normalized_in_tangent.abs_diff_eq(normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) && !normalized_in_tangent.abs_diff_eq(-normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) { + let intersection = line_intersection(in_segment.end(), in_tangent, out_segment.start(), out_tangent); + + // Draw the miter join if the intersection occurs in the correct direction with respect to the path + if (intersection - in_segment.end()).normalize().abs_diff_eq(in_tangent, MAX_ABSOLUTE_DIFFERENCE) + && (out_segment.start() - intersection).normalize().abs_diff_eq(out_tangent, MAX_ABSOLUTE_DIFFERENCE) + { + return Some(ManipulatorGroup { + anchor: intersection, + in_handle: None, + out_handle: None, + id: ManipulatorGroupId::new(), + }); + } } + // If we can't draw the miter join, default to a bevel join None } + + /// Returns the subpath that creates a round join with the provided center. + pub(crate) fn round_line_join(&self, other: &Subpath, center: DVec2) -> Option> { + let left = self.manipulator_groups[self.len() - 1].anchor; + let right = other.manipulator_groups[0].anchor; + + let angle = (right - center).angle_between(left - center) / 2.; + let rotation_matrix = DMat2::from_angle(angle); + let bottom = center + rotation_matrix.mul_vec2(right - center); + + // Based on https://pomax.github.io/bezierinfo/#circles_cubic + let handle_offset_factor = 4. / 3. * (angle / 4.).tan(); + + let manipulator_groups: Vec> = vec![ + ManipulatorGroup::new(left, None, Some(left + (center - left).perp() * handle_offset_factor)), + ManipulatorGroup::new( + bottom, + Some(bottom + (bottom - center).perp() * handle_offset_factor), + Some(bottom + (center - bottom).perp() * handle_offset_factor), + ), + ManipulatorGroup::new(right, Some(right + (right - center).perp() * handle_offset_factor), None), + ]; + Some(Subpath::new(manipulator_groups, false)) + } } #[cfg(test)] diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index 5bda10e38e9..984342e1a1a 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -4,7 +4,7 @@ use super::*; use crate::consts::MAX_ABSOLUTE_DIFFERENCE; use crate::utils::{Joint, SubpathTValue, TValue}; -use glam::DAffine2; +use glam::{DAffine2, DVec2}; /// Helper function to ensure the index and t value pair is mapped within a maximum index value. /// Allows for the point to be fetched without needing to handle an additional edge case. @@ -383,7 +383,13 @@ impl Subpath { } drop_common_point[j] = false; } - _ => unimplemented!(), + Joint::Round => { + let round_subpath = subpaths[i].round_line_join(&subpaths[j], self.manipulator_groups[j].anchor); + if let Some(round_subpath) = round_subpath { + subpaths[i].manipulator_groups.extend(round_subpath.manipulator_groups); + } + drop_common_point[j] = false; + } } } else { // Otherwise, default to the bevel join @@ -420,7 +426,10 @@ impl Subpath { } drop_common_point[0] = false; } - _ => unimplemented!(), + Joint::Round => { + // TODO: Handle this + drop_common_point[0] = false; + } } } } @@ -446,6 +455,47 @@ impl Subpath { Subpath::new(manipulator_groups, self.closed) } + // TODO: Add comment and consider refactoring + pub(crate) fn combine_outline(&self, other: &Subpath, joint: Joint, start: DVec2, end: DVec2) -> Subpath { + let mut result_manipulator_groups: Vec> = vec![]; + result_manipulator_groups.extend_from_slice(self.manipulator_groups()); + match joint { + Joint::Bevel => { + result_manipulator_groups.extend_from_slice(other.manipulator_groups()); + } + Joint::Miter => { + let miter_manipulator_group = self.miter_line_join(other); + if let Some(miter_manipulator_group) = miter_manipulator_group { + result_manipulator_groups.push(miter_manipulator_group); + } + result_manipulator_groups.extend_from_slice(other.manipulator_groups()); + let miter_manipulator_group = other.miter_line_join(self); + if let Some(miter_manipulator_group) = miter_manipulator_group { + result_manipulator_groups.push(miter_manipulator_group); + } + } + Joint::Round => { + let round_subpath = self.round_line_join(other, end); + if let Some(round_subpath) = round_subpath { + let last_index = result_manipulator_groups.len() - 1; + result_manipulator_groups[last_index].out_handle = round_subpath.manipulator_groups[0].out_handle; + result_manipulator_groups.push(round_subpath.manipulator_groups[1].clone()); + result_manipulator_groups.push(other.manipulator_groups[0].clone()); + result_manipulator_groups[last_index + 2].in_handle = round_subpath.manipulator_groups[2].in_handle; + } + result_manipulator_groups.extend_from_slice(&other.manipulator_groups[1..]); + let round_subpath = other.round_line_join(self, start); + if let Some(round_subpath) = round_subpath { + let last_index = result_manipulator_groups.len() - 1; + result_manipulator_groups[last_index].out_handle = round_subpath.manipulator_groups[0].out_handle; + result_manipulator_groups.push(round_subpath.manipulator_groups[1].clone()); + result_manipulator_groups[0].in_handle = round_subpath.manipulator_groups[2].in_handle; + } + } + } + Subpath::new(result_manipulator_groups, true) + } + // TODO: Replace this return type with `Path`, once the `Path` data type has been created. /// Outline returns a single closed subpath (if the original subpath was open) or two closed subpaths (if the original subpath was closed) that forms /// an approximate outline around the subpath at a specified distance from the curve. Outline takes the following parameters: @@ -453,26 +503,23 @@ impl Subpath { /// - `joint` - The joint type used to cap the endpoints of open bezier curves, and join successive subpath segments. /// pub fn outline(&self, distance: f64, joint: Joint) -> (Subpath, Option>) { - let mut pos_offset = self.offset(distance, joint); - let mut neg_offset = self.reverse().offset(distance, joint); + let pos_offset = self.offset(distance, joint); + let neg_offset = self.reverse().offset(distance, joint); if self.closed { return (pos_offset, Some(neg_offset)); } - match joint { - Joint::Bevel => { - pos_offset.manipulator_groups.append(&mut neg_offset.manipulator_groups); - pos_offset.closed = true; - (pos_offset, None) - } - Joint::Miter => { - pos_offset.manipulator_groups.append(&mut neg_offset.manipulator_groups); - pos_offset.closed = true; - (pos_offset, None) - } - _ => unimplemented!(), - } + // Handle join between the two offsets + let joint_to_use = match joint { + // Miter join would result in the same as bevel, so don't bother doing miter calculation + Joint::Miter => Joint::Bevel, + _ => joint, + }; + ( + pos_offset.combine_outline(&neg_offset, joint_to_use, self.manipulator_groups[0].anchor, self.manipulator_groups[self.len() - 1].anchor), + None, + ) } } 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 2a7983514d8..b35aff2982e 100644 --- a/website/other/bezier-rs-demos/src/features/bezier-features.ts +++ b/website/other/bezier-rs-demos/src/features/bezier-features.ts @@ -251,7 +251,7 @@ const bezierFeatures = { }, outline: { name: "Outline", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.outline(options.distance), + callback: (bezier: WasmBezierInstance, options: Record): string => bezier.outline(options.distance, options.joint), demoOptions: { Quadratic: { inputOptions: [ @@ -262,13 +262,21 @@ const bezierFeatures = { step: 1, default: 15, }, + { + variable: "joint", + min: 0, + max: 2, + step: 1, + default: 0, + unit: [": Bevel", ": Miter", ": Round"], + }, ], }, }, }, "graduated-outline": { name: "Graduated Outline", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.graduated_outline(options.start_distance, options.end_distance), + callback: (bezier: WasmBezierInstance, options: Record): string => bezier.graduated_outline(options.start_distance, options.end_distance, options.joint), demoOptions: { Quadratic: { inputOptions: [ @@ -286,6 +294,14 @@ const bezierFeatures = { step: 1, default: 15, }, + { + variable: "joint", + min: 0, + max: 2, + step: 1, + default: 0, + unit: [": Bevel", ": Miter", ": Round"], + }, ], }, }, @@ -300,7 +316,8 @@ const bezierFeatures = { }, "skewed-outline": { name: "Skewed Outline", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.skewed_outline(options.distance1, options.distance2, options.distance3, options.distance4), + callback: (bezier: WasmBezierInstance, options: Record): string => + bezier.skewed_outline(options.distance1, options.distance2, options.distance3, options.distance4, options.joint), demoOptions: { Quadratic: { inputOptions: [ @@ -332,6 +349,14 @@ const bezierFeatures = { step: 1, default: 5, }, + { + variable: "joint", + min: 0, + max: 2, + step: 1, + default: 0, + unit: [": Bevel", ": Miter", ": Round"], + }, ], }, }, diff --git a/website/other/bezier-rs-demos/wasm/src/bezier.rs b/website/other/bezier-rs-demos/wasm/src/bezier.rs index 6eb94533123..e8ca6837f97 100644 --- a/website/other/bezier-rs-demos/wasm/src/bezier.rs +++ b/website/other/bezier-rs-demos/wasm/src/bezier.rs @@ -1,4 +1,6 @@ use crate::svg_drawing::*; +use crate::utils::parse_joint; + use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, Identifier, ProjectionOptions, TValue}; use glam::DVec2; use serde::{Deserialize, Serialize}; @@ -570,8 +572,9 @@ impl WasmBezier { wrap_svg_tag(bezier_curves_svg) } - pub fn outline(&self, distance: f64) -> String { - let outline_subpath = self.0.outline::(distance); + pub fn outline(&self, distance: f64, joint: i32) -> String { + let joint = parse_joint(joint); + let outline_subpath = self.0.outline::(distance, joint); if outline_subpath.is_empty() { return String::new(); } @@ -583,8 +586,9 @@ impl WasmBezier { wrap_svg_tag(format!("{bezier_svg}{outline_svg}")) } - pub fn graduated_outline(&self, start_distance: f64, end_distance: f64) -> String { - let outline_subpath = self.0.graduated_outline::(start_distance, end_distance); + pub fn graduated_outline(&self, start_distance: f64, end_distance: f64, joint: i32) -> String { + let joint = parse_joint(joint); + let outline_subpath = self.0.graduated_outline::(start_distance, end_distance, joint); if outline_subpath.is_empty() { return String::new(); } @@ -596,8 +600,9 @@ impl WasmBezier { wrap_svg_tag(format!("{bezier_svg}{outline_svg}")) } - pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64) -> String { - let outline_subpath = self.0.skewed_outline::(distance1, distance2, distance3, distance4); + pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, joint: i32) -> String { + let joint = parse_joint(joint); + let outline_subpath = self.0.skewed_outline::(distance1, distance2, distance3, distance4, joint); if outline_subpath.is_empty() { return String::new(); } diff --git a/website/other/bezier-rs-demos/wasm/src/lib.rs b/website/other/bezier-rs-demos/wasm/src/lib.rs index 0ac7850c9e7..7bb48e208c6 100644 --- a/website/other/bezier-rs-demos/wasm/src/lib.rs +++ b/website/other/bezier-rs-demos/wasm/src/lib.rs @@ -1,3 +1,4 @@ pub mod bezier; pub mod subpath; mod svg_drawing; +mod utils; diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index cb897e87982..7cf2f7ae278 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -1,6 +1,7 @@ use crate::svg_drawing::*; +use crate::utils::parse_joint; -use bezier_rs::{Bezier, Joint, ManipulatorGroup, ProjectionOptions, Subpath, SubpathTValue}; +use bezier_rs::{Bezier, ManipulatorGroup, ProjectionOptions, Subpath, SubpathTValue}; use glam::DVec2; use std::fmt::Write; @@ -29,15 +30,6 @@ fn parse_t_variant(t_variant: &String, t: f64) -> SubpathTValue { } } -fn parse_joint(joint: i32) -> Joint { - match joint { - 0 => Joint::Bevel, - 1 => Joint::Miter, - 2 => Joint::Round, - _ => panic!("Unexpected Joint string: '{}'", joint), - } -} - #[wasm_bindgen] impl WasmSubpath { /// Expects js_points to be an unbounded list of triples, where each item is a tuple of floats. diff --git a/website/other/bezier-rs-demos/wasm/src/utils.rs b/website/other/bezier-rs-demos/wasm/src/utils.rs new file mode 100644 index 00000000000..044f3db29dd --- /dev/null +++ b/website/other/bezier-rs-demos/wasm/src/utils.rs @@ -0,0 +1,10 @@ +use bezier_rs::Joint; + +pub fn parse_joint(joint: i32) -> Joint { + match joint { + 0 => Joint::Bevel, + 1 => Joint::Miter, + 2 => Joint::Round, + _ => panic!("Unexpected Joint string: '{}'", joint), + } +} From 6fb029123fe644277d27167396af4d9e86b10cd6 Mon Sep 17 00:00:00 2001 From: Hannah Li Date: Mon, 6 Mar 2023 16:22:42 -0500 Subject: [PATCH 03/11] Get arcs to go opposite direction --- libraries/bezier-rs/src/subpath/solvers.rs | 52 +++++++++++++++++--- libraries/bezier-rs/src/subpath/transform.rs | 14 +++++- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 27e4e83c268..15c3c4e9e4b 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -4,6 +4,7 @@ use crate::utils::{line_intersection, SubpathTValue}; use crate::TValue; use glam::{DMat2, DVec2}; +use std::f64::consts::PI; impl Subpath { /// Calculate the point on the subpath based on the parametric `t`-value provided. @@ -171,21 +172,31 @@ impl Subpath { let left = self.manipulator_groups[self.len() - 1].anchor; let right = other.manipulator_groups[0].anchor; - let angle = (right - center).angle_between(left - center) / 2.; + let center_to_right = right - center; + let center_to_left = left - center; + + let in_segment = self.get_segment(self.len_segments() - 1).unwrap(); + let tangent_angle = (right - left).angle_between(in_segment.tangent(TValue::Parametric(1.))); + + let angle = center_to_right.angle_between(center_to_left) / 2.; + let rotation_matrix = DMat2::from_angle(angle); - let bottom = center + rotation_matrix.mul_vec2(right - center); + let arc_point = center + rotation_matrix.mul_vec2(center_to_right); + + let arc_direction_factor = if tangent_angle >= 0. { 1. } else { -1. }; + let center_to_arc_point = arc_point - center; // Based on https://pomax.github.io/bezierinfo/#circles_cubic let handle_offset_factor = 4. / 3. * (angle / 4.).tan(); let manipulator_groups: Vec> = vec![ - ManipulatorGroup::new(left, None, Some(left + (center - left).perp() * handle_offset_factor)), + ManipulatorGroup::new(left, None, Some(left - center_to_left.perp() * handle_offset_factor)), ManipulatorGroup::new( - bottom, - Some(bottom + (bottom - center).perp() * handle_offset_factor), - Some(bottom + (center - bottom).perp() * handle_offset_factor), + arc_point, + Some(arc_point + center_to_arc_point.perp() * handle_offset_factor), + Some(arc_point - center_to_arc_point.perp() * handle_offset_factor), ), - ManipulatorGroup::new(right, Some(right + (right - center).perp() * handle_offset_factor), None), + ManipulatorGroup::new(right, Some(right + center_to_right.perp() * handle_offset_factor), None), ]; Some(Subpath::new(manipulator_groups, false)) } @@ -204,6 +215,33 @@ mod tests { t * (n as f64) % 1. } + #[test] + fn round_join() { + // TODO: Remove or write actual test + let s1 = DVec2::new(100., 50.); + let e1 = DVec2::new(100., 150.); + let s2 = DVec2::new(150., 150.); + let e2 = DVec2::new(150., 50.); + + let center = DVec2::new(125., 100.); + let line1: Subpath = Subpath::from_bezier(&Bezier::from_linear_dvec2(s1, e1)); + let line2: Subpath = Subpath::from_bezier(&Bezier::from_linear_dvec2(s2, e2)); + + println!("{}", -1 % 1); + + let result = line1.round_line_join(&line2, center).unwrap(); + let mut str = String::new(); + result.to_svg( + &mut str, + "stroke=\"black\" stroke-width=\"2\" fill=\"none\"".to_string(), + String::new(), + String::new(), + "stroke=\"red\" stroke-width=\"1\" fill=\"none\"".to_string(), + ); + println!("{:?}\n{:?}\n{:?}", line1, line2, result); + println!("{}", str); + } + #[test] fn evaluate_one_subpath_curve() { let start = DVec2::new(20., 30.); diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index 984342e1a1a..f10c241aafd 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -386,7 +386,10 @@ impl Subpath { Joint::Round => { let round_subpath = subpaths[i].round_line_join(&subpaths[j], self.manipulator_groups[j].anchor); if let Some(round_subpath) = round_subpath { - subpaths[i].manipulator_groups.extend(round_subpath.manipulator_groups); + let last_index = subpaths[i].manipulator_groups.len() - 1; + subpaths[i].manipulator_groups[last_index].out_handle = round_subpath.manipulator_groups[0].out_handle; + subpaths[i].manipulator_groups.push(round_subpath.manipulator_groups[1].clone()); + subpaths[j].manipulator_groups[0].in_handle = round_subpath.manipulator_groups[2].in_handle; } drop_common_point[j] = false; } @@ -427,7 +430,14 @@ impl Subpath { drop_common_point[0] = false; } Joint::Round => { - // TODO: Handle this + let last_subpath_index = subpaths.len() - 1; + let round_subpath = subpaths[last_subpath_index].round_line_join(&subpaths[0], self.manipulator_groups[0].anchor); + if let Some(round_subpath) = round_subpath { + let last_index = subpaths[last_subpath_index].manipulator_groups.len() - 1; + subpaths[last_subpath_index].manipulator_groups[last_index].out_handle = round_subpath.manipulator_groups[0].out_handle; + subpaths[last_subpath_index].manipulator_groups.push(round_subpath.manipulator_groups[1].clone()); + subpaths[0].manipulator_groups[0].in_handle = round_subpath.manipulator_groups[2].in_handle; + } drop_common_point[0] = false; } } From 65f35a9733e08e24cd96f8c9c24de086a0a5d479 Mon Sep 17 00:00:00 2001 From: Hannah Li Date: Mon, 6 Mar 2023 23:37:48 -0500 Subject: [PATCH 04/11] Add cap and other refactors --- libraries/bezier-rs/src/bezier/transform.rs | 26 ++---- libraries/bezier-rs/src/lib.rs | 2 +- libraries/bezier-rs/src/subpath/solvers.rs | 89 ++++++++++++++----- libraries/bezier-rs/src/subpath/transform.rs | 88 +++++++----------- libraries/bezier-rs/src/utils.rs | 9 +- .../src/features/bezier-features.ts | 18 ++-- .../src/features/subpath-features.ts | 10 ++- .../other/bezier-rs-demos/wasm/src/bezier.rs | 20 ++--- .../other/bezier-rs-demos/wasm/src/subpath.rs | 7 +- .../other/bezier-rs-demos/wasm/src/utils.rs | 13 ++- 10 files changed, 158 insertions(+), 124 deletions(-) diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs index dea2bc5276e..421f1b384cc 100644 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ b/libraries/bezier-rs/src/bezier/transform.rs @@ -1,7 +1,7 @@ use super::*; use crate::compare::compare_points; -use crate::utils::{f64_compare, Joint, TValue}; +use crate::utils::{f64_compare, Cap, TValue}; use crate::{AppendType, Subpath}; use glam::DMat2; @@ -405,7 +405,7 @@ impl Bezier { /// Outline takes the following parameter: /// - `distance` - The outline's distance from the curve. /// - pub fn outline(&self, distance: f64, joint: Joint) -> Subpath { + pub fn outline(&self, distance: f64, cap: Cap) -> Subpath { let first_segment = self.offset(distance); let third_segment = self.reverse().offset(distance); @@ -413,25 +413,19 @@ impl Bezier { return Subpath::new(vec![], false); } - // Handle join between the two offsets - let joint_to_use = match joint { - // Miter join would result in the same as bevel, so don't bother doing miter calculation - Joint::Miter => Joint::Bevel, - _ => joint, - }; - first_segment.combine_outline(&third_segment, joint_to_use, self.start(), self.end()) + first_segment.combine_outline(&third_segment, cap) } /// Version of the `outline` function which draws the outline at the specified distances away from the curve. /// The outline begins `start_distance` away, and gradually move to being `end_distance` away. /// - pub fn graduated_outline(&self, start_distance: f64, end_distance: f64, joint: Joint) -> Subpath { - self.skewed_outline(start_distance, end_distance, end_distance, start_distance, joint) + pub fn graduated_outline(&self, start_distance: f64, end_distance: f64, cap: Cap) -> Subpath { + self.skewed_outline(start_distance, end_distance, end_distance, start_distance, cap) } /// Version of the `graduated_outline` function that allows for the 4 corners of the outline to be different distances away from the curve. /// - pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, joint: Joint) -> Subpath { + pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: Cap) -> Subpath { let first_segment = self.graduated_offset(distance1, distance2); let third_segment = self.reverse().graduated_offset(distance3, distance4); @@ -439,13 +433,7 @@ impl Bezier { return Subpath::new(vec![], false); } - // Handle join between the two offsets - let joint_to_use = match joint { - // Miter join would result in the same as bevel, so don't bother doing miter calculation - Joint::Miter => Joint::Bevel, - _ => joint, - }; - first_segment.combine_outline(&third_segment, joint_to_use, self.start(), self.end()) + first_segment.combine_outline(&third_segment, cap) } /// Approximate a bezier curve with circular arcs. diff --git a/libraries/bezier-rs/src/lib.rs b/libraries/bezier-rs/src/lib.rs index 61a7fd7bfa7..b57b7b2a302 100644 --- a/libraries/bezier-rs/src/lib.rs +++ b/libraries/bezier-rs/src/lib.rs @@ -8,4 +8,4 @@ mod utils; pub use bezier::*; pub use subpath::*; -pub use utils::{Joint, SubpathTValue, TValue}; +pub use utils::{Cap, Joint, SubpathTValue, TValue}; diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 15c3c4e9e4b..28cc9c6647c 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -1,6 +1,6 @@ use super::*; use crate::consts::MAX_ABSOLUTE_DIFFERENCE; -use crate::utils::{line_intersection, SubpathTValue}; +use crate::utils::{f64_compare, line_intersection, SubpathTValue}; use crate::TValue; use glam::{DMat2, DVec2}; @@ -168,7 +168,7 @@ impl Subpath { } /// Returns the subpath that creates a round join with the provided center. - pub(crate) fn round_line_join(&self, other: &Subpath, center: DVec2) -> Option> { + pub(crate) fn round_line_join(&self, other: &Subpath, center: DVec2) -> (DVec2, ManipulatorGroup, DVec2) { let left = self.manipulator_groups[self.len() - 1].anchor; let right = other.manipulator_groups[0].anchor; @@ -176,29 +176,60 @@ impl Subpath { let center_to_left = left - center; let in_segment = self.get_segment(self.len_segments() - 1).unwrap(); - let tangent_angle = (right - left).angle_between(in_segment.tangent(TValue::Parametric(1.))); + let tangent_angle = center_to_right.angle_between(in_segment.tangent(TValue::Parametric(1.))); - let angle = center_to_right.angle_between(center_to_left) / 2.; + let mut angle = center_to_right.angle_between(center_to_left) / 2.; + + if f64_compare(angle.abs(), PI / 2., MAX_ABSOLUTE_DIFFERENCE) && tangent_angle * angle < 0. { + angle = -angle; + } + // if (0. < tangent_angle && tangent_angle < PI && -PI < angle && angle < 0.) || (0. < angle && angle < PI && -PI < tangent_angle && tangent_angle < 0.) { + // angle = -angle; + // println!("angle: {}, tangent_angle: {}", angle, tangent_angle); + // } let rotation_matrix = DMat2::from_angle(angle); let arc_point = center + rotation_matrix.mul_vec2(center_to_right); - let arc_direction_factor = if tangent_angle >= 0. { 1. } else { -1. }; let center_to_arc_point = arc_point - center; // Based on https://pomax.github.io/bezierinfo/#circles_cubic let handle_offset_factor = 4. / 3. * (angle / 4.).tan(); - let manipulator_groups: Vec> = vec![ - ManipulatorGroup::new(left, None, Some(left - center_to_left.perp() * handle_offset_factor)), + ( + left - center_to_left.perp() * handle_offset_factor, ManipulatorGroup::new( arc_point, Some(arc_point + center_to_arc_point.perp() * handle_offset_factor), Some(arc_point - center_to_arc_point.perp() * handle_offset_factor), ), - ManipulatorGroup::new(right, Some(right + center_to_right.perp() * handle_offset_factor), None), - ]; - Some(Subpath::new(manipulator_groups, false)) + right + center_to_right.perp() * handle_offset_factor, + ) + } + + pub(crate) fn round_cap(&self, other: &Subpath) -> (DVec2, ManipulatorGroup, DVec2) { + // Based on https://pomax.github.io/bezierinfo/#circles_cubic + const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014; + + let left = self.manipulator_groups[self.len() - 1].anchor; + let right = other.manipulator_groups[0].anchor; + + let center = (right + left) / 2.; + let center_to_right = right - center; + let center_to_left = left - center; + + let arc_point = center + center_to_right.perp(); + let center_to_arc_point = arc_point - center; + + ( + left - center_to_left.perp() * HANDLE_OFFSET_FACTOR, + ManipulatorGroup::new( + arc_point, + Some(arc_point + center_to_arc_point.perp() * HANDLE_OFFSET_FACTOR), + Some(arc_point - center_to_arc_point.perp() * HANDLE_OFFSET_FACTOR), + ), + right + center_to_right.perp() * HANDLE_OFFSET_FACTOR, + ) } } @@ -218,18 +249,36 @@ mod tests { #[test] fn round_join() { // TODO: Remove or write actual test - let s1 = DVec2::new(100., 50.); - let e1 = DVec2::new(100., 150.); - let s2 = DVec2::new(150., 150.); - let e2 = DVec2::new(150., 50.); + let s1 = DVec2::new(163., 61.); + let h1 = DVec2::new(140., 30.); + let e1 = DVec2::new(91., 177.); + + let bezier = Bezier::from_quadratic_dvec2(s1, h1, e1); - let center = DVec2::new(125., 100.); - let line1: Subpath = Subpath::from_bezier(&Bezier::from_linear_dvec2(s1, e1)); - let line2: Subpath = Subpath::from_bezier(&Bezier::from_linear_dvec2(s2, e2)); + let pos_offset = bezier.offset::(15.); + let neg_offset = bezier.reverse().offset::(15.); - println!("{}", -1 % 1); + println!("test:{}", DVec2::new(0., 1.).angle_between(DVec2::new(1., 0.))); - let result = line1.round_line_join(&line2, center).unwrap(); + let (out_handle, manip, in_handle) = pos_offset.round_line_join(&neg_offset, e1); + let result = Subpath::new( + vec![ + ManipulatorGroup { + anchor: pos_offset.evaluate(1.), + out_handle: Some(out_handle), + in_handle: None, + id: EmptyId, + }, + manip.clone(), + ManipulatorGroup { + anchor: neg_offset.evaluate(0.), + out_handle: None, + in_handle: Some(in_handle), + id: EmptyId, + }, + ], + false, + ); let mut str = String::new(); result.to_svg( &mut str, @@ -238,7 +287,7 @@ mod tests { String::new(), "stroke=\"red\" stroke-width=\"1\" fill=\"none\"".to_string(), ); - println!("{:?}\n{:?}\n{:?}", line1, line2, result); + println!("{:?}", result); println!("{}", str); } diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index f10c241aafd..ff1e6374bda 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -2,9 +2,9 @@ use std::vec; use super::*; use crate::consts::MAX_ABSOLUTE_DIFFERENCE; -use crate::utils::{Joint, SubpathTValue, TValue}; +use crate::utils::{Cap, Joint, SubpathTValue, TValue}; -use glam::{DAffine2, DVec2}; +use glam::DAffine2; /// Helper function to ensure the index and t value pair is mapped within a maximum index value. /// Allows for the point to be fetched without needing to handle an additional edge case. @@ -384,13 +384,11 @@ impl Subpath { drop_common_point[j] = false; } Joint::Round => { - let round_subpath = subpaths[i].round_line_join(&subpaths[j], self.manipulator_groups[j].anchor); - if let Some(round_subpath) = round_subpath { - let last_index = subpaths[i].manipulator_groups.len() - 1; - subpaths[i].manipulator_groups[last_index].out_handle = round_subpath.manipulator_groups[0].out_handle; - subpaths[i].manipulator_groups.push(round_subpath.manipulator_groups[1].clone()); - subpaths[j].manipulator_groups[0].in_handle = round_subpath.manipulator_groups[2].in_handle; - } + let (out_handle, round_point, in_handle) = subpaths[i].round_line_join(&subpaths[j], self.manipulator_groups[j].anchor); + let last_index = subpaths[i].manipulator_groups.len() - 1; + subpaths[i].manipulator_groups[last_index].out_handle = Some(out_handle); + subpaths[i].manipulator_groups.push(round_point.clone()); + subpaths[j].manipulator_groups[0].in_handle = Some(in_handle); drop_common_point[j] = false; } } @@ -431,13 +429,11 @@ impl Subpath { } Joint::Round => { let last_subpath_index = subpaths.len() - 1; - let round_subpath = subpaths[last_subpath_index].round_line_join(&subpaths[0], self.manipulator_groups[0].anchor); - if let Some(round_subpath) = round_subpath { - let last_index = subpaths[last_subpath_index].manipulator_groups.len() - 1; - subpaths[last_subpath_index].manipulator_groups[last_index].out_handle = round_subpath.manipulator_groups[0].out_handle; - subpaths[last_subpath_index].manipulator_groups.push(round_subpath.manipulator_groups[1].clone()); - subpaths[0].manipulator_groups[0].in_handle = round_subpath.manipulator_groups[2].in_handle; - } + let (out_handle, round_point, in_handle) = subpaths[last_subpath_index].round_line_join(&subpaths[0], self.manipulator_groups[0].anchor); + let last_index = subpaths[last_subpath_index].manipulator_groups.len() - 1; + subpaths[last_subpath_index].manipulator_groups[last_index].out_handle = Some(out_handle); + subpaths[last_subpath_index].manipulator_groups.push(round_point); + subpaths[0].manipulator_groups[0].in_handle = Some(in_handle); drop_common_point[0] = false; } } @@ -466,41 +462,26 @@ impl Subpath { } // TODO: Add comment and consider refactoring - pub(crate) fn combine_outline(&self, other: &Subpath, joint: Joint, start: DVec2, end: DVec2) -> Subpath { + pub(crate) fn combine_outline(&self, other: &Subpath, cap: Cap) -> Subpath { let mut result_manipulator_groups: Vec> = vec![]; result_manipulator_groups.extend_from_slice(self.manipulator_groups()); - match joint { - Joint::Bevel => { - result_manipulator_groups.extend_from_slice(other.manipulator_groups()); + match cap { + Cap::Round => { + let last_index = result_manipulator_groups.len() - 1; + let (out_handle, round_point, in_handle) = self.round_cap(other); + result_manipulator_groups[last_index].out_handle = Some(out_handle); + result_manipulator_groups.push(round_point); + result_manipulator_groups.extend_from_slice(&other.manipulator_groups); + result_manipulator_groups[last_index + 2].in_handle = Some(in_handle); + + let last_index = result_manipulator_groups.len() - 1; + let (out_handle, round_point, in_handle) = other.round_cap(self); + result_manipulator_groups[last_index].out_handle = Some(out_handle); + result_manipulator_groups.push(round_point); + result_manipulator_groups[0].in_handle = Some(in_handle); } - Joint::Miter => { - let miter_manipulator_group = self.miter_line_join(other); - if let Some(miter_manipulator_group) = miter_manipulator_group { - result_manipulator_groups.push(miter_manipulator_group); - } + _ => { result_manipulator_groups.extend_from_slice(other.manipulator_groups()); - let miter_manipulator_group = other.miter_line_join(self); - if let Some(miter_manipulator_group) = miter_manipulator_group { - result_manipulator_groups.push(miter_manipulator_group); - } - } - Joint::Round => { - let round_subpath = self.round_line_join(other, end); - if let Some(round_subpath) = round_subpath { - let last_index = result_manipulator_groups.len() - 1; - result_manipulator_groups[last_index].out_handle = round_subpath.manipulator_groups[0].out_handle; - result_manipulator_groups.push(round_subpath.manipulator_groups[1].clone()); - result_manipulator_groups.push(other.manipulator_groups[0].clone()); - result_manipulator_groups[last_index + 2].in_handle = round_subpath.manipulator_groups[2].in_handle; - } - result_manipulator_groups.extend_from_slice(&other.manipulator_groups[1..]); - let round_subpath = other.round_line_join(self, start); - if let Some(round_subpath) = round_subpath { - let last_index = result_manipulator_groups.len() - 1; - result_manipulator_groups[last_index].out_handle = round_subpath.manipulator_groups[0].out_handle; - result_manipulator_groups.push(round_subpath.manipulator_groups[1].clone()); - result_manipulator_groups[0].in_handle = round_subpath.manipulator_groups[2].in_handle; - } } } Subpath::new(result_manipulator_groups, true) @@ -512,7 +493,7 @@ impl Subpath { /// - `distance` - The outline's distance from the curve. /// - `joint` - The joint type used to cap the endpoints of open bezier curves, and join successive subpath segments. /// - pub fn outline(&self, distance: f64, joint: Joint) -> (Subpath, Option>) { + pub fn outline(&self, distance: f64, joint: Joint, cap: Cap) -> (Subpath, Option>) { let pos_offset = self.offset(distance, joint); let neg_offset = self.reverse().offset(distance, joint); @@ -520,16 +501,7 @@ impl Subpath { return (pos_offset, Some(neg_offset)); } - // Handle join between the two offsets - let joint_to_use = match joint { - // Miter join would result in the same as bevel, so don't bother doing miter calculation - Joint::Miter => Joint::Bevel, - _ => joint, - }; - ( - pos_offset.combine_outline(&neg_offset, joint_to_use, self.manipulator_groups[0].anchor, self.manipulator_groups[self.len() - 1].anchor), - None, - ) + (pos_offset.combine_outline(&neg_offset, cap), None) } } diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs index 9a0975756b1..9355df0f5ce 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -30,9 +30,16 @@ pub enum SubpathTValue { #[derive(Copy, Clone)] pub enum Joint { - Miter, Bevel, + Miter, + Round, +} + +#[derive(Copy, Clone)] +pub enum Cap { + Butt, Round, + Square, } /// Helper to perform the computation of a and c, where b is the provided point on the curve. 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 b35aff2982e..2352baa66dd 100644 --- a/website/other/bezier-rs-demos/src/features/bezier-features.ts +++ b/website/other/bezier-rs-demos/src/features/bezier-features.ts @@ -251,7 +251,7 @@ const bezierFeatures = { }, outline: { name: "Outline", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.outline(options.distance, options.joint), + callback: (bezier: WasmBezierInstance, options: Record): string => bezier.outline(options.distance, options.cap), demoOptions: { Quadratic: { inputOptions: [ @@ -263,12 +263,12 @@ const bezierFeatures = { default: 15, }, { - variable: "joint", + variable: "cap", min: 0, max: 2, step: 1, default: 0, - unit: [": Bevel", ": Miter", ": Round"], + unit: [": Butt", ": Round", ": Square"], }, ], }, @@ -276,7 +276,7 @@ const bezierFeatures = { }, "graduated-outline": { name: "Graduated Outline", - callback: (bezier: WasmBezierInstance, options: Record): string => bezier.graduated_outline(options.start_distance, options.end_distance, options.joint), + callback: (bezier: WasmBezierInstance, options: Record): string => bezier.graduated_outline(options.start_distance, options.end_distance, options.cap), demoOptions: { Quadratic: { inputOptions: [ @@ -295,12 +295,12 @@ const bezierFeatures = { default: 15, }, { - variable: "joint", + variable: "cap", min: 0, max: 2, step: 1, default: 0, - unit: [": Bevel", ": Miter", ": Round"], + unit: [": Butt", ": Round", ": Square"], }, ], }, @@ -317,7 +317,7 @@ const bezierFeatures = { "skewed-outline": { name: "Skewed Outline", callback: (bezier: WasmBezierInstance, options: Record): string => - bezier.skewed_outline(options.distance1, options.distance2, options.distance3, options.distance4, options.joint), + bezier.skewed_outline(options.distance1, options.distance2, options.distance3, options.distance4, options.cap), demoOptions: { Quadratic: { inputOptions: [ @@ -350,12 +350,12 @@ const bezierFeatures = { default: 5, }, { - variable: "joint", + variable: "cap", min: 0, max: 2, step: 1, default: 0, - unit: [": Bevel", ": Miter", ": Round"], + unit: [": Butt", ": Round", ": Square"], }, ], }, 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 239e2d50634..773481650e2 100644 --- a/website/other/bezier-rs-demos/src/features/subpath-features.ts +++ b/website/other/bezier-rs-demos/src/features/subpath-features.ts @@ -128,7 +128,7 @@ const subpathFeatures = { }, outline: { name: "Outline", - callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.outline(options.distance, options.joint), + callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.outline(options.distance, options.joint, options.cap), inputOptions: [ { variable: "distance", @@ -145,6 +145,14 @@ const subpathFeatures = { default: 0, unit: [": Bevel", ": Miter", ": Round"], }, + { + variable: "cap", + min: 0, + max: 2, + step: 1, + default: 0, + unit: [": Butt", ": Round", ": Square"], + }, ], }, }; diff --git a/website/other/bezier-rs-demos/wasm/src/bezier.rs b/website/other/bezier-rs-demos/wasm/src/bezier.rs index e8ca6837f97..24e068895c2 100644 --- a/website/other/bezier-rs-demos/wasm/src/bezier.rs +++ b/website/other/bezier-rs-demos/wasm/src/bezier.rs @@ -1,5 +1,5 @@ use crate::svg_drawing::*; -use crate::utils::parse_joint; +use crate::utils::parse_cap; use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, Identifier, ProjectionOptions, TValue}; use glam::DVec2; @@ -572,9 +572,9 @@ impl WasmBezier { wrap_svg_tag(bezier_curves_svg) } - pub fn outline(&self, distance: f64, joint: i32) -> String { - let joint = parse_joint(joint); - let outline_subpath = self.0.outline::(distance, joint); + pub fn outline(&self, distance: f64, cap: i32) -> String { + let cap = parse_cap(cap); + let outline_subpath = self.0.outline::(distance, cap); if outline_subpath.is_empty() { return String::new(); } @@ -586,9 +586,9 @@ impl WasmBezier { wrap_svg_tag(format!("{bezier_svg}{outline_svg}")) } - pub fn graduated_outline(&self, start_distance: f64, end_distance: f64, joint: i32) -> String { - let joint = parse_joint(joint); - let outline_subpath = self.0.graduated_outline::(start_distance, end_distance, joint); + pub fn graduated_outline(&self, start_distance: f64, end_distance: f64, cap: i32) -> String { + let cap = parse_cap(cap); + let outline_subpath = self.0.graduated_outline::(start_distance, end_distance, cap); if outline_subpath.is_empty() { return String::new(); } @@ -600,9 +600,9 @@ impl WasmBezier { wrap_svg_tag(format!("{bezier_svg}{outline_svg}")) } - pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, joint: i32) -> String { - let joint = parse_joint(joint); - let outline_subpath = self.0.skewed_outline::(distance1, distance2, distance3, distance4, joint); + pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: i32) -> String { + let cap = parse_cap(cap); + let outline_subpath = self.0.skewed_outline::(distance1, distance2, distance3, distance4, cap); if outline_subpath.is_empty() { return String::new(); } diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index 7cf2f7ae278..f3904ab5f72 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -1,5 +1,5 @@ use crate::svg_drawing::*; -use crate::utils::parse_joint; +use crate::utils::{parse_cap, parse_joint}; use bezier_rs::{Bezier, ManipulatorGroup, ProjectionOptions, Subpath, SubpathTValue}; @@ -388,9 +388,10 @@ impl WasmSubpath { wrap_svg_tag(format!("{}{offset_svg}", self.to_default_svg())) } - pub fn outline(&self, distance: f64, joint: i32) -> String { + pub fn outline(&self, distance: f64, joint: i32, cap: i32) -> String { let joint = parse_joint(joint); - let (outline_piece1, outline_piece2) = self.0.outline(distance, joint); + let cap = parse_cap(cap); + let (outline_piece1, outline_piece2) = self.0.outline(distance, joint, cap); let mut outline_piece1_svg = String::new(); outline_piece1.to_svg(&mut outline_piece1_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); diff --git a/website/other/bezier-rs-demos/wasm/src/utils.rs b/website/other/bezier-rs-demos/wasm/src/utils.rs index 044f3db29dd..3ddc75b1448 100644 --- a/website/other/bezier-rs-demos/wasm/src/utils.rs +++ b/website/other/bezier-rs-demos/wasm/src/utils.rs @@ -1,10 +1,19 @@ -use bezier_rs::Joint; +use bezier_rs::{Cap, Joint}; pub fn parse_joint(joint: i32) -> Joint { match joint { 0 => Joint::Bevel, 1 => Joint::Miter, 2 => Joint::Round, - _ => panic!("Unexpected Joint string: '{}'", joint), + _ => panic!("Unexpected Joint value: '{}'", joint), + } +} + +pub fn parse_cap(cap: i32) -> Cap { + match cap { + 0 => Cap::Butt, + 1 => Cap::Round, + 2 => Cap::Square, + _ => panic!("Unexpected Cap value: '{}'", cap), } } From e869dcd346b2fe531d6842476a392ba7cf9c4c14 Mon Sep 17 00:00:00 2001 From: Hannah Li Date: Sat, 11 Mar 2023 17:59:02 -0500 Subject: [PATCH 05/11] Rename joint to join, fix some bugs --- libraries/bezier-rs/src/bezier/solvers.rs | 13 ++- libraries/bezier-rs/src/bezier/transform.rs | 92 ++++++++++++++++--- libraries/bezier-rs/src/lib.rs | 2 +- libraries/bezier-rs/src/subpath/solvers.rs | 16 +++- libraries/bezier-rs/src/subpath/transform.rs | 56 ++++++----- libraries/bezier-rs/src/utils.rs | 2 +- .../src/features/subpath-features.ts | 8 +- .../other/bezier-rs-demos/wasm/src/bezier.rs | 31 ++++--- .../other/bezier-rs-demos/wasm/src/subpath.rs | 14 +-- .../other/bezier-rs-demos/wasm/src/utils.rs | 14 +-- 10 files changed, 172 insertions(+), 76 deletions(-) diff --git a/libraries/bezier-rs/src/bezier/solvers.rs b/libraries/bezier-rs/src/bezier/solvers.rs index b0b771f9dde..18be83ec6e0 100644 --- a/libraries/bezier-rs/src/bezier/solvers.rs +++ b/libraries/bezier-rs/src/bezier/solvers.rs @@ -64,7 +64,12 @@ impl Bezier { /// pub fn tangent(&self, t: TValue) -> DVec2 { let t = self.t_value_to_parametric(t); - self.non_normalized_tangent(t).normalize() + let tangent = self.non_normalized_tangent(t); + if tangent.length() > 0. { + tangent.normalize() + } else { + tangent + } } /// Returns a normalized unit vector representing the direction of the normal at the point `t` along the curve. @@ -88,7 +93,7 @@ impl Bezier { let numerator = d.x * dd.y - d.y * dd.x; let denominator = (d.x.powf(2.) + d.y.powf(2.)).powf(1.5); - if denominator == 0. { + if denominator.abs() < MAX_ABSOLUTE_DIFFERENCE { 0. } else { numerator / denominator @@ -369,9 +374,9 @@ impl Bezier { } // Create iterators that combine a subcurve with the `t` value pair that it was trimmed with - let combined_iterator1 = self1.into_iter().zip(self1_t_values.windows(2).map(|t_pair| Range { start: t_pair[0], end: t_pair[1] })); + let combined_iterator1 = self1.into_iter().zip(self1_t_values.iter().map(|t_pair| Range { start: t_pair[0], end: t_pair[1] })); // Second one needs to be a list because Iterator does not implement copy - let combined_list2: Vec<(Bezier, Range)> = self2.into_iter().zip(self2_t_values.windows(2).map(|t_pair| Range { start: t_pair[0], end: t_pair[1] })).collect(); + let combined_list2: Vec<(Bezier, Range)> = self2.into_iter().zip(self2_t_values.iter().map(|t_pair| Range { start: t_pair[0], end: t_pair[1] })).collect(); // For each curve, look for intersections with every curve that is at least 2 indices away combined_iterator1 diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs index 421f1b384cc..0928119dca4 100644 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ b/libraries/bezier-rs/src/bezier/transform.rs @@ -158,7 +158,7 @@ impl Bezier { // Verify the angle formed by the endpoint normals is sufficiently small, ensuring the on-curve point for `t = 0.5` occurs roughly in the center of the polygon. let normal_0 = self.normal(TValue::Parametric(0.)); let normal_1 = self.normal(TValue::Parametric(1.)); - let endpoint_normal_angle = (normal_0.x * normal_1.x + normal_0.y * normal_1.y).acos(); + let endpoint_normal_angle = (normal_0.x * normal_1.x + normal_0.y * normal_1.y).min(1.).acos(); endpoint_normal_angle < SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE } @@ -176,10 +176,10 @@ impl Bezier { /// The function takes the following parameter: /// - `step_size` - Dictates the granularity at which the function searches for reducible subcurves. The default value is `0.01`. /// A small granularity may increase the chance the function does not introduce gaps, but will increase computation time. - pub(crate) fn reduced_curves_and_t_values(&self, step_size: Option) -> (Vec, Vec) { + pub(crate) fn reduced_curves_and_t_values(&self, step_size: Option) -> (Vec, Vec<[f64; 2]>) { // A linear segment is scalable, so return itself if let BezierHandles::Linear = self.handles { - return (vec![*self], vec![0., 1.]); + return (vec![*self], vec![[0., 1.]]); } let step_size = step_size.unwrap_or(DEFAULT_REDUCE_STEP_SIZE); @@ -192,7 +192,7 @@ impl Bezier { // Split each subcurve such that each resulting segment is scalable. let mut result_beziers: Vec = Vec::new(); - let mut result_t_values: Vec = vec![extrema[0]]; + let mut result_t_values: Vec<[f64; 2]> = vec![]; extrema.windows(2).for_each(|t_pair| { let t_subcurve_start = t_pair[0]; @@ -201,7 +201,7 @@ impl Bezier { // Perform no processing on the subcurve if it's already scalable. if subcurve.is_scalable() { result_beziers.push(subcurve); - result_t_values.push(t_subcurve_end); + result_t_values.push([t_subcurve_start, t_subcurve_end]); return; } @@ -209,6 +209,7 @@ impl Bezier { let mut segment: Bezier; let mut t1 = 0.; let mut t2 = step_size; + let mut is_prev_valid = false; while t2 <= 1. + step_size { segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(f64::min(t2, 1.))); if !segment.is_scalable() { @@ -216,14 +217,21 @@ impl Bezier { // If the previous step does not exist, the start of the subcurve is irreducible. // Otherwise, add the valid segment from the previous step to the result. - if f64::abs(t1 - t2) >= step_size { + if is_prev_valid { segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(t2)); - result_beziers.push(segment); - result_t_values.push(t_subcurve_start + t2 * (t_subcurve_end - t_subcurve_start)); + if segment.is_scalable() { + result_beziers.push(segment); + result_t_values.push([t_subcurve_start + t1 * (t_subcurve_end - t_subcurve_start), t_subcurve_start + t2 * (t_subcurve_end - t_subcurve_start)]); + } else { + t2 = t1 + step_size; + } } else { - return; + t2 = t1 + step_size; } t1 = t2; + is_prev_valid = false; + } else { + is_prev_valid = true; } t2 += step_size; } @@ -232,7 +240,7 @@ impl Bezier { segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(1.)); if segment.is_scalable() { result_beziers.push(segment); - result_t_values.push(t_subcurve_end); + result_t_values.push([t_subcurve_start + t1 * (t_subcurve_end - t_subcurve_start), t_subcurve_end]); } } }); @@ -589,7 +597,7 @@ impl Bezier { mod tests { use super::*; use crate::compare::{compare_arcs, compare_points, compare_vec_of_points}; - use crate::utils::{Joint, TValue}; + use crate::utils::{Cap, TValue}; use crate::EmptyId; #[test] @@ -760,7 +768,7 @@ mod tests { .all(|(bezier1, bezier2)| bezier1.abs_diff_eq(bezier2, MAX_ABSOLUTE_DIFFERENCE))); assert!(reduced_curves .iter() - .zip(helper_t_values.windows(2)) + .zip(helper_t_values.iter()) .all(|(curve, t_pair)| curve.abs_diff_eq(&bezier.trim(TValue::Parametric(t_pair[0]), TValue::Parametric(t_pair[1])), MAX_ABSOLUTE_DIFFERENCE))) } @@ -845,12 +853,70 @@ mod tests { } } + /* + Subpath { closed: false, manipulator_groups: [ManipulatorGroup { anchor: DVec2(66.71743644215402, 58.7517978288361), in_handle: None, out_handle: Some(DVec2(85.76739417779808, 55.1295582286271)) }, ManipulatorGroup { anchor: DVec2(95.39100346020761, 53.23529411764706), in_handle: Some(DVec2(95.37078484879083, 53.241946559250394)), out_handle: Some(DVec2(98.80457794824957, 52.11214057993674)) }, ManipulatorGroup { anchor: DVec2(82.53632686721097, 45.965127722288635), in_handle: Some(DVec2(83.31163513108629, 47.252232925393066)), out_handle: Some(DVec2(81.08946281562602, 43.5631589367744)) }, ManipulatorGroup { anchor: DVec2(80.3913043478261, 38.23534971644612), in_handle: Some(DVec2(80.62889818819768, 40.89454972115455)), out_handle: None }] } + Subpath { closed: false, manipulator_groups: [ManipulatorGroup { anchor: DVec2(29.268661097265387, 35.2507699252331), in_handle: None, out_handle: Some(DVec2(70.73545815536839, 27.576595038028945)) }, ManipulatorGroup { anchor: DVec2(92.0917215493776, 23.603011311070247), in_handle: Some(DVec2(91.67706090780973, 23.69632714594357)), out_handle: Some(DVec2(92.50638219094546, 23.509695476196924)) }, ManipulatorGroup { anchor: DVec2(80.39130434782608, 38.235349716446116), in_handle: Some(DVec2(79.81198607365604, 34.10692809178813)), out_handle: Some(DVec2(80.97062262199611, 42.363771341104105)) }, + ManipulatorGroup { anchor: DVec2(87.81572935960371, 51.1817900801487), in_handle: Some(DVec2(82.51846558453153, 47.007213623741976)), out_handle: Some(DVec2(88.69626225397128, 51.87570528731968)) }, ManipulatorGroup { anchor: DVec2(95.39100346020761, 53.23529411764706), in_handle: Some(DVec2(97.862658087303, 53.104799826374474)), out_handle: Some(DVec2(95.35694765786216, 53.237092138997646)) }, ManipulatorGroup { anchor: DVec2(66.71743644215402, 58.7517978288361), in_handle: Some(DVec2(85.76739417779808, 55.1295582286271)), out_handle: None }] } + Subpath { closed: true, manipulator_groups: [ManipulatorGroup { anchor: DVec2(66.71743644215402, 58.7517978288361), in_handle: None, out_handle: Some(DVec2(85.76739417779808, 55.1295582286271)) }, ManipulatorGroup { anchor: DVec2(95.39100346020761, 53.23529411764706), in_handle: Some(DVec2(95.37078484879083, 53.241946559250394)), out_handle: Some(DVec2(98.80457794824957, 52.11214057993674)) }, ManipulatorGroup { anchor: DVec2(82.53632686721097, 45.965127722288635), in_handle: Some(DVec2(83.31163513108629, 47.252232925393066)), out_handle: Some(DVec2(81.08946281562602, 43.5631589367744)) }, ManipulatorGroup { anchor: DVec2(80.3913043478261, 38.23534971644612), in_handle: Some(DVec2(80.62889818819768, 40.89454972115455)), out_handle: None }, ManipulatorGroup { anchor: DVec2(34.73133890273461, 64.7492300747669), in_handle: None, out_handle: Some(DVec2(76.9410635837621, 56.927752788058)) }, ManipulatorGroup { anchor: DVec2(98.67820888540503, 52.87105163789005), in_handle: Some(DVec2(98.25610430958164, 52.96604166313013)), out_handle: Some(DVec2(99.10031346122841, 52.77606161264997)) }, ManipulatorGroup { anchor: DVec2(110.39130434782608, 38.235349716446116), in_handle: Some(DVec2(110.97062262199611, 42.363789132719795)), out_handle: Some(DVec2(109.81198607365604, 34.10691030017244)) }, ManipulatorGroup { anchor: DVec2(102.96684467379482, 25.288862382878094), in_handle: Some(DVec2(108.2641445692411, 29.463435338945008)), out_handle: Some(DVec2(102.08624313883237, 24.594898396416724)) }, ManipulatorGroup { anchor: DVec2(95.39100346020761, 23.235294117647058), in_handle: Some(DVec2(99.67175329171658, 23.349120314318053)), out_handle: Some(DVec2(95.32104715271026, 23.233433962130153)) }, ManipulatorGroup { anchor: DVec2(61.28256355784598, 29.248202171163904), in_handle: Some(DVec2(83.95809601828032, 25.18416726156898)), out_handle: None }] } + + */ + + #[test] + fn test_outline_ex() { + // TODO: Remove or write actual test + let p1 = DVec2::new(55., 24.); + let p2 = DVec2::new(149., 30.); + let p3 = DVec2::new(150., 30.); + let p4 = DVec2::new(160., 160.); + + // let p1 = DVec2::new(64., 44.); + // let p2 = DVec2::new(140., 30.); + // let p3 = DVec2::new(32., 50.); + let line = Bezier::from_cubic_dvec2(p1, p2, p3, p4); + + // let reduce = line.reduce(None); + // println!("{:?}", reduce); + + let offset_pos = line.offset::(15.); + let mut str1 = String::new(); + offset_pos.to_svg( + &mut str1, + "stroke=\"black\" stroke-width=\"2\" fill=\"none\"".to_string(), + String::new(), + String::new(), + "r=\"3\" stroke=\"gray\" stroke-width=\"1.5\" fill=\"white\"".to_string(), + ); + println!("{}", str1); + + // let offset_neg = line.reverse().offset::(15.); + // let mut str2 = String::new(); + // offset_neg.to_svg( + // &mut str2, + // "stroke=\"black\" stroke-width=\"2\" fill=\"none\"".to_string(), + // String::new(), + // String::new(), + // "r=\"3\" stroke=\"gray\" stroke-width=\"1.5\" fill=\"white\"".to_string(), + // ); + // println!("{}", str2); + + // let outline = line.outline::(15., Cap::Butt); + // let mut str3 = String::new(); + // outline.to_svg( + // &mut str3, + // "stroke=\"black\" stroke-width=\"2\" fill=\"none\"".to_string(), + // String::new(), + // String::new(), + // "r=\"3\" stroke=\"gray\" stroke-width=\"1.5\" fill=\"white\"".to_string(), + // ); + // println!("{}", str3); + } + #[test] fn test_outline() { let p1 = DVec2::new(30., 50.); let p2 = DVec2::new(140., 30.); let line = Bezier::from_linear_dvec2(p1, p2); - let outline = line.outline::(10., Joint::Bevel); + let outline = line.outline::(10., Cap::Butt); assert_eq!(outline.len(), 4); diff --git a/libraries/bezier-rs/src/lib.rs b/libraries/bezier-rs/src/lib.rs index b57b7b2a302..cc74ff03a0f 100644 --- a/libraries/bezier-rs/src/lib.rs +++ b/libraries/bezier-rs/src/lib.rs @@ -8,4 +8,4 @@ mod utils; pub use bezier::*; pub use subpath::*; -pub use utils::{Cap, Joint, SubpathTValue, TValue}; +pub use utils::{Cap, Join, SubpathTValue, TValue}; diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 28cc9c6647c..a033ad167bc 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -231,6 +231,18 @@ impl Subpath { right + center_to_right.perp() * HANDLE_OFFSET_FACTOR, ) } + + pub(crate) fn square_cap(&self, other: &Subpath) -> [ManipulatorGroup; 2] { + let left = self.manipulator_groups[self.len() - 1].anchor; + let right = other.manipulator_groups[0].anchor; + + let center = (right + left) / 2.; + let center_to_right = right - center; + + let translation = center_to_right.perp(); + + [ManipulatorGroup::new_anchor(left + translation), ManipulatorGroup::new_anchor(right + translation)] + } } #[cfg(test)] @@ -264,14 +276,14 @@ mod tests { let result = Subpath::new( vec![ ManipulatorGroup { - anchor: pos_offset.evaluate(1.), + anchor: pos_offset.evaluate(SubpathTValue::GlobalParametric(1.)), out_handle: Some(out_handle), in_handle: None, id: EmptyId, }, manip.clone(), ManipulatorGroup { - anchor: neg_offset.evaluate(0.), + anchor: neg_offset.evaluate(SubpathTValue::GlobalParametric(0.)), out_handle: None, in_handle: Some(in_handle), id: EmptyId, diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index ff1e6374bda..4b76c833676 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -2,7 +2,7 @@ use std::vec; use super::*; use crate::consts::MAX_ABSOLUTE_DIFFERENCE; -use crate::utils::{Cap, Joint, SubpathTValue, TValue}; +use crate::utils::{Cap, Join, SubpathTValue, TValue}; use glam::DAffine2; @@ -278,6 +278,9 @@ impl Subpath { /// Smooths a Subpath up to the first derivative, using a weighted averaged based on segment length. /// The Subpath must be open, and contain no quadratic segments. pub(crate) fn smooth_open_subpath(&mut self) { + if self.len() < 2 { + return; + } for i in 1..self.len() - 1 { let first_bezier = self.manipulator_groups[i - 1].to_bezier(&self.manipulator_groups[i]); let second_bezier = self.manipulator_groups[i].to_bezier(&self.manipulator_groups[i + 1]); @@ -326,9 +329,9 @@ impl 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 `joint` argument. + /// The intersections of segments of the subpath are joined using the method specified by the `join` argument. /// - pub fn offset(&self, distance: f64, joint: Joint) -> Subpath { + pub fn offset(&self, distance: f64, join: Join) -> Subpath { assert!(self.len_segments() > 1, "Cannot offset an empty Subpath."); // An offset at a distance 0 from the curve is simply the same curve @@ -359,7 +362,7 @@ impl Subpath { let angle = out_tangent.angle_between(in_tangent); // The angle is concave. The Subpath overlap and must be clipped - let mut apply_joint = true; + let mut apply_join = true; if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) { // If the distance is large enough, there may still be no intersections. Also, if the angle is close enough to zero, // subpath intersections may find no intersections. In this case, the points are likely close enough that we can approximate @@ -367,23 +370,23 @@ impl Subpath { if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(subpath1, subpath2) { subpaths[i] = clipped_subpath1; subpaths[j] = clipped_subpath2; - apply_joint = false; + apply_join = false; } } - // The angle is convex. The Subpath must be joined using the specified Joint type - if apply_joint { - match joint { - Joint::Bevel => { + // The angle is convex. The Subpath must be joined using the specified join type + if apply_join { + match join { + Join::Bevel => { drop_common_point[j] = false; } - Joint::Miter => { + Join::Miter => { let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j]); if let Some(miter_manipulator_group) = miter_manipulator_group { subpaths[i].manipulator_groups.push(miter_manipulator_group); } drop_common_point[j] = false; } - Joint::Round => { + Join::Round => { let (out_handle, round_point, in_handle) = subpaths[i].round_line_join(&subpaths[j], self.manipulator_groups[j].anchor); let last_index = subpaths[i].manipulator_groups.len() - 1; subpaths[i].manipulator_groups[last_index].out_handle = Some(out_handle); @@ -404,22 +407,22 @@ impl Subpath { let in_tangent = self.get_segment(0).unwrap().tangent(TValue::Parametric(0.)); let angle = out_tangent.angle_between(in_tangent); - let mut apply_joint = true; + let mut apply_join = true; if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) { if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(&subpaths[subpaths.len() - 1], &subpaths[0]) { // Merge the clipped subpaths let last_index = subpaths.len() - 1; subpaths[last_index] = clipped_subpath1; subpaths[0] = clipped_subpath2; - apply_joint = false; + apply_join = false; } } - if apply_joint { - match joint { - Joint::Bevel => { + if apply_join { + match join { + Join::Bevel => { drop_common_point[0] = false; } - Joint::Miter => { + Join::Miter => { let last_subpath_index = subpaths.len() - 1; let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0]); if let Some(miter_manipulator_group) = miter_manipulator_group { @@ -427,7 +430,7 @@ impl Subpath { } drop_common_point[0] = false; } - Joint::Round => { + Join::Round => { let last_subpath_index = subpaths.len() - 1; let (out_handle, round_point, in_handle) = subpaths[last_subpath_index].round_line_join(&subpaths[0], self.manipulator_groups[0].anchor); let last_index = subpaths[last_subpath_index].manipulator_groups.len() - 1; @@ -466,6 +469,9 @@ impl Subpath { let mut result_manipulator_groups: Vec> = vec![]; result_manipulator_groups.extend_from_slice(self.manipulator_groups()); match cap { + Cap::Butt => { + result_manipulator_groups.extend_from_slice(other.manipulator_groups()); + } Cap::Round => { let last_index = result_manipulator_groups.len() - 1; let (out_handle, round_point, in_handle) = self.round_cap(other); @@ -480,8 +486,12 @@ impl Subpath { result_manipulator_groups.push(round_point); result_manipulator_groups[0].in_handle = Some(in_handle); } - _ => { + Cap::Square => { + let square_points = self.square_cap(other); + result_manipulator_groups.extend_from_slice(&square_points); result_manipulator_groups.extend_from_slice(other.manipulator_groups()); + let square_points = other.square_cap(self); + result_manipulator_groups.extend_from_slice(&square_points); } } Subpath::new(result_manipulator_groups, true) @@ -491,11 +501,11 @@ impl Subpath { /// Outline returns a single closed subpath (if the original subpath was open) or two closed subpaths (if the original subpath was closed) that forms /// an approximate outline around the subpath at a specified distance from the curve. Outline takes the following parameters: /// - `distance` - The outline's distance from the curve. - /// - `joint` - The joint type used to cap the endpoints of open bezier curves, and join successive subpath segments. + /// - `join` - The join type used to cap the endpoints of open bezier curves, and join successive subpath segments. /// - pub fn outline(&self, distance: f64, joint: Joint, cap: Cap) -> (Subpath, Option>) { - let pos_offset = self.offset(distance, joint); - let neg_offset = self.reverse().offset(distance, joint); + pub fn outline(&self, distance: f64, join: Join, cap: Cap) -> (Subpath, Option>) { + let pos_offset = self.offset(distance, join); + let neg_offset = self.reverse().offset(distance, join); if self.closed { return (pos_offset, Some(neg_offset)); diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs index 9355df0f5ce..c2d83707a27 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -29,7 +29,7 @@ pub enum SubpathTValue { } #[derive(Copy, Clone)] -pub enum Joint { +pub enum Join { Bevel, Miter, Round, 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 773481650e2..37b645a809d 100644 --- a/website/other/bezier-rs-demos/src/features/subpath-features.ts +++ b/website/other/bezier-rs-demos/src/features/subpath-features.ts @@ -107,7 +107,7 @@ const subpathFeatures = { }, offset: { name: "Offset", - callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.offset(options.distance, options.joint), + callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.offset(options.distance, options.join), inputOptions: [ { variable: "distance", @@ -117,7 +117,7 @@ const subpathFeatures = { default: 10, }, { - variable: "joint", + variable: "join", min: 0, max: 2, step: 1, @@ -128,7 +128,7 @@ const subpathFeatures = { }, outline: { name: "Outline", - callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.outline(options.distance, options.joint, options.cap), + callback: (subpath: WasmSubpathInstance, options: Record): string => subpath.outline(options.distance, options.join, options.cap), inputOptions: [ { variable: "distance", @@ -138,7 +138,7 @@ const subpathFeatures = { default: 10, }, { - variable: "joint", + variable: "join", min: 0, max: 2, step: 1, diff --git a/website/other/bezier-rs-demos/wasm/src/bezier.rs b/website/other/bezier-rs-demos/wasm/src/bezier.rs index 24e068895c2..de6c0585d43 100644 --- a/website/other/bezier-rs-demos/wasm/src/bezier.rs +++ b/website/other/bezier-rs-demos/wasm/src/bezier.rs @@ -221,22 +221,25 @@ impl WasmBezier { } pub fn curvature(&self, raw_t: f64, t_variant: String) -> String { - let bezier = self.get_bezier_path(); + let mut content = self.get_bezier_path(); let t = parse_t_variant(&t_variant, raw_t); - let radius = 1. / self.0.curvature(t); - let normal_point = self.0.normal(t); - let intersection_point = self.0.evaluate(t); - - let curvature_center = intersection_point + normal_point * radius; - - let 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), - ); + 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 curvature_center = intersection_point + normal_point * radius; + + content = format!( + "{content}{}{}{}{}", + 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) } diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index f3904ab5f72..d8d713a3ea6 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -1,5 +1,5 @@ use crate::svg_drawing::*; -use crate::utils::{parse_cap, parse_joint}; +use crate::utils::{parse_cap, parse_join}; use bezier_rs::{Bezier, ManipulatorGroup, ProjectionOptions, Subpath, SubpathTValue}; @@ -378,9 +378,9 @@ impl WasmSubpath { wrap_svg_tag(format!("{}{}", self.to_default_svg(), trimmed_subpath_svg)) } - pub fn offset(&self, distance: f64, joint: i32) -> String { - let joint = parse_joint(joint); - let offset_subpath = self.0.offset(distance, joint); + pub fn offset(&self, distance: f64, join: i32) -> String { + let join = parse_join(join); + let offset_subpath = self.0.offset(distance, join); let mut offset_svg = String::new(); offset_subpath.to_svg(&mut offset_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); @@ -388,10 +388,10 @@ impl WasmSubpath { wrap_svg_tag(format!("{}{offset_svg}", self.to_default_svg())) } - pub fn outline(&self, distance: f64, joint: i32, cap: i32) -> String { - let joint = parse_joint(joint); + pub fn outline(&self, distance: f64, join: i32, cap: i32) -> String { + let join = parse_join(join); let cap = parse_cap(cap); - let (outline_piece1, outline_piece2) = self.0.outline(distance, joint, cap); + let (outline_piece1, outline_piece2) = self.0.outline(distance, join, cap); let mut outline_piece1_svg = String::new(); outline_piece1.to_svg(&mut outline_piece1_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()); diff --git a/website/other/bezier-rs-demos/wasm/src/utils.rs b/website/other/bezier-rs-demos/wasm/src/utils.rs index 3ddc75b1448..58dc1329000 100644 --- a/website/other/bezier-rs-demos/wasm/src/utils.rs +++ b/website/other/bezier-rs-demos/wasm/src/utils.rs @@ -1,11 +1,11 @@ -use bezier_rs::{Cap, Joint}; +use bezier_rs::{Cap, Join}; -pub fn parse_joint(joint: i32) -> Joint { - match joint { - 0 => Joint::Bevel, - 1 => Joint::Miter, - 2 => Joint::Round, - _ => panic!("Unexpected Joint value: '{}'", joint), +pub fn parse_join(join: i32) -> Join { + match join { + 0 => Join::Bevel, + 1 => Join::Miter, + 2 => Join::Round, + _ => panic!("Unexpected Join value: '{}'", join), } } From 1040814beaaac626e98bfa324000b49f46808708 Mon Sep 17 00:00:00 2001 From: Hannah Li Date: Wed, 15 Mar 2023 17:14:01 -0400 Subject: [PATCH 06/11] Fix single point issue --- libraries/bezier-rs/src/bezier/core.rs | 8 + libraries/bezier-rs/src/bezier/transform.rs | 111 +++------- libraries/bezier-rs/src/subpath/core.rs | 2 +- libraries/bezier-rs/src/subpath/solvers.rs | 216 ++++++++++++++----- libraries/bezier-rs/src/subpath/transform.rs | 46 +++- 5 files changed, 247 insertions(+), 136 deletions(-) diff --git a/libraries/bezier-rs/src/bezier/core.rs b/libraries/bezier-rs/src/bezier/core.rs index bc39125982b..7d6e7c5310f 100644 --- a/libraries/bezier-rs/src/bezier/core.rs +++ b/libraries/bezier-rs/src/bezier/core.rs @@ -211,6 +211,14 @@ impl Bezier { self_points.len() == other_points.len() && self_points.into_iter().zip(other_points.into_iter()).all(|(a, b)| a.abs_diff_eq(b, max_abs_diff)) } + + /// Returns true if the start, end and handles of the Bezier are all at the same location + pub fn is_single_point(&self) -> bool { + let points = self.get_points().collect::>(); + let start = self.start(); + + points.iter().all(|point| point.abs_diff_eq(start, MAX_ABSOLUTE_DIFFERENCE)) + } } #[cfg(test)] diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs index 0928119dca4..9d92ba7a5d2 100644 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ b/libraries/bezier-rs/src/bezier/transform.rs @@ -166,8 +166,8 @@ impl Bezier { pub(crate) fn get_extrema_t_list(&self) -> Vec { let mut extrema = self.local_extrema().into_iter().flatten().collect::>(); extrema.append(&mut vec![0., 1.]); - extrema.dedup(); extrema.sort_by(|ex1, ex2| ex1.partial_cmp(ex2).unwrap()); + extrema.dedup(); extrema } @@ -361,10 +361,12 @@ impl Bezier { let mut scaled = Subpath::new(vec![], false); reduced.iter().enumerate().for_each(|(index, bezier)| { let scaled_bezier = bezier.scale(distance); - if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) { - scaled.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE)); - } else { - scaled.append_bezier(&scaled_bezier, AppendType::IgnoreStart); + if !bezier.is_single_point() { + if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) { + scaled.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE)); + } else { + scaled.append_bezier(&scaled_bezier, AppendType::IgnoreStart); + } } }); @@ -384,19 +386,24 @@ impl Bezier { let mut next_start_distance = start_distance; let distance_difference = end_distance - start_distance; let total_length = self.length(None); + if total_length < MAX_ABSOLUTE_DIFFERENCE { + return Subpath::new(vec![], false); + } let mut result = Subpath::new(vec![], false); reduced.iter().enumerate().for_each(|(index, bezier)| { - let current_length = bezier.length(None); - let next_end_distance = next_start_distance + (current_length / total_length) * distance_difference; - let scaled_bezier = bezier.graduated_scale(next_start_distance, next_end_distance); - - if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) { - result.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE)); - } else { - result.append_bezier(&scaled_bezier, AppendType::IgnoreStart); + if !bezier.is_single_point() { + let current_length = bezier.length(None); + let next_end_distance = next_start_distance + (current_length / total_length) * distance_difference; + let scaled_bezier = bezier.graduated_scale(next_start_distance, next_end_distance); + + if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) { + result.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE)); + } else { + result.append_bezier(&scaled_bezier, AppendType::IgnoreStart); + } + next_start_distance = next_end_distance; } - next_start_distance = next_end_distance; }); // If the curve is not linear, smooth the handles. All segments produced by bezier::scale will be cubic. @@ -596,7 +603,7 @@ impl Bezier { #[cfg(test)] mod tests { use super::*; - use crate::compare::{compare_arcs, compare_points, compare_vec_of_points}; + use crate::compare::{compare_arcs, compare_points}; use crate::utils::{Cap, TValue}; use crate::EmptyId; @@ -748,17 +755,8 @@ mod tests { let p3 = DVec2::new(0., 0.); let bezier = Bezier::from_quadratic_dvec2(p1, p2, p3); - let expected_bezier_points = vec![ - vec![DVec2::new(0., 0.), DVec2::new(0.5, 0.5), DVec2::new(0.989, 0.989)], - vec![DVec2::new(0.989, 0.989), DVec2::new(2.705, 2.705), DVec2::new(4.2975, 4.2975)], - vec![DVec2::new(4.2975, 4.2975), DVec2::new(5.6625, 5.6625), DVec2::new(6.9375, 6.9375)], - ]; let reduced_curves = bezier.reduce(None); - assert!(reduced_curves.iter().zip(expected_bezier_points.into_iter()).all(|(bezier, points)| compare_vec_of_points( - bezier.get_points().collect::>(), - points, - MAX_ABSOLUTE_DIFFERENCE - ))); + assert!(reduced_curves.iter().all(|bezier| bezier.is_scalable())); // Check that the reduce helper is correct let (helper_curves, helper_t_values) = bezier.reduced_curves_and_t_values(None); @@ -853,62 +851,21 @@ mod tests { } } - /* - Subpath { closed: false, manipulator_groups: [ManipulatorGroup { anchor: DVec2(66.71743644215402, 58.7517978288361), in_handle: None, out_handle: Some(DVec2(85.76739417779808, 55.1295582286271)) }, ManipulatorGroup { anchor: DVec2(95.39100346020761, 53.23529411764706), in_handle: Some(DVec2(95.37078484879083, 53.241946559250394)), out_handle: Some(DVec2(98.80457794824957, 52.11214057993674)) }, ManipulatorGroup { anchor: DVec2(82.53632686721097, 45.965127722288635), in_handle: Some(DVec2(83.31163513108629, 47.252232925393066)), out_handle: Some(DVec2(81.08946281562602, 43.5631589367744)) }, ManipulatorGroup { anchor: DVec2(80.3913043478261, 38.23534971644612), in_handle: Some(DVec2(80.62889818819768, 40.89454972115455)), out_handle: None }] } - Subpath { closed: false, manipulator_groups: [ManipulatorGroup { anchor: DVec2(29.268661097265387, 35.2507699252331), in_handle: None, out_handle: Some(DVec2(70.73545815536839, 27.576595038028945)) }, ManipulatorGroup { anchor: DVec2(92.0917215493776, 23.603011311070247), in_handle: Some(DVec2(91.67706090780973, 23.69632714594357)), out_handle: Some(DVec2(92.50638219094546, 23.509695476196924)) }, ManipulatorGroup { anchor: DVec2(80.39130434782608, 38.235349716446116), in_handle: Some(DVec2(79.81198607365604, 34.10692809178813)), out_handle: Some(DVec2(80.97062262199611, 42.363771341104105)) }, - ManipulatorGroup { anchor: DVec2(87.81572935960371, 51.1817900801487), in_handle: Some(DVec2(82.51846558453153, 47.007213623741976)), out_handle: Some(DVec2(88.69626225397128, 51.87570528731968)) }, ManipulatorGroup { anchor: DVec2(95.39100346020761, 53.23529411764706), in_handle: Some(DVec2(97.862658087303, 53.104799826374474)), out_handle: Some(DVec2(95.35694765786216, 53.237092138997646)) }, ManipulatorGroup { anchor: DVec2(66.71743644215402, 58.7517978288361), in_handle: Some(DVec2(85.76739417779808, 55.1295582286271)), out_handle: None }] } - Subpath { closed: true, manipulator_groups: [ManipulatorGroup { anchor: DVec2(66.71743644215402, 58.7517978288361), in_handle: None, out_handle: Some(DVec2(85.76739417779808, 55.1295582286271)) }, ManipulatorGroup { anchor: DVec2(95.39100346020761, 53.23529411764706), in_handle: Some(DVec2(95.37078484879083, 53.241946559250394)), out_handle: Some(DVec2(98.80457794824957, 52.11214057993674)) }, ManipulatorGroup { anchor: DVec2(82.53632686721097, 45.965127722288635), in_handle: Some(DVec2(83.31163513108629, 47.252232925393066)), out_handle: Some(DVec2(81.08946281562602, 43.5631589367744)) }, ManipulatorGroup { anchor: DVec2(80.3913043478261, 38.23534971644612), in_handle: Some(DVec2(80.62889818819768, 40.89454972115455)), out_handle: None }, ManipulatorGroup { anchor: DVec2(34.73133890273461, 64.7492300747669), in_handle: None, out_handle: Some(DVec2(76.9410635837621, 56.927752788058)) }, ManipulatorGroup { anchor: DVec2(98.67820888540503, 52.87105163789005), in_handle: Some(DVec2(98.25610430958164, 52.96604166313013)), out_handle: Some(DVec2(99.10031346122841, 52.77606161264997)) }, ManipulatorGroup { anchor: DVec2(110.39130434782608, 38.235349716446116), in_handle: Some(DVec2(110.97062262199611, 42.363789132719795)), out_handle: Some(DVec2(109.81198607365604, 34.10691030017244)) }, ManipulatorGroup { anchor: DVec2(102.96684467379482, 25.288862382878094), in_handle: Some(DVec2(108.2641445692411, 29.463435338945008)), out_handle: Some(DVec2(102.08624313883237, 24.594898396416724)) }, ManipulatorGroup { anchor: DVec2(95.39100346020761, 23.235294117647058), in_handle: Some(DVec2(99.67175329171658, 23.349120314318053)), out_handle: Some(DVec2(95.32104715271026, 23.233433962130153)) }, ManipulatorGroup { anchor: DVec2(61.28256355784598, 29.248202171163904), in_handle: Some(DVec2(83.95809601828032, 25.18416726156898)), out_handle: None }] } - - */ - #[test] - fn test_outline_ex() { - // TODO: Remove or write actual test - let p1 = DVec2::new(55., 24.); - let p2 = DVec2::new(149., 30.); + fn test_offset_curve_that_has_a_single_point_after_reduce() { + let p1 = DVec2::new(30., 30.); + let p2 = DVec2::new(150., 29.); let p3 = DVec2::new(150., 30.); let p4 = DVec2::new(160., 160.); - // let p1 = DVec2::new(64., 44.); - // let p2 = DVec2::new(140., 30.); - // let p3 = DVec2::new(32., 50.); - let line = Bezier::from_cubic_dvec2(p1, p2, p3, p4); - - // let reduce = line.reduce(None); - // println!("{:?}", reduce); - - let offset_pos = line.offset::(15.); - let mut str1 = String::new(); - offset_pos.to_svg( - &mut str1, - "stroke=\"black\" stroke-width=\"2\" fill=\"none\"".to_string(), - String::new(), - String::new(), - "r=\"3\" stroke=\"gray\" stroke-width=\"1.5\" fill=\"white\"".to_string(), - ); - println!("{}", str1); - - // let offset_neg = line.reverse().offset::(15.); - // let mut str2 = String::new(); - // offset_neg.to_svg( - // &mut str2, - // "stroke=\"black\" stroke-width=\"2\" fill=\"none\"".to_string(), - // String::new(), - // String::new(), - // "r=\"3\" stroke=\"gray\" stroke-width=\"1.5\" fill=\"white\"".to_string(), - // ); - // println!("{}", str2); - - // let outline = line.outline::(15., Cap::Butt); - // let mut str3 = String::new(); - // outline.to_svg( - // &mut str3, - // "stroke=\"black\" stroke-width=\"2\" fill=\"none\"".to_string(), - // String::new(), - // String::new(), - // "r=\"3\" stroke=\"gray\" stroke-width=\"1.5\" fill=\"white\"".to_string(), - // ); - // println!("{}", str3); + let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4); + + let reduce = bezier.reduce(None); + let offset = bezier.offset::(15.); + assert!(reduce.last().is_some()); + assert!(reduce.last().unwrap().is_single_point()); + // Expect the single point bezier to be dropped in the offset + assert_eq!(reduce.len(), offset.len_segments() + 1); } #[test] diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index de6768aecf9..e2895e7c694 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -88,7 +88,7 @@ impl Subpath { /// Returns the number of segments contained within the `Subpath`. pub fn len_segments(&self) -> usize { let mut number_of_curves = self.len(); - if !self.closed { + if !self.closed && number_of_curves > 0 { number_of_curves -= 1 } number_of_curves diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index a033ad167bc..8621deb122f 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -1,6 +1,6 @@ use super::*; use crate::consts::MAX_ABSOLUTE_DIFFERENCE; -use crate::utils::{f64_compare, line_intersection, SubpathTValue}; +use crate::utils::{line_intersection, SubpathTValue}; use crate::TValue; use glam::{DMat2, DVec2}; @@ -176,20 +176,21 @@ impl Subpath { let center_to_left = left - center; let in_segment = self.get_segment(self.len_segments() - 1).unwrap(); - let tangent_angle = center_to_right.angle_between(in_segment.tangent(TValue::Parametric(1.))); + let in_tangent = in_segment.tangent(TValue::Parametric(1.)); + let tangent_angle = (right - left).angle_between(in_tangent); let mut angle = center_to_right.angle_between(center_to_left) / 2.; - - if f64_compare(angle.abs(), PI / 2., MAX_ABSOLUTE_DIFFERENCE) && tangent_angle * angle < 0. { - angle = -angle; + if tangent_angle * angle < 0. { + println!("First fix"); + angle = (2. * angle - PI * (if angle < 0. { -1. } else { 1. })) / 2.; // (if angle < 0. { -1. } else { 1. })) } - // if (0. < tangent_angle && tangent_angle < PI && -PI < angle && angle < 0.) || (0. < angle && angle < PI && -PI < tangent_angle && tangent_angle < 0.) { - // angle = -angle; - // println!("angle: {}, tangent_angle: {}", angle, tangent_angle); - // } + let mut arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right); - let rotation_matrix = DMat2::from_angle(angle); - let arc_point = center + rotation_matrix.mul_vec2(center_to_right); + if (arc_point - left).angle_between(in_tangent).abs() > PI / 2. { + println!("Final fix"); + // angle = (PI - 2. * angle) / 2.; + // arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right); + } let center_to_arc_point = arc_point - center; @@ -258,51 +259,6 @@ mod tests { t * (n as f64) % 1. } - #[test] - fn round_join() { - // TODO: Remove or write actual test - let s1 = DVec2::new(163., 61.); - let h1 = DVec2::new(140., 30.); - let e1 = DVec2::new(91., 177.); - - let bezier = Bezier::from_quadratic_dvec2(s1, h1, e1); - - let pos_offset = bezier.offset::(15.); - let neg_offset = bezier.reverse().offset::(15.); - - println!("test:{}", DVec2::new(0., 1.).angle_between(DVec2::new(1., 0.))); - - let (out_handle, manip, in_handle) = pos_offset.round_line_join(&neg_offset, e1); - let result = Subpath::new( - vec![ - ManipulatorGroup { - anchor: pos_offset.evaluate(SubpathTValue::GlobalParametric(1.)), - out_handle: Some(out_handle), - in_handle: None, - id: EmptyId, - }, - manip.clone(), - ManipulatorGroup { - anchor: neg_offset.evaluate(SubpathTValue::GlobalParametric(0.)), - out_handle: None, - in_handle: Some(in_handle), - id: EmptyId, - }, - ], - false, - ); - let mut str = String::new(); - result.to_svg( - &mut str, - "stroke=\"black\" stroke-width=\"2\" fill=\"none\"".to_string(), - String::new(), - String::new(), - "stroke=\"red\" stroke-width=\"1\" fill=\"none\"".to_string(), - ); - println!("{:?}", result); - println!("{}", str); - } - #[test] fn evaluate_one_subpath_curve() { let start = DVec2::new(20., 30.); @@ -670,4 +626,152 @@ mod tests { } // TODO: add more intersection tests + + #[test] + fn round_join_counter_clockwise_rotation() { + let subpath = Subpath::new( + vec![ + ManipulatorGroup { + anchor: DVec2::new(20., 20.), + out_handle: Some(DVec2::new(10., 90.)), + in_handle: None, + id: EmptyId, + }, + ManipulatorGroup { + anchor: DVec2::new(114., 159.), + out_handle: None, + in_handle: Some(DVec2::new(60., 40.)), + id: EmptyId, + }, + ManipulatorGroup { + anchor: DVec2::new(148., 155.), + out_handle: None, + in_handle: None, + id: EmptyId, + }, + ], + false, + ); + + let offset = subpath.offset(10., utils::Join::Round); + let offset_len = offset.len(); + + let manipulator_groups = offset.manipulator_groups(); + let round_start = manipulator_groups[offset_len - 4].anchor; + let round_point = manipulator_groups[offset_len - 3].anchor; + let round_end = manipulator_groups[offset_len - 2].anchor; + + let middle = (round_start + round_end) / 2.; + + assert!((round_point - middle).angle_between(round_start - middle) > 0.); + assert!((round_end - middle).angle_between(round_point - middle) > 0.); + } + + #[test] + fn round_join_clockwise_rotation() { + let subpath = Subpath::new( + vec![ + ManipulatorGroup { + anchor: DVec2::new(20., 20.), + out_handle: Some(DVec2::new(10., 90.)), + in_handle: None, + id: EmptyId, + }, + ManipulatorGroup { + anchor: DVec2::new(150., 40.), + out_handle: None, + in_handle: Some(DVec2::new(60., 40.)), + id: EmptyId, + }, + ManipulatorGroup { + anchor: DVec2::new(78., 36.), + out_handle: None, + in_handle: None, + id: EmptyId, + }, + ], + false, + ); + + let offset = subpath.offset(-15., utils::Join::Round); + let offset_len = offset.len(); + + let manipulator_groups = offset.manipulator_groups(); + let round_start = manipulator_groups[offset_len - 4].anchor; + let round_point = manipulator_groups[offset_len - 3].anchor; + let round_end = manipulator_groups[offset_len - 2].anchor; + + let middle = (round_start + round_end) / 2.; + + assert!((round_point - middle).angle_between(round_start - middle) < 0.); + assert!((round_end - middle).angle_between(round_point - middle) < 0.); + } + + #[test] + fn round_join_test() { + // TODO: Case where reduce ends in a really short segment + // Results in a really weird in_tangent for the round join + //M38 35 C40 40 120 120 130 30 Q175 90 145 150 Q70 185 38 35 Z + let subpath = Subpath::new( + vec![ + ManipulatorGroup { + anchor: DVec2::new(145., 150.), + out_handle: Some(DVec2::new(70., 185.)), + in_handle: None, + id: EmptyId, + }, + ManipulatorGroup { + anchor: DVec2::new(38., 35.), + out_handle: Some(DVec2::new(40., 40.)), + in_handle: None, + id: EmptyId, + }, + ManipulatorGroup { + anchor: DVec2::new(130., 30.), + out_handle: Some(DVec2::new(175., 90.)), + in_handle: Some(DVec2::new(120., 120.)), + id: EmptyId, + }, + ], + false, + ); + + let offset = subpath.reverse().offset(10., utils::Join::Round); + let mut str = String::new(); + offset.to_svg(&mut str, "stroke=\"black\" stroke-width=\"2\" fill=\"none\"".to_string(), String::new(), String::new(), String::new()); + println!("{}", str); + } + + #[test] + fn round_join_test_2() { + // TODO: Case where almost linear join gets wonky?? + let subpath = Subpath::new( + vec![ + ManipulatorGroup { + anchor: DVec2::new(150., 40.), + out_handle: None, + in_handle: None, + id: EmptyId, + }, + ManipulatorGroup { + anchor: DVec2::new(111., 83.), + out_handle: Some(DVec2::new(57., 146.)), + in_handle: None, + id: EmptyId, + }, + ManipulatorGroup { + anchor: DVec2::new(37., 150.), + out_handle: None, + in_handle: None, + id: EmptyId, + }, + ], + false, + ); + + let offset = subpath.offset(-10., utils::Join::Round); + let mut str = String::new(); + offset.to_svg(&mut str, "stroke=\"black\" stroke-width=\"2\" fill=\"none\"".to_string(), String::new(), String::new(), String::new()); + println!("{}", str); + } } diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index 4b76c833676..35395127355 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -335,11 +335,16 @@ impl Subpath { assert!(self.len_segments() > 1, "Cannot offset an empty Subpath."); // An offset at a distance 0 from the curve is simply the same curve - if distance == 0. { + // An offset of a single point is not defined + if distance == 0. || self.len_segments() == 1 { return self.clone(); } - let mut subpaths = self.iter().map(|bezier| bezier.offset(distance)).collect::>>(); + let mut subpaths = self + .iter() + .filter(|bezier| !bezier.is_single_point()) + .map(|bezier| bezier.offset(distance)) + .collect::>>(); let mut drop_common_point = vec![true; self.len()]; // Clip or join consecutive Subpaths @@ -571,6 +576,43 @@ mod tests { subpath } + #[test] + fn outline_with_single_point_segment() { + let subpath = Subpath::new( + vec![ + ManipulatorGroup { + anchor: DVec2::new(20., 20.), + out_handle: Some(DVec2::new(10., 90.)), + in_handle: None, + id: EmptyId, + }, + ManipulatorGroup { + anchor: DVec2::new(150., 40.), + out_handle: None, + in_handle: Some(DVec2::new(60., 40.)), + id: EmptyId, + }, + ManipulatorGroup { + anchor: DVec2::new(150., 40.), + out_handle: Some(DVec2::new(40., 120.)), + in_handle: None, + id: EmptyId, + }, + ManipulatorGroup { + anchor: DVec2::new(100., 100.), + out_handle: None, + in_handle: None, + id: EmptyId, + }, + ], + false, + ); + + let outline = subpath.outline(10., crate::Join::Round, crate::Cap::Round).0; + assert_eq!(outline.len(), 25); + assert_eq!(outline.closed(), true); + } + #[test] fn split_an_open_subpath() { let subpath = set_up_open_subpath(); From 0c01ffbdb224bb556499271db266db5cdd95d43b Mon Sep 17 00:00:00 2001 From: Hannah Li Date: Fri, 17 Mar 2023 22:35:30 -0400 Subject: [PATCH 07/11] Clean up --- libraries/bezier-rs/src/subpath/solvers.rs | 31 +++++++++++++--------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 8621deb122f..3c1a587a92d 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -167,7 +167,11 @@ impl Subpath { None } - /// Returns the subpath that creates a round join with the provided center. + /// Returns the necessary information to create a round join with the provided center. + /// The returned items correspond to: + /// - The `out_handle` for the last manipulator group of `self` + /// - The new manipulator group to be added + /// - The `in_handle` for the first manipulator group of `other` pub(crate) fn round_line_join(&self, other: &Subpath, center: DVec2) -> (DVec2, ManipulatorGroup, DVec2) { let left = self.manipulator_groups[self.len() - 1].anchor; let right = other.manipulator_groups[0].anchor; @@ -177,19 +181,13 @@ impl Subpath { let in_segment = self.get_segment(self.len_segments() - 1).unwrap(); let in_tangent = in_segment.tangent(TValue::Parametric(1.)); - let tangent_angle = (right - left).angle_between(in_tangent); let mut angle = center_to_right.angle_between(center_to_left) / 2.; - if tangent_angle * angle < 0. { - println!("First fix"); - angle = (2. * angle - PI * (if angle < 0. { -1. } else { 1. })) / 2.; // (if angle < 0. { -1. } else { 1. })) - } let mut arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right); if (arc_point - left).angle_between(in_tangent).abs() > PI / 2. { - println!("Final fix"); - // angle = (PI - 2. * angle) / 2.; - // arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right); + angle = angle - PI * (if angle < 0. { -1. } else { 1. }); + arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right); } let center_to_arc_point = arc_point - center; @@ -208,6 +206,11 @@ impl Subpath { ) } + /// Returns the necessary information to create a round cap between the end of `self` and the beginning of `other`. + /// The returned items correspond to: + /// - The `out_handle` for the last manipulator group of `self` + /// - The new manipulator group to be added + /// - The `in_handle` for the first manipulator group of `other` pub(crate) fn round_cap(&self, other: &Subpath) -> (DVec2, ManipulatorGroup, DVec2) { // Based on https://pomax.github.io/bezierinfo/#circles_cubic const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014; @@ -233,6 +236,7 @@ impl Subpath { ) } + /// Returns the two manipulator groups that create a sqaure cap between the end of `self` and the beginning of `other`. pub(crate) fn square_cap(&self, other: &Subpath) -> [ManipulatorGroup; 2] { let left = self.manipulator_groups[self.len() - 1].anchor; let right = other.manipulator_groups[0].anchor; @@ -745,6 +749,7 @@ mod tests { #[test] fn round_join_test_2() { // TODO: Case where almost linear join gets wonky?? + // M20 20 C10 90 60 40 150 40 L 73 92 Q3 131 100 100 let subpath = Subpath::new( vec![ ManipulatorGroup { @@ -754,13 +759,13 @@ mod tests { id: EmptyId, }, ManipulatorGroup { - anchor: DVec2::new(111., 83.), - out_handle: Some(DVec2::new(57., 146.)), + anchor: DVec2::new(73., 92.), + out_handle: Some(DVec2::new(3., 131.)), in_handle: None, id: EmptyId, }, ManipulatorGroup { - anchor: DVec2::new(37., 150.), + anchor: DVec2::new(100., 100.), out_handle: None, in_handle: None, id: EmptyId, @@ -769,7 +774,7 @@ mod tests { false, ); - let offset = subpath.offset(-10., utils::Join::Round); + let offset = subpath.offset(10., utils::Join::Round); let mut str = String::new(); offset.to_svg(&mut str, "stroke=\"black\" stroke-width=\"2\" fill=\"none\"".to_string(), String::new(), String::new(), String::new()); println!("{}", str); From 46ab345de2540eba0e1c365b850209d9d2a7688f Mon Sep 17 00:00:00 2001 From: Hannah Li Date: Fri, 17 Mar 2023 23:34:40 -0400 Subject: [PATCH 08/11] Fix iframe sizes and update UI --- libraries/bezier-rs/src/bezier/transform.rs | 6 ++-- libraries/bezier-rs/src/subpath/solvers.rs | 24 +++++++-------- libraries/bezier-rs/src/subpath/transform.rs | 8 ++--- .../src/features/bezier-features.ts | 29 +++---------------- .../src/features/subpath-features.ts | 29 +++---------------- .../bezier-rs-demos/src/utils/options.ts | 14 +++++++++ 6 files changed, 41 insertions(+), 69 deletions(-) diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs index 9d92ba7a5d2..fb592f5ecdd 100644 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ b/libraries/bezier-rs/src/bezier/transform.rs @@ -419,7 +419,7 @@ impl Bezier { /// The 'caps', the linear segments at opposite ends of the outline, intersect the original curve at the midpoint of the cap. /// Outline takes the following parameter: /// - `distance` - The outline's distance from the curve. - /// + /// pub fn outline(&self, distance: f64, cap: Cap) -> Subpath { let first_segment = self.offset(distance); let third_segment = self.reverse().offset(distance); @@ -433,13 +433,13 @@ impl Bezier { /// Version of the `outline` function which draws the outline at the specified distances away from the curve. /// The outline begins `start_distance` away, and gradually move to being `end_distance` away. - /// + /// pub fn graduated_outline(&self, start_distance: f64, end_distance: f64, cap: Cap) -> Subpath { self.skewed_outline(start_distance, end_distance, end_distance, start_distance, cap) } /// Version of the `graduated_outline` function that allows for the 4 corners of the outline to be different distances away from the curve. - /// + /// pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: Cap) -> Subpath { let first_segment = self.graduated_offset(distance1, distance2); let third_segment = self.reverse().graduated_offset(distance3, distance4); diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 3c1a587a92d..41ac8459b76 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -23,7 +23,7 @@ impl Subpath { /// - `error`: an optional f64 value to provide an error bound /// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order. /// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two. - /// + /// pub fn intersections(&self, other: &Bezier, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { self.iter() .enumerate() @@ -35,20 +35,13 @@ impl Subpath { /// This function expects the following: /// - other: a [Bezier] curve to check intersections against /// - error: an optional f64 value to provide an error bound - /// + /// pub fn subpath_intersections(&self, other: &Subpath, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { let mut intersection_t_values: Vec<(usize, f64)> = other.iter().flat_map(|bezier| self.intersections(&bezier, error, minimum_separation)).collect(); intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap()); intersection_t_values } - /// Returns a normalized unit vector representing the tangent on the subpath based on the parametric `t`-value provided. - /// - pub fn tangent(&self, t: SubpathTValue) -> DVec2 { - let (segment_index, t) = self.t_value_to_parametric(t); - self.get_segment(segment_index).unwrap().tangent(TValue::Parametric(t)) - } - /// Returns a list of `t` values that correspond to the self intersection points of the subpath. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point. /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. /// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order. @@ -75,6 +68,13 @@ impl Subpath { intersections_vec } + /// Returns a normalized unit vector representing the tangent on the subpath based on the parametric `t`-value provided. + /// + pub fn tangent(&self, t: SubpathTValue) -> DVec2 { + let (segment_index, t) = self.t_value_to_parametric(t); + self.get_segment(segment_index).unwrap().tangent(TValue::Parametric(t)) + } + /// Returns a normalized unit vector representing the direction of the normal on the subpath based on the parametric `t`-value provided. /// pub fn normal(&self, t: SubpathTValue) -> DVec2 { @@ -84,7 +84,7 @@ impl Subpath { /// 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]`. - /// + /// pub fn local_extrema(&self) -> [Vec; 2] { let number_of_curves = self.len_segments() as f64; @@ -99,7 +99,7 @@ impl Subpath { } /// Return the min and max corners that represent the bounding box of the subpath. - /// + /// 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])]) } @@ -113,7 +113,7 @@ impl Subpath { /// 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]`. - /// + /// pub fn inflections(&self) -> Vec { let number_of_curves = self.len_segments() as f64; let inflection_t_values: Vec = self diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index 35395127355..6c8326e5427 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -121,7 +121,7 @@ impl Subpath { /// The resulting Subpath will wind from the given `t1` to `t2`. /// That means, if the value of `t1` > `t2`, it will cross the break between endpoints from `t1` to `t = 1 = 0` to `t2`. /// If a path winding in the reverse direction is desired, call `trim` on the `Subpath` returned from `Subpath::reverse`. - /// + /// pub fn trim(&self, t1: SubpathTValue, t2: SubpathTValue) -> Subpath { // Return a clone of the Subpath if it is not long enough to be a valid Bezier if self.manipulator_groups.is_empty() { @@ -330,7 +330,7 @@ impl 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. - /// + /// pub fn offset(&self, distance: f64, join: Join) -> Subpath { assert!(self.len_segments() > 1, "Cannot offset an empty Subpath."); @@ -469,7 +469,7 @@ impl Subpath { Subpath::new(manipulator_groups, self.closed) } - // TODO: Add comment and consider refactoring + /// Helper function to combine the two offsets that make up an outline. pub(crate) fn combine_outline(&self, other: &Subpath, cap: Cap) -> Subpath { let mut result_manipulator_groups: Vec> = vec![]; result_manipulator_groups.extend_from_slice(self.manipulator_groups()); @@ -507,7 +507,7 @@ impl Subpath { /// an approximate outline around the subpath at a specified distance from the curve. Outline takes the following parameters: /// - `distance` - The outline's distance from the curve. /// - `join` - The join type used to cap the endpoints of open bezier curves, and join successive subpath segments. - /// + /// pub fn outline(&self, distance: f64, join: Join, cap: Cap) -> (Subpath, Option>) { let pos_offset = self.offset(distance, join); let neg_offset = self.reverse().offset(distance, join); 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 2352baa66dd..cac89df6395 100644 --- a/website/other/bezier-rs-demos/src/features/bezier-features.ts +++ b/website/other/bezier-rs-demos/src/features/bezier-features.ts @@ -1,5 +1,5 @@ import { WasmBezier } from "@graphite/../wasm/pkg"; -import { tSliderOptions, bezierTValueVariantOptions, errorOptions, minimumSeparationOptions } from "@graphite/utils/options"; +import { capOptions, tSliderOptions, bezierTValueVariantOptions, errorOptions, minimumSeparationOptions } from "@graphite/utils/options"; import { BezierDemoOptions, WasmBezierInstance, BezierCallback, InputOption, BEZIER_T_VALUE_VARIANTS } from "@graphite/utils/types"; const bezierFeatures = { @@ -262,14 +262,7 @@ const bezierFeatures = { step: 1, default: 15, }, - { - variable: "cap", - min: 0, - max: 2, - step: 1, - default: 0, - unit: [": Butt", ": Round", ": Square"], - }, + capOptions, ], }, }, @@ -294,14 +287,7 @@ const bezierFeatures = { step: 1, default: 15, }, - { - variable: "cap", - min: 0, - max: 2, - step: 1, - default: 0, - unit: [": Butt", ": Round", ": Square"], - }, + capOptions, ], }, }, @@ -349,14 +335,7 @@ const bezierFeatures = { step: 1, default: 5, }, - { - variable: "cap", - min: 0, - max: 2, - step: 1, - default: 0, - unit: [": Butt", ": Round", ": Square"], - }, + capOptions, ], }, }, 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 37b645a809d..72a27d03e65 100644 --- a/website/other/bezier-rs-demos/src/features/subpath-features.ts +++ b/website/other/bezier-rs-demos/src/features/subpath-features.ts @@ -1,4 +1,4 @@ -import { tSliderOptions, subpathTValueVariantOptions, intersectionErrorOptions, minimumSeparationOptions } from "@graphite/utils/options"; +import { capOptions, joinOptions, tSliderOptions, subpathTValueVariantOptions, intersectionErrorOptions, minimumSeparationOptions } from "@graphite/utils/options"; import { InputOption, SubpathCallback, WasmSubpathInstance, SUBPATH_T_VALUE_VARIANTS } from "@graphite/utils/types"; const subpathFeatures = { @@ -116,14 +116,7 @@ const subpathFeatures = { step: 1, default: 10, }, - { - variable: "join", - min: 0, - max: 2, - step: 1, - default: 0, - unit: [": Bevel", ": Miter", ": Round"], - }, + joinOptions, ], }, outline: { @@ -137,22 +130,8 @@ const subpathFeatures = { step: 1, default: 10, }, - { - variable: "join", - min: 0, - max: 2, - step: 1, - default: 0, - unit: [": Bevel", ": Miter", ": Round"], - }, - { - variable: "cap", - min: 0, - max: 2, - step: 1, - default: 0, - unit: [": Butt", ": Round", ": Square"], - }, + joinOptions, + capOptions, ], }, }; diff --git a/website/other/bezier-rs-demos/src/utils/options.ts b/website/other/bezier-rs-demos/src/utils/options.ts index 2503d74022a..83a6e8dff40 100644 --- a/website/other/bezier-rs-demos/src/utils/options.ts +++ b/website/other/bezier-rs-demos/src/utils/options.ts @@ -45,3 +45,17 @@ export const subpathTValueVariantOptions = { inputType: "dropdown", options: SUBPATH_T_VALUE_VARIANTS, }; + +export const joinOptions = { + variable: "join", + default: 0, + inputType: "dropdown", + options: ["Bevel", "Miter", "Round"], +}; + +export const capOptions = { + variable: "cap", + default: 0, + inputType: "dropdown", + options: ["Butt", "Round", "Square"], +}; From 752c160ac0546c4b74160b263241f6730a0f9a25 Mon Sep 17 00:00:00 2001 From: Hannah Li Date: Wed, 22 Mar 2023 21:43:26 -0400 Subject: [PATCH 09/11] Address comments and handle single point outline --- libraries/bezier-rs/src/bezier/transform.rs | 63 ++++++++++- libraries/bezier-rs/src/subpath/core.rs | 11 ++ libraries/bezier-rs/src/subpath/solvers.rs | 107 ++---------------- libraries/bezier-rs/src/subpath/transform.rs | 85 ++++++++++++-- libraries/bezier-rs/src/utils.rs | 30 +++++ .../other/bezier-rs-demos/wasm/src/bezier.rs | 17 +-- 6 files changed, 183 insertions(+), 130 deletions(-) diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs index fb592f5ecdd..fbc7b84033e 100644 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ b/libraries/bezier-rs/src/bezier/transform.rs @@ -2,7 +2,7 @@ use super::*; use crate::compare::compare_points; use crate::utils::{f64_compare, Cap, TValue}; -use crate::{AppendType, Subpath}; +use crate::{AppendType, ManipulatorGroup, Subpath}; use glam::DMat2; use std::f64::consts::PI; @@ -357,6 +357,9 @@ impl Bezier { /// while negative values will offset in the opposite direction. /// pub fn offset(&self, distance: f64) -> Subpath { + if self.is_single_point() { + return Subpath::from_bezier(self); + } let reduced = self.reduce(None); let mut scaled = Subpath::new(vec![], false); reduced.iter().enumerate().for_each(|(index, bezier)| { @@ -421,8 +424,14 @@ impl Bezier { /// - `distance` - The outline's distance from the curve. /// pub fn outline(&self, distance: f64, cap: Cap) -> Subpath { - let first_segment = self.offset(distance); - let third_segment = self.reverse().offset(distance); + let (first_segment, third_segment) = if self.is_single_point() { + ( + Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::Y * distance)], false), + Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance)], false), + ) + } else { + (self.offset(distance), self.reverse().offset(distance)) + }; if first_segment.is_empty() || third_segment.is_empty() { return Subpath::new(vec![], false); @@ -441,8 +450,14 @@ impl Bezier { /// Version of the `graduated_outline` function that allows for the 4 corners of the outline to be different distances away from the curve. /// pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: Cap) -> Subpath { - let first_segment = self.graduated_offset(distance1, distance2); - let third_segment = self.reverse().graduated_offset(distance3, distance4); + let (first_segment, third_segment) = if self.is_single_point() { + ( + Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::Y * distance1)], false), + Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance1)], false), + ) + } else { + (self.graduated_offset(distance1, distance2), self.reverse().graduated_offset(distance3, distance4)) + }; if first_segment.is_empty() || third_segment.is_empty() { return Subpath::new(vec![], false); @@ -898,6 +913,44 @@ mod tests { assert!(outline.iter().nth(3).unwrap().evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE)); } + #[test] + fn test_outline_single_point_circle() { + let ellipse: Subpath = Subpath::new_ellipse(DVec2::new(50., 50.), DVec2::new(0., 0.)).reverse(); + let p = DVec2::new(25., 25.); + + let line = Bezier::from_linear_dvec2(p, p); + let outline = line.outline::(25., Cap::Round); + assert_eq!(outline, ellipse); + + let cubic = Bezier::from_cubic_dvec2(p, p, p, p); + let outline_cubic = cubic.outline::(25., Cap::Round); + assert_eq!(outline_cubic, ellipse); + } + + #[test] + fn test_outline_single_point_square() { + let square: Subpath = Subpath::from_anchors( + [ + DVec2::new(25., 50.), + DVec2::new(50., 50.), + DVec2::new(50., 0.), + DVec2::new(25., 0.), + DVec2::new(0., 0.), + DVec2::new(0., 50.), + ], + true, + ); + let p = DVec2::new(25., 25.); + + let line = Bezier::from_linear_dvec2(p, p); + let outline = line.outline::(25., Cap::Square); + assert_eq!(outline, square); + + let cubic = Bezier::from_cubic_dvec2(p, p, p, p); + let outline_cubic = cubic.outline::(25., Cap::Square); + assert_eq!(outline_cubic, square); + } + #[test] fn test_graduated_scale() { let bezier = Bezier::from_linear_coordinates(30., 60., 140., 120.); diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index e2895e7c694..647e017e326 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -112,6 +112,17 @@ impl Subpath { &self.manipulator_groups } + /// Returns if the Subpath is equivalent to a single point. + pub fn is_single_point(&self) -> bool { + if self.is_empty() { + return false; + } + let point = self.manipulator_groups[0].anchor; + self.manipulator_groups + .iter() + .all(|manipulator_group| manipulator_group.anchor.abs_diff_eq(point, MAX_ABSOLUTE_DIFFERENCE)) + } + /// Appends to the `svg` mutable string with an SVG shape representation of the curve. pub fn curve_to_svg(&self, svg: &mut String, attributes: String) { let curve_start_argument = format!("{SVG_ARG_MOVE}{} {}", self[0].anchor.x, self[0].anchor.y); diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 41ac8459b76..166a5e1ff65 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -1,6 +1,6 @@ use super::*; use crate::consts::MAX_ABSOLUTE_DIFFERENCE; -use crate::utils::{line_intersection, SubpathTValue}; +use crate::utils::{compute_circular_subpath_details, line_intersection, SubpathTValue}; use crate::TValue; use glam::{DMat2, DVec2}; @@ -48,7 +48,7 @@ impl Subpath { /// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two /// /// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out. - /// + /// pub fn self_intersections(&self, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { let mut intersections_vec = Vec::new(); let err = error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE); @@ -190,20 +190,7 @@ impl Subpath { arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right); } - let center_to_arc_point = arc_point - center; - - // Based on https://pomax.github.io/bezierinfo/#circles_cubic - let handle_offset_factor = 4. / 3. * (angle / 4.).tan(); - - ( - left - center_to_left.perp() * handle_offset_factor, - ManipulatorGroup::new( - arc_point, - Some(arc_point + center_to_arc_point.perp() * handle_offset_factor), - Some(arc_point - center_to_arc_point.perp() * handle_offset_factor), - ), - right + center_to_right.perp() * handle_offset_factor, - ) + compute_circular_subpath_details(left, arc_point, right, center, Some(angle)) } /// Returns the necessary information to create a round cap between the end of `self` and the beginning of `other`. @@ -212,28 +199,15 @@ impl Subpath { /// - The new manipulator group to be added /// - The `in_handle` for the first manipulator group of `other` pub(crate) fn round_cap(&self, other: &Subpath) -> (DVec2, ManipulatorGroup, DVec2) { - // Based on https://pomax.github.io/bezierinfo/#circles_cubic - const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014; - let left = self.manipulator_groups[self.len() - 1].anchor; let right = other.manipulator_groups[0].anchor; let center = (right + left) / 2.; let center_to_right = right - center; - let center_to_left = left - center; let arc_point = center + center_to_right.perp(); - let center_to_arc_point = arc_point - center; - - ( - left - center_to_left.perp() * HANDLE_OFFSET_FACTOR, - ManipulatorGroup::new( - arc_point, - Some(arc_point + center_to_arc_point.perp() * HANDLE_OFFSET_FACTOR), - Some(arc_point - center_to_arc_point.perp() * HANDLE_OFFSET_FACTOR), - ), - right + center_to_right.perp() * HANDLE_OFFSET_FACTOR, - ) + + compute_circular_subpath_details(left, arc_point, right, center, None) } /// Returns the two manipulator groups that create a sqaure cap between the end of `self` and the beginning of `other`. @@ -633,6 +607,7 @@ mod tests { #[test] fn round_join_counter_clockwise_rotation() { + // Test case where the round join is drawn in the counter clockwise direction between two consecutive offsets let subpath = Subpath::new( vec![ ManipulatorGroup { @@ -673,6 +648,7 @@ mod tests { #[test] fn round_join_clockwise_rotation() { + // Test case where the round join is drawn in the clockwise direction between two consecutive offsets let subpath = Subpath::new( vec![ ManipulatorGroup { @@ -710,73 +686,4 @@ mod tests { assert!((round_point - middle).angle_between(round_start - middle) < 0.); assert!((round_end - middle).angle_between(round_point - middle) < 0.); } - - #[test] - fn round_join_test() { - // TODO: Case where reduce ends in a really short segment - // Results in a really weird in_tangent for the round join - //M38 35 C40 40 120 120 130 30 Q175 90 145 150 Q70 185 38 35 Z - let subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: DVec2::new(145., 150.), - out_handle: Some(DVec2::new(70., 185.)), - in_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: DVec2::new(38., 35.), - out_handle: Some(DVec2::new(40., 40.)), - in_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: DVec2::new(130., 30.), - out_handle: Some(DVec2::new(175., 90.)), - in_handle: Some(DVec2::new(120., 120.)), - id: EmptyId, - }, - ], - false, - ); - - let offset = subpath.reverse().offset(10., utils::Join::Round); - let mut str = String::new(); - offset.to_svg(&mut str, "stroke=\"black\" stroke-width=\"2\" fill=\"none\"".to_string(), String::new(), String::new(), String::new()); - println!("{}", str); - } - - #[test] - fn round_join_test_2() { - // TODO: Case where almost linear join gets wonky?? - // M20 20 C10 90 60 40 150 40 L 73 92 Q3 131 100 100 - let subpath = Subpath::new( - vec![ - ManipulatorGroup { - anchor: DVec2::new(150., 40.), - out_handle: None, - in_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: DVec2::new(73., 92.), - out_handle: Some(DVec2::new(3., 131.)), - in_handle: None, - id: EmptyId, - }, - ManipulatorGroup { - anchor: DVec2::new(100., 100.), - out_handle: None, - in_handle: None, - id: EmptyId, - }, - ], - false, - ); - - let offset = subpath.offset(10., utils::Join::Round); - let mut str = String::new(); - offset.to_svg(&mut str, "stroke=\"black\" stroke-width=\"2\" fill=\"none\"".to_string(), String::new(), String::new(), String::new()); - println!("{}", str); - } } diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index 6c8326e5427..a833eabf94b 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -4,7 +4,7 @@ use super::*; use crate::consts::MAX_ABSOLUTE_DIFFERENCE; use crate::utils::{Cap, Join, SubpathTValue, TValue}; -use glam::DAffine2; +use glam::{DAffine2, DVec2}; /// Helper function to ensure the index and t value pair is mapped within a maximum index value. /// Allows for the point to be fetched without needing to handle an additional edge case. @@ -109,9 +109,14 @@ impl Subpath { } /// Returns a [Subpath] with a reversed winding order. + /// Note that a reversed closed subpath will start on the same manipulator group and simply wind the other direction pub fn reverse(&self) -> Subpath { + let mut reversed = Subpath::reverse_manipulator_groups(self.manipulator_groups()); + if self.closed { + reversed.rotate_right(1); + }; Subpath { - manipulator_groups: Subpath::reverse_manipulator_groups(&self.manipulator_groups), + manipulator_groups: reversed, closed: self.closed, } } @@ -336,7 +341,7 @@ impl Subpath { // An offset at a distance 0 from the curve is simply the same curve // An offset of a single point is not defined - if distance == 0. || self.len_segments() == 1 { + if distance == 0. || self.len() == 1 { return self.clone(); } @@ -402,7 +407,7 @@ impl Subpath { } } else { // Otherwise, default to the bevel join - drop_common_point[j] = false; + // drop_common_point[j] = false; } } @@ -509,10 +514,18 @@ impl Subpath { /// - `join` - The join type used to cap the endpoints of open bezier curves, and join successive subpath segments. /// pub fn outline(&self, distance: f64, join: Join, cap: Cap) -> (Subpath, Option>) { - let pos_offset = self.offset(distance, join); - let neg_offset = self.reverse().offset(distance, join); + let is_single_point = self.is_single_point(); + let (pos_offset, neg_offset) = if is_single_point { + let point = self.manipulator_groups[0].anchor; + ( + Subpath::new(vec![ManipulatorGroup::new_anchor(point + DVec2::Y * distance)], false), + Subpath::new(vec![ManipulatorGroup::new_anchor(point + DVec2::NEG_Y * distance)], false), + ) + } else { + (self.offset(distance, join), self.reverse().offset(distance, join)) + }; - if self.closed { + if self.closed && !is_single_point { return (pos_offset, Some(neg_offset)); } @@ -522,7 +535,7 @@ impl Subpath { #[cfg(test)] mod tests { - use super::{ManipulatorGroup, Subpath}; + use super::{Cap, Join, ManipulatorGroup, Subpath}; use crate::compare::{compare_points, compare_subpaths, compare_vec_of_points}; use crate::consts::MAX_ABSOLUTE_DIFFERENCE; use crate::utils::{SubpathTValue, TValue}; @@ -609,7 +622,7 @@ mod tests { ); let outline = subpath.outline(10., crate::Join::Round, crate::Cap::Round).0; - assert_eq!(outline.len(), 25); + assert!(outline.manipulator_groups.windows(2).all(|pair| !pair[0].anchor.abs_diff_eq(pair[1].anchor, MAX_ABSOLUTE_DIFFERENCE))); assert_eq!(outline.closed(), true); } @@ -732,9 +745,15 @@ mod tests { let result = temporary.reverse(); let end = result.len(); - assert_eq!(temporary.manipulator_groups[0].anchor, result.manipulator_groups[end - 1].anchor); - assert_eq!(temporary.manipulator_groups[0].in_handle, result.manipulator_groups[end - 1].out_handle); - assert_eq!(temporary.manipulator_groups[0].out_handle, result.manipulator_groups[end - 1].in_handle); + // Second manipulator group on the temporary subpath should be the reflected version of the last in the result + assert_eq!(temporary.manipulator_groups[1].anchor, result.manipulator_groups[end - 1].anchor); + assert_eq!(temporary.manipulator_groups[1].in_handle, result.manipulator_groups[end - 1].out_handle); + assert_eq!(temporary.manipulator_groups[1].out_handle, result.manipulator_groups[end - 1].in_handle); + + // The first manipulator group in both should be the reflected versions of each other + assert_eq!(temporary.manipulator_groups[0].anchor, result.manipulator_groups[0].anchor); + assert_eq!(temporary.manipulator_groups[0].in_handle, result.manipulator_groups[0].out_handle); + assert_eq!(temporary.manipulator_groups[0].out_handle, result.manipulator_groups[0].in_handle); assert_eq!(subpath, result); } @@ -1011,4 +1030,46 @@ mod tests { assert!(result.manipulator_groups[0].out_handle.is_none()); assert_eq!(result.manipulator_groups.len(), 1); } + + #[test] + fn outline_single_point_circle() { + let ellipse: Subpath = Subpath::new_ellipse(DVec2::new(50., 50.), DVec2::new(0., 0.)).reverse(); + let p = DVec2::new(25., 25.); + + let subpath: Subpath = Subpath::from_anchors([p, p, p], false); + let outline_open = subpath.outline(25., Join::Bevel, Cap::Round); + assert_eq!(outline_open.0, ellipse); + assert_eq!(outline_open.1, None); + + let subpath_closed: Subpath = Subpath::from_anchors([p, p, p], true); + let outline_closed = subpath_closed.outline(25., Join::Bevel, Cap::Round); + assert_eq!(outline_closed.0, ellipse); + assert_eq!(outline_closed.1, None); + } + + #[test] + fn outline_single_point_square() { + let square: Subpath = Subpath::from_anchors( + [ + DVec2::new(25., 50.), + DVec2::new(50., 50.), + DVec2::new(50., 0.), + DVec2::new(25., 0.), + DVec2::new(0., 0.), + DVec2::new(0., 50.), + ], + true, + ); + let p = DVec2::new(25., 25.); + + let subpath: Subpath = Subpath::from_anchors([p, p, p], false); + let outline_open = subpath.outline(25., Join::Bevel, Cap::Square); + assert_eq!(outline_open.0, square); + assert_eq!(outline_open.1, None); + + let subpath_closed: Subpath = Subpath::from_anchors([p, p, p], true); + let outline_closed = subpath_closed.outline(25., Join::Bevel, Cap::Square); + assert_eq!(outline_closed.0, square); + assert_eq!(outline_closed.1, None); + } } diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs index c2d83707a27..8567de741a7 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -1,4 +1,5 @@ use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, MIN_SEPARATION_VALUE, STRICT_MAX_ABSOLUTE_DIFFERENCE}; +use crate::ManipulatorGroup; use glam::{BVec2, DMat2, DVec2}; use std::f64::consts::PI; @@ -29,6 +30,8 @@ pub enum SubpathTValue { } #[derive(Copy, Clone)] +/// Enum to represent the join type between subpaths. +/// As defined in SVG: https://www.w3.org/TR/SVG2/painting.html#LineJoin. pub enum Join { Bevel, Miter, @@ -36,6 +39,8 @@ pub enum Join { } #[derive(Copy, Clone)] +/// Enum to represent the cap type at the ends of an outline +/// As defined in SVG: https://www.w3.org/TR/SVG2/painting.html#LineCaps. pub enum Cap { Butt, Round, @@ -273,6 +278,31 @@ pub fn scale_point_from_origin(point: DVec2, origin: DVec2, should_flip_directio scale_point_from_direction_vector(point, (origin - point).normalize(), should_flip_direction, distance) } +/// Computes the necessary details to form a circular join from `left` to `right`, along a circle around `center`. +/// By default, the angle is assumed to be 180 degrees. +pub fn compute_circular_subpath_details( + left: DVec2, + arc_point: DVec2, + right: DVec2, + center: DVec2, + angle: Option, +) -> (DVec2, ManipulatorGroup, DVec2) { + let center_to_arc_point = arc_point - center; + + // Based on https://pomax.github.io/bezierinfo/#circles_cubic + let handle_offset_factor = if let Some(angle) = angle { 4. / 3. * (angle / 4.).tan() } else { 0.551784777779014 }; + + ( + left - (left - center).perp() * handle_offset_factor, + ManipulatorGroup::new( + arc_point, + Some(arc_point + center_to_arc_point.perp() * handle_offset_factor), + Some(arc_point - center_to_arc_point.perp() * handle_offset_factor), + ), + right + (right - center).perp() * handle_offset_factor, + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/website/other/bezier-rs-demos/wasm/src/bezier.rs b/website/other/bezier-rs-demos/wasm/src/bezier.rs index de6c0585d43..e57a5cc614b 100644 --- a/website/other/bezier-rs-demos/wasm/src/bezier.rs +++ b/website/other/bezier-rs-demos/wasm/src/bezier.rs @@ -247,19 +247,10 @@ impl WasmBezier { let t = parse_t_variant(&t_variant, raw_t); let beziers: [Bezier; 2] = self.0.split(t); - let mut original_bezier_svg = String::new(); - self.0.to_svg( - &mut original_bezier_svg, - CURVE_ATTRIBUTES.to_string().replace(BLACK, WHITE), - ANCHOR_ATTRIBUTES.to_string().replace(BLACK, WHITE), - HANDLE_ATTRIBUTES.to_string(), - HANDLE_LINE_ATTRIBUTES.to_string(), - ); - let mut bezier_svg_1 = String::new(); beziers[0].to_svg( &mut bezier_svg_1, - CURVE_ATTRIBUTES.to_string().replace(BLACK, ORANGE), + CURVE_ATTRIBUTES.to_string().replace(BLACK, ORANGE).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"", ANCHOR_ATTRIBUTES.to_string().replace(BLACK, ORANGE), HANDLE_ATTRIBUTES.to_string().replace(GRAY, ORANGE), HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, ORANGE), @@ -268,13 +259,13 @@ impl WasmBezier { let mut bezier_svg_2 = String::new(); beziers[1].to_svg( &mut bezier_svg_2, - CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), + CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"", ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED), HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED), HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED), ); - wrap_svg_tag(format!("{original_bezier_svg}{bezier_svg_1}{bezier_svg_2}")) + wrap_svg_tag(format!("{}{bezier_svg_1}{bezier_svg_2}", self.get_bezier_path())) } pub fn trim(&self, raw_t1: f64, raw_t2: f64, t_variant: String) -> String { @@ -284,7 +275,7 @@ impl WasmBezier { let mut trimmed_bezier_svg = String::new(); trimmed_bezier.to_svg( &mut trimmed_bezier_svg, - CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), + CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"", ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED), HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED), HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED), From bb1dff8078d9cc6aef259700c03750c37a8cdf79 Mon Sep 17 00:00:00 2001 From: Hannah Li Date: Sat, 25 Mar 2023 13:49:59 -0400 Subject: [PATCH 10/11] Rename variables, fix branches in outline --- libraries/bezier-rs/src/bezier/core.rs | 2 +- libraries/bezier-rs/src/bezier/transform.rs | 20 ++++++++-------- libraries/bezier-rs/src/subpath/core.rs | 2 +- libraries/bezier-rs/src/subpath/transform.rs | 25 +++++++------------- 4 files changed, 20 insertions(+), 29 deletions(-) diff --git a/libraries/bezier-rs/src/bezier/core.rs b/libraries/bezier-rs/src/bezier/core.rs index 7d6e7c5310f..c1b3b06713b 100644 --- a/libraries/bezier-rs/src/bezier/core.rs +++ b/libraries/bezier-rs/src/bezier/core.rs @@ -213,7 +213,7 @@ impl Bezier { } /// Returns true if the start, end and handles of the Bezier are all at the same location - pub fn is_single_point(&self) -> bool { + pub fn is_point(&self) -> bool { let points = self.get_points().collect::>(); let start = self.start(); diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs index fbc7b84033e..66d0e035e37 100644 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ b/libraries/bezier-rs/src/bezier/transform.rs @@ -357,14 +357,14 @@ impl Bezier { /// while negative values will offset in the opposite direction. /// pub fn offset(&self, distance: f64) -> Subpath { - if self.is_single_point() { + if self.is_point() { return Subpath::from_bezier(self); } let reduced = self.reduce(None); let mut scaled = Subpath::new(vec![], false); reduced.iter().enumerate().for_each(|(index, bezier)| { let scaled_bezier = bezier.scale(distance); - if !bezier.is_single_point() { + if !bezier.is_point() { if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) { scaled.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE)); } else { @@ -395,7 +395,7 @@ impl Bezier { let mut result = Subpath::new(vec![], false); reduced.iter().enumerate().for_each(|(index, bezier)| { - if !bezier.is_single_point() { + if !bezier.is_point() { let current_length = bezier.length(None); let next_end_distance = next_start_distance + (current_length / total_length) * distance_difference; let scaled_bezier = bezier.graduated_scale(next_start_distance, next_end_distance); @@ -424,7 +424,7 @@ impl Bezier { /// - `distance` - The outline's distance from the curve. /// pub fn outline(&self, distance: f64, cap: Cap) -> Subpath { - let (first_segment, third_segment) = if self.is_single_point() { + let (pos_offset, neg_offset) = if self.is_point() { ( Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::Y * distance)], false), Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance)], false), @@ -433,11 +433,11 @@ impl Bezier { (self.offset(distance), self.reverse().offset(distance)) }; - if first_segment.is_empty() || third_segment.is_empty() { + if pos_offset.is_empty() || neg_offset.is_empty() { return Subpath::new(vec![], false); } - first_segment.combine_outline(&third_segment, cap) + pos_offset.combine_outline(&neg_offset, cap) } /// Version of the `outline` function which draws the outline at the specified distances away from the curve. @@ -450,7 +450,7 @@ impl Bezier { /// Version of the `graduated_outline` function that allows for the 4 corners of the outline to be different distances away from the curve. /// pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: Cap) -> Subpath { - let (first_segment, third_segment) = if self.is_single_point() { + let (pos_offset, neg_offset) = if self.is_point() { ( Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::Y * distance1)], false), Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance1)], false), @@ -459,11 +459,11 @@ impl Bezier { (self.graduated_offset(distance1, distance2), self.reverse().graduated_offset(distance3, distance4)) }; - if first_segment.is_empty() || third_segment.is_empty() { + if pos_offset.is_empty() || neg_offset.is_empty() { return Subpath::new(vec![], false); } - first_segment.combine_outline(&third_segment, cap) + pos_offset.combine_outline(&neg_offset, cap) } /// Approximate a bezier curve with circular arcs. @@ -878,7 +878,7 @@ mod tests { let reduce = bezier.reduce(None); let offset = bezier.offset::(15.); assert!(reduce.last().is_some()); - assert!(reduce.last().unwrap().is_single_point()); + assert!(reduce.last().unwrap().is_point()); // Expect the single point bezier to be dropped in the offset assert_eq!(reduce.len(), offset.len_segments() + 1); } diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index 647e017e326..1b7b67f7a8a 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -113,7 +113,7 @@ impl Subpath { } /// Returns if the Subpath is equivalent to a single point. - pub fn is_single_point(&self) -> bool { + pub fn is_point(&self) -> bool { if self.is_empty() { return false; } diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index a833eabf94b..f8556299cac 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -347,7 +347,7 @@ impl Subpath { let mut subpaths = self .iter() - .filter(|bezier| !bezier.is_single_point()) + .filter(|bezier| !bezier.is_point()) .map(|bezier| bezier.offset(distance)) .collect::>>(); let mut drop_common_point = vec![true; self.len()]; @@ -385,16 +385,14 @@ impl Subpath { } // The angle is convex. The Subpath must be joined using the specified join type if apply_join { + drop_common_point[j] = false; match join { - Join::Bevel => { - drop_common_point[j] = false; - } + Join::Bevel => {} Join::Miter => { let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j]); if let Some(miter_manipulator_group) = miter_manipulator_group { subpaths[i].manipulator_groups.push(miter_manipulator_group); } - drop_common_point[j] = false; } Join::Round => { let (out_handle, round_point, in_handle) = subpaths[i].round_line_join(&subpaths[j], self.manipulator_groups[j].anchor); @@ -402,12 +400,8 @@ impl Subpath { subpaths[i].manipulator_groups[last_index].out_handle = Some(out_handle); subpaths[i].manipulator_groups.push(round_point.clone()); subpaths[j].manipulator_groups[0].in_handle = Some(in_handle); - drop_common_point[j] = false; } } - } else { - // Otherwise, default to the bevel join - // drop_common_point[j] = false; } } @@ -428,17 +422,15 @@ impl Subpath { } } if apply_join { + drop_common_point[0] = false; match join { - Join::Bevel => { - drop_common_point[0] = false; - } + Join::Bevel => {} Join::Miter => { let last_subpath_index = subpaths.len() - 1; let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0]); if let Some(miter_manipulator_group) = miter_manipulator_group { subpaths[last_subpath_index].manipulator_groups.push(miter_manipulator_group); } - drop_common_point[0] = false; } Join::Round => { let last_subpath_index = subpaths.len() - 1; @@ -447,7 +439,6 @@ impl Subpath { subpaths[last_subpath_index].manipulator_groups[last_index].out_handle = Some(out_handle); subpaths[last_subpath_index].manipulator_groups.push(round_point); subpaths[0].manipulator_groups[0].in_handle = Some(in_handle); - drop_common_point[0] = false; } } } @@ -514,8 +505,8 @@ impl Subpath { /// - `join` - The join type used to cap the endpoints of open bezier curves, and join successive subpath segments. /// pub fn outline(&self, distance: f64, join: Join, cap: Cap) -> (Subpath, Option>) { - let is_single_point = self.is_single_point(); - let (pos_offset, neg_offset) = if is_single_point { + let is_point = self.is_point(); + let (pos_offset, neg_offset) = if is_point { let point = self.manipulator_groups[0].anchor; ( Subpath::new(vec![ManipulatorGroup::new_anchor(point + DVec2::Y * distance)], false), @@ -525,7 +516,7 @@ impl Subpath { (self.offset(distance, join), self.reverse().offset(distance, join)) }; - if self.closed && !is_single_point { + if self.closed && !is_point { return (pos_offset, Some(neg_offset)); } From 14ec93ea6c31d96bf423a5829ce5f42ce0f08b88 Mon Sep 17 00:00:00 2001 From: Hannah Li Date: Mon, 27 Mar 2023 14:18:26 -0400 Subject: [PATCH 11/11] Address comments --- libraries/bezier-rs/src/bezier/core.rs | 3 +-- libraries/bezier-rs/src/bezier/transform.rs | 12 ++++++------ libraries/bezier-rs/src/subpath/transform.rs | 10 +++++----- .../src/components/SubpathDemoPane.ts | 11 ++++++++--- .../bezier-rs-demos/src/features/subpath-features.ts | 6 +++--- website/other/bezier-rs-demos/src/utils/render.ts | 4 ++++ website/other/bezier-rs-demos/src/utils/types.ts | 5 +++++ 7 files changed, 32 insertions(+), 19 deletions(-) diff --git a/libraries/bezier-rs/src/bezier/core.rs b/libraries/bezier-rs/src/bezier/core.rs index c1b3b06713b..7f3886c2732 100644 --- a/libraries/bezier-rs/src/bezier/core.rs +++ b/libraries/bezier-rs/src/bezier/core.rs @@ -214,10 +214,9 @@ impl Bezier { /// Returns true if the start, end and handles of the Bezier are all at the same location pub fn is_point(&self) -> bool { - let points = self.get_points().collect::>(); let start = self.start(); - points.iter().all(|point| point.abs_diff_eq(start, MAX_ABSOLUTE_DIFFERENCE)) + self.get_points().all(|point| point.abs_diff_eq(start, MAX_ABSOLUTE_DIFFERENCE)) } } diff --git a/libraries/bezier-rs/src/bezier/transform.rs b/libraries/bezier-rs/src/bezier/transform.rs index 66d0e035e37..4df23d0d190 100644 --- a/libraries/bezier-rs/src/bezier/transform.rs +++ b/libraries/bezier-rs/src/bezier/transform.rs @@ -426,8 +426,8 @@ impl Bezier { pub fn outline(&self, distance: f64, cap: Cap) -> Subpath { let (pos_offset, neg_offset) = if self.is_point() { ( - Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::Y * distance)], false), Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance)], false), + Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::Y * distance)], false), ) } else { (self.offset(distance), self.reverse().offset(distance)) @@ -452,8 +452,8 @@ impl Bezier { pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: Cap) -> Subpath { let (pos_offset, neg_offset) = if self.is_point() { ( - Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::Y * distance1)], false), Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance1)], false), + Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::Y * distance1)], false), ) } else { (self.graduated_offset(distance1, distance2), self.reverse().graduated_offset(distance3, distance4)) @@ -915,7 +915,7 @@ mod tests { #[test] fn test_outline_single_point_circle() { - let ellipse: Subpath = Subpath::new_ellipse(DVec2::new(50., 50.), DVec2::new(0., 0.)).reverse(); + let ellipse: Subpath = Subpath::new_ellipse(DVec2::new(0., 0.), DVec2::new(50., 50.)).reverse(); let p = DVec2::new(25., 25.); let line = Bezier::from_linear_dvec2(p, p); @@ -931,12 +931,12 @@ mod tests { fn test_outline_single_point_square() { let square: Subpath = Subpath::from_anchors( [ - DVec2::new(25., 50.), - DVec2::new(50., 50.), - DVec2::new(50., 0.), DVec2::new(25., 0.), DVec2::new(0., 0.), DVec2::new(0., 50.), + DVec2::new(25., 50.), + DVec2::new(50., 50.), + DVec2::new(50., 0.), ], true, ); diff --git a/libraries/bezier-rs/src/subpath/transform.rs b/libraries/bezier-rs/src/subpath/transform.rs index f8556299cac..0087bce051d 100644 --- a/libraries/bezier-rs/src/subpath/transform.rs +++ b/libraries/bezier-rs/src/subpath/transform.rs @@ -509,8 +509,8 @@ impl Subpath { let (pos_offset, neg_offset) = if is_point { let point = self.manipulator_groups[0].anchor; ( - Subpath::new(vec![ManipulatorGroup::new_anchor(point + DVec2::Y * distance)], false), Subpath::new(vec![ManipulatorGroup::new_anchor(point + DVec2::NEG_Y * distance)], false), + Subpath::new(vec![ManipulatorGroup::new_anchor(point + DVec2::Y * distance)], false), ) } else { (self.offset(distance, join), self.reverse().offset(distance, join)) @@ -1024,7 +1024,7 @@ mod tests { #[test] fn outline_single_point_circle() { - let ellipse: Subpath = Subpath::new_ellipse(DVec2::new(50., 50.), DVec2::new(0., 0.)).reverse(); + let ellipse: Subpath = Subpath::new_ellipse(DVec2::new(0., 0.), DVec2::new(50., 50.)).reverse(); let p = DVec2::new(25., 25.); let subpath: Subpath = Subpath::from_anchors([p, p, p], false); @@ -1042,12 +1042,12 @@ mod tests { fn outline_single_point_square() { let square: Subpath = Subpath::from_anchors( [ - DVec2::new(25., 50.), - DVec2::new(50., 50.), - DVec2::new(50., 0.), DVec2::new(25., 0.), DVec2::new(0., 0.), DVec2::new(0., 50.), + DVec2::new(25., 50.), + DVec2::new(50., 50.), + DVec2::new(50., 0.), ], true, ); diff --git a/website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts b/website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts index 505926550f5..7348ee51193 100644 --- a/website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts +++ b/website/other/bezier-rs-demos/src/components/SubpathDemoPane.ts @@ -1,6 +1,6 @@ import subpathFeatures, { SubpathFeatureKey } from "@graphite/features/subpath-features"; import { renderDemoPane } from "@graphite/utils/render"; -import { Demo, DemoPane, InputOption, SubpathDemoArgs } from "@graphite/utils/types"; +import { Demo, DemoPane, SubpathDemoArgs, SubpathInputOption } from "@graphite/utils/types"; class SubpathDemoPane extends HTMLElement implements DemoPane { // Props @@ -8,7 +8,7 @@ class SubpathDemoPane extends HTMLElement implements DemoPane { name!: string; - inputOptions!: InputOption[]; + inputOptions!: SubpathInputOption[]; triggerOnMouseMove!: boolean; @@ -62,7 +62,12 @@ class SubpathDemoPane extends HTMLElement implements DemoPane { subpathDemo.setAttribute("triples", JSON.stringify(demo.triples)); subpathDemo.setAttribute("closed", String(demo.closed)); subpathDemo.setAttribute("key", this.key); - subpathDemo.setAttribute("inputOptions", JSON.stringify(this.inputOptions)); + + const inputOptions = this.inputOptions.map((option) => ({ + ...option, + disabled: option.isDisabledForClosed && demo.closed, + })); + subpathDemo.setAttribute("inputOptions", JSON.stringify(inputOptions)); subpathDemo.setAttribute("triggerOnMouseMove", String(this.triggerOnMouseMove)); return subpathDemo; } 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 72a27d03e65..9f9e7a3998d 100644 --- a/website/other/bezier-rs-demos/src/features/subpath-features.ts +++ b/website/other/bezier-rs-demos/src/features/subpath-features.ts @@ -1,5 +1,5 @@ import { capOptions, joinOptions, tSliderOptions, subpathTValueVariantOptions, intersectionErrorOptions, minimumSeparationOptions } from "@graphite/utils/options"; -import { InputOption, SubpathCallback, WasmSubpathInstance, SUBPATH_T_VALUE_VARIANTS } from "@graphite/utils/types"; +import { SubpathCallback, SubpathInputOption, WasmSubpathInstance, SUBPATH_T_VALUE_VARIANTS } from "@graphite/utils/types"; const subpathFeatures = { constructor: { @@ -131,7 +131,7 @@ const subpathFeatures = { default: 10, }, joinOptions, - capOptions, + { ...capOptions, isDisabledForClosed: true }, ], }, }; @@ -140,7 +140,7 @@ export type SubpathFeatureKey = keyof typeof subpathFeatures; export type SubpathFeatureOptions = { name: string; callback: SubpathCallback; - inputOptions?: InputOption[]; + inputOptions?: SubpathInputOption[]; triggerOnMouseMove?: boolean; }; export default subpathFeatures as Record; diff --git a/website/other/bezier-rs-demos/src/utils/render.ts b/website/other/bezier-rs-demos/src/utils/render.ts index 17fdc6a98e4..a4f4866197f 100644 --- a/website/other/bezier-rs-demos/src/utils/render.ts +++ b/website/other/bezier-rs-demos/src/utils/render.ts @@ -43,6 +43,10 @@ export function renderDemo(demo: Demo): void { selectInput.append(option); }); + if (inputOption.disabled) { + selectInput.disabled = true; + } + selectInput.addEventListener("change", (event: Event): void => { demo.sliderData[inputOption.variable] = Number((event.target as HTMLInputElement).value); demo.drawDemo(figure); diff --git a/website/other/bezier-rs-demos/src/utils/types.ts b/website/other/bezier-rs-demos/src/utils/types.ts index ed795753ddd..76253e27c73 100644 --- a/website/other/bezier-rs-demos/src/utils/types.ts +++ b/website/other/bezier-rs-demos/src/utils/types.ts @@ -22,6 +22,10 @@ export type BezierDemoOptions = { }; }; +export type SubpathInputOption = InputOption & { + isDisabledForClosed?: boolean; +}; + export type InputOption = { variable: string; min?: number; @@ -31,6 +35,7 @@ export type InputOption = { unit?: string | string[]; inputType?: "slider" | "dropdown"; options?: string[]; + disabled?: boolean; }; export function getCurveType(numPoints: number): BezierCurveType {