diff --git a/bezier-rs/docs/interactive-docs/package.json b/bezier-rs/docs/interactive-docs/package.json index b2bfc6291..6c78bbe69 100644 --- a/bezier-rs/docs/interactive-docs/package.json +++ b/bezier-rs/docs/interactive-docs/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { + "start": "vue-cli-service serve", "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" diff --git a/bezier-rs/docs/interactive-docs/public/favicon.ico b/bezier-rs/docs/interactive-docs/public/favicon.ico deleted file mode 100644 index df36fcfb7..000000000 Binary files a/bezier-rs/docs/interactive-docs/public/favicon.ico and /dev/null differ diff --git a/bezier-rs/docs/interactive-docs/public/index.html b/bezier-rs/docs/interactive-docs/public/index.html index 3e5a13962..00999673d 100644 --- a/bezier-rs/docs/interactive-docs/public/index.html +++ b/bezier-rs/docs/interactive-docs/public/index.html @@ -1,17 +1,16 @@ - - - - - - - <%= htmlWebpackPlugin.options.title %> - - - -
- - + + + + + Bezier-rs Interactive Docs + + + + +
+ + diff --git a/bezier-rs/docs/interactive-docs/src/App.vue b/bezier-rs/docs/interactive-docs/src/App.vue index ac010d3cd..1fc5f8362 100644 --- a/bezier-rs/docs/interactive-docs/src/App.vue +++ b/bezier-rs/docs/interactive-docs/src/App.vue @@ -12,8 +12,6 @@ :cubicOptions="feature.cubicOptions" /> -
-
@@ -26,21 +24,6 @@ import { Point, WasmBezierInstance } from "@/utils/types"; import ExamplePane from "@/components/ExamplePane.vue"; import SliderExample from "@/components/SliderExample.vue"; -// eslint-disable-next-line -const testBezierLib = async () => { - import("@/../wasm/pkg").then((wasm) => { - const bezier = wasm.WasmBezier.new_quadratic([ - [0, 0], - [50, 0], - [100, 100], - ]); - const svgContainer = document.getElementById("svg-test"); - if (svgContainer) { - svgContainer.innerHTML = bezier.to_svg(); - } - }); -}; - const tSliderOptions = { min: 0, max: 1, @@ -49,6 +32,8 @@ const tSliderOptions = { variable: "t", }; +const SCALE_UNIT_VECTOR_FACTOR = 50; + export default defineComponent({ name: "App", components: { @@ -63,7 +48,7 @@ export default defineComponent({ callback: (): void => {}, }, { - name: "Bezier through points", + name: "Bezier Through Points", // eslint-disable-next-line callback: (): void => {}, createThroughPoints: true, @@ -137,26 +122,20 @@ export default defineComponent({ }, }, { - name: "Derivative", + name: "Tangent", callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record): void => { const context = getContextFromCanvas(canvas); const intersection = JSON.parse(bezier.compute(options.t)); - const derivative = JSON.parse(bezier.derivative(options.t)); - const curveFactor = bezier.get_points().length - 1; + const tangent = JSON.parse(bezier.tangent(options.t)); - const tangentStart = { - x: intersection.x - derivative.x / curveFactor, - y: intersection.y - derivative.y / curveFactor, - }; const tangentEnd = { - x: intersection.x + derivative.x / curveFactor, - y: intersection.y + derivative.y / curveFactor, + x: intersection.x + tangent.x * SCALE_UNIT_VECTOR_FACTOR, + y: intersection.y + tangent.y * SCALE_UNIT_VECTOR_FACTOR, }; - drawLine(context, tangentStart, tangentEnd, COLORS.NON_INTERACTIVE.STROKE_1); - drawPoint(context, tangentStart, 3, COLORS.NON_INTERACTIVE.STROKE_1); drawPoint(context, intersection, 3, COLORS.NON_INTERACTIVE.STROKE_1); + drawLine(context, intersection, tangentEnd, COLORS.NON_INTERACTIVE.STROKE_1); drawPoint(context, tangentEnd, 3, COLORS.NON_INTERACTIVE.STROKE_1); }, template: markRaw(SliderExample), @@ -170,18 +149,13 @@ export default defineComponent({ const intersection = JSON.parse(bezier.compute(options.t)); const normal = JSON.parse(bezier.normal(options.t)); - const normalStart = { - x: intersection.x - normal.x * 20, - y: intersection.y - normal.y * 20, - }; const normalEnd = { - x: intersection.x + normal.x * 20, - y: intersection.y + normal.y * 20, + x: intersection.x - normal.x * SCALE_UNIT_VECTOR_FACTOR, + y: intersection.y - normal.y * SCALE_UNIT_VECTOR_FACTOR, }; - drawLine(context, normalStart, normalEnd, COLORS.NON_INTERACTIVE.STROKE_1); - drawPoint(context, normalStart, 3, COLORS.NON_INTERACTIVE.STROKE_1); drawPoint(context, intersection, 3, COLORS.NON_INTERACTIVE.STROKE_1); + drawLine(context, intersection, normalEnd, COLORS.NON_INTERACTIVE.STROKE_1); drawPoint(context, normalEnd, 3, COLORS.NON_INTERACTIVE.STROKE_1); }, template: markRaw(SliderExample), @@ -240,14 +214,16 @@ export default defineComponent({ name: "Local Extrema", callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => { const context = getContextFromCanvas(canvas); - const dimensionColors = [COLORS.NON_INTERACTIVE.STROKE_1, COLORS.NON_INTERACTIVE.STROKE_2]; + const dimensionColors = ["red", "green"]; const extrema: number[][] = JSON.parse(bezier.local_extrema()); extrema.forEach((tValues, index) => { tValues.forEach((t) => { - const point = JSON.parse(bezier.compute(t)); + const point: Point = JSON.parse(bezier.compute(t)); drawPoint(context, point, 4, dimensionColors[index]); }); }); + drawText(getContextFromCanvas(canvas), "X extrema", 5, canvas.height - 20, dimensionColors[0]); + drawText(getContextFromCanvas(canvas), "Y extrema", 5, canvas.height - 5, dimensionColors[1]); }, }, { @@ -255,7 +231,7 @@ export default defineComponent({ callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record): void => { const context = getContextFromCanvas(canvas); const rotatedBezier = bezier - .rotate((options.angle * Math.PI) / 180) + .rotate(options.angle * Math.PI) .get_points() .map((p) => JSON.parse(p)); drawBezier(context, rotatedBezier, null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 }); @@ -265,25 +241,26 @@ export default defineComponent({ sliders: [ { variable: "angle", - min: -90, - max: 90, - step: 5, - default: 15, + min: 0, + max: 2, + step: 1 / 16, + default: 1 / 8, + unit: "π", }, ], }, }, { - name: "Line Intersection", + name: "Intersect Line Segment", callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => { const context = getContextFromCanvas(canvas); const line = [ { x: 150, y: 150 }, - { x: 30, y: 30 }, + { x: 20, y: 20 }, ]; const mappedLine = line.map((p) => [p.x, p.y]); drawLine(context, line[0], line[1], COLORS.NON_INTERACTIVE.STROKE_1); - const intersections: Point[] = bezier.line_intersection(mappedLine).map((p) => JSON.parse(p)); + const intersections: Point[] = bezier.intersect_line_segment(mappedLine).map((p) => JSON.parse(p)); intersections.forEach((p: Point) => { drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2); }); diff --git a/bezier-rs/docs/interactive-docs/src/components/SliderExample.vue b/bezier-rs/docs/interactive-docs/src/components/SliderExample.vue index 4b6cc3ede..9d0fe2487 100644 --- a/bezier-rs/docs/interactive-docs/src/components/SliderExample.vue +++ b/bezier-rs/docs/interactive-docs/src/components/SliderExample.vue @@ -2,7 +2,7 @@
-
{{ slider.variable }} = {{ sliderData[slider.variable] }}
+
{{ slider.variable }} = {{ sliderData[slider.variable] }}{{ sliderUnits[slider.variable] }}
@@ -43,6 +43,7 @@ export default defineComponent({ const sliders = this.templateOptions.sliders; return { sliderData: Object.assign({}, ...sliders.map((s) => ({ [s.variable]: s.default }))), + sliderUnits: Object.assign({}, ...sliders.map((s) => ({ [s.variable]: s.unit }))), }; }, }); diff --git a/bezier-rs/docs/interactive-docs/src/utils/types.ts b/bezier-rs/docs/interactive-docs/src/utils/types.ts index 694d78eb8..def7320e8 100644 --- a/bezier-rs/docs/interactive-docs/src/utils/types.ts +++ b/bezier-rs/docs/interactive-docs/src/utils/types.ts @@ -12,6 +12,7 @@ export type SliderOption = { step: number; default: number; variable: string; + unit?: string; }; export type TemplateOption = { diff --git a/bezier-rs/docs/interactive-docs/wasm/src/lib.rs b/bezier-rs/docs/interactive-docs/wasm/src/lib.rs index f9a46b54e..8f492f55a 100644 --- a/bezier-rs/docs/interactive-docs/wasm/src/lib.rs +++ b/bezier-rs/docs/interactive-docs/wasm/src/lib.rs @@ -1,4 +1,4 @@ -use bezier_rs::Bezier; +use bezier_rs::{Bezier, ProjectionOptions}; use glam::DVec2; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; @@ -35,12 +35,12 @@ impl WasmBezier { pub fn quadratic_through_points(js_points: &JsValue, t: f64) -> WasmBezier { let points: [DVec2; 3] = js_points.into_serde().unwrap(); - WasmBezier(Bezier::quadratic_through_points(points[0], points[1], points[2], t)) + WasmBezier(Bezier::quadratic_through_points(points[0], points[1], points[2], Some(t))) } pub fn cubic_through_points(js_points: &JsValue, t: f64, midpoint_separation: f64) -> WasmBezier { let points: [DVec2; 3] = js_points.into_serde().unwrap(); - WasmBezier(Bezier::cubic_through_points(points[0], points[1], points[2], t, midpoint_separation)) + WasmBezier(Bezier::cubic_through_points(points[0], points[1], points[2], Some(t), Some(midpoint_separation))) } pub fn set_start(&mut self, x: f64, y: f64) { @@ -79,8 +79,8 @@ impl WasmBezier { self.0.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect() } - pub fn derivative(&self, t: f64) -> JsValue { - vec_to_point(&self.0.derivative(t)) + pub fn tangent(&self, t: f64) -> JsValue { + vec_to_point(&self.0.tangent(t)) } pub fn normal(&self, t: f64) -> JsValue { @@ -100,7 +100,7 @@ impl WasmBezier { } pub fn project(&self, x: f64, y: f64) -> JsValue { - vec_to_point(&self.0.project(DVec2::new(x, y), 20, 1e-4, 3, 10)) + vec_to_point(&self.0.project(DVec2::new(x, y), ProjectionOptions::default())) } pub fn local_extrema(&self) -> JsValue { @@ -112,8 +112,8 @@ impl WasmBezier { WasmBezier(self.0.rotate(angle)) } - pub fn line_intersection(&self, js_points: &JsValue) -> Vec { + pub fn intersect_line_segment(&self, js_points: &JsValue) -> Vec { let line: [DVec2; 2] = js_points.into_serde().unwrap(); - self.0.line_intersection(line).iter().map(|&p| vec_to_point(&p)).collect::>() + self.0.intersect_line_segment(line).iter().map(|&p| vec_to_point(&p)).collect::>() } } diff --git a/bezier-rs/lib/src/consts.rs b/bezier-rs/lib/src/consts.rs new file mode 100644 index 000000000..51dd92c34 --- /dev/null +++ b/bezier-rs/lib/src/consts.rs @@ -0,0 +1,14 @@ +/// 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; + +/// Number of subdivisions used in `length` calculation +pub const LENGTH_SUBDIVISIONS: i32 = 1000; + +/// Number of distances used in search algorithm for `project` +pub const NUM_DISTANCES: usize = 5; + +/// Constants used to determine if `f64`'s are equivalent +pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3; diff --git a/bezier-rs/lib/src/lib.rs b/bezier-rs/lib/src/lib.rs index b0d8103a9..7faa0f82f 100644 --- a/bezier-rs/lib/src/lib.rs +++ b/bezier-rs/lib/src/lib.rs @@ -1,18 +1,20 @@ //! Bezier-rs: A Bezier Math Library for Rust +mod consts; +use consts::*; +mod utils; + use glam::{DMat2, DVec2}; -mod utils; - -/// Representation of the handle point(s) in a bezier segment. +/// Representation of the handle point(s) in a bezier curve. #[derive(Copy, Clone)] -pub enum BezierHandles { - /// Handles for a quadratic segment. +enum BezierHandles { + /// Handles for a quadratic curve. Quadratic { /// Point representing the location of the single handle. handle: DVec2, }, - /// Handles for a cubic segment. + /// Handles for a cubic curve. Cubic { /// Point representing the location of the handle associated to the start point. handle_start: DVec2, @@ -21,14 +23,38 @@ pub enum BezierHandles { }, } -/// Representation of a bezier segment with 2D points. +/// 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)] pub struct Bezier { - /// Start point of the bezier segment. + /// Start point of the bezier curve. start: DVec2, - /// Start point of the bezier segment. + /// Start point of the bezier curve. end: DVec2, - /// Handles of the bezier segment. + /// Handles of the bezier curve. handles: BezierHandles, } @@ -75,9 +101,11 @@ impl Bezier { } /// Create a quadratic bezier curve that goes through 3 points, where the middle point will be at the corresponding position `t` on the curve. + /// - `t` - A representation of how far along the curve the provided point should occur at. The default value is 0.5. /// Note that when `t = 0` or `t = 1`, the expectation is that the `point_on_curve` should be equal to `start` and `end` respectively. /// In these cases, if the provided values are not equal, this function will use the `point_on_curve` as the `start`/`end` instead. - pub fn quadratic_through_points(start: DVec2, point_on_curve: DVec2, end: DVec2, t: f64) -> Self { + pub fn quadratic_through_points(start: DVec2, point_on_curve: DVec2, end: DVec2, t: Option) -> Self { + let t = t.unwrap_or(DEFAULT_T_VALUE); if t == 0. { return Bezier::from_quadratic_dvec2(point_on_curve, point_on_curve, end); } @@ -89,17 +117,20 @@ impl Bezier { } /// Create a cubic bezier curve that goes through 3 points, where the middle point will be at the corresponding position `t` on the curve. + /// - `t` - A representation of how far along the curve the provided point should occur at. The default value is 0.5. /// Note that when `t = 0` or `t = 1`, the expectation is that the `point_on_curve` should be equal to `start` and `end` respectively. /// In these cases, if the provided values are not equal, this function will use the `point_on_curve` as the `start`/`end` instead. - /// - `midpoint_separation` is a representation of the how wide the resulting curve will be around `t` on the curve. This parameter designates the distance between the `e1` and `e2` defined in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer. - pub fn cubic_through_points(start: DVec2, point_on_curve: DVec2, end: DVec2, t: f64, midpoint_separation: f64) -> Self { + /// - `midpoint_separation` - A representation of how wide the resulting curve will be around `t` on the curve. This parameter designates the distance between the `e1` and `e2` defined in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer. It is an optional parameter and the default value is the distance between the points `B` and `C` defined in the primer. + pub fn cubic_through_points(start: DVec2, point_on_curve: DVec2, end: DVec2, t: Option, midpoint_separation: Option) -> Self { + let t = t.unwrap_or(DEFAULT_T_VALUE); if t == 0. { return Bezier::from_cubic_dvec2(point_on_curve, point_on_curve, end, end); } if t == 1. { return Bezier::from_cubic_dvec2(start, start, point_on_curve, point_on_curve); } - let [a, b, _] = utils::compute_abc_for_cubic_through_points(start, point_on_curve, end, t); + let [a, b, c] = utils::compute_abc_for_cubic_through_points(start, point_on_curve, end, t); + let midpoint_separation = midpoint_separation.unwrap_or_else(|| b.distance(c)); let distance_between_start_and_end = (end - start) / (start.distance(end)); let e1 = b - (distance_between_start_and_end * midpoint_separation); let e2 = b + (distance_between_start_and_end * midpoint_separation * (1. - t) / t); @@ -113,8 +144,8 @@ impl Bezier { } /// Convert to SVG. - // TODO: Allow modifying the viewport, width and height pub fn to_svg(&self) -> String { + // TODO: Allow modifying the viewport, width and height let m_path = format!("M {} {}", self.start.x, self.start.y); let handles_path = match self.handles { BezierHandles::Quadratic { handle } => { @@ -228,7 +259,7 @@ impl Bezier { /// 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 { - let steps_unwrapped = steps.unwrap_or(10); + 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); @@ -240,14 +271,13 @@ impl Bezier { } /// Return an approximation of the length of the bezier curve. - /// Code example from . pub fn length(&self) -> f64 { + // Code example from . + // 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 - const SUBDIVISIONS: i32 = 1000; - - let lookup_table = self.compute_lookup_table(Some(SUBDIVISIONS)); + let lookup_table = self.compute_lookup_table(Some(LENGTH_SUBDIVISIONS)); let mut approx_curve_length = 0.0; let mut prev_point = lookup_table[0]; // calculate approximate distance between subdivision @@ -340,12 +370,15 @@ impl Bezier { } /// Returns the closest point on the curve to the provided point. - /// Uses a searching algorithm akin to binary search that can be customized using the following parameters: - /// - `lut_size` - Size of the lookup table for the initial passthrough. - /// - `convergence_epsilon` - Difference used between floating point numbers to be considered as equal. - /// - `convergence_limit` - Controls the number of iterations needed to consider that minimum distance to have converged. - /// - `iteration_limit` - Controls the maximum total number of iterations to be used. - pub fn project(&self, point: DVec2, lut_size: i32, convergence_epsilon: f64, convergence_limit: i32, iteration_limit: i32) -> DVec2 { + /// Uses a searching algorithm akin to binary search that can be customized using the [ProjectionOptions] structure. + pub fn project(&self, point: DVec2, options: ProjectionOptions) -> DVec2 { + let ProjectionOptions { + lut_size, + convergence_epsilon, + convergence_limit, + iteration_limit, + } = options; + // First find the closest point from the results of a lookup table let lut = self.compute_lookup_table(Some(lut_size)); let (minimum_position, minimum_distance) = utils::get_closest_point_in_lut(&lut, point); @@ -367,8 +400,8 @@ impl Bezier { let mut iteration_count = 0; // Counter to identify how many iterations have had a similar result. Used for convergence test let mut convergence_count = 0; + // Store calculated distances to minimize unnecessary recomputations - const NUM_DISTANCES: usize = 5; let mut distances: [f64; NUM_DISTANCES] = [ point.distance(lut[0.max(minimum_position - 1) as usize]), 0., @@ -491,7 +524,7 @@ impl Bezier { /// Returns a list of points where the provided line segment intersects with the Bezier curve. /// - `line` - A line segment expected to be received in the format of `[start_point, end_point]`. - pub fn line_intersection(&self, line: [DVec2; 2]) -> Vec { + pub fn intersect_line_segment(&self, line: [DVec2; 2]) -> Vec { // Rotate the bezier and the line by the angle that the line makes with the x axis let slope = line[1] - line[0]; let angle = slope.angle_between(DVec2::new(1., 0.)); @@ -527,25 +560,26 @@ impl Bezier { }; let min = line[0].min(line[1]); let max = line[0].max(line[1]); - let max_abs_diff = 1e-4; list_intersection_t .iter() - .filter(|&&t| utils::f64_approximately_in_range(t, 0., 1., max_abs_diff)) + .filter(|&&t| utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE)) .map(|&t| self.unrestricted_compute(t)) - .filter(|&point| utils::dvec2_approximately_in_range(point, min, max, max_abs_diff).all()) + .filter(|&point| utils::dvec2_approximately_in_range(point, min, max, MAX_ABSOLUTE_DIFFERENCE).all()) .collect::>() } } #[cfg(test)] mod tests { + use super::*; + use crate::consts::MAX_ABSOLUTE_DIFFERENCE; use crate::utils; - use crate::Bezier; + use glam::DVec2; fn compare_points(p1: DVec2, p2: DVec2) -> bool { - utils::dvec2_compare(p1, p2, 1e-3).all() + utils::dvec2_compare(p1, p2, MAX_ABSOLUTE_DIFFERENCE).all() } #[test] @@ -554,13 +588,13 @@ mod tests { let p2 = DVec2::new(140., 30.); let p3 = DVec2::new(160., 170.); - let bezier1 = Bezier::quadratic_through_points(p1, p2, p3, 0.5); + let bezier1 = Bezier::quadratic_through_points(p1, p2, p3, None); assert!(compare_points(bezier1.compute(0.5), p2)); - let bezier2 = Bezier::quadratic_through_points(p1, p2, p3, 0.8); + let bezier2 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.8)); assert!(compare_points(bezier2.compute(0.8), p2)); - let bezier3 = Bezier::quadratic_through_points(p1, p2, p3, 0.); + let bezier3 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.)); assert!(compare_points(bezier3.compute(0.), p2)); } @@ -570,28 +604,30 @@ mod tests { let p2 = DVec2::new(60., 140.); let p3 = DVec2::new(160., 160.); - let bezier1 = Bezier::cubic_through_points(p1, p2, p3, 0.3, 10.); + let bezier1 = Bezier::cubic_through_points(p1, p2, p3, Some(0.3), Some(10.)); assert!(compare_points(bezier1.compute(0.3), p2)); - let bezier2 = Bezier::cubic_through_points(p1, p2, p3, 0.8, 91.7); + let bezier2 = Bezier::cubic_through_points(p1, p2, p3, Some(0.8), Some(91.7)); assert!(compare_points(bezier2.compute(0.8), p2)); - let bezier3 = Bezier::cubic_through_points(p1, p2, p3, 0., 91.7); + let bezier3 = Bezier::cubic_through_points(p1, p2, p3, Some(0.), Some(91.7)); assert!(compare_points(bezier3.compute(0.), p2)); } #[test] fn project() { + let project_options = ProjectionOptions::default(); + let bezier1 = Bezier::from_cubic_coordinates(4., 4., 23., 45., 10., 30., 56., 90.); - assert!(bezier1.project(DVec2::new(100., 100.), 20, 0.0001, 3, 10) == DVec2::new(56., 90.)); - assert!(bezier1.project(DVec2::new(0., 0.), 20, 0.0001, 3, 10) == DVec2::new(4., 4.)); + assert!(bezier1.project(DVec2::new(100., 100.), project_options) == DVec2::new(56., 90.)); + assert!(bezier1.project(DVec2::new(0., 0.), project_options) == DVec2::new(4., 4.)); let bezier2 = Bezier::from_quadratic_coordinates(0., 0., 0., 100., 100., 100.); - assert!(bezier2.project(DVec2::new(100., 0.), 20, 0.0001, 3, 10) == DVec2::new(0., 0.)); + assert!(bezier2.project(DVec2::new(100., 0.), project_options) == DVec2::new(0., 0.)); } #[test] - fn line_intersection_quadratic() { + fn intersect_line_segment_quadratic() { let p1 = DVec2::new(30., 50.); let p2 = DVec2::new(140., 30.); let p3 = DVec2::new(160., 170.); @@ -599,18 +635,18 @@ mod tests { // Intersection at edge of curve let bezier1 = Bezier::from_quadratic_dvec2(p1, p2, p3); let line1 = [DVec2::new(20., 50.), DVec2::new(40., 50.)]; - let intersections1 = bezier1.line_intersection(line1); + let intersections1 = bezier1.intersect_line_segment(line1); assert!(intersections1.len() == 1); assert!(compare_points(intersections1[0], p1)); // Intersection in the middle of curve let line2 = [DVec2::new(150., 150.), DVec2::new(30., 30.)]; - let intersections2 = bezier1.line_intersection(line2); + let intersections2 = bezier1.intersect_line_segment(line2); assert!(compare_points(intersections2[0], DVec2::new(47.77355, 47.77354))); } #[test] - fn line_intersection_cubic() { + fn intersect_line_segment_cubic() { let p1 = DVec2::new(30., 30.); let p2 = DVec2::new(60., 140.); let p3 = DVec2::new(150., 30.); @@ -619,13 +655,13 @@ mod tests { let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4); // Intersection at edge of curve, Discriminant > 0 let line1 = [DVec2::new(20., 30.), DVec2::new(40., 30.)]; - let intersections1 = bezier.line_intersection(line1); + let intersections1 = bezier.intersect_line_segment(line1); assert!(intersections1.len() == 1); assert!(compare_points(intersections1[0], p1)); // Intersection at edge and in middle of curve, Discriminant < 0 let line2 = [DVec2::new(150., 150.), DVec2::new(30., 30.)]; - let intersections2 = bezier.line_intersection(line2); + let intersections2 = bezier.intersect_line_segment(line2); assert!(intersections2.len() == 2); assert!(compare_points(intersections2[0], p1)); assert!(compare_points(intersections2[1], DVec2::new(85.84, 85.84))); diff --git a/bezier-rs/lib/src/utils.rs b/bezier-rs/lib/src/utils.rs index 616edc7c7..e07ffb9a8 100644 --- a/bezier-rs/lib/src/utils.rs +++ b/bezier-rs/lib/src/utils.rs @@ -116,10 +116,10 @@ pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec { if a.abs() <= 1e-5 { if b.abs() <= 1e-5 { - // if both a and b are approximately 0, treat as a linear problem + // If both a and b are approximately 0, treat as a linear problem solve_linear(c, d) } else { - // if a is approximately 0, treat as a quadratic problem + // If a is approximately 0, treat as a quadratic problem let discriminant = c * c - 4. * b * d; solve_quadratic(discriminant, 2. * b, c, d) } @@ -159,6 +159,7 @@ pub fn dvec2_approximately_in_range(point: DVec2, min: DVec2, max: DVec2, max_ab #[cfg(test)] mod tests { use super::*; + use crate::consts::MAX_ABSOLUTE_DIFFERENCE; #[test] fn test_solve_cubic() { @@ -180,14 +181,14 @@ mod tests { // discriminant > 0 let roots4 = solve_cubic(1., 3., 0., 2.); assert!(roots4.len() == 1); - assert!(f64_compare(roots4[0], -3.196, 1e-3)); + assert!(f64_compare(roots4[0], -3.196, MAX_ABSOLUTE_DIFFERENCE)); // discriminant < 0 let roots5 = solve_cubic(1., 3., 0., -1.); assert!(roots5.len() == 3); - assert!(f64_compare(roots5[0], 0.532, 1e-3)); - assert!(f64_compare(roots5[1], -2.879, 1e-3)); - assert!(f64_compare(roots5[2], -0.653, 1e-3)); + assert!(f64_compare(roots5[0], 0.532, MAX_ABSOLUTE_DIFFERENCE)); + assert!(f64_compare(roots5[1], -2.879, MAX_ABSOLUTE_DIFFERENCE)); + assert!(f64_compare(roots5[2], -0.653, MAX_ABSOLUTE_DIFFERENCE)); // quadratic let roots6 = solve_cubic(0., 3., 0., -3.);