Implement arcs for Bezier math library (#731)
* added arcs impl Co-authored-by: Hannah Li <hannahli2010@gmail.com> Co-authored-by: Rob Nadal <RobNadal@users.noreply.github.com> * fixed arc drawing, todo - fix linear check Co-authored-by: Hannah Li <hannahli2010@gmail.com> * fixed linear bug + added comments and tests Co-authored-by: Hannah Li <hannahli2010@gmail.com> * 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 <RobNadal@users.noreply.github.com> * 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 <hannahli2010@gmail.com> Co-authored-by: Rob Nadal <RobNadal@users.noreply.github.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
0f88055573
commit
b84e647f40
|
|
@ -25,8 +25,8 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, markRaw } from "vue";
|
||||
|
||||
import { drawBezier, drawBezierHelper, drawCircle, drawCurve, drawLine, drawPoint, drawText, getContextFromCanvas, COLORS } from "@/utils/drawing";
|
||||
import { BezierCurveType, Point, WasmBezierInstance, WasmSubpathInstance } from "@/utils/types";
|
||||
import { drawBezier, drawBezierHelper, drawCircle, drawCircleSector, drawCurve, drawLine, drawPoint, drawText, getContextFromCanvas, COLORS } from "@/utils/drawing";
|
||||
import { BezierCurveType, CircleSector, Point, WasmBezierInstance, WasmSubpathInstance } from "@/utils/types";
|
||||
|
||||
import ExamplePane from "@/components/ExamplePane.vue";
|
||||
import SliderExample from "@/components/SliderExample.vue";
|
||||
|
|
@ -115,10 +115,10 @@ export default defineComponent({
|
|||
{
|
||||
name: "Lookup Table",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const lookupPoints = bezier.compute_lookup_table(options.steps);
|
||||
lookupPoints.forEach((serializedPoint, index) => {
|
||||
const lookupPoints: Point[] = JSON.parse(bezier.compute_lookup_table(options.steps));
|
||||
lookupPoints.forEach((point, index) => {
|
||||
if (index !== 0 && index !== lookupPoints.length - 1) {
|
||||
drawPoint(getContextFromCanvas(canvas), JSON.parse(serializedPoint), 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawPoint(getContextFromCanvas(canvas), point, 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -142,7 +142,7 @@ export default defineComponent({
|
|||
|
||||
const derivativeBezier = bezier.derivative();
|
||||
if (derivativeBezier) {
|
||||
const points: Point[] = derivativeBezier.get_points().map((p) => JSON.parse(p));
|
||||
const points: Point[] = JSON.parse(derivativeBezier.get_points());
|
||||
if (points.length === 2) {
|
||||
drawLine(context, points[0], points[1], COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
} else {
|
||||
|
|
@ -356,10 +356,7 @@ export default defineComponent({
|
|||
name: "Rotate",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const rotatedBezier = bezier
|
||||
.rotate(options.angle * Math.PI)
|
||||
.get_points()
|
||||
.map((p) => JSON.parse(p));
|
||||
const rotatedBezier = JSON.parse(bezier.rotate(options.angle * Math.PI).get_points());
|
||||
drawBezier(context, rotatedBezier, null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
|
|
@ -496,6 +493,57 @@ export default defineComponent({
|
|||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Arcs",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const arcs: CircleSector[] = JSON.parse(bezier.arcs(options.error, options.max_iterations, options.strategy));
|
||||
arcs.forEach((circleSector, index) => {
|
||||
drawCircleSector(context, circleSector, `hsl(${40 * index}, 100%, 50%, 75%)`, `hsl(${40 * index}, 100%, 50%, 37.5%)`);
|
||||
});
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: {
|
||||
sliders: [
|
||||
{
|
||||
variable: "strategy",
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 1,
|
||||
default: 0,
|
||||
unit: [": Automatic", ": FavorLargerArcs", ": FavorCorrectness"],
|
||||
},
|
||||
{
|
||||
variable: "error",
|
||||
min: 0.05,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
default: 0.5,
|
||||
},
|
||||
{
|
||||
variable: "max_iterations",
|
||||
min: 50,
|
||||
max: 200,
|
||||
step: 1,
|
||||
default: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
curveDegrees: new Set([BezierCurveType.Quadratic, BezierCurveType.Cubic]),
|
||||
customPoints: {
|
||||
[BezierCurveType.Quadratic]: [
|
||||
[50, 50],
|
||||
[85, 65],
|
||||
[100, 100],
|
||||
],
|
||||
[BezierCurveType.Cubic]: [
|
||||
[160, 180],
|
||||
[170, 10],
|
||||
[30, 90],
|
||||
[180, 160],
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Offset",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
|
|
|
|||
|
|
@ -35,16 +35,14 @@ class BezierDrawing {
|
|||
this.callback = callback;
|
||||
this.options = options;
|
||||
this.createThroughPoints = createThroughPoints;
|
||||
this.points = bezier
|
||||
.get_points()
|
||||
.map((p) => JSON.parse(p))
|
||||
.map((p, i, points) => ({
|
||||
x: p.x,
|
||||
y: p.y,
|
||||
r: getPointSizeByIndex(i, points.length),
|
||||
selected: false,
|
||||
manipulator: MANIPULATOR_KEYS_FROM_BEZIER_TYPE[points.length][i],
|
||||
}));
|
||||
const bezierPoints: Point[] = JSON.parse(bezier.get_points());
|
||||
this.points = bezierPoints.map((p, i, points) => ({
|
||||
x: p.x,
|
||||
y: p.y,
|
||||
r: getPointSizeByIndex(i, points.length),
|
||||
selected: false,
|
||||
manipulator: MANIPULATOR_KEYS_FROM_BEZIER_TYPE[points.length][i],
|
||||
}));
|
||||
|
||||
if (this.createThroughPoints && this.points.length === 4) {
|
||||
// Use the first handler as the middle point
|
||||
|
|
@ -122,7 +120,7 @@ class BezierDrawing {
|
|||
|
||||
// For the create through points cases, we store a bezier where the handle is actually the point that the curve should pass through
|
||||
// This is so that we can re-use the drag and drop logic, while simply drawing the desired bezier instead
|
||||
const actualBezierPointLength = this.bezier.get_points().length;
|
||||
const actualBezierPointLength = JSON.parse(this.bezier.get_points()).length;
|
||||
let pointsToDraw = this.points;
|
||||
|
||||
let styleConfig: Partial<BezierStyleConfig> = {
|
||||
|
|
@ -130,14 +128,14 @@ class BezierDrawing {
|
|||
};
|
||||
let dragIndex = this.dragIndex;
|
||||
if (this.createThroughPoints) {
|
||||
let serializedPoints;
|
||||
let bezierThroughPoints;
|
||||
const pointList = this.points.map((p) => [p.x, p.y]);
|
||||
if (actualBezierPointLength === 3) {
|
||||
serializedPoints = WasmBezier.quadratic_through_points(pointList, this.options.t);
|
||||
bezierThroughPoints = WasmBezier.quadratic_through_points(pointList, this.options.t);
|
||||
} else {
|
||||
serializedPoints = WasmBezier.cubic_through_points(pointList, this.options.t, this.options["midpoint separation"]);
|
||||
bezierThroughPoints = WasmBezier.cubic_through_points(pointList, this.options.t, this.options["midpoint separation"]);
|
||||
}
|
||||
pointsToDraw = serializedPoints.get_points().map((p) => JSON.parse(p));
|
||||
pointsToDraw = JSON.parse(bezierThroughPoints.get_points());
|
||||
if (this.dragIndex === 1) {
|
||||
// Do not propagate dragIndex when the the non-endpoint is moved
|
||||
dragIndex = null;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div>
|
||||
<Example :title="title" :bezier="bezier" :callback="callback" :options="sliderData" :createThroughPoints="createThroughPoints" />
|
||||
<div v-for="(slider, index) in templateOptions.sliders" :key="index">
|
||||
<div class="slider-label">{{ slider.variable }} = {{ sliderData[slider.variable] }}{{ sliderUnits[slider.variable] }}</div>
|
||||
<div class="slider-label">{{ slider.variable }} = {{ sliderData[slider.variable] }}{{ getSliderValue(sliderData[slider.variable], sliderUnits[slider.variable]) }}</div>
|
||||
<input class="slider" v-model.number="sliderData[slider.variable]" type="range" :step="slider.step" :min="slider.min" :max="slider.max" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -45,6 +45,9 @@ export default defineComponent({
|
|||
components: {
|
||||
Example,
|
||||
},
|
||||
methods: {
|
||||
getSliderValue: (sliderValue: number, sliderUnit?: string | string[]) => (Array.isArray(sliderUnit) ? sliderUnit[sliderValue] : sliderUnit),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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<BezierStyleConfig> = {}): void => {
|
||||
const points = bezier.get_points().map((p: string) => JSON.parse(p));
|
||||
const points = JSON.parse(bezier.get_points());
|
||||
drawBezier(ctx, points, null, bezierStyleConfig);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<T: Serialize>(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<JsValue> {
|
||||
self.0.get_points().map(|point| vec_to_point(&point)).collect()
|
||||
/// The wrapped return type is `Vec<Point>`.
|
||||
pub fn get_points(&self) -> JsValue {
|
||||
let points: Vec<Point> = 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<JsValue> {
|
||||
self.0.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect()
|
||||
/// The wrapped return type is `Vec<Point>`.
|
||||
pub fn compute_lookup_table(&self, steps: usize) -> JsValue {
|
||||
let table_values: Vec<Point> = self.0.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect();
|
||||
to_js_value(table_values)
|
||||
}
|
||||
|
||||
pub fn derivative(&self) -> Option<WasmBezier> {
|
||||
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<Point>; 2]`.
|
||||
pub fn split(&self, t: f64) -> JsValue {
|
||||
let bezier_points: [Vec<Point>; 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<f64>; 2]`.
|
||||
pub fn local_extrema(&self) -> JsValue {
|
||||
let local_extrema = self.0.local_extrema();
|
||||
let local_extrema: [Vec<f64>; 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<f64>`.
|
||||
pub fn inflections(&self) -> JsValue {
|
||||
let inflections = self.0.inflections();
|
||||
let inflections: Vec<f64> = self.0.inflections();
|
||||
to_js_value(inflections)
|
||||
}
|
||||
|
||||
/// The wrapped return type is `Vec<Vec<Point>>`.
|
||||
pub fn de_casteljau_points(&self, t: f64) -> JsValue {
|
||||
let hull = self
|
||||
let points: Vec<Vec<Point>> = self
|
||||
.0
|
||||
.de_casteljau_points(t)
|
||||
.iter()
|
||||
.map(|level| level.iter().map(|&point| Point { x: point.x, y: point.y }).collect::<Vec<Point>>())
|
||||
.collect::<Vec<Vec<Point>>>();
|
||||
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<Vec<Point>>`.
|
||||
pub fn offset(&self, distance: f64) -> JsValue {
|
||||
let bezier_points: Vec<Vec<Point>> = 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<CircleSector>`.
|
||||
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<CircleSector> = 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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: <https://pomax.github.io/bezierinfo/#explanation>.
|
||||
|
||||
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<i32>) -> Vec<DVec2> {
|
||||
pub fn compute_lookup_table(&self, steps: Option<usize>) -> Vec<DVec2> {
|
||||
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<i32>) -> f64 {
|
||||
pub fn length(&self, num_subdivisions: Option<usize>) -> 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<f64> {
|
||||
let mut extrema = self.local_extrema().into_iter().flatten().collect::<Vec<f64>>();
|
||||
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<f64> = self.local_extrema().into_iter().flatten().collect::<Vec<f64>>();
|
||||
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<Bezier> = 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<CircleArc> {
|
||||
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::<Vec<CircleArc>>(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<CircleArc>, 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::<Vec<DVec2>>(), 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<DVec2>, vec2: Vec<DVec2>, 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)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<i32>) -> f64 {
|
||||
pub fn length(&self, num_subdivisions: Option<usize>) -> f64 {
|
||||
self.iter().fold(0., |accumulator, bezier| accumulator + bezier.length(num_subdivisions))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DVec2> {
|
||||
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.));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<S::Ok, S::Error>
|
||||
pub fn serialize<'a, T, K, V, S>(target: T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
T: IntoIterator<Item = (&'a K, &'a V)>,
|
||||
|
|
@ -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<T, D::Error>
|
||||
pub fn deserialize<'de, T, K, V, D>(deserializer: D) -> Result<T, D::Error>
|
||||
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()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"extends": "./bezier-rs/docs/interactive-docs/tsconfig.json"
|
||||
}
|
||||
Loading…
Reference in New Issue