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"
-}