Merge branch 'asdf' into tauri-restructure-lite

This commit is contained in:
Keavon Chambers 2023-03-05 10:45:00 +01:00
commit 49a0a57975
29 changed files with 1103 additions and 238 deletions

View file

@ -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
View file

@ -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",

View file

@ -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

Before After
Before After

View file

@ -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",

View file

@ -24,4 +24,7 @@ export default defineConfig({
],
},
},
server: {
port: 8080
}
});

View file

@ -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

Before After
Before After

View file

@ -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",

View file

@ -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"
},

View file

@ -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",

View file

@ -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,
},
});

View file

@ -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);

View file

@ -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]

View file

@ -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())
}

View file

@ -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;

View file

@ -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};

View file

@ -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()

View file

@ -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)]

View file

@ -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
}

View file

@ -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),
}

View file

@ -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());
}
}

View file

@ -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);

View file

@ -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"

View file

@ -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() }
}
}

View file

@ -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"
}
}

View file

@ -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,
},

View file

@ -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;

View file

@ -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}"))

View file

@ -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()))
}
}

View file

@ -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();