Support linear bezier curve segments in the Bezier math library (#717)

* Support Linear line segments, add linear section to interactive docs

* Fix regression, customize points in UI examples, add optional subdivisions to length, minor refactors

* Refactor ExamplePane, use better example curves

* Update consts.rs comments

* Code review changes

* Address PR comments

* Code review

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Hannah Li
2022-07-08 17:41:15 -04:00
committed by Keavon Chambers
co-authored by Keavon Chambers
parent ad7097ea92
commit 00cc50d531
11 changed files with 440 additions and 215 deletions
+80 -25
View File
@@ -9,7 +9,9 @@
:name="feature.name" :name="feature.name"
:callback="feature.callback" :callback="feature.callback"
:createThroughPoints="feature.createThroughPoints" :createThroughPoints="feature.createThroughPoints"
:cubicOptions="feature.cubicOptions" :curveDegrees="feature.curveDegrees"
:customPoints="feature.customPoints"
:customOptions="feature.customOptions"
/> />
</div> </div>
</div> </div>
@@ -19,7 +21,7 @@
import { defineComponent, markRaw } from "vue"; import { defineComponent, markRaw } from "vue";
import { drawText, drawPoint, drawBezier, drawLine, getContextFromCanvas, drawBezierHelper, COLORS } from "@/utils/drawing"; import { drawText, drawPoint, drawBezier, drawLine, getContextFromCanvas, drawBezierHelper, COLORS } from "@/utils/drawing";
import { Point, WasmBezierInstance } from "@/utils/types"; import { BezierCurveType, Point, WasmBezierInstance } 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";
@@ -51,6 +53,7 @@ export default defineComponent({
name: "Bezier Through Points", name: "Bezier Through Points",
// eslint-disable-next-line // eslint-disable-next-line
callback: (): void => {}, callback: (): void => {},
curveDegrees: new Set([BezierCurveType.Quadratic, BezierCurveType.Cubic]),
createThroughPoints: true, createThroughPoints: true,
template: markRaw(SliderExample), template: markRaw(SliderExample),
templateOptions: { templateOptions: {
@@ -64,22 +67,31 @@ export default defineComponent({
}, },
], ],
}, },
cubicOptions: { customOptions: {
sliders: [ [BezierCurveType.Cubic]: {
{ sliders: [
min: 0.01, {
max: 0.99, min: 0.01,
step: 0.01, max: 0.99,
default: 0.5, step: 0.01,
variable: "t", default: 0.5,
}, variable: "t",
{ },
min: 0, {
max: 100, min: 0,
step: 5, max: 100,
default: 10, step: 2,
variable: "midpoint separation", default: 30,
}, variable: "midpoint separation",
},
],
},
},
customPoints: {
[BezierCurveType.Quadratic]: [
[30, 50],
[120, 70],
[160, 170],
], ],
}, },
}, },
@@ -90,9 +102,9 @@ export default defineComponent({
}, },
}, },
{ {
name: "Compute", name: "Evaluate",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => { callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const point = JSON.parse(bezier.compute(options.t)); const point = JSON.parse(bezier.evaluate(options.t));
drawPoint(getContextFromCanvas(canvas), point, 4, COLORS.NON_INTERACTIVE.STROKE_1); drawPoint(getContextFromCanvas(canvas), point, 4, COLORS.NON_INTERACTIVE.STROKE_1);
}, },
template: markRaw(SliderExample), template: markRaw(SliderExample),
@@ -121,12 +133,42 @@ export default defineComponent({
], ],
}, },
}, },
{
name: "Derivative",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
const context = getContextFromCanvas(canvas);
const derivativeBezier = bezier.derivative();
if (derivativeBezier) {
const points: Point[] = derivativeBezier.get_points().map((p) => JSON.parse(p));
if (points.length === 2) {
drawLine(context, points[0], points[1], COLORS.NON_INTERACTIVE.STROKE_1);
} else {
drawBezier(context, points, null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
}
}
},
curveDegrees: new Set([BezierCurveType.Quadratic, BezierCurveType.Cubic]),
customPoints: {
[BezierCurveType.Quadratic]: [
[30, 40],
[110, 50],
[120, 130],
],
[BezierCurveType.Cubic]: [
[50, 50],
[60, 100],
[100, 140],
[140, 150],
],
},
},
{ {
name: "Tangent", name: "Tangent",
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 intersection = JSON.parse(bezier.compute(options.t)); const intersection = JSON.parse(bezier.evaluate(options.t));
const tangent = JSON.parse(bezier.tangent(options.t)); const tangent = JSON.parse(bezier.tangent(options.t));
const tangentEnd = { const tangentEnd = {
@@ -146,7 +188,7 @@ export default defineComponent({
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 intersection = JSON.parse(bezier.compute(options.t)); const intersection = JSON.parse(bezier.evaluate(options.t));
const normal = JSON.parse(bezier.normal(options.t)); const normal = JSON.parse(bezier.normal(options.t));
const normalEnd = { const normalEnd = {
@@ -218,13 +260,26 @@ export default defineComponent({
const extrema: number[][] = JSON.parse(bezier.local_extrema()); const extrema: number[][] = JSON.parse(bezier.local_extrema());
extrema.forEach((tValues, index) => { extrema.forEach((tValues, index) => {
tValues.forEach((t) => { tValues.forEach((t) => {
const point: Point = JSON.parse(bezier.compute(t)); const point: Point = JSON.parse(bezier.evaluate(t));
drawPoint(context, point, 4, dimensionColors[index]); drawPoint(context, point, 4, dimensionColors[index]);
}); });
}); });
drawText(getContextFromCanvas(canvas), "X extrema", 5, canvas.height - 20, dimensionColors[0]); drawText(getContextFromCanvas(canvas), "X extrema", 5, canvas.height - 20, dimensionColors[0]);
drawText(getContextFromCanvas(canvas), "Y extrema", 5, canvas.height - 5, dimensionColors[1]); drawText(getContextFromCanvas(canvas), "Y extrema", 5, canvas.height - 5, dimensionColors[1]);
}, },
customPoints: {
[BezierCurveType.Quadratic]: [
[40, 40],
[160, 30],
[110, 150],
],
[BezierCurveType.Cubic]: [
[160, 180],
[170, 10],
[30, 90],
[180, 160],
],
},
}, },
{ {
name: "Rotate", name: "Rotate",
@@ -243,8 +298,8 @@ export default defineComponent({
variable: "angle", variable: "angle",
min: 0, min: 0,
max: 2, max: 2,
step: 1 / 16, step: 1 / 50,
default: 1 / 8, default: 0.12,
unit: "π", unit: "π",
}, },
], ],
@@ -1,13 +1,19 @@
import { WasmBezier } from "@/../wasm/pkg"; import { WasmBezier } from "@/../wasm/pkg";
import { COLORS, drawBezier, drawPoint, getContextFromCanvas, getPointSizeByIndex } from "@/utils/drawing"; import { COLORS, drawBezier, drawPoint, getContextFromCanvas, getPointSizeByIndex } from "@/utils/drawing";
import { BezierCallback, BezierPoint, BezierStyleConfig, Point, WasmBezierMutatorKey, WasmBezierInstance } from "@/utils/types"; import { BezierCallback, BezierPoint, BezierStyleConfig, Point, WasmBezierManipulatorKey, WasmBezierInstance } from "@/utils/types";
// Offset to increase selectable range, used to make points easier to grab // Offset to increase selectable range, used to make points easier to grab
const FUDGE_FACTOR = 3; const FUDGE_FACTOR = 3;
class BezierDrawing { // Given the number of points in the curve, map the index of a point to the correct manipulator key
static indexToMutator: WasmBezierMutatorKey[] = ["set_start", "set_handle_start", "set_handle_end", "set_end"]; const MANIPULATOR_KEYS_FROM_BEZIER_TYPE: { [k: number]: WasmBezierManipulatorKey[] } = {
2: ["set_start", "set_end"],
3: ["set_start", "set_handle_start", "set_end"],
4: ["set_start", "set_handle_start", "set_handle_end", "set_end"],
};
class BezierDrawing {
points: BezierPoint[]; points: BezierPoint[];
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
@@ -37,7 +43,7 @@ class BezierDrawing {
y: p.y, y: p.y,
r: getPointSizeByIndex(i, points.length), r: getPointSizeByIndex(i, points.length),
selected: false, selected: false,
mutator: BezierDrawing.indexToMutator[points.length === 3 && i > 1 ? i + 1 : i], manipulator: MANIPULATOR_KEYS_FROM_BEZIER_TYPE[points.length][i],
})); }));
if (this.createThroughPoints && this.points.length === 4) { if (this.createThroughPoints && this.points.length === 4) {
@@ -70,10 +76,7 @@ class BezierDrawing {
} }
mouseMoveHandler(evt: MouseEvent): void { mouseMoveHandler(evt: MouseEvent): void {
if (evt.buttons === 0) { if (evt.buttons === 0) this.deselectPointHandler();
this.deselectPointHandler();
return;
}
const mx = evt.offsetX; const mx = evt.offsetX;
const my = evt.offsetY; const my = evt.offsetY;
@@ -84,7 +87,7 @@ class BezierDrawing {
const selectedPoint = this.points[this.dragIndex]; const selectedPoint = this.points[this.dragIndex];
selectedPoint.x = mx; selectedPoint.x = mx;
selectedPoint.y = my; selectedPoint.y = my;
this.bezier[selectedPoint.mutator](selectedPoint.x, selectedPoint.y); this.bezier[selectedPoint.manipulator](selectedPoint.x, selectedPoint.y);
} }
} }
this.updateBezier({ x: mx, y: my }); this.updateBezier({ x: mx, y: my });
@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<h4 class="example_header">{{ title }}</h4> <h4 class="example-header">{{ title }}</h4>
<figure class="example_figure" ref="drawing"></figure> <figure class="example-figure" ref="drawing"></figure>
</div> </div>
</template> </template>
@@ -54,10 +54,11 @@ export default defineComponent({
</script> </script>
<style scoped> <style scoped>
.example_header { .example-header {
margin-bottom: 0; margin-bottom: 0;
} }
.example_figure {
.example-figure {
margin-top: 0.5em; margin-top: 0.5em;
} }
</style> </style>
@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<h2 class="example_pane_header">{{ name }}</h2> <h2 class="example-pane-header">{{ name }}</h2>
<div class="example_row"> <div class="example-row">
<div v-for="(example, index) in exampleData" :key="index"> <div v-for="(example, index) in exampleData" :key="index">
<component :is="template" :templateOptions="example.templateOptions" :title="example.title" :bezier="example.bezier" :callback="callback" :createThroughPoints="createThroughPoints" /> <component :is="template" :templateOptions="example.templateOptions" :title="example.title" :bezier="example.bezier" :callback="callback" :createThroughPoints="createThroughPoints" />
</div> </div>
@@ -12,7 +12,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType, Component } from "vue"; import { defineComponent, PropType, Component } from "vue";
import { BezierCallback, TemplateOption, WasmBezierInstance, WasmRawInstance } from "@/utils/types"; import { BezierCallback, BezierCurveType, TemplateOption, WasmBezierConstructorKey, WasmBezierInstance, WasmRawInstance } from "@/utils/types";
import Example from "@/components/Example.vue"; import Example from "@/components/Example.vue";
@@ -22,13 +22,51 @@ type ExampleData = {
templateOptions: TemplateOption; templateOptions: TemplateOption;
}; };
type CustomTemplateOptions = {
[key in BezierCurveType]?: TemplateOption;
};
type CustomPoints = {
[key in BezierCurveType]?: number[][];
};
const CurveTypeMapping = {
[BezierCurveType.Linear]: {
points: [
[30, 60],
[140, 120],
],
constructor: "new_linear" as WasmBezierConstructorKey,
},
[BezierCurveType.Quadratic]: {
points: [
[30, 50],
[140, 30],
[160, 170],
],
constructor: "new_quadratic" as WasmBezierConstructorKey,
},
[BezierCurveType.Cubic]: {
points: [
[30, 30],
[60, 140],
[150, 30],
[160, 160],
],
constructor: "new_cubic" as WasmBezierConstructorKey,
},
};
export default defineComponent({ export default defineComponent({
name: "ExamplePane", name: "ExamplePane",
components: { components: {
Example, Example,
}, },
props: { props: {
name: String, name: {
type: String as PropType<string>,
required: true,
},
callback: { callback: {
type: Function as PropType<BezierCallback>, type: Function as PropType<BezierCallback>,
required: true, required: true,
@@ -37,15 +75,26 @@ export default defineComponent({
type: Object as PropType<Component>, type: Object as PropType<Component>,
default: Example, default: Example,
}, },
templateOptions: Object as PropType<TemplateOption>, templateOptions: {
cubicOptions: {
type: Object as PropType<TemplateOption>, type: Object as PropType<TemplateOption>,
default: null, required: false,
},
customOptions: {
type: Object as PropType<CustomTemplateOptions>,
default: () => ({}),
}, },
createThroughPoints: { createThroughPoints: {
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
default: false, default: false,
}, },
curveDegrees: {
type: Set as PropType<Set<BezierCurveType>>,
default: () => new Set(Object.values(BezierCurveType)),
},
customPoints: {
type: Object as PropType<CustomPoints>,
default: () => ({}),
},
}, },
data() { data() {
return { return {
@@ -54,42 +103,32 @@ export default defineComponent({
}, },
mounted() { mounted() {
import("@/../wasm/pkg").then((wasm: WasmRawInstance) => { import("@/../wasm/pkg").then((wasm: WasmRawInstance) => {
const quadraticPoints = [ this.exampleData = [];
[30, 50], // Only add example for BezierCurveType that is in the curveDegrees set
[140, 30], Object.values(BezierCurveType).forEach((bezierType) => {
[160, 170], if (this.curveDegrees.has(bezierType)) {
]; const { points, constructor } = CurveTypeMapping[bezierType];
const cubicPoints = [ this.exampleData.push({
[30, 30], title: bezierType,
[60, 140], // Use custom options if they were provided for the current BezierCurveType
[150, 30], bezier: wasm.WasmBezier[constructor](this.customPoints[bezierType] || points),
[160, 160], templateOptions: (this.customOptions[bezierType] || this.templateOptions) as TemplateOption,
]; });
this.exampleData = [ }
{ });
title: "Quadratic",
bezier: wasm.WasmBezier.new_quadratic(quadraticPoints),
templateOptions: this.templateOptions as TemplateOption,
},
{
title: "Cubic",
bezier: wasm.WasmBezier.new_cubic(cubicPoints),
templateOptions: (this.cubicOptions || this.templateOptions) as TemplateOption,
},
];
}); });
}, },
}); });
</script> </script>
<style> <style>
.example_row { .example-row {
display: flex; /* or inline-flex */ display: flex; /* or inline-flex */
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
} }
.example_pane_header { .example-pane-header {
margin-bottom: 0; margin-bottom: 0;
} }
</style> </style>
@@ -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] }}{{ 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>
@@ -28,9 +28,9 @@ export const getContextFromCanvas = (canvas: HTMLCanvasElement): CanvasRendering
return ctx; return ctx;
}; };
export const drawLine = (ctx: CanvasRenderingContext2D, point1: Point, point2: Point, strokeColor = COLORS.INTERACTIVE.STROKE_2): void => { export const drawLine = (ctx: CanvasRenderingContext2D, point1: Point, point2: Point, strokeColor = COLORS.INTERACTIVE.STROKE_2, lineWidth = 1): void => {
ctx.strokeStyle = strokeColor; ctx.strokeStyle = strokeColor;
ctx.lineWidth = 1; ctx.lineWidth = lineWidth;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(point1.x, point1.y); ctx.moveTo(point1.x, point1.y);
@@ -59,6 +59,20 @@ export const drawText = (ctx: CanvasRenderingContext2D, text: string, x: number,
ctx.fillText(text, x, y); ctx.fillText(text, x, y);
}; };
export const drawCurve = (ctx: CanvasRenderingContext2D, points: Point[], strokeColor = COLORS.INTERACTIVE.STROKE_1): void => {
ctx.strokeStyle = strokeColor;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
if (points.length === 3) {
ctx.quadraticCurveTo(points[1].x, points[1].y, points[2].x, points[2].y);
} else {
ctx.bezierCurveTo(points[1].x, points[1].y, points[2].x, points[2].y, points[3].x, points[3].y);
}
ctx.stroke();
};
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 = bezier.get_points().map((p: string) => JSON.parse(p));
drawBezier(ctx, points, null, bezierStyleConfig); drawBezier(ctx, points, null, bezierStyleConfig);
@@ -95,27 +109,24 @@ export const drawBezier = (ctx: CanvasRenderingContext2D, points: Point[], dragI
handleStart = points[1]; handleStart = points[1];
handleEnd = points[2]; handleEnd = points[2];
end = points[3]; end = points[3];
} else { } else if (points.length === 3) {
handleStart = points[1]; handleStart = points[1];
handleEnd = handleStart; handleEnd = handleStart;
end = points[2]; end = points[2];
}
ctx.strokeStyle = styleConfig.curveStrokeColor;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
if (points.length === 3) {
ctx.quadraticCurveTo(handleStart.x, handleStart.y, end.x, end.y);
} else { } else {
ctx.bezierCurveTo(handleStart.x, handleStart.y, handleEnd.x, handleEnd.y, end.x, end.y); handleStart = start;
handleEnd = points[1];
end = handleEnd;
} }
ctx.stroke();
if (styleConfig.drawHandles) { if (points.length === 2) {
drawLine(ctx, start, handleStart, styleConfig.handleLineStrokeColor); drawLine(ctx, start, end, styleConfig.curveStrokeColor, 2);
drawLine(ctx, end, handleEnd, styleConfig.handleLineStrokeColor); } else {
drawCurve(ctx, points, styleConfig.curveStrokeColor);
if (styleConfig.drawHandles) {
drawLine(ctx, start, handleStart, styleConfig.handleLineStrokeColor);
drawLine(ctx, end, handleEnd, styleConfig.handleLineStrokeColor);
}
} }
points.forEach((point, index) => { points.forEach((point, index) => {
@@ -2,7 +2,14 @@ export type WasmRawInstance = typeof import("../../wasm/pkg");
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>; export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
export type WasmBezierKey = keyof WasmBezierInstance; export type WasmBezierKey = keyof WasmBezierInstance;
export type WasmBezierMutatorKey = "set_start" | "set_handle_start" | "set_handle_end" | "set_end"; export type WasmBezierConstructorKey = "new_linear" | "new_quadratic" | "new_cubic";
export type WasmBezierManipulatorKey = "set_start" | "set_handle_start" | "set_handle_end" | "set_end";
export enum BezierCurveType {
Linear = "Linear",
Quadratic = "Quadratic",
Cubic = "Cubic",
}
export type BezierCallback = (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: Point) => void; export type BezierCallback = (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: Point) => void;
@@ -25,7 +32,7 @@ export type Point = {
}; };
export type BezierPoint = Point & { export type BezierPoint = Point & {
mutator: WasmBezierMutatorKey; manipulator: WasmBezierManipulatorKey;
}; };
export type BezierStyleConfig = { export type BezierStyleConfig = {
@@ -21,7 +21,7 @@ fn vec_to_point(p: &DVec2) -> JsValue {
/// Convert a bezier to a list of points. /// Convert a bezier to a list of points.
fn bezier_to_points(bezier: Bezier) -> Vec<Point> { fn bezier_to_points(bezier: Bezier) -> Vec<Point> {
bezier.get_points().iter().flatten().map(|point| Point { x: point.x, y: point.y }).collect() bezier.get_points().map(|point| Point { x: point.x, y: point.y }).collect()
} }
/// Serialize some data and then convert it to a JsValue. /// Serialize some data and then convert it to a JsValue.
@@ -31,6 +31,12 @@ fn to_js_value<T: Serialize>(data: T) -> JsValue {
#[wasm_bindgen] #[wasm_bindgen]
impl WasmBezier { impl WasmBezier {
/// Expect js_points to be a list of 3 pairs.
pub fn new_linear(js_points: &JsValue) -> WasmBezier {
let points: [DVec2; 2] = js_points.into_serde().unwrap();
WasmBezier(Bezier::from_linear_dvec2(points[0], points[1]))
}
/// Expect js_points to be a list of 3 pairs. /// Expect js_points to be a list of 3 pairs.
pub fn new_quadratic(js_points: &JsValue) -> WasmBezier { pub fn new_quadratic(js_points: &JsValue) -> WasmBezier {
let points: [DVec2; 3] = js_points.into_serde().unwrap(); let points: [DVec2; 3] = js_points.into_serde().unwrap();
@@ -70,7 +76,7 @@ impl WasmBezier {
} }
pub fn get_points(&self) -> Vec<JsValue> { pub fn get_points(&self) -> Vec<JsValue> {
self.0.get_points().iter().flatten().map(vec_to_point).collect() self.0.get_points().map(|point| vec_to_point(&point)).collect()
} }
pub fn to_svg(&self) -> String { pub fn to_svg(&self) -> String {
@@ -78,17 +84,21 @@ impl WasmBezier {
} }
pub fn length(&self) -> f64 { pub fn length(&self) -> f64 {
self.0.length() self.0.length(None)
} }
pub fn compute(&self, t: f64) -> JsValue { pub fn evaluate(&self, t: f64) -> JsValue {
vec_to_point(&self.0.compute(t)) vec_to_point(&self.0.evaluate(t))
} }
pub fn compute_lookup_table(&self, steps: i32) -> Vec<JsValue> { pub fn compute_lookup_table(&self, steps: i32) -> Vec<JsValue> {
self.0.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect() self.0.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect()
} }
pub fn derivative(&self) -> Option<WasmBezier> {
self.0.derivative().map(WasmBezier)
}
pub fn tangent(&self, t: f64) -> JsValue { pub fn tangent(&self, t: f64) -> JsValue {
vec_to_point(&self.0.tangent(t)) vec_to_point(&self.0.tangent(t))
} }
+16 -15
View File
@@ -1,20 +1,21 @@
/// Implementation constants // Implementation constants
/// Constant used to determine if `f64`s are equivalent.
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
/// A stricter constant used to determine if `f64`s are equivalent.
pub const STRICT_MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-6;
/// Number of distances used in search algorithm for `project`.
pub const NUM_DISTANCES: usize = 5;
/// Maximum allowed angle that the normal of the `start` or `end` point can make with the normal of the corresponding handle for a curve to be considered scalable/simple.
pub const SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE: f64 = std::f64::consts::PI / 3.; pub const SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE: f64 = std::f64::consts::PI / 3.;
/// Method argument defaults // Method argument defaults
pub const REDUCE_STEP_SIZE_DEFAULT: f64 = 0.01;
/// 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: i32 = 10;
/// Default number of subdivisions used in `length` calculation.
/// Number of subdivisions used in `length` calculation pub const DEFAULT_LENGTH_SUBDIVISIONS: i32 = 1000;
pub const LENGTH_SUBDIVISIONS: i32 = 1000; /// Default step size for `reduce` function.
pub const DEFAULT_REDUCE_STEP_SIZE: f64 = 0.01;
/// Number of distances used in search algorithm for `project`
pub const NUM_DISTANCES: usize = 5;
/// Constants used to determine if `f64`'s are equivalent
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
+161 -77
View File
@@ -4,11 +4,13 @@ mod consts;
mod utils; mod utils;
use consts::*; use consts::*;
use glam::{DMat2, DVec2}; use glam::{DMat2, DVec2};
/// Representation of the handle point(s) in a bezier segment. /// Representation of the handle point(s) in a bezier segment.
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
enum BezierHandles { enum BezierHandles {
Linear,
/// Handles for a quadratic curve. /// Handles for a quadratic curve.
Quadratic { Quadratic {
/// Point representing the location of the single handle. /// Point representing the location of the single handle.
@@ -59,6 +61,25 @@ pub struct Bezier {
} }
impl Bezier { impl Bezier {
// TODO: Consider removing this function
/// Create a quadratic bezier using the provided coordinates as the start, handle, and end points.
pub fn from_linear_coordinates(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
Bezier {
start: DVec2::new(x1, y1),
handles: BezierHandles::Linear,
end: DVec2::new(x2, y2),
}
}
/// Create a quadratic bezier using the provided DVec2s as the start, handle, and end points.
pub fn from_linear_dvec2(p1: DVec2, p2: DVec2) -> Self {
Bezier {
start: p1,
handles: BezierHandles::Linear,
end: p2,
}
}
// TODO: Consider removing this function // TODO: Consider removing this function
/// Create a quadratic bezier using the provided coordinates as the start, handle, and end points. /// Create a quadratic bezier using the provided coordinates as the start, handle, and end points.
pub fn from_quadratic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Self { pub fn from_quadratic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Self {
@@ -148,14 +169,15 @@ impl Bezier {
// TODO: Allow modifying the viewport, width and height // TODO: Allow modifying the viewport, width and height
let m_path = format!("M {} {}", self.start.x, self.start.y); let m_path = format!("M {} {}", self.start.x, self.start.y);
let handles_path = match self.handles { let handles_path = match self.handles {
BezierHandles::Linear => "L".to_string(),
BezierHandles::Quadratic { handle } => { BezierHandles::Quadratic { handle } => {
format!("Q {} {}", handle.x, handle.y) format!("Q {} {},", handle.x, handle.y)
} }
BezierHandles::Cubic { handle_start, handle_end } => { BezierHandles::Cubic { handle_start, handle_end } => {
format!("C {} {}, {} {}", handle_start.x, handle_start.y, handle_end.x, handle_end.y) format!("C {} {}, {} {},", handle_start.x, handle_start.y, handle_end.x, handle_end.y)
} }
}; };
let curve_path = format!("{}, {} {}", handles_path, self.end.x, self.end.y); let curve_path = format!("{} {} {}", handles_path, self.end.x, self.end.y);
format!( format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}" width="{}px" height="{}px"><path d="{} {} {}" stroke="black" fill="transparent"/></svg>"#, r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}" width="{}px" height="{}px"><path d="{} {} {}" stroke="black" fill="transparent"/></svg>"#,
0, 0, 100, 100, 100, 100, "\n", m_path, curve_path 0, 0, 100, 100, 100, 100, "\n", m_path, curve_path
@@ -172,9 +194,12 @@ impl Bezier {
self.end = e; self.end = e;
} }
/// Set the coordinates of the first handle point. This represents the only handle in a quadratic segment. /// Set the coordinates of the first handle point. This represents the only handle in a quadratic segment. If used on a linear segment, it will be changed to a quadratic.
pub fn set_handle_start(&mut self, h1: DVec2) { pub fn set_handle_start(&mut self, h1: DVec2) {
match self.handles { match self.handles {
BezierHandles::Linear => {
self.handles = BezierHandles::Quadratic { handle: h1 };
}
BezierHandles::Quadratic { ref mut handle } => { BezierHandles::Quadratic { ref mut handle } => {
*handle = h1; *handle = h1;
} }
@@ -184,9 +209,15 @@ impl Bezier {
}; };
} }
/// Set the coordinates of the second handle point. This will convert a quadratic segment into a cubic one. /// Set the coordinates of the second handle point. This will convert both linear and quadratic segments into cubic ones. For a linear segment, the first handle will be set to the start point.
pub fn set_handle_end(&mut self, h2: DVec2) { pub fn set_handle_end(&mut self, h2: DVec2) {
match self.handles { match self.handles {
BezierHandles::Linear => {
self.handles = BezierHandles::Cubic {
handle_start: self.start,
handle_end: h2,
};
}
BezierHandles::Quadratic { handle } => { BezierHandles::Quadratic { handle } => {
self.handles = BezierHandles::Cubic { handle_start: handle, handle_end: h2 }; self.handles = BezierHandles::Cubic { handle_start: handle, handle_end: h2 };
} }
@@ -207,39 +238,45 @@ impl Bezier {
} }
/// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment. /// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment.
pub fn handle_start(&self) -> DVec2 { pub fn handle_start(&self) -> Option<DVec2> {
match self.handles { match self.handles {
BezierHandles::Quadratic { handle } => handle, BezierHandles::Linear => None,
BezierHandles::Cubic { handle_start, .. } => handle_start, BezierHandles::Quadratic { handle } => Some(handle),
BezierHandles::Cubic { handle_start, .. } => Some(handle_start),
} }
} }
/// Get the coordinates of the second handle point. This will return `None` for a quadratic segment. /// Get the coordinates of the second handle point. This will return `None` for a quadratic segment.
pub fn handle_end(&self) -> Option<DVec2> { pub fn handle_end(&self) -> Option<DVec2> {
match self.handles { match self.handles {
BezierHandles::Linear { .. } => None,
BezierHandles::Quadratic { .. } => None, BezierHandles::Quadratic { .. } => None,
BezierHandles::Cubic { handle_end, .. } => Some(handle_end), BezierHandles::Cubic { handle_end, .. } => Some(handle_end),
} }
} }
/// Get the coordinates of all points in an array of 4 optional points. /// Get an iterator over the coordinates of all points in a vector.
/// For a quadratic segment, the order of the points will be: `start`, `handle`, `end`. The fourth element will be `None`. /// - For a linear segment, the order of the points will be: `start`, `end`.
/// For a cubic segment, the order of the points will be: `start`, `handle_start`, `handle_end`, `end`. /// - For a quadratic segment, the order of the points will be: `start`, `handle`, `end`.
pub fn get_points(&self) -> [Option<DVec2>; 4] { /// - For a cubic segment, the order of the points will be: `start`, `handle_start`, `handle_end`, `end`.
pub fn get_points(&self) -> impl Iterator<Item = DVec2> {
match self.handles { match self.handles {
BezierHandles::Quadratic { handle } => [Some(self.start), Some(handle), Some(self.end), None], BezierHandles::Linear => [self.start, self.end, DVec2::ZERO, DVec2::ZERO].into_iter().take(2),
BezierHandles::Cubic { handle_start, handle_end } => [Some(self.start), Some(handle_start), Some(handle_end), Some(self.end)], BezierHandles::Quadratic { handle } => [self.start, handle, self.end, DVec2::ZERO].into_iter().take(3),
BezierHandles::Cubic { handle_start, handle_end } => [self.start, handle_start, handle_end, self.end].into_iter().take(4),
} }
} }
/// Calculate the point on the curve based on the `t`-value provided. /// Calculate the point on the curve based on the `t`-value provided.
/// Basis code based off of pseudocode found here: <https://pomax.github.io/bezierinfo/#explanation>. fn unrestricted_evaluate(&self, t: f64) -> DVec2 {
fn unrestricted_compute(&self, t: f64) -> DVec2 { // 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.0 - 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::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.0 * 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;
@@ -251,9 +288,9 @@ 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 compute(&self, t: f64) -> DVec2 { pub fn evaluate(&self, t: f64) -> DVec2 {
assert!((0.0..=1.0).contains(&t)); assert!((0.0..=1.0).contains(&t));
self.unrestricted_compute(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.
@@ -264,90 +301,102 @@ impl Bezier {
let mut steps_array = Vec::with_capacity((steps_unwrapped + 1) as usize); let mut steps_array = Vec::with_capacity((steps_unwrapped + 1) as usize);
for t in 0..steps_unwrapped + 1 { for t in 0..steps_unwrapped + 1 {
steps_array.push(self.compute(f64::from(t) * ratio)) steps_array.push(self.evaluate(f64::from(t) * ratio))
} }
steps_array steps_array
} }
/// Return an approximation of the length of the bezier curve. /// Return an approximation of the length of the bezier curve.
pub fn length(&self) -> f64 { /// - `num_subdivisions` - Number of subdivisions used to approximate the curve. The default value is 1000.
// Code example from <https://gamedev.stackexchange.com/questions/5373/moving-ships-between-two-planets-along-a-bezier-missing-some-equations-for-acce/5427#5427>. pub fn length(&self, num_subdivisions: Option<i32>) -> f64 {
match self.handles {
BezierHandles::Linear => self.start.distance(self.end),
_ => {
// Code example from <https://gamedev.stackexchange.com/questions/5373/moving-ships-between-two-planets-along-a-bezier-missing-some-equations-for-acce/5427#5427>.
// We will use an approximate approach where // We will use an approximate approach where we split the curve into many subdivisions
// 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(LENGTH_SUBDIVISIONS)); let mut approx_curve_length = 0.0;
let mut approx_curve_length = 0.0; let mut previous_point = lookup_table[0];
let mut prev_point = lookup_table[0]; // Calculate approximate distance between subdivision
// calculate approximate distance between subdivision for current_point in lookup_table.iter().skip(1) {
for curr_point in lookup_table.iter().skip(1) { // Calculate distance of subdivision
// calculate distance of subdivision approx_curve_length += (*current_point - previous_point).length();
approx_curve_length += (*curr_point - prev_point).length(); // Update the previous point
// update the prev point previous_point = *current_point;
prev_point = *curr_point; }
approx_curve_length
}
} }
approx_curve_length
} }
/// Returns a vector representing the derivative at the point designated by `t` on the curve. /// Returns a Bezier representing the derivative of the original curve.
pub fn derivative(&self, t: f64) -> DVec2 { /// - This function returns `None` for a linear segment.
let one_minus_t = 1. - t; pub fn derivative(&self) -> Option<Bezier> {
match self.handles { match self.handles {
BezierHandles::Linear => None,
BezierHandles::Quadratic { handle } => { BezierHandles::Quadratic { handle } => {
let p1_minus_p0 = handle - self.start; let p1_minus_p0 = handle - self.start;
let p2_minus_p1 = self.end - handle; let p2_minus_p1 = self.end - handle;
2. * one_minus_t * p1_minus_p0 + 2. * t * p2_minus_p1 Some(Bezier::from_linear_dvec2(2. * p1_minus_p0, 2. * p2_minus_p1))
} }
BezierHandles::Cubic { handle_start, handle_end } => { BezierHandles::Cubic { handle_start, handle_end } => {
let p1_minus_p0 = handle_start - self.start; let p1_minus_p0 = handle_start - self.start;
let p2_minus_p1 = handle_end - handle_start; let p2_minus_p1 = handle_end - handle_start;
let p3_minus_p2 = self.end - handle_end; let p3_minus_p2 = self.end - handle_end;
3. * one_minus_t * one_minus_t * p1_minus_p0 + 6. * t * one_minus_t * p2_minus_p1 + 3. * t * t * p3_minus_p2 Some(Bezier::from_quadratic_dvec2(3. * p1_minus_p0, 3. * p2_minus_p1, 3. * p3_minus_p2))
} }
} }
} }
/// Returns a normalized unit vector representing the tangent at the point designated by `t` on the curve. /// Returns a normalized unit vector representing the tangent at the point designated by `t` on the curve.
pub fn tangent(&self, t: f64) -> DVec2 { pub fn tangent(&self, t: f64) -> DVec2 {
self.derivative(t).normalize() match self.handles {
BezierHandles::Linear => self.end - self.start,
_ => self.derivative().unwrap().evaluate(t),
}
.normalize()
} }
/// Returns a normalized unit vector representing the direction of the normal at the point designated by `t` on the curve. /// Returns a normalized unit vector representing the direction of the normal at the point designated by `t` on the curve.
pub fn normal(&self, t: f64) -> DVec2 { pub fn normal(&self, t: f64) -> DVec2 {
let derivative = self.derivative(t); self.tangent(t).perp()
derivative.normalize().perp()
} }
/// Returns the pair of Bezier curves that result from splitting the original curve at the point corresponding to `t`. /// Returns the pair of Bezier curves that result from splitting the original curve at the point corresponding to `t`.
pub fn split(&self, t: f64) -> [Bezier; 2] { pub fn split(&self, t: f64) -> [Bezier; 2] {
let split_point = self.compute(t); let split_point = self.evaluate(t);
let t_squared = t * t;
let t_minus_one = t - 1.;
let squared_t_minus_one = t_minus_one * t_minus_one;
match self.handles { match self.handles {
BezierHandles::Linear => [Bezier::from_linear_dvec2(self.start, split_point), Bezier::from_linear_dvec2(split_point, self.end)],
// TODO: Actually calculate the correct handle locations // TODO: Actually calculate the correct handle locations
BezierHandles::Quadratic { handle } => [ BezierHandles::Quadratic { handle } => {
Bezier::from_quadratic_dvec2(self.start, t * handle - t_minus_one * self.start, split_point), let t_minus_one = t - 1.;
Bezier::from_quadratic_dvec2(split_point, t * self.end - t_minus_one * handle, self.end), [
], Bezier::from_quadratic_dvec2(self.start, t * handle - t_minus_one * self.start, split_point),
BezierHandles::Cubic { handle_start, handle_end } => [ Bezier::from_quadratic_dvec2(split_point, t * self.end - t_minus_one * handle, self.end),
Bezier::from_cubic_dvec2( ]
self.start, }
t * handle_start - t_minus_one * self.start, BezierHandles::Cubic { handle_start, handle_end } => {
t_squared * handle_end - 2. * t * t_minus_one * handle_start + squared_t_minus_one * self.start, let t_minus_one = t - 1.;
split_point, [
), Bezier::from_cubic_dvec2(
Bezier::from_cubic_dvec2( self.start,
split_point, t * handle_start - t_minus_one * self.start,
t_squared * self.end - 2. * t * t_minus_one * handle_end + squared_t_minus_one * handle_start, (t * t) * handle_end - 2. * t * t_minus_one * handle_start + (t_minus_one * t_minus_one) * self.start,
t * self.end - t_minus_one * handle_end, split_point,
self.end, ),
), Bezier::from_cubic_dvec2(
], split_point,
(t * t) * self.end - 2. * t * t_minus_one * handle_end + (t_minus_one * t_minus_one) * handle_start,
t * self.end - t_minus_one * handle_end,
self.end,
),
]
}
} }
} }
@@ -379,6 +428,7 @@ impl Bezier {
iteration_limit, iteration_limit,
} = options; } = options;
// TODO: Consider optimizations from precomputing useful values, or using the GPU
// First find the closest point from the results of a lookup table // First find the closest point from the results of a lookup table
let lut = self.compute_lookup_table(Some(lut_size)); let lut = self.compute_lookup_table(Some(lut_size));
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);
@@ -421,7 +471,7 @@ impl Bezier {
if step_index == 0 { if step_index == 0 {
distance = *table_distance; distance = *table_distance;
} else { } else {
distance = point.distance(self.compute(iterator_t)); distance = point.distance(self.evaluate(iterator_t));
*table_distance = distance; *table_distance = distance;
} }
if distance < new_minimum_distance { if distance < new_minimum_distance {
@@ -456,13 +506,14 @@ impl Bezier {
} }
} }
self.compute(final_t) self.evaluate(final_t)
} }
/// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric curves respectively. /// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric curves respectively.
/// The local extrema are defined to be points at which the derivative of the curve is equal to zero. /// The local extrema are defined to be points at which the derivative of the curve is equal to zero.
fn unrestricted_local_extrema(&self) -> [Vec<f64>; 2] { fn unrestricted_local_extrema(&self) -> [Vec<f64>; 2] {
match self.handles { match self.handles {
BezierHandles::Linear => [Vec::new(), Vec::new()],
BezierHandles::Quadratic { handle } => { BezierHandles::Quadratic { handle } => {
let a = handle - self.start; let a = handle - self.start;
let b = self.end - handle; let b = self.end - handle;
@@ -499,6 +550,7 @@ impl Bezier {
let transformed_start = transformation_function(self.start); let transformed_start = transformation_function(self.start);
let transformed_end = transformation_function(self.end); let transformed_end = transformation_function(self.end);
match self.handles { match self.handles {
BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end),
BezierHandles::Quadratic { handle } => { BezierHandles::Quadratic { handle } => {
let transformed_handle = transformation_function(handle); let transformed_handle = transformation_function(handle);
Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end) Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end)
@@ -522,7 +574,10 @@ impl Bezier {
self.apply_transformation(&|point| point + translation) self.apply_transformation(&|point| point + translation)
} }
/// Returns a list of points where the provided line segment intersects with the Bezier curve. // TODO: Use an `impl Iterator` return type instead of a `Vec`
// TODO: Change this to `intersect(&self, other: &Bezier)` to also work on quadratic and cubic segments
// TODO: (or keep this and add two more functions that perform the logic, and make the `intersect` function call the correct one)
/// Returns a list of points where the provided line segment intersects with the Bezier curve. If the provided segment is colinear with the bezier, zero intersection points will be returned.
/// - `line` - A line segment expected to be received in the format of `[start_point, end_point]`. /// - `line` - A line segment expected to be received in the format of `[start_point, end_point]`.
pub fn intersect_line_segment(&self, line: [DVec2; 2]) -> Vec<DVec2> { pub fn intersect_line_segment(&self, line: [DVec2; 2]) -> Vec<DVec2> {
// Rotate the bezier and the line by the angle that the line makes with the x axis // Rotate the bezier and the line by the angle that the line makes with the x axis
@@ -538,6 +593,12 @@ impl Bezier {
// Compute the roots of the resulting bezier curve // Compute the roots of the resulting bezier curve
let list_intersection_t = match translated_bezier.handles { let list_intersection_t = match translated_bezier.handles {
BezierHandles::Linear => {
// If the transformed linear bezier is on the x-axis, `a` and `b` will both be zero and `solve_linear` will return no roots
let a = translated_bezier.end.y - translated_bezier.start.y;
let b = translated_bezier.start.y;
utils::solve_linear(a, b)
}
BezierHandles::Quadratic { handle } => { BezierHandles::Quadratic { handle } => {
let a = translated_bezier.start.y - 2. * handle.y + translated_bezier.end.y; let a = translated_bezier.start.y - 2. * handle.y + translated_bezier.end.y;
let b = 2. * (handle.y - translated_bezier.start.y); let b = 2. * (handle.y - translated_bezier.start.y);
@@ -558,13 +619,14 @@ impl Bezier {
utils::solve_cubic(a, b, c, d) utils::solve_cubic(a, b, c, d)
} }
}; };
let min = line[0].min(line[1]); let min = line[0].min(line[1]);
let max = line[0].max(line[1]); let max = line[0].max(line[1]);
list_intersection_t list_intersection_t
.iter() .iter()
.filter(|&&t| utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE)) .filter(|&&t| utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE))
.map(|&t| self.unrestricted_compute(t)) .map(|&t| self.unrestricted_evaluate(t))
.filter(|&point| utils::dvec2_approximately_in_range(point, min, max, MAX_ABSOLUTE_DIFFERENCE).all()) .filter(|&point| utils::dvec2_approximately_in_range(point, min, max, MAX_ABSOLUTE_DIFFERENCE).all())
.collect::<Vec<DVec2>>() .collect::<Vec<DVec2>>()
} }
@@ -594,7 +656,12 @@ impl Bezier {
/// - `step_size` - Dictates the granularity at which the function searches for reducible subcurves. The default value is `0.01`. /// - `step_size` - Dictates the granularity at which the function searches for reducible subcurves. The default value is `0.01`.
/// A small granularity may increase the chance the function does not introduce gaps, but will increase computation time. /// A small granularity may increase the chance the function does not introduce gaps, but will increase computation time.
pub fn reduce(&self, step_size: Option<f64>) -> Vec<Bezier> { pub fn reduce(&self, step_size: Option<f64>) -> Vec<Bezier> {
let step_size = step_size.unwrap_or(REDUCE_STEP_SIZE_DEFAULT); // A linear segment is scalable, so return itself
if let BezierHandles::Linear = self.handles {
return vec![*self];
}
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 mut extrema: Vec<f64> = self.local_extrema().into_iter().flatten().collect::<Vec<f64>>();
extrema.append(&mut vec![0., 1.]); extrema.append(&mut vec![0., 1.]);
@@ -675,13 +742,13 @@ mod tests {
let p3 = DVec2::new(160., 170.); let p3 = DVec2::new(160., 170.);
let bezier1 = Bezier::quadratic_through_points(p1, p2, p3, None); let bezier1 = Bezier::quadratic_through_points(p1, p2, p3, None);
assert!(compare_points(bezier1.compute(0.5), p2)); assert!(compare_points(bezier1.evaluate(0.5), p2));
let bezier2 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.8)); let bezier2 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.8));
assert!(compare_points(bezier2.compute(0.8), p2)); assert!(compare_points(bezier2.evaluate(0.8), p2));
let bezier3 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.)); let bezier3 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.));
assert!(compare_points(bezier3.compute(0.), p2)); assert!(compare_points(bezier3.evaluate(0.), p2));
} }
#[test] #[test]
@@ -691,13 +758,13 @@ mod tests {
let p3 = DVec2::new(160., 160.); let p3 = DVec2::new(160., 160.);
let bezier1 = Bezier::cubic_through_points(p1, p2, p3, Some(0.3), Some(10.)); let bezier1 = Bezier::cubic_through_points(p1, p2, p3, Some(0.3), Some(10.));
assert!(compare_points(bezier1.compute(0.3), p2)); assert!(compare_points(bezier1.evaluate(0.3), p2));
let bezier2 = Bezier::cubic_through_points(p1, p2, p3, Some(0.8), Some(91.7)); let bezier2 = Bezier::cubic_through_points(p1, p2, p3, Some(0.8), Some(91.7));
assert!(compare_points(bezier2.compute(0.8), p2)); assert!(compare_points(bezier2.evaluate(0.8), p2));
let bezier3 = Bezier::cubic_through_points(p1, p2, p3, Some(0.), Some(91.7)); let bezier3 = Bezier::cubic_through_points(p1, p2, p3, Some(0.), Some(91.7));
assert!(compare_points(bezier3.compute(0.), p2)); assert!(compare_points(bezier3.evaluate(0.), p2));
} }
#[test] #[test]
@@ -711,6 +778,23 @@ 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.project(DVec2::new(100., 0.), project_options) == DVec2::new(0., 0.)); assert!(bezier2.project(DVec2::new(100., 0.), project_options) == DVec2::new(0., 0.));
} }
#[test]
fn intersect_line_segment_linear() {
let p1 = DVec2::new(30., 60.);
let p2 = DVec2::new(140., 120.);
// Intersection at edge of curve
let bezier1 = Bezier::from_linear_dvec2(p1, p2);
let line1 = [DVec2::new(20., 60.), DVec2::new(70., 60.)];
let intersections1 = bezier1.intersect_line_segment(line1);
assert!(intersections1.len() == 1);
assert!(compare_points(intersections1[0], DVec2::new(30., 60.)));
// Intersection in the middle of curve
let line2 = [DVec2::new(150., 150.), DVec2::new(30., 30.)];
let intersections2 = bezier1.intersect_line_segment(line2);
assert!(compare_points(intersections2[0], DVec2::new(96., 96.)));
}
#[test] #[test]
fn intersect_line_segment_quadratic() { fn intersect_line_segment_quadratic() {
+42 -28
View File
@@ -1,3 +1,5 @@
use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE};
use glam::{BVec2, DVec2}; use glam::{BVec2, DVec2};
use std::f64::consts::PI; use std::f64::consts::PI;
@@ -40,15 +42,18 @@ pub fn get_closest_point_in_lut(lut: &[DVec2], point: DVec2) -> (i32, f64) {
.unwrap() .unwrap()
} }
// TODO: Use an `Option` return type instead of a `Vec`
/// Find the roots of the linear equation `ax + b`. /// Find the roots of the linear equation `ax + b`.
pub fn solve_linear(a: f64, b: f64) -> Vec<f64> { pub fn solve_linear(a: f64, b: f64) -> Vec<f64> {
let mut roots = Vec::new(); let mut roots = Vec::new();
if a != 0. { // There exist roots when `a` is not 0
if a.abs() > MAX_ABSOLUTE_DIFFERENCE {
roots.push(-b / a); roots.push(-b / a);
} }
roots roots
} }
// TODO: Use an `impl Iterator` return type instead of a `Vec`
/// Find the roots of the linear equation `ax^2 + bx + c`. /// Find the roots of the linear equation `ax^2 + bx + c`.
/// Precompute the `discriminant` (`b^2 - 4ac`) and `two_times_a` arguments prior to calling this function for efficiency purposes. /// Precompute the `discriminant` (`b^2 - 4ac`) and `two_times_a` arguments prior to calling this function for efficiency purposes.
pub fn solve_quadratic(discriminant: f64, two_times_a: f64, b: f64, c: f64) -> Vec<f64> { pub fn solve_quadratic(discriminant: f64, two_times_a: f64, b: f64, c: f64) -> Vec<f64> {
@@ -76,35 +81,39 @@ fn cube_root(f: f64) -> f64 {
} }
} }
// TODO: Use an `impl Iterator` return type instead of a `Vec`
/// Solve a cubic of the form `x^3 + px + q`, derivation from: <https://trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm>. /// Solve a cubic of the form `x^3 + px + q`, derivation from: <https://trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm>.
pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec<f64> { pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec<f64> {
let mut roots = Vec::new(); let mut roots = Vec::new();
if p == 0. { if p.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
// Handle when p is approximately 0
roots.push(cube_root(-q)); roots.push(cube_root(-q));
} else if q == 0. { } else if q.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
// Handle when q is approximately 0
if p < 0. { if p < 0. {
roots.push((-p).powf(1. / 2.)); roots.push((-p).powf(1. / 2.));
} }
} else if discriminant == 0. { } else if discriminant.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
// When discriminant is 0 (check for approximation because of floating point errors), all roots are real, and 2 are repeated
let q_divided_by_2 = q / 2.; let q_divided_by_2 = q / 2.;
let a_divided_by_3 = a / 3.; let a_divided_by_3 = a / 3.;
// all roots are real, and 2 are repeated
roots.push(2. * cube_root(-q_divided_by_2) - a_divided_by_3); roots.push(2. * cube_root(-q_divided_by_2) - a_divided_by_3);
roots.push(cube_root(q_divided_by_2) - a_divided_by_3); roots.push(cube_root(q_divided_by_2) - a_divided_by_3);
} else if discriminant > 0. { } else if discriminant > 0. {
// one real and two imaginary roots // When discriminant > 0, there is one real and two imaginary roots
let q_divided_by_2 = q / 2.; let q_divided_by_2 = q / 2.;
let square_root_discriminant = discriminant.powf(1. / 2.); let square_root_discriminant = discriminant.powf(1. / 2.);
roots.push(cube_root(-q_divided_by_2 + square_root_discriminant) - cube_root(q_divided_by_2 + square_root_discriminant) - a / 3.); roots.push(cube_root(-q_divided_by_2 + square_root_discriminant) - cube_root(q_divided_by_2 + square_root_discriminant) - a / 3.);
} else { } else {
// three real roots // Otherwise, discriminant < 0 and there are three real roots
let p_divided_by_3 = p / 3.; let p_divided_by_3 = p / 3.;
let a_divided_by_3 = a / 3.; let a_divided_by_3 = a / 3.;
let cube_root_r = (-p_divided_by_3).powf(1. / 2.); let cube_root_r = (-p_divided_by_3).powf(1. / 2.);
let phi = (-q / (2. * cube_root_r.powi(3))).acos(); let phi = (-q / (2. * cube_root_r.powi(3))).acos();
let two_times_cube_root_r = 2. * cube_root_r; let two_times_cube_root_r = 2. * cube_root_r;
// three real roots
roots.push(two_times_cube_root_r * (phi / 3.).cos() - a_divided_by_3); roots.push(two_times_cube_root_r * (phi / 3.).cos() - a_divided_by_3);
roots.push(two_times_cube_root_r * ((phi + 2. * PI) / 3.).cos() - a_divided_by_3); roots.push(two_times_cube_root_r * ((phi + 2. * PI) / 3.).cos() - a_divided_by_3);
roots.push(two_times_cube_root_r * ((phi + 4. * PI) / 3.).cos() - a_divided_by_3); roots.push(two_times_cube_root_r * ((phi + 4. * PI) / 3.).cos() - a_divided_by_3);
@@ -112,10 +121,11 @@ pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec
roots roots
} }
// TODO: Use an `impl Iterator` return type instead of a `Vec`
/// Solve a cubic of the form `ax^3 + bx^2 + ct + d`. /// Solve a cubic of the form `ax^3 + bx^2 + ct + d`.
pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> { pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
if a.abs() <= 1e-5 { if a.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
if b.abs() <= 1e-5 { if b.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
// If both a and b are approximately 0, treat as a linear problem // If both a and b are approximately 0, treat as a linear problem
solve_linear(c, d) solve_linear(c, d)
} else { } else {
@@ -161,44 +171,48 @@ mod tests {
use super::*; use super::*;
use crate::consts::MAX_ABSOLUTE_DIFFERENCE; use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
/// Compare vectors of `f64`s with a provided max absolute value difference.
fn f64_compare_vector(vec1: Vec<f64>, vec2: Vec<f64>, max_abs_diff: f64) -> bool {
vec1.len() == vec2.len() && vec1.into_iter().zip(vec2.into_iter()).all(|(a, b)| f64_compare(a, b, max_abs_diff))
}
#[test]
fn test_solve_linear() {
// Line that is on the x-axis
assert!(solve_linear(0., 0.).is_empty());
// Line that is parallel to but not on the x-axis
assert!(solve_linear(0., 1.).is_empty());
// Line with a non-zero slope
assert!(solve_linear(2., -8.) == vec![4.]);
}
#[test] #[test]
fn test_solve_cubic() { fn test_solve_cubic() {
// discriminant == 0 // discriminant == 0
let roots1 = solve_cubic(1., 0., 0., 0.); let roots1 = solve_cubic(1., 0., 0., 0.);
assert!(roots1.len() == 1); assert!(roots1 == vec![0.]);
assert!(roots1[0] == 0.);
let roots2 = solve_cubic(1., 3., 0., -4.); let roots2 = solve_cubic(1., 3., 0., -4.);
assert!(roots2.len() == 2); assert!(roots2 == vec![1., -2.]);
assert!(roots2[0] == 1.);
assert!(roots2[1] == -2.);
// p == 0 // p == 0
let roots3 = solve_cubic(1., 0., 0., -1.); let roots3 = solve_cubic(1., 0., 0., -1.);
assert!(roots3.len() == 1); assert!(roots3 == vec![1.]);
assert!(roots3[0] == 1.);
// discriminant > 0 // discriminant > 0
let roots4 = solve_cubic(1., 3., 0., 2.); let roots4 = solve_cubic(1., 3., 0., 2.);
assert!(roots4.len() == 1); assert!(f64_compare_vector(roots4, vec![-3.196], MAX_ABSOLUTE_DIFFERENCE));
assert!(f64_compare(roots4[0], -3.196, MAX_ABSOLUTE_DIFFERENCE));
// discriminant < 0 // discriminant < 0
let roots5 = solve_cubic(1., 3., 0., -1.); let roots5 = solve_cubic(1., 3., 0., -1.);
assert!(roots5.len() == 3); assert!(f64_compare_vector(roots5, vec![0.532, -2.879, -0.653], MAX_ABSOLUTE_DIFFERENCE));
assert!(f64_compare(roots5[0], 0.532, MAX_ABSOLUTE_DIFFERENCE));
assert!(f64_compare(roots5[1], -2.879, MAX_ABSOLUTE_DIFFERENCE));
assert!(f64_compare(roots5[2], -0.653, MAX_ABSOLUTE_DIFFERENCE));
// quadratic // quadratic
let roots6 = solve_cubic(0., 3., 0., -3.); let roots6 = solve_cubic(0., 3., 0., -3.);
assert!(roots6.len() == 2); assert!(roots6 == vec![1., -1.]);
assert!(roots6[0] == 1.);
assert!(roots6[1] == -1.);
// linear // linear
let roots7 = solve_cubic(0., 0., 1., -1.); let roots7 = solve_cubic(0., 0., 1., -1.);
assert!(roots7.len() == 1); assert!(roots7 == vec![1.]);
assert!(roots7[0] == 1.);
} }
} }