Remove the deprecated/archived Bezier-rs library from the repo (#3058)

Remove the Bezier-rs library from the repo
This commit is contained in:
Keavon Chambers
2025-08-16 17:29:00 -07:00
committed by GitHub
parent d22b2ca927
commit 3bcec37493
60 changed files with 24 additions and 17053 deletions
-1
View File
@@ -3,4 +3,3 @@ public/
static/fonts/
static/syntax-highlighting.css
static/text-balancer.js
other/editor-structure/replacement.html
@@ -95,7 +95,7 @@ const outputFile = process.argv[3];
if (!inputFile || !outputFile) {
console.error("Error: Please provide the input text and output HTML file paths as arguments.");
console.log("Usage: node generate.js <input> <output>");
console.log("Usage: node generate-editor-structure.js <input txt> <output html>");
process.exit(1);
}
@@ -8,15 +8,15 @@ const basePath = path.resolve(__dirname);
// Define files to copy as [source, destination] pairs
// Files with the same destination will be concatenated
const FILES_TO_COPY = [
["node_modules/@fontsource-variable/inter/opsz.css", "static/fonts/common.css"],
["node_modules/@fontsource-variable/inter/opsz-italic.css", "static/fonts/common.css"],
["node_modules/@fontsource/bona-nova/700.css", "static/fonts/common.css"],
["../node_modules/@fontsource-variable/inter/opsz.css", "../static/fonts/common.css"],
["../node_modules/@fontsource-variable/inter/opsz-italic.css", "../static/fonts/common.css"],
["../node_modules/@fontsource/bona-nova/700.css", "../static/fonts/common.css"],
];
// Define directories to copy recursively as [source, destination] pairs
const DIRECTORIES_TO_COPY = [
["node_modules/@fontsource-variable/inter/files", "static/fonts/files"],
["node_modules/@fontsource/bona-nova/files", "static/fonts/files"],
["../node_modules/@fontsource-variable/inter/files", "../static/fonts/files"],
["../node_modules/@fontsource/bona-nova/files", "../static/fonts/files"],
];
// Track processed destination files and CSS content
@@ -159,7 +159,7 @@ console.log("\nFont installation complete!");
// Fetch and save text-balancer.js, which we don't commit to the repo so we're not version controlling dependency code
const textBalancerUrl = "https://static.graphite.rs/text-balancer/text-balancer.js";
const textBalancerDest = path.join(basePath, "static", "text-balancer.js");
const textBalancerDest = path.join(basePath, "../static", "text-balancer.js");
console.log("\nDownloading text-balancer.js...");
https
.get(textBalancerUrl, (res) => {
@@ -335,7 +335,7 @@ Duration: 9 months
Students: Hannah Li, Rob Nadal, Thomas Cheng, Linda Zheng, Jackie Chen
- [Bezier-rs library](https://crates.io/crates/bezier-rs)
- [Interactive web demo](/libraries/bezier-rs/)
- [Interactive web demo](https://keavon.github.io/Bezier-rs/)
**Outcomes:** The student group designed an API for representing and manipulating Bezier curves and paths as a standalone Rust library which was published to crates.io. It now serves as the underlying vector data format used in Graphite, and acts as a testbed for new computational geometry algorithms. The team also built an interactive web demo catalog to showcase many of the algorithms, which are also handily embedded in the library's [documentation](https://docs.rs/bezier-rs/latest/bezier_rs/).
@@ -1,87 +0,0 @@
module.exports = {
root: true,
env: { browser: true, node: true },
extends: ["eslint:recommended", "plugin:import/recommended", "plugin:@typescript-eslint/recommended", "plugin:import/typescript", "prettier"],
plugins: ["import", "@typescript-eslint", "prettier"],
settings: {
"import/parsers": { "@typescript-eslint/parser": [".ts"] },
"import/resolver": { typescript: true, node: true },
},
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
project: "./tsconfig.json",
},
ignorePatterns: [
// Ignore generated directories
"node_modules/",
"dist/",
"pkg/",
"wasm/pkg/",
// Don't ignore JS and TS dotfiles in this folder
"!.*.js",
"!.*.ts",
],
overrides: [
{
extends: ["plugin:@typescript-eslint/disable-type-checked"],
files: ["./*.js", "./*.cjs"],
},
],
rules: {
// Standard ESLint config (for ordinary JS syntax linting)
indent: "off",
quotes: ["error", "double", { allowTemplateLiterals: true }],
camelcase: ["error", { properties: "always" }],
"linebreak-style": ["error", "unix"],
"eol-last": ["error", "always"],
"max-len": ["error", { code: 200, tabWidth: 4, ignorePattern: `d="([\\s\\S]*?)"` }],
"prefer-destructuring": "off",
"no-console": "warn",
"no-debugger": "warn",
"no-param-reassign": ["error", { props: false }],
"no-bitwise": "off",
"no-shadow": "off",
"no-use-before-define": "off",
"no-restricted-imports": ["error", { patterns: [".*", "!@/*"] }],
// TypeScript plugin config (for TS-specific linting)
"@typescript-eslint/indent": "off",
"@typescript-eslint/camelcase": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as", objectLiteralTypeAssertions: "never" }],
"@typescript-eslint/consistent-indexed-object-style": ["error", "record"],
"@typescript-eslint/consistent-generic-constructors": ["error", "constructor"],
"@typescript-eslint/no-restricted-types": ["error", { types: { null: "Use `undefined` instead." } }],
// Prettier plugin config (for validating and fixing formatting)
"prettier/prettier": "error",
// Import plugin config (for intelligently validating module import statements)
"import/no-unresolved": "error",
"import/prefer-default-export": "off",
"import/no-relative-packages": "error",
"import/order": [
"error",
{
alphabetize: { order: "asc", caseInsensitive: true },
"newlines-between": "always-and-inside-groups",
warnOnUnassignedImports: true,
},
],
},
};
-5
View File
@@ -1,5 +0,0 @@
/node_modules
!/public
/public/build
/dist
/wasm/pkg
@@ -1,6 +0,0 @@
{
"singleQuote": false,
"useTabs": true,
"tabWidth": 4,
"printWidth": 200
}
-18
View File
@@ -1,18 +0,0 @@
# Bezier-rs interactive documentation
Open these interactive docs: <https://graphite.rs/libraries/bezier-rs/>
This page also serves isolated demos for iframes used in the Rustdoc [crate documentation](https://docs.rs/bezier-rs/latest/bezier_rs/).
## Building and running
Make sure [Node.js](https://nodejs.org/) (the latest LTS version) and [Rust](https://www.rust-lang.org/) (the latest stable release) are installed on your system, and [wasm-pack](https://rustwasm.github.io/wasm-pack/) has been installed by running `cargo install wasm-pack`.
- To run the development server with hot reloading:
```
npm start
```
- To compile an optimized production build:
```
npm run build
```
-13
View File
@@ -1,13 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Bezier-rs Interactive Documentation</title>
<link rel="stylesheet" href="./style.css" />
<link href="fonts.css" rel="stylesheet" />
</head>
<body>
<noscript>JavaScript is required</noscript>
<script type="module" src="src/main.ts"></script>
</body>
</html>
@@ -1,41 +0,0 @@
// This script automatically installs the npm packages listed in package-lock.json and runs before `npm start`.
// It skips the installation if this has already run and neither package.json nor package-lock.json has been modified since.
import { execSync } from "child_process";
import { existsSync, statSync, writeFileSync } from "fs";
const INSTALL_TIMESTAMP_FILE = "node_modules/.install-timestamp";
// Checks if the install is needed by comparing modification times
const isInstallNeeded = () => {
if (!existsSync(INSTALL_TIMESTAMP_FILE)) return true;
const timestamp = statSync(INSTALL_TIMESTAMP_FILE).mtime;
return ["package.json", "package-lock.json"].some((file) => {
return existsSync(file) && statSync(file).mtime > timestamp;
});
};
// Run `npm ci` if needed and update the install timestamp
if (isInstallNeeded()) {
try {
// eslint-disable-next-line no-console
console.log("Installing npm packages...");
// Check if packages are up to date, doing so quickly by using `npm ci`, preferring local cached packages, and skipping the package audit and other checks
execSync("npm ci --prefer-offline --no-audit --no-fund", { stdio: "inherit" });
// Touch the install timestamp file
writeFileSync(INSTALL_TIMESTAMP_FILE, "");
// eslint-disable-next-line no-console
console.log("Finished installing npm packages.");
} catch (_) {
// eslint-disable-next-line no-console
console.error("Failed to install npm packages. Please run `npm install` from the `/frontend` directory.");
process.exit(1);
}
} else {
// eslint-disable-next-line no-console
console.log("All npm packages are up-to-date.");
}
File diff suppressed because it is too large Load Diff
@@ -1,41 +0,0 @@
{
"name": "bezier-rs-demos",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"---------- DEV SERVER ----------": "",
"start": "npm run setup && npm run wasm:build-dev && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-dev\"",
"profiling": "npm run setup && npm run wasm:build-profiling && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-profiling\"",
"production": "npm run setup && npm run wasm:build-production && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-production\"",
"---------- BUILDS ----------": "",
"build-dev": "npm run wasm:build-dev && vite build",
"build-profiling": "npm run wasm:build-profiling && vite build",
"build": "npm run wasm:build-production && vite build",
"---------- UTILITIES ----------": "",
"lint": "ESLINT_USE_FLAT_CONFIG=false eslint . && tsc --noEmit",
"lint-fix": "ESLINT_USE_FLAT_CONFIG=false eslint . --fix && tsc --noEmit",
"---------- INTERNAL ----------": "",
"setup": "node package-installer.js",
"wasm:build-dev": "wasm-pack build ./wasm --dev --target=web",
"wasm:build-profiling": "wasm-pack build ./wasm --profiling --target=web",
"wasm:build-production": "wasm-pack build ./wasm --release --target=web",
"wasm:watch-dev": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --dev --target=web -- --color=always\"",
"wasm:watch-profiling": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --profiling --target=web -- --color=always\"",
"wasm:watch-production": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --release --target=web -- --color=always\""
},
"devDependencies": {
"@types/node": "^22.6.1",
"@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0",
"concurrently": "^9.0.1",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-prettier": "^5.2.1",
"prettier": "^3.3.3",
"ts-node": "^10.9.2",
"typescript": "^5.6.2",
"vite": "^5.4.7"
}
}
@@ -1,206 +0,0 @@
:root {
--color-navy: #16323f;
--color-gray: #cccccc;
--range-fill-dark: var(--color-navy);
--range-fill-light: var(--color-gray);
--range-thumb-height: 16px;
}
html,
body {
font-family: "Inter Variable", sans-serif;
text-align: center;
background-color: white;
}
.website-header {
color: var(--color-navy);
font-family: "Bona Nova", serif;
}
.website-description {
font-weight: 500;
color: var(--color-navy);
}
.category-header {
color: var(--color-navy);
font-family: "Bona Nova", serif;
margin-bottom: 0
}
body > h1 {
margin: 40px 0;
}
body > h1 ~ :last-child {
margin-bottom: 40px;
}
body > h1 + p {
max-width: 768px;
line-height: 1.4;
margin: auto;
text-align: justify;
}
body > h2 {
margin-top: 40px;
}
/* Demo group styles */
.demo-row {
display: flex;
flex-direction: row;
justify-content: center;
}
.t-variant-choice {
margin-top: 20px;
}
.demo-group-header {
display: inline-block;
position: relative;
margin-top: 2em;
margin-bottom: 0;
padding: 0 1em;
font-family: "Bona Nova", serif;
color: var(--color-navy);
}
.demo-group-header a {
display: none;
position: absolute;
left: 0;
text-decoration: none;
color: inherit;
opacity: 0.5;
}
.demo-group-header:hover a {
display: inline-block;
}
.demo-group-container {
position: relative;
width: fit-content;
margin: auto;
}
/* Demo styles */
.demo-header {
font-family: "Bona Nova", serif;
color: var(--color-navy);
margin-top: 10px;
margin-bottom: 5px;
}
.demo-figure {
width: 250px;
height: 200px;
margin: 10px;
border: solid 1px black;
}
.parent-input-container {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
svg text {
pointer-events: none;
-webkit-user-select: none;
user-select: none;
}
/* Slider Styles */
.slider-container {
/* width: fit-content; */
width: 250px;
padding-bottom: 5px;
}
.input-label {
font-family: monospace;
display: flex;
justify-content: left;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
margin-right: 15px;
width: 250px;
height: 7px;
background: rgba(255, 255, 255, 0.6);
border-radius: 5px;
background: linear-gradient(var(--range-fill-dark), var(--range-fill-dark)) 0 / calc(0.5 * var(--range-thumb-height) + var(--range-ratio) * (100% - var(--range-thumb-height))) var(--range-fill-light);
background-repeat: no-repeat;
}
/* Input Thumb */
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
height: var(--range-thumb-height);
width: var(--range-thumb-height);
border-radius: 50%;
background: var(--range-fill-dark);
box-shadow: 0 0 2px 0 #555;
transition: background .3s ease-in-out;
}
input[type="range"]::-moz-range-thumb {
-webkit-appearance: none;
appearance: none;
height: var(--range-thumb-height);
width: var(--range-thumb-height);
border-radius: 50%;
background: var(--range-fill-dark);
box-shadow: 0 0 2px 0 #555;
transition: background .3s ease-in-out;
}
input[type="range"]::-webkit-slider-thumb:hover {
background: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), var(--range-fill-dark);
}
input[type="range"]::-moz-range-thumb:hover {
background: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), var(--range-fill-dark);
}
/* Input Track */
input[type=range]::-webkit-slider-runnable-track {
-webkit-appearance: none;
box-shadow: none;
border: none;
background: transparent;
background: none;
}
input[type=range]::-moz-range-track {
-webkit-appearance: none;
appearance: none;
box-shadow: none;
border: none;
background: transparent;
background: none;
;
}
/* Select Styles */
select {
font-family: monospace;
}
.select-container {
width: 250px;
padding-bottom: 5px;
display: flex;
}
.select-input {
margin-left: 8px;
}
@@ -1,589 +0,0 @@
import { WasmBezier } from "@/../wasm/pkg";
import type { BezierDemoOptions, WasmBezierInstance, BezierCallback, InputOption } from "@/types";
import { capOptions, tSliderOptions, bezierTValueVariantOptions, errorOptions, minimumSeparationOptions, BEZIER_T_VALUE_VARIANTS } from "@/types";
const bezierFeatures = {
constructor: {
name: "Constructor",
callback: (bezier: WasmBezierInstance, _: Record<string, number>): string => bezier.to_svg(),
},
"bezier-through-points": {
name: "Bezier Through Points",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => {
const points = bezier.get_points();
if (Object.values(options).length === 1) {
return WasmBezier.quadratic_through_points(points, options.t);
}
return WasmBezier.cubic_through_points(points, options.t, options["midpoint separation"]);
},
demoOptions: {
Linear: {
disabled: true,
},
Quadratic: {
customPoints: [
[30, 50],
[120, 70],
[160, 170],
],
inputOptions: [
{
variable: "t",
inputType: "slider",
min: 0.01,
max: 0.99,
step: 0.01,
default: 0.5,
},
],
},
Cubic: {
customPoints: [
[30, 50],
[120, 70],
[160, 170],
],
inputOptions: [
{
variable: "t",
inputType: "slider",
min: 0.01,
max: 0.99,
step: 0.01,
default: 0.5,
},
{
variable: "midpoint separation",
inputType: "slider",
min: 0,
max: 100,
step: 2,
default: 30,
},
],
},
},
},
length: {
name: "Length",
callback: (bezier: WasmBezierInstance, _: Record<string, number>): string => bezier.length(),
},
"length-centroid": {
name: "Length Centroid",
callback: (bezier: WasmBezierInstance, _: Record<string, number>): string => bezier.length_centroid(),
},
evaluate: {
name: "Evaluate",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.evaluate(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
demoOptions: {
Quadratic: {
inputOptions: [bezierTValueVariantOptions, tSliderOptions],
},
},
},
"lookup-table": {
name: "Lookup Table",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.compute_lookup_table(options.steps, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
demoOptions: {
Quadratic: {
inputOptions: [
bezierTValueVariantOptions,
{
variable: "steps",
inputType: "slider",
min: 2,
max: 15,
step: 1,
default: 5,
},
],
},
},
},
derivative: {
name: "Derivative",
callback: (bezier: WasmBezierInstance, _: Record<string, number>): string => bezier.derivative(),
demoOptions: {
Linear: {
disabled: true,
},
Quadratic: {
customPoints: [
[30, 40],
[110, 50],
[120, 130],
],
},
Cubic: {
customPoints: [
[50, 50],
[60, 100],
[100, 140],
[140, 150],
],
},
},
},
tangent: {
name: "Tangent",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.tangent(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
demoOptions: {
Quadratic: {
inputOptions: [bezierTValueVariantOptions, tSliderOptions],
},
},
},
"tangents-to-point": {
name: "Tangents To Point",
callback: (bezier: WasmBezierInstance, _: Record<string, number>, mouseLocation?: [number, number]): string =>
mouseLocation ? bezier.tangents_to_point(mouseLocation[0], mouseLocation[1]) : bezier.to_svg(),
triggerOnMouseMove: true,
demoOptions: { Linear: { disabled: true } },
},
normal: {
name: "Normal",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.normal(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
demoOptions: {
Quadratic: {
inputOptions: [bezierTValueVariantOptions, tSliderOptions],
},
},
},
"normals-to-point": {
name: "Normals To Point",
callback: (bezier: WasmBezierInstance, _: Record<string, number>, mouseLocation?: [number, number]): string =>
mouseLocation ? bezier.normals_to_point(mouseLocation[0], mouseLocation[1]) : bezier.to_svg(),
triggerOnMouseMove: true,
},
curvature: {
name: "Curvature",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.curvature(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
demoOptions: {
Linear: {
disabled: true,
},
Quadratic: {
inputOptions: [bezierTValueVariantOptions, tSliderOptions],
},
Cubic: {
inputOptions: [bezierTValueVariantOptions, { ...tSliderOptions, default: 0.7 }],
},
},
},
split: {
name: "Split",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.split(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
demoOptions: {
Quadratic: {
inputOptions: [bezierTValueVariantOptions, tSliderOptions],
},
},
},
trim: {
name: "Trim",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.trim(options.t1, options.t2, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
demoOptions: {
Quadratic: {
inputOptions: [
bezierTValueVariantOptions,
{
variable: "t1",
inputType: "slider",
min: 0,
max: 1,
step: 0.01,
default: 0.25,
},
{
variable: "t2",
inputType: "slider",
min: 0,
max: 1,
step: 0.01,
default: 0.75,
},
],
},
},
},
project: {
name: "Project",
callback: (bezier: WasmBezierInstance, _: Record<string, number>, mouseLocation?: [number, number]): string =>
mouseLocation ? bezier.project(mouseLocation[0], mouseLocation[1]) : bezier.to_svg(),
triggerOnMouseMove: true,
},
"local-extrema": {
name: "Local Extrema",
callback: (bezier: WasmBezierInstance, _: Record<string, number>): string => bezier.local_extrema(),
demoOptions: {
Linear: {
disabled: true,
},
Quadratic: {
customPoints: [
[40, 40],
[160, 30],
[110, 150],
],
},
Cubic: {
customPoints: [
[160, 180],
[170, 10],
[30, 90],
[180, 160],
],
},
},
},
"bounding-box": {
name: "Bounding Box",
callback: (bezier: WasmBezierInstance, _: Record<string, number>): string => bezier.bounding_box(),
},
inflections: {
name: "Inflections",
callback: (bezier: WasmBezierInstance, _: Record<string, number>): string => bezier.inflections(),
demoOptions: {
Linear: {
disabled: true,
},
Quadratic: {
disabled: true,
},
},
},
reduce: {
name: "Reduce",
callback: (bezier: WasmBezierInstance): string => bezier.reduce(),
},
offset: {
name: "Offset",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.offset(options.distance),
demoOptions: {
Quadratic: {
inputOptions: [
{
variable: "distance",
inputType: "slider",
min: -30,
max: 30,
step: 1,
default: 15,
},
],
},
},
},
outline: {
name: "Outline",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.outline(options.distance, options.cap),
demoOptions: {
Quadratic: {
inputOptions: [
{
variable: "distance",
inputType: "slider",
min: 0,
max: 30,
step: 1,
default: 15,
},
capOptions,
],
},
},
},
"graduated-outline": {
name: "Graduated Outline",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.graduated_outline(options.start_distance, options.end_distance, options.cap),
demoOptions: {
Quadratic: {
inputOptions: [
{
variable: "start_distance",
inputType: "slider",
min: 0,
max: 30,
step: 1,
default: 5,
},
{
variable: "end_distance",
inputType: "slider",
min: 0,
max: 30,
step: 1,
default: 15,
},
capOptions,
],
},
},
customPoints: {
Cubic: [
[31, 94],
[40, 40],
[107, 107],
[106, 106],
],
},
},
"skewed-outline": {
name: "Skewed Outline",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string =>
bezier.skewed_outline(options.distance1, options.distance2, options.distance3, options.distance4, options.cap),
demoOptions: {
Quadratic: {
inputOptions: [
{
variable: "distance1",
inputType: "slider",
min: 0,
max: 30,
step: 1,
default: 20,
},
{
variable: "distance2",
inputType: "slider",
min: 0,
max: 30,
step: 1,
default: 10,
},
{
variable: "distance3",
inputType: "slider",
min: 0,
max: 30,
step: 1,
default: 30,
},
{
variable: "distance4",
inputType: "slider",
min: 0,
max: 30,
step: 1,
default: 5,
},
capOptions,
],
},
},
},
arcs: {
name: "Arcs",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.arcs(options.error, options.max_iterations, options.strategy),
demoOptions: ((): BezierDemoOptions => {
const inputOptions: InputOption[] = [
{
variable: "strategy",
inputType: "dropdown",
default: 0,
options: ["Automatic", "FavorLargerArcs", "FavorCorrectness"],
},
{
variable: "error",
inputType: "slider",
min: 0.05,
max: 1,
step: 0.05,
default: 0.5,
},
{
variable: "max_iterations",
inputType: "slider",
min: 50,
max: 200,
step: 1,
default: 100,
},
];
return {
Linear: {
disabled: true,
},
Quadratic: {
customPoints: [
[70, 40],
[180, 50],
[160, 150],
],
inputOptions,
disabled: false,
},
Cubic: {
customPoints: [
[160, 180],
[170, 10],
[30, 90],
[180, 160],
],
inputOptions,
disabled: false,
},
};
})(),
},
"intersect-linear": {
name: "Intersect (Linear Segment)",
callback: (bezier: WasmBezierInstance): string => {
const line = [
[45, 30],
[195, 160],
];
return bezier.intersect_line_segment(line);
},
},
"intersect-quadratic": {
name: "Intersect (Quadratic Segment)",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => {
const quadratic = [
[45, 80],
[205, 10],
[115, 120],
];
return bezier.intersect_quadratic_segment(quadratic, options.error, options.minimum_separation);
},
demoOptions: {
Quadratic: {
inputOptions: [errorOptions, minimumSeparationOptions],
},
},
},
"intersect-cubic": {
name: "Intersect (Cubic Segment)",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => {
const cubic = [
[65, 20],
[125, 40],
[65, 120],
[200, 140],
];
return bezier.intersect_cubic_segment(cubic, options.error, options.minimum_separation);
},
demoOptions: {
Quadratic: {
inputOptions: [errorOptions, minimumSeparationOptions],
},
},
},
"intersect-self": {
name: "Intersect (Self)",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.intersect_self(options.error, options.minimum_separation),
demoOptions: {
Linear: {
disabled: true,
},
Quadratic: {
disabled: true,
},
Cubic: {
inputOptions: [errorOptions, minimumSeparationOptions],
customPoints: [
[160, 180],
[170, 10],
[30, 90],
[180, 140],
],
},
},
},
"intersect-rectangle": {
name: "Intersect (Rectangle)",
callback: (bezier: WasmBezierInstance): string =>
bezier.intersect_rectangle([
[75, 50],
[175, 150],
]),
},
rotate: {
name: "Rotate",
callback: (bezier: WasmBezierInstance, options: Record<string, number>): string => bezier.rotate(options.angle * Math.PI, 125, 100),
demoOptions: {
Quadratic: {
inputOptions: [
{
variable: "angle",
inputType: "slider",
min: 0,
max: 2,
step: 1 / 50,
default: 0.12,
unit: "π",
},
],
},
},
},
"de-casteljau-points": {
name: "De Casteljau Points",
callback: (bezier: WasmBezierInstance, options: Record<string, number>, _: undefined): string => bezier.de_casteljau_points(options.t, BEZIER_T_VALUE_VARIANTS[options.TVariant]),
demoOptions: {
Quadratic: {
inputOptions: [bezierTValueVariantOptions, tSliderOptions],
},
},
},
join: {
name: "Join",
callback: (bezier: WasmBezierInstance): string => {
const points = bezier.get_points();
let examplePoints = [];
if (points.length === 2) {
examplePoints = [
[145, 155],
[65, 155],
];
} else if (points.length === 3) {
examplePoints = [
[65, 150],
[120, 195],
[190, 145],
];
} else {
examplePoints = [
[165, 150],
[110, 110],
[90, 180],
[55, 140],
];
}
return bezier.join(examplePoints);
},
demoOptions: {
Linear: {
customPoints: [
[70, 40],
[155, 90],
],
},
Quadratic: {
customPoints: [
[185, 40],
[65, 20],
[100, 85],
],
},
Cubic: {
customPoints: [
[45, 80],
[65, 20],
[115, 100],
[155, 55],
],
},
},
},
};
export type BezierFeatureKey = keyof typeof bezierFeatures;
export type BezierFeatureOptions = {
name: string;
callback: BezierCallback;
demoOptions?: Partial<BezierDemoOptions>;
triggerOnMouseMove?: boolean;
};
export default bezierFeatures as Record<BezierFeatureKey, BezierFeatureOptions>;
@@ -1,252 +0,0 @@
import type { SubpathCallback, SubpathInputOption, WasmSubpathInstance } from "@/types";
import { capOptions, joinOptions, tSliderOptions, subpathTValueVariantOptions, intersectionErrorOptions, minimumSeparationOptions, separationDiskDiameter, SUBPATH_T_VALUE_VARIANTS } from "@/types";
const subpathFeatures = {
constructor: {
name: "Constructor",
callback: (subpath: WasmSubpathInstance): string => subpath.to_svg(),
},
insert: {
name: "Insert",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.insert(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
inputOptions: [subpathTValueVariantOptions, tSliderOptions],
},
length: {
name: "Length",
callback: (subpath: WasmSubpathInstance): string => subpath.length(),
},
"length-centroid": {
name: "Length Centroid",
callback: (subpath: WasmSubpathInstance): string => subpath.length_centroid(),
},
area: {
name: "Area",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.area(options.error, options.minimum_separation),
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
},
"area-centroid": {
name: "Area Centroid",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.area_centroid(options.error, options.minimum_separation),
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
},
"poisson-disk-points": {
name: "Poisson-Disk Points",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.poisson_disk_points(options.separation_disk_diameter),
inputOptions: [separationDiskDiameter],
},
evaluate: {
name: "Evaluate",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.evaluate(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
inputOptions: [subpathTValueVariantOptions, tSliderOptions],
},
"lookup-table": {
name: "Lookup Table",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.compute_lookup_table(options.steps, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
inputOptions: [
subpathTValueVariantOptions,
{
variable: "steps",
inputType: "slider",
min: 2,
max: 30,
step: 1,
default: 5,
},
],
},
project: {
name: "Project",
callback: (subpath: WasmSubpathInstance, _: Record<string, number>, mouseLocation?: [number, number]): string =>
mouseLocation ? subpath.project(mouseLocation[0], mouseLocation[1]) : subpath.to_svg(),
triggerOnMouseMove: true,
},
tangent: {
name: "Tangent",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.tangent(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
inputOptions: [subpathTValueVariantOptions, tSliderOptions],
},
normal: {
name: "Normal",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.normal(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
inputOptions: [subpathTValueVariantOptions, tSliderOptions],
},
"local-extrema": {
name: "Local Extrema",
callback: (subpath: WasmSubpathInstance): string => subpath.local_extrema(),
},
"bounding-box": {
name: "Bounding Box",
callback: (subpath: WasmSubpathInstance): string => subpath.bounding_box(),
},
inflections: {
name: "Inflections",
callback: (subpath: WasmSubpathInstance): string => subpath.inflections(),
},
"intersect-linear": {
name: "Intersect (Linear Segment)",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string =>
subpath.intersect_line_segment(
[
[80, 30],
[210, 150],
],
options.error,
options.minimum_separation,
),
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
},
"intersect-quadratic": {
name: "Intersect (Quadratic Segment)",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string =>
subpath.intersect_quadratic_segment(
[
[25, 50],
[205, 10],
[135, 180],
],
options.error,
options.minimum_separation,
),
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
},
"intersect-cubic": {
name: "Intersect (Cubic Segment)",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string =>
subpath.intersect_cubic_segment(
[
[65, 20],
[125, 40],
[65, 120],
[200, 140],
],
options.error,
options.minimum_separation,
),
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
},
"intersect-self": {
name: "Intersect (Self)",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.self_intersections(options.error, options.minimum_separation),
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
},
"intersect-rectangle": {
name: "Intersect (Rectangle)",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string =>
subpath.intersect_rectangle(
[
[75, 50],
[175, 150],
],
options.error,
options.minimum_separation,
),
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
},
"inside-other": {
name: "Inside (Other Subpath)",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string =>
subpath.inside_subpath(
[
[40, 40],
[160, 40],
[160, 80],
[200, 100],
[160, 120],
[160, 160],
[40, 160],
[40, 120],
[80, 100],
[40, 80],
],
options.error,
options.minimum_separation,
),
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
},
curvature: {
name: "Curvature",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.curvature(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
inputOptions: [subpathTValueVariantOptions, { ...tSliderOptions, default: 0.2 }],
},
split: {
name: "Split",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.split(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
inputOptions: [subpathTValueVariantOptions, tSliderOptions],
},
trim: {
name: "Trim",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.trim(options.t1, options.t2, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
inputOptions: [subpathTValueVariantOptions, { ...tSliderOptions, default: 0.2, variable: "t1" }, { ...tSliderOptions, variable: "t2" }],
},
offset: {
name: "Offset",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.offset(options.distance, options.join, options.miter_limit),
inputOptions: [
{
variable: "distance",
inputType: "slider",
min: -25,
max: 25,
step: 1,
default: 10,
},
joinOptions,
{
variable: "join: Miter - limit",
inputType: "slider",
min: 1,
max: 10,
step: 0.25,
default: 4,
},
],
},
outline: {
name: "Outline",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.outline(options.distance, options.join, options.cap, options.miter_limit),
inputOptions: [
{
variable: "distance",
inputType: "slider",
min: 0,
max: 25,
step: 1,
default: 10,
},
joinOptions,
{
variable: "join: Miter - limit",
inputType: "slider",
min: 1,
max: 10,
step: 0.25,
default: 4,
},
{ ...capOptions, isDisabledForClosed: true },
],
},
rotate: {
name: "Rotate",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.rotate(options.angle * Math.PI, 125, 100),
inputOptions: [
{
variable: "angle",
inputType: "slider",
min: 0,
max: 2,
step: 1 / 50,
default: 0.12,
unit: "π",
},
],
},
};
export type SubpathFeatureKey = keyof typeof subpathFeatures;
export type SubpathFeatureOptions = {
name: string;
callback: SubpathCallback;
inputOptions?: SubpathInputOption[];
triggerOnMouseMove?: boolean;
};
export default subpathFeatures as Record<SubpathFeatureKey, SubpathFeatureOptions>;
-345
View File
@@ -1,345 +0,0 @@
import { default as init, WasmSubpath, WasmBezier } from "@/../wasm/pkg";
import bezierFeatures from "@/features-bezier";
import type { BezierFeatureKey, BezierFeatureOptions } from "@/features-bezier";
import subpathFeatures from "@/features-subpath";
import type { SubpathFeatureKey, SubpathFeatureOptions } from "@/features-subpath";
import type { DemoArgs, BezierCurveType, BezierDemoArgs, SubpathDemoArgs, DemoData, WasmSubpathInstance, WasmSubpathManipulatorKey, InputOption, DemoDataBezier, DemoDataSubpath } from "@/types";
import { BEZIER_CURVE_TYPE, getBezierDemoPointDefaults, getSubpathDemoArgs, POINT_INDEX_TO_MANIPULATOR, getConstructorKey, getCurveType, MANIPULATOR_KEYS_FROM_BEZIER_TYPE } from "@/types";
init().then(renderPage);
function renderPage() {
// Determine whether the page needs to recompute which examples to show
window.addEventListener("hashchange", (e: HashChangeEvent) => {
const isUrlSolo = (url: string) => {
const splitHash = url.split("#")?.[1]?.split("/");
return splitHash?.length === 3 && splitHash?.[2] === "solo";
};
const isOldHashSolo = isUrlSolo(e.oldURL);
const isNewHashSolo = isUrlSolo(e.newURL);
const target = document.getElementById(window.location.hash.substring(1));
if (!target || isOldHashSolo !== isNewHashSolo) renderPage();
});
// Get the hash from the URL
const hash = window.location.hash;
const splitHash = hash.split("/");
// Scroll to specified hash if it exists
if (hash) document.getElementById(hash.substring(1))?.scrollIntoView();
// Determine which examples to render based on hash
if (splitHash[0] === "#bezier" && splitHash[1] in bezierFeatures && splitHash[2] === "solo") {
window.document.body.innerHTML = `<div id="bezier-demos"></div>`;
const container = document.getElementById("bezier-demos");
if (!container) return;
const key = splitHash[1];
const value = (bezierFeatures as Record<string, BezierFeatureOptions>)[key];
if (value) container.append(bezierDemoGroup(key as BezierFeatureKey, value));
return;
}
if (splitHash[0] === "#subpath" && splitHash[1] in subpathFeatures && splitHash[2] === "solo") {
window.document.body.innerHTML = `<div id="subpath-demos"></div>`;
const container = document.getElementById("subpath-demos");
if (!container) return;
const key = splitHash[1];
const value = (subpathFeatures as Record<string, SubpathFeatureOptions>)[key];
if (value) container.append(subpathDemoGroup(key as SubpathFeatureKey, value));
return;
}
window.document.body.innerHTML = `
<h1 class="website-header">Bezier-rs Interactive Documentation</h1>
<p class="website-description">
This is the interactive documentation for the <a href="https://crates.io/crates/bezier-rs">Bezier-rs</a> library. View the
<a href="https://docs.rs/bezier-rs/latest/bezier_rs">crate documentation</a>
for detailed function descriptions and API usage. Click and drag on the endpoints of the demo curves to visualize the various Bezier utilities and functions.
</p>
<h2 class="category-header">Beziers</h2>
<div id="bezier-demos"></div>
<h2 class="category-header">Subpaths</h2>
<div id="subpath-demos"></div>
`.trim();
const bezierDemos = document.getElementById("bezier-demos") || undefined;
if (bezierDemos) Object.entries(bezierFeatures).forEach(([key, options]) => bezierDemos.appendChild(bezierDemoGroup(key as BezierFeatureKey, options)));
const subpathDemos = document.getElementById("subpath-demos") || undefined;
if (subpathDemos) Object.entries(subpathFeatures).forEach(([key, options]) => subpathDemos.appendChild(subpathDemoGroup(key as SubpathFeatureKey, options)));
}
function bezierDemoGroup(key: BezierFeatureKey, options: BezierFeatureOptions): HTMLDivElement {
const demoOptions = options.demoOptions || {};
const demos: BezierDemoArgs[] = BEZIER_CURVE_TYPE.map((curveType: BezierCurveType) => ({
title: curveType,
disabled: demoOptions[curveType]?.disabled || false,
points: demoOptions[curveType]?.customPoints || getBezierDemoPointDefaults()[curveType],
inputOptions: demoOptions[curveType]?.inputOptions || demoOptions.Quadratic?.inputOptions || [],
}));
return renderDemoGroup(`bezier/${key}`, bezierFeatures[key].name, demos, (demo: BezierDemoArgs) =>
demoBezier(demo.title, demo.points, key, demo.inputOptions, options.triggerOnMouseMove || false),
);
}
function subpathDemoGroup(key: SubpathFeatureKey, options: SubpathFeatureOptions): HTMLDivElement {
const buildDemo = (demo: SubpathDemoArgs) => {
const newInputOptions = (options.inputOptions || []).map((option) => ({
...option,
disabled: option.isDisabledForClosed && demo.closed,
}));
return demoSubpath(demo.title, demo.triples, key, demo.closed, newInputOptions, options.triggerOnMouseMove || false);
};
return renderDemoGroup(`subpath/${key}`, subpathFeatures[key].name, getSubpathDemoArgs(), buildDemo);
}
function demoBezier(title: string, points: number[][], key: BezierFeatureKey, inputOptions: InputOption[], triggerOnMouseMove: boolean): DemoDataBezier {
return {
kind: "bezier",
title,
element: document.createElement("div"),
inputOptions,
locked: false,
triggerOnMouseMove,
sliderData: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.default }))),
sliderUnits: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.unit }))),
activePointIndex: undefined as number | undefined,
manipulatorKeys: MANIPULATOR_KEYS_FROM_BEZIER_TYPE[getCurveType(points.length)],
bezier: WasmBezier[getConstructorKey(getCurveType(points.length))](points),
points,
callback: bezierFeatures[key].callback,
};
}
function demoSubpath(title: string, triples: (number[] | undefined)[][], key: SubpathFeatureKey, closed: boolean, inputOptions: InputOption[], triggerOnMouseMove: boolean): DemoDataSubpath {
return {
kind: "subpath",
title,
element: document.createElement("div"),
inputOptions,
locked: false,
triggerOnMouseMove,
sliderData: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.default }))),
sliderUnits: Object.assign({}, ...inputOptions.map((s) => ({ [s.variable]: s.unit }))),
activePointIndex: undefined as number | undefined,
activeManipulatorIndex: undefined as number | undefined,
manipulatorKeys: undefined as undefined | WasmSubpathManipulatorKey[],
subpath: WasmSubpath.from_triples(triples, closed) as WasmSubpathInstance,
triples,
callback: subpathFeatures[key].callback,
};
}
function updateDemoSVG(data: DemoData, figure: HTMLElement, mouseLocation?: [number, number]) {
if (data.kind === "subpath") figure.innerHTML = data.callback(data.subpath, data.sliderData, mouseLocation);
if (data.kind === "bezier") figure.innerHTML = data.callback(data.bezier, data.sliderData, mouseLocation);
}
function onMouseDown(data: DemoData, e: MouseEvent) {
const SELECTABLE_RANGE = 10;
let distances;
if (data.kind === "bezier") {
distances = data.points.flatMap((point, pointIndex) => {
if (!point) return [];
const distance = Math.sqrt(Math.pow(e.offsetX - point[0], 2) + Math.pow(e.offsetY - point[1], 2));
return distance < SELECTABLE_RANGE ? [{ manipulatorIndex: undefined, pointIndex, distance }] : [];
});
} else if (data.kind === "subpath") {
distances = data.triples.flatMap((triple, manipulatorIndex) =>
triple.flatMap((point, pointIndex) => {
if (!point) return [];
const distance = Math.sqrt(Math.pow(e.offsetX - point[0], 2) + Math.pow(e.offsetY - point[1], 2));
return distance < SELECTABLE_RANGE ? [{ manipulatorIndex, pointIndex, distance }] : [];
}),
);
} else {
return;
}
const closest = distances.sort((a, b) => a.distance - b.distance)[0];
if (closest) {
if (data.kind === "subpath") data.activeManipulatorIndex = closest.manipulatorIndex;
data.activePointIndex = closest.pointIndex;
}
}
function onMouseMove(data: DemoData, e: MouseEvent) {
if (data.locked || !(e.currentTarget instanceof HTMLElement)) return;
data.locked = true;
if (data.kind === "bezier" && data.activePointIndex !== undefined) {
data.bezier[data.manipulatorKeys[data.activePointIndex]](e.offsetX, e.offsetY);
data.points[data.activePointIndex] = [e.offsetX, e.offsetY];
updateDemoSVG(data, e.currentTarget);
} else if (data.kind === "subpath" && data.activePointIndex !== undefined && data.activeManipulatorIndex !== undefined) {
data.subpath[POINT_INDEX_TO_MANIPULATOR[data.activePointIndex]](data.activeManipulatorIndex, e.offsetX, e.offsetY);
data.triples[data.activeManipulatorIndex][data.activePointIndex] = [e.offsetX, e.offsetY];
updateDemoSVG(data, e.currentTarget);
} else if (data.triggerOnMouseMove) {
updateDemoSVG(data, e.currentTarget, [e.offsetX, e.offsetY]);
}
data.locked = false;
}
function onMouseUp(data: DemoData) {
data.activePointIndex = undefined;
if (data.kind === "subpath") data.activeManipulatorIndex = undefined;
}
function renderDemoGroup<T extends DemoArgs>(id: string, name: string, demos: T[], buildDemo: (demo: T) => DemoData): HTMLDivElement {
const demoGroup = document.createElement("div");
demoGroup.className = "demo-group-container";
demoGroup.insertAdjacentHTML(
"beforeend",
`
${(() => {
// Add header and href anchor if not on a solo example page
const currentHash = window.location.hash.split("/");
if (currentHash.length === 3 || currentHash[2] === "solo") return "";
return `
<h3 class="demo-group-header">
<a href="#${id}">#</a>
${name}
</h3>
`.trim();
})()}
<div class="demo-row" data-demo-row></div>
`.trim(),
);
const demoRow = demoGroup.querySelector("[data-demo-row]");
if (!demoRow) return demoGroup;
demos.forEach((demo) => {
if (demo.disabled) return;
const data = buildDemo(demo);
renderDemo(data);
const figure = data.element.querySelector("[data-demo-figure]");
if (figure instanceof HTMLElement) updateDemoSVG(data, figure);
demoRow.append(data.element);
});
return demoGroup;
}
function renderDemo(demo: DemoData) {
const getSliderUnit = (data: DemoData, variable: string): string => {
return (Array.isArray(data.sliderUnits[variable]) ? "" : data.sliderUnits[variable]) || "";
};
demo.element.insertAdjacentHTML(
"beforeend",
`
<h4 class="demo-header">${demo.title}</h4>
<div class="demo-figure" data-demo-figure></div>
<div class="parent-input-container" data-parent-input-container>
${(() =>
demo.inputOptions
.map((inputOption) =>
`
<div
class="${(() => {
if (inputOption.inputType === "dropdown") return "select-container";
if (inputOption.inputType === "slider") return "slider-container";
return "";
})()}"
data-input-container
>
<div class="input-label" data-input-label>
${inputOption.variable}: ${inputOption.inputType === "dropdown" ? "" : demo.sliderData[inputOption.variable]}${getSliderUnit(demo, inputOption.variable)}
</div>
${(() => {
if (inputOption.inputType !== "dropdown") return "";
return `
<select class="select-input" value="${inputOption.default}" ${inputOption.disabled ? "disabled" : ""} data-select>
${inputOption.options?.map((value, idx) => `<option value="${idx}" id="${idx}-${value}">${value}</option>`).join("\n")}
</select>
`.trim();
})()}
${(() => {
if (inputOption.inputType !== "slider") return "";
const ratio = (Number(inputOption.default) - (inputOption.min || 0)) / ((inputOption.max || 100) - (inputOption.min || 0));
return `
<input
class="slider-input"
type="range"
max="${inputOption.max}"
min="${inputOption.min}"
step="${inputOption.step}"
value="${inputOption.default}"
style="--range-ratio: ${ratio}"
data-slider-input
/>
`.trim();
})()}
</div>
`.trim(),
)
.join("\n"))()}
</div>
`.trim(),
);
const figure = demo.element.querySelector(`[data-demo-figure]`);
if (!(figure instanceof HTMLElement)) return;
figure.addEventListener("mousedown", (e) => onMouseDown(demo, e));
figure.addEventListener("mouseup", () => onMouseUp(demo));
figure.addEventListener("mousemove", (e) => onMouseMove(demo, e));
demo.inputOptions.forEach((inputOption, index) => {
const inputContainer = demo.element.querySelectorAll(`[data-parent-input-container] [data-input-container]`)[index];
if (!(inputContainer instanceof HTMLDivElement)) return;
if (inputOption.inputType === "dropdown") {
const selectElement = inputContainer.querySelector("[data-select]");
if (!(selectElement instanceof HTMLSelectElement)) return;
selectElement.addEventListener("change", (e: Event) => {
if (!(e.target instanceof HTMLSelectElement)) return;
demo.sliderData[inputOption.variable] = Number(e.target.value);
updateDemoSVG(demo, figure);
});
}
if (inputOption.inputType === "slider") {
const sliderInput = inputContainer.querySelector("[data-slider-input]");
if (!(sliderInput instanceof HTMLInputElement)) return;
sliderInput.addEventListener("input", (e: Event) => {
const target = e.target;
if (!(target instanceof HTMLInputElement)) return;
// Set the slider label text
const variable = inputOption.variable;
const data = demo.sliderData[variable];
const unit = getSliderUnit(demo, variable);
const label = inputContainer.querySelector("[data-input-label]");
if (!(label instanceof HTMLDivElement)) return;
label.innerText = `${variable}: ${data}${unit}`;
// Set the slider input range percentage
sliderInput.style.setProperty("--range-ratio", String((Number(target.value) - (inputOption.min || 0)) / ((inputOption.max || 100) - (inputOption.min || 0))));
// Update the slider data and redraw the demo
demo.sliderData[variable] = Number(target.value);
updateDemoSVG(demo, figure);
});
}
});
}
-241
View File
@@ -1,241 +0,0 @@
import type * as WasmPkg from "@/../wasm/pkg";
type WasmRawInstance = typeof WasmPkg;
export type WasmBezierInstance = InstanceType<WasmRawInstance["WasmBezier"]>;
export type WasmSubpathInstance = InstanceType<WasmRawInstance["WasmSubpath"]>;
export type WasmSubpathManipulatorKey = "set_anchor" | "set_in_handle" | "set_out_handle";
type WasmBezierConstructorKey = "new_linear" | "new_quadratic" | "new_cubic";
type WasmBezierManipulatorKey = "set_start" | "set_handle_start" | "set_handle_end" | "set_end";
type DemoDataCommon = {
title: string;
element: HTMLDivElement;
inputOptions: InputOption[];
locked: boolean;
triggerOnMouseMove: boolean;
sliderData: Record<string, number>;
sliderUnits: Record<string, string | string[]>;
activePointIndex: number | undefined;
};
export type DemoDataBezier = DemoDataCommon & {
kind: "bezier";
manipulatorKeys: WasmBezierManipulatorKey[];
bezier: WasmBezierInstance;
points: number[][];
callback: BezierCallback;
};
export type DemoDataSubpath = DemoDataCommon & {
kind: "subpath";
activeManipulatorIndex: number | undefined;
manipulatorKeys: WasmSubpathManipulatorKey[] | undefined;
subpath: WasmSubpathInstance;
triples: (number[] | undefined)[][];
callback: SubpathCallback;
};
export type DemoData = DemoDataBezier | DemoDataSubpath;
export const BEZIER_CURVE_TYPE = ["Linear", "Quadratic", "Cubic"] as const;
export type BezierCurveType = (typeof BEZIER_CURVE_TYPE)[number];
export type BezierCallback = (bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: [number, number]) => string;
export type SubpathCallback = (subpath: WasmSubpathInstance, options: Record<string, number>, mouseLocation?: [number, number]) => string;
export type BezierDemoOptions = {
[key in BezierCurveType]: {
disabled?: boolean;
inputOptions?: InputOption[];
customPoints?: number[][];
};
};
export type InputOption = {
variable: string;
min?: number;
max?: number;
step?: number;
default?: number;
unit?: string | string[];
inputType?: "slider" | "dropdown";
options?: string[];
disabled?: boolean;
};
export type SubpathInputOption = InputOption & {
isDisabledForClosed?: boolean;
};
export function getCurveType(numPoints: number): BezierCurveType {
const mapping: Record<number, BezierCurveType> = {
2: "Linear",
3: "Quadratic",
4: "Cubic",
};
if (!(numPoints in mapping)) throw new Error("Invalid number of points for a bezier");
return mapping[numPoints];
}
export function getConstructorKey(bezierCurveType: BezierCurveType): WasmBezierConstructorKey {
const mapping: Record<BezierCurveType, WasmBezierConstructorKey> = {
Linear: "new_linear",
Quadratic: "new_quadratic",
Cubic: "new_cubic",
};
return mapping[bezierCurveType];
}
export type DemoArgs = {
title: string;
disabled?: boolean;
};
export type BezierDemoArgs = {
points: number[][];
inputOptions: InputOption[];
} & DemoArgs;
export type SubpathDemoArgs = {
triples: (number[] | undefined)[][];
closed: boolean;
} & DemoArgs;
export const BEZIER_T_VALUE_VARIANTS = ["Parametric", "Euclidean"] as const;
export const SUBPATH_T_VALUE_VARIANTS = ["GlobalParametric", "GlobalEuclidean"] as const;
const CAP_VARIANTS = ["Butt", "Round", "Square"] as const;
const JOIN_VARIANTS = ["Bevel", "Miter", "Round"] as const;
export const POINT_INDEX_TO_MANIPULATOR: WasmSubpathManipulatorKey[] = ["set_anchor", "set_in_handle", "set_out_handle"];
// Given the number of points in the curve, map the index of a point to the correct manipulator key
export const MANIPULATOR_KEYS_FROM_BEZIER_TYPE: { [key in BezierCurveType]: WasmBezierManipulatorKey[] } = {
Linear: ["set_start", "set_end"],
Quadratic: ["set_start", "set_handle_start", "set_end"],
Cubic: ["set_start", "set_handle_start", "set_handle_end", "set_end"],
};
export function getBezierDemoPointDefaults() {
// We use a function to generate a new object each time it is called
// to prevent one instance from being shared and modified across demos
return {
Linear: [
[55, 60],
[165, 120],
],
Quadratic: [
[55, 50],
[165, 30],
[185, 170],
],
Cubic: [
[55, 30],
[85, 140],
[175, 30],
[185, 160],
],
};
}
export function getSubpathDemoArgs(): SubpathDemoArgs[] {
// We use a function to generate a new object each time it is called
// to prevent one instance from being shared and modified across demos
return [
{
title: "Open Subpath",
triples: [
[[45, 20], undefined, [35, 90]],
[[175, 40], [85, 40], undefined],
[[200, 175], undefined, undefined],
[[125, 100], [65, 120], undefined],
],
closed: false,
},
{
title: "Closed Subpath",
triples: [
[[60, 125], undefined, [65, 40]],
[[155, 30], [145, 120], undefined],
[
[170, 150],
[200, 90],
[95, 185],
],
],
closed: true,
},
];
}
export const tSliderOptions = {
variable: "t",
inputType: "slider",
min: -0.01,
max: 1.01,
step: 0.01,
default: 0.5,
};
export const errorOptions = {
variable: "error",
inputType: "slider",
min: 0.1,
max: 2,
step: 0.1,
default: 0.5,
};
export const minimumSeparationOptions = {
variable: "minimum_separation",
inputType: "slider",
min: 0.001,
max: 0.25,
step: 0.001,
default: 0.05,
};
export const intersectionErrorOptions = {
variable: "error",
inputType: "slider",
min: 0.001,
max: 0.525,
step: 0.0025,
default: 0.02,
};
export const separationDiskDiameter = {
variable: "separation_disk_diameter",
inputType: "slider",
min: 2.5,
max: 25,
step: 0.1,
default: 5,
};
export const bezierTValueVariantOptions = {
variable: "TVariant",
inputType: "dropdown",
default: 0,
options: BEZIER_T_VALUE_VARIANTS,
};
export const subpathTValueVariantOptions = {
variable: "TVariant",
inputType: "dropdown",
default: 0,
options: SUBPATH_T_VALUE_VARIANTS,
};
export const joinOptions = {
variable: "join",
inputType: "dropdown",
default: 0,
options: JOIN_VARIANTS,
};
export const capOptions = {
variable: "cap",
inputType: "dropdown",
default: 0,
options: CAP_VARIANTS,
};
@@ -1,28 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"strict": true,
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"lib": ["ESNext", "DOM", "DOM.Iterable", "ScriptHost"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "*.ts", "*.js", "*.cjs"],
"exclude": ["node_modules"],
"ts-node": {
"compilerOptions": {
"module": "commonjs",
"useDefineForClassFields": false,
"noImplicitOverride": true
}
}
}
@@ -1,19 +0,0 @@
/* eslint-disable no-console */
import path from "path";
import { defineConfig } from "vite";
const projectRootDir = path.resolve(__dirname);
// https://vitejs.dev/config/
export default defineConfig({
base: "",
resolve: {
alias: [{ find: "@", replacement: path.resolve(projectRootDir, "src") }],
},
server: {
port: 8000,
host: "0.0.0.0",
},
});
@@ -1,50 +0,0 @@
[package]
name = "bezier-rs-wasm"
publish = false
version = "0.0.0"
rust-version = "1.85"
authors = ["Graphite Authors <contact@graphite.rs>"]
edition = "2024"
readme = "../../README.md"
homepage = "https://graphite.rs"
repository = "https://github.com/GraphiteEditor/Graphite"
license = "Apache-2.0"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = []
logging = ["log"]
[dependencies]
# Workspace dependencies
bezier-rs = { workspace = true }
wasm-bindgen = { workspace = true }
js-sys = { workspace = true }
glam = { workspace = true }
log = { workspace = true, optional = true }
[dev-dependencies]
log = { workspace = true }
[package.metadata.wasm-pack.profile.dev]
wasm-opt = false
[package.metadata.wasm-pack.profile.dev.wasm-bindgen]
debug-js-glue = true
demangle-name-section = true
dwarf-debug-info = false
[package.metadata.wasm-pack.profile.release]
wasm-opt = ["-Oz", "--enable-bulk-memory"]
[package.metadata.wasm-pack.profile.release.wasm-bindgen]
debug-js-glue = false
demangle-name-section = false
dwarf-debug-info = false
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(wasm_bindgen_unstable_test_coverage)',
] }
@@ -1,723 +0,0 @@
use crate::svg_drawing::*;
use crate::utils::{parse_cap, parse_point};
use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, Identifier, TValue, TValueType};
use glam::DVec2;
use js_sys::Array;
use wasm_bindgen::prelude::*;
use wasm_bindgen::{JsCast, JsValue};
#[wasm_bindgen]
pub enum WasmMaximizeArcs {
Automatic, // 0
On, // 1
Off, // 2
}
const SCALE_UNIT_VECTOR_FACTOR: f64 = 50.;
/// Wrapper of the `Bezier` struct to be used in JS.
#[wasm_bindgen]
#[derive(Clone)]
pub struct WasmBezier(Bezier);
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,
}
}
fn parse_t_variant(t_variant: &String, t: f64) -> TValue {
match t_variant.as_str() {
"Parametric" => TValue::Parametric(t),
"Euclidean" => TValue::Euclidean(t),
_ => panic!("Unexpected TValue string: '{t_variant}'"),
}
}
/// An empty id type for use in tests
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct EmptyId;
impl Identifier for EmptyId {
fn new() -> Self {
Self
}
}
#[wasm_bindgen]
impl WasmBezier {
pub fn new_linear(js_points: JsValue) -> WasmBezier {
let array = js_points.dyn_into::<Array>().unwrap();
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
WasmBezier(Bezier::from_linear_dvec2(point1, point2))
}
/// Expect js_points to be a list of 3 pairs.
pub fn new_quadratic(js_points: JsValue) -> WasmBezier {
let array = js_points.dyn_into::<Array>().unwrap();
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
let point3 = parse_point(&array.get(2));
WasmBezier(Bezier::from_quadratic_dvec2(point1, point2, point3))
}
/// Expect js_points to be a list of 4 pairs.
pub fn new_cubic(js_points: JsValue) -> WasmBezier {
let array = js_points.dyn_into::<Array>().unwrap();
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
let point3 = parse_point(&array.get(2));
let point4 = parse_point(&array.get(3));
WasmBezier(Bezier::from_cubic_dvec2(point1, point2, point3, point4))
}
fn draw_bezier_through_points(bezier: Bezier, through_point: DVec2) -> String {
let mut bezier_string = String::new();
bezier.to_svg(
&mut bezier_string,
CURVE_ATTRIBUTES.to_string(),
ANCHOR_ATTRIBUTES.to_string(),
HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED),
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED),
);
let through_point_circle = format!(r#"<circle cx="{}" cy="{}" {}/>"#, through_point.x, through_point.y, ANCHOR_ATTRIBUTES);
wrap_svg_tag(format!("{bezier_string}{through_point_circle}"))
}
pub fn quadratic_through_points(js_points: JsValue, t: f64) -> String {
let array = js_points.dyn_into::<Array>().unwrap();
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
let point3 = parse_point(&array.get(2));
let bezier = Bezier::quadratic_through_points(point1, point2, point3, Some(t));
WasmBezier::draw_bezier_through_points(bezier, point2)
}
pub fn cubic_through_points(js_points: JsValue, t: f64, midpoint_separation: f64) -> String {
let array = js_points.dyn_into::<Array>().unwrap();
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
let point3 = parse_point(&array.get(2));
let bezier = Bezier::cubic_through_points(point1, point2, point3, Some(t), Some(midpoint_separation));
WasmBezier::draw_bezier_through_points(bezier, point2)
}
pub fn set_start(&mut self, x: f64, y: f64) {
self.0.set_start(DVec2::new(x, y));
}
pub fn set_end(&mut self, x: f64, y: f64) {
self.0.set_end(DVec2::new(x, y));
}
pub fn set_handle_start(&mut self, x: f64, y: f64) {
self.0.set_handle_start(DVec2::new(x, y));
}
pub fn set_handle_end(&mut self, x: f64, y: f64) {
self.0.set_handle_end(DVec2::new(x, y));
}
pub fn get_points(&self) -> JsValue {
JsValue::from(
self.0
.get_points()
.map(|point| [JsValue::from_f64(point.x), JsValue::from_f64(point.y)].iter().collect::<Array>())
.collect::<Array>(),
)
}
fn get_bezier_path(&self) -> String {
let mut bezier = String::new();
self.0.to_svg(
&mut bezier,
CURVE_ATTRIBUTES.to_string(),
ANCHOR_ATTRIBUTES.to_string(),
HANDLE_ATTRIBUTES.to_string(),
HANDLE_LINE_ATTRIBUTES.to_string(),
);
bezier
}
pub fn to_svg(&self) -> String {
wrap_svg_tag(self.get_bezier_path())
}
pub fn length(&self) -> String {
let bezier = self.get_bezier_path();
wrap_svg_tag(format!("{bezier}{}", draw_text(format!("Length: {:.2}", self.0.length(None)), TEXT_OFFSET_X, TEXT_OFFSET_Y, BLACK)))
}
pub fn length_centroid(&self) -> String {
let bezier = self.get_bezier_path();
let centroid = self.0.length_centroid(None);
let point_text = draw_circle(centroid, 4., RED, 1.5, WHITE);
wrap_svg_tag(format!("{bezier}{}", point_text))
}
pub fn evaluate(&self, raw_t: f64, t_variant: String) -> String {
let bezier = self.get_bezier_path();
let t = parse_t_variant(&t_variant, raw_t);
let point = self.0.evaluate(t);
let content = format!("{bezier}{}", draw_circle(point, 4., RED, 1.5, WHITE));
wrap_svg_tag(content)
}
pub fn compute_lookup_table(&self, steps: usize, t_variant: String) -> String {
let bezier = self.get_bezier_path();
let tvalue_type = match t_variant.as_str() {
"Parametric" => TValueType::Parametric,
"Euclidean" => TValueType::Euclidean,
_ => panic!("Unexpected TValue string: '{t_variant}'"),
};
let table_values: Vec<DVec2> = self.0.compute_lookup_table(Some(steps), Some(tvalue_type)).collect();
let circles: String = table_values
.iter()
.map(|point| draw_circle(*point, 3., RED, 1.5, WHITE))
.fold("".to_string(), |acc, circle| acc + &circle);
let content = format!("{bezier}{circles}");
wrap_svg_tag(content)
}
pub fn derivative(&self) -> String {
let bezier = self.get_bezier_path();
let derivative = self.0.derivative();
if derivative.is_none() {
return bezier;
}
let mut derivative_svg_path = String::new();
derivative.unwrap().to_svg(
&mut derivative_svg_path,
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED),
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED),
HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED),
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED),
);
let content = format!("{bezier}{derivative_svg_path}");
wrap_svg_tag(content)
}
pub fn tangent(&self, raw_t: f64, t_variant: String) -> String {
let bezier = self.get_bezier_path();
let t = parse_t_variant(&t_variant, raw_t);
let tangent_point = self.0.tangent(t);
let intersection_point = self.0.evaluate(t);
let tangent_end = intersection_point + tangent_point * SCALE_UNIT_VECTOR_FACTOR;
let content = format!(
"{bezier}{}{}{}",
draw_circle(intersection_point, 3., RED, 1., WHITE),
draw_line(intersection_point.x, intersection_point.y, tangent_end.x, tangent_end.y, RED, 1.),
draw_circle(tangent_end, 3., RED, 1., WHITE),
);
wrap_svg_tag(content)
}
pub fn normal(&self, raw_t: f64, t_variant: String) -> String {
let bezier = self.get_bezier_path();
let t = parse_t_variant(&t_variant, raw_t);
let normal_point = self.0.normal(t);
let intersection_point = self.0.evaluate(t);
let normal_end = intersection_point + normal_point * SCALE_UNIT_VECTOR_FACTOR;
let content = format!(
"{bezier}{}{}{}",
draw_line(intersection_point.x, intersection_point.y, normal_end.x, normal_end.y, RED, 1.),
draw_circle(intersection_point, 3., RED, 1., WHITE),
draw_circle(normal_end, 3., RED, 1., WHITE),
);
wrap_svg_tag(content)
}
pub fn curvature(&self, raw_t: f64, t_variant: String) -> String {
let bezier = self.get_bezier_path();
let t = parse_t_variant(&t_variant, raw_t);
let intersection_point = self.0.evaluate(t);
let normal_point = self.0.normal(t);
let curvature = self.0.curvature(t);
let content = if curvature.abs() < 0.000001 {
// Linear curve segment: the radius is infinite so we don't draw it
format!("{bezier}{}", draw_circle(intersection_point, 3., RED, 1., WHITE))
} else {
let radius = 1. / curvature;
let curvature_center = intersection_point + normal_point * radius;
format!(
"{bezier}{}{}{}{}",
draw_circle(curvature_center, radius.abs(), RED, 1., NONE),
draw_line(intersection_point.x, intersection_point.y, curvature_center.x, curvature_center.y, RED, 1.),
draw_circle(intersection_point, 3., RED, 1., WHITE),
draw_circle(curvature_center, 3., RED, 1., WHITE),
)
};
wrap_svg_tag(content)
}
pub fn split(&self, raw_t: f64, t_variant: String) -> String {
let t = parse_t_variant(&t_variant, raw_t);
let beziers: [Bezier; 2] = self.0.split(t);
let mut bezier_svg_1 = String::new();
beziers[0].to_svg(
&mut bezier_svg_1,
CURVE_ATTRIBUTES.to_string().replace(BLACK, ORANGE).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"",
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, ORANGE),
HANDLE_ATTRIBUTES.to_string().replace(GRAY, ORANGE),
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, ORANGE),
);
let mut bezier_svg_2 = String::new();
beziers[1].to_svg(
&mut bezier_svg_2,
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"",
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED),
HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED),
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED),
);
wrap_svg_tag(format!("{}{bezier_svg_1}{bezier_svg_2}", self.get_bezier_path()))
}
pub fn trim(&self, raw_t1: f64, raw_t2: f64, t_variant: String) -> String {
let (t1, t2) = (parse_t_variant(&t_variant, raw_t1), parse_t_variant(&t_variant, raw_t2));
let trimmed_bezier = self.0.trim(t1, t2);
let mut trimmed_bezier_svg = String::new();
trimmed_bezier.to_svg(
&mut trimmed_bezier_svg,
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"",
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED),
HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED),
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED),
);
wrap_svg_tag(format!("{}{trimmed_bezier_svg}", self.get_bezier_path()))
}
pub fn project(&self, x: f64, y: f64) -> String {
let projected_t_value = self.0.project(DVec2::new(x, y));
let projected_point = self.0.evaluate(TValue::Parametric(projected_t_value));
let bezier = self.get_bezier_path();
let content = format!("{bezier}{}", draw_line(projected_point.x, projected_point.y, x, y, RED, 1.),);
wrap_svg_tag(content)
}
pub fn tangents_to_point(&self, x: f64, y: f64) -> String {
let bezier = self.get_bezier_path();
let mut content = String::new();
for t in self.0.tangents_to_point(DVec2::new(x, y)) {
let point = self.0.evaluate(TValue::Parametric(t));
content += &draw_line(x, y, point.x, point.y, RED, 1.);
}
use std::fmt::Write;
write!(content, "{bezier}").unwrap();
wrap_svg_tag(content)
}
pub fn normals_to_point(&self, x: f64, y: f64) -> String {
let bezier = self.get_bezier_path();
let mut content = String::new();
for t in self.0.normals_to_point(DVec2::new(x, y)) {
let point = self.0.evaluate(TValue::Parametric(t));
content += &draw_line(x, y, point.x, point.y, RED, 1.);
}
use std::fmt::Write;
write!(content, "{bezier}").unwrap();
wrap_svg_tag(content)
}
pub fn local_extrema(&self) -> String {
let local_extrema = self.0.local_extrema();
let bezier = self.get_bezier_path();
let circles: String = local_extrema
.into_iter()
.zip([RED, GREEN])
.flat_map(|(t_value_list, color)| {
t_value_list.map(move |t_value| {
let point = self.0.evaluate(TValue::Parametric(t_value));
draw_circle(point, 3., color, 1.5, WHITE)
})
})
.fold("".to_string(), |acc, circle| acc + &circle);
let content = format!(
"{bezier}{circles}{}{}",
draw_text("X extrema".to_string(), TEXT_OFFSET_X, TEXT_OFFSET_Y - 20., RED),
draw_text("Y extrema".to_string(), TEXT_OFFSET_X, TEXT_OFFSET_Y, GREEN),
);
wrap_svg_tag(content)
}
pub fn bounding_box(&self) -> String {
let [bbox_min_corner, bbox_max_corner] = self.0.bounding_box();
let bezier = self.get_bezier_path();
let content = format!(
"{bezier}<rect x={} y ={} width=\"{}\" height=\"{}\" style=\"fill:{NONE};stroke:{RED};stroke-width:1\" />",
bbox_min_corner.x,
bbox_min_corner.y,
bbox_max_corner.x - bbox_min_corner.x,
bbox_max_corner.y - bbox_min_corner.y,
);
wrap_svg_tag(content)
}
pub fn inflections(&self) -> String {
let inflections: Vec<f64> = self.0.inflections();
let bezier = self.get_bezier_path();
let circles: String = inflections
.iter()
.map(|&t_value| {
let point = self.0.evaluate(TValue::Parametric(t_value));
draw_circle(point, 3., RED, 1.5, WHITE)
})
.fold("".to_string(), |acc, circle| acc + &circle);
let content = format!("{bezier}{circles}");
wrap_svg_tag(content)
}
pub fn de_casteljau_points(&self, raw_t: f64, t_variant: String) -> String {
let t = parse_t_variant(&t_variant, raw_t);
let points: Vec<Vec<DVec2>> = self.0.de_casteljau_points(t);
let bezier_svg = self.get_bezier_path();
let casteljau_svg = points
.iter()
.enumerate()
.map(|(index, points)| {
let color_light = format!("hsl({}, 100%, 50%)", 90 * index);
let points_and_handle_lines = points
.iter()
.enumerate()
.map(|(index, point)| {
let circle = draw_circle(*point, 3., &color_light, 1.5, WHITE);
if index != 0 {
let prev_point = points[index - 1];
let line = draw_line(prev_point.x, prev_point.y, point.x, point.y, &color_light, 1.5);
circle + line.as_str()
} else {
circle
}
})
.fold("".to_string(), |acc, point_svg| acc + &point_svg);
points_and_handle_lines
})
.fold("".to_string(), |acc, points_svg| acc + &points_svg);
let content = format!("{bezier_svg}{casteljau_svg}");
wrap_svg_tag(content)
}
pub fn rotate(&self, angle: f64, pivot_x: f64, pivot_y: f64) -> String {
let original_bezier_svg = self.get_bezier_path();
let rotated_bezier = self.0.rotate_about_point(angle, DVec2::new(pivot_x, pivot_y));
let mut rotated_bezier_svg = String::new();
rotated_bezier.to_svg(&mut rotated_bezier_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
let pivot = draw_circle(DVec2::new(pivot_x, pivot_y), 3., GRAY, 1.5, WHITE);
// Line between pivot and start point on curve
let original_dashed_line = format!(
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
self.0.start().x,
self.0.start().y
);
let rotated_dashed_line = format!(
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
rotated_bezier.start().x,
rotated_bezier.start().y
);
wrap_svg_tag(format!("{original_bezier_svg}{rotated_bezier_svg}{pivot}{original_dashed_line}{rotated_dashed_line}"))
}
fn intersect(&self, curve: &Bezier, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<f64> {
self.0.intersections(curve, error, minimum_separation)
}
pub fn intersect_line_segment(&self, js_points: JsValue) -> String {
let array = js_points.dyn_into::<Array>().unwrap();
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
let line = Bezier::from_linear_dvec2(point1, point2);
let bezier_curve_svg = self.get_bezier_path();
let mut line_svg = String::new();
line.to_svg(&mut line_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
let intersections_svg = self
.intersect(&line, None, None)
.iter()
.map(|intersection_t| {
let point = &self.0.evaluate(TValue::Parametric(*intersection_t));
draw_circle(*point, 4., RED, 1.5, WHITE)
})
.fold(String::new(), |acc, item| format!("{acc}{item}"));
wrap_svg_tag(format!("{bezier_curve_svg}{line_svg}{intersections_svg}"))
}
pub fn intersect_quadratic_segment(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String {
let array = js_points.dyn_into::<Array>().unwrap();
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
let point3 = parse_point(&array.get(2));
let quadratic = Bezier::from_quadratic_dvec2(point1, point2, point3);
let bezier_curve_svg = self.get_bezier_path();
let mut quadratic_svg = String::new();
quadratic.to_svg(&mut quadratic_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
let intersections_svg = self
.intersect(&quadratic, Some(error), Some(minimum_separation))
.iter()
.map(|intersection_t| {
let point = &self.0.evaluate(TValue::Parametric(*intersection_t));
draw_circle(*point, 4., RED, 1.5, WHITE)
})
.fold(String::new(), |acc, item| format!("{acc}{item}"));
wrap_svg_tag(format!("{bezier_curve_svg}{quadratic_svg}{intersections_svg}"))
}
pub fn intersect_cubic_segment(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String {
let array = js_points.dyn_into::<Array>().unwrap();
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
let point3 = parse_point(&array.get(2));
let point4 = parse_point(&array.get(3));
let cubic = Bezier::from_cubic_dvec2(point1, point2, point3, point4);
let bezier_curve_svg = self.get_bezier_path();
let mut cubic_svg = String::new();
cubic.to_svg(&mut cubic_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
let intersections_svg = self
.intersect(&cubic, Some(error), Some(minimum_separation))
.iter()
.map(|intersection_t| {
let point = &self.0.evaluate(TValue::Parametric(*intersection_t));
draw_circle(*point, 4., RED, 1.5, WHITE)
})
.fold(String::new(), |acc, item| format!("{acc}{item}"));
wrap_svg_tag(format!("{bezier_curve_svg}{cubic_svg}{intersections_svg}"))
}
/// The wrapped return type is `Vec<[f64; 2]>`.
pub fn intersect_self(&self, error: f64, minimum_separation: f64) -> String {
let bezier_curve_svg = self.get_bezier_path();
let intersect_self_svg = self
.0
.self_intersections(Some(error), Some(minimum_separation))
.iter()
.map(|intersection_t| {
let point = &self.0.evaluate(TValue::Parametric(intersection_t[0]));
draw_circle(*point, 4., RED, 1.5, WHITE)
})
.fold(bezier_curve_svg, |acc, item| format!("{acc}{item}"));
wrap_svg_tag(intersect_self_svg)
}
pub fn intersect_rectangle(&self, js_points: JsValue) -> String {
let array = js_points.dyn_into::<Array>().unwrap();
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
let bezier_curve_svg = self.get_bezier_path();
let mut rectangle_svg = String::new();
[
Bezier::from_linear_coordinates(point1.x, point1.y, point2.x, point1.y),
Bezier::from_linear_coordinates(point2.x, point1.y, point2.x, point2.y),
Bezier::from_linear_coordinates(point2.x, point2.y, point1.x, point2.y),
Bezier::from_linear_coordinates(point1.x, point2.y, point1.x, point1.y),
]
.iter()
.for_each(|line| line.to_svg(&mut rectangle_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()));
let intersections_svg = self
.0
.rectangle_intersections(point1, point2)
.iter()
.map(|intersection_t| {
let point = &self.0.evaluate(TValue::Parametric(*intersection_t));
draw_circle(*point, 4., RED, 1.5, WHITE)
})
.fold(String::new(), |acc, item| format!("{acc}{item}"));
wrap_svg_tag(format!("{bezier_curve_svg}{rectangle_svg}{intersections_svg}"))
}
pub fn reduce(&self) -> String {
let original_curve_svg = self.get_bezier_path();
let bezier_curves_svg: String = self
.0
.reduce(None)
.iter()
.enumerate()
.map(|(index, bezier_curve)| {
let mut curve_svg = String::new();
bezier_curve.to_svg(
&mut curve_svg,
CURVE_ATTRIBUTES.to_string().replace(BLACK, &format!("hsl({}, 100%, 50%)", (40 * index))),
String::new(),
String::new(),
String::new(),
);
curve_svg
})
.fold(original_curve_svg, |acc, item| format!("{acc}{item}"));
wrap_svg_tag(bezier_curves_svg)
}
pub fn offset(&self, distance: f64) -> String {
let original_curve_svg = self.get_bezier_path();
let bezier_curves_svg = self
.0
.offset::<EmptyId>(distance)
.iter()
.enumerate()
.map(|(index, bezier_curve)| {
let mut curve_svg = String::new();
bezier_curve.to_svg(
&mut curve_svg,
CURVE_ATTRIBUTES.to_string().replace(BLACK, &format!("hsl({}, 100%, 50%)", (40 * index))),
String::new(),
String::new(),
String::new(),
);
curve_svg
})
.fold(original_curve_svg, |acc, item| format!("{acc}{item}"));
wrap_svg_tag(bezier_curves_svg)
}
pub fn outline(&self, distance: f64, cap: i32) -> String {
let cap = parse_cap(cap);
let outline_subpath = self.0.outline::<EmptyId>(distance, cap);
if outline_subpath.is_empty() {
return String::new();
}
let mut outline_svg = String::new();
outline_subpath.to_svg(&mut outline_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
let bezier_svg = self.get_bezier_path();
wrap_svg_tag(format!("{bezier_svg}{outline_svg}"))
}
pub fn graduated_outline(&self, start_distance: f64, end_distance: f64, cap: i32) -> String {
let cap = parse_cap(cap);
let outline_subpath = self.0.graduated_outline::<EmptyId>(start_distance, end_distance, cap);
if outline_subpath.is_empty() {
return String::new();
}
let mut outline_svg = String::new();
outline_subpath.to_svg(&mut outline_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
let bezier_svg = self.get_bezier_path();
wrap_svg_tag(format!("{bezier_svg}{outline_svg}"))
}
pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: i32) -> String {
let cap = parse_cap(cap);
let outline_subpath = self.0.skewed_outline::<EmptyId>(distance1, distance2, distance3, distance4, cap);
if outline_subpath.is_empty() {
return String::new();
}
let mut outline_svg = String::new();
outline_subpath.to_svg(&mut outline_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
let bezier_svg = self.get_bezier_path();
wrap_svg_tag(format!("{bezier_svg}{outline_svg}"))
}
pub fn arcs(&self, error: f64, max_iterations: usize, maximize_arcs: WasmMaximizeArcs) -> String {
let original_curve_svg = self.get_bezier_path();
// Get sectors
let strategy = convert_wasm_maximize_arcs(maximize_arcs);
let options = ArcsOptions { error, max_iterations, strategy };
let arcs_svg = self
.0
.arcs(options)
.iter()
.enumerate()
.map(|(idx, sector)| {
draw_sector(
sector.center,
sector.radius,
-sector.start_angle,
-sector.end_angle,
format!("hsl({}, 100%, 50%, 75%)", (40 * idx)).as_str(),
1.,
format!("hsl({}, 100%, 50%, 37.5%)", (40 * idx)).as_str(),
)
})
.fold(original_curve_svg, |acc, item| format!("{acc}{item}"));
wrap_svg_tag(arcs_svg)
}
pub fn join(&self, js_points: &JsValue) -> String {
let array = js_points.to_owned().dyn_into::<Array>().unwrap();
let other_bezier: Bezier = match self.0.get_points().count() {
2 => {
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
Bezier::from_linear_dvec2(point1, point2)
}
3 => {
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
let point3 = parse_point(&array.get(2));
Bezier::from_quadratic_dvec2(point1, point2, point3)
}
4 => {
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
let point3 = parse_point(&array.get(2));
let point4 = parse_point(&array.get(3));
Bezier::from_cubic_dvec2(point1, point2, point3, point4)
}
_ => unreachable!(),
};
let mut other_bezier_svg = String::new();
other_bezier.to_svg(
&mut other_bezier_svg,
CURVE_ATTRIBUTES.to_string().replace(BLACK, GRAY),
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, GRAY),
String::new(),
String::new(),
);
let joining_bezier: Bezier = self.0.join(&other_bezier);
let mut joining_bezier_svg = String::new();
joining_bezier.to_svg(
&mut joining_bezier_svg,
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED),
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED),
String::new(),
String::new(),
);
let bezier_svg = self.get_bezier_path();
wrap_svg_tag(format!("{bezier_svg}{joining_bezier_svg}{other_bezier_svg}"))
}
}
@@ -1,71 +0,0 @@
pub mod bezier;
pub mod subpath;
mod svg_drawing;
mod utils;
use wasm_bindgen::prelude::*;
#[cfg(feature = "logging")]
pub static LOGGER: WasmLog = WasmLog;
#[cfg(feature = "logging")]
thread_local! { pub static HAS_CRASHED: std::cell::RefCell<bool> = const { std::cell::RefCell::new(false) } }
/// Initialize the backend
#[wasm_bindgen(start)]
pub fn init() {
#[cfg(feature = "logging")]
{
// Set up the logger with a default level of debug
log::set_logger(&LOGGER).expect("Failed to set logger");
log::set_max_level(log::LevelFilter::Trace);
fn panic_hook(info: &std::panic::PanicHookInfo<'_>) {
// Skip if we have already panicked
if HAS_CRASHED.with(|cell| cell.replace(true)) {
return;
}
log::error!("{}", info);
}
std::panic::set_hook(Box::new(panic_hook));
}
}
/// Logging to the JS console
#[cfg(feature = "logging")]
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(msg: &str, format: &str);
#[wasm_bindgen(js_namespace = console)]
fn info(msg: &str, format: &str);
#[wasm_bindgen(js_namespace = console)]
fn warn(msg: &str, format: &str);
#[wasm_bindgen(js_namespace = console)]
fn error(msg: &str, format: &str);
}
#[cfg(feature = "logging")]
#[derive(Default)]
pub struct WasmLog;
#[cfg(feature = "logging")]
impl log::Log for WasmLog {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() <= log::Level::Info
}
fn log(&self, record: &log::Record) {
let (log, name, color): (fn(&str, &str), &str, &str) = match record.level() {
log::Level::Trace => (log, "trace", "color:plum"),
log::Level::Debug => (log, "debug", "color:cyan"),
log::Level::Warn => (warn, "warn", "color:goldenrod"),
log::Level::Info => (info, "info", "color:mediumseagreen"),
log::Level::Error => (error, "error", "color:red"),
};
let msg = &format!("%c{}\t{}", name, record.args());
log(msg, color)
}
fn flush(&self) {}
}
@@ -1,585 +0,0 @@
use crate::svg_drawing::*;
use crate::utils::{parse_cap, parse_join, parse_point};
use bezier_rs::{Bezier, ManipulatorGroup, Subpath, SubpathTValue, TValueType};
use glam::DVec2;
use js_sys::Array;
use js_sys::Math;
use std::fmt::Write;
use wasm_bindgen::prelude::*;
use wasm_bindgen::{JsCast, JsValue};
#[derive(Clone, PartialEq, Hash)]
pub(crate) struct EmptyId;
impl bezier_rs::Identifier for EmptyId {
fn new() -> Self {
Self
}
}
/// Wrapper of the `Subpath` struct to be used in JS.
#[wasm_bindgen]
pub struct WasmSubpath(Subpath<EmptyId>);
const SCALE_UNIT_VECTOR_FACTOR: f64 = 50.;
fn parse_t_variant(t_variant: &String, t: f64) -> SubpathTValue {
match t_variant.as_str() {
"GlobalParametric" => SubpathTValue::GlobalParametric(t),
"GlobalEuclidean" => SubpathTValue::GlobalEuclidean(t),
_ => panic!("Unexpected TValue string: '{t_variant}'"),
}
}
#[wasm_bindgen]
impl WasmSubpath {
/// Expects js_points to be an unbounded list of triples, where each item is a tuple of floats.
/// The input TypeScript type is: (number[] | undefined)[][]
pub fn from_triples(js_points: JsValue, closed: bool) -> WasmSubpath {
let point_triples = js_points
.dyn_into::<Array>()
.unwrap()
.iter()
.map(|manipulator_group| {
let triple = manipulator_group.dyn_into::<Array>().unwrap();
let anchor = parse_point(&triple.get(0));
let in_handle = if triple.get(1).is_falsy() { None } else { Some(parse_point(&triple.get(1))) };
let out_handle = if triple.get(2).is_falsy() { None } else { Some(parse_point(&triple.get(2))) };
[Some(anchor), in_handle, out_handle]
})
.collect::<Vec<_>>();
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],
id: EmptyId,
})
.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.to_default_svg(), SVG_CLOSE_TAG)
}
fn to_default_svg(&self) -> String {
let mut subpath_svg = String::new();
self.0.to_svg(
&mut subpath_svg,
CURVE_ATTRIBUTES.to_string(),
ANCHOR_ATTRIBUTES.to_string(),
HANDLE_ATTRIBUTES.to_string(),
HANDLE_LINE_ATTRIBUTES.to_string(),
);
subpath_svg
}
fn to_filled_svg(&self) -> String {
let mut subpath_svg = String::new();
self.0.to_svg(
&mut subpath_svg,
CURVE_FILLED_ATTRIBUTES.to_string(),
ANCHOR_ATTRIBUTES.to_string(),
HANDLE_ATTRIBUTES.to_string(),
HANDLE_LINE_ATTRIBUTES.to_string(),
);
subpath_svg
}
pub fn insert(&self, t: f64, t_variant: String) -> String {
let mut subpath = self.0.clone();
let t = parse_t_variant(&t_variant, t);
subpath.insert(t);
let point = self.0.evaluate(t);
let point_text = draw_circle(point, 4., RED, 1.5, WHITE);
wrap_svg_tag(format!("{}{}", WasmSubpath(subpath).to_default_svg(), point_text))
}
pub fn length(&self) -> String {
let length_text = draw_text(format!("Length: {:.2}", self.0.length(None)), 5., 193., BLACK);
wrap_svg_tag(format!("{}{}", self.to_default_svg(), length_text))
}
pub fn length_centroid(&self) -> String {
let centroid = self.0.length_centroid(None, true).unwrap();
let point_text = draw_circle(centroid, 4., RED, 1.5, WHITE);
wrap_svg_tag(format!("{}{}", self.to_default_svg(), point_text))
}
pub fn area(&self, error: f64, minimum_separation: f64) -> String {
let area_text = draw_text(format!("Area: {}", self.0.area(Some(error), Some(minimum_separation))), 5., 193., BLACK);
wrap_svg_tag(format!("{}{}", self.to_filled_svg(), area_text))
}
pub fn area_centroid(&self, error: f64, minimum_separation: f64) -> String {
let point_text = draw_circle(self.0.area_centroid(Some(error), Some(minimum_separation), None).unwrap(), 4., RED, 1.5, WHITE);
wrap_svg_tag(format!("{}{}", self.to_filled_svg(), point_text))
}
pub fn poisson_disk_points(&self, separation_disk_diameter: f64) -> String {
let r = separation_disk_diameter / 2.;
let subpath_svg = self.to_default_svg();
let points = self
.0
.poisson_disk_points(separation_disk_diameter, Math::random, &[(self.0.clone(), self.0.bounding_box().unwrap())], 0);
let points_style = format!("<style class=\"poisson\">style.poisson ~ circle {{ fill: {RED}; opacity: 0.25; }}</style>");
let content = points
.iter()
.map(|point| format!("<circle cx=\"{}\" cy=\"{}\" r=\"{r}\" />", point.x, point.y))
.collect::<Vec<_>>()
.join("");
wrap_svg_tag(format!("{subpath_svg}{points_style}{content}"))
}
pub fn evaluate(&self, t: f64, t_variant: String) -> String {
let t = parse_t_variant(&t_variant, t);
let point = self.0.evaluate(t);
let point_text = draw_circle(point, 4., RED, 1.5, WHITE);
wrap_svg_tag(format!("{}{}", self.to_default_svg(), point_text))
}
pub fn compute_lookup_table(&self, steps: usize, t_variant: String) -> String {
let subpath = self.to_default_svg();
let tvalue_type = match t_variant.as_str() {
"GlobalParametric" => TValueType::Parametric,
"GlobalEuclidean" => TValueType::Euclidean,
_ => panic!("Unexpected TValue string: '{t_variant}'"),
};
let table_values: Vec<DVec2> = self.0.compute_lookup_table(Some(steps), Some(tvalue_type));
let circles: String = table_values
.iter()
.map(|point| draw_circle(*point, 3., RED, 1.5, WHITE))
.fold("".to_string(), |acc, circle| acc + &circle);
let content = format!("{subpath}{circles}");
wrap_svg_tag(content)
}
pub fn tangent(&self, t: f64, t_variant: String) -> String {
let t = parse_t_variant(&t_variant, t);
let intersection_point = self.0.evaluate(t);
let tangent_point = self.0.tangent(t);
let tangent_end = intersection_point + tangent_point * SCALE_UNIT_VECTOR_FACTOR;
let point_text = draw_circle(intersection_point, 4., RED, 1.5, WHITE);
let line_text = draw_line(intersection_point.x, intersection_point.y, tangent_end.x, tangent_end.y, RED, 1.);
let tangent_end_point = draw_circle(tangent_end, 3., RED, 1., WHITE);
wrap_svg_tag(format!("{}{}{}{}", self.to_default_svg(), point_text, line_text, tangent_end_point))
}
pub fn normal(&self, t: f64, t_variant: String) -> String {
let t = parse_t_variant(&t_variant, t);
let intersection_point = self.0.evaluate(t);
let normal_point = self.0.normal(t);
let normal_end = intersection_point + normal_point * SCALE_UNIT_VECTOR_FACTOR;
let point_text = draw_circle(intersection_point, 4., RED, 1.5, WHITE);
let line_text = draw_line(intersection_point.x, intersection_point.y, normal_end.x, normal_end.y, RED, 1.);
let normal_end_point = draw_circle(normal_end, 3., RED, 1., WHITE);
wrap_svg_tag(format!("{}{}{}{}", self.to_default_svg(), point_text, line_text, normal_end_point))
}
pub fn local_extrema(&self) -> String {
let local_extrema: [Vec<f64>; 2] = self.0.local_extrema();
let bezier = self.to_default_svg();
let circles: String = local_extrema
.iter()
.zip([RED, GREEN])
.flat_map(|(t_value_list, color)| {
t_value_list.iter().map(|&t_value| {
let point = self.0.evaluate(SubpathTValue::GlobalParametric(t_value));
draw_circle(point, 3., color, 1.5, WHITE)
})
})
.fold("".to_string(), |acc, circle| acc + &circle);
let content = format!(
"{bezier}{circles}{}{}",
draw_text("X extrema".to_string(), TEXT_OFFSET_X, TEXT_OFFSET_Y - 20., RED),
draw_text("Y extrema".to_string(), TEXT_OFFSET_X, TEXT_OFFSET_Y, GREEN),
);
wrap_svg_tag(content)
}
pub fn bounding_box(&self) -> String {
let subpath_svg = self.to_default_svg();
let bounding_box = self.0.bounding_box();
match bounding_box {
None => wrap_svg_tag(subpath_svg),
Some(bounding_box) => {
let content = format!(
"{subpath_svg}<rect x={} y={} width=\"{}\" height=\"{}\" style=\"fill:{NONE};stroke:{RED};stroke-width:1\" />",
bounding_box[0].x,
bounding_box[0].y,
bounding_box[1].x - bounding_box[0].x,
bounding_box[1].y - bounding_box[0].y,
);
wrap_svg_tag(content)
}
}
}
pub fn inflections(&self) -> String {
let inflections: Vec<f64> = self.0.inflections();
let bezier = self.to_default_svg();
let circles: String = inflections
.iter()
.map(|&t_value| {
let point = self.0.evaluate(SubpathTValue::GlobalParametric(t_value));
draw_circle(point, 3., RED, 1.5, WHITE)
})
.fold("".to_string(), |acc, circle| acc + &circle);
let content = format!("{bezier}{circles}");
wrap_svg_tag(content)
}
pub fn rotate(&self, angle: f64, pivot_x: f64, pivot_y: f64) -> String {
let subpath_svg = self.to_default_svg();
let rotated_subpath = self.0.rotate_about_point(angle, DVec2::new(pivot_x, pivot_y));
let mut rotated_subpath_svg = String::new();
rotated_subpath.to_svg(&mut rotated_subpath_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
let pivot = draw_circle(DVec2::new(pivot_x, pivot_y), 3., GRAY, 1.5, WHITE);
// Line between pivot and start point on curve
let original_dashed_line = format!(
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
self.0.iter().next().unwrap().start().x,
self.0.iter().next().unwrap().start().y
);
let rotated_dashed_line = format!(
r#"<line x1="{pivot_x}" y1="{pivot_y}" x2="{}" y2="{}" stroke="{ORANGE}" stroke-dasharray="0, 4" stroke-width="2" stroke-linecap="round"/>"#,
rotated_subpath.iter().next().unwrap().start().x,
rotated_subpath.iter().next().unwrap().start().y
);
wrap_svg_tag(format!("{subpath_svg}{rotated_subpath_svg}{pivot}{original_dashed_line}{rotated_dashed_line}"))
}
pub fn project(&self, x: f64, y: f64) -> String {
let (segment_index, projected_t) = self.0.project(DVec2::new(x, y)).unwrap();
let projected_point = self.0.evaluate(SubpathTValue::Parametric { segment_index, t: projected_t });
let subpath_svg = self.to_default_svg();
let content = format!("{subpath_svg}{}", draw_line(projected_point.x, projected_point.y, x, y, RED, 1.),);
wrap_svg_tag(content)
}
pub fn intersect_line_segment(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String {
let array = js_points.dyn_into::<Array>().unwrap();
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
let line = Bezier::from_linear_dvec2(point1, point2);
let subpath_svg = self.to_default_svg();
let empty_string = String::new();
let mut line_svg = String::new();
line.to_svg(
&mut line_svg,
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED),
empty_string.clone(),
empty_string.clone(),
empty_string,
);
let intersections_svg = self
.0
.intersections(&line, Some(error), Some(minimum_separation))
.iter()
.map(|(segment_index, intersection_t)| {
let point = self.0.evaluate(SubpathTValue::Parametric {
segment_index: *segment_index,
t: *intersection_t,
});
draw_circle(point, 4., RED, 1.5, WHITE)
})
.fold(String::new(), |acc, item| format!("{acc}{item}"));
wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}"))
}
pub fn intersect_quadratic_segment(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String {
let array = js_points.dyn_into::<Array>().unwrap();
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
let point3 = parse_point(&array.get(2));
let line = Bezier::from_quadratic_dvec2(point1, point2, point3);
let subpath_svg = self.to_default_svg();
let empty_string = String::new();
let mut line_svg = String::new();
line.to_svg(
&mut line_svg,
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED),
empty_string.clone(),
empty_string.clone(),
empty_string,
);
let intersections_svg = self
.0
.intersections(&line, Some(error), Some(minimum_separation))
.iter()
.map(|(segment_index, intersection_t)| {
let point = self.0.evaluate(SubpathTValue::Parametric {
segment_index: *segment_index,
t: *intersection_t,
});
draw_circle(point, 4., RED, 1.5, WHITE)
})
.fold(String::new(), |acc, item| format!("{acc}{item}"));
wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}"))
}
pub fn intersect_cubic_segment(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String {
let array = js_points.dyn_into::<Array>().unwrap();
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
let point3 = parse_point(&array.get(2));
let point4 = parse_point(&array.get(3));
let line = Bezier::from_cubic_dvec2(point1, point2, point3, point4);
let subpath_svg = self.to_default_svg();
let empty_string = String::new();
let mut line_svg = String::new();
line.to_svg(
&mut line_svg,
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED),
empty_string.clone(),
empty_string.clone(),
empty_string,
);
let intersections_svg = self
.0
.intersections(&line, Some(error), Some(minimum_separation))
.iter()
.map(|(segment_index, intersection_t)| {
let point = self.0.evaluate(SubpathTValue::Parametric {
segment_index: *segment_index,
t: *intersection_t,
});
draw_circle(point, 4., RED, 1.5, WHITE)
})
.fold(String::new(), |acc, item| format!("{acc}{item}"));
wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}"))
}
pub fn self_intersections(&self, error: f64, minimum_separation: f64) -> String {
let subpath_svg = self.to_default_svg();
let self_intersections_svg = self
.0
.self_intersections(Some(error), Some(minimum_separation))
.iter()
.map(|(segment_index, intersection_t)| {
let point = self.0.evaluate(SubpathTValue::Parametric {
segment_index: *segment_index,
t: *intersection_t,
});
draw_circle(point, 4., RED, 1.5, WHITE)
})
.fold(String::new(), |acc, item| format!("{acc}{item}"));
wrap_svg_tag(format!("{subpath_svg}{self_intersections_svg}"))
}
pub fn intersect_rectangle(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String {
let array = js_points.dyn_into::<Array>().unwrap();
let point1 = parse_point(&array.get(0));
let point2 = parse_point(&array.get(1));
let subpath_svg = self.to_default_svg();
let mut rectangle_svg = String::new();
[
Bezier::from_linear_coordinates(point1.x, point1.y, point2.x, point1.y),
Bezier::from_linear_coordinates(point2.x, point1.y, point2.x, point2.y),
Bezier::from_linear_coordinates(point2.x, point2.y, point1.x, point2.y),
Bezier::from_linear_coordinates(point1.x, point2.y, point1.x, point1.y),
]
.iter()
.for_each(|line| line.to_svg(&mut rectangle_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new()));
let intersections_svg = self
.0
.rectangle_intersections(point1, point2, Some(error), Some(minimum_separation))
.iter()
.map(|(segment_index, intersection_t)| {
let point = self.0.evaluate(SubpathTValue::Parametric {
segment_index: *segment_index,
t: *intersection_t,
});
draw_circle(point, 4., RED, 1.5, WHITE)
})
.fold(String::new(), |acc, item| format!("{acc}{item}"));
wrap_svg_tag(format!("{subpath_svg}{rectangle_svg}{intersections_svg}"))
}
pub fn inside_subpath(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String {
let array = js_points.dyn_into::<Array>().unwrap();
let points = array.iter().map(|p| parse_point(&p));
let other = Subpath::<EmptyId>::from_anchors(points, true);
let is_inside = self.0.is_inside_subpath(&other, Some(error), Some(minimum_separation));
let color = if is_inside { RED } else { BLACK };
let self_svg = self.to_default_svg();
let mut other_svg = String::new();
other.curve_to_svg(&mut other_svg, CURVE_ATTRIBUTES.replace(BLACK, color));
wrap_svg_tag(format!("{self_svg}{other_svg}"))
}
pub fn curvature(&self, t: f64, t_variant: String) -> String {
let subpath = self.to_default_svg();
let t = parse_t_variant(&t_variant, t);
let intersection_point = self.0.evaluate(t);
let normal_point = self.0.normal(t);
let curvature = self.0.curvature(t);
let content = if curvature.abs() < 0.000001 {
// Linear curve segment: the radius is infinite so we don't draw it
format!("{subpath}{}", draw_circle(intersection_point, 3., RED, 1., WHITE))
} else {
let radius = 1. / curvature;
let curvature_center = intersection_point + normal_point * radius;
format!(
"{subpath}{}{}{}{}",
draw_circle(curvature_center, radius.abs(), RED, 1., NONE),
draw_line(intersection_point.x, intersection_point.y, curvature_center.x, curvature_center.y, RED, 1.),
draw_circle(intersection_point, 3., RED, 1., WHITE),
draw_circle(curvature_center, 3., RED, 1., WHITE),
)
};
wrap_svg_tag(content)
}
pub fn split(&self, t: f64, t_variant: String) -> String {
let t = parse_t_variant(&t_variant, t);
let (main_subpath, optional_subpath) = self.0.split(t);
let mut main_subpath_svg = String::new();
let mut other_subpath_svg = String::new();
if optional_subpath.is_some() {
main_subpath.to_svg(
&mut main_subpath_svg,
CURVE_ATTRIBUTES.to_string().replace(BLACK, ORANGE).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"",
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, ORANGE),
HANDLE_ATTRIBUTES.to_string().replace(GRAY, ORANGE),
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, ORANGE),
);
} else {
main_subpath.iter().enumerate().for_each(|(index, bezier)| {
let hue1 = &format!("hsla({}, 100%, 50%, 0.5)", 40 * index);
let hue2 = &format!("hsla({}, 100%, 50%, 0.5)", 40 * (index + 1));
let gradient_id = &format!("gradient{index}");
let start = bezier.start();
let end = bezier.end();
let _ = write!(
main_subpath_svg,
r#"<defs><linearGradient id="{}" x1="{}%" y1="{}%" x2="{}%" y2="{}%"><stop offset="0%" stop-color="{}"/><stop offset="100%" stop-color="{}"/></linearGradient></defs>"#,
gradient_id,
start.x / 2.,
start.y / 2.,
end.x / 2.,
end.y / 2.,
hue1,
hue2
);
let stroke = &format!("url(#{gradient_id})");
bezier.curve_to_svg(
&mut main_subpath_svg,
CURVE_ATTRIBUTES.to_string().replace(BLACK, stroke).replace("stroke-width=\"2\"", "stroke-width=\"8\""),
);
bezier.anchors_to_svg(&mut main_subpath_svg, ANCHOR_ATTRIBUTES.to_string().replace(BLACK, hue1));
bezier.handles_to_svg(&mut main_subpath_svg, HANDLE_ATTRIBUTES.to_string().replace(GRAY, hue1));
bezier.handle_lines_to_svg(&mut main_subpath_svg, HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, hue1));
});
}
if let Some(subpath) = optional_subpath {
subpath.to_svg(
&mut other_subpath_svg,
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"",
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED),
HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED),
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED),
);
}
wrap_svg_tag(format!("{}{}{}", self.to_default_svg(), main_subpath_svg, other_subpath_svg))
}
pub fn trim(&self, t1: f64, t2: f64, t_variant: String) -> String {
let t1 = parse_t_variant(&t_variant, t1);
let t2 = parse_t_variant(&t_variant, t2);
let trimmed_subpath = self.0.trim(t1, t2);
let mut trimmed_subpath_svg = String::new();
trimmed_subpath.to_svg(
&mut trimmed_subpath_svg,
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"",
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED),
HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED),
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED),
);
wrap_svg_tag(format!("{}{}", self.to_default_svg(), trimmed_subpath_svg))
}
pub fn offset(&self, distance: f64, join: i32, miter_limit: f64) -> String {
let join = parse_join(join, miter_limit);
let offset_subpath = self.0.offset(distance, join);
let mut offset_svg = String::new();
offset_subpath.to_svg(&mut offset_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
wrap_svg_tag(format!("{}{offset_svg}", self.to_default_svg()))
}
pub fn outline(&self, distance: f64, join: i32, cap: i32, miter_limit: f64) -> String {
let join = parse_join(join, miter_limit);
let cap = parse_cap(cap);
let (outline_piece1, outline_piece2) = self.0.outline(distance, join, cap);
let mut outline_piece1_svg = String::new();
outline_piece1.to_svg(&mut outline_piece1_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
let mut outline_piece2_svg = String::new();
if let Some(outline) = outline_piece2 {
outline.to_svg(&mut outline_piece2_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
}
wrap_svg_tag(format!("{}{outline_piece1_svg}{outline_piece2_svg}", self.to_default_svg()))
}
}
@@ -1,69 +0,0 @@
use glam::DVec2;
// SVG drawing constants
pub const SVG_OPEN_TAG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="250px" height="200px">"#;
pub const SVG_CLOSE_TAG: &str = "</svg>";
// Stylistic constants
pub const BLACK: &str = "black";
pub const WHITE: &str = "white";
pub const GRAY: &str = "gray";
pub const RED: &str = "red";
pub const ORANGE: &str = "orange";
// pub const PINK: &str = "pink";
pub const GREEN: &str = "green";
pub const NONE: &str = "none";
// Default attributes
pub const CURVE_ATTRIBUTES: &str = "stroke=\"black\" stroke-width=\"2\" fill=\"none\"";
pub const CURVE_FILLED_ATTRIBUTES: &str = "stroke=\"black\" stroke-width=\"2\" fill=\"lightgray\"";
pub const HANDLE_LINE_ATTRIBUTES: &str = "stroke=\"gray\" stroke-width=\"1\" fill=\"none\"";
pub const ANCHOR_ATTRIBUTES: &str = "r=\"4\" stroke=\"black\" stroke-width=\"2\" fill=\"white\"";
pub const HANDLE_ATTRIBUTES: &str = "r=\"3\" stroke=\"gray\" stroke-width=\"1.5\" fill=\"white\"";
// Text constants
pub const TEXT_OFFSET_X: f64 = 5.;
pub const TEXT_OFFSET_Y: f64 = 193.;
pub fn wrap_svg_tag(contents: String) -> String {
format!("{SVG_OPEN_TAG}{contents}{SVG_CLOSE_TAG}")
}
/// Helper function to create an SVG text entity.
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}" font-family="monospace">{text}</text>"#)
}
/// Helper function to create an SVG circle entity.
pub fn draw_circle(position: DVec2, radius: f64, stroke: &str, stroke_width: f64, fill: &str) -> String {
format!(
r#"<circle cx="{}" cy="{}" r="{radius}" stroke="{stroke}" stroke-width="{stroke_width}" fill="{fill}"/>"#,
position.x, position.y
)
}
/// Helper function to create an SVG circle entity.
pub fn draw_line(start_x: f64, start_y: f64, end_x: f64, end_y: f64, stroke: &str, stroke_width: f64) -> String {
format!(r#"<line x1="{start_x}" y1="{start_y}" x2="{end_x}" y2="{end_y}" stroke="{stroke}" stroke-width="{stroke_width}"/>"#)
}
// Helper function to convert polar to cartesian coordinates
fn polar_to_cartesian(center_x: f64, center_y: f64, radius: f64, angle_in_rad: f64) -> [f64; 2] {
let x = center_x + radius * angle_in_rad.cos();
let y = center_y + radius * -angle_in_rad.sin();
[x, y]
}
// Helper function to create an SVG drawing of a sector
pub fn draw_sector(center: DVec2, radius: f64, start_angle: f64, end_angle: f64, stroke: &str, stroke_width: f64, fill: &str) -> String {
let [start_x, start_y] = polar_to_cartesian(center.x, center.y, radius, start_angle);
let [end_x, end_y] = polar_to_cartesian(center.x, center.y, radius, end_angle);
// draw sector with fill color
let sector_svg = format!(
r#"<path d="M {start_x} {start_y} A {radius} {radius} 0 0 1 {end_x} {end_y} L {} {} L {start_x} {start_y} Z" stroke="none" fill="{fill}" />"#,
center.x, center.y
);
// draw arc with stroke color
let arc_svg = format!(r#"<path d="M {start_x} {start_y} A {radius} {radius} 0 0 1 {end_x} {end_y}" stroke="{stroke}" stroke-width="{stroke_width}" fill="none"/>"#);
format!("{sector_svg}{arc_svg}")
}
@@ -1,29 +0,0 @@
use bezier_rs::{Cap, Join};
use glam::DVec2;
use js_sys::Array;
use wasm_bindgen::{JsCast, JsValue};
pub fn parse_join(join: i32, miter_limit: f64) -> Join {
match join {
0 => Join::Bevel,
1 => Join::Miter(Some(miter_limit)),
2 => Join::Round,
_ => panic!("Unexpected Join value: '{join}'"),
}
}
pub fn parse_cap(cap: i32) -> Cap {
match cap {
0 => Cap::Butt,
1 => Cap::Round,
2 => Cap::Square,
_ => panic!("Unexpected Cap value: '{cap}'"),
}
}
pub fn parse_point(js_point: &JsValue) -> DVec2 {
let point = js_point.to_owned().dyn_into::<Array>().unwrap();
let x = point.get(0).as_f64().unwrap();
let y = point.get(1).as_f64().unwrap();
DVec2::new(x, y)
}
-34
View File
@@ -1,34 +0,0 @@
#!/bin/sh
set -e # Exit with nonzero exit code if any individual command fails throughout the script
echo 📁 Create output directory in 'website/other/dist'
cd website/other
mkdir dist
echo 🔧 Install the latest Rust
curl https://sh.rustup.rs -sSf | sh -s -- -y
export PATH=$PATH:/opt/buildhome/.cargo/bin
rustup update stable
echo rustc version:
rustc --version
echo 📦 Install wasm-pack
cargo install wasm-pack
echo wasm-pack version:
wasm-pack --version
echo 🚧 Print installed node and npm versions
echo node version:
node --version
echo npm version:
npm --version
echo 👷 Build Bezier-rs demos to 'website/other/dist/libraries/bezier-rs'
mkdir dist/libraries
mkdir dist/libraries/bezier-rs
cd bezier-rs-demos
npm ci
NODE_ENV=production npm run build
cp ../../static/fonts/common.css dist/fonts.css
mv dist/* ../dist/libraries/bezier-rs
cd ..
+2 -1
View File
@@ -10,7 +10,8 @@
"license": "Apache-2.0",
"homepage": "https://graphite.rs",
"scripts": {
"install-fonts": "npm ci && node install-fonts.js"
"install-fonts": "npm ci && node build-scripts/install-fonts.js",
"generate-editor-structure": "node build-scripts/generate-editor-structure.js ../hierarchical_message_system_tree.txt ../hierarchical_message_system_tree.html"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.7.0",
+3 -7
View File
@@ -40,16 +40,12 @@
{% endmacro text_balancer %}
{% macro hierarchical_message_system_tree() %}
{%- set content = load_data(path = "other/editor-structure/replacement.html", format = "plain", required = false) -%}
{%- set content = load_data(path = "../../hierarchical_message_system_tree.html", format = "plain", required = false) -%}
{%- set fallback = "<pre>THIS CONTENT IS FILLED IN WHEN CI BUILDS THE WEBSITE.
TO TEST IT LOCALLY, RUN:
cd website/other/editor-structure
AND THEN:
TO TEST IT LOCALLY, FROM THE `website` DIRECTORY, RUN:
cargo test --package graphite-editor --lib -- messages::message::test::generate_message_tree
node generate.js ../../../hierarchical_message_system_tree.txt replacement.html</pre>" -%}
npm run generate-editor-structure</pre>" -%}
{{ content | default(value = fallback) | safe }}
{% endmacro hierarchical_message_system_tree %}