mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-27 20:58:12 +08:00
Added Subpaths to bezier-rs (#730)
* Added Subpath constructor, iterator and length * Asserted that subpaths of len < 2 cannot be closed. Added len() and comments * Added subpath to_svg(), made structs public, made quadratic use either in_handle or out_handle * add bezier handles * Added basic interactivity and index traits * Added SubPath interactivity * Added svg styling * Broke subpath impl across multiple files. More sylistic changes per review * Fixed format error * Added closed subpath to documentation page * Modified subpath to_svg to use functional style * Stylistic changes per review * Fixed build errors * More sylistic changes per review * Moved svg commands to constants * Moved formatting for svg arguments to ToSVGOptions * Renamed files in git * Even more stylistic changes per review
This commit is contained in:
@@ -2,7 +2,8 @@
|
|||||||
<div class="App">
|
<div class="App">
|
||||||
<h1>Bezier-rs Interactive Documentation</h1>
|
<h1>Bezier-rs Interactive Documentation</h1>
|
||||||
<p>This is the interactive documentation for the <b>bezier-rs</b> library. Click and drag on the endpoints of the example curves to visualize the various Bezier utilities and functions.</p>
|
<p>This is the interactive documentation for the <b>bezier-rs</b> library. Click and drag on the endpoints of the example curves to visualize the various Bezier utilities and functions.</p>
|
||||||
<div v-for="(feature, index) in features" :key="index">
|
<h2>Beziers</h2>
|
||||||
|
<div v-for="(feature, index) in bezierFeatures" :key="index">
|
||||||
<ExamplePane
|
<ExamplePane
|
||||||
:template="feature.template"
|
:template="feature.template"
|
||||||
:templateOptions="feature.templateOptions"
|
:templateOptions="feature.templateOptions"
|
||||||
@@ -14,6 +15,10 @@
|
|||||||
:customOptions="feature.customOptions"
|
:customOptions="feature.customOptions"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<h2>Subpaths</h2>
|
||||||
|
<div v-for="(feature, index) in subpathFeatures" :key="index">
|
||||||
|
<SubpathExamplePane :name="feature.name" :callback="feature.callback" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -21,10 +26,11 @@
|
|||||||
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 { BezierCurveType, Point, WasmBezierInstance } from "@/utils/types";
|
import { BezierCurveType, 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";
|
||||||
|
import SubpathExamplePane from "@/components/SubpathExamplePane.vue";
|
||||||
|
|
||||||
const tSliderOptions = {
|
const tSliderOptions = {
|
||||||
min: 0,
|
min: 0,
|
||||||
@@ -37,13 +43,9 @@ const tSliderOptions = {
|
|||||||
const SCALE_UNIT_VECTOR_FACTOR = 50;
|
const SCALE_UNIT_VECTOR_FACTOR = 50;
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "App",
|
|
||||||
components: {
|
|
||||||
ExamplePane,
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
features: [
|
bezierFeatures: [
|
||||||
{
|
{
|
||||||
name: "Constructor",
|
name: "Constructor",
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@@ -355,8 +357,22 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
subpathFeatures: [
|
||||||
|
{
|
||||||
|
name: "Constructor",
|
||||||
|
callback: (subpath: WasmSubpathInstance): string => subpath.to_svg(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Length",
|
||||||
|
callback: (subpath: WasmSubpathInstance): string => subpath.length(),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
ExamplePane,
|
||||||
|
SubpathExamplePane,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,6 @@ import BezierDrawing from "@/components/BezierDrawing";
|
|||||||
import { BezierCallback, WasmBezierInstance } from "@/utils/types";
|
import { BezierCallback, WasmBezierInstance } from "@/utils/types";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "ExampleComponent",
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
bezierDrawing: new BezierDrawing(this.bezier, this.callback, this.options, this.createThroughPoints),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
title: String,
|
title: String,
|
||||||
bezier: {
|
bezier: {
|
||||||
@@ -37,6 +31,11 @@ export default defineComponent({
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
bezierDrawing: new BezierDrawing(this.bezier, this.callback, this.options, this.createThroughPoints),
|
||||||
|
};
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const drawing = this.$refs.drawing as HTMLElement;
|
const drawing = this.$refs.drawing as HTMLElement;
|
||||||
drawing.appendChild(this.bezierDrawing.getCanvas());
|
drawing.appendChild(this.bezierDrawing.getCanvas());
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="example-pane-header">{{ name }}</h2>
|
<h3 class="example-pane-header">{{ name }}</h3>
|
||||||
<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" />
|
||||||
@@ -58,10 +58,6 @@ const CurveTypeMapping = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "ExamplePane",
|
|
||||||
components: {
|
|
||||||
Example,
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
name: {
|
name: {
|
||||||
type: String as PropType<string>,
|
type: String as PropType<string>,
|
||||||
@@ -118,12 +114,15 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
Example,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.example-row {
|
.example-row {
|
||||||
display: flex; /* or inline-flex */
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ import { BezierCallback, TemplateOption, WasmBezierInstance } from "@/utils/type
|
|||||||
import Example from "@/components/Example.vue";
|
import Example from "@/components/Example.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "SliderExample",
|
|
||||||
components: {
|
|
||||||
Example,
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
title: String,
|
title: String,
|
||||||
bezier: {
|
bezier: {
|
||||||
@@ -46,6 +42,9 @@ export default defineComponent({
|
|||||||
sliderUnits: Object.assign({}, ...sliders.map((s) => ({ [s.variable]: s.unit }))),
|
sliderUnits: Object.assign({}, ...sliders.map((s) => ({ [s.variable]: s.unit }))),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
Example,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h4 class="example-header">{{ title }}</h4>
|
||||||
|
<figure @mousedown="onMouseDown" @mouseup="onMouseUp" @mousemove="onMouseMove" class="example-figure" v-html="subpathSVG"></figure>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from "vue";
|
||||||
|
|
||||||
|
import { WasmSubpath } from "@/../wasm/pkg";
|
||||||
|
import { SubpathCallback, WasmSubpathInstance, WasmSubpathManipulatorKey } from "@/utils/types";
|
||||||
|
|
||||||
|
const SELECTABLE_RANGE = 10;
|
||||||
|
const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"];
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
title: String,
|
||||||
|
triples: {
|
||||||
|
type: Array as PropType<Array<Array<number[] | undefined>>>,
|
||||||
|
required: true,
|
||||||
|
mutable: true,
|
||||||
|
},
|
||||||
|
closed: {
|
||||||
|
type: Boolean as PropType<boolean>,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
callback: {
|
||||||
|
type: Function as PropType<SubpathCallback>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const subpath = WasmSubpath.from_triples(this.triples, this.closed) as WasmSubpathInstance;
|
||||||
|
return {
|
||||||
|
subpath,
|
||||||
|
subpathSVG: this.callback(subpath),
|
||||||
|
activeIndex: undefined as number[] | undefined,
|
||||||
|
mutableTriples: JSON.parse(JSON.stringify(this.triples)),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onMouseDown(event: MouseEvent) {
|
||||||
|
const mx = event.offsetX;
|
||||||
|
const my = event.offsetY;
|
||||||
|
for (let controllerIndex = 0; controllerIndex < this.mutableTriples.length; controllerIndex += 1) {
|
||||||
|
for (let pointIndex = 0; pointIndex < 3; pointIndex += 1) {
|
||||||
|
const point = this.mutableTriples[controllerIndex][pointIndex];
|
||||||
|
if (point && Math.abs(mx - point[0]) < SELECTABLE_RANGE && Math.abs(my - point[1]) < SELECTABLE_RANGE) {
|
||||||
|
this.activeIndex = [controllerIndex, pointIndex];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMouseUp() {
|
||||||
|
this.activeIndex = undefined;
|
||||||
|
},
|
||||||
|
onMouseMove(event: MouseEvent) {
|
||||||
|
const mx = event.offsetX;
|
||||||
|
const my = event.offsetY;
|
||||||
|
if (this.activeIndex) {
|
||||||
|
this.subpath[POINT_INDEX_TO_MANIPULATOR[this.activeIndex[1]]](this.activeIndex[0], mx, my);
|
||||||
|
this.mutableTriples[this.activeIndex[0]][this.activeIndex[1]] = [mx, my];
|
||||||
|
this.subpathSVG = this.callback(this.subpath);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.example-figure {
|
||||||
|
border: solid 1px black;
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3 class="example-pane-header">{{ name }}</h3>
|
||||||
|
<div class="example-row">
|
||||||
|
<div v-for="(example, index) in examples" :key="index">
|
||||||
|
<SubpathExample :title="example.title" :triples="example.triples" :closed="example.closed" :callback="callback" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from "vue";
|
||||||
|
|
||||||
|
import { SubpathCallback } from "@/utils/types";
|
||||||
|
|
||||||
|
import SubpathExample from "@/components/SubpathExample.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
callback: {
|
||||||
|
type: Function as PropType<SubpathCallback>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
title: "Open Subpath",
|
||||||
|
triples: [
|
||||||
|
[[20, 20], undefined, [10, 90]],
|
||||||
|
[[150, 40], [60, 40], undefined],
|
||||||
|
[[175, 175], undefined, undefined],
|
||||||
|
[[100, 100], [40, 120], undefined],
|
||||||
|
],
|
||||||
|
closed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Closed Subpath",
|
||||||
|
triples: [
|
||||||
|
[[35, 125], undefined, [40, 40]],
|
||||||
|
[[130, 30], [120, 120], undefined],
|
||||||
|
[
|
||||||
|
[145, 150],
|
||||||
|
[175, 90],
|
||||||
|
[70, 185],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
closed: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
SubpathExample,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.example-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-pane-header {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,7 +7,7 @@ export const COLORS = {
|
|||||||
CANVAS: "white",
|
CANVAS: "white",
|
||||||
INTERACTIVE: {
|
INTERACTIVE: {
|
||||||
STROKE_1: "black",
|
STROKE_1: "black",
|
||||||
STROKE_2: "grey",
|
STROKE_2: "gray",
|
||||||
SELECTED: "blue",
|
SELECTED: "blue",
|
||||||
},
|
},
|
||||||
NON_INTERACTIVE: {
|
NON_INTERACTIVE: {
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ export type WasmBezierKey = keyof WasmBezierInstance;
|
|||||||
export type WasmBezierConstructorKey = "new_linear" | "new_quadratic" | "new_cubic";
|
export type WasmBezierConstructorKey = "new_linear" | "new_quadratic" | "new_cubic";
|
||||||
export type WasmBezierManipulatorKey = "set_start" | "set_handle_start" | "set_handle_end" | "set_end";
|
export type WasmBezierManipulatorKey = "set_start" | "set_handle_start" | "set_handle_end" | "set_end";
|
||||||
|
|
||||||
|
export type WasmSubpathInstance = InstanceType<WasmRawInstance["WasmSubpath"]>;
|
||||||
|
export type WasmSubpathManipulatorKey = "set_anchor" | "set_in_handle" | "set_out_handle";
|
||||||
|
|
||||||
export enum BezierCurveType {
|
export enum BezierCurveType {
|
||||||
Linear = "Linear",
|
Linear = "Linear",
|
||||||
Quadratic = "Quadratic",
|
Quadratic = "Quadratic",
|
||||||
@@ -12,6 +15,7 @@ export enum BezierCurveType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
export type SubpathCallback = (subpath: WasmSubpathInstance) => string;
|
||||||
|
|
||||||
export type SliderOption = {
|
export type SliderOption = {
|
||||||
min: number;
|
min: number;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
pub mod subpath;
|
||||||
|
mod svg_drawing;
|
||||||
|
|
||||||
use bezier_rs::{Bezier, ProjectionOptions};
|
use bezier_rs::{Bezier, ProjectionOptions};
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -31,7 +34,7 @@ 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.
|
/// Expect js_points to be a list of 2 pairs.
|
||||||
pub fn new_linear(js_points: &JsValue) -> WasmBezier {
|
pub fn new_linear(js_points: &JsValue) -> WasmBezier {
|
||||||
let points: [DVec2; 2] = js_points.into_serde().unwrap();
|
let points: [DVec2; 2] = js_points.into_serde().unwrap();
|
||||||
WasmBezier(Bezier::from_linear_dvec2(points[0], points[1]))
|
WasmBezier(Bezier::from_linear_dvec2(points[0], points[1]))
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use bezier_rs::subpath::{ManipulatorGroup, Subpath, ToSVGOptions};
|
||||||
|
use glam::DVec2;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use crate::svg_drawing::*;
|
||||||
|
|
||||||
|
/// Wrapper of the `Subpath` struct to be used in JS.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct WasmSubpath(Subpath);
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl WasmSubpath {
|
||||||
|
/// Expects js_points to be an unbounded list of triples, where each item is a tuple of floats.
|
||||||
|
pub fn from_triples(js_points: &JsValue, closed: bool) -> WasmSubpath {
|
||||||
|
let point_triples: Vec<[Option<DVec2>; 3]> = js_points.into_serde().unwrap();
|
||||||
|
let manipulator_groups = point_triples
|
||||||
|
.into_iter()
|
||||||
|
.map(|point_triple| ManipulatorGroup {
|
||||||
|
anchor: point_triple[0].unwrap(),
|
||||||
|
in_handle: point_triple[1],
|
||||||
|
out_handle: point_triple[2],
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
WasmSubpath(Subpath::new(manipulator_groups, closed))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_anchor(&mut self, index: usize, x: f64, y: f64) {
|
||||||
|
self.0[index].anchor = DVec2::new(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_in_handle(&mut self, index: usize, x: f64, y: f64) {
|
||||||
|
self.0[index].in_handle = Some(DVec2::new(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_out_handle(&mut self, index: usize, x: f64, y: f64) {
|
||||||
|
self.0[index].out_handle = Some(DVec2::new(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_svg(&self) -> String {
|
||||||
|
format!("{}{}{}", SVG_OPEN_TAG, self.0.to_svg(ToSVGOptions::default()), SVG_CLOSE_TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn length(&self) -> String {
|
||||||
|
let length_text = draw_text(format!("Length: {:.2}", self.0.length(None)), 5., 193., BLACK);
|
||||||
|
format!("{}{}{}{}", SVG_OPEN_TAG, self.0.to_svg(ToSVGOptions::default()), length_text, SVG_CLOSE_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// SVG drawing constants
|
||||||
|
pub const SVG_OPEN_TAG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="200px" height="200px">"#;
|
||||||
|
pub const SVG_CLOSE_TAG: &str = "</svg>";
|
||||||
|
|
||||||
|
// Sylistic constants
|
||||||
|
pub const BLACK: &str = "black";
|
||||||
|
|
||||||
|
/// Helper function to create an SVG text entitty.
|
||||||
|
pub fn draw_text(text: String, x_pos: f64, y_pos: f64, fill: &str) -> String {
|
||||||
|
format!(r#"<text x="{x_pos}" y="{y_pos}" fill="{fill}">{text}</text>"#)
|
||||||
|
}
|
||||||
@@ -19,3 +19,10 @@ pub const DEFAULT_LUT_STEP_SIZE: i32 = 10;
|
|||||||
pub const DEFAULT_LENGTH_SUBDIVISIONS: i32 = 1000;
|
pub const DEFAULT_LENGTH_SUBDIVISIONS: i32 = 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;
|
||||||
|
|
||||||
|
// SVG constants
|
||||||
|
pub const SVG_ARG_CUBIC: &str = "C";
|
||||||
|
pub const SVG_ARG_LINEAR: &str = "L";
|
||||||
|
pub const SVG_ARG_MOVE: &str = "M";
|
||||||
|
pub const SVG_ARG_QUADRATIC: &str = "Q";
|
||||||
|
pub const SVG_ARG_CLOSED: &str = "Z";
|
||||||
|
|||||||
+39
-11
@@ -1,9 +1,11 @@
|
|||||||
//! Bezier-rs: A Bezier Math Library for Rust
|
//! Bezier-rs: A Bezier Math Library for Rust
|
||||||
|
|
||||||
mod consts;
|
mod consts;
|
||||||
|
pub mod subpath;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use consts::*;
|
use consts::*;
|
||||||
|
pub use subpath::*;
|
||||||
|
|
||||||
use glam::{DMat2, DVec2};
|
use glam::{DMat2, DVec2};
|
||||||
|
|
||||||
@@ -164,23 +166,49 @@ impl Bezier {
|
|||||||
Bezier::from_cubic_dvec2(start, handle_start, handle_end, end)
|
Bezier::from_cubic_dvec2(start, handle_start, handle_end, end)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert to SVG.
|
/// Return the string argument used to create a curve in an SVG `path`, excluding the start point.
|
||||||
pub fn to_svg(&self) -> String {
|
pub(crate) fn svg_curve_argument(&self) -> String {
|
||||||
// TODO: Allow modifying the viewport, width and height
|
let handle_args = match self.handles {
|
||||||
let m_path = format!("M {} {}", self.start.x, self.start.y);
|
BezierHandles::Linear => SVG_ARG_LINEAR.to_string(),
|
||||||
let handles_path = match self.handles {
|
|
||||||
BezierHandles::Linear => "L".to_string(),
|
|
||||||
BezierHandles::Quadratic { handle } => {
|
BezierHandles::Quadratic { handle } => {
|
||||||
format!("Q {} {},", handle.x, handle.y)
|
format!("{SVG_ARG_QUADRATIC}{} {}", 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!("{SVG_ARG_CUBIC}{} {} {} {}", handle_start.x, handle_start.y, handle_end.x, handle_end.y)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let curve_path = format!("{} {} {}", handles_path, self.end.x, self.end.y);
|
format!("{handle_args} {} {}", self.end.x, self.end.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the string argument used to create the lines connecting handles to endpoints in an SVG `path`
|
||||||
|
pub(crate) fn svg_handle_line_argument(&self) -> Option<String> {
|
||||||
|
match self.handles {
|
||||||
|
BezierHandles::Linear => None,
|
||||||
|
BezierHandles::Quadratic { handle } => {
|
||||||
|
let handle_line = format!("{SVG_ARG_LINEAR}{} {}", handle.x, handle.y);
|
||||||
|
Some(format!(
|
||||||
|
"{SVG_ARG_MOVE}{} {} {handle_line} {SVG_ARG_MOVE}{} {} {handle_line}",
|
||||||
|
self.start.x, self.start.y, self.end.x, self.end.y
|
||||||
|
))
|
||||||
|
}
|
||||||
|
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||||
|
let handle_start_line = format!("{SVG_ARG_LINEAR}{} {}", handle_start.x, handle_start.y);
|
||||||
|
let handle_end_line = format!("{SVG_ARG_LINEAR}{} {}", handle_end.x, handle_end.y);
|
||||||
|
Some(format!(
|
||||||
|
"{SVG_ARG_MOVE}{} {} {handle_start_line} {SVG_ARG_MOVE}{} {} {handle_end_line}",
|
||||||
|
self.start.x, self.start.y, self.end.x, self.end.y
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert `Bezier` to SVG `path`.
|
||||||
|
pub fn to_svg(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}" width="{}px" height="{}px"><path d="{} {} {}" stroke="black" fill="transparent"/></svg>"#,
|
r#"<path d="{SVG_ARG_MOVE}{} {} {}" stroke="black" fill="none"/>"#,
|
||||||
0, 0, 100, 100, 100, 100, "\n", m_path, curve_path
|
self.start.x,
|
||||||
|
self.start.y,
|
||||||
|
self.svg_curve_argument()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::consts::*;
|
||||||
|
|
||||||
|
/// Functionality relating to core `Subpath` operations, such as constructors and `iter`.
|
||||||
|
impl Subpath {
|
||||||
|
/// Create a new `Subpath` using a list of [ManipulatorGroup]s.
|
||||||
|
/// A `Subpath` with less than 2 [ManipulatorGroup]s may not be closed.
|
||||||
|
pub fn new(manipulator_groups: Vec<ManipulatorGroup>, closed: bool) -> Subpath {
|
||||||
|
assert!(!closed || manipulator_groups.len() > 1, "A closed Subpath must contain more than 1 ManipulatorGroup.");
|
||||||
|
Subpath { manipulator_groups, closed }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a `Subpath` consisting of 2 manipulator groups from a `Bezier`.
|
||||||
|
pub fn from_bezier(bezier: Bezier) -> Self {
|
||||||
|
Subpath::new(
|
||||||
|
vec![
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: bezier.start(),
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: bezier.handle_start(),
|
||||||
|
},
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: bezier.end(),
|
||||||
|
in_handle: bezier.handle_end(),
|
||||||
|
out_handle: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the `Subpath` contains no [ManipulatorGroup].
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.manipulator_groups.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of [ManipulatorGroup]s contained within the `Subpath`.
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.manipulator_groups.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator of the [Bezier]s along the `Subpath`.
|
||||||
|
pub fn iter(&self) -> SubpathIter {
|
||||||
|
SubpathIter { sub_path: self, index: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an SVG representation of the `Subpath`.
|
||||||
|
pub fn to_svg(&self, options: ToSVGOptions) -> String {
|
||||||
|
if self.is_empty() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let curve_start_argument = format!("{SVG_ARG_MOVE}{} {}", self[0].anchor.x, self[0].anchor.y);
|
||||||
|
let mut curve_arguments: Vec<String> = self.iter().map(|bezier| bezier.svg_curve_argument()).collect();
|
||||||
|
if self.closed {
|
||||||
|
curve_arguments.push(String::from(SVG_ARG_CLOSED));
|
||||||
|
}
|
||||||
|
|
||||||
|
let anchor_arguments = options.formatted_anchor_arguments();
|
||||||
|
let anchor_circles = self
|
||||||
|
.manipulator_groups
|
||||||
|
.iter()
|
||||||
|
.map(|point| format!(r#"<circle cx="{}" cy="{}" {}/>"#, point.anchor.x, point.anchor.y, anchor_arguments))
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
let handle_point_arguments = options.formatted_handle_point_arguments();
|
||||||
|
let handle_circles: Vec<String> = self
|
||||||
|
.manipulator_groups
|
||||||
|
.iter()
|
||||||
|
.flat_map(|group| [group.in_handle, group.out_handle])
|
||||||
|
.flatten()
|
||||||
|
.map(|handle| format!(r#"<circle cx="{}" cy="{}" {}/>"#, handle.x, handle.y, handle_point_arguments))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let handle_pieces: Vec<String> = self.iter().filter_map(|bezier| bezier.svg_handle_line_argument()).collect();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"<path d="{} {}" {}/><path d="{}" {}/>{}{}"#,
|
||||||
|
curve_start_argument,
|
||||||
|
curve_arguments.join(" "),
|
||||||
|
options.formatted_curve_arguments(),
|
||||||
|
handle_pieces.join(" "),
|
||||||
|
options.formatted_handle_line_arguments(),
|
||||||
|
handle_circles.join(""),
|
||||||
|
anchor_circles.join(""),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`.
|
||||||
|
impl Subpath {
|
||||||
|
/// Return the sum of the approximation of the length of each `Bezier` curve along the `Subpath`.
|
||||||
|
/// - `num_subdivisions` - Number of subdivisions used to approximate the curve. The default value is `1000`.
|
||||||
|
pub fn length(&self, num_subdivisions: Option<i32>) -> f64 {
|
||||||
|
self.iter().fold(0., |accumulator, bezier| accumulator + bezier.length(num_subdivisions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Bezier;
|
||||||
|
use glam::DVec2;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn length_quadratic() {
|
||||||
|
let start = DVec2::new(20., 30.);
|
||||||
|
let middle = DVec2::new(80., 90.);
|
||||||
|
let end = DVec2::new(60., 45.);
|
||||||
|
let handle1 = DVec2::new(75., 85.);
|
||||||
|
let handle2 = DVec2::new(40., 30.);
|
||||||
|
let handle3 = DVec2::new(10., 10.);
|
||||||
|
|
||||||
|
let bezier1 = Bezier::from_quadratic_dvec2(start, handle1, middle);
|
||||||
|
let bezier2 = Bezier::from_quadratic_dvec2(middle, handle2, end);
|
||||||
|
let bezier3 = Bezier::from_quadratic_dvec2(end, handle3, start);
|
||||||
|
|
||||||
|
let mut subpath = Subpath::new(
|
||||||
|
vec![
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: start,
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: Some(handle1),
|
||||||
|
},
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: middle,
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: Some(handle2),
|
||||||
|
},
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: end,
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: Some(handle3),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert_eq!(subpath.length(None), bezier1.length(None) + bezier2.length(None));
|
||||||
|
|
||||||
|
subpath.closed = true;
|
||||||
|
assert_eq!(subpath.length(None), bezier1.length(None) + bezier2.length(None) + bezier3.length(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn length_mixed() {
|
||||||
|
let start = DVec2::new(20., 30.);
|
||||||
|
let middle = DVec2::new(70., 70.);
|
||||||
|
let end = DVec2::new(60., 45.);
|
||||||
|
let handle1 = DVec2::new(75., 85.);
|
||||||
|
let handle2 = DVec2::new(40., 30.);
|
||||||
|
let handle3 = DVec2::new(10., 10.);
|
||||||
|
|
||||||
|
let linear_bezier = Bezier::from_linear_dvec2(start, middle);
|
||||||
|
let quadratic_bezier = Bezier::from_quadratic_dvec2(middle, handle1, end);
|
||||||
|
let cubic_bezier = Bezier::from_cubic_dvec2(end, handle2, handle3, start);
|
||||||
|
|
||||||
|
let mut subpath = Subpath::new(
|
||||||
|
vec![
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: start,
|
||||||
|
in_handle: Some(handle3),
|
||||||
|
out_handle: None,
|
||||||
|
},
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: middle,
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: Some(handle1),
|
||||||
|
},
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: end,
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: Some(handle2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None));
|
||||||
|
|
||||||
|
subpath.closed = true;
|
||||||
|
assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None) + cubic_bezier.length(None));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
mod core;
|
||||||
|
mod lookup;
|
||||||
|
mod structs;
|
||||||
|
pub use structs::*;
|
||||||
|
|
||||||
|
use crate::Bezier;
|
||||||
|
|
||||||
|
use std::ops::{Index, IndexMut};
|
||||||
|
|
||||||
|
/// Structure used to represent a path composed of [Bezier] curves.
|
||||||
|
pub struct Subpath {
|
||||||
|
manipulator_groups: Vec<ManipulatorGroup>,
|
||||||
|
closed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iteration structure for iterating across each curve of a `Subpath`, using an intermediate `Bezier` representation.
|
||||||
|
pub struct SubpathIter<'a> {
|
||||||
|
index: usize,
|
||||||
|
sub_path: &'a Subpath,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<usize> for Subpath {
|
||||||
|
type Output = ManipulatorGroup;
|
||||||
|
|
||||||
|
fn index(&self, index: usize) -> &Self::Output {
|
||||||
|
assert!(index < self.len(), "Index out of bounds in trait Index of SubPath.");
|
||||||
|
&self.manipulator_groups[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IndexMut<usize> for Subpath {
|
||||||
|
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
|
||||||
|
assert!(index < self.len(), "Index out of bounds in trait IndexMut of SubPath.");
|
||||||
|
&mut self.manipulator_groups[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for SubpathIter<'_> {
|
||||||
|
type Item = Bezier;
|
||||||
|
|
||||||
|
// Returns the Bezier representation of each `Subpath` segment, defined between a pair of adjacent manipulator points.
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let len = self.sub_path.len() - 1
|
||||||
|
+ match self.sub_path.closed {
|
||||||
|
true => 1,
|
||||||
|
false => 0,
|
||||||
|
};
|
||||||
|
if self.index >= len {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let start_index = self.index;
|
||||||
|
let end_index = (self.index + 1) % self.sub_path.len();
|
||||||
|
self.index += 1;
|
||||||
|
|
||||||
|
let start = self.sub_path[start_index].anchor;
|
||||||
|
let end = self.sub_path[end_index].anchor;
|
||||||
|
let out_handle = self.sub_path[start_index].out_handle;
|
||||||
|
let in_handle = self.sub_path[end_index].in_handle;
|
||||||
|
|
||||||
|
if let (Some(handle1), Some(handle2)) = (out_handle, in_handle) {
|
||||||
|
Some(Bezier::from_cubic_dvec2(start, handle1, handle2, end))
|
||||||
|
} else if let Some(handle) = out_handle.or(in_handle) {
|
||||||
|
Some(Bezier::from_quadratic_dvec2(start, handle, end))
|
||||||
|
} else {
|
||||||
|
Some(Bezier::from_linear_dvec2(start, end))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
use glam::DVec2;
|
||||||
|
|
||||||
|
/// Structure used to represent a single anchor with up to two optional associated handles along a `Subpath`
|
||||||
|
pub struct ManipulatorGroup {
|
||||||
|
pub anchor: DVec2,
|
||||||
|
pub in_handle: Option<DVec2>,
|
||||||
|
pub out_handle: Option<DVec2>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structure to represent optional parameters that can be passed to the `into_svg` function.
|
||||||
|
pub struct ToSVGOptions {
|
||||||
|
/// Color of the line segments along the `Subpath`. Defaulted to `black`.
|
||||||
|
pub curve_stroke_color: String,
|
||||||
|
/// Width of the line segments along the `Subpath`. Defaulted to `2.`.
|
||||||
|
pub curve_stroke_width: f64,
|
||||||
|
/// Stroke color outlining circles marking anchors on the `Subpath`. Defaulted to `black`.
|
||||||
|
pub anchor_stroke_color: String,
|
||||||
|
/// Stroke width outlining circles marking anchors on the `Subpath`. Defaulted to `2.`.
|
||||||
|
pub anchor_stroke_width: f64,
|
||||||
|
/// Radius of the circles marking anchors on the `Subpath`. Defaulted to `4.`.
|
||||||
|
pub anchor_radius: f64,
|
||||||
|
/// Fill color of the circles marking anchors on the `Subpath`. Defaulted to `white`.
|
||||||
|
pub anchor_fill: String,
|
||||||
|
/// Color of the line segments connecting anchors to handle points. Defaulted to `gray`.
|
||||||
|
pub handle_line_stroke_color: String,
|
||||||
|
/// Width of the line segments connecting anchors to handle points. Defaulted to `1.`.
|
||||||
|
pub handle_line_stroke_width: f64,
|
||||||
|
/// Stroke color outlining circles marking the handles of `Subpath`. Defaulted to `gray`.
|
||||||
|
pub handle_point_stroke_color: String,
|
||||||
|
/// Stroke color outlining circles marking the handles of `Subpath`. Defaulted to `1.5`.
|
||||||
|
pub handle_point_stroke_width: f64,
|
||||||
|
/// Radius of the circles marking the handles of `Subpath`. Defaulted to `3.`.
|
||||||
|
pub handle_point_radius: f64,
|
||||||
|
/// Fill color of the circles marking the handles of `Subpath`. Defaulted to `white`.
|
||||||
|
pub handle_point_fill: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToSVGOptions {
|
||||||
|
/// Combine and format curve styling options for an SVG path.
|
||||||
|
pub(crate) fn formatted_curve_arguments(&self) -> String {
|
||||||
|
format!(r#"stroke="{}" stroke-width="{}" fill="none""#, self.curve_stroke_color, self.curve_stroke_width)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combine and format anchor styling options an SVG circle.
|
||||||
|
pub(crate) fn formatted_anchor_arguments(&self) -> String {
|
||||||
|
format!(
|
||||||
|
r#"r="{}", stroke="{}" stroke-width="{}" fill="{}""#,
|
||||||
|
self.anchor_radius, self.anchor_stroke_color, self.anchor_stroke_width, self.anchor_fill
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combine and format handle point styling options for an SVG circle.
|
||||||
|
pub(crate) fn formatted_handle_point_arguments(&self) -> String {
|
||||||
|
format!(
|
||||||
|
r#"r="{}", stroke="{}" stroke-width="{}" fill="{}""#,
|
||||||
|
self.handle_point_radius, self.handle_point_stroke_color, self.handle_point_stroke_width, self.handle_point_fill
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combine and format handle line styling options an SVG path.
|
||||||
|
pub(crate) fn formatted_handle_line_arguments(&self) -> String {
|
||||||
|
format!(r#"stroke="{}" stroke-width="{}" fill="none""#, self.handle_line_stroke_color, self.handle_line_stroke_width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ToSVGOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
ToSVGOptions {
|
||||||
|
curve_stroke_color: String::from("black"),
|
||||||
|
curve_stroke_width: 2.,
|
||||||
|
anchor_stroke_color: String::from("black"),
|
||||||
|
anchor_stroke_width: 2.,
|
||||||
|
anchor_radius: 4.,
|
||||||
|
anchor_fill: String::from("white"),
|
||||||
|
handle_line_stroke_color: String::from("gray"),
|
||||||
|
handle_line_stroke_width: 1.,
|
||||||
|
handle_point_stroke_color: String::from("gray"),
|
||||||
|
handle_point_stroke_width: 1.5,
|
||||||
|
handle_point_radius: 3.,
|
||||||
|
handle_point_fill: String::from("white"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user