From b84e647f40fa2f06cea522a2eb1206bccf4523f4 Mon Sep 17 00:00:00 2001 From: Thomas Cheng <35661641+Androxium@users.noreply.github.com> Date: Sat, 6 Aug 2022 01:34:39 -0400 Subject: [PATCH] Implement arcs for Bezier math library (#731) * added arcs impl Co-authored-by: Hannah Li Co-authored-by: Rob Nadal * fixed arc drawing, todo - fix linear check Co-authored-by: Hannah Li * fixed linear bug + added comments and tests Co-authored-by: Hannah Li * added max iteration guard + made params optional + added impl todo * Add functionality to get arcs between extrema * Add ArcsOptions to manage optional parameters of the arcs function * added slider to toggle between arcs impl Co-authored-by: Rob Nadal * Remove unused types * address some comments * added rustdoc for CircularArc struct * Extract duplicate code into helper, remove loop labels, use window function * Make JsValue handling consistent in WasmBezier and add comments for the underlying type * Add enum for MaximizeArcs Auto/On/Off functionality * Change Auto to Automatic * fix errors from resolving merge conflict * fixed error from resolving merge conflicts * fixed formatting * address comments * Small fix * Add some missing comments * address comments * rename variable * Use unit to show maximize_arcs values * Change i32 to usize and other minor adjustments * Change computation for middle t values * Remove tsconfig * Fix more usize number handling Co-authored-by: Hannah Li Co-authored-by: Rob Nadal Co-authored-by: Keavon Chambers --- bezier-rs/docs/interactive-docs/src/App.vue | 68 +++- .../src/components/BezierDrawing.ts | 28 +- .../src/components/SliderExample.vue | 5 +- .../interactive-docs/src/utils/drawing.ts | 18 +- .../docs/interactive-docs/src/utils/types.ts | 9 +- .../docs/interactive-docs/wasm/src/lib.rs | 98 ++++-- bezier-rs/lib/src/consts.rs | 4 +- bezier-rs/lib/src/lib.rs | 298 +++++++++++++++--- bezier-rs/lib/src/structs.rs | 95 ++++++ bezier-rs/lib/src/subpath/lookup.rs | 2 +- bezier-rs/lib/src/utils.rs | 49 ++- .../utility_types/vectorize_layer_metadata.rs | 8 +- tsconfig.json | 3 - 13 files changed, 582 insertions(+), 103 deletions(-) create mode 100644 bezier-rs/lib/src/structs.rs delete mode 100644 tsconfig.json diff --git a/bezier-rs/docs/interactive-docs/src/App.vue b/bezier-rs/docs/interactive-docs/src/App.vue index 25504a5a..88d07237 100644 --- a/bezier-rs/docs/interactive-docs/src/App.vue +++ b/bezier-rs/docs/interactive-docs/src/App.vue @@ -25,8 +25,8 @@ diff --git a/bezier-rs/docs/interactive-docs/src/utils/drawing.ts b/bezier-rs/docs/interactive-docs/src/utils/drawing.ts index 838a2df4..5d71f199 100644 --- a/bezier-rs/docs/interactive-docs/src/utils/drawing.ts +++ b/bezier-rs/docs/interactive-docs/src/utils/drawing.ts @@ -1,4 +1,4 @@ -import { BezierStyleConfig, Point, WasmBezierInstance } from "@/utils/types"; +import { BezierStyleConfig, CircleSector, Point, WasmBezierInstance } from "@/utils/types"; const HANDLE_RADIUS_FACTOR = 2 / 3; const DEFAULT_ENDPOINT_RADIUS = 5; @@ -81,8 +81,22 @@ export const drawCircle = (ctx: CanvasRenderingContext2D, point: Point, radius: ctx.stroke(); }; +export const drawCircleSector = (ctx: CanvasRenderingContext2D, circleSector: CircleSector, strokeColor = COLORS.INTERACTIVE.STROKE_1, fillColor = COLORS.NON_INTERACTIVE.STROKE_1): void => { + ctx.strokeStyle = strokeColor; + ctx.fillStyle = fillColor; + ctx.lineWidth = 2; + + const { center, radius, startAngle, endAngle } = circleSector; + ctx.beginPath(); + ctx.moveTo(center.x, center.y); + ctx.arc(center.x, center.y, radius, startAngle, endAngle); + ctx.lineTo(center.x, center.y); + ctx.closePath(); + ctx.fill(); +}; + export const drawBezierHelper = (ctx: CanvasRenderingContext2D, bezier: WasmBezierInstance, bezierStyleConfig: Partial = {}): void => { - const points = bezier.get_points().map((p: string) => JSON.parse(p)); + const points = JSON.parse(bezier.get_points()); drawBezier(ctx, points, null, bezierStyleConfig); }; diff --git a/bezier-rs/docs/interactive-docs/src/utils/types.ts b/bezier-rs/docs/interactive-docs/src/utils/types.ts index 013bf674..79a71cdc 100644 --- a/bezier-rs/docs/interactive-docs/src/utils/types.ts +++ b/bezier-rs/docs/interactive-docs/src/utils/types.ts @@ -23,7 +23,7 @@ export type SliderOption = { step: number; default: number; variable: string; - unit?: string; + unit?: string | string[]; }; export type TemplateOption = { @@ -46,3 +46,10 @@ export type BezierStyleConfig = { radius: number; drawHandles: boolean; }; + +export type CircleSector = { + center: Point; + radius: number; + startAngle: number; + endAngle: number; +}; diff --git a/bezier-rs/docs/interactive-docs/wasm/src/lib.rs b/bezier-rs/docs/interactive-docs/wasm/src/lib.rs index ba9b6fe2..f9620921 100644 --- a/bezier-rs/docs/interactive-docs/wasm/src/lib.rs +++ b/bezier-rs/docs/interactive-docs/wasm/src/lib.rs @@ -1,25 +1,42 @@ pub mod subpath; mod svg_drawing; -use bezier_rs::{Bezier, ProjectionOptions}; +use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, ProjectionOptions}; use glam::DVec2; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; +#[derive(Serialize, Deserialize)] +struct CircleSector { + center: Point, + radius: f64, + #[serde(rename = "startAngle")] + start_angle: f64, + #[serde(rename = "endAngle")] + end_angle: f64, +} + #[derive(Serialize, Deserialize)] struct Point { x: f64, y: f64, } +#[wasm_bindgen] +pub enum WasmMaximizeArcs { + Automatic, // 0 + On, // 1 + Off, // 2 +} + /// Wrapper of the `Bezier` struct to be used in JS. #[wasm_bindgen] #[derive(Clone)] pub struct WasmBezier(Bezier); -/// Convert a `DVec2` into a `JsValue`. -fn vec_to_point(p: &DVec2) -> JsValue { - JsValue::from_serde(&serde_json::to_string(&Point { x: p.x, y: p.y }).unwrap()).unwrap() +/// Convert a `DVec2` into a `Point`. +fn vec_to_point(p: &DVec2) -> Point { + Point { x: p.x, y: p.y } } /// Convert a bezier to a list of points. @@ -32,6 +49,14 @@ fn to_js_value(data: T) -> JsValue { JsValue::from_serde(&serde_json::to_string(&data).unwrap()).unwrap() } +fn convert_wasm_maximize_arcs(wasm_enum_value: WasmMaximizeArcs) -> ArcStrategy { + match wasm_enum_value { + WasmMaximizeArcs::Automatic => ArcStrategy::Automatic, + WasmMaximizeArcs::On => ArcStrategy::FavorLargerArcs, + WasmMaximizeArcs::Off => ArcStrategy::FavorCorrectness, + } +} + #[wasm_bindgen] impl WasmBezier { /// Expect js_points to be a list of 2 pairs. @@ -78,8 +103,10 @@ impl WasmBezier { self.0.set_handle_end(DVec2::new(x, y)); } - pub fn get_points(&self) -> Vec { - self.0.get_points().map(|point| vec_to_point(&point)).collect() + /// The wrapped return type is `Vec`. + pub fn get_points(&self) -> JsValue { + let points: Vec = self.0.get_points().map(|point| vec_to_point(&point)).collect(); + to_js_value(points) } pub fn to_svg(&self) -> String { @@ -90,26 +117,39 @@ impl WasmBezier { self.0.length(None) } + /// The wrapped return type is `Point`. pub fn evaluate(&self, t: f64) -> JsValue { - vec_to_point(&self.0.evaluate(t)) + let point: Point = vec_to_point(&self.0.evaluate(t)); + to_js_value(point) } - pub fn compute_lookup_table(&self, steps: i32) -> Vec { - self.0.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect() + /// The wrapped return type is `Vec`. + pub fn compute_lookup_table(&self, steps: usize) -> JsValue { + let table_values: Vec = self.0.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect(); + to_js_value(table_values) } pub fn derivative(&self) -> Option { self.0.derivative().map(WasmBezier) } + /// The wrapped return type is `Point`. pub fn tangent(&self, t: f64) -> JsValue { - vec_to_point(&self.0.tangent(t)) + let tangent_point: Point = vec_to_point(&self.0.tangent(t)); + to_js_value(tangent_point) } + /// The wrapped return type is `Point`. pub fn normal(&self, t: f64) -> JsValue { - vec_to_point(&self.0.normal(t)) + let normal_point: Point = vec_to_point(&self.0.normal(t)); + to_js_value(normal_point) } + pub fn curvature(&self, t: f64) -> f64 { + self.0.curvature(t) + } + + /// The wrapped return type is `[Vec; 2]`. pub fn split(&self, t: f64) -> JsValue { let bezier_points: [Vec; 2] = self.0.split(t).map(bezier_to_points); to_js_value(bezier_points) @@ -123,29 +163,33 @@ impl WasmBezier { self.0.project(DVec2::new(x, y), ProjectionOptions::default()) } + /// The wrapped return type is `[Vec; 2]`. pub fn local_extrema(&self) -> JsValue { - let local_extrema = self.0.local_extrema(); + let local_extrema: [Vec; 2] = self.0.local_extrema(); to_js_value(local_extrema) } + /// The wrapped return type is `[Point; 2]`. pub fn bounding_box(&self) -> JsValue { let bbox_points: [Point; 2] = self.0.bounding_box().map(|p| Point { x: p.x, y: p.y }); to_js_value(bbox_points) } + /// The wrapped return type is `Vec`. pub fn inflections(&self) -> JsValue { - let inflections = self.0.inflections(); + let inflections: Vec = self.0.inflections(); to_js_value(inflections) } + /// The wrapped return type is `Vec>`. pub fn de_casteljau_points(&self, t: f64) -> JsValue { - let hull = self + let points: Vec> = self .0 .de_casteljau_points(t) .iter() .map(|level| level.iter().map(|&point| Point { x: point.x, y: point.y }).collect::>()) - .collect::>>(); - to_js_value(hull) + .collect(); + to_js_value(points) } pub fn rotate(&self, angle: f64) -> WasmBezier { @@ -185,12 +229,30 @@ impl WasmBezier { to_js_value(bezier_points) } + /// The wrapped return type is `Vec>`. pub fn offset(&self, distance: f64) -> JsValue { let bezier_points: Vec> = self.0.offset(distance).into_iter().map(bezier_to_points).collect(); to_js_value(bezier_points) } - pub fn curvature(&self, t: f64) -> f64 { - self.0.curvature(t) + /// The wrapped return type is `Vec`. + pub fn arcs(&self, error: f64, max_iterations: usize, maximize_arcs: WasmMaximizeArcs) -> JsValue { + let strategy = convert_wasm_maximize_arcs(maximize_arcs); + let options = ArcsOptions { error, max_iterations, strategy }; + let circle_sectors: Vec = self + .0 + .arcs(options) + .iter() + .map(|sector| CircleSector { + center: Point { + x: sector.center.x, + y: sector.center.y, + }, + radius: sector.radius, + start_angle: sector.start_angle, + end_angle: sector.end_angle, + }) + .collect(); + to_js_value(circle_sectors) } } diff --git a/bezier-rs/lib/src/consts.rs b/bezier-rs/lib/src/consts.rs index c6e5dfdd..21c81343 100644 --- a/bezier-rs/lib/src/consts.rs +++ b/bezier-rs/lib/src/consts.rs @@ -14,9 +14,9 @@ pub const SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE: f64 = std::f64::consts::PI / /// Default `t` value used for the `curve_through_points` functions. pub const DEFAULT_T_VALUE: f64 = 0.5; /// Default LUT step size in `compute_lookup_table` function. -pub const DEFAULT_LUT_STEP_SIZE: i32 = 10; +pub const DEFAULT_LUT_STEP_SIZE: usize = 10; /// Default number of subdivisions used in `length` calculation. -pub const DEFAULT_LENGTH_SUBDIVISIONS: i32 = 1000; +pub const DEFAULT_LENGTH_SUBDIVISIONS: usize = 1000; /// Default step size for `reduce` function. pub const DEFAULT_REDUCE_STEP_SIZE: f64 = 0.01; diff --git a/bezier-rs/lib/src/lib.rs b/bezier-rs/lib/src/lib.rs index a5aa7efb..f0908c7f 100644 --- a/bezier-rs/lib/src/lib.rs +++ b/bezier-rs/lib/src/lib.rs @@ -1,13 +1,16 @@ //! Bezier-rs: A Bezier Math Library for Rust mod consts; +mod structs; pub mod subpath; mod utils; use consts::*; +pub use structs::*; pub use subpath::*; use glam::{DMat2, DVec2}; +use std::f64::consts::PI; use std::fmt::{Debug, Formatter, Result}; use std::ops::Range; @@ -29,30 +32,6 @@ enum BezierHandles { }, } -/// Struct to represent optional parameters that can be passed to the `project` function. -#[derive(Copy, Clone)] -pub struct ProjectionOptions { - /// Size of the lookup table for the initial passthrough. The default value is 20. - pub lut_size: i32, - /// Difference used between floating point numbers to be considered as equal. The default value is `0.0001` - pub convergence_epsilon: f64, - /// Controls the number of iterations needed to consider that minimum distance to have converged. The default value is 3. - pub convergence_limit: i32, - /// Controls the maximum total number of iterations to be used. The default value is 10. - pub iteration_limit: i32, -} - -impl Default for ProjectionOptions { - fn default() -> Self { - ProjectionOptions { - lut_size: 20, - convergence_epsilon: 1e-4, - convergence_limit: 3, - iteration_limit: 10, - } - } -} - /// Representation of a bezier curve with 2D points. #[derive(Copy, Clone, PartialEq)] pub struct Bezier { @@ -317,16 +296,16 @@ impl Bezier { // Basis code based off of pseudocode found here: . let t_squared = t * t; - let one_minus_t = 1.0 - t; + let one_minus_t = 1. - t; let squared_one_minus_t = one_minus_t * one_minus_t; match self.handles { BezierHandles::Linear => self.start.lerp(self.end, t), - BezierHandles::Quadratic { handle } => squared_one_minus_t * self.start + 2.0 * one_minus_t * t * handle + t_squared * self.end, + BezierHandles::Quadratic { handle } => squared_one_minus_t * self.start + 2. * one_minus_t * t * handle + t_squared * self.end, BezierHandles::Cubic { handle_start, handle_end } => { let t_cubed = t_squared * t; let cubed_one_minus_t = squared_one_minus_t * one_minus_t; - cubed_one_minus_t * self.start + 3.0 * squared_one_minus_t * t * handle_start + 3.0 * one_minus_t * t_squared * handle_end + t_cubed * self.end + cubed_one_minus_t * self.start + 3. * squared_one_minus_t * t * handle_start + 3. * one_minus_t * t_squared * handle_end + t_cubed * self.end } } } @@ -334,19 +313,19 @@ impl Bezier { /// Calculate the point on the curve based on the `t`-value provided. /// Expects `t` to be within the inclusive range `[0, 1]`. pub fn evaluate(&self, t: f64) -> DVec2 { - assert!((0.0..=1.0).contains(&t)); + assert!((0.0..=1.).contains(&t)); self.unrestricted_evaluate(t) } /// Return a selection of equidistant points on the bezier curve. /// If no value is provided for `steps`, then the function will default `steps` to be 10. - pub fn compute_lookup_table(&self, steps: Option) -> Vec { + pub fn compute_lookup_table(&self, steps: Option) -> Vec { let steps_unwrapped = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE); - let ratio: f64 = 1.0 / (steps_unwrapped as f64); - let mut steps_array = Vec::with_capacity((steps_unwrapped + 1) as usize); + let ratio: f64 = 1. / (steps_unwrapped as f64); + let mut steps_array = Vec::with_capacity(steps_unwrapped + 1); for t in 0..steps_unwrapped + 1 { - steps_array.push(self.evaluate(f64::from(t) * ratio)) + steps_array.push(self.evaluate(f64::from(t as i32) * ratio)) } steps_array @@ -354,7 +333,7 @@ impl Bezier { /// Return an approximation of the length of the bezier curve. /// - `num_subdivisions` - Number of subdivisions used to approximate the curve. The default value is 1000. - pub fn length(&self, num_subdivisions: Option) -> f64 { + pub fn length(&self, num_subdivisions: Option) -> f64 { match self.handles { BezierHandles::Linear => self.start.distance(self.end), _ => { @@ -363,7 +342,7 @@ impl Bezier { // We will use an approximate approach where we split the curve into many subdivisions // and calculate the euclidean distance between the two endpoints of the subdivision let lookup_table = self.compute_lookup_table(Some(num_subdivisions.unwrap_or(DEFAULT_LENGTH_SUBDIVISIONS))); - let mut approx_curve_length = 0.0; + let mut approx_curve_length = 0.; let mut previous_point = lookup_table[0]; // Calculate approximate distance between subdivision for current_point in lookup_table.iter().skip(1) { @@ -499,8 +478,10 @@ impl Bezier { let (minimum_position, minimum_distance) = utils::get_closest_point_in_lut(&lut, point); // Get the t values to the left and right of the closest result in the lookup table - let mut left_t = (0.max(minimum_position - 1) as f64) / lut_size as f64; - let mut right_t = (lut_size.min(minimum_position + 1)) as f64 / lut_size as f64; + let lut_size_f64 = lut_size as f64; + let minimum_position_f64 = minimum_position as f64; + let mut left_t = (minimum_position_f64 - 1.).max(0.) / lut_size_f64; + let mut right_t = (minimum_position_f64 + 1.).min(lut_size_f64) / lut_size_f64; // Perform a finer search by finding closest t from 5 points between [left_t, right_t] inclusive // Choose new left_t and right_t for a smaller range around the closest t and repeat the process @@ -518,16 +499,16 @@ impl Bezier { // Store calculated distances to minimize unnecessary recomputations let mut distances: [f64; NUM_DISTANCES] = [ - point.distance(lut[0.max(minimum_position - 1) as usize]), + point.distance(lut[(minimum_position as i64 - 1).max(0) as usize]), 0., 0., 0., - point.distance(lut[lut_size.min(minimum_position + 1) as usize]), + point.distance(lut[lut_size.min(minimum_position + 1)]), ]; while left_t <= right_t && convergence_count < convergence_limit && iteration_count < iteration_limit { previous_distance = new_minimum_distance; - let step = (right_t - left_t) / ((NUM_DISTANCES - 1) as f64); + let step = (right_t - left_t) / (NUM_DISTANCES as f64 - 1.); let mut iterator_t = left_t; let mut target_index = 0; // Iterate through first 4 points and will handle the right most point later @@ -839,6 +820,15 @@ impl Bezier { endpoint_normal_angle < SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE } + /// Add the bezier endpoints if not already present, and combine and sort the dimensional extrema. + 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 + } + /// Returns a tuple of the scalable subcurves and the corresponding `t` values that were used to split the curve. /// This function may introduce gaps if subsections of the curve are not reducible. /// The function takes the following parameter: @@ -852,10 +842,7 @@ impl Bezier { let step_size = step_size.unwrap_or(DEFAULT_REDUCE_STEP_SIZE); - let mut extrema: Vec = 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()); + let extrema = self.get_extrema_t_list(); // Split each subcurve such that each resulting segment is scalable. let mut result_beziers: Vec = Vec::new(); @@ -923,6 +910,153 @@ impl Bezier { self.reduced_curves_and_t_values(step_size).0 } + /// Approximate a bezier curve with circular arcs. + /// The algorithm can be customized using the [ArcsOptions] structure. + pub fn arcs(&self, arcs_options: ArcsOptions) -> Vec { + let ArcsOptions { + strategy: maximize_arcs, + error, + max_iterations, + } = arcs_options; + + match maximize_arcs { + ArcStrategy::Automatic => { + let (auto_arcs, final_low_t) = self.approximate_curve_with_arcs(0., 1., error, max_iterations, true); + let arc_approximations = self.split(final_low_t)[1].arcs(ArcsOptions { + strategy: ArcStrategy::FavorCorrectness, + error, + max_iterations, + }); + if final_low_t != 1. { + [auto_arcs, arc_approximations].concat() + } else { + auto_arcs + } + } + ArcStrategy::FavorLargerArcs => self.approximate_curve_with_arcs(0., 1., error, max_iterations, false).0, + ArcStrategy::FavorCorrectness => self + .get_extrema_t_list() + .windows(2) + .flat_map(|t_pair| self.approximate_curve_with_arcs(t_pair[0], t_pair[1], error, max_iterations, false).0) + .collect::>(), + } + } + + /// Implements an algorithm that approximates a bezier curve with circular arcs. + /// This algorithm uses a method akin to binary search to find an arc that approximates a maximal segment of the curve. + /// Once a maximal arc has been found for a sub-segment of the curve, the algorithm continues by starting again at the end of the previous approximation. + /// More details can be found in the [Approximating a Bezier curve with circular arcs](https://pomax.github.io/bezierinfo/#arcapproximation) section of Pomax's bezier curve primer. + /// A caveat with this algorithm is that it is possible to find erroneous approximations in cases such as in a very narrow `U`. + /// - `stop_when_invalid`: Used to determine whether the algorithm should terminate early if erroneous approximations are encountered. + /// + /// Returns a tuple where the first element is the list of circular arcs and the second is the `t` value where the next segment should start from. + /// The second value will be `1.` except for when `stop_when_invalid` is true and an invalid approximation is encountered. + fn approximate_curve_with_arcs(&self, local_low: f64, local_high: f64, error: f64, max_iterations: usize, stop_when_invalid: bool) -> (Vec, f64) { + let mut low = local_low; + let mut middle = (local_low + local_high) / 2.; + let mut high = local_high; + let mut previous_high = local_high; + + let mut iterations = 0; + let mut previous_arc = CircleArc::default(); + let mut was_previous_good = false; + let mut arcs = Vec::new(); + + // Outer loop to iterate over the curve + while low < local_high { + // Inner loop to find the next maximal segment of the curve that can be approximated with a circular arc + while iterations <= max_iterations { + iterations += 1; + let p1 = self.evaluate(low); + let p2 = self.evaluate(middle); + let p3 = self.evaluate(high); + + let wrapped_center = utils::compute_circle_center_from_points(p1, p2, p3); + // If the segment is linear, move on to next segment + if wrapped_center.is_none() { + previous_high = high; + low = high; + high = 1.; + middle = (low + high) / 2.; + was_previous_good = false; + break; + } + + let center = wrapped_center.unwrap(); + let radius = center.distance(p1); + + let angle_p1 = DVec2::new(1., 0.).angle_between(p1 - center); + let angle_p2 = DVec2::new(1., 0.).angle_between(p2 - center); + let angle_p3 = DVec2::new(1., 0.).angle_between(p3 - center); + + let mut start_angle = angle_p1; + let mut end_angle = angle_p3; + + // Adjust start and end angles of the arc to ensure that it travels in the counter-clockwise direction + if angle_p1 < angle_p3 { + if angle_p2 < angle_p1 || angle_p3 < angle_p2 { + std::mem::swap(&mut start_angle, &mut end_angle); + } + } else if angle_p2 < angle_p1 && angle_p3 < angle_p2 { + std::mem::swap(&mut start_angle, &mut end_angle); + } + + let new_arc = CircleArc { + center, + radius, + start_angle, + end_angle, + }; + + // Use points in between low, middle, and high to evaluate how well the arc approximates the curve + let e1 = self.evaluate((low + middle) / 2.); + let e2 = self.evaluate((middle + high) / 2.); + + // Iterate until we find the largest good approximation such that the next iteration is not a good approximation with an arc + if utils::f64_compare(radius, e1.distance(center), error) && utils::f64_compare(radius, e2.distance(center), error) { + // Check if the good approximation is actually valid: the sector angle cannot be larger than 180 degrees (PI radians) + let mut sector_angle = end_angle - start_angle; + if sector_angle < 0. { + sector_angle += 2. * PI; + } + if stop_when_invalid && sector_angle > PI { + return (arcs, low); + } + if high == local_high { + // Found the final arc approximation + arcs.push(new_arc); + low = high; + break; + } + // If the approximation is good, expand the segment by half to try finding a larger good approximation + previous_high = high; + high = (high + (high - low) / 2.).min(local_high); + middle = (low + high) / 2.; + previous_arc = new_arc; + was_previous_good = true; + } else if was_previous_good { + // If the previous approximation was good and the current one is bad, then we use the previous good approximation + arcs.push(previous_arc); + + // Continue searching for approximations for the rest of the curve + low = previous_high; + high = local_high; + middle = low + (high - low) / 2.; + was_previous_good = false; + break; + } else { + // If no good approximation has been seen yet, try again with half the segment + previous_high = high; + high = middle; + middle = low + (high - low) / 2.; + previous_arc = new_arc; + } + } + } + + (arcs, low) + } + /// Return the min and max corners that represent the bounding box of the curve. pub fn bounding_box(&self) -> [DVec2; 2] { // Start by taking min/max of endpoints. @@ -1049,6 +1183,14 @@ mod tests { .all(|(&a, b)| compare_vector_of_points(a.get_points().collect::>(), b.to_vec())) } + // Compare circle arcs by allowing some maximum absolute difference between values to account for floating point errors + fn compare_arcs(arc1: CircleArc, arc2: CircleArc) -> bool { + compare_points(arc1.center, arc2.center) + && utils::f64_compare(arc1.radius, arc1.radius, MAX_ABSOLUTE_DIFFERENCE) + && utils::f64_compare(arc1.start_angle, arc2.start_angle, MAX_ABSOLUTE_DIFFERENCE) + && utils::f64_compare(arc1.end_angle, arc2.end_angle, MAX_ABSOLUTE_DIFFERENCE) + } + // Compare vectors of points with some maximum allowed absolute difference between the values fn compare_vec_of_points(vec1: Vec, vec2: Vec, max_absolute_difference: f64) -> bool { vec1.into_iter().zip(vec2).all(|(p1, p2)| p1.abs_diff_eq(p2, max_absolute_difference)) @@ -1097,6 +1239,7 @@ mod tests { let bezier2 = Bezier::from_quadratic_coordinates(0., 0., 0., 100., 100., 100.); assert!(bezier2.evaluate(bezier2.project(DVec2::new(100., 0.), project_options)) == DVec2::new(0., 0.)); } + #[test] fn test_intersect_line_segment_linear() { let p1 = DVec2::new(30., 60.); @@ -1232,4 +1375,73 @@ mod tests { .zip(helper_t_values.windows(2)) .all(|(curve, t_pair)| curve.abs_diff_eq(&bezier.trim(t_pair[0], t_pair[1]), MAX_ABSOLUTE_DIFFERENCE))) } + + #[test] + fn test_arcs_linear() { + let bezier = Bezier::from_linear_coordinates(30., 60., 140., 120.); + let linear_arcs = bezier.arcs(ArcsOptions::default()); + assert!(linear_arcs.is_empty()); + } + + #[test] + fn test_arcs_quadratic() { + let bezier1 = Bezier::from_quadratic_coordinates(30., 30., 50., 50., 100., 100.); + assert!(bezier1.arcs(ArcsOptions::default()).is_empty()); + + let bezier2 = Bezier::from_quadratic_coordinates(50., 50., 85., 65., 100., 100.); + let actual_arcs = bezier2.arcs(ArcsOptions::default()); + let expected_arc = CircleArc { + center: DVec2::new(15., 135.), + radius: 91.92388, + start_angle: -1.18019, + end_angle: -0.39061, + }; + assert_eq!(actual_arcs.len(), 1); + assert!(compare_arcs(actual_arcs[0], expected_arc)); + } + + #[test] + fn test_arcs_cubic() { + let bezier = Bezier::from_cubic_coordinates(30., 30., 30., 80., 60., 80., 60., 140.); + let actual_arcs = bezier.arcs(ArcsOptions::default()); + let expected_arcs = vec![ + CircleArc { + center: DVec2::new(122.394877, 30.7777189), + radius: 92.39815, + start_angle: 2.5637146, + end_angle: -3.1331755, + }, + CircleArc { + center: DVec2::new(-47.54881, 136.169378), + radius: 107.61701, + start_angle: -0.53556, + end_angle: 0.0356025, + }, + ]; + + assert_eq!(actual_arcs.len(), 2); + assert!(compare_arcs(actual_arcs[0], expected_arcs[0])); + assert!(compare_arcs(actual_arcs[1], expected_arcs[1])); + + // Bezier that contains the erroneous case when maximizing arcs + let bezier2 = Bezier::from_cubic_coordinates(48., 176., 170., 10., 30., 90., 180., 160.); + let auto_arcs = bezier2.arcs(ArcsOptions::default()); + + let extrema_arcs = bezier2.arcs(ArcsOptions { + strategy: ArcStrategy::FavorCorrectness, + ..ArcsOptions::default() + }); + + let maximal_arcs = bezier2.arcs(ArcsOptions { + strategy: ArcStrategy::FavorLargerArcs, + ..ArcsOptions::default() + }); + + // Resulting automatic arcs match the maximal results until the bad arc (in this case, only index 0 should match) + assert_eq!(auto_arcs[0], maximal_arcs[0]); + // Check that the first result from MaximizeArcs::Automatic should not equal the first results from MaximizeArcs::Off + assert_ne!(auto_arcs[0], extrema_arcs[0]); + // The remaining results (index 2 onwards) should match the results where MaximizeArcs::Off from the next extrema point onwards (after index 2). + assert!(auto_arcs.iter().skip(2).zip(extrema_arcs.iter().skip(2)).all(|(arc1, arc2)| compare_arcs(*arc1, *arc2))); + } } diff --git a/bezier-rs/lib/src/structs.rs b/bezier-rs/lib/src/structs.rs new file mode 100644 index 00000000..60a36b3c --- /dev/null +++ b/bezier-rs/lib/src/structs.rs @@ -0,0 +1,95 @@ +use glam::DVec2; +use std::fmt::{Debug, Formatter, Result}; + +/// Struct to represent optional parameters that can be passed to the `project` function. +#[derive(Copy, Clone)] +pub struct ProjectionOptions { + /// Size of the lookup table for the initial passthrough. The default value is `20`. + pub lut_size: usize, + /// Difference used between floating point numbers to be considered as equal. The default value is `0.0001` + pub convergence_epsilon: f64, + /// Controls the number of iterations needed to consider that minimum distance to have converged. The default value is `3`. + pub convergence_limit: usize, + /// Controls the maximum total number of iterations to be used. The default value is `10`. + pub iteration_limit: usize, +} + +impl Default for ProjectionOptions { + fn default() -> Self { + ProjectionOptions { + lut_size: 20, + convergence_epsilon: 1e-4, + convergence_limit: 3, + iteration_limit: 10, + } + } +} + +/// Struct used to represent the different strategies for generating arc approximations. +#[derive(Copy, Clone)] +pub enum ArcStrategy { + /// Start with the greedy strategy of maximizing arc approximations and automatically switch to the divide-and-conquer when the greedy approximations no longer fall within the error bound. + Automatic, + /// Use the greedy strategy to maximize approximated arcs, despite potentially erroneous arcs. + FavorLargerArcs, + /// Use the divide-and-conquer strategy that prioritizes correctness over maximal arcs. + FavorCorrectness, +} + +/// Struct to represent optional parameters that can be passed to the `arcs` function. +#[derive(Copy, Clone)] +pub struct ArcsOptions { + /// Determines how the approximated arcs are computed. + /// When maximizing the arcs, the algorithm may return incorrect arcs when the curve contains any small loops or segments that look like a very thin "U". + /// The enum options behave as follows: + /// - `Automatic`: Maximize arcs until an erroneous approximation is found. Compute the arcs of the rest of the curve by first splitting on extremas to ensure no more erroneous cases are encountered. + /// - `FavorLargerArcs`: Maximize arcs using the original algorithm from the [Approximating a Bezier curve with circular arcs](https://pomax.github.io/bezierinfo/#arcapproximation) section of Pomax's bezier curve primer. Erroneous arcs are possible. + /// - `FavorCorrectness`: Prioritize correctness by first spliting the curve by its extremas and determine the arc approximation of each segment instead. + /// + /// The default value is `Automatic`. + pub strategy: ArcStrategy, + /// The error used for approximating the arc's fit. The default is `0.5`. + pub error: f64, + /// The maximum number of segment iterations used as attempts for arc approximations. The default is `100`. + pub max_iterations: usize, +} + +impl Default for ArcsOptions { + fn default() -> Self { + ArcsOptions { + strategy: ArcStrategy::Automatic, + error: 0.5, + max_iterations: 100, + } + } +} + +/// Struct to represent the circular arc approximation used in the `arcs` bezier function. +#[derive(Copy, Clone, PartialEq)] +pub struct CircleArc { + /// The center point of the circle. + pub center: DVec2, + /// The radius of the circle. + pub radius: f64, + /// The start angle of the circle sector in rad. + pub start_angle: f64, + /// The end angle of the circle sector in rad. + pub end_angle: f64, +} + +impl Debug for CircleArc { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "Center: {}, radius: {}, start to end angles: {} to {}", self.center, self.radius, self.start_angle, self.end_angle) + } +} + +impl Default for CircleArc { + fn default() -> Self { + CircleArc { + center: DVec2::ZERO, + radius: 0., + start_angle: 0., + end_angle: 0., + } + } +} diff --git a/bezier-rs/lib/src/subpath/lookup.rs b/bezier-rs/lib/src/subpath/lookup.rs index d1051eaf..66f58b4c 100644 --- a/bezier-rs/lib/src/subpath/lookup.rs +++ b/bezier-rs/lib/src/subpath/lookup.rs @@ -4,7 +4,7 @@ use super::*; impl Subpath { /// Return the sum of the approximation of the length of each `Bezier` curve along the `Subpath`. /// - `num_subdivisions` - Number of subdivisions used to approximate the curve. The default value is `1000`. - pub fn length(&self, num_subdivisions: Option) -> f64 { + pub fn length(&self, num_subdivisions: Option) -> f64 { self.iter().fold(0., |accumulator, bezier| accumulator + bezier.length(num_subdivisions)) } } diff --git a/bezier-rs/lib/src/utils.rs b/bezier-rs/lib/src/utils.rs index 7d122502..2b57e2f0 100644 --- a/bezier-rs/lib/src/utils.rs +++ b/bezier-rs/lib/src/utils.rs @@ -1,6 +1,6 @@ use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE}; -use glam::{BVec2, DVec2}; +use glam::{BVec2, DMat2, DVec2}; use std::f64::consts::PI; /// Helper to perform the computation of a and c, where b is the provided point on the curve. @@ -34,10 +34,10 @@ pub fn compute_abc_for_cubic_through_points(start_point: DVec2, point_on_curve: } /// Return the index and the value of the closest point in the LUT compared to the provided point. -pub fn get_closest_point_in_lut(lut: &[DVec2], point: DVec2) -> (i32, f64) { +pub fn get_closest_point_in_lut(lut: &[DVec2], point: DVec2) -> (usize, f64) { lut.iter() .enumerate() - .map(|(i, p)| (i as i32, point.distance_squared(*p))) + .map(|(i, p)| (i, point.distance_squared(*p))) .min_by(|x, y| (&(x.1)).partial_cmp(&(y.1)).unwrap()) .unwrap() } @@ -181,6 +181,33 @@ pub fn line_intersection(point1: DVec2, point1_slope_vector: DVec2, point2: DVec } } +/// Check if 3 points are collinear. +pub fn are_points_collinear(p1: DVec2, p2: DVec2, p3: DVec2) -> bool { + let matrix = DMat2::from_cols(p1 - p2, p2 - p3); + f64_compare(matrix.determinant() / 2., 0., MAX_ABSOLUTE_DIFFERENCE) +} + +/// Compute the center of the circle that passes through all three provided points. The provided points cannot be collinear. +pub fn compute_circle_center_from_points(p1: DVec2, p2: DVec2, p3: DVec2) -> Option { + if are_points_collinear(p1, p2, p3) { + return None; + } + + let midpoint_a = p1.lerp(p2, 0.5); + let midpoint_b = p2.lerp(p3, 0.5); + let midpoint_c = p3.lerp(p1, 0.5); + + let tangent_a = (p1 - p2).perp(); + let tangent_b = (p2 - p3).perp(); + let tangent_c = (p3 - p1).perp(); + + let intersect_a_b = line_intersection(midpoint_a, tangent_a, midpoint_b, tangent_b); + let intersect_b_c = line_intersection(midpoint_b, tangent_b, midpoint_c, tangent_c); + let intersect_c_a = line_intersection(midpoint_c, tangent_c, midpoint_a, tangent_a); + + Some((intersect_a_b + intersect_b_c + intersect_c_a) / 3.) +} + /// Compare two `f64` numbers with a provided max absolute value difference. pub fn f64_compare(f1: f64, f2: f64, max_abs_diff: f64) -> bool { (f1 - f2).abs() < max_abs_diff @@ -287,4 +314,20 @@ mod tests { let end_direction2 = DVec2::new(1., -1.); assert!(line_intersection(start2, start_direction2, end2, end_direction2) == DVec2::new(4., 4.)); } + + #[test] + fn test_are_points_collinear() { + assert!(are_points_collinear(DVec2::new(2., 4.), DVec2::new(6., 8.), DVec2::new(4., 6.))); + assert!(!are_points_collinear(DVec2::new(1., 4.), DVec2::new(6., 8.), DVec2::new(4., 6.))); + } + + #[test] + fn test_compute_circle_center_from_points() { + // 3/4 of unit circle + let center1 = compute_circle_center_from_points(DVec2::new(0., 1.), DVec2::new(-1., 0.), DVec2::new(1., 0.)); + assert_eq!(center1.unwrap(), DVec2::new(0., 0.)); + // 1/4 of unit circle + let center2 = compute_circle_center_from_points(DVec2::new(-1., 0.), DVec2::new(0., 1.), DVec2::new(1., 0.)); + assert_eq!(center2.unwrap(), DVec2::new(0., 0.)); + } } diff --git a/editor/src/messages/portfolio/document/utility_types/vectorize_layer_metadata.rs b/editor/src/messages/portfolio/document/utility_types/vectorize_layer_metadata.rs index d18f826b..a32ebb60 100644 --- a/editor/src/messages/portfolio/document/utility_types/vectorize_layer_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/vectorize_layer_metadata.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::iter::FromIterator; /// Necessary because serde can't serialize hashmaps when the keys don't implement display. -pub fn serialize<'a, T, K, V, S>(target: T, ser: S) -> Result +pub fn serialize<'a, T, K, V, S>(target: T, serializer: S) -> Result where S: Serializer, T: IntoIterator, @@ -10,16 +10,16 @@ where V: Serialize + 'a, { let container: Vec<_> = target.into_iter().collect(); - serde::Serialize::serialize(&container, ser) + serde::Serialize::serialize(&container, serializer) } -pub fn deserialize<'de, T, K, V, D>(des: D) -> Result +pub fn deserialize<'de, T, K, V, D>(deserializer: D) -> Result where D: Deserializer<'de>, T: FromIterator<(K, V)>, K: Deserialize<'de>, V: Deserialize<'de>, { - let container: Vec<_> = serde::Deserialize::deserialize(des)?; + let container: Vec<_> = serde::Deserialize::deserialize(deserializer)?; Ok(T::from_iter(container.into_iter())) } diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index edd30447..00000000 --- a/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./bezier-rs/docs/interactive-docs/tsconfig.json" -}