mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
Merge branch 'asdf' into tauri-restructure-lite
This commit is contained in:
commit
49a0a57975
29 changed files with 1103 additions and 238 deletions
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
@ -50,7 +50,8 @@ jobs:
|
|||
NODE_ENV: production
|
||||
run: |
|
||||
cd frontend
|
||||
npm run lint
|
||||
# npm run lint
|
||||
echo "Currently skipping linting, it should be reenabled after switching to Svelte."
|
||||
|
||||
- name: 🔬 Check Rust formatting
|
||||
run: |
|
||||
|
|
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -365,6 +365,12 @@ dependencies = [
|
|||
"dyn-any",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boxcar"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38c99613cb3cd7429889a08dfcf651721ca971c86afa30798461f8eee994de47"
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "3.3.4"
|
||||
|
@ -1687,6 +1693,7 @@ dependencies = [
|
|||
"autoquant",
|
||||
"bezier-rs",
|
||||
"borrow_stack",
|
||||
"boxcar",
|
||||
"bytemuck",
|
||||
"compilation-client",
|
||||
"dyn-any",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<svg width="937" height="240" viewBox="0 0 937 240" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<svg viewBox="0 0 937 240" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<path d="M934.29,139.3c-3.08,2.94-6.82,5.09-10.91,6.27c-3.49,1.06-7.1,1.63-10.74,1.71c-6.08,0.08-11.98-2.06-16.6-6.02c-4.78-4.01-7.49-10.63-8.14-19.86l48.01-6.02c0-8.68-2.58-15.71-7.73-21.08c-5.16-5.37-12.72-8.06-22.7-8.06c-7.19-0.04-14.29,1.57-20.75,4.72c-6.37,3.07-11.75,7.86-15.54,13.83c-3.91,6.08-5.86,13.46-5.86,22.14c0,8.03,1.76,14.98,5.29,20.83c3.41,5.76,8.38,10.44,14.32,13.51c6.21,3.19,13.11,4.81,20.1,4.72c9.01,0,16.14-2.2,21.41-6.59c5.51-4.74,9.78-10.74,12.45-17.5L934.29,139.3z M891.64,99.01c2.28-3.85,5.26-5.78,8.95-5.78c3.79,0,6.48,1.84,8.06,5.53c1.68,4.2,2.59,8.66,2.69,13.18l-23.6,2.93C888.06,108.15,889.37,102.86,891.64,99.01" />
|
||||
<path d="M844.61,151.33c-7.06,0-10.58-4.34-10.58-13.02v-34.5c0-4.34,2.17-6.51,6.51-6.51h14.65v-8.62h-21.16c0-4.12,0.05-8.19,0.16-12.21c0.11-4.01,0.59-11.63,0.91-15.76l-25.49,11.81v16.16h-9.77v8.62h9.77v44.27c0,7.16,2.01,13.02,6.02,17.58c4.01,4.56,9.87,6.83,17.58,6.84c4.07,0.13,8.11-0.71,11.8-2.44c3.03-1.49,5.72-3.6,7.89-6.18c1.98-2.37,3.62-5,4.88-7.81l-2.6-2.6C852.42,149.81,848.59,151.4,844.61,151.33" />
|
||||
<path d="M783.25,154.67c-0.64-2.97-0.91-6-0.81-9.03v-38.9c0-5.21,0.08-9.52,0.24-12.94s0.3-5.94,0.41-7.57l-0.98-0.98l-35.48,16.44l1.63,3.74c1.09-0.4,2.2-0.73,3.34-0.98c0.94-0.21,1.89-0.31,2.85-0.32c0.97-0.07,1.92,0.22,2.69,0.81c0.59,0.54,0.89,1.63,0.9,3.26v37.43c0.08,3.03-0.14,6.05-0.65,9.03c-0.44,2.01-1.2,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.39,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.39-6.02-1.14C784.64,157.85,783.56,156.38,783.25,154.67 M771.04,77.28c3.74,0.07,7.35-1.44,9.93-4.15c2.64-2.59,4.11-6.15,4.07-9.85c0.03-3.72-1.44-7.3-4.07-9.93c-2.56-2.75-6.17-4.29-9.93-4.23c-3.81-0.09-7.48,1.45-10.09,4.23c-2.64,2.63-4.1,6.21-4.07,9.93c0.02,7.75,6.32,14.02,14.07,14C770.98,77.29,771.01,77.29,771.04,77.28" />
|
||||
|
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
@ -5,7 +5,7 @@
|
|||
"description": "Graphite's web app frontend. Planned to be replaced by a native GUI written in Rust in the future.",
|
||||
"author": "Graphite Authors <contact@graphite.rs>",
|
||||
"scripts": {
|
||||
"dev": "cargo watch -s \"wasm-pack build frontend-svelte/wasm --dev\" & vite || kill $!",
|
||||
"dev": "concurrently -k --handle-input \"vite\" \"npm run watch:wasm\"",
|
||||
"lint": "eslint src",
|
||||
"build": "npm run build-wasm && vite build",
|
||||
"build-wasm": "wasm-pack build ./wasm --release",
|
||||
|
@ -29,6 +29,7 @@
|
|||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"concurrently": "^7.6.0",
|
||||
"prettier": "^2.8.2",
|
||||
"rollup-plugin-license": "^3.0.1",
|
||||
"sass": "^1.57.1",
|
||||
|
|
|
@ -24,4 +24,7 @@ export default defineConfig({
|
|||
],
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 8080
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<svg width="937" height="240" viewBox="0 0 937 240" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<svg viewBox="0 0 937 240" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<path d="M934.29,139.3c-3.08,2.94-6.82,5.09-10.91,6.27c-3.49,1.06-7.1,1.63-10.74,1.71c-6.08,0.08-11.98-2.06-16.6-6.02c-4.78-4.01-7.49-10.63-8.14-19.86l48.01-6.02c0-8.68-2.58-15.71-7.73-21.08c-5.16-5.37-12.72-8.06-22.7-8.06c-7.19-0.04-14.29,1.57-20.75,4.72c-6.37,3.07-11.75,7.86-15.54,13.83c-3.91,6.08-5.86,13.46-5.86,22.14c0,8.03,1.76,14.98,5.29,20.83c3.41,5.76,8.38,10.44,14.32,13.51c6.21,3.19,13.11,4.81,20.1,4.72c9.01,0,16.14-2.2,21.41-6.59c5.51-4.74,9.78-10.74,12.45-17.5L934.29,139.3z M891.64,99.01c2.28-3.85,5.26-5.78,8.95-5.78c3.79,0,6.48,1.84,8.06,5.53c1.68,4.2,2.59,8.66,2.69,13.18l-23.6,2.93C888.06,108.15,889.37,102.86,891.64,99.01" />
|
||||
<path d="M844.61,151.33c-7.06,0-10.58-4.34-10.58-13.02v-34.5c0-4.34,2.17-6.51,6.51-6.51h14.65v-8.62h-21.16c0-4.12,0.05-8.19,0.16-12.21c0.11-4.01,0.59-11.63,0.91-15.76l-25.49,11.81v16.16h-9.77v8.62h9.77v44.27c0,7.16,2.01,13.02,6.02,17.58c4.01,4.56,9.87,6.83,17.58,6.84c4.07,0.13,8.11-0.71,11.8-2.44c3.03-1.49,5.72-3.6,7.89-6.18c1.98-2.37,3.62-5,4.88-7.81l-2.6-2.6C852.42,149.81,848.59,151.4,844.61,151.33" />
|
||||
<path d="M783.25,154.67c-0.64-2.97-0.91-6-0.81-9.03v-38.9c0-5.21,0.08-9.52,0.24-12.94s0.3-5.94,0.41-7.57l-0.98-0.98l-35.48,16.44l1.63,3.74c1.09-0.4,2.2-0.73,3.34-0.98c0.94-0.21,1.89-0.31,2.85-0.32c0.97-0.07,1.92,0.22,2.69,0.81c0.59,0.54,0.89,1.63,0.9,3.26v37.43c0.08,3.03-0.14,6.05-0.65,9.03c-0.44,2.01-1.2,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.39,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.39-6.02-1.14C784.64,157.85,783.56,156.38,783.25,154.67 M771.04,77.28c3.74,0.07,7.35-1.44,9.93-4.15c2.64-2.59,4.11-6.15,4.07-9.85c0.03-3.72-1.44-7.3-4.07-9.93c-2.56-2.75-6.17-4.29-9.93-4.23c-3.81-0.09-7.48,1.45-10.09,4.23c-2.64,2.63-4.1,6.21-4.07,9.93c0.02,7.75,6.32,14.02,14.07,14C770.98,77.29,771.01,77.29,771.04,77.28" />
|
||||
|
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
435
frontend/package-lock.json
generated
435
frontend/package-lock.json
generated
|
@ -23,6 +23,8 @@
|
|||
"@vue/compiler-sfc": "^3.2.31",
|
||||
"@vue/eslint-config-airbnb": "^6.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.2",
|
||||
"@wasm-tool/wasm-pack-plugin": "^1.6.0",
|
||||
"concurrently": "^7.6.0",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
|
@ -35,7 +37,8 @@
|
|||
"typescript": "^4.9.3",
|
||||
"vite": "^4.1.1",
|
||||
"vite-plugin-top-level-await": "^1.2.2",
|
||||
"vite-plugin-wasm": "^3.1.1",
|
||||
"vite-plugin-wasm": "^3.2.1",
|
||||
"vite-svg-loader": "^4.0.0",
|
||||
"vue-cli-plugin-tauri": "~1.0.0",
|
||||
"vue-loader": "^17.0.1"
|
||||
}
|
||||
|
@ -1082,9 +1085,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@sideway/formula": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
|
||||
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
|
||||
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
|
@ -2535,6 +2538,14 @@
|
|||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@wasm-tool/wasm-pack-plugin": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@wasm-tool/wasm-pack-plugin/-/wasm-pack-plugin-1.6.0.tgz",
|
||||
"integrity": "sha512-Iax4nEgIvVCZqrmuseJm7ln/muWpg7uT5fXMAT0crYo+k5JTuZE58DJvBQoeIAegA3IM9cZgfkcZjAOUCPsT1g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
|
||||
|
@ -3862,6 +3873,159 @@
|
|||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.6.0.tgz",
|
||||
"integrity": "sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"date-fns": "^2.29.1",
|
||||
"lodash": "^4.17.21",
|
||||
"rxjs": "^7.0.0",
|
||||
"shell-quote": "^1.7.3",
|
||||
"spawn-command": "^0.0.2-1",
|
||||
"supports-color": "^8.1.0",
|
||||
"tree-kill": "^1.2.2",
|
||||
"yargs": "^17.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/concurrently/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/yargs": {
|
||||
"version": "17.7.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz",
|
||||
"integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/confusing-browser-globals": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
|
||||
|
@ -4325,6 +4489,19 @@
|
|||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
}
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
|
@ -7809,9 +7986,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
|
@ -8025,9 +8202,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/loader-utils/node_modules/json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
|
@ -10775,6 +10952,15 @@
|
|||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz",
|
||||
"integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safari-14-idb-fix": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz",
|
||||
|
@ -11196,6 +11382,12 @@
|
|||
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
|
||||
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
|
||||
},
|
||||
"node_modules/spawn-command": {
|
||||
"version": "0.0.2-1",
|
||||
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
|
||||
"integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/spdx-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz",
|
||||
|
@ -11815,6 +12007,15 @@
|
|||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
|
||||
|
@ -11828,9 +12029,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths/node_modules/json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.0"
|
||||
|
@ -12302,6 +12503,7 @@
|
|||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-svg-loader/-/vite-svg-loader-4.0.0.tgz",
|
||||
"integrity": "sha512-0MMf1yzzSYlV4MGePsLVAOqXsbF5IVxbn4EEzqRnWxTQl8BJg/cfwIzfQNmNQxZp5XXwd4kyRKF1LytuHZTnqA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-sfc": "^3.2.20",
|
||||
"svgo": "^3.0.2"
|
||||
|
@ -12311,6 +12513,7 @@
|
|||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
|
@ -12319,6 +12522,7 @@
|
|||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
|
@ -12334,6 +12538,7 @@
|
|||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.30",
|
||||
"source-map-js": "^1.0.1"
|
||||
|
@ -12346,6 +12551,7 @@
|
|||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
|
||||
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"css-tree": "~2.2.0"
|
||||
},
|
||||
|
@ -12358,6 +12564,7 @@
|
|||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
|
||||
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.28",
|
||||
"source-map-js": "^1.0.1"
|
||||
|
@ -12370,12 +12577,14 @@
|
|||
"node_modules/vite-svg-loader/node_modules/csso/node_modules/mdn-data": {
|
||||
"version": "2.0.28",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
|
||||
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="
|
||||
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vite-svg-loader/node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
|
@ -12389,6 +12598,7 @@
|
|||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
|
@ -12403,6 +12613,7 @@
|
|||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz",
|
||||
"integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
|
@ -12416,6 +12627,7 @@
|
|||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz",
|
||||
"integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
|
@ -12426,12 +12638,14 @@
|
|||
"node_modules/vite-svg-loader/node_modules/mdn-data": {
|
||||
"version": "2.0.30",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vite-svg-loader/node_modules/svgo": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.2.tgz",
|
||||
"integrity": "sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@trysound/sax": "0.2.0",
|
||||
"commander": "^7.2.0",
|
||||
|
@ -14419,9 +14633,9 @@
|
|||
}
|
||||
},
|
||||
"@sideway/formula": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
|
||||
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
|
||||
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
|
@ -15449,6 +15663,14 @@
|
|||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"@wasm-tool/wasm-pack-plugin": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@wasm-tool/wasm-pack-plugin/-/wasm-pack-plugin-1.6.0.tgz",
|
||||
"integrity": "sha512-Iax4nEgIvVCZqrmuseJm7ln/muWpg7uT5fXMAT0crYo+k5JTuZE58DJvBQoeIAegA3IM9cZgfkcZjAOUCPsT1g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"@webassemblyjs/ast": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
|
||||
|
@ -16502,6 +16724,117 @@
|
|||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"concurrently": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.6.0.tgz",
|
||||
"integrity": "sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"date-fns": "^2.29.1",
|
||||
"lodash": "^4.17.21",
|
||||
"rxjs": "^7.0.0",
|
||||
"shell-quote": "^1.7.3",
|
||||
"spawn-command": "^0.0.2-1",
|
||||
"supports-color": "^8.1.0",
|
||||
"tree-kill": "^1.2.2",
|
||||
"yargs": "^17.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"yargs": {
|
||||
"version": "17.7.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz",
|
||||
"integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"confusing-browser-globals": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
|
||||
|
@ -16836,6 +17169,12 @@
|
|||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
|
||||
"dev": true
|
||||
},
|
||||
"de-indent": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
|
@ -19419,9 +19758,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true
|
||||
},
|
||||
"jsonfile": {
|
||||
|
@ -19590,9 +19929,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
|
@ -21643,6 +21982,15 @@
|
|||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"rxjs": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz",
|
||||
"integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"safari-14-idb-fix": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz",
|
||||
|
@ -21993,6 +22341,12 @@
|
|||
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
|
||||
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
|
||||
},
|
||||
"spawn-command": {
|
||||
"version": "0.0.2-1",
|
||||
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
|
||||
"integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==",
|
||||
"dev": true
|
||||
},
|
||||
"spdx-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz",
|
||||
|
@ -22478,6 +22832,12 @@
|
|||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true
|
||||
},
|
||||
"tsconfig-paths": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
|
||||
|
@ -22491,9 +22851,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.0"
|
||||
|
@ -22801,6 +23161,7 @@
|
|||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-svg-loader/-/vite-svg-loader-4.0.0.tgz",
|
||||
"integrity": "sha512-0MMf1yzzSYlV4MGePsLVAOqXsbF5IVxbn4EEzqRnWxTQl8BJg/cfwIzfQNmNQxZp5XXwd4kyRKF1LytuHZTnqA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@vue/compiler-sfc": "^3.2.20",
|
||||
"svgo": "^3.0.2"
|
||||
|
@ -22809,12 +23170,14 @@
|
|||
"commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"dev": true
|
||||
},
|
||||
"css-select": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
|
@ -22827,6 +23190,7 @@
|
|||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mdn-data": "2.0.30",
|
||||
"source-map-js": "^1.0.1"
|
||||
|
@ -22836,6 +23200,7 @@
|
|||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
|
||||
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"css-tree": "~2.2.0"
|
||||
},
|
||||
|
@ -22844,6 +23209,7 @@
|
|||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
|
||||
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mdn-data": "2.0.28",
|
||||
"source-map-js": "^1.0.1"
|
||||
|
@ -22852,7 +23218,8 @@
|
|||
"mdn-data": {
|
||||
"version": "2.0.28",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
|
||||
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="
|
||||
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -22860,6 +23227,7 @@
|
|||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
|
@ -22870,6 +23238,7 @@
|
|||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"domelementtype": "^2.3.0"
|
||||
}
|
||||
|
@ -22878,6 +23247,7 @@
|
|||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz",
|
||||
"integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
|
@ -22887,17 +23257,20 @@
|
|||
"entities": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz",
|
||||
"integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA=="
|
||||
"integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==",
|
||||
"dev": true
|
||||
},
|
||||
"mdn-data": {
|
||||
"version": "2.0.30",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
||||
"dev": true
|
||||
},
|
||||
"svgo": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.2.tgz",
|
||||
"integrity": "sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@trysound/sax": "0.2.0",
|
||||
"commander": "^7.2.0",
|
||||
|
|
|
@ -4,15 +4,16 @@
|
|||
"description": "Graphite's web app frontend. Planned to be replaced by a native GUI written in Rust in the future.",
|
||||
"author": "Graphite Authors <contact@graphite.rs>",
|
||||
"scripts": {
|
||||
"dev": "cargo watch -s \"wasm-pack build frontend/wasm --dev\" & vite || kill $!",
|
||||
"start": "npm run dev",
|
||||
"dev": "wasm-pack build ./wasm --dev && concurrently -k --handle-input \"vite\" \"npm run watch:wasm\" || (npm run print-building-help && exit 1)",
|
||||
"tauri:dev": "echo 'Make sure you build the wasm binary for tauri using `npm run tauri:build-wasm`' &&vite",
|
||||
"tauri:build-wasm": "wasm-pack build wasm --release -- --features tauri",
|
||||
"build": "npm run build-wasm && vite build || (npm run print-building-help && exit 1)",
|
||||
"lint": "eslint src || (npm run print-linting-help && exit 1)",
|
||||
"build-wasm": "wasm-pack build ./wasm --release",
|
||||
"watch:wasm": "cd wasm && cargo watch -s \"wasm-pack build --dev\"",
|
||||
"watch:wasm": "cargo watch --postpone -C wasm -s \"wasm-pack build . --dev\"",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"--------------------": "",
|
||||
"print-building-help": "echo 'Graphite project failed to build. Did you remember to `npm install` the dependencies in `/frontend`?'",
|
||||
"print-linting-help": "echo 'Graphite project had lint errors, or may have otherwise failed. In the latter case, did you remember to `npm install` the dependencies in `/frontend`?'"
|
||||
},
|
||||
|
@ -38,6 +39,7 @@
|
|||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-prettier-vue": "^4.2.0",
|
||||
"eslint-plugin-vue": "^9.7.0",
|
||||
"concurrently": "^7.6.0",
|
||||
"license-checker-webpack-plugin": "^0.2.1",
|
||||
"prettier": "^2.7.1",
|
||||
"rollup-plugin-license": "^3.0.1",
|
||||
|
@ -45,7 +47,8 @@
|
|||
"typescript": "^4.9.3",
|
||||
"vite": "^4.1.1",
|
||||
"vite-plugin-top-level-await": "^1.2.2",
|
||||
"vite-plugin-wasm": "^3.1.1",
|
||||
"vite-plugin-wasm": "^3.2.1",
|
||||
"vite-svg-loader": "^4.0.0",
|
||||
"vue-cli-plugin-tauri": "~1.0.0",
|
||||
"vue-loader": "^17.0.1"
|
||||
},
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"beforeBuildCommand": "npm run build",
|
||||
"beforeDevCommand": "npm run tauri:dev",
|
||||
"distDir": "../dist",
|
||||
"devPath": "http://127.0.0.1:5173/"
|
||||
"devPath": "http://127.0.0.1:8080/"
|
||||
},
|
||||
"package": {
|
||||
"productName": "graphite-tauri",
|
||||
|
|
|
@ -1,23 +1,26 @@
|
|||
// vite.config.js
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import wasm from "vite-plugin-wasm";
|
||||
import toplevelawait from "vite-plugin-top-level-await"
|
||||
import license from "rollup-plugin-license"
|
||||
import * as path from "path";
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import license from "rollup-plugin-license";
|
||||
import { defineConfig } from "vite";
|
||||
import toplevelawait from "vite-plugin-top-level-await";
|
||||
import wasm from "vite-plugin-wasm";
|
||||
import svgLoader from "vite-svg-loader";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), toplevelawait(), wasm() , svgLoader()],
|
||||
resolve: {
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||
plugins: [vue(), toplevelawait(), wasm(), svgLoader()],
|
||||
resolve: {
|
||||
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"],
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ["graphite-wasm"],
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
plugins: [
|
||||
|
@ -29,4 +32,7 @@ export default defineConfig({
|
|||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
server: {
|
||||
port: 8080,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -53,7 +53,7 @@ impl Bezier {
|
|||
}
|
||||
|
||||
/// Returns the non-normalized vector representing the tangent at the point `t` along the curve.
|
||||
fn non_normalized_tangent(&self, t: f64) -> DVec2 {
|
||||
pub(crate) fn non_normalized_tangent(&self, t: f64) -> DVec2 {
|
||||
match self.handles {
|
||||
BezierHandles::Linear => self.end - self.start,
|
||||
_ => self.derivative().unwrap().evaluate(TValue::Parametric(t)),
|
||||
|
@ -202,7 +202,7 @@ impl Bezier {
|
|||
/// Implementation of the algorithm to find curve intersections by iterating on bounding boxes.
|
||||
/// - `self_original_t_interval` - Used to identify the `t` values of the original parent of `self` that the current iteration is representing.
|
||||
/// - `other_original_t_interval` - Used to identify the `t` values of the original parent of `other` that the current iteration is representing.
|
||||
fn intersections_between_subcurves(&self, self_original_t_interval: Range<f64>, other: &Bezier, other_original_t_interval: Range<f64>, error: f64) -> Vec<[f64; 2]> {
|
||||
pub(crate) fn intersections_between_subcurves(&self, self_original_t_interval: Range<f64>, other: &Bezier, other_original_t_interval: Range<f64>, error: f64) -> Vec<[f64; 2]> {
|
||||
let bounding_box1 = self.bounding_box();
|
||||
let bounding_box2 = other.bounding_box();
|
||||
|
||||
|
@ -245,8 +245,8 @@ impl Bezier {
|
|||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Returns a list of filtered parametric `t` values that correspond to intersection points between the current bezier curve and the provided one
|
||||
/// such that the difference between adjacent `t` values in sorted order is greater than some minimum seperation value. If the difference
|
||||
/// between 2 adjacent `t` values is lesss than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value.
|
||||
/// such that the difference between adjacent `t` values in sorted order is greater than some minimum separation value. If the difference
|
||||
/// between 2 adjacent `t` values is less than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value.
|
||||
/// The returned `t` values are with respect to the current bezier, not the provided parameter.
|
||||
/// If the provided curve is linear, then zero intersection points will be returned along colinear segments.
|
||||
/// - `error` - For intersections where the provided bezier is non-linear, `error` defines the threshold for bounding boxes to be considered an intersection point.
|
||||
|
@ -259,7 +259,7 @@ impl Bezier {
|
|||
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
|
||||
intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| {
|
||||
if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_seperation.unwrap_or(MIN_SEPERATION_VALUE) {
|
||||
if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_seperation.unwrap_or(MIN_SEPARATION_VALUE) {
|
||||
accumulator.pop();
|
||||
}
|
||||
accumulator.push(*t);
|
||||
|
|
|
@ -1,11 +1,41 @@
|
|||
use super::*;
|
||||
|
||||
use crate::compare::compare_points;
|
||||
use crate::utils::{f64_compare, TValue};
|
||||
use crate::{AppendType, ManipulatorGroup, Subpath};
|
||||
|
||||
use glam::DMat2;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
/// Functionality that transform Beziers, such as split, reduce, offset, etc.
|
||||
impl Bezier {
|
||||
/// Returns a linear approximation of the given [Bezier]. For higher order [Bezier], this means simply dropping the handles.
|
||||
pub fn to_linear(&self) -> Bezier {
|
||||
Bezier::from_linear_dvec2(self.start(), self.end())
|
||||
}
|
||||
|
||||
/// Returns a quadratic approximation of the given [Bezier]. For cubic Bezier, which typically cannot be represented by a single
|
||||
/// quadratic segment, this function simply takes the average of the cubic handles to be the new quadratic handle.
|
||||
pub fn to_quadratic(&self) -> Bezier {
|
||||
let handle = match self.handles {
|
||||
BezierHandles::Linear => self.start,
|
||||
BezierHandles::Quadratic { handle } => handle,
|
||||
BezierHandles::Cubic { handle_start, handle_end } => (handle_start + handle_end) / 2.,
|
||||
};
|
||||
Bezier::from_quadratic_dvec2(self.start, handle, self.end)
|
||||
}
|
||||
|
||||
/// Returns a cubic approximation of the given [Bezier].
|
||||
pub fn to_cubic(&self) -> Bezier {
|
||||
let (handle_start, handle_end) = match self.handles {
|
||||
BezierHandles::Linear => (self.start, self.end),
|
||||
// Conversion reference source: https://stackoverflow.com/a/63059651/775283
|
||||
BezierHandles::Quadratic { handle } => (self.start + (2. / 3.) * (handle - self.start), self.end + (2. / 3.) * (handle - self.end)),
|
||||
BezierHandles::Cubic { handle_start: _, handle_end: _ } => return *self,
|
||||
};
|
||||
Bezier::from_cubic_dvec2(self.start, handle_start, handle_end, self.end)
|
||||
}
|
||||
|
||||
/// Returns the pair of Bezier curves that result from splitting the original curve at the point `t` along the curve.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#bezier/split/solo" title="Split Demo"></iframe>
|
||||
pub fn split(&self, t: TValue) -> [Bezier; 2] {
|
||||
|
@ -154,7 +184,11 @@ impl Bezier {
|
|||
|
||||
let step_size = step_size.unwrap_or(DEFAULT_REDUCE_STEP_SIZE);
|
||||
|
||||
let extrema = self.get_extrema_t_list();
|
||||
let mut extrema = self.get_extrema_t_list();
|
||||
if let BezierHandles::Cubic { handle_start: _, handle_end: _ } = self.handles {
|
||||
extrema.append(&mut self.inflections());
|
||||
extrema.sort_by(|ex1, ex2| ex1.partial_cmp(ex2).unwrap());
|
||||
}
|
||||
|
||||
// Split each subcurve such that each resulting segment is scalable.
|
||||
let mut result_beziers: Vec<Bezier> = Vec::new();
|
||||
|
@ -170,15 +204,6 @@ impl Bezier {
|
|||
result_t_values.push(t_subcurve_end);
|
||||
return;
|
||||
}
|
||||
// According to <https://pomax.github.io/bezierinfo/#offsetting>, it is generally sufficient to split subcurves with no local extrema at `t = 0.5` to generate two scalable segments.
|
||||
let [first_half, second_half] = subcurve.split(TValue::Parametric(0.5));
|
||||
if first_half.is_scalable() && second_half.is_scalable() {
|
||||
result_beziers.push(first_half);
|
||||
result_beziers.push(second_half);
|
||||
result_t_values.push(t_subcurve_start + (t_subcurve_end - t_subcurve_start) / 2.);
|
||||
result_t_values.push(t_subcurve_end);
|
||||
return;
|
||||
}
|
||||
|
||||
// Greedily iterate across the subcurve at intervals of size `step_size` to break up the curve into maximally large segments
|
||||
let mut segment: Bezier;
|
||||
|
@ -242,8 +267,14 @@ impl Bezier {
|
|||
// Find the intersection point of the endpoint normals
|
||||
let intersection = utils::line_intersection(self.start, normal_start, self.end, normal_end);
|
||||
|
||||
// If the Bezier is a quadratic, convert it to a cubic to increase expressiveness
|
||||
let intermediate = match self.handles {
|
||||
BezierHandles::Quadratic { handle: _ } => self.to_cubic(),
|
||||
_ => *self,
|
||||
};
|
||||
|
||||
let should_flip_direction = (self.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE);
|
||||
self.apply_transformation(&|point| {
|
||||
intermediate.apply_transformation(&|point| {
|
||||
let mut direction_unit_vector = (intersection - point).normalize();
|
||||
if should_flip_direction {
|
||||
direction_unit_vector *= -1.;
|
||||
|
@ -258,49 +289,47 @@ impl Bezier {
|
|||
pub fn graduated_scale(&self, start_distance: f64, end_distance: f64) -> Bezier {
|
||||
assert!(self.is_scalable(), "The curve provided to scale is not scalable. Reduce the curve first.");
|
||||
|
||||
let normal_start = self.normal(TValue::Parametric(0.));
|
||||
let normal_end = self.normal(TValue::Parametric(1.));
|
||||
// If the Bezier is a quadratic, convert it to a cubic to increase expressiveness
|
||||
let intermediate = match self.handles {
|
||||
BezierHandles::Quadratic { handle: _ } => self.to_cubic(),
|
||||
_ => *self,
|
||||
};
|
||||
|
||||
let normal_start = intermediate.normal(TValue::Parametric(0.));
|
||||
let normal_end = intermediate.normal(TValue::Parametric(1.));
|
||||
|
||||
// If normal unit vectors are equal, then the lines are parallel
|
||||
if normal_start.abs_diff_eq(normal_end, MAX_ABSOLUTE_DIFFERENCE) {
|
||||
let transformed_start = utils::scale_point_from_direction_vector(self.start, self.normal(TValue::Parametric(0.)), false, start_distance);
|
||||
let transformed_end = utils::scale_point_from_direction_vector(self.end, self.normal(TValue::Parametric(1.)), false, end_distance);
|
||||
let transformed_start = utils::scale_point_from_direction_vector(intermediate.start, intermediate.normal(TValue::Parametric(0.)), false, start_distance);
|
||||
let transformed_end = utils::scale_point_from_direction_vector(intermediate.end, intermediate.normal(TValue::Parametric(1.)), false, end_distance);
|
||||
|
||||
return match self.handles {
|
||||
return match intermediate.handles {
|
||||
BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end),
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
let handle_closest_t = self.project(handle, ProjectionOptions::default());
|
||||
let handle_scale_distance = (1. - handle_closest_t) * start_distance + handle_closest_t * end_distance;
|
||||
let transformed_handle = utils::scale_point_from_direction_vector(handle, self.normal(TValue::Parametric(handle_closest_t)), false, handle_scale_distance);
|
||||
Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end)
|
||||
}
|
||||
BezierHandles::Quadratic { handle: _ } => unreachable!(),
|
||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
let handle_start_closest_t = self.project(handle_start, ProjectionOptions::default());
|
||||
let handle_start_closest_t = intermediate.project(handle_start, ProjectionOptions::default());
|
||||
let handle_start_scale_distance = (1. - handle_start_closest_t) * start_distance + handle_start_closest_t * end_distance;
|
||||
let transformed_handle_start = utils::scale_point_from_direction_vector(handle_start, self.normal(TValue::Parametric(handle_start_closest_t)), false, handle_start_scale_distance);
|
||||
let transformed_handle_start =
|
||||
utils::scale_point_from_direction_vector(handle_start, intermediate.normal(TValue::Parametric(handle_start_closest_t)), false, handle_start_scale_distance);
|
||||
|
||||
let handle_end_closest_t = self.project(handle_start, ProjectionOptions::default());
|
||||
let handle_end_closest_t = intermediate.project(handle_start, ProjectionOptions::default());
|
||||
let handle_end_scale_distance = (1. - handle_end_closest_t) * start_distance + handle_end_closest_t * end_distance;
|
||||
let transformed_handle_end = utils::scale_point_from_direction_vector(handle_end, self.normal(TValue::Parametric(handle_end_closest_t)), false, handle_end_scale_distance);
|
||||
let transformed_handle_end = utils::scale_point_from_direction_vector(handle_end, intermediate.normal(TValue::Parametric(handle_end_closest_t)), false, handle_end_scale_distance);
|
||||
Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Find the intersection point of the endpoint normals
|
||||
let intersection = utils::line_intersection(self.start, normal_start, self.end, normal_end);
|
||||
let should_flip_direction = (self.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE);
|
||||
let intersection = utils::line_intersection(intermediate.start, normal_start, intermediate.end, normal_end);
|
||||
let should_flip_direction = (intermediate.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE);
|
||||
|
||||
let transformed_start = utils::scale_point_from_origin(self.start, intersection, should_flip_direction, start_distance);
|
||||
let transformed_end = utils::scale_point_from_origin(self.end, intersection, should_flip_direction, end_distance);
|
||||
let transformed_start = utils::scale_point_from_origin(intermediate.start, intersection, should_flip_direction, start_distance);
|
||||
let transformed_end = utils::scale_point_from_origin(intermediate.end, intersection, should_flip_direction, end_distance);
|
||||
|
||||
match self.handles {
|
||||
match intermediate.handles {
|
||||
BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end),
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
let handle_scale_distance = (start_distance + end_distance) / 2.;
|
||||
let transformed_handle = utils::scale_point_from_origin(handle, intersection, should_flip_direction, handle_scale_distance);
|
||||
Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end)
|
||||
}
|
||||
BezierHandles::Quadratic { handle: _ } => unreachable!(),
|
||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
let handle_start_scale_distance = (start_distance * 2. + end_distance) / 3.;
|
||||
let transformed_handle_start = utils::scale_point_from_origin(handle_start, intersection, should_flip_direction, handle_start_scale_distance);
|
||||
|
@ -312,78 +341,107 @@ impl Bezier {
|
|||
}
|
||||
}
|
||||
|
||||
/// Offset will get all the reduceable subcurves, and for each subcurve, it will scale the subcurve a set distance away from the original curve.
|
||||
/// Offset will break down the Bezier into reducible subcurves, and scale each subcurve a set distance from the original curve.
|
||||
/// Note that not all bezier curves are possible to offset, so this function first reduces the curve to scalable segments and then offsets those segments.
|
||||
/// A proof for why this is true can be found in the [Curve offsetting section](https://pomax.github.io/bezierinfo/#offsetting) of Pomax's bezier curve primer.
|
||||
/// Offset takes the following parameter:
|
||||
/// - `distance` - The offset's distance from the curve. Positive values will offset the curve in the same direction as the endpoint normals,
|
||||
/// while negative values will offset in the opposite direction.
|
||||
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#bezier/offset/solo" title="Offset Demo"></iframe>
|
||||
pub fn offset(&self, distance: f64) -> Vec<Bezier> {
|
||||
let mut reduced = self.reduce(None);
|
||||
reduced.iter_mut().for_each(|bezier| *bezier = bezier.scale(distance));
|
||||
reduced
|
||||
pub fn offset<ManipulatorGroupId: crate::Identifier>(&self, distance: f64) -> Subpath<ManipulatorGroupId> {
|
||||
let reduced = self.reduce(None);
|
||||
let mut scaled = Subpath::new(vec![], false);
|
||||
reduced.iter().enumerate().for_each(|(index, bezier)| {
|
||||
let scaled_bezier = bezier.scale(distance);
|
||||
if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) {
|
||||
scaled.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE));
|
||||
} else {
|
||||
scaled.append_bezier(&scaled_bezier, AppendType::IgnoreStart);
|
||||
}
|
||||
});
|
||||
|
||||
// If the curve is not linear, smooth the handles. All segments produced by bezier::scale will be cubic.
|
||||
if self.handles != BezierHandles::Linear {
|
||||
scaled.smooth_open_subpath();
|
||||
}
|
||||
|
||||
scaled
|
||||
}
|
||||
|
||||
/// Version of the `offset` function which scales the offset such that the start of the offset is `start_distance` from the original curve, while the end of
|
||||
/// of the offset is `end_distance` from the original curve. The curve transitions from `start_distance` to `end_distance` gradually, proportional to the
|
||||
/// distance along the equation (`t`-value) of the curve. Similarily to the `offset` function, the returned result is an approximation.
|
||||
pub fn graduated_offset(&self, start_distance: f64, end_distance: f64) -> Vec<Bezier> {
|
||||
/// distance along the equation (`t`-value) of the curve. Similarly to the `offset` function, the returned result is an approximation.
|
||||
pub fn graduated_offset<ManipulatorGroupId: crate::Identifier>(&self, start_distance: f64, end_distance: f64) -> Subpath<ManipulatorGroupId> {
|
||||
let reduced = self.reduce(None);
|
||||
let mut next_start_distance = start_distance;
|
||||
let distance_difference = end_distance - start_distance;
|
||||
let total_length = self.length(None);
|
||||
|
||||
let mut result = vec![];
|
||||
reduced.iter().for_each(|bezier| {
|
||||
let mut result = Subpath::new(vec![], false);
|
||||
reduced.iter().enumerate().for_each(|(index, bezier)| {
|
||||
let current_length = bezier.length(None);
|
||||
let next_end_distance = next_start_distance + (current_length / total_length) * distance_difference;
|
||||
result.push(bezier.graduated_scale(next_start_distance, next_end_distance));
|
||||
let scaled_bezier = bezier.graduated_scale(next_start_distance, next_end_distance);
|
||||
|
||||
if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) {
|
||||
result.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE));
|
||||
} else {
|
||||
result.append_bezier(&scaled_bezier, AppendType::IgnoreStart);
|
||||
}
|
||||
next_start_distance = next_end_distance;
|
||||
});
|
||||
|
||||
// If the curve is not linear, smooth the handles. All segments produced by bezier::scale will be cubic.
|
||||
if self.handles != BezierHandles::Linear {
|
||||
result.smooth_open_subpath();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Outline will return a vector of Beziers that creates an outline around the curve at the designated distance away from the curve.
|
||||
/// It makes use of the `offset` function, thus restrictions applicable to `offset` are relevant to this function as well.
|
||||
/// The 'caps', the linear segments at opposite ends of the outline, intersect the original curve at the midpoint of the cap.
|
||||
///
|
||||
/// Outline takes the following parameter:
|
||||
/// - `distance` - The outline's distance from the curve.
|
||||
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#bezier/outline/solo" title="Outline Demo"></iframe>
|
||||
pub fn outline(&self, distance: f64) -> Vec<Bezier> {
|
||||
pub fn outline<ManipulatorGroupId: crate::Identifier>(&self, distance: f64) -> Subpath<ManipulatorGroupId> {
|
||||
let first_segment = self.offset(distance);
|
||||
let third_segment = self.reverse().offset(distance);
|
||||
|
||||
if first_segment.is_empty() || third_segment.is_empty() {
|
||||
return vec![];
|
||||
return Subpath::new(vec![], false);
|
||||
}
|
||||
|
||||
let second_segment = Bezier::from_linear_dvec2(first_segment.last().unwrap().end, third_segment.first().unwrap().start);
|
||||
let fourth_segment = Bezier::from_linear_dvec2(third_segment.last().unwrap().end, first_segment.first().unwrap().start);
|
||||
[first_segment, vec![second_segment], third_segment, vec![fourth_segment]].concat()
|
||||
let mut result_manipulator_groups: Vec<ManipulatorGroup<ManipulatorGroupId>> = vec![];
|
||||
result_manipulator_groups.extend_from_slice(first_segment.manipulator_groups());
|
||||
// TODO: Handle other caps here
|
||||
result_manipulator_groups.extend_from_slice(third_segment.manipulator_groups());
|
||||
Subpath::new(result_manipulator_groups, true)
|
||||
}
|
||||
|
||||
/// Version of the `outline` function which draws the outline at the specified distances away from the curve.
|
||||
/// The outline begins `start_distance` away, and gradually move to being `end_distance` away.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#bezier/graduated-outline/solo" title="Graduated Outline Demo"></iframe>
|
||||
pub fn graduated_outline(&self, start_distance: f64, end_distance: f64) -> Vec<Bezier> {
|
||||
pub fn graduated_outline<ManipulatorGroupId: crate::Identifier>(&self, start_distance: f64, end_distance: f64) -> Subpath<ManipulatorGroupId> {
|
||||
self.skewed_outline(start_distance, end_distance, end_distance, start_distance)
|
||||
}
|
||||
|
||||
/// Version of the `graduated_outline` function that allows for the 4 corners of the outline to be different distances away from the curve.
|
||||
/// <iframe frameBorder="0" width="100%" height="475px" src="https://graphite.rs/bezier-rs-demos#bezier/skewed-outline/solo" title="Skewed Outline Demo"></iframe>
|
||||
pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64) -> Vec<Bezier> {
|
||||
pub fn skewed_outline<ManipulatorGroupId: crate::Identifier>(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64) -> Subpath<ManipulatorGroupId> {
|
||||
let first_segment = self.graduated_offset(distance1, distance2);
|
||||
let third_segment = self.reverse().graduated_offset(distance3, distance4);
|
||||
|
||||
if first_segment.is_empty() || third_segment.is_empty() {
|
||||
return vec![];
|
||||
return Subpath::new(vec![], false);
|
||||
}
|
||||
|
||||
let second_segment = Bezier::from_linear_dvec2(first_segment.last().unwrap().end, third_segment.first().unwrap().start);
|
||||
let fourth_segment = Bezier::from_linear_dvec2(third_segment.last().unwrap().end, first_segment.first().unwrap().start);
|
||||
[first_segment, vec![second_segment], third_segment, vec![fourth_segment]].concat()
|
||||
let mut result_manipulator_groups: Vec<ManipulatorGroup<ManipulatorGroupId>> = vec![];
|
||||
result_manipulator_groups.extend_from_slice(first_segment.manipulator_groups());
|
||||
// TODO: Handle other caps here
|
||||
result_manipulator_groups.extend_from_slice(third_segment.manipulator_groups());
|
||||
Subpath::new(result_manipulator_groups, true)
|
||||
}
|
||||
|
||||
/// Approximate a bezier curve with circular arcs.
|
||||
|
@ -538,8 +596,9 @@ impl Bezier {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::compare::{compare_arcs, compare_vector_of_beziers};
|
||||
use crate::compare::{compare_arcs, compare_points, compare_vec_of_points};
|
||||
use crate::utils::TValue;
|
||||
use crate::EmptyId;
|
||||
|
||||
#[test]
|
||||
fn test_split() {
|
||||
|
@ -695,41 +754,103 @@ mod tests {
|
|||
vec![DVec2::new(4.2975, 4.2975), DVec2::new(5.6625, 5.6625), DVec2::new(6.9375, 6.9375)],
|
||||
];
|
||||
let reduced_curves = bezier.reduce(None);
|
||||
assert!(compare_vector_of_beziers(&reduced_curves, expected_bezier_points));
|
||||
assert!(reduced_curves.iter().zip(expected_bezier_points.into_iter()).all(|(bezier, points)| compare_vec_of_points(
|
||||
bezier.get_points().collect::<Vec<DVec2>>(),
|
||||
points,
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)));
|
||||
|
||||
// Check that the reduce helper is correct
|
||||
let (helper_curves, helper_t_values) = bezier.reduced_curves_and_t_values(None);
|
||||
assert_eq!(&reduced_curves, &helper_curves);
|
||||
assert!(reduced_curves
|
||||
.iter()
|
||||
.zip(helper_curves.iter())
|
||||
.all(|(bezier1, bezier2)| bezier1.abs_diff_eq(bezier2, MAX_ABSOLUTE_DIFFERENCE)));
|
||||
assert!(reduced_curves
|
||||
.iter()
|
||||
.zip(helper_t_values.windows(2))
|
||||
.all(|(curve, t_pair)| curve.abs_diff_eq(&bezier.trim(TValue::Parametric(t_pair[0]), TValue::Parametric(t_pair[1])), MAX_ABSOLUTE_DIFFERENCE)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_offset() {
|
||||
let p1 = DVec2::new(30., 50.);
|
||||
let p2 = DVec2::new(140., 30.);
|
||||
let p3 = DVec2::new(160., 170.);
|
||||
let bezier1 = Bezier::from_quadratic_dvec2(p1, p2, p3);
|
||||
let expected_bezier_points1 = vec![
|
||||
vec![DVec2::new(31.7888, 59.8387), DVec2::new(44.5924, 57.46446), DVec2::new(56.09375, 57.5)],
|
||||
vec![DVec2::new(56.09375, 57.5), DVec2::new(94.94197, 56.5019), DVec2::new(117.6473, 84.5936)],
|
||||
vec![DVec2::new(117.6473, 84.5936), DVec2::new(142.3985, 113.403), DVec2::new(150.1005, 171.4142)],
|
||||
];
|
||||
assert!(compare_vector_of_beziers(&bezier1.offset(10.), expected_bezier_points1));
|
||||
fn assert_valid_offset<ManipulatorGroupId: crate::Identifier>(bezier: &Bezier, offset: &Subpath<ManipulatorGroupId>, expected_distance: f64) {
|
||||
// Verify that the offset is smooth
|
||||
if offset.len() > 1 {
|
||||
offset.iter().take(offset.len() - 2).zip(offset.iter().skip(1)).for_each(|beziers_pair| {
|
||||
assert!(compare_points(beziers_pair.0.end, beziers_pair.1.start));
|
||||
assert!(compare_points(beziers_pair.0.normal(TValue::Parametric(1.)), beziers_pair.1.normal(TValue::Parametric(0.))));
|
||||
});
|
||||
}
|
||||
|
||||
let p4 = DVec2::new(32., 77.);
|
||||
let p5 = DVec2::new(169., 25.);
|
||||
let p6 = DVec2::new(164., 157.);
|
||||
let bezier2 = Bezier::from_quadratic_dvec2(p4, p5, p6);
|
||||
let expected_bezier_points2 = vec![
|
||||
vec![DVec2::new(42.6458, 105.04758), DVec2::new(75.0218, 91.9939), DVec2::new(98.09357, 92.3043)],
|
||||
vec![DVec2::new(98.09357, 92.3043), DVec2::new(116.5995, 88.5479), DVec2::new(123.9055, 102.0401)],
|
||||
vec![DVec2::new(123.9055, 102.0401), DVec2::new(136.6087, 116.9522), DVec2::new(134.1761, 147.9324)],
|
||||
vec![DVec2::new(134.1761, 147.9324), DVec2::new(134.1812, 151.7987), DVec2::new(134.0215, 155.86445)],
|
||||
];
|
||||
assert!(compare_vector_of_beziers(&bezier2.offset(30.), expected_bezier_points2));
|
||||
// Verify that the offset spans the length of the curve
|
||||
let start_distance = bezier.evaluate(TValue::Parametric(0.)).distance(offset.iter().next().unwrap().evaluate(TValue::Parametric(0.)));
|
||||
assert!(f64_compare(start_distance, expected_distance, MAX_ABSOLUTE_DIFFERENCE));
|
||||
let end_distance = bezier.evaluate(TValue::Parametric(1.)).distance(offset.iter().last().unwrap().evaluate(TValue::Parametric(1.)));
|
||||
assert!(f64_compare(end_distance, expected_distance, MAX_ABSOLUTE_DIFFERENCE));
|
||||
|
||||
let err_threshold = expected_distance / 10.;
|
||||
// Sample the curve and verify that the offset lies at the correct distance from the curve.
|
||||
// Collect the t-value associated with the point on the bezier closest to the sample.
|
||||
let t_values: Vec<f64> = offset
|
||||
.iter()
|
||||
.flat_map(|offset_segment| {
|
||||
[0.1, 0.25, 0.5, 0.75, 0.9]
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let offset_point = offset_segment.evaluate(TValue::Parametric(*t));
|
||||
let closest_point_t = bezier.project(offset_point, ProjectionOptions::default());
|
||||
let closest_point = bezier.evaluate(TValue::Parametric(closest_point_t));
|
||||
let actual_distance = offset_point.distance(closest_point);
|
||||
|
||||
assert!(f64_compare(actual_distance, expected_distance, err_threshold));
|
||||
closest_point_t
|
||||
})
|
||||
.collect::<Vec<f64>>()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Verify that the curve segments are in the correct order by asserting that t_values is sorted
|
||||
for i in 1..t_values.len() {
|
||||
assert!(t_values[i - 1] < t_values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_offset_linear() {
|
||||
let start = DVec2::new(30., 60.);
|
||||
let end = DVec2::new(140., 120.);
|
||||
let bezier = Bezier::from_linear_dvec2(start, end);
|
||||
|
||||
for distance in [-20., -10., 10., 20.] {
|
||||
let offset = bezier.offset::<EmptyId>(distance);
|
||||
assert_valid_offset(&bezier, &offset, distance.abs());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_offset_quadratic() {
|
||||
let start = DVec2::new(30., 50.);
|
||||
let handle = DVec2::new(140., 30.);
|
||||
let end = DVec2::new(160., 170.);
|
||||
let bezier = Bezier::from_quadratic_dvec2(start, handle, end);
|
||||
|
||||
for distance in [-20., -10., 10., 20.] {
|
||||
let offset = bezier.offset::<EmptyId>(distance);
|
||||
assert_valid_offset(&bezier, &offset, distance.abs());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_offset_cubic() {
|
||||
let start = DVec2::new(30., 30.);
|
||||
let handle1 = DVec2::new(60., 140.);
|
||||
let handle2 = DVec2::new(150., 30.);
|
||||
let end = DVec2::new(160., 160.);
|
||||
let bezier = Bezier::from_cubic_dvec2(start, handle1, handle2, end);
|
||||
|
||||
for distance in [-20., -10., 10., 20.] {
|
||||
let offset = bezier.offset::<EmptyId>(distance);
|
||||
assert_valid_offset(&bezier, &offset, distance.abs());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -737,29 +858,29 @@ mod tests {
|
|||
let p1 = DVec2::new(30., 50.);
|
||||
let p2 = DVec2::new(140., 30.);
|
||||
let line = Bezier::from_linear_dvec2(p1, p2);
|
||||
let outline = line.outline(10.);
|
||||
let outline = line.outline::<EmptyId>(10.);
|
||||
|
||||
assert_eq!(outline.len(), 4);
|
||||
|
||||
// Assert the first length-wise piece of the outline is 10 units from the line
|
||||
assert!(f64_compare(
|
||||
outline[0].evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.25))),
|
||||
outline.iter().next().unwrap().evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.25))),
|
||||
10.,
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)); // f64
|
||||
|
||||
// Assert the first cap touches the line end point at the halfway point
|
||||
assert!(outline[1].evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.end(), MAX_ABSOLUTE_DIFFERENCE));
|
||||
assert!(outline.iter().nth(1).unwrap().evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.end(), MAX_ABSOLUTE_DIFFERENCE));
|
||||
|
||||
// Assert the second length-wise piece of the outline is 10 units from the line
|
||||
assert!(f64_compare(
|
||||
outline[2].evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.75))),
|
||||
outline.iter().nth(2).unwrap().evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.75))),
|
||||
10.,
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)); // f64
|
||||
|
||||
// Assert the second cap touches the line start point at the halfway point
|
||||
assert!(outline[3].evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE));
|
||||
assert!(outline.iter().nth(3).unwrap().evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
/// Comparison functions used for tests in the bezier module
|
||||
use super::{Bezier, CircleArc, Subpath};
|
||||
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
#[cfg(test)]
|
||||
use super::{CircleArc, Subpath};
|
||||
#[cfg(test)]
|
||||
use crate::utils::f64_compare;
|
||||
|
||||
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
|
||||
use glam::DVec2;
|
||||
|
||||
// Compare two f64s with some maximum absolute difference to account for floating point errors
|
||||
#[cfg(test)]
|
||||
pub fn compare_f64s(f1: f64, f2: f64) -> bool {
|
||||
f64_compare(f1, f2, MAX_ABSOLUTE_DIFFERENCE)
|
||||
}
|
||||
|
@ -16,19 +20,13 @@ pub fn compare_points(p1: DVec2, p2: DVec2) -> bool {
|
|||
}
|
||||
|
||||
/// Compare vectors of points by allowing some maximum absolute difference to account for floating point errors
|
||||
#[cfg(test)]
|
||||
pub fn compare_vec_of_points(a: Vec<DVec2>, b: Vec<DVec2>, max_absolute_difference: f64) -> bool {
|
||||
a.len() == b.len() && a.into_iter().zip(b.into_iter()).all(|(p1, p2)| p1.abs_diff_eq(p2, max_absolute_difference))
|
||||
}
|
||||
|
||||
/// Compare vectors of beziers by allowing some maximum absolute difference between points to account for floating point errors
|
||||
pub fn compare_vector_of_beziers(beziers: &[Bezier], expected_bezier_points: Vec<Vec<DVec2>>) -> bool {
|
||||
beziers
|
||||
.iter()
|
||||
.zip(expected_bezier_points.iter())
|
||||
.all(|(&a, b)| compare_vec_of_points(a.get_points().collect::<Vec<DVec2>>(), b.to_vec(), MAX_ABSOLUTE_DIFFERENCE))
|
||||
}
|
||||
|
||||
/// Compare circle arcs by allowing some maximum absolute difference between values to account for floating point errors
|
||||
#[cfg(test)]
|
||||
pub fn compare_arcs(arc1: CircleArc, arc2: CircleArc) -> bool {
|
||||
compare_points(arc1.center, arc2.center)
|
||||
&& f64_compare(arc1.radius, arc1.radius, MAX_ABSOLUTE_DIFFERENCE)
|
||||
|
@ -38,6 +36,7 @@ pub fn compare_arcs(arc1: CircleArc, arc2: CircleArc) -> bool {
|
|||
|
||||
/// Compare Subpath by verifying that their bezier segments match.
|
||||
/// In this way, matching quadratic segments where the handles are on opposite manipulator groups will be considered equal.
|
||||
#[cfg(test)]
|
||||
pub fn compare_subpaths<ManipulatorGroupId: crate::Identifier>(subpath1: &Subpath<ManipulatorGroupId>, subpath2: &Subpath<ManipulatorGroupId>) -> bool {
|
||||
subpath1.len() == subpath2.len() && subpath1.closed() == subpath2.closed() && subpath1.iter().eq(subpath2.iter())
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ pub const NUM_DISTANCES: usize = 5;
|
|||
/// Maximum allowed angle that the normal of the `start` or `end` point can make with the normal of the corresponding handle for a curve to be considered scalable/simple.
|
||||
pub const SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE: f64 = std::f64::consts::PI / 3.;
|
||||
/// Minimum allowable separation between adjacent `t` values when calculating curve intersections
|
||||
pub const MIN_SEPERATION_VALUE: f64 = 5. * 1e-3;
|
||||
pub const MIN_SEPARATION_VALUE: f64 = 5. * 1e-3;
|
||||
/// Default error bound for `t_value_to_parametric` function when TValue argument is Euclidean
|
||||
pub const DEFAULT_EUCLIDEAN_ERROR_BOUND: f64 = 0.001;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
//! Bezier-rs: A Bezier Math Library for Rust
|
||||
#[cfg(test)]
|
||||
pub(crate) mod compare;
|
||||
|
||||
mod bezier;
|
||||
|
@ -9,4 +8,4 @@ mod utils;
|
|||
|
||||
pub use bezier::*;
|
||||
pub use subpath::*;
|
||||
pub use utils::{SubpathTValue, TValue};
|
||||
pub use utils::{Joint, SubpathTValue, TValue};
|
||||
|
|
|
@ -14,7 +14,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
}
|
||||
|
||||
/// Create a `Subpath` consisting of 2 manipulator groups from a `Bezier`.
|
||||
pub fn from_bezier(bezier: Bezier) -> Self {
|
||||
pub fn from_bezier(bezier: &Bezier) -> Self {
|
||||
Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
|
@ -34,6 +34,47 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
)
|
||||
}
|
||||
|
||||
/// Creates a subpath from a slice of [Bezier]. When two consecutive Beziers do not share an end and start point, this function
|
||||
/// resolves the discrepancy by simply taking the start-point of the second Bezier as the anchor of the Manipulator Group.
|
||||
pub fn from_beziers(beziers: &[Bezier], closed: bool) -> Self {
|
||||
assert!(!closed || beziers.len() > 1, "A closed Subpath must contain at least 1 Bezier.");
|
||||
if beziers.is_empty() {
|
||||
return Subpath::new(vec![], closed);
|
||||
}
|
||||
|
||||
let first = beziers.first().unwrap();
|
||||
let mut manipulator_groups = vec![ManipulatorGroup {
|
||||
anchor: first.start(),
|
||||
in_handle: None,
|
||||
out_handle: first.handle_start(),
|
||||
id: ManipulatorGroupId::new(),
|
||||
}];
|
||||
let mut inner_groups: Vec<ManipulatorGroup<ManipulatorGroupId>> = beziers
|
||||
.windows(2)
|
||||
.map(|bezier_pair| ManipulatorGroup {
|
||||
anchor: bezier_pair[1].start(),
|
||||
in_handle: bezier_pair[0].handle_end(),
|
||||
out_handle: bezier_pair[1].handle_start(),
|
||||
id: ManipulatorGroupId::new(),
|
||||
})
|
||||
.collect::<Vec<ManipulatorGroup<ManipulatorGroupId>>>();
|
||||
manipulator_groups.append(&mut inner_groups);
|
||||
|
||||
let last = beziers.last().unwrap();
|
||||
if !closed {
|
||||
manipulator_groups.push(ManipulatorGroup {
|
||||
anchor: last.end(),
|
||||
in_handle: last.handle_end(),
|
||||
out_handle: None,
|
||||
id: ManipulatorGroupId::new(),
|
||||
});
|
||||
return Subpath::new(manipulator_groups, false);
|
||||
}
|
||||
|
||||
manipulator_groups[0].in_handle = last.handle_end();
|
||||
Subpath::new(manipulator_groups, true)
|
||||
}
|
||||
|
||||
/// Returns true if the `Subpath` contains no [ManipulatorGroup].
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.manipulator_groups.is_empty()
|
||||
|
|
|
@ -34,6 +34,47 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
self.manipulator_groups[segment_index % number_of_groups].out_handle = first.handle_start();
|
||||
self.manipulator_groups[(segment_index + 2) % number_of_groups].in_handle = second.handle_end();
|
||||
}
|
||||
|
||||
/// Append a [Bezier] to the end of a subpath from a vector of [Bezier].
|
||||
/// The `append_type` parameter determines how the function behaves when the subpath's last anchor is not equal to the Bezier's start point.
|
||||
/// - `IgnoreStart`: drops the bezier's start point in favor of the subpath's last anchor
|
||||
/// - `SmoothJoin(f64)`: joins the subpath's endpoint with the bezier's start with a another Bezier segment that is continuous up to the second derivative
|
||||
/// if the difference between the subpath's end point and Bezier's start point exceeds the wrapped integer value.
|
||||
/// This function assumes that the position of the [Bezier]'s starting point is equal to that of the Subpath's last manipulator group.
|
||||
pub fn append_bezier(&mut self, bezier: &Bezier, append_type: AppendType) {
|
||||
if self.manipulator_groups.is_empty() {
|
||||
self.manipulator_groups = vec![ManipulatorGroup {
|
||||
anchor: bezier.start(),
|
||||
in_handle: None,
|
||||
out_handle: None,
|
||||
id: ManipulatorGroupId::new(),
|
||||
}];
|
||||
}
|
||||
let mut last_index = self.manipulator_groups.len() - 1;
|
||||
let last_anchor = self.manipulator_groups[last_index].anchor;
|
||||
|
||||
if let AppendType::SmoothJoin(max_absolute_difference) = append_type {
|
||||
// If the provided Bezier does not start at a location similar to the end of the Subpath,
|
||||
// add an additional manipulator group to represent a smooth join with a new bezier in between
|
||||
if !last_anchor.abs_diff_eq(bezier.start(), max_absolute_difference) {
|
||||
let last_bezier = if self.manipulator_groups.len() > 1 {
|
||||
self.manipulator_groups[last_index - 1].to_bezier(&self.manipulator_groups[last_index])
|
||||
} else {
|
||||
Bezier::from_linear_dvec2(last_anchor, last_anchor)
|
||||
};
|
||||
let join_bezier = last_bezier.join(bezier);
|
||||
self.append_bezier(&join_bezier, AppendType::IgnoreStart);
|
||||
last_index = self.manipulator_groups.len() - 1;
|
||||
}
|
||||
}
|
||||
self.manipulator_groups[last_index].out_handle = bezier.handle_start();
|
||||
self.manipulator_groups.push(ManipulatorGroup {
|
||||
anchor: bezier.end(),
|
||||
in_handle: bezier.handle_end(),
|
||||
out_handle: None,
|
||||
id: ManipulatorGroupId::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -17,20 +17,27 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
/// Calculates the intersection points the subpath has with a given curve and returns a list of `(usize, f64)` tuples,
|
||||
/// where the `usize` represents the index of the curve in the subpath, and the `f64` represents the `t`-value local to
|
||||
/// that curve where the intersection occured.
|
||||
/// This function expects the following:
|
||||
/// Expects the following:
|
||||
/// - `other`: a [Bezier] curve to check intersections against
|
||||
/// - `error`: an optional f64 value to provide an error bound
|
||||
/// - `minimum_seperation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
|
||||
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two.
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
|
||||
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_seperation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
// TODO: account for either euclidean or parametric type
|
||||
let intersection_t_values: Vec<(usize, f64)> = self
|
||||
.iter()
|
||||
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
self.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(index, bezier)| bezier.intersections(other, error, minimum_seperation).into_iter().map(|t| (index, t)).collect::<Vec<(usize, f64)>>())
|
||||
.collect();
|
||||
.flat_map(|(index, bezier)| bezier.intersections(other, error, minimum_separation).into_iter().map(|t| (index, t)).collect::<Vec<(usize, f64)>>())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Calculates the intersection points the subpath has with another given subpath and returns a list of global parametric `t`-values.
|
||||
/// This function expects the following:
|
||||
/// - other: a [Bezier] curve to check intersections against
|
||||
/// - error: an optional f64 value to provide an error bound
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
|
||||
pub fn subpath_intersections(&self, other: &Subpath<ManipulatorGroupId>, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
let mut intersection_t_values: Vec<(usize, f64)> = other.iter().flat_map(|bezier| self.intersections(&bezier, error, minimum_separation)).collect();
|
||||
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
intersection_t_values
|
||||
}
|
||||
|
||||
|
|
|
@ -93,3 +93,9 @@ impl<ManipulatorGroupId: crate::Identifier> ManipulatorGroup<ManipulatorGroupId>
|
|||
self.out_handle = self.out_handle.map(|out_handle| affine_transform.transform_point2(out_handle));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum AppendType {
|
||||
IgnoreStart,
|
||||
SmoothJoin(f64),
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use std::vec;
|
||||
|
||||
use super::*;
|
||||
use crate::utils::SubpathTValue;
|
||||
use crate::utils::TValue;
|
||||
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
use crate::utils::{Joint, SubpathTValue, TValue};
|
||||
|
||||
use glam::DAffine2;
|
||||
|
||||
|
@ -235,7 +237,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
// If the target curve_indices are the same, then the trim must be happening within one bezier
|
||||
// This means curve1 == curve2 must be true, and we can simply call the Bezier trim.
|
||||
if t1_curve_index == t2_curve_index {
|
||||
return Subpath::from_bezier(curve1.trim(TValue::Parametric(t1_curve_t), TValue::Parametric(t2_curve_t)));
|
||||
return Subpath::from_bezier(&curve1.trim(TValue::Parametric(t1_curve_t), TValue::Parametric(t2_curve_t)));
|
||||
}
|
||||
|
||||
// Split the bezier's with the according t value and keep the correct half
|
||||
|
@ -272,6 +274,183 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
manipulator_group.apply_transform(affine_transform);
|
||||
}
|
||||
}
|
||||
|
||||
/// Smooths a Subpath up to the first derivative, using a weighted averaged based on segment length.
|
||||
/// The Subpath must be open, and contain no quadratic segments.
|
||||
pub(crate) fn smooth_open_subpath(&mut self) {
|
||||
for i in 1..self.len() - 1 {
|
||||
let first_bezier = self.manipulator_groups[i - 1].to_bezier(&self.manipulator_groups[i]);
|
||||
let second_bezier = self.manipulator_groups[i].to_bezier(&self.manipulator_groups[i + 1]);
|
||||
if first_bezier.handle_end().is_none() || second_bezier.handle_end().is_none() {
|
||||
continue;
|
||||
}
|
||||
let end_tangent = first_bezier.non_normalized_tangent(1.);
|
||||
let start_tangent = second_bezier.non_normalized_tangent(0.);
|
||||
|
||||
// Compute an average unit vector, weighing the segments by a rough estimation of their relative size.
|
||||
let segment1_len = first_bezier.length(Some(5));
|
||||
let segment2_len = second_bezier.length(Some(5));
|
||||
let average_unit_tangent = (end_tangent.normalize() * segment1_len + start_tangent.normalize() * segment2_len) / (segment1_len + segment2_len);
|
||||
|
||||
// Adjust start and end handles to fit the average tangent
|
||||
let end_point = first_bezier.end();
|
||||
self.manipulator_groups[i].in_handle = Some((average_unit_tangent / 3. * -1.) * end_tangent.length() + end_point);
|
||||
|
||||
let start_point = second_bezier.start();
|
||||
self.manipulator_groups[i].out_handle = Some((average_unit_tangent / 3.) * start_tangent.length() + start_point);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: If a segment curls back on itself tightly enough it could intersect again at the portion that should be trimmed. This could cause the Subpaths to be clipped
|
||||
// at the incorrect location. This can be avoided by first trimming the two Subpaths at any extrema, effectively ignoring loopbacks.
|
||||
/// Helper function to clip overlap of two intersecting open Subpaths. Returns an optional, as intersections may not exist for certain arrangements and distances.
|
||||
/// Assumes that the Subpaths represents simple Bezier segments, and clips the Subpaths at the last intersection of the first Subpath, and first intersection of the last Subpath.
|
||||
fn clip_simple_subpaths(subpath1: &Subpath<ManipulatorGroupId>, subpath2: &Subpath<ManipulatorGroupId>) -> Option<(Subpath<ManipulatorGroupId>, Subpath<ManipulatorGroupId>)> {
|
||||
// Split the first subpath at its last intersection
|
||||
let intersections1 = subpath1.subpath_intersections(subpath2, None, None);
|
||||
if intersections1.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (segment_index, t) = *intersections1.last().unwrap();
|
||||
let (clipped_subpath1, _) = subpath1.split(SubpathTValue::Parametric { segment_index, t });
|
||||
|
||||
// Split the second subpath at its first intersection
|
||||
let intersections2 = subpath2.subpath_intersections(subpath1, None, None);
|
||||
if intersections2.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (segment_index, t) = intersections2[0];
|
||||
let (_, clipped_subpath2) = subpath2.split(SubpathTValue::Parametric { segment_index, t });
|
||||
|
||||
Some((clipped_subpath1, clipped_subpath2.unwrap()))
|
||||
}
|
||||
|
||||
/// Reduces the segments of the subpath into simple subcurves, then scales each subcurve a set `distance` away.
|
||||
/// The intersections of segments of the subpath are joined using the method specified by the `joint` argument.
|
||||
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#subpath/offset/solo" title="Offset Demo"></iframe>
|
||||
pub fn offset(&self, distance: f64, joint: Joint) -> Subpath<ManipulatorGroupId> {
|
||||
assert!(self.len_segments() > 1, "Cannot offset an empty Subpath.");
|
||||
|
||||
// An offset at a distance 0 from the curve is simply the same curve
|
||||
if distance == 0. {
|
||||
return self.clone();
|
||||
}
|
||||
|
||||
let mut subpaths = self.iter().map(|bezier| bezier.offset(distance)).collect::<Vec<Subpath<ManipulatorGroupId>>>();
|
||||
let mut drop_common_point = vec![true; self.len()];
|
||||
|
||||
// Clip or join consecutive Subpaths
|
||||
for i in 0..subpaths.len() - 1 {
|
||||
let j = i + 1;
|
||||
let subpath1 = &subpaths[i];
|
||||
let subpath2 = &subpaths[j];
|
||||
|
||||
let last_segment = subpath1.get_segment(subpath1.len_segments() - 1).unwrap();
|
||||
let first_segment = subpath2.get_segment(0).unwrap();
|
||||
|
||||
// If the anchors are approximately equal, there is no need to clip / join the segments
|
||||
if last_segment.end().abs_diff_eq(first_segment.start(), MAX_ABSOLUTE_DIFFERENCE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate the angle formed between two consecutive Subpaths
|
||||
let out_tangent = self.get_segment(i).unwrap().tangent(TValue::Parametric(1.));
|
||||
let in_tangent = self.get_segment(j).unwrap().tangent(TValue::Parametric(0.));
|
||||
let angle = out_tangent.angle_between(in_tangent);
|
||||
|
||||
// The angle is concave. The Subpath overlap and must be clipped
|
||||
let mut apply_joint = true;
|
||||
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
|
||||
// If the distance is large enough, there may still be no intersections. Also, if the angle is close enough to zero,
|
||||
// subpath intersections may find no intersections. In this case, the points are likely close enough that we can approximate
|
||||
// the points as being on top of one another.
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(subpath1, subpath2) {
|
||||
subpaths[i] = clipped_subpath1;
|
||||
subpaths[j] = clipped_subpath2;
|
||||
apply_joint = false;
|
||||
}
|
||||
}
|
||||
// The angle is convex. The Subpath must be joined using the specified Joint type
|
||||
if apply_joint {
|
||||
match joint {
|
||||
Joint::Bevel => {
|
||||
drop_common_point[j] = false;
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clip any overlap in the last segment
|
||||
if self.closed {
|
||||
let out_tangent = self.get_segment(self.len_segments() - 1).unwrap().tangent(TValue::Parametric(1.));
|
||||
let in_tangent = self.get_segment(0).unwrap().tangent(TValue::Parametric(0.));
|
||||
let angle = out_tangent.angle_between(in_tangent);
|
||||
|
||||
let mut apply_joint = true;
|
||||
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(&subpaths[subpaths.len() - 1], &subpaths[0]) {
|
||||
// Merge the clipped subpaths
|
||||
let last_index = subpaths.len() - 1;
|
||||
subpaths[last_index] = clipped_subpath1;
|
||||
subpaths[0] = clipped_subpath2;
|
||||
apply_joint = false;
|
||||
}
|
||||
}
|
||||
if apply_joint {
|
||||
match joint {
|
||||
Joint::Bevel => {
|
||||
drop_common_point[0] = false;
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge the subpaths. Drop points which overlap with one another.
|
||||
let mut manipulator_groups = subpaths[0].manipulator_groups.clone();
|
||||
for i in 1..subpaths.len() {
|
||||
if drop_common_point[i] {
|
||||
let last_group = manipulator_groups.pop().unwrap();
|
||||
let mut manipulators_copy = subpaths[i].manipulator_groups.clone();
|
||||
manipulators_copy[0].in_handle = last_group.in_handle;
|
||||
|
||||
manipulator_groups.append(&mut manipulators_copy);
|
||||
} else {
|
||||
manipulator_groups.append(&mut subpaths[i].manipulator_groups.clone());
|
||||
}
|
||||
}
|
||||
if self.closed && drop_common_point[0] {
|
||||
let last_group = manipulator_groups.pop().unwrap();
|
||||
manipulator_groups[0].in_handle = last_group.in_handle;
|
||||
}
|
||||
|
||||
Subpath::new(manipulator_groups, self.closed)
|
||||
}
|
||||
|
||||
// TODO: Replace this return type with `Path`, once the `Path` data type has been created.
|
||||
/// Outline returns a single closed subpath (if the original subpath was open) or two closed subpaths (if the original subpath was closed) that forms
|
||||
/// an approximate outline around the subpath at a specified distance from the curve. Outline takes the following parameters:
|
||||
/// - `distance` - The outline's distance from the curve.
|
||||
/// - `joint` - The joint type used to cap the endpoints of open bezier curves, and join successive subpath segments.
|
||||
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#subpath/outline/solo" title="Outline Demo"></iframe>
|
||||
pub fn outline(&self, distance: f64, joint: Joint) -> (Subpath<ManipulatorGroupId>, Option<Subpath<ManipulatorGroupId>>) {
|
||||
let mut pos_offset = self.offset(distance, joint);
|
||||
let mut neg_offset = self.reverse().offset(distance, joint);
|
||||
|
||||
if self.closed {
|
||||
return (pos_offset, Some(neg_offset));
|
||||
}
|
||||
|
||||
match joint {
|
||||
Joint::Bevel => {
|
||||
pos_offset.manipulator_groups.append(&mut neg_offset.manipulator_groups);
|
||||
pos_offset.closed = true;
|
||||
(pos_offset, None)
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -515,7 +694,7 @@ mod tests {
|
|||
let result1 = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(0.2));
|
||||
let result2 = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.8));
|
||||
|
||||
assert!(compare_subpaths(&result1, &result2));
|
||||
assert!(compare_subpaths::<EmptyId>(&result1, &result2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -544,7 +723,7 @@ mod tests {
|
|||
let result = subpath.trim(SubpathTValue::GlobalParametric(0.), SubpathTValue::GlobalParametric(1.));
|
||||
|
||||
// Assume that resulting subpath would no longer have the any meaningless handles
|
||||
let mut expected_subpath = subpath.clone();
|
||||
let mut expected_subpath = subpath;
|
||||
expected_subpath[3].out_handle = None;
|
||||
|
||||
assert_eq!(result.manipulator_groups[0].anchor, location_front);
|
||||
|
@ -561,7 +740,7 @@ mod tests {
|
|||
|
||||
assert_eq!(result.manipulator_groups[0].anchor, location_front);
|
||||
assert_eq!(result.manipulator_groups[3].anchor, location_back);
|
||||
assert!(compare_subpaths(&subpath, &result));
|
||||
assert!(compare_subpaths::<EmptyId>(&subpath, &result));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -728,10 +907,4 @@ mod tests {
|
|||
assert!(result.manipulator_groups[0].out_handle.is_none());
|
||||
assert_eq!(result.manipulator_groups.len(), 1);
|
||||
}
|
||||
|
||||
fn transform_subpath() {
|
||||
let mut subpath = set_up_open_subpath();
|
||||
subpath.apply_transform(glam::DAffine2::IDENTITY);
|
||||
assert_eq!(subpath, set_up_open_subpath());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, MIN_SEPERATION_VALUE, STRICT_MAX_ABSOLUTE_DIFFERENCE};
|
||||
use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, MIN_SEPARATION_VALUE, STRICT_MAX_ABSOLUTE_DIFFERENCE};
|
||||
|
||||
use glam::{BVec2, DMat2, DVec2};
|
||||
use std::f64::consts::PI;
|
||||
|
@ -28,6 +28,13 @@ pub enum SubpathTValue {
|
|||
GlobalEuclideanWithinError { t: f64, error: f64 },
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Joint {
|
||||
Miter,
|
||||
Bevel,
|
||||
Round,
|
||||
}
|
||||
|
||||
/// Helper to perform the computation of a and c, where b is the provided point on the curve.
|
||||
/// Given the correct power of `t` and `(1-t)`, the computation is the same for quadratic and cubic cases.
|
||||
/// Relevant derivation and the definitions of a, b, and c can be found in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer.
|
||||
|
@ -115,7 +122,7 @@ pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec
|
|||
let a_divided_by_3 = a / 3.;
|
||||
let root_1 = 2. * cube_root(-q_divided_by_2) - a_divided_by_3;
|
||||
let root_2 = cube_root(q_divided_by_2) - a_divided_by_3;
|
||||
if (root_1 - root_2).abs() > MIN_SEPERATION_VALUE {
|
||||
if (root_1 - root_2).abs() > MIN_SEPARATION_VALUE {
|
||||
roots.push(root_1);
|
||||
}
|
||||
roots.push(root_2);
|
||||
|
|
|
@ -19,7 +19,7 @@ quantization = ["autoquant"]
|
|||
|
||||
[dependencies]
|
||||
autoquant = { git = "https://github.com/truedoctor/autoquant", optional = true, features = ["fitting"] }
|
||||
graphene-core = {path = "../gcore", features = ["async", "std" ], default-features = false}
|
||||
graphene-core = {path = "../gcore", features = ["async", "std", "serde" ], default-features = false}
|
||||
borrow_stack = {path = "../borrow_stack"}
|
||||
dyn-any = {path = "../../libraries/dyn-any", features = ["derive"]}
|
||||
graph-craft = {path = "../graph-craft"}
|
||||
|
@ -38,12 +38,13 @@ image = "*"
|
|||
dyn-clone = "1.0"
|
||||
|
||||
log = "0.4"
|
||||
bezier-rs = { path = "../../libraries/bezier-rs" }
|
||||
bezier-rs = { path = "../../libraries/bezier-rs" , features = ["serde"] }
|
||||
kurbo = { git = "https://github.com/linebender/kurbo.git", features = [
|
||||
"serde",
|
||||
] }
|
||||
glam = { version = "0.22", features = ["serde"] }
|
||||
node-macro = { path="../node-macro" }
|
||||
boxcar = "0.1.0"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
|
|
|
@ -1,50 +1,77 @@
|
|||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use graphene_core::Node;
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
/// Caches the output of a given Node and acts as a proxy
|
||||
#[derive(Default)]
|
||||
pub struct CacheNode<T> {
|
||||
cache: OnceCell<T>,
|
||||
// We have to use an append only data structure to make sure the references
|
||||
// to the cache entries are always valid
|
||||
cache: boxcar::Vec<(u64, T)>,
|
||||
}
|
||||
impl<'i, T: 'i> Node<'i, T> for CacheNode<T> {
|
||||
impl<'i, T: 'i + Hash> Node<'i, T> for CacheNode<T> {
|
||||
type Output = &'i T;
|
||||
fn eval<'s: 'i>(&'s self, input: T) -> Self::Output {
|
||||
self.cache.get_or_init(|| {
|
||||
trace!("Creating new cache node");
|
||||
input
|
||||
})
|
||||
let mut hasher = DefaultHasher::new();
|
||||
input.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
|
||||
if let Some((_, cached_value)) = self.cache.iter().find(|(h, _)| *h == hash) {
|
||||
return cached_value;
|
||||
} else {
|
||||
trace!("Cache miss");
|
||||
let index = self.cache.push((hash, input));
|
||||
return &self.cache[index].1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> CacheNode<T> {
|
||||
pub const fn new() -> CacheNode<T> {
|
||||
CacheNode { cache: OnceCell::new() }
|
||||
pub fn new() -> CacheNode<T> {
|
||||
CacheNode { cache: boxcar::Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Caches the output of a given Node and acts as a proxy
|
||||
/// It provides two modes of operation, it can either be set
|
||||
/// when calling the node with a `Some<T>` variant or the last
|
||||
/// value that was added is returned when calling it with `None`
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct LetNode<T> {
|
||||
cache: OnceCell<T>,
|
||||
// We have to use an append only data structure to make sure the references
|
||||
// to the cache entries are always valid
|
||||
// TODO: We only ever access the last value so there is not really a reason for us
|
||||
// to store the previous entries. This should be reworked in the future
|
||||
cache: boxcar::Vec<(u64, T)>,
|
||||
}
|
||||
impl<'i, T: 'i> Node<'i, Option<T>> for LetNode<T> {
|
||||
impl<'i, T: 'i + Hash> Node<'i, Option<T>> for LetNode<T> {
|
||||
type Output = &'i T;
|
||||
fn eval<'s: 'i>(&'s self, input: Option<T>) -> Self::Output {
|
||||
match input {
|
||||
Some(input) => {
|
||||
self.cache.set(input).unwrap_or_else(|_| error!("Let node was set twice but is not mutable"));
|
||||
self.cache.get().unwrap()
|
||||
let mut hasher = DefaultHasher::new();
|
||||
input.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
|
||||
if let Some((cached_hash, cached_value)) = self.cache.iter().last() {
|
||||
if hash == *cached_hash {
|
||||
return cached_value;
|
||||
}
|
||||
}
|
||||
trace!("Cache miss");
|
||||
let index = self.cache.push((hash, input));
|
||||
return &self.cache[index].1;
|
||||
}
|
||||
None => self.cache.get().expect("Let node was not initialized"),
|
||||
None => &self.cache.iter().last().expect("Let node was not initialized").1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> LetNode<T> {
|
||||
pub const fn new() -> LetNode<T> {
|
||||
LetNode { cache: OnceCell::new() }
|
||||
pub fn new() -> LetNode<T> {
|
||||
LetNode { cache: boxcar::Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"description": "A convenience package for calling the real package.json in ./frontend",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "cd frontend && npm run serve",
|
||||
"serve": "cd frontend && npm run serve"
|
||||
"start": "cd frontend && npm start",
|
||||
"serve": "cd frontend && npm start"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -239,10 +239,10 @@ const bezierFeatures = {
|
|||
sliderOptions: [
|
||||
{
|
||||
variable: "distance",
|
||||
min: -50,
|
||||
max: 50,
|
||||
min: -30,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 20,
|
||||
default: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -257,9 +257,9 @@ const bezierFeatures = {
|
|||
{
|
||||
variable: "distance",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 20,
|
||||
default: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -274,16 +274,16 @@ const bezierFeatures = {
|
|||
{
|
||||
variable: "start_distance",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 30,
|
||||
default: 5,
|
||||
},
|
||||
{
|
||||
variable: "end_distance",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 30,
|
||||
default: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -306,28 +306,28 @@ const bezierFeatures = {
|
|||
{
|
||||
variable: "distance1",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 20,
|
||||
},
|
||||
{
|
||||
variable: "distance2",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 10,
|
||||
},
|
||||
{
|
||||
variable: "distance3",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 30,
|
||||
},
|
||||
{
|
||||
variable: "distance4",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 5,
|
||||
},
|
||||
|
|
|
@ -114,6 +114,32 @@ const subpathFeatures = {
|
|||
],
|
||||
chooseTVariant: true,
|
||||
},
|
||||
offset: {
|
||||
name: "Offset",
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.offset(options.distance),
|
||||
sliderOptions: [
|
||||
{
|
||||
variable: "distance",
|
||||
min: -25,
|
||||
max: 25,
|
||||
step: 1,
|
||||
default: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
outline: {
|
||||
name: "Outline",
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.outline(options.distance),
|
||||
sliderOptions: [
|
||||
{
|
||||
variable: "distance",
|
||||
min: 0,
|
||||
max: 25,
|
||||
step: 1,
|
||||
default: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export type SubpathFeatureKey = keyof typeof subpathFeatures;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::svg_drawing::*;
|
||||
use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, ProjectionOptions, TValue};
|
||||
use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, Identifier, ProjectionOptions, TValue};
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
@ -49,6 +49,16 @@ fn parse_t_variant(t_variant: &String, t: f64) -> TValue {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// Expect js_points to be a list of 2 pairs.
|
||||
|
@ -542,7 +552,7 @@ impl WasmBezier {
|
|||
let original_curve_svg = self.get_bezier_path();
|
||||
let bezier_curves_svg = self
|
||||
.0
|
||||
.offset(distance)
|
||||
.offset::<EmptyId>(distance)
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, bezier_curve)| {
|
||||
|
@ -561,36 +571,39 @@ impl WasmBezier {
|
|||
}
|
||||
|
||||
pub fn outline(&self, distance: f64) -> String {
|
||||
let outline_beziers = self.0.outline(distance);
|
||||
if outline_beziers.is_empty() {
|
||||
let outline_subpath = self.0.outline::<EmptyId>(distance);
|
||||
if outline_subpath.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let outline_svg = draw_beziers(outline_beziers, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED));
|
||||
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) -> String {
|
||||
let outline_beziers = self.0.graduated_outline(start_distance, end_distance);
|
||||
if outline_beziers.is_empty() {
|
||||
let outline_subpath = self.0.graduated_outline::<EmptyId>(start_distance, end_distance);
|
||||
if outline_subpath.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let outline_svg = draw_beziers(outline_beziers, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED));
|
||||
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) -> String {
|
||||
let outline_beziers = self.0.skewed_outline(distance1, distance2, distance3, distance4);
|
||||
if outline_beziers.is_empty() {
|
||||
let outline_subpath = self.0.skewed_outline::<EmptyId>(distance1, distance2, distance3, distance4);
|
||||
if outline_subpath.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let outline_svg = draw_beziers(outline_beziers, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED));
|
||||
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}"))
|
||||
|
|
|
@ -376,4 +376,29 @@ impl WasmSubpath {
|
|||
|
||||
wrap_svg_tag(format!("{}{}", self.to_default_svg(), trimmed_subpath_svg))
|
||||
}
|
||||
|
||||
pub fn offset(&self, distance: f64) -> String {
|
||||
let offset_subpath = self.0.offset(distance, bezier_rs::Joint::Bevel);
|
||||
|
||||
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) -> String {
|
||||
let (outline_piece1, outline_piece2) = self.0.outline(distance, bezier_rs::Joint::Bevel);
|
||||
|
||||
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 outline_piece2.is_some() {
|
||||
outline_piece2
|
||||
.unwrap()
|
||||
.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,6 +1,4 @@
|
|||
use bezier_rs::Bezier;
|
||||
use glam::DVec2;
|
||||
use std::fmt::Write;
|
||||
|
||||
// SVG drawing constants
|
||||
pub const SVG_OPEN_TAG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="200px" height="200px">"#;
|
||||
|
@ -48,19 +46,6 @@ pub fn draw_line(start_x: f64, start_y: f64, end_x: f64, end_y: f64, stroke: &st
|
|||
format!(r#"<line x1="{start_x}" y1="{start_y}" x2="{end_x}" y2="{end_y}" stroke="{stroke}" stroke-width="{stroke_width}"/>"#)
|
||||
}
|
||||
|
||||
/// Helper function to draw a list of beziers.
|
||||
pub fn draw_beziers(beziers: Vec<Bezier>, options: String) -> String {
|
||||
let start_point = beziers.first().unwrap().start();
|
||||
let mut svg = format!("<path d=\"M {} {}", start_point.x, start_point.y);
|
||||
|
||||
beziers.iter().for_each(|bezier| {
|
||||
let _ = write!(svg, " {}", bezier.svg_curve_argument());
|
||||
});
|
||||
|
||||
let _ = write!(svg, " Z\" {}/>", options);
|
||||
svg
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue