mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-22 06:38:12 +08:00
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:
committed by
Keavon Chambers
co-authored by
Hannah Li
Rob Nadal
Keavon Chambers
parent
0f88055573
commit
b84e647f40
@@ -25,8 +25,8 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, markRaw } from "vue";
|
||||
|
||||
import { drawBezier, drawBezierHelper, drawCircle, drawCurve, drawLine, drawPoint, drawText, getContextFromCanvas, COLORS } from "@/utils/drawing";
|
||||
import { BezierCurveType, Point, WasmBezierInstance, WasmSubpathInstance } from "@/utils/types";
|
||||
import { drawBezier, drawBezierHelper, drawCircle, drawCircleSector, drawCurve, drawLine, drawPoint, drawText, getContextFromCanvas, COLORS } from "@/utils/drawing";
|
||||
import { BezierCurveType, CircleSector, Point, WasmBezierInstance, WasmSubpathInstance } from "@/utils/types";
|
||||
|
||||
import ExamplePane from "@/components/ExamplePane.vue";
|
||||
import SliderExample from "@/components/SliderExample.vue";
|
||||
@@ -115,10 +115,10 @@ export default defineComponent({
|
||||
{
|
||||
name: "Lookup Table",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const lookupPoints = bezier.compute_lookup_table(options.steps);
|
||||
lookupPoints.forEach((serializedPoint, index) => {
|
||||
const lookupPoints: Point[] = JSON.parse(bezier.compute_lookup_table(options.steps));
|
||||
lookupPoints.forEach((point, index) => {
|
||||
if (index !== 0 && index !== lookupPoints.length - 1) {
|
||||
drawPoint(getContextFromCanvas(canvas), JSON.parse(serializedPoint), 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
drawPoint(getContextFromCanvas(canvas), point, 3, COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -142,7 +142,7 @@ export default defineComponent({
|
||||
|
||||
const derivativeBezier = bezier.derivative();
|
||||
if (derivativeBezier) {
|
||||
const points: Point[] = derivativeBezier.get_points().map((p) => JSON.parse(p));
|
||||
const points: Point[] = JSON.parse(derivativeBezier.get_points());
|
||||
if (points.length === 2) {
|
||||
drawLine(context, points[0], points[1], COLORS.NON_INTERACTIVE.STROKE_1);
|
||||
} else {
|
||||
@@ -356,10 +356,7 @@ export default defineComponent({
|
||||
name: "Rotate",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const rotatedBezier = bezier
|
||||
.rotate(options.angle * Math.PI)
|
||||
.get_points()
|
||||
.map((p) => JSON.parse(p));
|
||||
const rotatedBezier = JSON.parse(bezier.rotate(options.angle * Math.PI).get_points());
|
||||
drawBezier(context, rotatedBezier, null, { curveStrokeColor: COLORS.NON_INTERACTIVE.STROKE_1, radius: 3.5 });
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
@@ -496,6 +493,57 @@ export default defineComponent({
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Arcs",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
const context = getContextFromCanvas(canvas);
|
||||
const arcs: CircleSector[] = JSON.parse(bezier.arcs(options.error, options.max_iterations, options.strategy));
|
||||
arcs.forEach((circleSector, index) => {
|
||||
drawCircleSector(context, circleSector, `hsl(${40 * index}, 100%, 50%, 75%)`, `hsl(${40 * index}, 100%, 50%, 37.5%)`);
|
||||
});
|
||||
},
|
||||
template: markRaw(SliderExample),
|
||||
templateOptions: {
|
||||
sliders: [
|
||||
{
|
||||
variable: "strategy",
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 1,
|
||||
default: 0,
|
||||
unit: [": Automatic", ": FavorLargerArcs", ": FavorCorrectness"],
|
||||
},
|
||||
{
|
||||
variable: "error",
|
||||
min: 0.05,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
default: 0.5,
|
||||
},
|
||||
{
|
||||
variable: "max_iterations",
|
||||
min: 50,
|
||||
max: 200,
|
||||
step: 1,
|
||||
default: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
curveDegrees: new Set([BezierCurveType.Quadratic, BezierCurveType.Cubic]),
|
||||
customPoints: {
|
||||
[BezierCurveType.Quadratic]: [
|
||||
[50, 50],
|
||||
[85, 65],
|
||||
[100, 100],
|
||||
],
|
||||
[BezierCurveType.Cubic]: [
|
||||
[160, 180],
|
||||
[170, 10],
|
||||
[30, 90],
|
||||
[180, 160],
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Offset",
|
||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
|
||||
|
||||
@@ -35,16 +35,14 @@ class BezierDrawing {
|
||||
this.callback = callback;
|
||||
this.options = options;
|
||||
this.createThroughPoints = createThroughPoints;
|
||||
this.points = bezier
|
||||
.get_points()
|
||||
.map((p) => JSON.parse(p))
|
||||
.map((p, i, points) => ({
|
||||
x: p.x,
|
||||
y: p.y,
|
||||
r: getPointSizeByIndex(i, points.length),
|
||||
selected: false,
|
||||
manipulator: MANIPULATOR_KEYS_FROM_BEZIER_TYPE[points.length][i],
|
||||
}));
|
||||
const bezierPoints: Point[] = JSON.parse(bezier.get_points());
|
||||
this.points = bezierPoints.map((p, i, points) => ({
|
||||
x: p.x,
|
||||
y: p.y,
|
||||
r: getPointSizeByIndex(i, points.length),
|
||||
selected: false,
|
||||
manipulator: MANIPULATOR_KEYS_FROM_BEZIER_TYPE[points.length][i],
|
||||
}));
|
||||
|
||||
if (this.createThroughPoints && this.points.length === 4) {
|
||||
// Use the first handler as the middle point
|
||||
@@ -122,7 +120,7 @@ class BezierDrawing {
|
||||
|
||||
// For the create through points cases, we store a bezier where the handle is actually the point that the curve should pass through
|
||||
// This is so that we can re-use the drag and drop logic, while simply drawing the desired bezier instead
|
||||
const actualBezierPointLength = this.bezier.get_points().length;
|
||||
const actualBezierPointLength = JSON.parse(this.bezier.get_points()).length;
|
||||
let pointsToDraw = this.points;
|
||||
|
||||
let styleConfig: Partial<BezierStyleConfig> = {
|
||||
@@ -130,14 +128,14 @@ class BezierDrawing {
|
||||
};
|
||||
let dragIndex = this.dragIndex;
|
||||
if (this.createThroughPoints) {
|
||||
let serializedPoints;
|
||||
let bezierThroughPoints;
|
||||
const pointList = this.points.map((p) => [p.x, p.y]);
|
||||
if (actualBezierPointLength === 3) {
|
||||
serializedPoints = WasmBezier.quadratic_through_points(pointList, this.options.t);
|
||||
bezierThroughPoints = WasmBezier.quadratic_through_points(pointList, this.options.t);
|
||||
} else {
|
||||
serializedPoints = WasmBezier.cubic_through_points(pointList, this.options.t, this.options["midpoint separation"]);
|
||||
bezierThroughPoints = WasmBezier.cubic_through_points(pointList, this.options.t, this.options["midpoint separation"]);
|
||||
}
|
||||
pointsToDraw = serializedPoints.get_points().map((p) => JSON.parse(p));
|
||||
pointsToDraw = JSON.parse(bezierThroughPoints.get_points());
|
||||
if (this.dragIndex === 1) {
|
||||
// Do not propagate dragIndex when the the non-endpoint is moved
|
||||
dragIndex = null;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<Example :title="title" :bezier="bezier" :callback="callback" :options="sliderData" :createThroughPoints="createThroughPoints" />
|
||||
<div v-for="(slider, index) in templateOptions.sliders" :key="index">
|
||||
<div class="slider-label">{{ slider.variable }} = {{ sliderData[slider.variable] }}{{ sliderUnits[slider.variable] }}</div>
|
||||
<div class="slider-label">{{ slider.variable }} = {{ sliderData[slider.variable] }}{{ getSliderValue(sliderData[slider.variable], sliderUnits[slider.variable]) }}</div>
|
||||
<input class="slider" v-model.number="sliderData[slider.variable]" type="range" :step="slider.step" :min="slider.min" :max="slider.max" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,6 +45,9 @@ export default defineComponent({
|
||||
components: {
|
||||
Example,
|
||||
},
|
||||
methods: {
|
||||
getSliderValue: (sliderValue: number, sliderUnit?: string | string[]) => (Array.isArray(sliderUnit) ? sliderUnit[sliderValue] : sliderUnit),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BezierStyleConfig, Point, WasmBezierInstance } from "@/utils/types";
|
||||
import { BezierStyleConfig, CircleSector, Point, WasmBezierInstance } from "@/utils/types";
|
||||
|
||||
const HANDLE_RADIUS_FACTOR = 2 / 3;
|
||||
const DEFAULT_ENDPOINT_RADIUS = 5;
|
||||
@@ -81,8 +81,22 @@ export const drawCircle = (ctx: CanvasRenderingContext2D, point: Point, radius:
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
export const drawCircleSector = (ctx: CanvasRenderingContext2D, circleSector: CircleSector, strokeColor = COLORS.INTERACTIVE.STROKE_1, fillColor = COLORS.NON_INTERACTIVE.STROKE_1): void => {
|
||||
ctx.strokeStyle = strokeColor;
|
||||
ctx.fillStyle = fillColor;
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
const { center, radius, startAngle, endAngle } = circleSector;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(center.x, center.y);
|
||||
ctx.arc(center.x, center.y, radius, startAngle, endAngle);
|
||||
ctx.lineTo(center.x, center.y);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
export const drawBezierHelper = (ctx: CanvasRenderingContext2D, bezier: WasmBezierInstance, bezierStyleConfig: Partial<BezierStyleConfig> = {}): void => {
|
||||
const points = bezier.get_points().map((p: string) => JSON.parse(p));
|
||||
const points = JSON.parse(bezier.get_points());
|
||||
drawBezier(ctx, points, null, bezierStyleConfig);
|
||||
};
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export type SliderOption = {
|
||||
step: number;
|
||||
default: number;
|
||||
variable: string;
|
||||
unit?: string;
|
||||
unit?: string | string[];
|
||||
};
|
||||
|
||||
export type TemplateOption = {
|
||||
@@ -46,3 +46,10 @@ export type BezierStyleConfig = {
|
||||
radius: number;
|
||||
drawHandles: boolean;
|
||||
};
|
||||
|
||||
export type CircleSector = {
|
||||
center: Point;
|
||||
radius: number;
|
||||
startAngle: number;
|
||||
endAngle: number;
|
||||
};
|
||||
|
||||
@@ -1,25 +1,42 @@
|
||||
pub mod subpath;
|
||||
mod svg_drawing;
|
||||
|
||||
use bezier_rs::{Bezier, ProjectionOptions};
|
||||
use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, ProjectionOptions};
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct CircleSector {
|
||||
center: Point,
|
||||
radius: f64,
|
||||
#[serde(rename = "startAngle")]
|
||||
start_angle: f64,
|
||||
#[serde(rename = "endAngle")]
|
||||
end_angle: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Point {
|
||||
x: f64,
|
||||
y: f64,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub enum WasmMaximizeArcs {
|
||||
Automatic, // 0
|
||||
On, // 1
|
||||
Off, // 2
|
||||
}
|
||||
|
||||
/// Wrapper of the `Bezier` struct to be used in JS.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone)]
|
||||
pub struct WasmBezier(Bezier);
|
||||
|
||||
/// Convert a `DVec2` into a `JsValue`.
|
||||
fn vec_to_point(p: &DVec2) -> JsValue {
|
||||
JsValue::from_serde(&serde_json::to_string(&Point { x: p.x, y: p.y }).unwrap()).unwrap()
|
||||
/// Convert a `DVec2` into a `Point`.
|
||||
fn vec_to_point(p: &DVec2) -> Point {
|
||||
Point { x: p.x, y: p.y }
|
||||
}
|
||||
|
||||
/// Convert a bezier to a list of points.
|
||||
@@ -32,6 +49,14 @@ fn to_js_value<T: Serialize>(data: T) -> JsValue {
|
||||
JsValue::from_serde(&serde_json::to_string(&data).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn convert_wasm_maximize_arcs(wasm_enum_value: WasmMaximizeArcs) -> ArcStrategy {
|
||||
match wasm_enum_value {
|
||||
WasmMaximizeArcs::Automatic => ArcStrategy::Automatic,
|
||||
WasmMaximizeArcs::On => ArcStrategy::FavorLargerArcs,
|
||||
WasmMaximizeArcs::Off => ArcStrategy::FavorCorrectness,
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmBezier {
|
||||
/// Expect js_points to be a list of 2 pairs.
|
||||
@@ -78,8 +103,10 @@ impl WasmBezier {
|
||||
self.0.set_handle_end(DVec2::new(x, y));
|
||||
}
|
||||
|
||||
pub fn get_points(&self) -> Vec<JsValue> {
|
||||
self.0.get_points().map(|point| vec_to_point(&point)).collect()
|
||||
/// The wrapped return type is `Vec<Point>`.
|
||||
pub fn get_points(&self) -> JsValue {
|
||||
let points: Vec<Point> = self.0.get_points().map(|point| vec_to_point(&point)).collect();
|
||||
to_js_value(points)
|
||||
}
|
||||
|
||||
pub fn to_svg(&self) -> String {
|
||||
@@ -90,26 +117,39 @@ impl WasmBezier {
|
||||
self.0.length(None)
|
||||
}
|
||||
|
||||
/// The wrapped return type is `Point`.
|
||||
pub fn evaluate(&self, t: f64) -> JsValue {
|
||||
vec_to_point(&self.0.evaluate(t))
|
||||
let point: Point = vec_to_point(&self.0.evaluate(t));
|
||||
to_js_value(point)
|
||||
}
|
||||
|
||||
pub fn compute_lookup_table(&self, steps: i32) -> Vec<JsValue> {
|
||||
self.0.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect()
|
||||
/// The wrapped return type is `Vec<Point>`.
|
||||
pub fn compute_lookup_table(&self, steps: usize) -> JsValue {
|
||||
let table_values: Vec<Point> = self.0.compute_lookup_table(Some(steps)).iter().map(vec_to_point).collect();
|
||||
to_js_value(table_values)
|
||||
}
|
||||
|
||||
pub fn derivative(&self) -> Option<WasmBezier> {
|
||||
self.0.derivative().map(WasmBezier)
|
||||
}
|
||||
|
||||
/// The wrapped return type is `Point`.
|
||||
pub fn tangent(&self, t: f64) -> JsValue {
|
||||
vec_to_point(&self.0.tangent(t))
|
||||
let tangent_point: Point = vec_to_point(&self.0.tangent(t));
|
||||
to_js_value(tangent_point)
|
||||
}
|
||||
|
||||
/// The wrapped return type is `Point`.
|
||||
pub fn normal(&self, t: f64) -> JsValue {
|
||||
vec_to_point(&self.0.normal(t))
|
||||
let normal_point: Point = vec_to_point(&self.0.normal(t));
|
||||
to_js_value(normal_point)
|
||||
}
|
||||
|
||||
pub fn curvature(&self, t: f64) -> f64 {
|
||||
self.0.curvature(t)
|
||||
}
|
||||
|
||||
/// The wrapped return type is `[Vec<Point>; 2]`.
|
||||
pub fn split(&self, t: f64) -> JsValue {
|
||||
let bezier_points: [Vec<Point>; 2] = self.0.split(t).map(bezier_to_points);
|
||||
to_js_value(bezier_points)
|
||||
@@ -123,29 +163,33 @@ impl WasmBezier {
|
||||
self.0.project(DVec2::new(x, y), ProjectionOptions::default())
|
||||
}
|
||||
|
||||
/// The wrapped return type is `[Vec<f64>; 2]`.
|
||||
pub fn local_extrema(&self) -> JsValue {
|
||||
let local_extrema = self.0.local_extrema();
|
||||
let local_extrema: [Vec<f64>; 2] = self.0.local_extrema();
|
||||
to_js_value(local_extrema)
|
||||
}
|
||||
|
||||
/// The wrapped return type is `[Point; 2]`.
|
||||
pub fn bounding_box(&self) -> JsValue {
|
||||
let bbox_points: [Point; 2] = self.0.bounding_box().map(|p| Point { x: p.x, y: p.y });
|
||||
to_js_value(bbox_points)
|
||||
}
|
||||
|
||||
/// The wrapped return type is `Vec<f64>`.
|
||||
pub fn inflections(&self) -> JsValue {
|
||||
let inflections = self.0.inflections();
|
||||
let inflections: Vec<f64> = self.0.inflections();
|
||||
to_js_value(inflections)
|
||||
}
|
||||
|
||||
/// The wrapped return type is `Vec<Vec<Point>>`.
|
||||
pub fn de_casteljau_points(&self, t: f64) -> JsValue {
|
||||
let hull = self
|
||||
let points: Vec<Vec<Point>> = self
|
||||
.0
|
||||
.de_casteljau_points(t)
|
||||
.iter()
|
||||
.map(|level| level.iter().map(|&point| Point { x: point.x, y: point.y }).collect::<Vec<Point>>())
|
||||
.collect::<Vec<Vec<Point>>>();
|
||||
to_js_value(hull)
|
||||
.collect();
|
||||
to_js_value(points)
|
||||
}
|
||||
|
||||
pub fn rotate(&self, angle: f64) -> WasmBezier {
|
||||
@@ -185,12 +229,30 @@ impl WasmBezier {
|
||||
to_js_value(bezier_points)
|
||||
}
|
||||
|
||||
/// The wrapped return type is `Vec<Vec<Point>>`.
|
||||
pub fn offset(&self, distance: f64) -> JsValue {
|
||||
let bezier_points: Vec<Vec<Point>> = self.0.offset(distance).into_iter().map(bezier_to_points).collect();
|
||||
to_js_value(bezier_points)
|
||||
}
|
||||
|
||||
pub fn curvature(&self, t: f64) -> f64 {
|
||||
self.0.curvature(t)
|
||||
/// The wrapped return type is `Vec<CircleSector>`.
|
||||
pub fn arcs(&self, error: f64, max_iterations: usize, maximize_arcs: WasmMaximizeArcs) -> JsValue {
|
||||
let strategy = convert_wasm_maximize_arcs(maximize_arcs);
|
||||
let options = ArcsOptions { error, max_iterations, strategy };
|
||||
let circle_sectors: Vec<CircleSector> = self
|
||||
.0
|
||||
.arcs(options)
|
||||
.iter()
|
||||
.map(|sector| CircleSector {
|
||||
center: Point {
|
||||
x: sector.center.x,
|
||||
y: sector.center.y,
|
||||
},
|
||||
radius: sector.radius,
|
||||
start_angle: sector.start_angle,
|
||||
end_angle: sector.end_angle,
|
||||
})
|
||||
.collect();
|
||||
to_js_value(circle_sectors)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user