mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-28 03:08: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">
|
||||
<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>
|
||||
<div v-for="(feature, index) in features" :key="index">
|
||||
<h2>Beziers</h2>
|
||||
<div v-for="(feature, index) in bezierFeatures" :key="index">
|
||||
<ExamplePane
|
||||
:template="feature.template"
|
||||
:templateOptions="feature.templateOptions"
|
||||
@@ -14,6 +15,10 @@
|
||||
:customOptions="feature.customOptions"
|
||||
/>
|
||||
</div>
|
||||
<h2>Subpaths</h2>
|
||||
<div v-for="(feature, index) in subpathFeatures" :key="index">
|
||||
<SubpathExamplePane :name="feature.name" :callback="feature.callback" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -21,10 +26,11 @@
|
||||
import { defineComponent, markRaw } from "vue";
|
||||
|
||||
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 SliderExample from "@/components/SliderExample.vue";
|
||||
import SubpathExamplePane from "@/components/SubpathExamplePane.vue";
|
||||
|
||||
const tSliderOptions = {
|
||||
min: 0,
|
||||
@@ -37,13 +43,9 @@ const tSliderOptions = {
|
||||
const SCALE_UNIT_VECTOR_FACTOR = 50;
|
||||
|
||||
export default defineComponent({
|
||||
name: "App",
|
||||
components: {
|
||||
ExamplePane,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
features: [
|
||||
bezierFeatures: [
|
||||
{
|
||||
name: "Constructor",
|
||||
// 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>
|
||||
|
||||
|
||||
@@ -12,12 +12,6 @@ import BezierDrawing from "@/components/BezierDrawing";
|
||||
import { BezierCallback, WasmBezierInstance } from "@/utils/types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ExampleComponent",
|
||||
data() {
|
||||
return {
|
||||
bezierDrawing: new BezierDrawing(this.bezier, this.callback, this.options, this.createThroughPoints),
|
||||
};
|
||||
},
|
||||
props: {
|
||||
title: String,
|
||||
bezier: {
|
||||
@@ -37,6 +31,11 @@ export default defineComponent({
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
bezierDrawing: new BezierDrawing(this.bezier, this.callback, this.options, this.createThroughPoints),
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const drawing = this.$refs.drawing as HTMLElement;
|
||||
drawing.appendChild(this.bezierDrawing.getCanvas());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="example-pane-header">{{ name }}</h2>
|
||||
<h3 class="example-pane-header">{{ name }}</h3>
|
||||
<div class="example-row">
|
||||
<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" />
|
||||
@@ -58,10 +58,6 @@ const CurveTypeMapping = {
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "ExamplePane",
|
||||
components: {
|
||||
Example,
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String as PropType<string>,
|
||||
@@ -118,12 +114,15 @@ export default defineComponent({
|
||||
});
|
||||
});
|
||||
},
|
||||
components: {
|
||||
Example,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.example-row {
|
||||
display: flex; /* or inline-flex */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -16,10 +16,6 @@ import { BezierCallback, TemplateOption, WasmBezierInstance } from "@/utils/type
|
||||
import Example from "@/components/Example.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SliderExample",
|
||||
components: {
|
||||
Example,
|
||||
},
|
||||
props: {
|
||||
title: String,
|
||||
bezier: {
|
||||
@@ -46,6 +42,9 @@ export default defineComponent({
|
||||
sliderUnits: Object.assign({}, ...sliders.map((s) => ({ [s.variable]: s.unit }))),
|
||||
};
|
||||
},
|
||||
components: {
|
||||
Example,
|
||||
},
|
||||
});
|
||||
</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",
|
||||
INTERACTIVE: {
|
||||
STROKE_1: "black",
|
||||
STROKE_2: "grey",
|
||||
STROKE_2: "gray",
|
||||
SELECTED: "blue",
|
||||
},
|
||||
NON_INTERACTIVE: {
|
||||
|
||||
@@ -5,6 +5,9 @@ export type WasmBezierKey = keyof WasmBezierInstance;
|
||||
export type WasmBezierConstructorKey = "new_linear" | "new_quadratic" | "new_cubic";
|
||||
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 {
|
||||
Linear = "Linear",
|
||||
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 SubpathCallback = (subpath: WasmSubpathInstance) => string;
|
||||
|
||||
export type SliderOption = {
|
||||
min: number;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
pub mod subpath;
|
||||
mod svg_drawing;
|
||||
|
||||
use bezier_rs::{Bezier, ProjectionOptions};
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -31,7 +34,7 @@ fn to_js_value<T: Serialize>(data: T) -> JsValue {
|
||||
|
||||
#[wasm_bindgen]
|
||||
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 {
|
||||
let points: [DVec2; 2] = js_points.into_serde().unwrap();
|
||||
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>"#)
|
||||
}
|
||||
Reference in New Issue
Block a user