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">
|
<script lang="ts">
|
||||||
import { defineComponent, markRaw } from "vue";
|
import { defineComponent, markRaw } from "vue";
|
||||||
|
|
||||||
import { drawBezier, drawBezierHelper, drawCircle, drawCurve, drawLine, drawPoint, drawText, getContextFromCanvas, COLORS } from "@/utils/drawing";
|
import { drawBezier, drawBezierHelper, drawCircle, drawCircleSector, drawCurve, drawLine, drawPoint, drawText, getContextFromCanvas, COLORS } from "@/utils/drawing";
|
||||||
import { BezierCurveType, Point, WasmBezierInstance, WasmSubpathInstance } from "@/utils/types";
|
import { BezierCurveType, CircleSector, Point, WasmBezierInstance, WasmSubpathInstance } from "@/utils/types";
|
||||||
|
|
||||||
import ExamplePane from "@/components/ExamplePane.vue";
|
import ExamplePane from "@/components/ExamplePane.vue";
|
||||||
import SliderExample from "@/components/SliderExample.vue";
|
import SliderExample from "@/components/SliderExample.vue";
|
||||||
|
|
@ -115,10 +115,10 @@ export default defineComponent({
|
||||||
{
|
{
|
||||||
name: "Lookup Table",
|
name: "Lookup Table",
|
||||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||||
const lookupPoints = bezier.compute_lookup_table(options.steps);
|
const lookupPoints: Point[] = JSON.parse(bezier.compute_lookup_table(options.steps));
|
||||||
lookupPoints.forEach((serializedPoint, index) => {
|
lookupPoints.forEach((point, index) => {
|
||||||
if (index !== 0 && index !== lookupPoints.length - 1) {
|
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();
|
const derivativeBezier = bezier.derivative();
|
||||||
if (derivativeBezier) {
|
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) {
|
if (points.length === 2) {
|
||||||
drawLine(context, points[0], points[1], COLORS.NON_INTERACTIVE.STROKE_1);
|
drawLine(context, points[0], points[1], COLORS.NON_INTERACTIVE.STROKE_1);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -356,10 +356,7 @@ export default defineComponent({
|
||||||
name: "Rotate",
|
name: "Rotate",
|
||||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||||
const context = getContextFromCanvas(canvas);
|
const context = getContextFromCanvas(canvas);
|
||||||
const rotatedBezier = bezier
|
const rotatedBezier = JSON.parse(bezier.rotate(options.angle * Math.PI).get_points());
|
||||||
.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 });
|
drawBezier(context, rotatedBezier, null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
|
||||||
},
|
},
|
||||||
template: markRaw(SliderExample),
|
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",
|
name: "Offset",
|
||||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||||
|
|
|
||||||
|
|
@ -35,16 +35,14 @@ class BezierDrawing {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.createThroughPoints = createThroughPoints;
|
this.createThroughPoints = createThroughPoints;
|
||||||
this.points = bezier
|
const bezierPoints: Point[] = JSON.parse(bezier.get_points());
|
||||||
.get_points()
|
this.points = bezierPoints.map((p, i, points) => ({
|
||||||
.map((p) => JSON.parse(p))
|
x: p.x,
|
||||||
.map((p, i, points) => ({
|
y: p.y,
|
||||||
x: p.x,
|
r: getPointSizeByIndex(i, points.length),
|
||||||
y: p.y,
|
selected: false,
|
||||||
r: getPointSizeByIndex(i, points.length),
|
manipulator: MANIPULATOR_KEYS_FROM_BEZIER_TYPE[points.length][i],
|
||||||
selected: false,
|
}));
|
||||||
manipulator: MANIPULATOR_KEYS_FROM_BEZIER_TYPE[points.length][i],
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (this.createThroughPoints && this.points.length === 4) {
|
if (this.createThroughPoints && this.points.length === 4) {
|
||||||
// Use the first handler as the middle point
|
// 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
|
// 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
|
// 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 pointsToDraw = this.points;
|
||||||
|
|
||||||
let styleConfig: Partial<BezierStyleConfig> = {
|
let styleConfig: Partial<BezierStyleConfig> = {
|
||||||
|
|
@ -130,14 +128,14 @@ class BezierDrawing {
|
||||||
};
|
};
|
||||||
let dragIndex = this.dragIndex;
|
let dragIndex = this.dragIndex;
|
||||||
if (this.createThroughPoints) {
|
if (this.createThroughPoints) {
|
||||||
let serializedPoints;
|
let bezierThroughPoints;
|
||||||
const pointList = this.points.map((p) => [p.x, p.y]);
|
const pointList = this.points.map((p) => [p.x, p.y]);
|
||||||
if (actualBezierPointLength === 3) {
|
if (actualBezierPointLength === 3) {
|
||||||
serializedPoints = WasmBezier.quadratic_through_points(pointList, this.options.t);
|
bezierThroughPoints = WasmBezier.quadratic_through_points(pointList, this.options.t);
|
||||||
} else {
|
} 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) {
|
if (this.dragIndex === 1) {
|
||||||
// Do not propagate dragIndex when the the non-endpoint is moved
|
// Do not propagate dragIndex when the the non-endpoint is moved
|
||||||
dragIndex = null;
|
dragIndex = null;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<div>
|
<div>
|
||||||
<Example :title="title" :bezier="bezier" :callback="callback" :options="sliderData" :createThroughPoints="createThroughPoints" />
|
<Example :title="title" :bezier="bezier" :callback="callback" :options="sliderData" :createThroughPoints="createThroughPoints" />
|
||||||
<div v-for="(slider, index) in templateOptions.sliders" :key="index">
|
<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" />
|
<input class="slider" v-model.number="sliderData[slider.variable]" type="range" :step="slider.step" :min="slider.min" :max="slider.max" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -45,6 +45,9 @@ export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
Example,
|
Example,
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
getSliderValue: (sliderValue: number, sliderUnit?: string | string[]) => (Array.isArray(sliderUnit) ? sliderUnit[sliderValue] : sliderUnit),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</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 HANDLE_RADIUS_FACTOR = 2 / 3;
|
||||||
const DEFAULT_ENDPOINT_RADIUS = 5;
|
const DEFAULT_ENDPOINT_RADIUS = 5;
|
||||||
|
|
@ -81,8 +81,22 @@ export const drawCircle = (ctx: CanvasRenderingContext2D, point: Point, radius:
|
||||||
ctx.stroke();
|
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 => {
|
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);
|
drawBezier(ctx, points, null, bezierStyleConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export type SliderOption = {
|
||||||
step: number;
|
step: number;
|
||||||
default: number;
|
default: number;
|
||||||
variable: string;
|
variable: string;
|
||||||
unit?: string;
|
unit?: string | string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TemplateOption = {
|
export type TemplateOption = {
|
||||||
|
|
@ -46,3 +46,10 @@ export type BezierStyleConfig = {
|
||||||
radius: number;
|
radius: number;
|
||||||
drawHandles: boolean;
|
drawHandles: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CircleSector = {
|
||||||
|
center: Point;
|
||||||
|
radius: number;
|
||||||
|
startAngle: number;
|
||||||
|
endAngle: number;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,42 @@
|
||||||
pub mod subpath;
|
pub mod subpath;
|
||||||
mod svg_drawing;
|
mod svg_drawing;
|
||||||
|
|
||||||
use bezier_rs::{Bezier, ProjectionOptions};
|
use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, ProjectionOptions};
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use wasm_bindgen::prelude::*;
|
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)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct Point {
|
struct Point {
|
||||||
x: f64,
|
x: f64,
|
||||||
y: f64,
|
y: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub enum WasmMaximizeArcs {
|
||||||
|
Automatic, // 0
|
||||||
|
On, // 1
|
||||||
|
Off, // 2
|
||||||
|
}
|
||||||
|
|
||||||
/// Wrapper of the `Bezier` struct to be used in JS.
|
/// Wrapper of the `Bezier` struct to be used in JS.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct WasmBezier(Bezier);
|
pub struct WasmBezier(Bezier);
|
||||||
|
|
||||||
/// Convert a `DVec2` into a `JsValue`.
|
/// Convert a `DVec2` into a `Point`.
|
||||||
fn vec_to_point(p: &DVec2) -> JsValue {
|
fn vec_to_point(p: &DVec2) -> Point {
|
||||||
JsValue::from_serde(&serde_json::to_string(&Point { x: p.x, y: p.y }).unwrap()).unwrap()
|
Point { x: p.x, y: p.y }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a bezier to a list of points.
|
/// 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()
|
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]
|
#[wasm_bindgen]
|
||||||
impl WasmBezier {
|
impl WasmBezier {
|
||||||
/// Expect js_points to be a list of 2 pairs.
|
/// 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));
|
self.0.set_handle_end(DVec2::new(x, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_points(&self) -> Vec<JsValue> {
|
/// The wrapped return type is `Vec<Point>`.
|
||||||
self.0.get_points().map(|point| vec_to_point(&point)).collect()
|
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 {
|
pub fn to_svg(&self) -> String {
|
||||||
|
|
@ -90,26 +117,39 @@ impl WasmBezier {
|
||||||
self.0.length(None)
|
self.0.length(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The wrapped return type is `Point`.
|
||||||
pub fn evaluate(&self, t: f64) -> JsValue {
|
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> {
|
/// The wrapped return type is `Vec<Point>`.
|
||||||
self.0.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect()
|
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> {
|
pub fn derivative(&self) -> Option<WasmBezier> {
|
||||||
self.0.derivative().map(WasmBezier)
|
self.0.derivative().map(WasmBezier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The wrapped return type is `Point`.
|
||||||
pub fn tangent(&self, t: f64) -> JsValue {
|
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 {
|
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 {
|
pub fn split(&self, t: f64) -> JsValue {
|
||||||
let bezier_points: [Vec<Point>; 2] = self.0.split(t).map(bezier_to_points);
|
let bezier_points: [Vec<Point>; 2] = self.0.split(t).map(bezier_to_points);
|
||||||
to_js_value(bezier_points)
|
to_js_value(bezier_points)
|
||||||
|
|
@ -123,29 +163,33 @@ impl WasmBezier {
|
||||||
self.0.project(DVec2::new(x, y), ProjectionOptions::default())
|
self.0.project(DVec2::new(x, y), ProjectionOptions::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The wrapped return type is `[Vec<f64>; 2]`.
|
||||||
pub fn local_extrema(&self) -> JsValue {
|
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)
|
to_js_value(local_extrema)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The wrapped return type is `[Point; 2]`.
|
||||||
pub fn bounding_box(&self) -> JsValue {
|
pub fn bounding_box(&self) -> JsValue {
|
||||||
let bbox_points: [Point; 2] = self.0.bounding_box().map(|p| Point { x: p.x, y: p.y });
|
let bbox_points: [Point; 2] = self.0.bounding_box().map(|p| Point { x: p.x, y: p.y });
|
||||||
to_js_value(bbox_points)
|
to_js_value(bbox_points)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The wrapped return type is `Vec<f64>`.
|
||||||
pub fn inflections(&self) -> JsValue {
|
pub fn inflections(&self) -> JsValue {
|
||||||
let inflections = self.0.inflections();
|
let inflections: Vec<f64> = self.0.inflections();
|
||||||
to_js_value(inflections)
|
to_js_value(inflections)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The wrapped return type is `Vec<Vec<Point>>`.
|
||||||
pub fn de_casteljau_points(&self, t: f64) -> JsValue {
|
pub fn de_casteljau_points(&self, t: f64) -> JsValue {
|
||||||
let hull = self
|
let points: Vec<Vec<Point>> = self
|
||||||
.0
|
.0
|
||||||
.de_casteljau_points(t)
|
.de_casteljau_points(t)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|level| level.iter().map(|&point| Point { x: point.x, y: point.y }).collect::<Vec<Point>>())
|
.map(|level| level.iter().map(|&point| Point { x: point.x, y: point.y }).collect::<Vec<Point>>())
|
||||||
.collect::<Vec<Vec<Point>>>();
|
.collect();
|
||||||
to_js_value(hull)
|
to_js_value(points)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rotate(&self, angle: f64) -> WasmBezier {
|
pub fn rotate(&self, angle: f64) -> WasmBezier {
|
||||||
|
|
@ -185,12 +229,30 @@ impl WasmBezier {
|
||||||
to_js_value(bezier_points)
|
to_js_value(bezier_points)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The wrapped return type is `Vec<Vec<Point>>`.
|
||||||
pub fn offset(&self, distance: f64) -> JsValue {
|
pub fn offset(&self, distance: f64) -> JsValue {
|
||||||
let bezier_points: Vec<Vec<Point>> = self.0.offset(distance).into_iter().map(bezier_to_points).collect();
|
let bezier_points: Vec<Vec<Point>> = self.0.offset(distance).into_iter().map(bezier_to_points).collect();
|
||||||
to_js_value(bezier_points)
|
to_js_value(bezier_points)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn curvature(&self, t: f64) -> f64 {
|
/// The wrapped return type is `Vec<CircleSector>`.
|
||||||
self.0.curvature(t)
|
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.
|
/// Default `t` value used for the `curve_through_points` functions.
|
||||||
pub const DEFAULT_T_VALUE: f64 = 0.5;
|
pub const DEFAULT_T_VALUE: f64 = 0.5;
|
||||||
/// Default LUT step size in `compute_lookup_table` function.
|
/// 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.
|
/// 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.
|
/// Default step size for `reduce` function.
|
||||||
pub const DEFAULT_REDUCE_STEP_SIZE: f64 = 0.01;
|
pub const DEFAULT_REDUCE_STEP_SIZE: f64 = 0.01;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
//! Bezier-rs: A Bezier Math Library for Rust
|
//! Bezier-rs: A Bezier Math Library for Rust
|
||||||
|
|
||||||
mod consts;
|
mod consts;
|
||||||
|
mod structs;
|
||||||
pub mod subpath;
|
pub mod subpath;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use consts::*;
|
use consts::*;
|
||||||
|
pub use structs::*;
|
||||||
pub use subpath::*;
|
pub use subpath::*;
|
||||||
|
|
||||||
use glam::{DMat2, DVec2};
|
use glam::{DMat2, DVec2};
|
||||||
|
use std::f64::consts::PI;
|
||||||
use std::fmt::{Debug, Formatter, Result};
|
use std::fmt::{Debug, Formatter, Result};
|
||||||
use std::ops::Range;
|
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.
|
/// Representation of a bezier curve with 2D points.
|
||||||
#[derive(Copy, Clone, PartialEq)]
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
pub struct Bezier {
|
pub struct Bezier {
|
||||||
|
|
@ -317,16 +296,16 @@ impl Bezier {
|
||||||
// Basis code based off of pseudocode found here: <https://pomax.github.io/bezierinfo/#explanation>.
|
// Basis code based off of pseudocode found here: <https://pomax.github.io/bezierinfo/#explanation>.
|
||||||
|
|
||||||
let t_squared = t * t;
|
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;
|
let squared_one_minus_t = one_minus_t * one_minus_t;
|
||||||
|
|
||||||
match self.handles {
|
match self.handles {
|
||||||
BezierHandles::Linear => self.start.lerp(self.end, t),
|
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 } => {
|
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||||
let t_cubed = t_squared * t;
|
let t_cubed = t_squared * t;
|
||||||
let cubed_one_minus_t = squared_one_minus_t * one_minus_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.
|
/// Calculate the point on the curve based on the `t`-value provided.
|
||||||
/// Expects `t` to be within the inclusive range `[0, 1]`.
|
/// Expects `t` to be within the inclusive range `[0, 1]`.
|
||||||
pub fn evaluate(&self, t: f64) -> DVec2 {
|
pub fn evaluate(&self, t: f64) -> DVec2 {
|
||||||
assert!((0.0..=1.0).contains(&t));
|
assert!((0.0..=1.).contains(&t));
|
||||||
self.unrestricted_evaluate(t)
|
self.unrestricted_evaluate(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a selection of equidistant points on the bezier curve.
|
/// 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.
|
/// 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 steps_unwrapped = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE);
|
||||||
let ratio: f64 = 1.0 / (steps_unwrapped as f64);
|
let ratio: f64 = 1. / (steps_unwrapped as f64);
|
||||||
let mut steps_array = Vec::with_capacity((steps_unwrapped + 1) as usize);
|
let mut steps_array = Vec::with_capacity(steps_unwrapped + 1);
|
||||||
|
|
||||||
for t in 0..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
|
steps_array
|
||||||
|
|
@ -354,7 +333,7 @@ impl Bezier {
|
||||||
|
|
||||||
/// Return an approximation of the length of the bezier curve.
|
/// 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.
|
/// - `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 {
|
match self.handles {
|
||||||
BezierHandles::Linear => self.start.distance(self.end),
|
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
|
// 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
|
// 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 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];
|
let mut previous_point = lookup_table[0];
|
||||||
// Calculate approximate distance between subdivision
|
// Calculate approximate distance between subdivision
|
||||||
for current_point in lookup_table.iter().skip(1) {
|
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);
|
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
|
// 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 lut_size_f64 = lut_size as f64;
|
||||||
let mut right_t = (lut_size.min(minimum_position + 1)) as 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
|
// 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
|
// 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
|
// Store calculated distances to minimize unnecessary recomputations
|
||||||
let mut distances: [f64; NUM_DISTANCES] = [
|
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.,
|
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 {
|
while left_t <= right_t && convergence_count < convergence_limit && iteration_count < iteration_limit {
|
||||||
previous_distance = new_minimum_distance;
|
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 iterator_t = left_t;
|
||||||
let mut target_index = 0;
|
let mut target_index = 0;
|
||||||
// Iterate through first 4 points and will handle the right most point later
|
// 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
|
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.
|
/// 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.
|
/// This function may introduce gaps if subsections of the curve are not reducible.
|
||||||
/// The function takes the following parameter:
|
/// The function takes the following parameter:
|
||||||
|
|
@ -852,10 +842,7 @@ impl Bezier {
|
||||||
|
|
||||||
let step_size = step_size.unwrap_or(DEFAULT_REDUCE_STEP_SIZE);
|
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>>();
|
let extrema = self.get_extrema_t_list();
|
||||||
extrema.append(&mut vec![0., 1.]);
|
|
||||||
extrema.dedup();
|
|
||||||
extrema.sort_by(|ex1, ex2| ex1.partial_cmp(ex2).unwrap());
|
|
||||||
|
|
||||||
// Split each subcurve such that each resulting segment is scalable.
|
// Split each subcurve such that each resulting segment is scalable.
|
||||||
let mut result_beziers: Vec<Bezier> = Vec::new();
|
let mut result_beziers: Vec<Bezier> = Vec::new();
|
||||||
|
|
@ -923,6 +910,153 @@ impl Bezier {
|
||||||
self.reduced_curves_and_t_values(step_size).0
|
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.
|
/// Return the min and max corners that represent the bounding box of the curve.
|
||||||
pub fn bounding_box(&self) -> [DVec2; 2] {
|
pub fn bounding_box(&self) -> [DVec2; 2] {
|
||||||
// Start by taking min/max of endpoints.
|
// 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()))
|
.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
|
// 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 {
|
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))
|
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.);
|
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.));
|
assert!(bezier2.evaluate(bezier2.project(DVec2::new(100., 0.), project_options)) == DVec2::new(0., 0.));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_intersect_line_segment_linear() {
|
fn test_intersect_line_segment_linear() {
|
||||||
let p1 = DVec2::new(30., 60.);
|
let p1 = DVec2::new(30., 60.);
|
||||||
|
|
@ -1232,4 +1375,73 @@ mod tests {
|
||||||
.zip(helper_t_values.windows(2))
|
.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)))
|
.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 {
|
impl Subpath {
|
||||||
/// Return the sum of the approximation of the length of each `Bezier` curve along the `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`.
|
/// - `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))
|
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 crate::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE};
|
||||||
|
|
||||||
use glam::{BVec2, DVec2};
|
use glam::{BVec2, DMat2, DVec2};
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
/// Helper to perform the computation of a and c, where b is the provided point on the curve.
|
/// 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.
|
/// 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()
|
lut.iter()
|
||||||
.enumerate()
|
.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())
|
.min_by(|x, y| (&(x.1)).partial_cmp(&(y.1)).unwrap())
|
||||||
.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.
|
/// Compare two `f64` numbers with a provided max absolute value difference.
|
||||||
pub fn f64_compare(f1: f64, f2: f64, max_abs_diff: f64) -> bool {
|
pub fn f64_compare(f1: f64, f2: f64, max_abs_diff: f64) -> bool {
|
||||||
(f1 - f2).abs() < max_abs_diff
|
(f1 - f2).abs() < max_abs_diff
|
||||||
|
|
@ -287,4 +314,20 @@ mod tests {
|
||||||
let end_direction2 = DVec2::new(1., -1.);
|
let end_direction2 = DVec2::new(1., -1.);
|
||||||
assert!(line_intersection(start2, start_direction2, end2, end_direction2) == DVec2::new(4., 4.));
|
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;
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
/// Necessary because serde can't serialize hashmaps when the keys don't implement display.
|
/// 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
|
where
|
||||||
S: Serializer,
|
S: Serializer,
|
||||||
T: IntoIterator<Item = (&'a K, &'a V)>,
|
T: IntoIterator<Item = (&'a K, &'a V)>,
|
||||||
|
|
@ -10,16 +10,16 @@ where
|
||||||
V: Serialize + 'a,
|
V: Serialize + 'a,
|
||||||
{
|
{
|
||||||
let container: Vec<_> = target.into_iter().collect();
|
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
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
T: FromIterator<(K, V)>,
|
T: FromIterator<(K, V)>,
|
||||||
K: Deserialize<'de>,
|
K: Deserialize<'de>,
|
||||||
V: 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()))
|
Ok(T::from_iter(container.into_iter()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "./bezier-rs/docs/interactive-docs/tsconfig.json"
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue