81 Commits

Author SHA1 Message Date
9b94159f8e register works 2026-01-23 11:23:07 +01:00
aa4d7f73a8 setup zig node 2026-01-23 10:24:21 +01:00
1efb94b09c Add zig to flake packages 2026-01-23 09:07:05 +01:00
Felix Hungenberg
5570d975f5 feat: unmigrate number into universal float, inherit step if unset
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m1s
2026-01-22 23:57:56 +01:00
Felix Hungenberg
8c1ba2ee65 feat: move add context menu within view if outside
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m59s
2026-01-22 23:26:56 +01:00
Felix Hungenberg
3e019e4e21 feat: don't move graph on right click drag 2026-01-22 23:26:26 +01:00
Felix Hungenberg
a58b19e935 Merge branch 'main' of github.com:jim-fx/nodarium
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m57s
2026-01-22 14:06:44 +01:00
Max Richter
6f5c5bb46e feat: change initial camera position so that nodes are visible
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m3s
2026-01-22 12:07:37 +01:00
7f2214f15c fix(utils): make sure we do not build a .wasm file for utils
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m54s
2026-01-21 17:24:54 +01:00
43ef563ae7 feat: show all nodes in add menu
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m57s
2026-01-21 17:08:47 +01:00
Felix Hungenberg
714d01da94 chore: move pnpm links to workspace (auto link) 2026-01-21 16:39:54 +01:00
Felix Hungenberg
92308fc43a chore: run ui debug server from root
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m57s
2026-01-21 16:36:20 +01:00
Felix Hungenberg
5adf67ed52 Merge branch 'main' of git.max-richter.dev:max/nodarium
Some checks failed
Deploy to GitHub Pages / build_site (push) Has been cancelled
2026-01-21 16:35:34 +01:00
Felix Hungenberg
f54cde734e fix: integer width 2026-01-21 16:35:13 +01:00
70d8095869 Merge pull request 'feat: project manager' (#21) from feat/project-manager into main
Some checks failed
Deploy to GitHub Pages / build_site (push) Has been cancelled
Reviewed-on: #21
2026-01-21 16:35:03 +01:00
Felix Hungenberg
2a90d5de3f chore: dev scripts & linting 2026-01-21 16:31:26 +01:00
d7e9e8b8de chore: remove some old console.logs 2026-01-21 16:01:11 +01:00
bdbaab25a4 feat: initial working version of project manager 2026-01-21 15:02:34 +01:00
Felix Hungenberg
c7bfb0f05b fix(ui): integrate number input to exports, ui page, benchmark
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m46s
2026-01-21 11:55:10 +01:00
Felix Hungenberg
d3a46af4c2 chore: update pnpm corepack up
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m47s
2026-01-21 11:24:53 +01:00
4c76c62a3e feat: add header element 2026-01-21 11:09:51 +01:00
36cf9211d2 fix: run pnpm i :)
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m46s
2026-01-20 19:28:35 +01:00
97a2ffb683 feat: use workspace instead of link for app/package.json
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 6s
2026-01-20 19:18:17 +01:00
fffa8c7cdf feat: cleanup Dockerfiles and use prepared image for deployments
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m46s
2026-01-20 19:12:56 +01:00
de799c2d55 Merge pull request 'feat/remove-wasm-bindgen' (#19) from feat/remove-wasm-bindgen into main
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m45s
Reviewed-on: #19
2026-01-20 18:57:42 +01:00
24bef0460c Merge branch 'main' into feat/remove-wasm-bindgen 2026-01-20 18:57:34 +01:00
93b64fc7dd feat: add app/Dockerfile
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m4s
2026-01-20 18:50:54 +01:00
64ac28f60c chore: cleanup node buildscripts 2026-01-20 18:26:48 +01:00
bd0c2eaacd Merge remote-tracking branch 'origin/main' into feat/remove-wasm-bindgen 2026-01-20 18:04:56 +01:00
8693c63d16 feat: resize canvases to fit window height
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m3s
Closes #16

The canvases fit their parents size, so adding a wrapper with 100vh
solved it. https://threlte.xyz/docs/reference/core/canvas#size
2026-01-20 17:55:58 +01:00
fbd82bbdfa Merge pull request 'feat/drop-node-on-connection' (#18) from feat/drop-node-on-connection into main
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m2s
Reviewed-on: #18
2026-01-20 17:47:04 +01:00
63997ec262 Merge branch 'main' into feat/drop-node-on-connection 2026-01-20 17:46:55 +01:00
a3d10d6094 feat: drop node on edge
Closes #13
2026-01-20 17:46:09 +01:00
Felix Hungenberg
6b6038e546 feat: use new number input
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m2s
fix missing id in html
2026-01-20 16:49:56 +01:00
Felix Hungenberg
3e3d41ae98 feat: open keyboard shortcuts with ?
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m4s
2026-01-20 16:23:21 +01:00
Felix Hungenberg
a8c76a846e fmt: ignore pnpm-lock.YAML
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 9m27s
2026-01-20 15:19:39 +01:00
f98b90dcd3 Merge remote-tracking branch 'origin/main' into feat/drop-node-on-connection 2026-01-20 15:17:17 +01:00
1ea7d6629f chore: remove ai comments from dockerfile 2026-01-20 14:44:20 +01:00
617dfb0c9d feat: add Dockerfile for app to deploy preview 2026-01-20 14:08:45 +01:00
Felix Hungenberg
c46bf9e64f ci: fix lockfile
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m1s
2026-01-19 16:57:54 +01:00
Felix Hungenberg
0cfd1e5c96 feat(ui): add id prop for inputs
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 5s
2026-01-19 16:43:41 +01:00
Felix Hungenberg
ecfd4d5f2f feat(ui): cleanup integer input
remove warnings, migrate deprecated (dispatch to prop), include min max switch from float input, include esc/enter from float input, include precision, include partial styling from float input
2026-01-19 16:43:07 +01:00
Felix Hungenberg
03102fdc75 ci: improve dev setup
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 7s
2026-01-19 16:33:24 +01:00
Felix Hungenberg
0bd00b0192 feat(app): add description and input debug info to node params 2026-01-19 16:32:19 +01:00
Felix Hungenberg
edbc71ed8f add dprint config 2026-01-19 16:31:27 +01:00
Felix Hungenberg
4485cef82b chore(app): remove old (pnpm v6 - now 9) pnpm lock 2026-01-19 16:30:45 +01:00
cdef71265e Add svelte language server
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m45s
2026-01-19 16:26:15 +01:00
8e5412c25c Merge remote-tracking branch 'origin/main' into feat/remove-wasm-bindgen 2026-01-19 16:26:06 +01:00
2904c13c41 feat: init 2026-01-19 16:25:29 +01:00
450262b4ae fix(app): remove unused func
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m7s
2026-01-19 14:24:47 +01:00
11de746c01 feat(app): allow disabling of runtime/registry caches
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m58s
2026-01-19 14:22:14 +01:00
83cb2bd950 feat: move analytics script to env
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m2s
2026-01-19 14:04:00 +01:00
e84c715f4c chore: Add flake and direnv stuff 2026-01-19 12:51:33 +01:00
f5cea555cd chore: Add flake and direnv stuff
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m7s
2026-01-19 12:50:12 +01:00
Max Richter
ecbcc814ed chore: remove store code 2026-01-19 01:29:28 +01:00
Max Richter
be97387252 feat: trying to remove wasm-bindgen 2026-01-19 01:29:12 +01:00
Max Richter
987ece2a4b fix: update performance bars to work with tailwind
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m15s
2026-01-18 18:55:18 +01:00
Max Richter
8d2e3f006b fix: make graph source work 2026-01-18 18:54:53 +01:00
Max Richter
80d3e117b4 feat: update sidebar to svelte-5
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m1s
2026-01-18 18:39:02 +01:00
Max Richter
8a540522dd chore: replace unocss with tailwind
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m4s
2026-01-18 17:11:47 +01:00
Max Richter
a11214072f chore: some updates
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m6s
2026-01-18 16:27:42 +01:00
d068828b68 refactor: rename state.svelte.ts to graph-state.svelte.ts
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m59s
2025-12-09 20:00:52 +01:00
3565a18364 feat: cache everything in node store not only wasm 2025-12-05 14:19:29 +01:00
73be4fdd73 feat: better handle node position updates 2025-12-05 14:19:11 +01:00
702c3ee6cf feat: better handle camera positioning 2025-12-05 14:18:56 +01:00
98672eb702 fix: error that changes in active node panel did not get saved 2025-12-05 12:28:30 +01:00
3eafdc50b1 feat: keep benchmark result if panel is hidden 2025-12-05 11:49:10 +01:00
Max Richter
548e445eb7 fix: correctly show hide geometries in geometrypool
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m4s
2025-12-03 22:59:06 +01:00
db77a4fd94 Merge pull request 'refactor: split ui/runtime/serialized node types' (#10) from refactor/split-node-runtime-types into main
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m3s
Reviewed-on: #10
2025-12-03 19:19:17 +01:00
Max Richter
7ae1fae3b9 refactor: split ui/runtime/serialized node types
Closes #6
2025-12-03 19:18:56 +01:00
1126cf8f9f feat: dont use custom edge geometry
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m55s
2025-12-03 10:33:24 +01:00
Max Richter
ef479d0557 chore: update
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 3m50s
2025-12-02 17:31:58 +01:00
Max Richter
a1c926c3cf fix: better handle randomGeneration 2025-12-02 17:27:34 +01:00
ca8b1e15ac chore: cleanup edge and node code
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m8s
2025-12-02 16:59:43 +01:00
4878d02705 refactor: remove unneeded random var in node 2025-12-02 16:59:29 +01:00
2b4c81f557 fix: make sure new nodes are reactive
Closes #7
2025-12-02 16:59:11 +01:00
d178f812fb refactor: move event handlers to own classes 2025-12-02 16:58:31 +01:00
669a2c7991 docs: remove placeholder content in readme 2025-12-02 15:20:26 +01:00
becd7a1eb3 fix: make sure we do not pass svelte state into comlink
cant clone proxies
2025-12-02 15:20:13 +01:00
d140f42468 feat: better a18n for node parameters
Dunno of a18n would even be possible for the node graph
2025-12-02 15:19:48 +01:00
be835e5cff fix: better stroke width and color for edges 2025-12-02 15:00:41 +01:00
236 changed files with 7782 additions and 10367 deletions

70
.dprint.jsonc Normal file
View File

@@ -0,0 +1,70 @@
{
"$schema": "https://dprint.dev/schemas/v0.json",
"indentWidth": 2,
"lineWidth": 100,
"typescript": {
// https://dprint.dev/plugins/typescript/config/
"quoteStyle": "preferSingle",
"trailingCommas": "never",
},
"json": {
// https://dprint.dev/plugins/json/config/
},
"markdown": {
},
"toml": {
},
"dockerfile": {
},
"ruff": {
},
"jupyter": {
},
"malva": {
},
"markup": {
// https://dprint.dev/plugins/markup_fmt/config/
"scriptIndent": true,
"styleIndent": true,
},
"yaml": {
},
"graphql": {
},
"exec": {
"cwd": "${configDir}",
"commands": [{
"command": "rustfmt",
"exts": ["rs"],
"cacheKeyFiles": [
"rustfmt.toml",
"rust-toolchain.toml",
],
}],
},
"excludes": [
"**/node_modules",
"**/build",
"**/.svelte-kit",
"**/package",
"**/*-lock.yaml",
"**/yaml.lock",
"**/.DS_Store",
],
"plugins": [
"https://plugins.dprint.dev/typescript-0.95.13.wasm",
"https://plugins.dprint.dev/json-0.21.1.wasm",
"https://plugins.dprint.dev/markdown-0.20.0.wasm",
"https://plugins.dprint.dev/toml-0.7.0.wasm",
"https://plugins.dprint.dev/dockerfile-0.3.3.wasm",
"https://plugins.dprint.dev/ruff-0.6.11.wasm",
"https://plugins.dprint.dev/jupyter-0.2.1.wasm",
"https://plugins.dprint.dev/g-plane/malva-v0.15.1.wasm",
"https://plugins.dprint.dev/g-plane/markup_fmt-v0.25.3.wasm",
"https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm",
"https://plugins.dprint.dev/g-plane/pretty_graphql-v0.2.3.wasm",
"https://plugins.dprint.dev/exec-0.6.0.json@a054130d458f124f9b5c91484833828950723a5af3f8ff2bd1523bd47b83b364",
"https://plugins.dprint.dev/biome-0.11.10.wasm",
],
}

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ node_modules/
# Added by cargo # Added by cargo
/target /target
.direnv/

367
Cargo.lock generated
View File

@@ -1,176 +1,80 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.2.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]] [[package]]
name = "box" name = "box"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"console_error_panic_hook",
"nodarium_macros", "nodarium_macros",
"nodarium_utils", "nodarium_utils",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
] ]
[[package]] [[package]]
name = "branch" name = "branch"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"console_error_panic_hook",
"glam",
"nodarium_macros", "nodarium_macros",
"nodarium_utils", "nodarium_utils",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
] ]
[[package]] [[package]]
name = "float" name = "float"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"console_error_panic_hook",
"nodarium_macros", "nodarium_macros",
"nodarium_utils", "nodarium_utils",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
] ]
[[package]] [[package]]
name = "glam" name = "glam"
version = "0.27.0" version = "0.30.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9"
[[package]] [[package]]
name = "gravity" name = "gravity"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"console_error_panic_hook",
"glam", "glam",
"nodarium_macros", "nodarium_macros",
"nodarium_utils", "nodarium_utils",
"noise", ]
"serde",
"serde-wasm-bindgen", [[package]]
"wasm-bindgen", name = "instance"
"wasm-bindgen-test", version = "0.1.0"
"web-sys", dependencies = [
"glam",
"nodarium_macros",
"nodarium_utils",
] ]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.11" version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "js-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "log"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]] [[package]]
name = "math" name = "math"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"console_error_panic_hook",
"nodarium_macros", "nodarium_macros",
"nodarium_utils", "nodarium_utils",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
] ]
[[package]] [[package]]
name = "max-plantarium-triangle" name = "memchr"
version = "0.1.0" version = "2.7.6"
dependencies = [ source = "registry+https://github.com/rust-lang/crates.io-index"
"console_error_panic_hook", checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
"nodarium_macros",
"nodarium_utils",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
]
[[package]]
name = "max-plantarium-vec3"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"nodarium_macros",
"nodarium_utils",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
]
[[package]]
name = "nodarium_instance"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"glam",
"nodarium_macros",
"nodarium_utils",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
]
[[package]] [[package]]
name = "nodarium_macros" name = "nodarium_macros"
@@ -180,7 +84,7 @@ dependencies = [
"quote", "quote",
"serde", "serde",
"serde_json", "serde_json",
"syn 1.0.109", "syn",
] ]
[[package]] [[package]]
@@ -195,29 +99,19 @@ dependencies = [
name = "nodarium_utils" name = "nodarium_utils"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"console_error_panic_hook",
"glam", "glam",
"noise", "noise 0.9.0",
"serde", "serde",
"serde_json", "serde_json",
"wasm-bindgen",
"web-sys",
] ]
[[package]] [[package]]
name = "nodes-noise" name = "noise"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"console_error_panic_hook",
"glam",
"nodarium_macros", "nodarium_macros",
"nodarium_utils", "nodarium_utils",
"noise", "noise 0.9.0",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
] ]
[[package]] [[package]]
@@ -233,48 +127,35 @@ dependencies = [
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.18" version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]] [[package]]
name = "output" name = "output"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"console_error_panic_hook",
"glam",
"nodarium_macros", "nodarium_macros",
"nodarium_utils", "nodarium_utils",
"serde",
"serde_json",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
] ]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.81" version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.36" version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -307,103 +188,76 @@ dependencies = [
name = "random" name = "random"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"console_error_panic_hook",
"nodarium_macros", "nodarium_macros",
"nodarium_utils", "nodarium_utils",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
] ]
[[package]] [[package]]
name = "rotate" name = "rotate"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"console_error_panic_hook",
"glam", "glam",
"nodarium_macros", "nodarium_macros",
"nodarium_utils", "nodarium_utils",
"serde", "serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
] ]
[[package]]
name = "ryu"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.198" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]]
name = "serde-wasm-bindgen"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.198" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.60", "syn",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.116" version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "memchr",
"serde", "serde",
"serde_core",
"zmij",
] ]
[[package]] [[package]]
name = "stem" name = "stem"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"console_error_panic_hook",
"nodarium_macros", "nodarium_macros",
"nodarium_utils", "nodarium_utils",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
] ]
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -411,119 +265,30 @@ dependencies = [
] ]
[[package]] [[package]]
name = "syn" name = "triangle"
version = "2.0.60" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
dependencies = [ dependencies = [
"proc-macro2", "nodarium_macros",
"quote", "nodarium_utils",
"unicode-ident",
] ]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.12" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]] [[package]]
name = "wasm-bindgen" name = "vec3"
version = "0.2.92" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [ dependencies = [
"cfg-if", "nodarium_macros",
"wasm-bindgen-macro", "nodarium_utils",
"serde",
] ]
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "zmij"
version = "0.2.92" version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.60",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "wasm-bindgen-test"
version = "0.3.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b"
dependencies = [
"console_error_panic_hook",
"js-sys",
"scoped-tls",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test-macro",
]
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
]
[[package]]
name = "web-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
dependencies = [
"js-sys",
"wasm-bindgen",
]

View File

@@ -6,7 +6,10 @@ members = [
"packages/types", "packages/types",
"packages/utils", "packages/utils",
] ]
exclude = ["nodes/max/plantarium/.template"] exclude = [
"nodes/max/plantarium/.template",
"nodes/max/plantarium/zig"
]
[profile.release] [profile.release]
lto = true lto = true

View File

@@ -2,10 +2,6 @@ FROM node:24-alpine
RUN apk add --no-cache --update curl rclone g++ RUN apk add --no-cache --update curl rclone g++
RUN RUSTUP_URL="https://sh.rustup.rs" \
&& curl --silent --show-error --location --fail --retry 3 --proto '=https' --tlsv1.2 --output /tmp/rustup-linux-install.sh $RUSTUP_URL \
&& sh /tmp/rustup-linux-install.sh -y
ENV RUSTUP_HOME=/usr/local/rustup \ ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \ CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH PATH=/usr/local/cargo/bin:$PATH
@@ -15,7 +11,5 @@ RUN curl --silent --show-error --location --fail --retry 3 \
--output /tmp/rustup-init.sh https://sh.rustup.rs \ --output /tmp/rustup-init.sh https://sh.rustup.rs \
&& sh /tmp/rustup-init.sh -y --no-modify-path --profile minimal \ && sh /tmp/rustup-init.sh -y --no-modify-path --profile minimal \
&& rm /tmp/rustup-init.sh \ && rm /tmp/rustup-init.sh \
&& rustup default stable \
&& rustup target add wasm32-unknown-unknown \ && rustup target add wasm32-unknown-unknown \
&& cargo install wasm-pack \
&& npm i -g pnpm && npm i -g pnpm

1
app/.env Normal file
View File

@@ -0,0 +1 @@
PUBLIC_ANALYTIC_SCRIPT=""

43
app/Dockerfile Normal file
View File

@@ -0,0 +1,43 @@
FROM jimfx/nodes:latest AS builder
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json Cargo.lock Cargo.toml ./
COPY packages/ ./packages/
COPY nodes/ ./nodes/
COPY app/package.json ./app/
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
--mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
pnpm install --frozen-lockfile
COPY . .
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
--mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
pnpm build:nodes && \
pnpm --filter @nodarium/app... build
FROM nginx:alpine AS runner
RUN rm /etc/nginx/conf.d/default.conf
COPY <<EOF /etc/nginx/conf.d/app.conf
server {
listen 80;
server_name _;
root /app;
index index.html;
location / {
try_files \$uri \$uri/ /index.html;
}
}
EOF
COPY --from=builder /app/app/build /app
EXPOSE 80

View File

@@ -1,7 +1 @@
# Tauri + Svelte + Typescript # Nodarium App
This template should help get you started developing with Tauri, Svelte and TypeScript in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).

View File

@@ -10,37 +10,38 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@nodarium/registry": "link:../packages/registry", "@nodarium/registry": "workspace:*",
"@nodarium/ui": "link:../packages/ui", "@nodarium/ui": "workspace:*",
"@nodarium/utils": "link:../packages/utils", "@nodarium/utils": "workspace:*",
"@sveltejs/kit": "^2.49.0", "@sveltejs/kit": "^2.50.0",
"@threlte/core": "8.3.0", "@tailwindcss/vite": "^4.1.18",
"@threlte/extras": "9.7.0", "@threlte/core": "8.3.1",
"@types/three": "^0.181.0", "@threlte/extras": "9.7.1",
"@unocss/reset": "^66.5.9",
"comlink": "^4.4.2", "comlink": "^4.4.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"idb": "^8.0.3", "idb": "^8.0.3",
"jsondiffpatch": "^0.7.3", "jsondiffpatch": "^0.7.3",
"three": "^0.181.2" "tailwindcss": "^4.1.18",
"three": "^0.182.0",
"wabt": "^1.0.39"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/tabler": "^1.2.23", "@iconify-json/tabler": "^1.2.26",
"@nodarium/types": "link:../packages/types", "@iconify/tailwind4": "^1.2.1",
"@nodarium/types": "workspace:",
"@sveltejs/adapter-static": "^3.0.10", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/vite-plugin-svelte": "^6.2.1", "@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tsconfig/svelte": "^5.0.6", "@tsconfig/svelte": "^5.0.6",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@unocss/preset-icons": "^66.5.9", "@types/three": "^0.182.0",
"svelte": "^5.43.14", "svelte": "^5.46.4",
"svelte-check": "^4.3.4", "svelte-check": "^4.3.5",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"unocss": "^66.5.9", "vite": "^7.3.1",
"vite": "^7.2.4",
"vite-plugin-comlink": "^5.3.0", "vite-plugin-comlink": "^5.3.0",
"vite-plugin-glsl": "^1.5.4", "vite-plugin-glsl": "^1.5.5",
"vite-plugin-wasm": "^3.5.0", "vite-plugin-wasm": "^3.5.0",
"vitest": "^4.0.13" "vitest": "^4.0.17"
} }
} }

1576
app/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

6
app/src/app.css Normal file
View File

@@ -0,0 +1,6 @@
@import "tailwindcss";
@source "../../packages/ui/**/*.svelte";
@plugin "@iconify/tailwind4" {
prefix: "i";
icon-sets: from-folder(custom, "./src/lib/icons")
};

View File

@@ -5,7 +5,6 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/svelte.svg" /> <link rel="icon" href="%sveltekit.assets%/svelte.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<script defer src="https://umami.max-richter.dev/script.js" data-website-id="585c442b-0524-4874-8955-f9853b44b17e"></script>
%sveltekit.head% %sveltekit.head%
<title>Nodes</title> <title>Nodes</title>
<script> <script>

2
app/src/lib/config.ts Normal file
View File

@@ -0,0 +1,2 @@
import { PUBLIC_ANALYTIC_SCRIPT } from "$env/static/public";
export const ANALYTIC_SCRIPT = PUBLIC_ANALYTIC_SCRIPT;

View File

@@ -1,25 +1,32 @@
<script lang="ts"> <script lang="ts">
import { HTML } from "@threlte/extras"; import type { NodeId, NodeInstance } from '@nodarium/types';
import { onMount } from "svelte"; import { HTML } from '@threlte/extras';
import type { Node, NodeType } from "@nodarium/types"; import { onMount } from 'svelte';
import { getGraphManager, getGraphState } from "../graph/state.svelte"; import { getGraphManager, getGraphState } from '../graph-state.svelte';
type Props = {
onnode: (n: NodeInstance) => void;
};
const { onnode }: Props = $props();
const graph = getGraphManager(); const graph = getGraphManager();
const graphState = getGraphState(); const graphState = getGraphState();
let input: HTMLInputElement; let input: HTMLInputElement;
let wrapper: HTMLDivElement;
let value = $state<string>(); let value = $state<string>();
let activeNodeId = $state<NodeType>(); let activeNodeId = $state<NodeId>();
const allNodes = graphState.activeSocket const allNodes = graphState.activeSocket
? graph.getPossibleNodes(graphState.activeSocket) ? graph.getPossibleNodes(graphState.activeSocket)
: graph.getNodeDefinitions(); : graph.getNodeDefinitions();
function filterNodes() { function filterNodes() {
return allNodes.filter((node) => node.id.includes(value ?? "")); return allNodes.filter((node) => node.id.includes(value ?? ''));
} }
const nodes = $derived(value === "" ? allNodes : filterNodes()); const nodes = $derived(value === '' ? allNodes : filterNodes());
$effect(() => { $effect(() => {
if (nodes) { if (nodes) {
if (activeNodeId === undefined) { if (activeNodeId === undefined) {
@@ -33,49 +40,38 @@
} }
}); });
function handleNodeCreation(nodeType: Node["type"]) { function handleNodeCreation(nodeType: NodeInstance['type']) {
if (!graphState.addMenuPosition) return; if (!graphState.addMenuPosition) return;
onnode?.({
const newNode = graph.createNode({ id: -1,
type: nodeType, type: nodeType,
position: graphState.addMenuPosition, position: [...graphState.addMenuPosition],
props: {}, props: {},
state: {}
}); });
const edgeInputSocket = graphState.activeSocket;
if (edgeInputSocket && newNode) {
if (typeof edgeInputSocket.index === "number") {
graph.smartConnect(edgeInputSocket.node, newNode);
} else {
graph.smartConnect(newNode, edgeInputSocket.node);
}
}
graphState.activeSocket = null;
graphState.addMenuPosition = null;
} }
function handleKeyDown(event: KeyboardEvent) { function handleKeyDown(event: KeyboardEvent) {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
if (event.key === "Escape") { if (event.key === 'Escape') {
graphState.addMenuPosition = null; graphState.addMenuPosition = null;
return; return;
} }
if (event.key === "ArrowDown") { if (event.key === 'ArrowDown') {
const index = nodes.findIndex((node) => node.id === activeNodeId); const index = nodes.findIndex((node) => node.id === activeNodeId);
activeNodeId = nodes[(index + 1) % nodes.length].id; activeNodeId = nodes[(index + 1) % nodes.length].id;
return; return;
} }
if (event.key === "ArrowUp") { if (event.key === 'ArrowUp') {
const index = nodes.findIndex((node) => node.id === activeNodeId); const index = nodes.findIndex((node) => node.id === activeNodeId);
activeNodeId = nodes[(index - 1 + nodes.length) % nodes.length].id; activeNodeId = nodes[(index - 1 + nodes.length) % nodes.length].id;
return; return;
} }
if (event.key === "Enter") { if (event.key === 'Enter') {
if (activeNodeId && graphState.addMenuPosition) { if (activeNodeId && graphState.addMenuPosition) {
handleNodeCreation(activeNodeId); handleNodeCreation(activeNodeId);
} }
@@ -86,6 +82,16 @@
onMount(() => { onMount(() => {
input.disabled = false; input.disabled = false;
setTimeout(() => input.focus(), 50); setTimeout(() => input.focus(), 50);
const rect = wrapper.getBoundingClientRect();
const deltaY = rect.bottom - window.innerHeight;
const deltaX = rect.right - window.innerWidth;
if (deltaY > 0) {
wrapper.style.marginTop = `-${deltaY + 30}px`;
}
if (deltaX > 0) {
wrapper.style.marginLeft = `-${deltaX + 30}px`;
}
}); });
</script> </script>
@@ -94,7 +100,7 @@
position.z={graphState.addMenuPosition?.[1]} position.z={graphState.addMenuPosition?.[1]}
transform={false} transform={false}
> >
<div class="add-menu-wrapper"> <div class="add-menu-wrapper" bind:this={wrapper}>
<div class="header"> <div class="header">
<input <input
id="add-menu" id="add-menu"
@@ -117,7 +123,7 @@
tabindex="0" tabindex="0"
aria-selected={node.id === activeNodeId} aria-selected={node.id === activeNodeId}
onkeydown={(event) => { onkeydown={(event) => {
if (event.key === "Enter") { if (event.key === 'Enter') {
handleNodeCreation(node.id); handleNodeCreation(node.id);
} }
}} }}
@@ -130,7 +136,7 @@
activeNodeId = node.id; activeNodeId = node.id;
}} }}
> >
{node.id.split("/").at(-1)} {node.id.split('/').at(-1)}
</div> </div>
{/each} {/each}
</div> </div>
@@ -172,6 +178,8 @@
min-height: none; min-height: none;
width: 100%; width: 100%;
color: var(--text-color); color: var(--text-color);
max-height: 300px;
overflow-y: auto;
} }
.result { .result {

View File

@@ -1,19 +1,44 @@
<script lang="ts"> <script lang="ts">
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras'; import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
import { points, lines } from './store.js'; import { points, lines, rects } from "./store.js";
import { T } from '@threlte/core'; import { T } from "@threlte/core";
import { Color } from "three";
</script> </script>
{#each $points as point} {#each $points as point}
<T.Mesh position.x={point.x} position.y={point.y} position.z={point.z} rotation.x={-Math.PI / 2}> <T.Mesh
<T.CircleGeometry args={[0.2, 32]} /> position.x={point.x}
<T.MeshBasicMaterial color="red" /> position.y={point.y}
</T.Mesh> position.z={point.z}
rotation.x={-Math.PI / 2}
>
<T.CircleGeometry args={[0.2, 32]} />
<T.MeshBasicMaterial color="red" />
</T.Mesh>
{/each}
{#each $rects as rect, i}
<T.Mesh
position.x={(rect.minX + rect.maxX) / 2}
position.y={0}
position.z={(rect.minY + rect.maxY) / 2}
rotation.x={-Math.PI / 2}
>
<T.PlaneGeometry args={[rect.maxX - rect.minX, rect.maxY - rect.minY]} />
<T.MeshBasicMaterial
color={new Color().setHSL((i * 1.77) % 1, 1, 0.5)}
opacity={0.9}
/>
</T.Mesh>
{/each} {/each}
{#each $lines as line} {#each $lines as line}
<T.Mesh> <T.Mesh position.y={1}>
<MeshLineGeometry points={line} /> <MeshLineGeometry points={line.points} />
<MeshLineMaterial color="red" linewidth={1} attenuate={false} /> <MeshLineMaterial
</T.Mesh> color={line.color || "red"}
linewidth={1}
attenuate={false}
/>
</T.Mesh>
{/each} {/each}

View File

@@ -1,5 +1,8 @@
import { Vector3 } from "three/src/math/Vector3.js"; import type { Box } from '@nodarium/types';
import { lines, points } from "./store"; import type { Color } from 'three';
import { Vector3 } from 'three/src/math/Vector3.js';
import Component from './Debug.svelte';
import { lines, points, rects } from './store';
export function debugPosition(x: number, y: number) { export function debugPosition(x: number, y: number) {
points.update((p) => { points.update((p) => {
@@ -8,18 +11,27 @@ export function debugPosition(x: number, y: number) {
}); });
} }
export function debugRect(rect: Box) {
rects.update((r) => {
r.push(rect);
return r;
});
}
export function clear() { export function clear() {
points.set([]); points.set([]);
lines.set([]); lines.set([]);
rects.set([]);
} }
export function debugLine(line: Vector3[]) { export function debugLine(points: Vector3[], color?: Color) {
lines.update((l) => { lines.update((l) => {
l.push(line); l.push({ points, color });
return l; return l;
}); });
} }
import Component from "./Debug.svelte"; export default Component;
export function clearLines() {
export default Component lines.set([]);
}

View File

@@ -1,6 +1,8 @@
import { writable } from "svelte/store"; import type { Box } from '@nodarium/types';
import { Vector3 } from "three/src/math/Vector3.js"; import { writable } from 'svelte/store';
import type { Color } from 'three';
import { Vector3 } from 'three/src/math/Vector3.js';
export const points = writable<Vector3[]>([]); export const points = writable<Vector3[]>([]);
export const rects = writable<Box[]>([]);
export const lines = writable<Vector3[][]>([]); export const lines = writable<{ points: Vector3[]; color?: Color }[]>([]);

View File

@@ -6,10 +6,13 @@
toneMapped: false, toneMapped: false,
}); });
let lineColor = $state(colors.edge.clone().convertSRGBToLinear());
$effect.root(() => { $effect.root(() => {
$effect(() => { $effect(() => {
appSettings.value.theme; appSettings.value.theme;
circleMaterial.color = colors.edge.clone().convertSRGBToLinear(); circleMaterial.color = colors.edge.clone().convertSRGBToLinear();
lineColor = colors.edge.clone().convertSRGBToLinear();
}); });
}); });
@@ -23,39 +26,41 @@
<script lang="ts"> <script lang="ts">
import { T } from "@threlte/core"; import { T } from "@threlte/core";
import { MeshLineMaterial } from "@threlte/extras"; import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
import { Mesh, MeshBasicMaterial, Vector3 } from "three"; import { MeshBasicMaterial, Vector3 } from "three";
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js"; import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
import { Vector2 } from "three/src/math/Vector2.js"; import { Vector2 } from "three/src/math/Vector2.js";
import { createEdgeGeometry } from "./createEdgeGeometry.js";
import { appSettings } from "$lib/settings/app-settings.svelte"; import { appSettings } from "$lib/settings/app-settings.svelte";
import { getGraphState } from "../graph-state.svelte";
import { onDestroy } from "svelte";
const graphState = getGraphState();
type Props = { type Props = {
from: { x: number; y: number }; x1: number;
to: { x: number; y: number }; y1: number;
x2: number;
y2: number;
z: number; z: number;
id?: string;
}; };
const { from, to, z }: Props = $props(); const { x1, y1, x2, y2, z, id }: Props = $props();
let mesh = $state<Mesh>(); const thickness = $derived(Math.max(0.001, 0.00082 * Math.exp(0.055 * z)));
const lineColor = $derived( let points = $state<Vector3[]>([]);
appSettings.value.theme && colors.edge.clone().convertSRGBToLinear(),
);
let lastId: number | null = null; let lastId: string | null = null;
const curveId = $derived(`${x1}-${y1}-${x2}-${y2}`);
const primeA = 31;
const primeB = 37;
function update() { function update() {
const new_x = to.x - from.x; const new_x = x2 - x1;
const new_y = to.y - from.y; const new_y = y2 - y1;
const curveId = new_x * primeA + new_y * primeB;
if (lastId === curveId) { if (lastId === curveId) {
return; return;
} }
lastId = curveId;
const length = Math.floor( const length = Math.floor(
Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4, Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4,
@@ -68,26 +73,35 @@
curve.v2.set(new_x / 2, new_y); curve.v2.set(new_x / 2, new_y);
curve.v3.set(new_x, new_y); curve.v3.set(new_x, new_y);
const points = curve points = curve
.getPoints(samples) .getPoints(samples)
.map((p) => new Vector3(p.x, 0, p.y)) .map((p) => new Vector3(p.x, 0, p.y))
.flat(); .flat();
if (mesh) { if (id) {
mesh.geometry = createEdgeGeometry(points); graphState.setEdgeGeometry(
id,
x1,
y1,
$state.snapshot(points) as unknown as Vector3[],
);
} }
} }
$effect(() => { $effect(() => {
if (from || to) { if (x1 || x2 || y1 || y2) {
update(); update();
} }
}); });
onDestroy(() => {
if (id) graphState.removeEdgeGeometry(id);
});
</script> </script>
<T.Mesh <T.Mesh
position.x={from.x} position.x={x1}
position.z={from.y} position.z={y1}
position.y={0.8} position.y={0.8}
rotation.x={-Math.PI / 2} rotation.x={-Math.PI / 2}
material={circleMaterial} material={circleMaterial}
@@ -96,8 +110,8 @@
</T.Mesh> </T.Mesh>
<T.Mesh <T.Mesh
position.x={to.x} position.x={x2}
position.z={to.y} position.z={y2}
position.y={0.8} position.y={0.8}
rotation.x={-Math.PI / 2} rotation.x={-Math.PI / 2}
material={circleMaterial} material={circleMaterial}
@@ -105,11 +119,19 @@
<T.CircleGeometry args={[0.5, 16]} /> <T.CircleGeometry args={[0.5, 16]} />
</T.Mesh> </T.Mesh>
<T.Mesh {#if graphState.hoveredEdgeId === id}
bind:ref={mesh} <T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
position.x={from.x} <MeshLineGeometry {points} />
position.z={from.y} <MeshLineMaterial
position.y={0.1} width={thickness * 5}
> color={lineColor}
<MeshLineMaterial width={Math.max(z * 0.00012, 0.00003)} color={lineColor} /> opacity={0.5}
transparent
/>
</T.Mesh>
{/if}
<T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
<MeshLineGeometry {points} />
<MeshLineMaterial width={thickness} color={lineColor} />
</T.Mesh> </T.Mesh>

View File

@@ -1,12 +0,0 @@
<script lang="ts">
import Edge from "./Edge.svelte";
type Props = {
from: { x: number; y: number };
to: { x: number; y: number };
z: number;
};
const { from, to, z }: Props = $props();
</script>
<Edge {from} {to} {z} />

View File

@@ -1,112 +0,0 @@
import { BufferAttribute, BufferGeometry, Vector3 } from 'three';
import { setXY, setXYZ, setXYZW, setXYZXYZ } from './utils.js';
export function createEdgeGeometry(points: Vector3[]) {
const length = points[0].distanceTo(points[points.length - 1]);
const startRadius = 8;
const constantWidth = 2;
const taperFraction = 0.8 / length;
function ease(t: number) {
return t * t * (3 - 2 * t);
}
let shapeFunction = (alpha: number) => {
if (alpha < taperFraction) {
const easedAlpha = ease(alpha / taperFraction);
return startRadius + (constantWidth - startRadius) * easedAlpha;
} else if (alpha > 1 - taperFraction) {
const easedAlpha = ease((alpha - (1 - taperFraction)) / taperFraction);
return constantWidth + (startRadius - constantWidth) * easedAlpha;
} else {
return constantWidth;
}
};
// When the component first runs we create the buffer geometry and allocate the buffer attributes
let pointCount = points.length
let counters: number[] = []
let counterIndex = 0
let side: number[] = []
let widthArray: number[] = []
let doubleIndex = 0
let uvArray: number[] = []
let uvIndex = 0
let indices: number[] = []
let indicesIndex = 0
for (let j = 0; j < pointCount; j++) {
const c = j / points.length
counters[counterIndex + 0] = c
counters[counterIndex + 1] = c
counterIndex += 2
setXY(side, doubleIndex, 1, -1)
let width = shapeFunction((j / (pointCount - 1)))
setXY(widthArray, doubleIndex, width, width)
doubleIndex += 2
setXYZW(uvArray, uvIndex, j / (pointCount - 1), 0, j / (pointCount - 1), 1)
uvIndex += 4
if (j < pointCount - 1) {
const n = j * 2
setXYZ(indices, indicesIndex, n + 0, n + 1, n + 2)
setXYZ(indices, indicesIndex + 3, n + 2, n + 1, n + 3)
indicesIndex += 6
}
}
const geometry = new BufferGeometry()
// create these buffer attributes at the correct length but leave them empty for now
geometry.setAttribute('position', new BufferAttribute(new Float32Array(pointCount * 6), 3))
geometry.setAttribute('previous', new BufferAttribute(new Float32Array(pointCount * 6), 3))
geometry.setAttribute('next', new BufferAttribute(new Float32Array(pointCount * 6), 3))
// create and populate these buffer attributes
geometry.setAttribute('counters', new BufferAttribute(new Float32Array(counters), 1))
geometry.setAttribute('side', new BufferAttribute(new Float32Array(side), 1))
geometry.setAttribute('width', new BufferAttribute(new Float32Array(widthArray), 1))
geometry.setAttribute('uv', new BufferAttribute(new Float32Array(uvArray), 2))
geometry.setIndex(new BufferAttribute(new Uint16Array(indices), 1))
let positions: number[] = []
let previous: number[] = []
let next: number[] = []
let positionIndex = 0
let previousIndex = 0
let nextIndex = 0
setXYZXYZ(previous, previousIndex, points[0].x, points[0].y, points[0].z)
previousIndex += 6
for (let j = 0; j < pointCount; j++) {
const p = points[j]
setXYZXYZ(positions, positionIndex, p.x, p.y, p.z)
positionIndex += 6
if (j < pointCount - 1) {
setXYZXYZ(previous, previousIndex, p.x, p.y, p.z)
previousIndex += 6
}
if (j > 0 && j + 1 <= pointCount) {
setXYZXYZ(next, nextIndex, p.x, p.y, p.z)
nextIndex += 6
}
}
setXYZXYZ(
next,
nextIndex,
points[pointCount - 1].x,
points[pointCount - 1].y,
points[pointCount - 1].z
)
const positionAttribute = (geometry.getAttribute('position') as BufferAttribute).set(positions)
const previousAttribute = (geometry.getAttribute('previous') as BufferAttribute).set(previous)
const nextAttribute = (geometry.getAttribute('next') as BufferAttribute).set(next)
positionAttribute.needsUpdate = true
previousAttribute.needsUpdate = true
nextAttribute.needsUpdate = true
geometry.computeBoundingSphere()
return geometry;
}

View File

@@ -1,31 +1,33 @@
import throttle from '$lib/helpers/throttle';
import { RemoteNodeRegistry } from '@nodarium/registry';
import type { import type {
Edge, Edge,
Graph, Graph,
Node,
NodeDefinition, NodeDefinition,
NodeId,
NodeInput, NodeInput,
NodeInstance,
NodeRegistry, NodeRegistry,
NodeType, Socket
Socket, } from '@nodarium/types';
} from "@nodarium/types"; import { fastHashString } from '@nodarium/utils';
import { fastHashString } from "@nodarium/utils"; import { createLogger } from '@nodarium/utils';
import { SvelteMap } from "svelte/reactivity"; import { SvelteMap } from 'svelte/reactivity';
import EventEmitter from "./helpers/EventEmitter"; import EventEmitter from './helpers/EventEmitter';
import { createLogger } from "@nodarium/utils"; import { HistoryManager } from './history-manager';
import throttle from "$lib/helpers/throttle";
import { HistoryManager } from "./history-manager";
const logger = createLogger("graph-manager"); const logger = createLogger('graph-manager');
logger.mute(); logger.mute();
const clone = const remoteRegistry = new RemoteNodeRegistry('');
"structuredClone" in self
? self.structuredClone const clone = 'structuredClone' in self
: (args: any) => JSON.parse(JSON.stringify(args)); ? self.structuredClone
: (args: any) => JSON.parse(JSON.stringify(args));
function areSocketsCompatible( function areSocketsCompatible(
output: string | undefined, output: string | undefined,
inputs: string | (string | undefined)[] | undefined, inputs: string | (string | undefined)[] | undefined
) { ) {
if (Array.isArray(inputs) && output) { if (Array.isArray(inputs) && output) {
return inputs.includes(output); return inputs.includes(output);
@@ -34,24 +36,23 @@ function areSocketsCompatible(
} }
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) { function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
if (firstEdge[0].id !== secondEdge[0].id) { if (firstEdge[0].id !== secondEdge[0].id) {
return false; return false;
} }
if (firstEdge[1] !== secondEdge[1]) { if (firstEdge[1] !== secondEdge[1]) {
return false return false;
} }
if (firstEdge[2].id !== secondEdge[2].id) { if (firstEdge[2].id !== secondEdge[2].id) {
return false return false;
} }
if (firstEdge[3] !== secondEdge[3]) { if (firstEdge[3] !== secondEdge[3]) {
return false return false;
} }
return true return true;
} }
export class GraphManager extends EventEmitter<{ export class GraphManager extends EventEmitter<{
@@ -62,13 +63,13 @@ export class GraphManager extends EventEmitter<{
values: Record<string, unknown>; values: Record<string, unknown>;
}; };
}> { }> {
status = $state<"loading" | "idle" | "error">(); status = $state<'loading' | 'idle' | 'error'>();
loaded = false; loaded = false;
graph: Graph = { id: 0, nodes: [], edges: [] }; graph: Graph = { id: 0, nodes: [], edges: [] };
id = $state(0); id = $state(0);
nodes = new SvelteMap<number, Node>(); nodes = new SvelteMap<number, NodeInstance>();
edges = $state<Edge[]>([]); edges = $state<Edge[]>([]);
@@ -88,7 +89,7 @@ export class GraphManager extends EventEmitter<{
history: HistoryManager = new HistoryManager(); history: HistoryManager = new HistoryManager();
execute = throttle(() => { execute = throttle(() => {
if (this.loaded === false) return; if (this.loaded === false) return;
this.emit("result", this.serialize()); this.emit('result', this.serialize());
}, 10); }, 10);
constructor(public registry: NodeRegistry) { constructor(public registry: NodeRegistry) {
@@ -100,21 +101,22 @@ export class GraphManager extends EventEmitter<{
id: node.id, id: node.id,
position: [...node.position], position: [...node.position],
type: node.type, type: node.type,
props: node.props, props: node.props
})) as Node[]; })) as NodeInstance[];
const edges = this.edges.map((edge) => [ const edges = this.edges.map((edge) => [
edge[0].id, edge[0].id,
edge[1], edge[1],
edge[2].id, edge[2].id,
edge[3], edge[3]
]) as Graph["edges"]; ]) as Graph['edges'];
const serialized = { const serialized = {
id: this.graph.id, id: this.graph.id,
settings: $state.snapshot(this.settings), settings: $state.snapshot(this.settings),
meta: $state.snapshot(this.graph.meta),
nodes, nodes,
edges, edges
}; };
logger.log("serializing graph", serialized); logger.log('serializing graph', serialized);
return clone($state.snapshot(serialized)); return clone($state.snapshot(serialized));
} }
@@ -133,8 +135,8 @@ export class GraphManager extends EventEmitter<{
return this.registry.getAllNodes(); return this.registry.getAllNodes();
} }
getLinkedNodes(node: Node) { getLinkedNodes(node: NodeInstance) {
const nodes = new Set<Node>(); const nodes = new Set<NodeInstance>();
const stack = [node]; const stack = [node];
while (stack.length) { while (stack.length) {
const n = stack.pop(); const n = stack.pop();
@@ -148,21 +150,112 @@ export class GraphManager extends EventEmitter<{
return [...nodes.values()]; return [...nodes.values()];
} }
getEdgesBetweenNodes(nodes: Node[]): [number, number, number, string][] { getEdgeId(e: Edge) {
return `${e[0].id}-${e[1]}-${e[2].id}-${e[3]}`;
}
getEdgeById(id: string): Edge | undefined {
return this.edges.find((e) => this.getEdgeId(e) === id);
}
dropNodeOnEdge(nodeId: number, edge: Edge) {
const draggedNode = this.getNode(nodeId);
if (!draggedNode || !draggedNode.state?.type) return;
const [fromNode, fromSocketIdx, toNode, toSocketKey] = edge;
const draggedInputs = Object.entries(draggedNode.state.type.inputs ?? {});
const draggedOutputs = draggedNode.state.type.outputs ?? [];
const edgeOutputSocketType = fromNode.state?.type?.outputs?.[fromSocketIdx];
const targetInput = toNode.state?.type?.inputs?.[toSocketKey];
const targetAcceptedTypes = [targetInput?.type, ...(targetInput?.accepts || [])];
const bestInputEntry = draggedInputs.find(([_, input]) => {
const accepted = [input.type, ...(input.accepts || [])];
return areSocketsCompatible(edgeOutputSocketType, accepted);
});
const bestOutputIdx = draggedOutputs.findIndex(outputType =>
areSocketsCompatible(outputType, targetAcceptedTypes)
);
if (!bestInputEntry || bestOutputIdx === -1) {
logger.error('Could not find compatible sockets for drop');
return;
}
this.startUndoGroup();
this.removeEdge(edge, { applyDeletion: false });
this.createEdge(fromNode, fromSocketIdx, draggedNode, bestInputEntry[0], {
applyUpdate: false
});
this.createEdge(draggedNode, bestOutputIdx, toNode, toSocketKey, {
applyUpdate: false
});
this.saveUndoGroup();
this.execute();
}
getPossibleDropOnEdges(nodeId: number): Edge[] {
const draggedNode = this.getNode(nodeId);
if (!draggedNode || !draggedNode.state?.type) return [];
const draggedInputs = Object.values(draggedNode.state.type.inputs ?? {});
const draggedOutputs = draggedNode.state.type.outputs ?? [];
// Optimization: Pre-calculate parents to avoid cycles
const parentIds = new Set(this.getParentsOfNode(draggedNode).map(n => n.id));
return this.edges.filter((edge) => {
const [fromNode, fromSocketIdx, toNode, toSocketKey] = edge;
// 1. Prevent cycles: If the target node is already a parent, we can't drop here
if (parentIds.has(toNode.id)) return false;
// 2. Prevent self-dropping: Don't drop on edges already connected to this node
if (fromNode.id === nodeId || toNode.id === nodeId) return false;
// 3. Check if edge.source can plug into ANY draggedNode.input
const edgeOutputSocketType = fromNode.state?.type?.outputs?.[fromSocketIdx];
const canPlugIntoDragged = draggedInputs.some(input => {
const acceptedTypes = [input.type, ...(input.accepts || [])];
return areSocketsCompatible(edgeOutputSocketType, acceptedTypes);
});
if (!canPlugIntoDragged) return false;
// 4. Check if ANY draggedNode.output can plug into edge.target
const targetInput = toNode.state?.type?.inputs?.[toSocketKey];
const targetAcceptedTypes = [targetInput?.type, ...(targetInput?.accepts || [])];
const draggedCanPlugIntoTarget = draggedOutputs.some(outputType =>
areSocketsCompatible(outputType, targetAcceptedTypes)
);
return draggedCanPlugIntoTarget;
});
}
getEdgesBetweenNodes(nodes: NodeInstance[]): [number, number, number, string][] {
const edges = []; const edges = [];
for (const node of nodes) { for (const node of nodes) {
const children = node.tmp?.children || []; const children = node.state?.children || [];
for (const child of children) { for (const child of children) {
if (nodes.includes(child)) { if (nodes.includes(child)) {
const edge = this.edges.find( const edge = this.edges.find(
(e) => e[0].id === node.id && e[2].id === child.id, (e) => e[0].id === node.id && e[2].id === child.id
); );
if (edge) { if (edge) {
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [ edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [
number, number,
number, number,
number, number,
string, string
]); ]);
} }
} }
@@ -174,35 +267,31 @@ export class GraphManager extends EventEmitter<{
private _init(graph: Graph) { private _init(graph: Graph) {
const nodes = new Map( const nodes = new Map(
graph.nodes.map((node: Node) => { graph.nodes.map((node) => {
const nodeType = this.registry.getNode(node.type); const nodeType = this.registry.getNode(node.type);
const n = node as NodeInstance;
if (nodeType) { if (nodeType) {
node.tmp = { n.state = {
random: (Math.random() - 0.5) * 2, type: nodeType
type: nodeType,
}; };
} }
return [node.id, node]; return [node.id, n];
}), })
); );
const edges = graph.edges.map((edge) => { this.edges = graph.edges.map((edge) => {
const from = nodes.get(edge[0]); const from = nodes.get(edge[0]);
const to = nodes.get(edge[2]); const to = nodes.get(edge[2]);
if (!from || !to) { if (!from || !to) {
throw new Error("Edge references non-existing node"); throw new Error('Edge references non-existing node');
} }
from.tmp = from.tmp || {}; from.state.children = from.state.children || [];
from.tmp.children = from.tmp.children || []; from.state.children.push(to);
from.tmp.children.push(to); to.state.parents = to.state.parents || [];
to.tmp = to.tmp || {}; to.state.parents.push(from);
to.tmp.parents = to.tmp.parents || [];
to.tmp.parents.push(from);
return [from, edge[1], to, edge[3]] as Edge; return [from, edge[1], to, edge[3]] as Edge;
}); });
this.edges = [...edges];
this.nodes.clear(); this.nodes.clear();
for (const [id, node] of nodes) { for (const [id, node] of nodes) {
this.nodes.set(id, node); this.nodes.set(id, node);
@@ -216,26 +305,42 @@ export class GraphManager extends EventEmitter<{
this.loaded = false; this.loaded = false;
this.graph = graph; this.graph = graph;
this.status = "loading"; this.status = 'loading';
this.id = graph.id; this.id = graph.id;
logger.info("loading graph", $state.snapshot(graph)); logger.info('loading graph', { nodes: graph.nodes, edges: graph.edges, id: graph.id });
const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)])); const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)]));
await this.registry.load(nodeIds); await this.registry.load(nodeIds);
logger.info("loaded node types", this.registry.getAllNodes()); // Fetch all nodes from all collections of the loaded nodes
const allCollections = new Set<`${string}/${string}`>();
for (const id of nodeIds) {
const [user, collection] = id.split('/');
allCollections.add(`${user}/${collection}`);
}
for (const collection of allCollections) {
remoteRegistry
.fetchCollection(collection)
.then((collection: { nodes: { id: NodeId }[] }) => {
const ids = collection.nodes.map((n) => n.id);
return this.registry.load(ids);
});
}
logger.info('loaded node types', this.registry.getAllNodes());
for (const node of this.graph.nodes) { for (const node of this.graph.nodes) {
const nodeType = this.registry.getNode(node.type); const nodeType = this.registry.getNode(node.type);
if (!nodeType) { if (!nodeType) {
logger.error(`Node type not found: ${node.type}`); logger.error(`Node type not found: ${node.type}`);
this.status = "error"; this.status = 'error';
return; return;
} }
node.tmp = node.tmp || {}; // Turn into runtime node
node.tmp.random = (Math.random() - 0.5) * 2; const n = node as NodeInstance;
node.tmp.type = nodeType; n.state = {};
n.state.type = nodeType;
} }
// load settings // load settings
@@ -254,11 +359,11 @@ export class GraphManager extends EventEmitter<{
settingTypes[settingId] = { settingTypes[settingId] = {
__node_type: type.id, __node_type: type.id,
__node_input: key, __node_input: key,
...type.inputs[key], ...type.inputs[key]
}; };
if ( if (
settingValues[settingId] === undefined && settingValues[settingId] === undefined
"value" in type.inputs[key] && 'value' in type.inputs[key]
) { ) {
settingValues[settingId] = type.inputs[key].value; settingValues[settingId] = type.inputs[key].value;
} }
@@ -268,14 +373,14 @@ export class GraphManager extends EventEmitter<{
} }
this.settings = settingValues; this.settings = settingValues;
this.emit("settings", { types: settingTypes, values: settingValues }); this.emit('settings', { types: settingTypes, values: settingValues });
this.history.reset(); this.history.reset();
this._init(this.graph); this._init(this.graph);
this.save(); this.save();
this.status = "idle"; this.status = 'idle';
this.loaded = true; this.loaded = true;
logger.log(`Graph loaded in ${performance.now() - a}ms`); logger.log(`Graph loaded in ${performance.now() - a}ms`);
@@ -294,7 +399,7 @@ export class GraphManager extends EventEmitter<{
return this.registry.getNode(id); return this.registry.getNode(id);
} }
async loadNodeType(id: NodeType) { async loadNodeType(id: NodeId) {
await this.registry.load([id]); await this.registry.load([id]);
const nodeType = this.registry.getNode(id); const nodeType = this.registry.getNode(id);
@@ -308,9 +413,9 @@ export class GraphManager extends EventEmitter<{
if (settingId) { if (settingId) {
settingTypes[settingId] = nodeType.inputs[key]; settingTypes[settingId] = nodeType.inputs[key];
if ( if (
settingValues && settingValues
settingValues?.[settingId] === undefined && && settingValues?.[settingId] === undefined
"value" in nodeType.inputs[key] && 'value' in nodeType.inputs[key]
) { ) {
settingValues[settingId] = nodeType.inputs[key].value; settingValues[settingId] = nodeType.inputs[key].value;
} }
@@ -320,22 +425,22 @@ export class GraphManager extends EventEmitter<{
this.settings = settingValues; this.settings = settingValues;
this.settingTypes = settingTypes; this.settingTypes = settingTypes;
this.emit("settings", { types: settingTypes, values: settingValues }); this.emit('settings', { types: settingTypes, values: settingValues });
} }
getChildren(node: Node) { getChildren(node: NodeInstance) {
const children = []; const children = [];
const stack = node.tmp?.children?.slice(0); const stack = node.state?.children?.slice(0);
while (stack?.length) { while (stack?.length) {
const child = stack.pop(); const child = stack.pop();
if (!child) continue; if (!child) continue;
children.push(child); children.push(child);
stack.push(...(child.tmp?.children || [])); stack.push(...(child.state?.children || []));
} }
return children; return children;
} }
getNodesBetween(from: Node, to: Node): Node[] | undefined { getNodesBetween(from: NodeInstance, to: NodeInstance): NodeInstance[] | undefined {
// < - - - - from // < - - - - from
const toParents = this.getParentsOfNode(to); const toParents = this.getParentsOfNode(to);
// < - - - - from - - - - to // < - - - - from - - - - to
@@ -352,7 +457,7 @@ export class GraphManager extends EventEmitter<{
} }
} }
removeNode(node: Node, { restoreEdges = false } = {}) { removeNode(node: NodeInstance, { restoreEdges = false } = {}) {
const edgesToNode = this.edges.filter((edge) => edge[2].id === node.id); const edgesToNode = this.edges.filter((edge) => edge[2].id === node.id);
const edgesFromNode = this.edges.filter((edge) => edge[0].id === node.id); const edgesFromNode = this.edges.filter((edge) => edge[0].id === node.id);
for (const edge of [...edgesToNode, ...edgesFromNode]) { for (const edge of [...edgesToNode, ...edgesFromNode]) {
@@ -365,11 +470,11 @@ export class GraphManager extends EventEmitter<{
for (const [to, toSocket] of inputSockets) { for (const [to, toSocket] of inputSockets) {
for (const [from, fromSocket] of outputSockets) { for (const [from, fromSocket] of outputSockets) {
const outputType = from.tmp?.type?.outputs?.[fromSocket]; const outputType = from.state?.type?.outputs?.[fromSocket];
const inputType = to?.tmp?.type?.inputs?.[toSocket]?.type; const inputType = to?.state?.type?.inputs?.[toSocket]?.type;
if (outputType === inputType) { if (outputType === inputType) {
this.createEdge(from, fromSocket, to, toSocket, { this.createEdge(from, fromSocket, to, toSocket, {
applyUpdate: false, applyUpdate: false
}); });
continue; continue;
} }
@@ -382,9 +487,9 @@ export class GraphManager extends EventEmitter<{
this.save(); this.save();
} }
smartConnect(from: Node, to: Node): Edge | undefined { smartConnect(from: NodeInstance, to: NodeInstance): Edge | undefined {
const inputs = Object.entries(to.tmp?.type?.inputs ?? {}); const inputs = Object.entries(to.state?.type?.inputs ?? {});
const outputs = from.tmp?.type?.outputs ?? []; const outputs = from.state?.type?.outputs ?? [];
for (let i = 0; i < inputs.length; i++) { for (let i = 0; i < inputs.length; i++) {
const [inputName, input] = inputs[0]; const [inputName, input] = inputs[0];
for (let o = 0; o < outputs.length; o++) { for (let o = 0; o < outputs.length; o++) {
@@ -400,11 +505,11 @@ export class GraphManager extends EventEmitter<{
return Math.max(0, ...this.nodes.keys()) + 1; return Math.max(0, ...this.nodes.keys()) + 1;
} }
createGraph(nodes: Node[], edges: [number, number, number, string][]) { createGraph(nodes: NodeInstance[], edges: [number, number, number, string][]) {
// map old ids to new ids // map old ids to new ids
const idMap = new Map<number, number>(); const idMap = new Map<number, number>();
let startId = this.createNodeId() let startId = this.createNodeId();
nodes = nodes.map((node) => { nodes = nodes.map((node) => {
const id = startId++; const id = startId++;
@@ -421,16 +526,14 @@ export class GraphManager extends EventEmitter<{
const to = nodes.find((n) => n.id === idMap.get(edge[2])); const to = nodes.find((n) => n.id === idMap.get(edge[2]));
if (!from || !to) { if (!from || !to) {
throw new Error("Edge references non-existing node"); throw new Error('Edge references non-existing node');
} }
to.tmp = to.tmp || {}; to.state.parents = to.state.parents || [];
to.tmp.parents = to.tmp.parents || []; to.state.parents.push(from);
to.tmp.parents.push(from);
from.tmp = from.tmp || {}; from.state.children = from.state.children || [];
from.tmp.children = from.tmp.children || []; from.state.children.push(to);
from.tmp.children.push(to);
return [from, edge[1], to, edge[3]] as Edge; return [from, edge[1], to, edge[3]] as Edge;
}); });
@@ -448,11 +551,11 @@ export class GraphManager extends EventEmitter<{
createNode({ createNode({
type, type,
position, position,
props = {}, props = {}
}: { }: {
type: Node["type"]; type: NodeInstance['type'];
position: Node["position"]; position: NodeInstance['position'];
props: Node["props"]; props: NodeInstance['props'];
}) { }) {
const nodeType = this.registry.getNode(type); const nodeType = this.registry.getNode(type);
if (!nodeType) { if (!nodeType) {
@@ -460,56 +563,55 @@ export class GraphManager extends EventEmitter<{
return; return;
} }
const node: Node = { const node: NodeInstance = $state({
id: this.createNodeId(), id: this.createNodeId(),
type, type,
position, position,
tmp: { type: nodeType }, state: { type: nodeType },
props, props
}; });
this.nodes.set(node.id, node); this.nodes.set(node.id, node);
this.save(); this.save();
return node return node;
} }
createEdge( createEdge(
from: Node, from: NodeInstance,
fromSocket: number, fromSocket: number,
to: Node, to: NodeInstance,
toSocket: string, toSocket: string,
{ applyUpdate = true } = {}, { applyUpdate = true } = {}
): Edge | undefined { ): Edge | undefined {
const existingEdges = this.getEdgesToNode(to); const existingEdges = this.getEdgesToNode(to);
// check if this exact edge already exists // check if this exact edge already exists
const existingEdge = existingEdges.find( const existingEdge = existingEdges.find(
(e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket, (e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket
); );
if (existingEdge) { if (existingEdge) {
logger.error("Edge already exists", existingEdge); logger.error('Edge already exists', existingEdge);
return; return;
} }
// check if socket types match // check if socket types match
const fromSocketType = from.tmp?.type?.outputs?.[fromSocket]; const fromSocketType = from.state?.type?.outputs?.[fromSocket];
const toSocketType = [to.tmp?.type?.inputs?.[toSocket]?.type]; const toSocketType = [to.state?.type?.inputs?.[toSocket]?.type];
if (to.tmp?.type?.inputs?.[toSocket]?.accepts) { if (to.state?.type?.inputs?.[toSocket]?.accepts) {
toSocketType.push(...(to?.tmp?.type?.inputs?.[toSocket]?.accepts || [])); toSocketType.push(...(to?.state?.type?.inputs?.[toSocket]?.accepts || []));
} }
if (!areSocketsCompatible(fromSocketType, toSocketType)) { if (!areSocketsCompatible(fromSocketType, toSocketType)) {
logger.error( logger.error(
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`, `Socket types do not match: ${fromSocketType} !== ${toSocketType}`
); );
return; return;
} }
const edgeToBeReplaced = this.edges.find( const edgeToBeReplaced = this.edges.find(
(e) => e[2].id === to.id && e[3] === toSocket, (e) => e[2].id === to.id && e[3] === toSocket
); );
if (edgeToBeReplaced) { if (edgeToBeReplaced) {
this.removeEdge(edgeToBeReplaced, { applyDeletion: false }); this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
@@ -519,13 +621,10 @@ export class GraphManager extends EventEmitter<{
this.edges.push(edge); this.edges.push(edge);
from.tmp = from.tmp || {}; from.state.children = from.state.children || [];
from.tmp.children = from.tmp.children || []; from.state.children.push(to);
from.tmp.children.push(to); to.state.parents = to.state.parents || [];
to.state.parents.push(from);
to.tmp = to.tmp || {};
to.tmp.parents = to.tmp.parents || [];
to.tmp.parents.push(from);
if (applyUpdate) { if (applyUpdate) {
this.save(); this.save();
@@ -539,7 +638,7 @@ export class GraphManager extends EventEmitter<{
const nextState = this.history.undo(); const nextState = this.history.undo();
if (nextState) { if (nextState) {
this._init(nextState); this._init(nextState);
this.emit("save", this.serialize()); this.emit('save', this.serialize());
} }
} }
@@ -547,7 +646,7 @@ export class GraphManager extends EventEmitter<{
const nextState = this.history.redo(); const nextState = this.history.redo();
if (nextState) { if (nextState) {
this._init(nextState); this._init(nextState);
this.emit("save", this.serialize()); this.emit('save', this.serialize());
} }
} }
@@ -564,22 +663,29 @@ export class GraphManager extends EventEmitter<{
if (this.currentUndoGroup) return; if (this.currentUndoGroup) return;
const state = this.serialize(); const state = this.serialize();
this.history.save(state); this.history.save(state);
this.emit("save", state);
logger.log("saving graphs", state); // This is some stupid race condition where the graph-manager emits a save event
// when the graph is not fully loaded
if (this.nodes.size === 0 && this.edges.length === 0) {
return;
}
this.emit('save', state);
logger.log('saving graphs', state);
} }
getParentsOfNode(node: Node) { getParentsOfNode(node: NodeInstance) {
const parents = []; const parents = [];
const stack = node.tmp?.parents?.slice(0); const stack = node.state?.parents?.slice(0);
while (stack?.length) { while (stack?.length) {
if (parents.length > 1000000) { if (parents.length > 1000000) {
logger.warn("Infinite loop detected"); logger.warn('Infinite loop detected');
break; break;
} }
const parent = stack.pop(); const parent = stack.pop();
if (!parent) continue; if (!parent) continue;
parents.push(parent); parents.push(parent);
stack.push(...(parent.tmp?.parents || [])); stack.push(...(parent.state?.parents || []));
} }
return parents.reverse(); return parents.reverse();
} }
@@ -587,51 +693,53 @@ export class GraphManager extends EventEmitter<{
getPossibleNodes(socket: Socket): NodeDefinition[] { getPossibleNodes(socket: Socket): NodeDefinition[] {
const allDefinitions = this.getNodeDefinitions(); const allDefinitions = this.getNodeDefinitions();
const nodeType = socket.node.tmp?.type; const nodeType = socket.node.state?.type;
if (!nodeType) { if (!nodeType) {
return []; return [];
} }
const definitions = typeof socket.index === "string" const definitions = typeof socket.index === 'string'
? allDefinitions.filter(s => { ? allDefinitions.filter(s => {
return s.outputs?.find(_s => Object return s.outputs?.find(_s =>
.values(nodeType?.inputs || {}) Object
.map(s => s.type) .values(nodeType?.inputs || {})
.includes(_s as NodeInput["type"]) .map(s => s.type)
) .includes(_s as NodeInput['type'])
);
}) })
: allDefinitions.filter(s => Object : allDefinitions.filter(s =>
.values(s.inputs ?? {}) Object
.find(s => { .values(s.inputs ?? {})
if (s.hidden) return false; .find(s => {
if (nodeType.outputs?.includes(s.type)) { if (s.hidden) return false;
return true if (nodeType.outputs?.includes(s.type)) {
} return true;
return s.accepts?.find(a => nodeType.outputs?.includes(a)) }
})) return s.accepts?.find(a => nodeType.outputs?.includes(a));
})
return definitions );
return definitions;
} }
getPossibleSockets({ node, index }: Socket): [Node, string | number][] { getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
const nodeType = node?.tmp?.type; const nodeType = node?.state?.type;
if (!nodeType) return []; if (!nodeType) return [];
const sockets: [Node, string | number][] = []; const sockets: [NodeInstance, string | number][] = [];
// if index is a string, we are an input looking for outputs // if index is a string, we are an input looking for outputs
if (typeof index === "string") { if (typeof index === 'string') {
// filter out self and child nodes // filter out self and child nodes
const children = new Set(this.getChildren(node).map((n) => n.id)); const children = new Set(this.getChildren(node).map((n) => n.id));
const nodes = this.getAllNodes().filter( const nodes = this.getAllNodes().filter(
(n) => n.id !== node.id && !children.has(n.id), (n) => n.id !== node.id && !children.has(n.id)
); );
const ownType = nodeType?.inputs?.[index].type; const ownType = nodeType?.inputs?.[index].type;
for (const node of nodes) { for (const node of nodes) {
const nodeType = node?.tmp?.type; const nodeType = node?.state?.type;
const inputs = nodeType?.outputs; const inputs = nodeType?.outputs;
if (!inputs) continue; if (!inputs) continue;
for (let index = 0; index < inputs.length; index++) { for (let index = 0; index < inputs.length; index++) {
@@ -640,34 +748,34 @@ export class GraphManager extends EventEmitter<{
} }
} }
} }
} else if (typeof index === "number") { } else if (typeof index === 'number') {
// if index is a number, we are an output looking for inputs // if index is a number, we are an output looking for inputs
// filter out self and parent nodes // filter out self and parent nodes
const parents = new Set(this.getParentsOfNode(node).map((n) => n.id)); const parents = new Set(this.getParentsOfNode(node).map((n) => n.id));
const nodes = this.getAllNodes().filter( const nodes = this.getAllNodes().filter(
(n) => n.id !== node.id && !parents.has(n.id), (n) => n.id !== node.id && !parents.has(n.id)
); );
// get edges from this socket // get edges from this socket
const edges = new Map( const edges = new Map(
this.getEdgesFromNode(node) this.getEdgesFromNode(node)
.filter((e) => e[1] === index) .filter((e) => e[1] === index)
.map((e) => [e[2].id, e[3]]), .map((e) => [e[2].id, e[3]])
); );
const ownType = nodeType.outputs?.[index]; const ownType = nodeType.outputs?.[index];
for (const node of nodes) { for (const node of nodes) {
const inputs = node?.tmp?.type?.inputs; const inputs = node?.state?.type?.inputs;
if (!inputs) continue; if (!inputs) continue;
for (const key in inputs) { for (const key in inputs) {
const otherType = [inputs[key].type]; const otherType = [inputs[key].type];
otherType.push(...(inputs[key].accepts || [])); otherType.push(...(inputs[key].accepts || []));
if ( if (
areSocketsCompatible(ownType, otherType) && areSocketsCompatible(ownType, otherType)
edges.get(node.id) !== key && edges.get(node.id) !== key
) { ) {
sockets.push([node, key]); sockets.push([node, key]);
} }
@@ -680,7 +788,7 @@ export class GraphManager extends EventEmitter<{
removeEdge( removeEdge(
edge: Edge, edge: Edge,
{ applyDeletion = true }: { applyDeletion?: boolean } = {}, { applyDeletion = true }: { applyDeletion?: boolean } = {}
) { ) {
const id0 = edge[0].id; const id0 = edge[0].id;
const sid0 = edge[1]; const sid0 = edge[1];
@@ -688,23 +796,20 @@ export class GraphManager extends EventEmitter<{
const sid2 = edge[3]; const sid2 = edge[3];
const _edge = this.edges.find( const _edge = this.edges.find(
(e) => (e) => e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2
e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2,
); );
if (!_edge) return; if (!_edge) return;
edge[0].tmp = edge[0].tmp || {}; if (edge[0].state.children) {
if (edge[0].tmp.children) { edge[0].state.children = edge[0].state.children.filter(
edge[0].tmp.children = edge[0].tmp.children.filter( (n: NodeInstance) => n.id !== id2
(n: Node) => n.id !== id2,
); );
} }
edge[2].tmp = edge[2].tmp || {}; if (edge[2].state.parents) {
if (edge[2].tmp.parents) { edge[2].state.parents = edge[2].state.parents.filter(
edge[2].tmp.parents = edge[2].tmp.parents.filter( (n: NodeInstance) => n.id !== id0
(n: Node) => n.id !== id0,
); );
} }
@@ -713,10 +818,9 @@ export class GraphManager extends EventEmitter<{
this.execute(); this.execute();
this.save(); this.save();
} }
} }
getEdgesToNode(node: Node) { getEdgesToNode(node: NodeInstance) {
return this.edges return this.edges
.filter((edge) => edge[2].id === node.id) .filter((edge) => edge[2].id === node.id)
.map((edge) => { .map((edge) => {
@@ -725,10 +829,10 @@ export class GraphManager extends EventEmitter<{
if (!from || !to) return; if (!from || !to) return;
return [from, edge[1], to, edge[3]] as const; return [from, edge[1], to, edge[3]] as const;
}) })
.filter(Boolean) as unknown as [Node, number, Node, string][]; .filter(Boolean) as unknown as [NodeInstance, number, NodeInstance, string][];
} }
getEdgesFromNode(node: Node) { getEdgesFromNode(node: NodeInstance) {
return this.edges return this.edges
.filter((edge) => edge[0].id === node.id) .filter((edge) => edge[0].id === node.id)
.map((edge) => { .map((edge) => {
@@ -737,6 +841,6 @@ export class GraphManager extends EventEmitter<{
if (!from || !to) return; if (!from || !to) return;
return [from, edge[1], to, edge[3]] as const; return [from, edge[1], to, edge[3]] as const;
}) })
.filter(Boolean) as unknown as [Node, number, Node, string][]; .filter(Boolean) as unknown as [NodeInstance, number, NodeInstance, string][];
} }
} }

View File

@@ -1,44 +1,73 @@
import type { Node, Socket } from "@nodarium/types"; import type { NodeInstance, Socket } from '@nodarium/types';
import { getContext, setContext } from "svelte"; import { getContext, setContext } from 'svelte';
import { SvelteSet } from "svelte/reactivity"; import { SvelteSet } from 'svelte/reactivity';
import type { GraphManager } from "../graph-manager.svelte"; import type { OrthographicCamera, Vector3 } from 'three';
import type { OrthographicCamera } from "three"; import type { GraphManager } from './graph-manager.svelte';
const graphStateKey = Symbol('graph-state');
const graphStateKey = Symbol("graph-state");
export function getGraphState() { export function getGraphState() {
return getContext<GraphState>(graphStateKey); return getContext<GraphState>(graphStateKey);
} }
export function setGraphState(graphState: GraphState) { export function setGraphState(graphState: GraphState) {
return setContext(graphStateKey, graphState) return setContext(graphStateKey, graphState);
} }
const graphManagerKey = Symbol("graph-manager"); const graphManagerKey = Symbol('graph-manager');
export function getGraphManager() { export function getGraphManager() {
return getContext<GraphManager>(graphManagerKey) return getContext<GraphManager>(graphManagerKey);
} }
export function setGraphManager(manager: GraphManager) { export function setGraphManager(manager: GraphManager) {
return setContext(graphManagerKey, manager); return setContext(graphManagerKey, manager);
} }
export class GraphState { type EdgeData = {
x1: number;
y1: number;
points: Vector3[];
};
constructor(private graph: GraphManager) { } export class GraphState {
constructor(private graph: GraphManager) {
$effect.root(() => {
$effect(() => {
localStorage.setItem(
'cameraPosition',
`[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]`
);
});
});
const storedPosition = localStorage.getItem('cameraPosition');
if (storedPosition) {
try {
const d = JSON.parse(storedPosition);
this.cameraPosition[0] = d[0];
this.cameraPosition[1] = d[1];
this.cameraPosition[2] = d[2];
} catch (e) {
console.log('Failed to parsed stored camera position', e);
}
}
}
width = $state(100); width = $state(100);
height = $state(100); height = $state(100);
hoveredEdgeId = $state<string | null>(null);
edges = new Map<string, EdgeData>();
wrapper = $state<HTMLDivElement>(null!); wrapper = $state<HTMLDivElement>(null!);
rect: DOMRect = $derived( rect: DOMRect = $derived(
(this.wrapper && this.width && this.height) ? this.wrapper.getBoundingClientRect() : new DOMRect(0, 0, 0, 0), (this.wrapper && this.width && this.height)
? this.wrapper.getBoundingClientRect()
: new DOMRect(0, 0, 0, 0)
); );
camera = $state<OrthographicCamera>(null!); camera = $state<OrthographicCamera>(null!);
cameraPosition: [number, number, number] = $state([0, 0, 4]); cameraPosition: [number, number, number] = $state([140, 100, 3.5]);
clipboard: null | { clipboard: null | {
nodes: Node[]; nodes: NodeInstance[];
edges: [number, number, number, string][]; edges: [number, number, number, string][];
} = null; } = null;
@@ -46,13 +75,23 @@ export class GraphState {
this.cameraPosition[0] - this.width / this.cameraPosition[2] / 2, this.cameraPosition[0] - this.width / this.cameraPosition[2] / 2,
this.cameraPosition[0] + this.width / this.cameraPosition[2] / 2, this.cameraPosition[0] + this.width / this.cameraPosition[2] / 2,
this.cameraPosition[1] - this.height / this.cameraPosition[2] / 2, this.cameraPosition[1] - this.height / this.cameraPosition[2] / 2,
this.cameraPosition[1] + this.height / this.cameraPosition[2] / 2, this.cameraPosition[1] + this.height / this.cameraPosition[2] / 2
]); ]);
boxSelection = $state(false); boxSelection = $state(false);
edgeEndPosition = $state<[number, number] | null>(); edgeEndPosition = $state<[number, number] | null>();
addMenuPosition = $state<[number, number] | null>(null); addMenuPosition = $state<[number, number] | null>(null);
snapToGrid = $state(false);
showGrid = $state(true);
showHelp = $state(false);
cameraDown = [0, 0];
mouseDownNodeId = -1;
isPanning = $state(false);
isDragging = $state(false);
hoveredNodeId = $state(-1);
mousePosition = $state([0, 0]); mousePosition = $state([0, 0]);
mouseDown = $state<[number, number] | null>(null); mouseDown = $state<[number, number] | null>(null);
activeNodeId = $state(-1); activeNodeId = $state(-1);
@@ -61,51 +100,49 @@ export class GraphState {
hoveredSocket = $state<Socket | null>(null); hoveredSocket = $state<Socket | null>(null);
possibleSockets = $state<Socket[]>([]); possibleSockets = $state<Socket[]>([]);
possibleSocketIds = $derived( possibleSocketIds = $derived(
new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`)), new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`))
); );
getEdges() {
return $state.snapshot(this.edges);
}
clearSelection() { clearSelection() {
this.selectedNodes.clear(); this.selectedNodes.clear();
} }
isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT"; isBodyFocused = () => document?.activeElement?.nodeName !== 'INPUT';
setCameraTransform( setEdgeGeometry(edgeId: string, x1: number, y1: number, points: Vector3[]) {
x = this.cameraPosition[0], this.edges.set(edgeId, { x1, y1, points });
y = this.cameraPosition[1],
z = this.cameraPosition[2],
) {
if (this.camera) {
this.camera.position.x = x;
this.camera.position.z = y;
this.camera.zoom = z;
}
this.cameraPosition = [x, y, z];
localStorage.setItem("cameraPosition", JSON.stringify(this.cameraPosition));
} }
removeEdgeGeometry(edgeId: string) {
this.edges.delete(edgeId);
}
updateNodePosition(node: Node) { getEdgeData() {
if (node?.tmp?.ref && node?.tmp?.mesh) { return this.edges;
if (node.tmp["x"] !== undefined && node.tmp["y"] !== undefined) { }
node.tmp.ref.style.setProperty("--nx", `${node.tmp.x * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.tmp.y * 10}px`); updateNodePosition(node: NodeInstance) {
node.tmp.mesh.position.x = node.tmp.x + 10; if (
node.tmp.mesh.position.z = node.tmp.y + this.getNodeHeight(node.type) / 2; node.state.x === node.position[0]
if ( && node.state.y === node.position[1]
node.tmp.x === node.position[0] && ) {
node.tmp.y === node.position[1] delete node.state.x;
) { delete node.state.y;
delete node.tmp.x; }
delete node.tmp.y;
} if (node.state['x'] !== undefined && node.state['y'] !== undefined) {
this.graph.edges = [...this.graph.edges]; if (node.state.ref) {
} else { node.state.ref.style.setProperty('--nx', `${node.state.x * 10}px`);
node.tmp.ref.style.setProperty("--nx", `${node.position[0] * 10}px`); node.state.ref.style.setProperty('--ny', `${node.state.y * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.position[1] * 10}px`); }
node.tmp.mesh.position.x = node.position[0] + 10; } else {
node.tmp.mesh.position.z = if (node.state.ref) {
node.position[1] + this.getNodeHeight(node.type) / 2; node.state.ref.style.setProperty('--nx', `${node.position[0] * 10}px`);
node.state.ref.style.setProperty('--ny', `${node.position[1] * 10}px`);
} }
} }
} }
@@ -124,19 +161,19 @@ export class GraphState {
} }
getSocketPosition( getSocketPosition(
node: Node, node: NodeInstance,
index: string | number, index: string | number
): [number, number] { ): [number, number] {
if (typeof index === "number") { if (typeof index === 'number') {
return [ return [
(node?.tmp?.x ?? node.position[0]) + 20, (node?.state?.x ?? node.position[0]) + 20,
(node?.tmp?.y ?? node.position[1]) + 2.5 + 10 * index, (node?.state?.y ?? node.position[1]) + 2.5 + 10 * index
]; ];
} else { } else {
const _index = Object.keys(node.tmp?.type?.inputs || {}).indexOf(index); const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index);
return [ return [
node?.tmp?.x ?? node.position[0], node?.state?.x ?? node.position[0],
(node?.tmp?.y ?? node.position[1]) + 10 + 10 * _index, (node?.state?.y ?? node.position[1]) + 10 + 10 * _index
]; ];
} }
} }
@@ -150,52 +187,26 @@ export class GraphState {
if (!node?.inputs) { if (!node?.inputs) {
return 5; return 5;
} }
const height = const height = 5
5 + + 10
10 * * Object.keys(node.inputs).filter(
Object.keys(node.inputs).filter( (p) =>
(p) => p !== 'seed'
p !== "seed" && && node?.inputs
node?.inputs && && !('setting' in node?.inputs?.[p])
!("setting" in node?.inputs?.[p]) && && node.inputs[p].hidden !== true
node.inputs[p].hidden !== true, ).length;
).length;
this.nodeHeightCache[nodeTypeId] = height; this.nodeHeightCache[nodeTypeId] = height;
return height; return height;
} }
setNodePosition(node: Node) {
if (node?.tmp?.ref && node?.tmp?.mesh) {
if (node.tmp["x"] !== undefined && node.tmp["y"] !== undefined) {
node.tmp.ref.style.setProperty("--nx", `${node.tmp.x * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.tmp.y * 10}px`);
node.tmp.mesh.position.x = node.tmp.x + 10;
node.tmp.mesh.position.z = node.tmp.y + this.getNodeHeight(node.type) / 2;
if (
node.tmp.x === node.position[0] &&
node.tmp.y === node.position[1]
) {
delete node.tmp.x;
delete node.tmp.y;
}
this.graph.edges = [...this.graph.edges];
} else {
node.tmp.ref.style.setProperty("--nx", `${node.position[0] * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.position[1] * 10}px`);
node.tmp.mesh.position.x = node.position[0] + 10;
node.tmp.mesh.position.z =
node.position[1] + this.getNodeHeight(node.type) / 2;
}
}
}
copyNodes() { copyNodes() {
if (this.activeNodeId === -1 && !this.selectedNodes?.size) if (this.activeNodeId === -1 && !this.selectedNodes?.size) {
return; return;
}
let nodes = [ let nodes = [
this.activeNodeId, this.activeNodeId,
...(this.selectedNodes?.values() || []), ...(this.selectedNodes?.values() || [])
] ]
.map((id) => this.graph.getNode(id)) .map((id) => this.graph.getNode(id))
.filter(b => !!b); .filter(b => !!b);
@@ -205,14 +216,14 @@ export class GraphState {
...node, ...node,
position: [ position: [
this.mousePosition[0] - node.position[0], this.mousePosition[0] - node.position[0],
this.mousePosition[1] - node.position[1], this.mousePosition[1] - node.position[1]
], ],
tmp: undefined, tmp: undefined
})); }));
this.clipboard = { this.clipboard = {
nodes: nodes, nodes: nodes,
edges: edges, edges: edges
}; };
} }
@@ -221,12 +232,11 @@ export class GraphState {
const nodes = this.clipboard.nodes const nodes = this.clipboard.nodes
.map((node) => { .map((node) => {
node.tmp = node.tmp || {};
node.position[0] = this.mousePosition[0] - node.position[0]; node.position[0] = this.mousePosition[0] - node.position[0];
node.position[1] = this.mousePosition[1] - node.position[1]; node.position[1] = this.mousePosition[1] - node.position[1];
return node; return node;
}) })
.filter(Boolean) as Node[]; .filter(Boolean) as NodeInstance[];
const newNodes = this.graph.createGraph(nodes, this.clipboard.edges); const newNodes = this.graph.createGraph(nodes, this.clipboard.edges);
this.selectedNodes.clear(); this.selectedNodes.clear();
@@ -235,14 +245,13 @@ export class GraphState {
} }
} }
setDownSocket(socket: Socket) { setDownSocket(socket: Socket) {
this.activeSocket = socket; this.activeSocket = socket;
let { node, index, position } = socket; let { node, index, position } = socket;
// remove existing edge // remove existing edge
if (typeof index === "string") { if (typeof index === 'string') {
const edges = this.graph.getEdgesToNode(node); const edges = this.graph.getEdgesToNode(node);
for (const edge of edges) { for (const edge of edges) {
if (edge[3] === index) { if (edge[3] === index) {
@@ -259,7 +268,7 @@ export class GraphState {
this.activeSocket = { this.activeSocket = {
node, node,
index, index,
position, position
}; };
this.possibleSockets = this.graph this.possibleSockets = this.graph
@@ -268,18 +277,17 @@ export class GraphState {
return { return {
node, node,
index, index,
position: this.getSocketPosition(node, index), position: this.getSocketPosition(node, index)
}; };
}); });
}; }
projectScreenToWorld(x: number, y: number): [number, number] { projectScreenToWorld(x: number, y: number): [number, number] {
return [ return [
this.cameraPosition[0] + this.cameraPosition[0]
(x - this.width / 2) / this.cameraPosition[2], + (x - this.width / 2) / this.cameraPosition[2],
this.cameraPosition[1] + this.cameraPosition[1]
(y - this.height / 2) / this.cameraPosition[2], + (y - this.height / 2) / this.cameraPosition[2]
]; ];
} }
@@ -292,8 +300,8 @@ export class GraphState {
if (event.button === 0) { if (event.button === 0) {
// check if the clicked element is a node // check if the clicked element is a node
if (event.target instanceof HTMLElement) { if (event.target instanceof HTMLElement) {
const nodeElement = event.target.closest(".node"); const nodeElement = event.target.closest('.node');
const nodeId = nodeElement?.getAttribute?.("data-node-id"); const nodeId = nodeElement?.getAttribute?.('data-node-id');
if (nodeId) { if (nodeId) {
clickedNodeId = parseInt(nodeId, 10); clickedNodeId = parseInt(nodeId, 10);
} }
@@ -317,14 +325,18 @@ export class GraphState {
return clickedNodeId; return clickedNodeId;
} }
isNodeInView(node: Node) { isNodeInView(node: NodeInstance) {
const height = this.getNodeHeight(node.type); const height = this.getNodeHeight(node.type);
const width = 20; const width = 20;
return ( return (
node.position[0] > this.cameraBounds[0] - width && node.position[0] > this.cameraBounds[0] - width
node.position[0] < this.cameraBounds[1] && && node.position[0] < this.cameraBounds[1]
node.position[1] > this.cameraBounds[2] - height && && node.position[1] > this.cameraBounds[2] - height
node.position[1] < this.cameraBounds[3] && node.position[1] < this.cameraBounds[3]
); );
}; }
openNodePalette() {
this.addMenuPosition = [this.mousePosition[0], this.mousePosition[1]];
}
} }

View File

@@ -1,52 +1,31 @@
<script lang="ts"> <script lang="ts">
import type { Edge, Node, NodeType } from "@nodarium/types"; import type { Edge, NodeInstance } from '@nodarium/types';
import { GraphSchema } from "@nodarium/types"; import { Canvas } from '@threlte/core';
import { onMount } from "svelte"; import { HTML } from '@threlte/extras';
import { createKeyMap } from "../../helpers/createKeyMap"; import { createKeyMap } from '../../helpers/createKeyMap';
import AddMenu from "../components/AddMenu.svelte"; import Background from '../background/Background.svelte';
import Background from "../background/Background.svelte"; import AddMenu from '../components/AddMenu.svelte';
import BoxSelection from "../components/BoxSelection.svelte"; import BoxSelection from '../components/BoxSelection.svelte';
import EdgeEl from "../edges/Edge.svelte"; import Camera from '../components/Camera.svelte';
import NodeEl from "../node/Node.svelte"; import HelpView from '../components/HelpView.svelte';
import Camera from "../components/Camera.svelte"; import Debug from '../debug/Debug.svelte';
import FloatingEdge from "../edges/FloatingEdge.svelte"; import EdgeEl from '../edges/Edge.svelte';
import { import { getGraphManager, getGraphState } from '../graph-state.svelte';
animate, import NodeEl from '../node/Node.svelte';
lerp, import { maxZoom, minZoom } from './constants';
snapToGrid as snapPointToGrid, import { FileDropEventManager } from './drop.events';
} from "../helpers/index.js"; import { MouseEventManager } from './mouse.events';
import { Canvas } from "@threlte/core";
import HelpView from "../components/HelpView.svelte";
import { getGraphManager, getGraphState } from "./state.svelte";
import { HTML } from "@threlte/extras";
const { const {
snapToGrid = $bindable(true), keymap
showGrid = $bindable(true),
showHelp = $bindable(false),
keymap,
}: { }: {
snapToGrid: boolean;
showGrid: boolean;
showHelp: boolean;
keymap: ReturnType<typeof createKeyMap>; keymap: ReturnType<typeof createKeyMap>;
} = $props(); } = $props();
const minZoom = 1;
const maxZoom = 40;
let mouseDownNodeId = -1;
const cameraDown = [0, 0];
let isPanning = $state(false);
let isDragging = $state(false);
let hoveredNodeId = $state(-1);
const graph = getGraphManager(); const graph = getGraphManager();
const graphState = getGraphState(); const graphState = getGraphState();
const fileDropEvents = new FileDropEventManager(graph, graphState);
function getEdgeId(edge: Edge) { const mouseEvents = new MouseEventManager(graph, graphState);
return `${edge[0].id}-${edge[1]}-${edge[2].id}-${edge[3]}`;
}
function getEdgePosition(edge: Edge) { function getEdgePosition(edge: Edge) {
const fromNode = graph.nodes.get(edge[0].id); const fromNode = graph.nodes.get(edge[0].id);
@@ -62,505 +41,88 @@
return [pos1[0], pos1[1], pos2[0], pos2[1]]; return [pos1[0], pos1[1], pos2[0], pos2[1]];
} }
function handleMouseMove(event: MouseEvent) { function handleNodeCreation(node: NodeInstance) {
let mx = event.clientX - graphState.rect.x; const newNode = graph.createNode({
let my = event.clientY - graphState.rect.y; type: node.type,
position: node.position,
props: node.props
});
if (!newNode) return;
graphState.mousePosition = graphState.projectScreenToWorld(mx, my); if (graphState.activeSocket) {
hoveredNodeId = graphState.getNodeIdFromEvent(event); if (typeof graphState.activeSocket.index === 'number') {
const socketType = graphState.activeSocket.node.state?.type?.outputs?.[
if (!graphState.mouseDown) return; graphState.activeSocket.index
// we are creating a new edge here
if (graphState.activeSocket || graphState.possibleSockets?.length) {
let smallestDist = 1000;
let _socket;
for (const socket of graphState.possibleSockets) {
const dist = Math.sqrt(
(socket.position[0] - graphState.mousePosition[0]) ** 2 +
(socket.position[1] - graphState.mousePosition[1]) ** 2,
);
if (dist < smallestDist) {
smallestDist = dist;
_socket = socket;
}
}
if (_socket && smallestDist < 0.9) {
graphState.mousePosition = _socket.position;
graphState.hoveredSocket = _socket;
} else {
graphState.hoveredSocket = null;
}
return;
}
// handle box selection
if (graphState.boxSelection) {
event.preventDefault();
event.stopPropagation();
const mouseD = graphState.projectScreenToWorld(
graphState.mouseDown[0],
graphState.mouseDown[1],
);
const x1 = Math.min(mouseD[0], graphState.mousePosition[0]);
const x2 = Math.max(mouseD[0], graphState.mousePosition[0]);
const y1 = Math.min(mouseD[1], graphState.mousePosition[1]);
const y2 = Math.max(mouseD[1], graphState.mousePosition[1]);
for (const node of graph.nodes.values()) {
if (!node?.tmp) continue;
const x = node.position[0];
const y = node.position[1];
const height = graphState.getNodeHeight(node.type);
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
graphState.selectedNodes?.add(node.id);
} else {
graphState.selectedNodes?.delete(node.id);
}
}
return;
}
// here we are handling dragging of nodes
if (graphState.activeNodeId !== -1 && mouseDownNodeId !== -1) {
const node = graph.getNode(graphState.activeNodeId);
if (!node || event.buttons !== 1) return;
node.tmp = node.tmp || {};
const oldX = node.tmp.downX || 0;
const oldY = node.tmp.downY || 0;
let newX =
oldX + (mx - graphState.mouseDown[0]) / graphState.cameraPosition[2];
let newY =
oldY + (my - graphState.mouseDown[1]) / graphState.cameraPosition[2];
if (event.ctrlKey) {
const snapLevel = graphState.getSnapLevel();
if (snapToGrid) {
newX = snapPointToGrid(newX, 5 / snapLevel);
newY = snapPointToGrid(newY, 5 / snapLevel);
}
}
if (!node.tmp.isMoving) {
const dist = Math.sqrt((oldX - newX) ** 2 + (oldY - newY) ** 2);
if (dist > 0.2) {
node.tmp.isMoving = true;
}
}
const vecX = oldX - newX;
const vecY = oldY - newY;
if (graphState.selectedNodes?.size) {
for (const nodeId of graphState.selectedNodes) {
const n = graph.getNode(nodeId);
if (!n?.tmp) continue;
n.tmp.x = (n?.tmp?.downX || 0) - vecX;
n.tmp.y = (n?.tmp?.downY || 0) - vecY;
graphState.updateNodePosition(n);
}
}
node.tmp.x = newX;
node.tmp.y = newY;
graphState.updateNodePosition(node);
return;
}
// here we are handling panning of camera
isPanning = true;
let newX =
cameraDown[0] -
(mx - graphState.mouseDown[0]) / graphState.cameraPosition[2];
let newY =
cameraDown[1] -
(my - graphState.mouseDown[1]) / graphState.cameraPosition[2];
graphState.setCameraTransform(newX, newY);
}
const zoomSpeed = 2;
function handleMouseScroll(event: WheelEvent) {
const bodyIsFocused =
document.activeElement === document.body ||
document.activeElement === graphState.wrapper ||
document?.activeElement?.id === "graph";
if (!bodyIsFocused) return;
// Define zoom speed and clamp it between -1 and 1
const isNegative = event.deltaY < 0;
const normalizedDelta = Math.abs(event.deltaY * 0.01);
const delta = Math.pow(0.95, zoomSpeed * normalizedDelta);
// Calculate new zoom level and clamp it between minZoom and maxZoom
const newZoom = Math.max(
minZoom,
Math.min(
maxZoom,
isNegative
? graphState.cameraPosition[2] / delta
: graphState.cameraPosition[2] * delta,
),
);
// Calculate the ratio of the new zoom to the original zoom
const zoomRatio = newZoom / graphState.cameraPosition[2];
// Update camera position and zoom level
graphState.setCameraTransform(
graphState.mousePosition[0] -
(graphState.mousePosition[0] - graphState.cameraPosition[0]) /
zoomRatio,
graphState.mousePosition[1] -
(graphState.mousePosition[1] - graphState.cameraPosition[1]) /
zoomRatio,
newZoom,
);
}
function handleMouseDown(event: MouseEvent) {
if (graphState.mouseDown) return;
graphState.edgeEndPosition = null;
if (event.target instanceof HTMLElement) {
if (
event.target.nodeName !== "CANVAS" &&
!event.target.classList.contains("node") &&
!event.target.classList.contains("content")
) {
return;
}
}
let mx = event.clientX - graphState.rect.x;
let my = event.clientY - graphState.rect.y;
graphState.mouseDown = [mx, my];
cameraDown[0] = graphState.cameraPosition[0];
cameraDown[1] = graphState.cameraPosition[1];
const clickedNodeId = graphState.getNodeIdFromEvent(event);
mouseDownNodeId = clickedNodeId;
// if we clicked on a node
if (clickedNodeId !== -1) {
if (graphState.activeNodeId === -1) {
graphState.activeNodeId = clickedNodeId;
// if the selected node is the same as the clicked node
} else if (graphState.activeNodeId === clickedNodeId) {
//$activeNodeId = -1;
// if the clicked node is different from the selected node and secondary
} else if (event.ctrlKey) {
graphState.selectedNodes.add(graphState.activeNodeId);
graphState.selectedNodes.delete(clickedNodeId);
graphState.activeNodeId = clickedNodeId;
// select the node
} else if (event.shiftKey) {
const activeNode = graph.getNode(graphState.activeNodeId);
const newNode = graph.getNode(clickedNodeId);
if (activeNode && newNode) {
const edge = graph.getNodesBetween(activeNode, newNode);
if (edge) {
graphState.selectedNodes.clear();
for (const node of edge) {
graphState.selectedNodes.add(node.id);
}
graphState.selectedNodes.add(clickedNodeId);
}
}
} else if (!graphState.selectedNodes.has(clickedNodeId)) {
graphState.activeNodeId = clickedNodeId;
graphState.clearSelection();
}
} else if (event.ctrlKey) {
graphState.boxSelection = true;
}
const node = graph.getNode(graphState.activeNodeId);
if (!node) return;
node.tmp = node.tmp || {};
node.tmp.downX = node.position[0];
node.tmp.downY = node.position[1];
if (graphState.selectedNodes) {
for (const nodeId of graphState.selectedNodes) {
const n = graph.getNode(nodeId);
if (!n) continue;
n.tmp = n.tmp || {};
n.tmp.downX = n.position[0];
n.tmp.downY = n.position[1];
}
}
graphState.edgeEndPosition = null;
}
function handleMouseUp(event: MouseEvent) {
isPanning = false;
if (!graphState.mouseDown) return;
const activeNode = graph.getNode(graphState.activeNodeId);
const clickedNodeId = graphState.getNodeIdFromEvent(event);
if (clickedNodeId !== -1) {
if (activeNode) {
if (!activeNode?.tmp?.isMoving && !event.ctrlKey && !event.shiftKey) {
graphState.activeNodeId = clickedNodeId;
graphState.clearSelection();
}
}
}
if (activeNode?.tmp?.isMoving) {
activeNode.tmp = activeNode.tmp || {};
activeNode.tmp.isMoving = false;
if (snapToGrid) {
const snapLevel = graphState.getSnapLevel();
activeNode.position[0] = snapPointToGrid(
activeNode?.tmp?.x ?? activeNode.position[0],
5 / snapLevel,
);
activeNode.position[1] = snapPointToGrid(
activeNode?.tmp?.y ?? activeNode.position[1],
5 / snapLevel,
);
} else {
activeNode.position[0] = activeNode?.tmp?.x ?? activeNode.position[0];
activeNode.position[1] = activeNode?.tmp?.y ?? activeNode.position[1];
}
const nodes = [
...[...(graphState.selectedNodes?.values() || [])].map((id) =>
graph.getNode(id),
),
] as Node[];
const vec = [
activeNode.position[0] - (activeNode?.tmp.x || 0),
activeNode.position[1] - (activeNode?.tmp.y || 0),
];
for (const node of nodes) {
if (!node) continue;
node.tmp = node.tmp || {};
const { x, y } = node.tmp;
if (x !== undefined && y !== undefined) {
node.position[0] = x + vec[0];
node.position[1] = y + vec[1];
}
}
nodes.push(activeNode);
animate(500, (a: number) => {
for (const node of nodes) {
if (
node?.tmp &&
node.tmp["x"] !== undefined &&
node.tmp["y"] !== undefined
) {
node.tmp.x = lerp(node.tmp.x, node.position[0], a);
node.tmp.y = lerp(node.tmp.y, node.position[1], a);
graphState.updateNodePosition(node);
if (node?.tmp?.isMoving) {
return false;
}
}
}
});
graph.save();
} else if (graphState.hoveredSocket && graphState.activeSocket) {
if (
typeof graphState.hoveredSocket.index === "number" &&
typeof graphState.activeSocket.index === "string"
) {
graph.createEdge(
graphState.hoveredSocket.node,
graphState.hoveredSocket.index || 0,
graphState.activeSocket.node,
graphState.activeSocket.index,
);
} else if (
typeof graphState.activeSocket.index == "number" &&
typeof graphState.hoveredSocket.index === "string"
) {
graph.createEdge(
graphState.activeSocket.node,
graphState.activeSocket.index || 0,
graphState.hoveredSocket.node,
graphState.hoveredSocket.index,
);
}
graph.save();
} else if (graphState.activeSocket && event.ctrlKey) {
// Handle automatic adding of nodes on ctrl+mouseUp
graphState.edgeEndPosition = [
graphState.mousePosition[0],
graphState.mousePosition[1],
];
if (typeof graphState.activeSocket.index === "number") {
graphState.addMenuPosition = [
graphState.mousePosition[0],
graphState.mousePosition[1] - 25 / graphState.cameraPosition[2],
]; ];
const input = Object.entries(newNode?.state?.type?.inputs || {}).find(
(inp) => inp[1].type === socketType
);
if (input) {
graph.createEdge(
graphState.activeSocket.node,
graphState.activeSocket.index,
newNode,
input[0]
);
}
} else { } else {
graphState.addMenuPosition = [ const socketType = graphState.activeSocket.node.state?.type?.inputs?.[
graphState.mousePosition[0] - 155 / graphState.cameraPosition[2], graphState.activeSocket.index
graphState.mousePosition[1] - 25 / graphState.cameraPosition[2],
]; ];
const output = newNode.state?.type?.outputs?.find((out) => {
if (socketType?.type === out) return true;
if (socketType?.accepts?.includes(out as any)) return true;
return false;
});
if (output) {
graph.createEdge(
newNode,
output.indexOf(output),
graphState.activeSocket.node,
graphState.activeSocket.index
);
}
} }
return;
} }
// check if camera moved
if (
clickedNodeId === -1 &&
!graphState.boxSelection &&
cameraDown[0] === graphState.cameraPosition[0] &&
cameraDown[1] === graphState.cameraPosition[1] &&
graphState.isBodyFocused()
) {
graphState.activeNodeId = -1;
graphState.clearSelection();
}
graphState.mouseDown = null;
graphState.boxSelection = false;
graphState.activeSocket = null; graphState.activeSocket = null;
graphState.possibleSockets = [];
graphState.hoveredSocket = null;
graphState.addMenuPosition = null; graphState.addMenuPosition = null;
} }
function handleMouseLeave() {
isDragging = false;
isPanning = false;
}
function handleDrop(event: DragEvent) {
event.preventDefault();
isDragging = false;
if (!event.dataTransfer) return;
const nodeId = event.dataTransfer.getData("data/node-id") as NodeType;
let mx = event.clientX - graphState.rect.x;
let my = event.clientY - graphState.rect.y;
if (nodeId) {
let nodeOffsetX = event.dataTransfer.getData("data/node-offset-x");
let nodeOffsetY = event.dataTransfer.getData("data/node-offset-y");
if (nodeOffsetX && nodeOffsetY) {
mx += parseInt(nodeOffsetX);
my += parseInt(nodeOffsetY);
}
let props = {};
let rawNodeProps = event.dataTransfer.getData("data/node-props");
if (rawNodeProps) {
try {
props = JSON.parse(rawNodeProps);
} catch (e) {}
}
const pos = graphState.projectScreenToWorld(mx, my);
graph.registry.load([nodeId]).then(() => {
graph.createNode({
type: nodeId,
props,
position: pos,
});
});
} else if (event.dataTransfer.files.length) {
const file = event.dataTransfer.files[0];
if (file.type === "application/wasm") {
const reader = new FileReader();
reader.onload = async (e) => {
const buffer = e.target?.result;
if (buffer?.constructor === ArrayBuffer) {
const nodeType = await graph.registry.register(buffer);
graph.createNode({
type: nodeType.id,
props: {},
position: graphState.projectScreenToWorld(mx, my),
});
}
};
reader.readAsArrayBuffer(file);
} else if (file.type === "application/json") {
const reader = new FileReader();
reader.onload = (e) => {
const buffer = e.target?.result as ArrayBuffer;
if (buffer) {
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
graph.load(state);
}
};
reader.readAsText(file);
}
}
}
function handleDragEnter(e: DragEvent) {
e.preventDefault();
isDragging = true;
isPanning = false;
}
function handlerDragOver(e: DragEvent) {
e.preventDefault();
isDragging = true;
isPanning = false;
}
function handleDragEnd(e: DragEvent) {
e.preventDefault();
isDragging = true;
isPanning = false;
}
onMount(() => {
if (localStorage.getItem("cameraPosition")) {
const cPosition = JSON.parse(localStorage.getItem("cameraPosition")!);
if (Array.isArray(cPosition)) {
graphState.setCameraTransform(cPosition[0], cPosition[1], cPosition[2]);
}
}
});
</script> </script>
<svelte:window onmousemove={handleMouseMove} onmouseup={handleMouseUp} /> <svelte:window
onmousemove={(ev) => mouseEvents.handleWindowMouseMove(ev)}
onmouseup={(ev) => mouseEvents.handleWindowMouseUp(ev)}
/>
<div <div
onwheel={handleMouseScroll} onwheel={(ev) => mouseEvents.handleMouseScroll(ev)}
bind:this={graphState.wrapper} bind:this={graphState.wrapper}
class="graph-wrapper" class="graph-wrapper"
class:is-panning={isPanning} style="height: 100%"
class:is-hovering={hoveredNodeId !== -1} class:is-panning={graphState.isPanning}
class:is-hovering={graphState.hoveredNodeId !== -1}
aria-label="Graph" aria-label="Graph"
role="button" role="button"
tabindex="0" tabindex="0"
bind:clientWidth={graphState.width} bind:clientWidth={graphState.width}
bind:clientHeight={graphState.height} bind:clientHeight={graphState.height}
ondragenter={handleDragEnter} onkeydown={(ev) => keymap.handleKeyboardEvent(ev)}
ondragover={handlerDragOver} onmousedown={(ev) => mouseEvents.handleMouseDown(ev)}
ondragexit={handleDragEnd} oncontextmenu={(ev) => mouseEvents.handleContextMenu(ev)}
ondrop={handleDrop} {...fileDropEvents.getEventListenerProps()}
onmouseleave={handleMouseLeave}
onkeydown={keymap.handleKeyboardEvent}
onmousedown={handleMouseDown}
> >
<input <input
type="file" type="file"
accept="application/wasm,application/json" accept="application/wasm,application/json"
id="drop-zone" id="drop-zone"
disabled={!isDragging} disabled={!graphState.isDragging}
ondragend={handleDragEnd} ondragend={(ev) => fileDropEvents.handleDragEnd(ev)}
ondragleave={handleDragEnd} ondragleave={(ev) => fileDropEvents.handleDragEnd(ev)}
/> />
<label for="drop-zone"></label> <label for="drop-zone"></label>
@@ -570,7 +132,7 @@
position={graphState.cameraPosition} position={graphState.cameraPosition}
/> />
{#if showGrid !== false} {#if graphState.showGrid !== false}
<Background <Background
cameraPosition={graphState.cameraPosition} cameraPosition={graphState.cameraPosition}
{maxZoom} {maxZoom}
@@ -584,53 +146,46 @@
<BoxSelection <BoxSelection
cameraPosition={graphState.cameraPosition} cameraPosition={graphState.cameraPosition}
p1={{ p1={{
x: x: graphState.cameraPosition[0]
graphState.cameraPosition[0] + + (graphState.mouseDown[0] - graphState.width / 2)
(graphState.mouseDown[0] - graphState.width / 2) / / graphState.cameraPosition[2],
graphState.cameraPosition[2], y: graphState.cameraPosition[1]
y: + (graphState.mouseDown[1] - graphState.height / 2)
graphState.cameraPosition[1] + / graphState.cameraPosition[2]
(graphState.mouseDown[1] - graphState.height / 2) /
graphState.cameraPosition[2],
}} }}
p2={{ x: graphState.mousePosition[0], y: graphState.mousePosition[1] }} p2={{ x: graphState.mousePosition[0], y: graphState.mousePosition[1] }}
/> />
{/if} {/if}
{#if graph.status === "idle"} {#if graph.status === 'idle'}
{#if graphState.addMenuPosition} {#if graphState.addMenuPosition}
<AddMenu /> <AddMenu onnode={handleNodeCreation} />
{/if} {/if}
{#if graphState.activeSocket} {#if graphState.activeSocket}
<FloatingEdge <EdgeEl
z={graphState.cameraPosition[2]} z={graphState.cameraPosition[2]}
from={{ x1={graphState.activeSocket.position[0]}
x: graphState.activeSocket.position[0], y1={graphState.activeSocket.position[1]}
y: graphState.activeSocket.position[1], x2={graphState.edgeEndPosition?.[0] ?? graphState.mousePosition[0]}
}} y2={graphState.edgeEndPosition?.[1] ?? graphState.mousePosition[1]}
to={{
x: graphState.edgeEndPosition?.[0] ?? graphState.mousePosition[0],
y: graphState.edgeEndPosition?.[1] ?? graphState.mousePosition[1],
}}
/> />
{/if} {/if}
{#each graph.edges as edge (getEdgeId(edge))} {#each graph.edges as edge}
{@const [x1, y1, x2, y2] = getEdgePosition(edge)} {@const [x1, y1, x2, y2] = getEdgePosition(edge)}
<EdgeEl <EdgeEl
id={graph.getEdgeId(edge)}
z={graphState.cameraPosition[2]} z={graphState.cameraPosition[2]}
from={{ {x1}
x: x1, {y1}
y: y1, {x2}
}} {y2}
to={{
x: x2,
y: y2,
}}
/> />
{/each} {/each}
<Debug />
<HTML transform={false}> <HTML transform={false}>
<div <div
role="tree" role="tree"
@@ -649,15 +204,15 @@
{/each} {/each}
</div> </div>
</HTML> </HTML>
{:else if graph.status === "loading"} {:else if graph.status === 'loading'}
<span>Loading</span> <span>Loading</span>
{:else if graph.status === "error"} {:else if graph.status === 'error'}
<span>Error</span> <span>Error</span>
{/if} {/if}
</Canvas> </Canvas>
</div> </div>
{#if showHelp} {#if graphState.showHelp}
<HelpView registry={graph.registry} /> <HelpView registry={graph.registry} />
{/if} {/if}

View File

@@ -1,18 +1,22 @@
<script lang="ts"> <script lang="ts">
import type { Graph, Node, NodeRegistry } from "@nodarium/types"; import type { Graph, NodeInstance, NodeRegistry } from "@nodarium/types";
import GraphEl from "./Graph.svelte"; import GraphEl from "./Graph.svelte";
import { GraphManager } from "../graph-manager.svelte"; import { GraphManager } from "../graph-manager.svelte";
import { createKeyMap } from "$lib/helpers/createKeyMap"; import { createKeyMap } from "$lib/helpers/createKeyMap";
import { GraphState, setGraphManager, setGraphState } from "./state.svelte"; import {
GraphState,
setGraphManager,
setGraphState,
} from "../graph-state.svelte";
import { setupKeymaps } from "../keymaps"; import { setupKeymaps } from "../keymaps";
type Props = { type Props = {
graph: Graph; graph?: Graph;
registry: NodeRegistry; registry: NodeRegistry;
settings?: Record<string, any>; settings?: Record<string, any>;
activeNode?: Node; activeNode?: NodeInstance;
showGrid?: boolean; showGrid?: boolean;
snapToGrid?: boolean; snapToGrid?: boolean;
showHelp?: boolean; showHelp?: boolean;
@@ -41,6 +45,12 @@
setGraphManager(manager); setGraphManager(manager);
const graphState = new GraphState(manager); const graphState = new GraphState(manager);
$effect(() => {
graphState.showGrid = showGrid;
graphState.snapToGrid = snapToGrid;
graphState.showHelp = showHelp;
});
setGraphState(graphState); setGraphState(graphState);
setupKeymaps(keymap, manager, graphState); setupKeymaps(keymap, manager, graphState);
@@ -60,12 +70,6 @@
} }
}); });
$effect(() => {
if (settingTypes && settings) {
manager.setSettings(settings);
}
});
manager.on("settings", (_settings) => { manager.on("settings", (_settings) => {
settingTypes = { ...settingTypes, ..._settings.types }; settingTypes = { ...settingTypes, ..._settings.types };
settings = _settings.values; settings = _settings.values;
@@ -75,7 +79,11 @@
manager.on("save", (save) => onsave?.(save)); manager.on("save", (save) => onsave?.(save));
manager.load(graph); $effect(() => {
if (graph) {
manager.load(graph);
}
});
</script> </script>
<GraphEl {keymap} bind:showGrid bind:snapToGrid bind:showHelp /> <GraphEl {keymap} />

View File

@@ -0,0 +1,3 @@
export const minZoom = 1;
export const maxZoom = 40;
export const zoomSpeed = 2;

View File

@@ -0,0 +1,107 @@
import { GraphSchema, type NodeId } from '@nodarium/types';
import type { GraphManager } from '../graph-manager.svelte';
import type { GraphState } from '../graph-state.svelte';
export class FileDropEventManager {
constructor(
private graph: GraphManager,
private state: GraphState
) { }
handleFileDrop(event: DragEvent) {
event.preventDefault();
this.state.isDragging = false;
if (!event.dataTransfer) return;
const nodeId = event.dataTransfer.getData('data/node-id') as NodeId;
let mx = event.clientX - this.state.rect.x;
let my = event.clientY - this.state.rect.y;
if (nodeId) {
let nodeOffsetX = event.dataTransfer.getData('data/node-offset-x');
let nodeOffsetY = event.dataTransfer.getData('data/node-offset-y');
if (nodeOffsetX && nodeOffsetY) {
mx += parseInt(nodeOffsetX);
my += parseInt(nodeOffsetY);
}
let props = {};
let rawNodeProps = event.dataTransfer.getData('data/node-props');
if (rawNodeProps) {
try {
props = JSON.parse(rawNodeProps);
} catch (e) { }
}
const pos = this.state.projectScreenToWorld(mx, my);
this.graph.registry.load([nodeId]).then(() => {
this.graph.createNode({
type: nodeId,
props,
position: pos
});
});
} else if (event.dataTransfer.files.length) {
const file = event.dataTransfer.files[0];
if (file.type === 'application/wasm') {
const reader = new FileReader();
reader.onload = async (e) => {
const buffer = e.target?.result;
if (buffer?.constructor === ArrayBuffer) {
const nodeType = await this.graph.registry.register(buffer);
this.graph.createNode({
type: nodeType.id,
props: {},
position: this.state.projectScreenToWorld(mx, my)
});
}
};
reader.readAsArrayBuffer(file);
} else if (file.type === 'application/json') {
const reader = new FileReader();
reader.onload = (e) => {
const buffer = e.target?.result as ArrayBuffer;
if (buffer) {
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
this.graph.load(state);
}
};
reader.readAsText(file);
}
}
}
handleMouseLeave() {
this.state.isDragging = false;
this.state.isPanning = false;
}
handleDragEnter(e: DragEvent) {
e.preventDefault();
this.state.isDragging = true;
this.state.isPanning = false;
}
handleDragOver(e: DragEvent) {
e.preventDefault();
this.state.isDragging = true;
this.state.isPanning = false;
}
handleDragEnd(e: DragEvent) {
e.preventDefault();
this.state.isDragging = true;
this.state.isPanning = false;
}
getEventListenerProps() {
return {
ondragenter: (ev: DragEvent) => this.handleDragEnter(ev),
ondragover: (ev: DragEvent) => this.handleDragOver(ev),
ondragexit: (ev: DragEvent) => this.handleDragEnd(ev),
ondrop: (ev: DragEvent) => this.handleFileDrop(ev),
onmouseleave: () => this.handleMouseLeave()
};
}
}

View File

@@ -0,0 +1,110 @@
import type { Box } from '@nodarium/types';
import type { GraphManager } from '../graph-manager.svelte';
import type { GraphState } from '../graph-state.svelte';
import { distanceFromPointToSegment } from '../helpers';
export class EdgeInteractionManager {
constructor(
private graph: GraphManager,
private state: GraphState
) { }
private MIN_DISTANCE = 3;
private _boundingBoxes = new Map<string, Box>();
handleMouseDown() {
this._boundingBoxes.clear();
const possibleEdges = this.graph
.getPossibleDropOnEdges(this.state.activeNodeId)
.map(e => this.graph.getEdgeId(e));
const edges = this.state.getEdges();
for (const edge of edges) {
const edgeId = edge[0];
if (!possibleEdges.includes(edgeId)) {
edges.delete(edgeId);
}
}
for (const [edgeId, data] of edges) {
const points = data.points;
let minX = points[0].x + data.x1;
let maxX = points[0].x + data.x1;
let minY = points[0].z + data.y1;
let maxY = points[0].z + data.y1;
// Iterate through all points to find the true bounds
for (let i = 0; i < points.length; i++) {
const x = data.x1 + points[i].x;
const y = data.y1 + points[i].z;
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
}
const boundingBox = {
minX: minX - this.MIN_DISTANCE,
maxX: maxX + this.MIN_DISTANCE,
minY: minY - this.MIN_DISTANCE,
maxY: maxY + this.MIN_DISTANCE
};
this._boundingBoxes.set(edgeId, boundingBox);
}
}
handleMouseMove() {
const [mouseX, mouseY] = this.state.mousePosition;
const hoveredEdgeIds: string[] = [];
const edges = this.state.getEdges();
// Check if mouse is inside any bounding box
for (const [edgeId, box] of this._boundingBoxes) {
const isInside = mouseX >= box.minX
&& mouseX <= box.maxX
&& mouseY >= box.minY
&& mouseY <= box.maxY;
if (isInside) {
hoveredEdgeIds.push(edgeId);
}
}
let hoveredEdgeId: string | null = null;
let hoveredEdgeDistance = Infinity;
const DENSITY = 10; // higher DENSITY = less points checked (yes density might not be the best name :-)
for (const edgeId of hoveredEdgeIds) {
const edge = edges.get(edgeId)!;
for (let i = 0; i < edge.points.length - DENSITY; i += DENSITY) {
const pointAx = edge.points[i].x + edge.x1;
const pointAy = edge.points[i].z + edge.y1;
const pointBx = edge.points[i + DENSITY].x + edge.x1;
const pointBy = edge.points[i + DENSITY].z + edge.y1;
const distance = distanceFromPointToSegment(pointAx, pointAy, pointBx, pointBy, mouseX, mouseY);
if (distance < this.MIN_DISTANCE) {
if (distance < hoveredEdgeDistance) {
hoveredEdgeDistance = distance;
hoveredEdgeId = edgeId;
}
}
}
}
this.state.hoveredEdgeId = hoveredEdgeId;
}
handleMouseUp() {
if (this.state.hoveredEdgeId) {
const edge = this.graph.getEdgeById(this.state.hoveredEdgeId);
if (edge) {
this.graph.dropNodeOnEdge(this.state.activeNodeId, edge);
}
this.state.hoveredEdgeId = null;
}
}
}

View File

@@ -0,0 +1,397 @@
import { animate, lerp } from '$lib/helpers';
import { type NodeInstance } from '@nodarium/types';
import type { GraphManager } from '../graph-manager.svelte';
import { type GraphState } from '../graph-state.svelte';
import { snapToGrid as snapPointToGrid } from '../helpers';
import { maxZoom, minZoom, zoomSpeed } from './constants';
import { EdgeInteractionManager } from './edge.events';
export class MouseEventManager {
edgeInteractionManager: EdgeInteractionManager;
constructor(
private graph: GraphManager,
private state: GraphState
) {
this.edgeInteractionManager = new EdgeInteractionManager(graph, state);
}
handleWindowMouseUp(event: MouseEvent) {
this.edgeInteractionManager.handleMouseUp();
this.state.isPanning = false;
if (!this.state.mouseDown) return;
const activeNode = this.graph.getNode(this.state.activeNodeId);
const clickedNodeId = this.state.getNodeIdFromEvent(event);
if (clickedNodeId !== -1) {
if (activeNode) {
if (!activeNode?.state?.isMoving && !event.ctrlKey && !event.shiftKey) {
this.state.activeNodeId = clickedNodeId;
this.state.clearSelection();
}
}
}
if (activeNode?.state?.isMoving) {
activeNode.state = activeNode.state || {};
activeNode.state.isMoving = false;
if (this.state.snapToGrid) {
const snapLevel = this.state.getSnapLevel();
activeNode.position[0] = snapPointToGrid(
activeNode?.state?.x ?? activeNode.position[0],
5 / snapLevel
);
activeNode.position[1] = snapPointToGrid(
activeNode?.state?.y ?? activeNode.position[1],
5 / snapLevel
);
} else {
activeNode.position[0] = activeNode?.state?.x ?? activeNode.position[0];
activeNode.position[1] = activeNode?.state?.y ?? activeNode.position[1];
}
const nodes = [
...[...(this.state.selectedNodes?.values() || [])].map((id) => this.graph.getNode(id))
] as NodeInstance[];
const vec = [
activeNode.position[0] - (activeNode?.state.x || 0),
activeNode.position[1] - (activeNode?.state.y || 0)
];
for (const node of nodes) {
if (!node) continue;
node.state = node.state || {};
const { x, y } = node.state;
if (x !== undefined && y !== undefined) {
node.position[0] = x + vec[0];
node.position[1] = y + vec[1];
}
}
nodes.push(activeNode);
animate(500, (a: number) => {
for (const node of nodes) {
if (
node?.state
&& node.state['x'] !== undefined
&& node.state['y'] !== undefined
) {
node.state.x = lerp(node.state.x, node.position[0], a);
node.state.y = lerp(node.state.y, node.position[1], a);
this.state.updateNodePosition(node);
if (node?.state?.isMoving) {
return false;
}
}
}
});
this.graph.save();
} else if (this.state.hoveredSocket && this.state.activeSocket) {
if (
typeof this.state.hoveredSocket.index === 'number'
&& typeof this.state.activeSocket.index === 'string'
) {
this.graph.createEdge(
this.state.hoveredSocket.node,
this.state.hoveredSocket.index || 0,
this.state.activeSocket.node,
this.state.activeSocket.index
);
} else if (
typeof this.state.activeSocket.index == 'number'
&& typeof this.state.hoveredSocket.index === 'string'
) {
this.graph.createEdge(
this.state.activeSocket.node,
this.state.activeSocket.index || 0,
this.state.hoveredSocket.node,
this.state.hoveredSocket.index
);
}
this.graph.save();
} else if (this.state.activeSocket && event.ctrlKey) {
// Handle automatic adding of nodes on ctrl+mouseUp
this.state.edgeEndPosition = [
this.state.mousePosition[0],
this.state.mousePosition[1]
];
if (typeof this.state.activeSocket.index === 'number') {
this.state.addMenuPosition = [
this.state.mousePosition[0],
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2]
];
} else {
this.state.addMenuPosition = [
this.state.mousePosition[0] - 155 / this.state.cameraPosition[2],
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2]
];
}
return;
}
// check if camera moved
if (
clickedNodeId === -1
&& !this.state.boxSelection
&& this.state.cameraDown[0] === this.state.cameraPosition[0]
&& this.state.cameraDown[1] === this.state.cameraPosition[1]
&& this.state.isBodyFocused()
) {
this.state.activeNodeId = -1;
this.state.clearSelection();
}
this.state.mouseDown = null;
this.state.boxSelection = false;
this.state.activeSocket = null;
this.state.possibleSockets = [];
this.state.hoveredSocket = null;
this.state.addMenuPosition = null;
}
handleContextMenu(event: MouseEvent) {
if (!this.state.addMenuPosition) {
event.preventDefault();
this.state.openNodePalette();
}
}
handleMouseDown(event: MouseEvent) {
// Right click
if (event.button === 2) {
return;
}
if (this.state.mouseDown) return;
this.state.edgeEndPosition = null;
if (event.target instanceof HTMLElement) {
if (
event.target.nodeName !== 'CANVAS'
&& !event.target.classList.contains('node')
&& !event.target.classList.contains('content')
) {
return;
}
}
let mx = event.clientX - this.state.rect.x;
let my = event.clientY - this.state.rect.y;
this.state.mouseDown = [mx, my];
this.state.cameraDown[0] = this.state.cameraPosition[0];
this.state.cameraDown[1] = this.state.cameraPosition[1];
const clickedNodeId = this.state.getNodeIdFromEvent(event);
this.state.mouseDownNodeId = clickedNodeId;
// if we clicked on a node
if (clickedNodeId !== -1) {
if (this.state.activeNodeId === -1) {
this.state.activeNodeId = clickedNodeId;
// if the selected node is the same as the clicked node
} else if (this.state.activeNodeId === clickedNodeId) {
// $activeNodeId = -1;
// if the clicked node is different from the selected node and secondary
} else if (event.ctrlKey) {
this.state.selectedNodes.add(this.state.activeNodeId);
this.state.selectedNodes.delete(clickedNodeId);
this.state.activeNodeId = clickedNodeId;
// select the node
} else if (event.shiftKey) {
const activeNode = this.graph.getNode(this.state.activeNodeId);
const newNode = this.graph.getNode(clickedNodeId);
if (activeNode && newNode) {
const edge = this.graph.getNodesBetween(activeNode, newNode);
if (edge) {
this.state.selectedNodes.clear();
for (const node of edge) {
this.state.selectedNodes.add(node.id);
}
this.state.selectedNodes.add(clickedNodeId);
}
}
} else if (!this.state.selectedNodes.has(clickedNodeId)) {
this.state.activeNodeId = clickedNodeId;
this.state.clearSelection();
}
this.edgeInteractionManager.handleMouseDown();
} else if (event.ctrlKey) {
this.state.boxSelection = true;
}
const node = this.graph.getNode(this.state.activeNodeId);
if (!node) return;
node.state = node.state || {};
node.state.downX = node.position[0];
node.state.downY = node.position[1];
if (this.state.selectedNodes) {
for (const nodeId of this.state.selectedNodes) {
const n = this.graph.getNode(nodeId);
if (!n) continue;
n.state = n.state || {};
n.state.downX = n.position[0];
n.state.downY = n.position[1];
}
}
this.state.edgeEndPosition = null;
}
handleWindowMouseMove(event: MouseEvent) {
let mx = event.clientX - this.state.rect.x;
let my = event.clientY - this.state.rect.y;
this.state.mousePosition = this.state.projectScreenToWorld(mx, my);
this.state.hoveredNodeId = this.state.getNodeIdFromEvent(event);
if (!this.state.mouseDown) return;
// we are creating a new edge here
if (this.state.activeSocket || this.state.possibleSockets?.length) {
let smallestDist = 1000;
let _socket;
for (const socket of this.state.possibleSockets) {
const dist = Math.sqrt(
(socket.position[0] - this.state.mousePosition[0]) ** 2
+ (socket.position[1] - this.state.mousePosition[1]) ** 2
);
if (dist < smallestDist) {
smallestDist = dist;
_socket = socket;
}
}
if (_socket && smallestDist < 0.9) {
this.state.mousePosition = _socket.position;
this.state.hoveredSocket = _socket;
} else {
this.state.hoveredSocket = null;
}
return;
}
// handle box selection
if (this.state.boxSelection) {
event.preventDefault();
event.stopPropagation();
const mouseD = this.state.projectScreenToWorld(
this.state.mouseDown[0],
this.state.mouseDown[1]
);
const x1 = Math.min(mouseD[0], this.state.mousePosition[0]);
const x2 = Math.max(mouseD[0], this.state.mousePosition[0]);
const y1 = Math.min(mouseD[1], this.state.mousePosition[1]);
const y2 = Math.max(mouseD[1], this.state.mousePosition[1]);
for (const node of this.graph.nodes.values()) {
if (!node?.state) continue;
const x = node.position[0];
const y = node.position[1];
const height = this.state.getNodeHeight(node.type);
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
this.state.selectedNodes?.add(node.id);
} else {
this.state.selectedNodes?.delete(node.id);
}
}
return;
}
// here we are handling dragging of nodes
if (this.state.activeNodeId !== -1 && this.state.mouseDownNodeId !== -1) {
this.edgeInteractionManager.handleMouseMove();
const node = this.graph.getNode(this.state.activeNodeId);
if (!node || event.buttons !== 1) return;
node.state = node.state || {};
const oldX = node.state.downX || 0;
const oldY = node.state.downY || 0;
let newX = oldX + (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
let newY = oldY + (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
if (event.ctrlKey) {
const snapLevel = this.state.getSnapLevel();
if (this.state.snapToGrid) {
newX = snapPointToGrid(newX, 5 / snapLevel);
newY = snapPointToGrid(newY, 5 / snapLevel);
}
}
if (!node.state.isMoving) {
const dist = Math.sqrt((oldX - newX) ** 2 + (oldY - newY) ** 2);
if (dist > 0.2) {
node.state.isMoving = true;
}
}
const vecX = oldX - newX;
const vecY = oldY - newY;
if (this.state.selectedNodes?.size) {
for (const nodeId of this.state.selectedNodes) {
const n = this.graph.getNode(nodeId);
if (!n?.state) continue;
n.state.x = (n?.state?.downX || 0) - vecX;
n.state.y = (n?.state?.downY || 0) - vecY;
this.state.updateNodePosition(n);
}
}
node.state.x = newX;
node.state.y = newY;
this.state.updateNodePosition(node);
return;
}
// here we are handling panning of camera
this.state.isPanning = true;
let newX = this.state.cameraDown[0]
- (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
let newY = this.state.cameraDown[1]
- (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
this.state.cameraPosition[0] = newX;
this.state.cameraPosition[1] = newY;
}
handleMouseScroll(event: WheelEvent) {
const bodyIsFocused = document.activeElement === document.body
|| document.activeElement === this.state.wrapper
|| document?.activeElement?.id === 'graph';
if (!bodyIsFocused) return;
// Define zoom speed and clamp it between -1 and 1
const isNegative = event.deltaY < 0;
const normalizedDelta = Math.abs(event.deltaY * 0.01);
const delta = Math.pow(0.95, zoomSpeed * normalizedDelta);
// Calculate new zoom level and clamp it between minZoom and maxZoom
const newZoom = Math.max(
minZoom,
Math.min(
maxZoom,
isNegative
? this.state.cameraPosition[2] / delta
: this.state.cameraPosition[2] * delta
)
);
// Calculate the ratio of the new zoom to the original zoom
const zoomRatio = newZoom / this.state.cameraPosition[2];
// Update camera position and zoom level
this.state.cameraPosition[0] = this.state.mousePosition[0]
- (this.state.mousePosition[0] - this.state.cameraPosition[0])
/ zoomRatio;
this.state.cameraPosition[1] = this.state.mousePosition[1]
- (this.state.mousePosition[1] - this.state.cameraPosition[1])
/ zoomRatio, this.state.cameraPosition[2] = newZoom;
}
}

View File

@@ -8,7 +8,7 @@ export function lerp(a: number, b: number, t: number) {
export function animate( export function animate(
duration: number, duration: number,
callback: (progress: number) => void | false, callback: (progress: number) => void | false
) { ) {
const start = performance.now(); const start = performance.now();
const loop = (time: number) => { const loop = (time: number) => {
@@ -33,41 +33,37 @@ export function createNodePath({
cornerBottom = 0, cornerBottom = 0,
leftBump = false, leftBump = false,
rightBump = false, rightBump = false,
aspectRatio = 1, aspectRatio = 1
} = {}) { } = {}) {
return `M0,${cornerTop} return `M0,${cornerTop}
${ ${cornerTop
cornerTop ? ` V${cornerTop}
? ` V${cornerTop}
Q0,0 ${cornerTop * aspectRatio},0 Q0,0 ${cornerTop * aspectRatio},0
H${100 - cornerTop * aspectRatio} H${100 - cornerTop * aspectRatio}
Q100,0 100,${cornerTop} Q100,0 100,${cornerTop}
` `
: ` V0 : ` V0
H100 H100
` `
} }
V${y - height / 2} V${y - height / 2}
${ ${rightBump
rightBump ? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}` : ` H100`
: ` H100` }
} ${cornerBottom
${ ? ` V${100 - cornerBottom}
cornerBottom
? ` V${100 - cornerBottom}
Q100,100 ${100 - cornerBottom * aspectRatio},100 Q100,100 ${100 - cornerBottom * aspectRatio},100
H${cornerBottom * aspectRatio} H${cornerBottom * aspectRatio}
Q0,100 0,${100 - cornerBottom} Q0,100 0,${100 - cornerBottom}
` `
: `${leftBump ? `V100 H0` : `V100`}` : `${leftBump ? `V100 H0` : `V100`}`
} }
${ ${leftBump
leftBump ? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}` : ` H0`
: ` H0` }
} Z`.replace(/\s+/g, ' ');
Z`.replace(/\s+/g, " ");
} }
export const debounce = (fn: Function, ms = 300) => { export const debounce = (fn: Function, ms = 300) => {
@@ -78,14 +74,13 @@ export const debounce = (fn: Function, ms = 300) => {
}; };
}; };
export const clone: <T>(v: T) => T = export const clone: <T>(v: T) => T = 'structedClone' in globalThis
"structedClone" in globalThis ? globalThis.structuredClone
? globalThis.structuredClone : (obj) => JSON.parse(JSON.stringify(obj));
: (obj) => JSON.parse(JSON.stringify(obj));
export function withSubComponents<A, B extends Record<string, any>>( export function withSubComponents<A, B extends Record<string, any>>(
component: A, component: A,
subcomponents: B, subcomponents: B
): A & B { ): A & B {
Object.keys(subcomponents).forEach((key) => { Object.keys(subcomponents).forEach((key) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -93,3 +88,27 @@ export function withSubComponents<A, B extends Record<string, any>>(
}); });
return component as A & B; return component as A & B;
} }
export function distanceFromPointToSegment(
x1: number,
y1: number,
x2: number,
y2: number,
x0: number,
y0: number
): number {
const dx = x2 - x1;
const dy = y2 - y1;
if (dx === 0 && dy === 0) {
return Math.hypot(x0 - x1, y0 - y1);
}
const t = ((x0 - x1) * dx + (y0 - y1) * dy) / (dx * dx + dy * dy);
const clampedT = Math.max(0, Math.min(1, t));
const px = x1 + clampedT * dx;
const py = y1 + clampedT * dy;
return Math.hypot(x0 - px, y0 - py);
}

View File

@@ -1,16 +1,15 @@
import { animate, lerp } from "$lib/helpers"; import { animate, lerp } from '$lib/helpers';
import type { createKeyMap } from "$lib/helpers/createKeyMap"; import type { createKeyMap } from '$lib/helpers/createKeyMap';
import FileSaver from "file-saver"; import { panelState } from '$lib/sidebar/PanelState.svelte';
import type { GraphManager } from "./graph-manager.svelte"; import FileSaver from 'file-saver';
import type { GraphState } from "./graph/state.svelte"; import type { GraphManager } from './graph-manager.svelte';
import type { GraphState } from './graph-state.svelte';
type Keymap = ReturnType<typeof createKeyMap>; type Keymap = ReturnType<typeof createKeyMap>;
export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: GraphState) { export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: GraphState) {
keymap.addShortcut({ keymap.addShortcut({
key: "l", key: 'l',
description: "Select linked nodes", description: 'Select linked nodes',
callback: () => { callback: () => {
const activeNode = graph.getNode(graphState.activeNodeId); const activeNode = graph.getNode(graphState.activeNodeId);
if (activeNode) { if (activeNode) {
@@ -20,56 +19,52 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
graphState.selectedNodes.add(node.id); graphState.selectedNodes.add(node.id);
} }
} }
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "?", key: '?',
description: "Toggle Help", description: 'Toggle Help',
callback: () => { callback: () => {
// TODO: fix this panelState.setActivePanel('shortcuts');
// showHelp = !showHelp; }
},
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "c", key: 'c',
ctrl: true, ctrl: true,
description: "Copy active nodes", description: 'Copy active nodes',
callback: () => graphState.copyNodes(), callback: () => graphState.copyNodes()
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "v", key: 'v',
ctrl: true, ctrl: true,
description: "Paste nodes", description: 'Paste nodes',
callback: () => graphState.pasteNodes(), callback: () => graphState.pasteNodes()
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "Escape", key: 'Escape',
description: "Deselect nodes", description: 'Deselect nodes',
callback: () => { callback: () => {
graphState.activeNodeId = -1; graphState.activeNodeId = -1;
graphState.clearSelection(); graphState.clearSelection();
graphState.edgeEndPosition = null; graphState.edgeEndPosition = null;
(document.activeElement as HTMLElement)?.blur(); (document.activeElement as HTMLElement)?.blur();
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "A", key: 'A',
shift: true, shift: true,
description: "Add new Node", description: 'Add new Node',
callback: () => { callback: () => graphState.openNodePalette()
graphState.addMenuPosition = [graphState.mousePosition[0], graphState.mousePosition[1]];
},
}); });
keymap.addShortcut({ keymap.addShortcut({
key: ".", key: '.',
description: "Center camera", description: 'Center camera',
callback: () => { callback: () => {
if (!graphState.isBodyFocused()) return; if (!graphState.isBodyFocused()) return;
@@ -88,71 +83,69 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
const ease = (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t); const ease = (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t);
animate(500, (a: number) => { animate(500, (a: number) => {
graphState.setCameraTransform( graphState.cameraPosition[0] = lerp(camX, average[0], ease(a));
lerp(camX, average[0], ease(a)), graphState.cameraPosition[1] = lerp(camY, average[1], ease(a));
lerp(camY, average[1], ease(a)), graphState.cameraPosition[2] = lerp(camZ, 2, ease(a));
lerp(camZ, 2, ease(a)),
);
if (graphState.mouseDown) return false; if (graphState.mouseDown) return false;
}); });
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "a", key: 'a',
ctrl: true, ctrl: true,
preventDefault: true, preventDefault: true,
description: "Select all nodes", description: 'Select all nodes',
callback: () => { callback: () => {
if (!graphState.isBodyFocused()) return; if (!graphState.isBodyFocused()) return;
for (const node of graph.nodes.keys()) { for (const node of graph.nodes.keys()) {
graphState.selectedNodes.add(node); graphState.selectedNodes.add(node);
} }
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "z", key: 'z',
ctrl: true, ctrl: true,
description: "Undo", description: 'Undo',
callback: () => { callback: () => {
if (!graphState.isBodyFocused()) return; if (!graphState.isBodyFocused()) return;
graph.undo(); graph.undo();
for (const node of graph.nodes.values()) { for (const node of graph.nodes.values()) {
graphState.updateNodePosition(node); graphState.updateNodePosition(node);
} }
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "y", key: 'y',
ctrl: true, ctrl: true,
description: "Redo", description: 'Redo',
callback: () => { callback: () => {
graph.redo(); graph.redo();
for (const node of graph.nodes.values()) { for (const node of graph.nodes.values()) {
graphState.updateNodePosition(node); graphState.updateNodePosition(node);
} }
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "s", key: 's',
ctrl: true, ctrl: true,
description: "Save", description: 'Save',
preventDefault: true, preventDefault: true,
callback: () => { callback: () => {
const state = graph.serialize(); const state = graph.serialize();
const blob = new Blob([JSON.stringify(state)], { const blob = new Blob([JSON.stringify(state)], {
type: "application/json;charset=utf-8", type: 'application/json;charset=utf-8'
}); });
FileSaver.saveAs(blob, "nodarium-graph.json"); FileSaver.saveAs(blob, 'nodarium-graph.json');
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: ["Delete", "Backspace", "x"], key: ['Delete', 'Backspace', 'x'],
description: "Delete selected nodes", description: 'Delete selected nodes',
callback: (event) => { callback: (event) => {
if (!graphState.isBodyFocused()) return; if (!graphState.isBodyFocused()) return;
graph.startUndoGroup(); graph.startUndoGroup();
@@ -173,20 +166,18 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
graphState.clearSelection(); graphState.clearSelection();
} }
graph.saveUndoGroup(); graph.saveUndoGroup();
}, }
}); });
keymap.addShortcut({ keymap.addShortcut({
key: "f", key: 'f',
description: "Smart Connect Nodes", description: 'Smart Connect Nodes',
callback: () => { callback: () => {
const nodes = [...graphState.selectedNodes.values()] const nodes = [...graphState.selectedNodes.values()]
.map((g) => graph.getNode(g)) .map((g) => graph.getNode(g))
.filter((n) => !!n); .filter((n) => !!n);
const edge = graph.smartConnect(nodes[0], nodes[1]); const edge = graph.smartConnect(nodes[0], nodes[1]);
if (!edge) graph.smartConnect(nodes[1], nodes[0]); if (!edge) graph.smartConnect(nodes[1], nodes[0]);
}, }
}); });
} }

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { Node } from "@nodarium/types"; import type { NodeInstance } from "@nodarium/types";
import { onMount } from "svelte"; import { getGraphState } from "../graph-state.svelte";
import { getGraphManager, getGraphState } from "../graph/state.svelte";
import { T } from "@threlte/core"; import { T } from "@threlte/core";
import { type Mesh } from "three"; import { type Mesh } from "three";
import NodeFrag from "./Node.frag"; import NodeFrag from "./Node.frag";
@@ -10,47 +9,41 @@
import { colors } from "../graph/colors.svelte"; import { colors } from "../graph/colors.svelte";
import { appSettings } from "$lib/settings/app-settings.svelte"; import { appSettings } from "$lib/settings/app-settings.svelte";
const graph = getGraphManager();
const graphState = getGraphState(); const graphState = getGraphState();
type Props = { type Props = {
node: Node; node: NodeInstance;
inView: boolean; inView: boolean;
z: number; z: number;
}; };
let { node, inView, z }: Props = $props(); let { node = $bindable(), inView, z }: Props = $props();
const isActive = $derived(graphState.activeNodeId === node.id); const isActive = $derived(graphState.activeNodeId === node.id);
const isSelected = $derived(graphState.selectedNodes.has(node.id)); const isSelected = $derived(graphState.selectedNodes.has(node.id));
let strokeColor = $state(colors.selected); const strokeColor = $derived(
$effect(() => { appSettings.value.theme &&
appSettings.value.theme; (isSelected
strokeColor = isSelected ? colors.selected
? colors.selected : isActive
: isActive ? colors.active
? colors.active : colors.outline),
: colors.outline; );
});
let meshRef: Mesh | undefined = $state(); let meshRef: Mesh | undefined = $state();
const height = graphState.getNodeHeight(node.type); const height = graphState.getNodeHeight(node.type);
$effect(() => { $effect(() => {
if (!node?.tmp) node.tmp = {}; if (meshRef && !node.state?.mesh) {
node.tmp.mesh = meshRef; node.state.mesh = meshRef;
}); graphState.updateNodePosition(node);
}
onMount(() => {
if (!node.tmp) node.tmp = {};
node.tmp.mesh = meshRef;
graphState.updateNodePosition(node);
}); });
</script> </script>
<T.Mesh <T.Mesh
position.x={node.position[0] + 10} position.x={(node.state.x ?? node.position[0]) + 10}
position.z={node.position[1] + height / 2} position.z={(node.state.y ?? node.position[1]) + height / 2}
position.y={0.8} position.y={0.8}
rotation.x={-Math.PI / 2} rotation.x={-Math.PI / 2}
bind:ref={meshRef} bind:ref={meshRef}
@@ -74,4 +67,4 @@
/> />
</T.Mesh> </T.Mesh>
<NodeHtml {node} {inView} {isActive} {isSelected} {z} /> <NodeHtml bind:node {inView} {isActive} {isSelected} {z} />

View File

@@ -1,16 +1,15 @@
<script lang="ts"> <script lang="ts">
import type { Node } from "@nodarium/types"; import type { NodeInstance } from "@nodarium/types";
import NodeHeader from "./NodeHeader.svelte"; import NodeHeader from "./NodeHeader.svelte";
import NodeParameter from "./NodeParameter.svelte"; import NodeParameter from "./NodeParameter.svelte";
import { onMount } from "svelte"; import { getGraphState } from "../graph-state.svelte";
import { getGraphState } from "../graph/state.svelte";
let ref: HTMLDivElement; let ref: HTMLDivElement;
const graphState = getGraphState(); const graphState = getGraphState();
type Props = { type Props = {
node: Node; node: NodeInstance;
position?: "absolute" | "fixed" | "relative"; position?: "absolute" | "fixed" | "relative";
isActive?: boolean; isActive?: boolean;
isSelected?: boolean; isSelected?: boolean;
@@ -27,18 +26,20 @@
z = 2, z = 2,
}: Props = $props(); }: Props = $props();
const zOffset = (node.tmp?.random || 0) * 0.5; // If we dont have a random offset, all nodes becom visible at the same zoom level -> stuttering
const zOffset = Math.random() - 0.5;
const zLimit = 2 - zOffset; const zLimit = 2 - zOffset;
const parameters = Object.entries(node?.tmp?.type?.inputs || {}).filter( const parameters = Object.entries(node.state?.type?.inputs || {}).filter(
(p) => (p) =>
p[1].type !== "seed" && !("setting" in p[1]) && p[1]?.hidden !== true, p[1].type !== "seed" && !("setting" in p[1]) && p[1]?.hidden !== true,
); );
onMount(() => { $effect(() => {
node.tmp = node.tmp || {}; if ("state" in node && !node.state.ref) {
node.tmp.ref = ref; node.state.ref = ref;
graphState?.updateNodePosition(node); graphState?.updateNodePosition(node);
}
}); });
</script> </script>

View File

@@ -1,24 +1,26 @@
<script lang="ts"> <script lang="ts">
import { getGraphState } from "../graph/state.svelte.js"; import { getGraphState } from "../graph-state.svelte";
import { createNodePath } from "../helpers/index.js"; import { createNodePath } from "../helpers/index.js";
import type { Node } from "@nodarium/types"; import type { NodeInstance } from "@nodarium/types";
const graphState = getGraphState(); const graphState = getGraphState();
const { node }: { node: Node } = $props(); const { node }: { node: NodeInstance } = $props();
function handleMouseDown(event: MouseEvent) { function handleMouseDown(event: MouseEvent) {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
graphState.setDownSocket?.({ if ("state" in node) {
node, graphState.setDownSocket?.({
index: 0, node,
position: graphState.getSocketPosition?.(node, 0), index: 0,
}); position: graphState.getSocketPosition?.(node, 0),
});
}
} }
const cornerTop = 10; const cornerTop = 10;
const rightBump = !!node?.tmp?.type?.outputs?.length; const rightBump = !!node?.state?.type?.outputs?.length;
const aspectRatio = 0.25; const aspectRatio = 0.25;
const path = createNodePath({ const path = createNodePath({
@@ -29,14 +31,6 @@
rightBump, rightBump,
aspectRatio, aspectRatio,
}); });
// const pathDisabled = createNodePath({
// depth: 0,
// height: 15,
// y: 50,
// cornerTop,
// rightBump,
// aspectRatio,
// });
const pathHover = createNodePath({ const pathHover = createNodePath({
depth: 8.5, depth: 8.5,
height: 50, height: 50,

View File

@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import type { Node, NodeInput } from "@nodarium/types"; import type { NodeInstance, NodeInput } from "@nodarium/types";
import { Input } from "@nodarium/ui"; import { Input } from "@nodarium/ui";
import type { GraphManager } from "../graph-manager.svelte"; import type { GraphManager } from "../graph-manager.svelte";
type Props = { type Props = {
node: Node; node: NodeInstance;
input: NodeInput; input: NodeInput;
id: string; id: string;
elementId?: string; elementId?: string;

View File

@@ -1,15 +1,12 @@
<script lang="ts"> <script lang="ts">
import type { import type { NodeInput, NodeInstance } from "@nodarium/types";
NodeInput as NodeInputType, import { createNodePath } from "../helpers";
Node as NodeType, import NodeInputEl from "./NodeInput.svelte";
} from "@nodarium/types"; import { getGraphManager, getGraphState } from "../graph-state.svelte";
import { createNodePath } from "../helpers/index.js";
import NodeInput from "./NodeInput.svelte";
import { getGraphManager, getGraphState } from "../graph/state.svelte.js";
type Props = { type Props = {
node: NodeType; node: NodeInstance;
input: NodeInputType; input: NodeInput;
id: string; id: string;
isLast?: boolean; isLast?: boolean;
}; };
@@ -18,7 +15,7 @@
let { node = $bindable(), input, id, isLast }: Props = $props(); let { node = $bindable(), input, id, isLast }: Props = $props();
const inputType = node?.tmp?.type?.inputs?.[id]!; const inputType = node?.state?.type?.inputs?.[id]!;
const socketId = `${node.id}-${id}`; const socketId = `${node.id}-${id}`;
@@ -37,7 +34,7 @@
}); });
} }
const leftBump = node.tmp?.type?.inputs?.[id].internal !== true; const leftBump = node.state?.type?.inputs?.[id].internal !== true;
const cornerBottom = isLast ? 5 : 0; const cornerBottom = isLast ? 5 : 0;
const aspectRatio = 0.5; const aspectRatio = 0.5;
@@ -76,21 +73,21 @@
{#key id && graphId} {#key id && graphId}
<div class="content" class:disabled={graph?.inputSockets?.has(socketId)}> <div class="content" class:disabled={graph?.inputSockets?.has(socketId)}>
{#if inputType.label !== ""} {#if inputType.label !== ""}
<label for={elementId}>{input.label || id}</label> <label for={elementId} title={input.description}
>{input.label || id}</label
>
{/if} {/if}
<span
class="absolute i-[tabler--help-circle] size-4 block top-2 right-2 opacity-30"
title={JSON.stringify(input, null, 2)}
></span>
{#if inputType.external !== true} {#if inputType.external !== true}
<NodeInput {graph} {elementId} bind:node {input} {id} /> <NodeInputEl {graph} {elementId} bind:node {input} {id} />
{/if} {/if}
</div> </div>
{#if node?.tmp?.type?.inputs?.[id]?.internal !== true} {#if node?.state?.type?.inputs?.[id]?.internal !== true}
<div <div data-node-socket class="large target"></div>
data-node-socket
class="large target"
onmousedown={handleMouseDown}
role="button"
tabindex="0"
></div>
<div <div
data-node-socket data-node-socket
class="small target" class="small target"
@@ -190,9 +187,6 @@
.content.disabled { .content.disabled {
opacity: 0.2; opacity: 0.2;
} }
.content.disabled > * {
pointer-events: none;
}
.disabled svg path { .disabled svg path {
d: var(--hover-path-disabled) !important; d: var(--hover-path-disabled) !important;

View File

@@ -16,9 +16,6 @@ export function grid(width: number, height: number) {
graph.nodes.push({ graph.nodes.push({
id: i, id: i,
tmp: {
visible: false,
},
position: [x * 30, y * 40], position: [x * 30, y * 40],
props: i == 0 ? { value: 0 } : { op_type: 0, a: 1, b: 0.05 }, props: i == 0 ? { value: 0 } : { op_type: 0, a: 1, b: 0.05 },
type: i == 0 ? "max/plantarium/float" : "max/plantarium/math", type: i == 0 ? "max/plantarium/float" : "max/plantarium/math",
@@ -29,9 +26,6 @@ export function grid(width: number, height: number) {
graph.nodes.push({ graph.nodes.push({
id: amount, id: amount,
tmp: {
visible: false,
},
position: [width * 30, (height - 1) * 40], position: [width * 30, (height - 1) * 40],
type: "max/plantarium/output", type: "max/plantarium/output",
props: {}, props: {},

View File

@@ -1,8 +1,8 @@
import type { Graph, Node } from "@nodarium/types"; import type { Graph, SerializedNode } from "@nodarium/types";
export function tree(depth: number): Graph { export function tree(depth: number): Graph {
const nodes: Node[] = [ const nodes: SerializedNode[] = [
{ {
id: 0, id: 0,
type: "max/plantarium/output", type: "max/plantarium/output",

View File

@@ -1,12 +1,16 @@
<script lang="ts"> <script lang="ts">
import { getContext } from "svelte"; import { getContext, type Snippet } from "svelte";
let index = -1; let index = $state(-1);
let wrapper: HTMLDivElement; let wrapper: HTMLDivElement;
$: if (index === -1) { const { children } = $props<{ children?: Snippet }>();
index = getContext<() => number>("registerCell")();
} $effect(() => {
if (index === -1) {
index = getContext<() => number>("registerCell")();
}
});
const sizes = getContext<{ value: string[] }>("sizes"); const sizes = getContext<{ value: string[] }>("sizes");
@@ -31,8 +35,8 @@
</script> </script>
<svelte:window <svelte:window
on:mouseup={() => (mouseDown = false)} onmouseup={() => (mouseDown = false)}
on:mousemove={handleMouseMove} onmousemove={handleMouseMove}
/> />
{#if index > 0} {#if index > 0}
@@ -40,12 +44,12 @@
class="seperator" class="seperator"
role="button" role="button"
tabindex="0" tabindex="0"
on:mousedown={handleMouseDown} onmousedown={handleMouseDown}
></div> ></div>
{/if} {/if}
<div class="cell" bind:this={wrapper}> <div class="cell" bind:this={wrapper}>
<slot /> {@render children?.()}
</div> </div>
<style> <style>

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { setContext } from "svelte"; import { setContext, type Snippet } from "svelte";
export let id = "grid-0"; const { children, id } = $props<{ children?: Snippet; id?: string }>();
setContext("grid-id", id); setContext("grid-id", id);
</script> </script>
<slot {id} /> {@render children({ id })}

View File

@@ -126,7 +126,7 @@ export function humanizeDuration(durationInMilliseconds: number) {
} }
if (millis > 0 || durationString === '') { if (millis > 0 || durationString === '') {
durationString += millis + 'ms'; durationString += Math.floor(millis) + 'ms';
} }
return durationString.trim(); return durationString.trim();

View File

@@ -1,4 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="17" y="8" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2"/> <rect x="17" y="8" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2"/>
<rect x="2" y="3" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2"/> <rect x="2" y="3" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2"/>
<rect x="2" y="14" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2"/> <rect x="2" y="14" width="5" height="7" rx="1" stroke="currentColor" stroke-width="2"/>

Before

Width:  |  Height:  |  Size: 519 B

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -1,9 +1,9 @@
import { createWasmWrapper } from "@nodarium/utils"; import { createWasmWrapper } from '@nodarium/utils';
import fs from "fs/promises"; import fs from 'fs/promises';
import path from "path"; import path from 'path';
export async function getWasm(id: `${string}/${string}/${string}`) { export async function getWasm(id: `${string}/${string}/${string}`) {
const filePath = path.resolve(`../nodes/${id}/pkg/index_bg.wasm`); const filePath = path.resolve(`./static/nodes/${id}`);
try { try {
await fs.access(filePath); await fs.access(filePath);
@@ -36,12 +36,12 @@ export async function getNode(id: `${string}/${string}/${string}`) {
} }
export async function getCollectionNodes(userId: `${string}/${string}`) { export async function getCollectionNodes(userId: `${string}/${string}`) {
const nodes = await fs.readdir(path.resolve(`../nodes/${userId}`)); const nodes = await fs.readdir(path.resolve(`./static/nodes/${userId}`));
return nodes return nodes
.filter((n) => n !== "pkg" && n !== ".template") .filter((n) => n !== 'pkg' && n !== '.template')
.map((n) => { .map((n) => {
return { return {
id: `${userId}/${n}`, id: `${userId}/${n}`
}; };
}); });
} }
@@ -50,20 +50,20 @@ export async function getCollection(userId: `${string}/${string}`) {
const nodes = await getCollectionNodes(userId); const nodes = await getCollectionNodes(userId);
return { return {
id: userId, id: userId,
nodes, nodes
}; };
} }
export async function getUserCollections(userId: string) { export async function getUserCollections(userId: string) {
const collections = await fs.readdir(path.resolve(`../nodes/${userId}`)); const collections = await fs.readdir(path.resolve(`./static/nodes/${userId}`));
return Promise.all( return Promise.all(
collections.map(async (n) => { collections.map(async (n) => {
const nodes = await getCollectionNodes(`${userId}/${n}`); const nodes = await getCollectionNodes(`${userId}/${n}`);
return { return {
id: `${userId}/${n}`, id: `${userId}/${n}`,
nodes, nodes
}; };
}), })
); );
} }
@@ -71,20 +71,20 @@ export async function getUser(userId: string) {
const collections = await getUserCollections(userId); const collections = await getUserCollections(userId);
return { return {
id: userId, id: userId,
collections, collections
}; };
} }
export async function getUsers() { export async function getUsers() {
const nodes = await fs.readdir(path.resolve("../nodes")); const nodes = await fs.readdir(path.resolve('./static/nodes'));
const users = await Promise.all( const users = await Promise.all(
nodes.map(async (n) => { nodes.map(async (n) => {
const collections = await getUserCollections(n); const collections = await getUserCollections(n);
return { return {
id: n, id: n,
collections, collections
}; };
}), })
); );
return users; return users;
} }

View File

@@ -1,10 +1,11 @@
<script lang="ts"> <script lang="ts">
import { Select } from "@nodarium/ui"; import { Select } from "@nodarium/ui";
import type { Writable } from "svelte/store";
let activeStore = 0; let activeStore = $state(0);
export let activeId: Writable<string>; let { activeId }: { activeId: string } = $props();
$: [activeUser, activeCollection, activeNode] = $activeId.split(`/`); const [activeUser, activeCollection, activeNode] = $derived(
activeId.split(`/`),
);
</script> </script>
<div class="breadcrumbs"> <div class="breadcrumbs">
@@ -12,16 +13,16 @@
<Select id="root" options={["root"]} bind:value={activeStore}></Select> <Select id="root" options={["root"]} bind:value={activeStore}></Select>
{#if activeCollection} {#if activeCollection}
<button <button
on:click={() => { onclick={() => {
$activeId = activeUser; activeId = activeUser;
}} }}
> >
{activeUser} {activeUser}
</button> </button>
{#if activeNode} {#if activeNode}
<button <button
on:click={() => { onclick={() => {
$activeId = `${activeUser}/${activeCollection}`; activeId = `${activeUser}/${activeCollection}`;
}} }}
> >
{activeCollection} {activeCollection}

View File

@@ -1,27 +1,27 @@
<script lang="ts"> <script lang="ts">
import NodeHtml from "$lib/graph-interface/node/NodeHTML.svelte"; import NodeHtml from "$lib/graph-interface/node/NodeHTML.svelte";
import type { NodeDefinition } from "@nodarium/types"; import type { NodeDefinition, NodeId, NodeInstance } from "@nodarium/types";
export let node: NodeDefinition; const { node }: { node: NodeDefinition } = $props();
let dragging = false; let dragging = $state(false);
let nodeData = { let nodeData = $state<NodeInstance>({
id: 0, id: 0,
type: node?.id, type: node.id as unknown as NodeId,
position: [0, 0] as [number, number], position: [0, 0] as [number, number],
props: {}, props: {},
tmp: { state: {
type: node, type: node,
}, },
}; });
function handleDragStart(e: DragEvent) { function handleDragStart(e: DragEvent) {
dragging = true; dragging = true;
const box = (e?.target as HTMLElement)?.getBoundingClientRect(); const box = (e?.target as HTMLElement)?.getBoundingClientRect();
if (e.dataTransfer === null) return; if (e.dataTransfer === null) return;
e.dataTransfer.effectAllowed = "move"; e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("data/node-id", node.id); e.dataTransfer.setData("data/node-id", node.id.toString());
if (nodeData.props) { if (nodeData.props) {
e.dataTransfer.setData("data/node-props", JSON.stringify(nodeData.props)); e.dataTransfer.setData("data/node-props", JSON.stringify(nodeData.props));
} }
@@ -38,15 +38,15 @@
<div class="node-wrapper" class:dragging> <div class="node-wrapper" class:dragging>
<div <div
on:dragend={() => { ondragend={() => {
dragging = false; dragging = false;
}} }}
draggable={true} draggable={true}
role="button" role="button"
tabindex="0" tabindex="0"
on:dragstart={handleDragStart} ondragstart={handleDragStart}
> >
<NodeHtml inView={true} position={"relative"} z={5} bind:node={nodeData} /> <NodeHtml bind:node={nodeData} inView={true} position={"relative"} z={5} />
</div> </div>
</div> </div>

View File

@@ -1,19 +1,16 @@
<script lang="ts"> <script lang="ts">
import { writable } from "svelte/store";
import BreadCrumbs from "./BreadCrumbs.svelte"; import BreadCrumbs from "./BreadCrumbs.svelte";
import DraggableNode from "./DraggableNode.svelte"; import DraggableNode from "./DraggableNode.svelte";
import type { RemoteNodeRegistry } from "@nodarium/registry"; import type { RemoteNodeRegistry } from "@nodarium/registry";
export let registry: RemoteNodeRegistry; const { registry }: { registry: RemoteNodeRegistry } = $props();
const activeId = writable("max/plantarium"); let activeId = $state("max/plantarium");
let showBreadCrumbs = false; let showBreadCrumbs = false;
// const activeId = localStore< const [activeUser, activeCollection, activeNode] = $derived(
// `${string}` | `${string}/${string}` | `${string}/${string}/${string}` activeId.split(`/`),
// >("nodes.store.activeId", ""); );
$: [activeUser, activeCollection, activeNode] = $activeId.split(`/`);
</script> </script>
{#if showBreadCrumbs} {#if showBreadCrumbs}
@@ -27,8 +24,8 @@
{:then users} {:then users}
{#each users as user} {#each users as user}
<button <button
on:click={() => { onclick={() => {
$activeId = user.id; activeId = user.id;
}}>{user.id}</button }}>{user.id}</button
> >
{/each} {/each}
@@ -41,8 +38,8 @@
{:then user} {:then user}
{#each user.collections as collection} {#each user.collections as collection}
<button <button
on:click={() => { onclick={() => {
$activeId = collection.id; activeId = collection.id;
}} }}
> >
{collection.id.split(`/`)[1]} {collection.id.split(`/`)[1]}

View File

@@ -1,8 +1,12 @@
<script lang="ts"> <script lang="ts">
export let labels: string[] = []; type Props = {
export let values: number[] = []; labels: string[];
values: number[];
};
$: total = values.reduce((acc, v) => acc + v, 0); const { labels, values }: Props = $props();
const total = $derived(values.reduce((acc, v) => acc + v, 0));
let colors = ["red", "green", "blue"]; let colors = ["red", "green", "blue"];
</script> </script>
@@ -10,7 +14,10 @@
<div class="wrapper"> <div class="wrapper">
<div class="bars"> <div class="bars">
{#each values as value, i} {#each values as value, i}
<div class="bar bg-{colors[i]}" style="width: {(value / total) * 100}%;"> <div
class="bar bg-{colors[i]}-400"
style="width: {(value / total) * 100}%;"
>
{Math.round(value)}ms {Math.round(value)}ms
</div> </div>
{/each} {/each}
@@ -18,12 +25,11 @@
<div class="labels mt-2"> <div class="labels mt-2">
{#each values as _label, i} {#each values as _label, i}
<div class="text-{colors[i]}">{labels[i]}</div> <div class="text-{colors[i]}-400">{labels[i]}</div>
{/each} {/each}
</div> </div>
<span <span
class="bg-red bg-green bg-yellow bg-blue text-red text-green text-yellow text-blue" class="bg-red-400 bg-green-400 bg-blue-400 text-red-400 text-green-400 text-blue-400"
></span> ></span>
</div> </div>

View File

@@ -1,52 +1,59 @@
<script lang="ts"> <script lang="ts">
export let points: number[]; type Props = {
points: number[];
type?: string;
title?: string;
max?: number;
min?: number;
};
export let type = "ms"; let {
export let title = "Performance"; points,
export let max: number | undefined = undefined; type = "ms",
export let min: number | undefined = undefined; title = "Performance",
max,
min,
}: Props = $props();
function getMax(m?: number) { let internalMax = $derived(max ?? Math.max(...points));
let internalMin = $derived(min ?? Math.min(...points))!;
const maxText = $derived.by(() => {
if (type === "%") { if (type === "%") {
return 100; return 100;
} }
if (m !== undefined) { if (internalMax !== undefined) {
if (m < 1) { if (internalMax < 1) {
return Math.floor(m * 100) / 100; return Math.floor(internalMax * 100) / 100;
} }
if (m < 10) { if (internalMax < 10) {
return Math.floor(m * 10) / 10; return Math.floor(internalMax * 10) / 10;
} }
return Math.floor(m); return Math.floor(internalMax);
} }
return 1; return 1;
} });
function constructPath() { const path = $derived(
max = max !== undefined ? max : Math.max(...points); points
min = min !== undefined ? min : Math.min(...points);
const mi = min as number;
const ma = max as number;
return points
.map((point, i) => { .map((point, i) => {
const x = (i / (points.length - 1)) * 100; const x = (i / (points.length - 1)) * 100;
const y = 100 - ((point - mi) / (ma - mi)) * 100; const y =
100 - ((point - internalMin) / (internalMax - internalMin)) * 100;
return `${x},${y}`; return `${x},${y}`;
}) })
.join(" "); .join(" "),
} );
</script> </script>
<div class="wrapper"> <div class="wrapper">
<p>{title}</p> <p>{title}</p>
<span class="min">{Math.floor(min || 0)}{type}</span> <span class="min">{Math.floor(internalMin || 0)}{type}</span>
<span class="max">{getMax(max)}{type}</span> <span class="max">{maxText}{type}</span>
<svg preserveAspectRatio="none" viewBox="0 0 100 100"> <svg preserveAspectRatio="none" viewBox="0 0 100 100">
{#key points} <polyline vector-effect="non-scaling-stroke" points={path} />
<polyline vector-effect="non-scaling-stroke" points={constructPath()} />
{/key}
</svg> </svg>
</div> </div>

View File

@@ -2,23 +2,13 @@
import Monitor from "./Monitor.svelte"; import Monitor from "./Monitor.svelte";
import { humanizeNumber } from "$lib/helpers"; import { humanizeNumber } from "$lib/helpers";
import { Checkbox } from "@nodarium/ui"; import { Checkbox } from "@nodarium/ui";
import localStore from "$lib/helpers/localStore"; import type { PerformanceData } from "@nodarium/utils";
import { type PerformanceData } from "@nodarium/utils";
import BarSplit from "./BarSplit.svelte"; import BarSplit from "./BarSplit.svelte";
export let data: PerformanceData; const { data }: { data: PerformanceData } = $props();
let activeType = localStore<string>("nodes.performance.active-type", "total"); let activeType = $state("total");
let showAverage = true; let showAverage = $state(true);
function getAverage(key: string) {
return (
data
.map((run) => run[key]?.[0])
.filter((v) => v !== undefined)
.reduce((acc, run) => acc + run, 0) / data.length
);
}
function round(v: number) { function round(v: number) {
if (v < 1) { if (v < 1) {
@@ -30,45 +20,15 @@
return Math.floor(v); return Math.floor(v);
} }
function getAverages() { function getTitle(t: string) {
let lastRun = data.at(-1); if (t.includes("/")) {
if (!lastRun) return {}; return `Node ${t.split("/").slice(-1).join("/")}`;
return Object.keys(lastRun).reduce(
(acc, key) => {
acc[key] = getAverage(key);
return acc;
},
{} as Record<string, number>,
);
}
function getLast(key: string) {
return data.at(-1)?.[key]?.[0] || 0;
}
function getLasts() {
return data.at(-1) || {};
}
function getTotalPerformance(onlyLast = false) {
if (onlyLast) {
return (
getLast("runtime") +
getLast("update-geometries") +
getLast("worker-transfer")
);
} }
return (
getAverage("runtime") +
getAverage("update-geometries") +
getAverage("worker-transfer")
);
}
function getCacheRatio(onlyLast = false) { return t
let ratio = onlyLast ? getLast("cache-hit") : getAverage("cache-hit"); .split("-")
.map((v) => v[0].toUpperCase() + v.slice(1))
return Math.floor(ratio * 100); .join(" ");
} }
const viewerKeys = [ const viewerKeys = [
@@ -78,10 +38,53 @@
"split-result", "split-result",
]; ];
function getPerformanceData(onlyLast: boolean = false) { // --- Small helpers that query `data` directly ---
let data = onlyLast ? getLasts() : getAverages(); function getAverage(key: string) {
const vals = data
.map((run) => run[key]?.[0])
.filter((v) => v !== undefined) as number[];
return Object.entries(data) if (vals.length === 0) return 0;
return vals.reduce((acc, v) => acc + v, 0) / vals.length;
}
function getLast(key: string) {
return data.at(-1)?.[key]?.[0] || 0;
}
const averages = $derived.by(() => {
const lr = data.at(-1);
if (!lr) return {} as Record<string, number>;
return Object.keys(lr).reduce((acc: Record<string, number>, key) => {
acc[key] = getAverage(key);
return acc;
}, {});
});
const lasts = $derived.by(() => data.at(-1) || {});
const totalPerformance = $derived.by(() => {
const onlyLast =
getLast("runtime") +
getLast("update-geometries") +
getLast("worker-transfer");
const average =
getAverage("runtime") +
getAverage("update-geometries") +
getAverage("worker-transfer");
return { onlyLast, average };
});
const cacheRatio = $derived.by(() => {
return {
onlyLast: Math.floor(getLast("cache-hit") * 100),
average: Math.floor(getAverage("cache-hit") * 100),
};
});
const performanceData = $derived.by(() => {
const source = showAverage ? averages : lasts;
return Object.entries(source)
.filter( .filter(
([key]) => ([key]) =>
!key.startsWith("node/") && !key.startsWith("node/") &&
@@ -90,19 +93,18 @@
!viewerKeys.includes(key), !viewerKeys.includes(key),
) )
.sort((a, b) => b[1] - a[1]); .sort((a, b) => b[1] - a[1]);
} });
function getNodePerformanceData(onlyLast: boolean = false) { const nodePerformanceData = $derived.by(() => {
let data = onlyLast ? getLasts() : getAverages(); const source = showAverage ? averages : lasts;
return Object.entries(source)
return Object.entries(data)
.filter(([key]) => key.startsWith("node/")) .filter(([key]) => key.startsWith("node/"))
.sort((a, b) => b[1] - a[1]); .sort((a, b) => b[1] - a[1]);
} });
function getViewerPerformanceData(onlyLast: boolean = false) { const viewerPerformanceData = $derived.by(() => {
let data = onlyLast ? getLasts() : getAverages(); const source = showAverage ? averages : lasts;
return Object.entries(data) return Object.entries(source)
.filter( .filter(
([key]) => ([key]) =>
key !== "total-vertices" && key !== "total-vertices" &&
@@ -110,14 +112,29 @@
viewerKeys.includes(key), viewerKeys.includes(key),
) )
.sort((a, b) => b[1] - a[1]); .sort((a, b) => b[1] - a[1]);
} });
function getTotalPoints() { const splitValues = $derived.by(() => {
if (showAverage) {
return [
getAverage("worker-transfer"),
getAverage("runtime"),
getAverage("update-geometries"),
];
}
return [
getLast("worker-transfer"),
getLast("runtime"),
getLast("update-geometries"),
];
});
const totalPoints = $derived.by(() => {
if (showAverage) { if (showAverage) {
return data.map((run) => { return data.map((run) => {
return ( return (
run["runtime"].reduce((acc, v) => acc + v, 0) + (run["runtime"]?.reduce((acc, v) => acc + v, 0) || 0) +
run["update-geometries"].reduce((acc, v) => acc + v, 0) + (run["update-geometries"]?.reduce((acc, v) => acc + v, 0) || 0) +
(run["worker-transfer"]?.reduce((acc, v) => acc + v, 0) || 0) (run["worker-transfer"]?.reduce((acc, v) => acc + v, 0) || 0)
); );
}); });
@@ -125,16 +142,16 @@
return data.map((run) => { return data.map((run) => {
return ( return (
run["runtime"][0] + (run["runtime"]?.[0] || 0) +
run["update-geometries"][0] + (run["update-geometries"]?.[0] || 0) +
(run["worker-transfer"]?.[0] || 0) (run["worker-transfer"]?.[0] || 0)
); );
}); });
} });
function constructPoints(key: string) { function constructPoints(key: string) {
if (key === "total") { if (key === "total") {
return getTotalPoints(); return totalPoints;
} }
return data.map((run) => { return data.map((run) => {
if (key in run) { if (key in run) {
@@ -148,47 +165,33 @@
}); });
} }
function getSplitValues(): number[] { const computedTotalDisplay = $derived.by(() =>
if (showAverage) { round(showAverage ? totalPerformance.average : totalPerformance.onlyLast),
return [ );
getAverage("worker-transfer"),
getAverage("runtime"),
getAverage("update-geometries"),
];
}
return [ const computedFps = $derived.by(() =>
getLast("worker-transfer"), Math.floor(
getLast("runtime"), 1000 /
getLast("update-geometries"), (showAverage
]; ? totalPerformance.average || 1
} : totalPerformance.onlyLast || 1),
),
function getTitle(t: string) { );
if (t.includes("/")) {
return `Node ${t.split("/").slice(-1).join("/")}`;
}
return t
.split("-")
.map((v) => v[0].toUpperCase() + v.slice(1))
.join(" ");
}
</script> </script>
{#key $activeType && data} {#if data.length !== 0}
{#if $activeType === "cache-hit"} {#if activeType === "cache-hit"}
<Monitor <Monitor
title="Cache Hits" title="Cache Hits"
points={constructPoints($activeType)} points={constructPoints(activeType)}
min={0} min={0}
max={1} max={1}
type="%" type="%"
/> />
{:else} {:else}
<Monitor <Monitor
title={getTitle($activeType)} title={getTitle(activeType)}
points={constructPoints($activeType)} points={constructPoints(activeType)}
/> />
{/if} {/if}
@@ -198,116 +201,108 @@
<label for="show-total">Show Average</label> <label for="show-total">Show Average</label>
</div> </div>
{#if data.length !== 0} <BarSplit
<BarSplit labels={["worker-transfer", "runtime", "update-geometries"]}
labels={["worker-transfer", "runtime", "update-geometries"]} values={splitValues}
values={getSplitValues()} />
/>
<h3>General</h3> <h3>General</h3>
<table> <table>
<tbody> <tbody>
<tr>
<td>
{computedTotalDisplay}<span>ms</span>
</td>
<td
class:active={activeType === "total"}
onclick={() => (activeType = "total")}
>
total<span>({computedFps}fps)</span>
</td>
</tr>
{#each performanceData as [key, value]}
<tr> <tr>
<td> <td>{round(value)}<span>ms</span></td>
{round(getTotalPerformance(!showAverage))}<span>ms</span>
</td>
<td <td
class:active={$activeType === "total"} class:active={activeType === key}
on:click={() => ($activeType = "total")} onclick={() => (activeType = key)}
> >
total<span {key}
>({Math.floor(
1000 / getTotalPerformance(showAverage),
)}fps)</span
>
</td> </td>
</tr> </tr>
{#each getPerformanceData(!showAverage) as [key, value]} {/each}
<tr>
<td>
{round(value)}<span>ms</span>
</td>
<td
class:active={$activeType === key}
on:click={() => ($activeType = key)}
>
{key}
</td>
</tr>
{/each}
<tr>
<td>{data.length}</td>
<td>Samples</td>
</tr>
</tbody>
<tbody>
<tr><td><h3>Nodes</h3></td></tr>
</tbody>
<tbody>
<tr>
<td
>{showAverage ? cacheRatio.average : cacheRatio.onlyLast}<span
>%</span
></td
>
<td
class:active={activeType === "cache-hit"}
onclick={() => (activeType = "cache-hit")}
>
cache hits
</td>
</tr>
{#each nodePerformanceData as [key, value]}
<tr> <tr>
<td>{data.length}</td> <td>{round(value)}<span>ms</span></td>
<td>Samples</td>
</tr>
</tbody>
<tbody>
<tr>
<td>
<h3>Nodes</h3>
</td>
</tr>
</tbody>
<tbody>
<tr>
<td> {getCacheRatio(!showAverage)}<span>%</span> </td>
<td <td
class:active={$activeType === "cache-hit"} class:active={activeType === key}
on:click={() => ($activeType = "cache-hit")}>cache hits</td onclick={() => (activeType = key)}
> >
</tr> {key.split("/").slice(-1).join("/")}
{#each getNodePerformanceData(!showAverage) as [key, value]}
<tr>
<td>
{round(value)}<span>ms</span>
</td>
<td
class:active={$activeType === key}
on:click={() => ($activeType = key)}
>
{key.split("/").slice(-1).join("/")}
</td>
</tr>
{/each}
</tbody>
<tbody>
<tr>
<td>
<h3>Viewer</h3>
</td> </td>
</tr> </tr>
</tbody> {/each}
<tbody> </tbody>
<tbody>
<tr><td><h3>Viewer</h3></td></tr>
</tbody>
<tbody>
<tr>
<td>{humanizeNumber(getLast("total-vertices"))}</td>
<td>Vertices</td>
</tr>
<tr>
<td>{humanizeNumber(getLast("total-faces"))}</td>
<td>Faces</td>
</tr>
{#each viewerPerformanceData as [key, value]}
<tr> <tr>
<td>{humanizeNumber(getLast("total-vertices"))}</td> <td>{round(value)}<span>ms</span></td>
<td>Vertices</td> <td
class:active={activeType === key}
onclick={() => (activeType = key)}
>
{key.split("/").slice(-1).join("/")}
</td>
</tr> </tr>
<tr> {/each}
<td>{humanizeNumber(getLast("total-faces"))}</td> </tbody>
<td>Faces</td> </table>
</tr>
{#each getViewerPerformanceData(!showAverage) as [key, value]}
<tr>
<td>
{round(value)}<span>ms</span>
</td>
<td
class:active={$activeType === key}
on:click={() => ($activeType = key)}
>
{key.split("/").slice(-1).join("/")}
</td>
</tr>
{/each}
</tbody>
</table>
{:else}
<p>No runs available</p>
{/if}
</div> </div>
{/key} {:else}
<p>No runs available</p>
{/if}
<style> <style>
h3 { h3 {

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
export let points: number[]; const { points }: { points: number[] } = $props();
function constructPath() { const path = $derived.by(() => {
const max = Math.max(...points); const max = Math.max(...points);
const min = Math.min(...points); const min = Math.min(...points);
return points return points
@@ -11,13 +11,11 @@
return `${x},${y}`; return `${x},${y}`;
}) })
.join(" "); .join(" ");
} });
</script> </script>
<svg preserveAspectRatio="none" viewBox="0 0 100 100"> <svg preserveAspectRatio="none" viewBox="0 0 100 100">
{#key points} <polyline vector-effect="non-scaling-stroke" points={path} />
<polyline vector-effect="non-scaling-stroke" points={constructPath()} />
{/key}
</svg> </svg>
<style> <style>

View File

@@ -1,25 +1,23 @@
<script lang="ts"> <script lang="ts">
import { humanizeDuration, humanizeNumber } from "$lib/helpers"; import { humanizeDuration, humanizeNumber } from "$lib/helpers";
import localStore from "$lib/helpers/localStore"; import { localState } from "$lib/helpers/localState.svelte";
import SmallGraph from "./SmallGraph.svelte"; import SmallGraph from "./SmallGraph.svelte";
import type { PerformanceData, PerformanceStore } from "@nodarium/utils"; import type { PerformanceData, PerformanceStore } from "@nodarium/utils";
export let store: PerformanceStore; const { store, fps }: { store: PerformanceStore; fps: number[] } = $props();
const open = localStore("node.performance.small.open", { const open = localState("node.performance.small.open", {
runtime: false, runtime: false,
fps: false, fps: false,
}); });
$: vertices = $store?.at(-1)?.["total-vertices"]?.[0] || 0; const vertices = $derived($store?.at(-1)?.["total-vertices"]?.[0] || 0);
$: faces = $store?.at(-1)?.["total-faces"]?.[0] || 0; const faces = $derived($store?.at(-1)?.["total-faces"]?.[0] || 0);
$: runtime = $store?.at(-1)?.["runtime"]?.[0] || 0; const runtime = $derived($store?.at(-1)?.["runtime"]?.[0] || 0);
function getPoints(data: PerformanceData, key: string) { function getPoints(data: PerformanceData, key: string) {
return data?.map((run) => run[key]?.[0] || 0) || []; return data?.map((run) => run[key]?.[0] || 0) || [];
} }
export let fps: number[] = [];
</script> </script>
<div class="wrapper"> <div class="wrapper">
@@ -27,12 +25,12 @@
<tbody> <tbody>
<tr <tr
style="cursor:pointer;" style="cursor:pointer;"
on:click={() => ($open.runtime = !$open.runtime)} onclick={() => (open.value.runtime = !open.value.runtime)}
> >
<td>{$open.runtime ? "-" : "+"} runtime </td> <td>{open.value.runtime ? "-" : "+"} runtime </td>
<td>{humanizeDuration(runtime || 1000)}</td> <td>{humanizeDuration(runtime || 1000)}</td>
</tr> </tr>
{#if $open.runtime} {#if open.value.runtime}
<tr> <tr>
<td colspan="2"> <td colspan="2">
<SmallGraph points={getPoints($store, "runtime")} /> <SmallGraph points={getPoints($store, "runtime")} />
@@ -40,13 +38,16 @@
</tr> </tr>
{/if} {/if}
<tr style="cursor:pointer;" on:click={() => ($open.fps = !$open.fps)}> <tr
<td>{$open.fps ? "-" : "+"} fps </td> style="cursor:pointer;"
onclick={() => (open.value.fps = !open.value.fps)}
>
<td>{open.value.fps ? "-" : "+"} fps </td>
<td> <td>
{Math.floor(fps[fps.length - 1])}fps {Math.floor(fps[fps.length - 1])}fps
</td> </td>
</tr> </tr>
{#if $open.fps} {#if open.value.fps}
<tr> <tr>
<td colspan="2"> <td colspan="2">
<SmallGraph points={fps} /> <SmallGraph points={fps} />

View File

@@ -0,0 +1,108 @@
<script lang="ts">
import type { Graph } from "$lib/types";
import { defaultPlant, plant, lottaFaces } from "$lib/graph-templates";
import type { ProjectManager } from "./project-manager.svelte";
const { projectManager } = $props<{ projectManager: ProjectManager }>();
let showNewProject = $state(false);
let newProjectName = $state("");
let selectedTemplate = $state("defaultPlant");
const templates = [
{
name: "Default Plant",
value: "defaultPlant",
graph: defaultPlant as unknown as Graph,
},
{ name: "Plant", value: "plant", graph: plant as unknown as Graph },
{
name: "Lotta Faces",
value: "lottaFaces",
graph: lottaFaces as unknown as Graph,
},
];
function handleCreate() {
const template =
templates.find((t) => t.value === selectedTemplate) || templates[0];
projectManager.handleCreateProject(template.graph, newProjectName);
newProjectName = "";
showNewProject = false;
}
</script>
<header
class="flex justify-between px-4 h-[70px] border-b-1 border-[var(--outline)] items-center"
>
<h3>Project</h3>
<button
class="px-3 py-1 bg-[var(--layer-0)] rounded"
onclick={() => (showNewProject = !showNewProject)}
>
New
</button>
</header>
{#if showNewProject}
<div class="flex flex-col px-4 py-3 border-b-1 border-[var(--outline)] gap-2">
<input
type="text"
bind:value={newProjectName}
placeholder="Project name"
class="w-full px-2 py-2 bg-gray-800 border border-gray-700 rounded"
onkeydown={(e) => e.key === "Enter" && handleCreate()}
/>
<select
bind:value={selectedTemplate}
class="w-full px-2 py-2 bg-gray-800 border border-gray-700 rounded"
>
{#each templates as template}
<option value={template.value}>{template.name}</option>
{/each}
</select>
<button
class="cursor-pointer self-end px-3 py-1 bg-blue-600 rounded"
onclick={() => handleCreate()}
>
Create
</button>
</div>
{/if}
<div class="p-4 text-white min-h-screen">
{#if projectManager.loading}
<p>Loading...</p>
{/if}
<ul class="space-y-2">
{#each projectManager.projects as project (project.id)}
<li>
<div
class="w-full text-left px-3 py-2 rounded cursor-pointer {projectManager
.activeProjectId.value === project.id
? 'bg-blue-600'
: 'bg-gray-800 hover:bg-gray-700'}"
onclick={() => projectManager.handleSelectProject(project.id!)}
role="button"
tabindex="0"
onkeydown={(e) =>
e.key === "Enter" &&
projectManager.handleSelectProject(project.id!)}
>
<div class="flex justify-between items-center">
<span>{project.meta?.title || "Untitled"}</span>
<button
class="text-red-400 hover:text-red-300"
onclick={() => {
projectManager.handleDeleteProject(project.id!);
}}
>
×
</button>
</div>
</div>
</li>
{/each}
</ul>
</div>

View File

@@ -0,0 +1,52 @@
import type { Graph } from '@nodarium/types';
import { type IDBPDatabase, openDB } from 'idb';
export interface GraphDatabase {
projects: Graph;
}
const DB_NAME = 'nodarium-graphs';
const DB_VERSION = 1;
const STORE_NAME = 'graphs';
let dbPromise: Promise<IDBPDatabase<GraphDatabase>> | null = null;
export function getDB() {
if (!dbPromise) {
dbPromise = openDB<GraphDatabase>(DB_NAME, DB_VERSION, {
upgrade(db) {
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
}
}
});
}
return dbPromise;
}
export async function getGraph(id: number): Promise<Graph | undefined> {
const db = await getDB();
return db.get(STORE_NAME, id);
}
export async function saveGraph(graph: Graph): Promise<Graph> {
const db = await getDB();
graph.meta = { ...graph.meta, lastModified: new Date().toISOString() };
await db.put(STORE_NAME, graph);
return graph;
}
export async function deleteGraph(id: number): Promise<void> {
const db = await getDB();
await db.delete(STORE_NAME, id);
}
export async function getGraphs(): Promise<Graph[]> {
const db = await getDB();
return db.getAll(STORE_NAME);
}
export async function clear(): Promise<void> {
const db = await getDB();
return db.clear(STORE_NAME);
}

View File

@@ -0,0 +1,85 @@
import * as templates from '$lib/graph-templates';
import { localState } from '$lib/helpers/localState.svelte';
import type { Graph } from '@nodarium/types';
import * as db from './project-database.svelte';
export class ProjectManager {
public graph = $state<Graph>();
private projects = $state<Graph[]>([]);
private activeProjectId = localState<number | undefined>(
'node.activeProjectId',
undefined
);
public readonly loading = $derived(this.graph?.id !== this.activeProjectId.value);
constructor() {
this.init();
}
async saveGraph(g: Graph) {
db.saveGraph(g);
}
private async init() {
await db.getDB();
this.projects = await db.getGraphs();
if (this.activeProjectId.value !== undefined) {
let loadedGraph = await db.getGraph(this.activeProjectId.value);
if (loadedGraph) {
this.graph = loadedGraph;
}
}
if (!this.graph) {
if (this.projects?.length && this.projects[0]?.id !== undefined) {
this.graph = this.projects[0];
this.activeProjectId.value = this.graph.id;
}
}
if (!this.graph) {
this.handleCreateProject();
}
}
public handleCreateProject(
g: Graph = templates.defaultPlant as unknown as Graph,
title: string = 'New Project'
) {
let id = g?.id || 0;
while (this.projects.find((p) => p.id === id)) {
id++;
}
g.id = id;
if (!g.meta) g.meta = {};
if (!g.meta.title) g.meta.title = title;
db.saveGraph(g);
this.projects = [...this.projects, g];
this.handleSelectProject(id);
}
public async handleDeleteProject(projectId: number) {
await db.deleteGraph(projectId);
if (this.projects.length === 1) {
this.graph = undefined;
this.projects = [];
} else {
this.projects = this.projects.filter((p) => p.id !== projectId);
const id = this.projects[0].id;
if (id !== undefined) {
this.handleSelectProject(id);
}
}
}
public async handleSelectProject(id: number) {
if (this.activeProjectId.value !== id) {
const project = await db.getGraph(id);
this.graph = project;
this.activeProjectId.value = id;
}
}
}

View File

@@ -35,6 +35,9 @@
scene = $bindable(), scene = $bindable(),
}: Props = $props(); }: Props = $props();
let geometries = $state.raw<BufferGeometry[]>([]);
let center = $state(new Vector3(0, 4, 0));
useTask( useTask(
(delta) => { (delta) => {
fps.push(1 / delta); fps.push(1 / delta);
@@ -45,11 +48,13 @@
export const invalidate = function () { export const invalidate = function () {
if (scene) { if (scene) {
geometries = scene.children const geos: BufferGeometry[] = [];
.filter((child) => "geometry" in child && child.isObject3D) scene.traverse(function (child) {
.map((child) => { if (isMesh(child)) {
return (child as Mesh).geometry; geos.push(child.geometry);
}); }
});
geometries = geos;
} }
if (geometries && scene && centerCamera) { if (geometries && scene && centerCamera) {
@@ -62,9 +67,6 @@
_invalidate(); _invalidate();
}; };
let geometries = $state<BufferGeometry[]>();
let center = $state(new Vector3(0, 4, 0));
function isMesh(child: Mesh | any): child is Mesh { function isMesh(child: Mesh | any): child is Mesh {
return child.isObject3D && "material" in child; return child.isObject3D && "material" in child;
} }
@@ -76,7 +78,7 @@
$effect(() => { $effect(() => {
const wireframe = appSettings.value.debug.wireframe; const wireframe = appSettings.value.debug.wireframe;
scene.traverse(function (child) { scene.traverse(function (child) {
if (isMesh(child) && isMatCapMaterial(child.material)) { if (isMesh(child) && isMatCapMaterial(child.material) && child.visible) {
child.material.wireframe = wireframe; child.material.wireframe = wireframe;
} }
}); });

View File

@@ -95,12 +95,14 @@
<SmallPerformanceViewer {fps} store={perf} /> <SmallPerformanceViewer {fps} store={perf} />
{/if} {/if}
<Canvas> <div style="height: 100%">
<Scene <Canvas>
bind:this={sceneComponent} <Scene
{lines} bind:this={sceneComponent}
{centerCamera} {lines}
bind:scene {centerCamera}
bind:fps bind:scene
/> bind:fps
</Canvas> />
</Canvas>
</div>

View File

@@ -11,7 +11,7 @@ import {
} from "three"; } from "three";
function fastArrayHash(arr: Int32Array) { function fastArrayHash(arr: Int32Array) {
const sampleDistance = Math.max(Math.floor(arr.length / 100), 1); const sampleDistance = Math.max(Math.floor(arr.length / 1000), 1);
const sampleCount = Math.floor(arr.length / sampleDistance); const sampleCount = Math.floor(arr.length / sampleDistance);
let hash = new Int32Array(sampleCount); let hash = new Int32Array(sampleCount);
@@ -40,6 +40,9 @@ export function createGeometryPool(parentScene: Group, material: Material) {
let hash = fastArrayHash(data); let hash = fastArrayHash(data);
let geometry = existingMesh ? existingMesh.geometry : new BufferGeometry(); let geometry = existingMesh ? existingMesh.geometry : new BufferGeometry();
if (existingMesh) {
existingMesh.visible = true;
}
// Extract data from the encoded array // Extract data from the encoded array
// const geometryType = encodedData[index++]; // const geometryType = encodedData[index++];
@@ -121,7 +124,6 @@ export function createGeometryPool(parentScene: Group, material: Material) {
updateSingleGeometry(input, existingMesh || null); updateSingleGeometry(input, existingMesh || null);
} else if (existingMesh) { } else if (existingMesh) {
existingMesh.visible = false; existingMesh.visible = false;
scene.remove(existingMesh);
} }
} }
return { totalVertices, totalFaces }; return { totalVertices, totalFaces };
@@ -258,7 +260,6 @@ export function createInstancedGeometryPool(
updateSingleInstance(input, existingMesh || null); updateSingleInstance(input, existingMesh || null);
} else if (existingMesh) { } else if (existingMesh) {
existingMesh.visible = false; existingMesh.visible = false;
scene.remove(existingMesh);
} }
} }
return { totalVertices, totalFaces }; return { totalVertices, totalFaces };

View File

@@ -1,19 +1,33 @@
import { type SyncCache } from "@nodarium/types"; import { type SyncCache } from "@nodarium/types";
export class MemoryRuntimeCache implements SyncCache { export class MemoryRuntimeCache implements SyncCache {
private map = new Map<string, unknown>();
size: number;
private cache: [string, unknown][] = []; constructor(size = 50) {
size = 50; this.size = size;
}
get<T>(key: string): T | undefined { get<T>(key: string): T | undefined {
return this.cache.find(([k]) => k === key)?.[1] as T; if (!this.map.has(key)) return undefined;
} const value = this.map.get(key) as T;
set<T>(key: string, value: T): void { this.map.delete(key);
this.cache.push([key, value]); this.map.set(key, value);
this.cache = this.cache.slice(-this.size); return value;
}
clear(): void {
this.cache = [];
} }
set<T>(key: string, value: T): void {
if (this.map.has(key)) {
this.map.delete(key);
}
this.map.set(key, value);
while (this.map.size > this.size) {
const oldestKey = this.map.keys().next().value as string;
this.map.delete(oldestKey);
}
}
clear(): void {
this.map.clear();
}
} }

View File

@@ -1,6 +1,5 @@
import type { import type {
Graph, Graph,
Node,
NodeDefinition, NodeDefinition,
NodeInput, NodeInput,
NodeRegistry, NodeRegistry,
@@ -14,6 +13,7 @@ import {
fastHashArrayBuffer, fastHashArrayBuffer,
type PerformanceStore, type PerformanceStore,
} from "@nodarium/utils"; } from "@nodarium/utils";
import type { RuntimeNode } from "./types";
const log = createLogger("runtime-executor"); const log = createLogger("runtime-executor");
log.mute(); log.mute();
@@ -58,13 +58,13 @@ function getValue(input: NodeInput, value?: unknown) {
export class MemoryRuntimeExecutor implements RuntimeExecutor { export class MemoryRuntimeExecutor implements RuntimeExecutor {
private definitionMap: Map<string, NodeDefinition> = new Map(); private definitionMap: Map<string, NodeDefinition> = new Map();
private randomSeed = Math.floor(Math.random() * 100000000); private seed = Math.floor(Math.random() * 100000000);
perf?: PerformanceStore; perf?: PerformanceStore;
constructor( constructor(
private registry: NodeRegistry, private registry: NodeRegistry,
private cache?: SyncCache<Int32Array>, public cache?: SyncCache<Int32Array>,
) { ) {
this.cache = undefined; this.cache = undefined;
} }
@@ -92,18 +92,27 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
// First, lets check if all nodes have a definition // First, lets check if all nodes have a definition
this.definitionMap = await this.getNodeDefinitions(graph); this.definitionMap = await this.getNodeDefinitions(graph);
const outputNode = graph.nodes.find((node) => const graphNodes = graph.nodes.map(node => {
const n = node as RuntimeNode;
n.state = {
depth: 0,
children: [],
parents: [],
inputNodes: {},
}
return n
})
const outputNode = graphNodes.find((node) =>
node.type.endsWith("/output"), node.type.endsWith("/output"),
) as Node; );
if (!outputNode) { if (!outputNode) {
throw new Error("No output node found"); throw new Error("No output node found");
} }
outputNode.tmp = outputNode.tmp || {}; const nodeMap = new Map(
outputNode.tmp.depth = 0; graphNodes.map((node) => [node.id, node]),
const nodeMap = new Map<number, Node>(
graph.nodes.map((node) => [node.id, node]),
); );
// loop through all edges and assign the parent and child nodes to each node // loop through all edges and assign the parent and child nodes to each node
@@ -112,14 +121,9 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
const parent = nodeMap.get(parentId); const parent = nodeMap.get(parentId);
const child = nodeMap.get(childId); const child = nodeMap.get(childId);
if (parent && child) { if (parent && child) {
parent.tmp = parent.tmp || {}; parent.state.children.push(child);
parent.tmp.children = parent.tmp.children || []; child.state.parents.push(parent);
parent.tmp.children.push(child); child.state.inputNodes[childInput] = parent;
child.tmp = child.tmp || {};
child.tmp.parents = child.tmp.parents || [];
child.tmp.parents.push(parent);
child.tmp.inputNodes = child.tmp.inputNodes || {};
child.tmp.inputNodes[childInput] = parent;
} }
} }
@@ -130,20 +134,10 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
while (stack.length) { while (stack.length) {
const node = stack.pop(); const node = stack.pop();
if (!node) continue; if (!node) continue;
node.tmp = node.tmp || {}; for (const parent of node.state.parents) {
if (node?.tmp?.depth === undefined) { parent.state = parent.state || {};
node.tmp.depth = 0; parent.state.depth = node.state.depth + 1;
} stack.push(parent);
if (node?.tmp?.parents !== undefined) {
for (const parent of node.tmp.parents) {
parent.tmp = parent.tmp || {};
if (parent.tmp?.depth === undefined) {
parent.tmp.depth = node.tmp.depth + 1;
stack.push(parent);
} else {
parent.tmp.depth = Math.max(parent.tmp.depth, node.tmp.depth + 1);
}
}
} }
nodes.push(node); nodes.push(node);
} }
@@ -175,16 +169,20 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
// we execute the nodes from the bottom up // we execute the nodes from the bottom up
const sortedNodes = nodes.sort( const sortedNodes = nodes.sort(
(a, b) => (b.tmp?.depth || 0) - (a.tmp?.depth || 0), (a, b) => (b.state?.depth || 0) - (a.state?.depth || 0),
); );
// here we store the intermediate results of the nodes // here we store the intermediate results of the nodes
const results: Record<string, Int32Array> = {}; const results: Record<string, Int32Array> = {};
if (settings["randomSeed"]) {
this.seed = Math.floor(Math.random() * 100000000);
}
for (const node of sortedNodes) { for (const node of sortedNodes) {
const node_type = this.definitionMap.get(node.type)!; const node_type = this.definitionMap.get(node.type)!;
if (!node_type || !node.tmp || !node_type.execute) { if (!node_type || !node.state || !node_type.execute) {
log.warn(`Node ${node.id} has no definition`); log.warn(`Node ${node.id} has no definition`);
continue; continue;
} }
@@ -195,10 +193,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
const inputs = Object.entries(node_type.inputs || {}).map( const inputs = Object.entries(node_type.inputs || {}).map(
([key, input]) => { ([key, input]) => {
if (input.type === "seed") { if (input.type === "seed") {
if (settings["randomSeed"] === true) { return this.seed;
this.randomSeed = Math.floor(Math.random() * 100000000);
}
return this.randomSeed;
} }
// If the input is linked to a setting, we use that value // If the input is linked to a setting, we use that value
@@ -207,7 +202,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
} }
// check if the input is connected to another node // check if the input is connected to another node
const inputNode = node.tmp?.inputNodes?.[key]; const inputNode = node.state.inputNodes[key];
if (inputNode) { if (inputNode) {
if (results[inputNode.id] === undefined) { if (results[inputNode.id] === undefined) {
throw new Error( throw new Error(
@@ -249,13 +244,14 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
} }
this.perf?.addPoint("cache-hit", 0); this.perf?.addPoint("cache-hit", 0);
log.group(`executing ${node_type.id || node.id}`); log.group(`executing ${node_type.id}-${node.id}`);
log.log(`Inputs:`, inputs); log.log(`Inputs:`, inputs);
a = performance.now(); a = performance.now();
results[node.id] = node_type.execute(encoded_inputs); results[node.id] = node_type.execute(encoded_inputs);
log.log("Executed", node.type, node.id)
b = performance.now(); b = performance.now();
if (this.cache) { if (this.cache && node.id !== outputNode.id) {
this.cache.set(inputHash, results[node.id]); this.cache.set(inputHash, results[node.id]);
} }

View File

@@ -0,0 +1,10 @@
import type { SerializedNode } from "@nodarium/types";
type RuntimeState = {
depth: number
parents: RuntimeNode[],
children: RuntimeNode[],
inputNodes: Record<string, RuntimeNode>
}
export type RuntimeNode = SerializedNode & { state: RuntimeState }

View File

@@ -2,15 +2,33 @@ import { MemoryRuntimeExecutor } from "./runtime-executor";
import { RemoteNodeRegistry, IndexDBCache } from "@nodarium/registry"; import { RemoteNodeRegistry, IndexDBCache } from "@nodarium/registry";
import type { Graph } from "@nodarium/types"; import type { Graph } from "@nodarium/types";
import { createPerformanceStore } from "@nodarium/utils"; import { createPerformanceStore } from "@nodarium/utils";
import { MemoryRuntimeCache } from "./runtime-executor-cache";
const indexDbCache = new IndexDBCache("node-registry"); const indexDbCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry("", indexDbCache); const nodeRegistry = new RemoteNodeRegistry("", indexDbCache);
const executor = new MemoryRuntimeExecutor(nodeRegistry); const cache = new MemoryRuntimeCache()
const executor = new MemoryRuntimeExecutor(nodeRegistry, cache);
const performanceStore = createPerformanceStore(); const performanceStore = createPerformanceStore();
executor.perf = performanceStore; executor.perf = performanceStore;
export async function setUseRegistryCache(useCache: boolean) {
if (useCache) {
nodeRegistry.cache = indexDbCache;
} else {
nodeRegistry.cache = undefined;
}
}
export async function setUseRuntimeCache(useCache: boolean) {
if (useCache) {
executor.cache = cache;
} else {
executor.cache = undefined;
}
}
export async function executeGraph( export async function executeGraph(
graph: Graph, graph: Graph,
settings: Record<string, unknown>, settings: Record<string, unknown>,

View File

@@ -11,5 +11,11 @@ export class WorkerRuntimeExecutor implements RuntimeExecutor {
async getPerformanceData() { async getPerformanceData() {
return this.worker.getPerformanceData(); return this.worker.getPerformanceData();
} }
set useRuntimeCache(useCache: boolean) {
this.worker.setUseRuntimeCache(useCache);
}
set useRegistryCache(useCache: boolean) {
this.worker.setUseRegistryCache(useCache);
}
} }

View File

@@ -54,7 +54,7 @@ export const AppSettingTypes = {
}, },
useWorker: { useWorker: {
type: "boolean", type: "boolean",
label: "Execute runtime in worker", label: "Execute in WebWorker",
value: true, value: true,
}, },
showIndices: { showIndices: {
@@ -87,6 +87,19 @@ export const AppSettingTypes = {
label: "Show Graph Source", label: "Show Graph Source",
value: false, value: false,
}, },
cache: {
title: "Cache",
useRuntimeCache: {
type: "boolean",
label: "Node Results",
value: true,
},
useRegistryCache: {
type: "boolean",
label: "Node Source",
value: true,
},
},
stressTest: { stressTest: {
title: "Stress Test", title: "Stress Test",
amount: { amount: {

View File

@@ -1,48 +1,46 @@
<script lang="ts"> <script lang="ts">
import { getContext } from "svelte"; import { type Snippet } from "svelte";
import type { Readable } from "svelte/store"; import { panelState } from "./PanelState.svelte";
export let id: string; const {
export let icon: string = ""; id,
export let title = ""; icon = "",
export let classes = ""; title = "",
export let hidden: boolean | undefined = undefined; classes = "",
hidden,
children,
} = $props<{
id: string;
icon?: string;
title?: string;
classes?: string;
hidden?: boolean;
children?: Snippet;
}>();
const setVisibility = const panel = panelState.registerPanel(id, icon, classes, hidden);
getContext<(id: string, visible: boolean) => void>("setVisibility"); $effect(() => {
panel.hidden = hidden;
$: if (typeof hidden === "boolean") { });
setVisibility(id, !hidden);
}
const registerPanel =
getContext<
(id: string, icon: string, classes: string) => Readable<boolean>
>("registerPanel");
let visible = registerPanel(id, icon, classes);
</script> </script>
{#if $visible} {#if panelState.activePanel.value === id}
<div class="wrapper" class:hidden> <div class="wrapper" class:hidden>
{#if title} {#if title}
<header> <header>
<h3>{title}</h3> <h3>{title}</h3>
</header> </header>
{/if} {/if}
<slot /> {@render children?.()}
</div> </div>
{/if} {/if}
<style> <style>
header { header {
border-bottom: solid thin var(--outline); border-bottom: solid thin var(--outline);
height: 69px; height: 70px;
display: flex; display: flex;
align-items: center; align-items: center;
padding-left: 1em; padding-left: 1em;
} }
h3 {
margin: 0px;
}
</style> </style>

View File

@@ -0,0 +1,40 @@
import { localState } from '$lib/helpers/localState.svelte';
type Panel = {
icon: string;
classes: string;
hidden?: boolean;
};
class PanelState {
panels = $state<Record<string, Panel>>({});
activePanel = localState<string | boolean>('node.activePanel', '');
get keys() {
return Object.keys(this.panels);
}
public registerPanel(id: string, icon: string, classes: string, hidden: boolean): Panel {
const state = $state({
icon: icon,
classes: classes,
hidden: hidden
});
this.panels[id] = state;
return state;
}
public toggleOpen() {
if (this.activePanel.value) {
this.activePanel.value = false;
} else {
this.activePanel.value = this.keys[0];
}
}
public setActivePanel(panelId: string) {
this.activePanel.value = panelId;
}
}
export const panelState = new PanelState();

View File

@@ -1,77 +1,32 @@
<script lang="ts"> <script lang="ts">
import localStore from "$lib/helpers/localStore"; import { type Snippet } from "svelte";
import { setContext } from "svelte"; import { panelState as state } from "./PanelState.svelte";
import { derived } from "svelte/store";
let panels: Record< const { children } = $props<{ children?: Snippet }>();
string,
{
icon: string;
id: string;
classes: string;
visible?: boolean;
}
> = {};
const activePanel = localStore<keyof typeof panels | false>(
"nodes.settings.activePanel",
false,
);
$: keys = panels
? (Object.keys(panels) as unknown as (keyof typeof panels)[]).filter(
(key) => !!panels[key]?.id,
)
: [];
setContext("setVisibility", (id: string, visible: boolean) => {
panels[id].visible = visible;
panels = { ...panels };
});
setContext("registerPanel", (id: string, icon: string, classes: string) => {
panels[id] = { id, icon, classes };
return derived(activePanel, ($activePanel) => {
return $activePanel === id;
});
});
function setActivePanel(panel: keyof typeof panels | false) {
if (panel === $activePanel) {
$activePanel = false;
} else if (panel) {
$activePanel = panel;
} else {
$activePanel = false;
}
}
</script> </script>
<div class="wrapper" class:visible={$activePanel}> <div class="wrapper" class:visible={state.activePanel.value}>
<div class="tabs"> <div class="tabs">
<button <button aria-label="Close" onclick={() => state.toggleOpen()}>
aria-label="Close" <span class="icon-[tabler--settings]"></span>
on:click={() => { <span class="absolute i-[tabler--chevron-left] w-6 h-6 block"></span>
setActivePanel($activePanel ? false : keys[0]);
}}
>
<span class="absolute i-tabler-chevron-left w-6 h-6 block"></span>
</button> </button>
{#each keys as panel (panels[panel].id)} {#each state.keys as panelId (panelId)}
{#if panels[panel].visible !== false} {#if !state.panels[panelId].hidden}
<button <button
aria-label={panel} aria-label={panelId}
class="tab {panels[panel].classes}" class="tab {state.panels[panelId].classes}"
class:active={panel === $activePanel} class:active={panelId === state.activePanel.value}
on:click={() => setActivePanel(panel)} onclick={() => (state.activePanel.value = panelId)}
> >
<span class={`block w-6 h-6 ${panels[panel].icon}`}></span> <span class={`block w-6 h-6 iconify ${state.panels[panelId].icon}`}
></span>
</button> </button>
{/if} {/if}
{/each} {/each}
</div> </div>
<div class="content"> <div class="content">
<slot /> {@render children?.()}
</div> </div>
</div> </div>

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { Node, NodeInput } from "@nodarium/types"; import type { NodeInstance, NodeInput } from "@nodarium/types";
import NestedSettings from "$lib/settings/NestedSettings.svelte"; import NestedSettings from "$lib/settings/NestedSettings.svelte";
import type { GraphManager } from "$lib/graph-interface/graph-manager.svelte"; import type { GraphManager } from "$lib/graph-interface/graph-manager.svelte";
type Props = { type Props = {
manager: GraphManager; manager: GraphManager;
node: Node; node: NodeInstance;
}; };
const { manager, node = $bindable() }: Props = $props(); const { manager, node = $bindable() }: Props = $props();
@@ -19,19 +19,19 @@
}) })
.map(([key, value]) => { .map(([key, value]) => {
//@ts-ignore //@ts-ignore
value.__node_type = node?.tmp?.type.id; value.__node_type = node.state?.type.id;
//@ts-ignore //@ts-ignore
value.__node_input = key; value.__node_input = key;
return [key, value]; return [key, value];
}), }),
); );
} }
const nodeDefinition = filterInputs(node.tmp?.type?.inputs); const nodeDefinition = filterInputs(node.state?.type?.inputs);
type Store = Record<string, number | number[]>; type Store = Record<string, number | number[]>;
let store = $state<Store>(createStore(node?.props, nodeDefinition)); let store = $state<Store>(createStore(node?.props, nodeDefinition));
function createStore( function createStore(
props: Node["props"], props: NodeInstance["props"],
inputs: Record<string, NodeInput>, inputs: Record<string, NodeInput>,
): Store { ): Store {
const store: Store = {}; const store: Store = {};
@@ -64,6 +64,7 @@
lastPropsHash = propsHash; lastPropsHash = propsHash;
if (needsUpdate) { if (needsUpdate) {
manager.save();
manager.execute(); manager.execute();
} }
} }

View File

@@ -1,24 +1,24 @@
<script lang="ts"> <script lang="ts">
import type { Node } from "@nodarium/types"; import type { NodeInstance } from "@nodarium/types";
import type { GraphManager } from "$lib/graph-interface/graph-manager.svelte"; import type { GraphManager } from "$lib/graph-interface/graph-manager.svelte";
import ActiveNodeSelected from "./ActiveNodeSelected.svelte"; import ActiveNodeSelected from "./ActiveNodeSelected.svelte";
type Props = { type Props = {
manager: GraphManager; manager: GraphManager;
node: Node | undefined; node: NodeInstance | undefined;
}; };
const { manager, node }: Props = $props(); let { manager, node = $bindable() }: Props = $props();
</script> </script>
{#if node} {#if node}
{#key node.id} {#key node.id}
{#if node} {#if node}
<ActiveNodeSelected {manager} {node} /> <ActiveNodeSelected {manager} bind:node />
{:else} {:else}
<p class="mx-4">Active Node has no Settings</p> <p class="mx-4">Active Node has no Settings</p>
{/if} {/if}
{/key} {/key}
{:else} {:else}
<p class="mx-4">No active node</p> <p class="mx-4">No node selected</p>
{/if} {/if}

View File

@@ -1,143 +1,148 @@
<script lang="ts"> <script lang="ts" module>
import localStore from "$lib/helpers/localStore"; let result:
import { Integer } from "@nodarium/ui"; | { stdev: number; avg: number; duration: number; samples: number[] }
import { writable } from "svelte/store"; | undefined = $state();
import { humanizeDuration } from "$lib/helpers";
import Monitor from "$lib/performance/Monitor.svelte";
function calculateStandardDeviation(array: number[]) {
const n = array.length;
const mean = array.reduce((a, b) => a + b) / n;
return Math.sqrt(
array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n,
);
}
export let run: () => Promise<any>;
let isRunning = false;
let amount = localStore<number>("nodes.benchmark.samples", 500);
let samples = 0;
let warmUp = writable(0);
let warmUpAmount = 10;
let state = "";
let result:
| { stdev: number; avg: number; duration: number; samples: number[] }
| undefined;
const copyContent = async (text?: string | number) => {
if (!text) return;
if (typeof text !== "string") {
text = (Math.floor(text * 100) / 100).toString();
}
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error("Failed to copy: ", err);
}
};
async function benchmark() {
if (isRunning) return;
isRunning = true;
result = undefined;
samples = 0;
$warmUp = 0;
await new Promise((r) => setTimeout(r, 50));
// warm up
for (let i = 0; i < warmUpAmount; i++) {
await run();
$warmUp = i + 1;
}
let a = performance.now();
let results = [];
// perform run
for (let i = 0; i < $amount; i++) {
const a = performance.now();
await run();
samples = i;
const b = performance.now();
await new Promise((r) => setTimeout(r, 20));
results.push(b - a);
}
result = {
stdev: calculateStandardDeviation(results),
samples: results,
duration: performance.now() - a,
avg: results.reduce((a, b) => a + b) / results.length,
};
}
</script> </script>
{state} <script lang="ts">
import { humanizeDuration } from '$lib/helpers';
import { localState } from '$lib/helpers/localState.svelte';
import Monitor from '$lib/performance/Monitor.svelte';
import { Number } from '@nodarium/ui';
import { writable } from 'svelte/store';
function calculateStandardDeviation(array: number[]) {
const n = array.length;
const mean = array.reduce((a, b) => a + b) / n;
return Math.sqrt(
array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n
);
}
type Props = {
run: () => Promise<any>;
};
const { run }: Props = $props();
let isRunning = $state(false);
let amount = localState<number>('nodes.benchmark.samples', 500);
let samples = $state(0);
let warmUp = writable(0);
let warmUpAmount = 10;
let status = '';
const copyContent = async (text?: string | number) => {
if (!text) return;
if (typeof text !== 'string') {
text = (Math.floor(text * 100) / 100).toString();
}
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error('Failed to copy: ', err);
}
};
async function benchmark() {
if (isRunning) return;
isRunning = true;
result = undefined;
samples = 0;
$warmUp = 0;
await new Promise((r) => setTimeout(r, 50));
// warm up
for (let i = 0; i < warmUpAmount; i++) {
await run();
$warmUp = i + 1;
}
let a = performance.now();
let results = [];
// perform run
for (let i = 0; i < amount.value; i++) {
const a = performance.now();
await run();
samples = i;
const b = performance.now();
await new Promise((r) => setTimeout(r, 20));
results.push(b - a);
}
result = {
stdev: calculateStandardDeviation(results),
samples: results,
duration: performance.now() - a,
avg: results.reduce((a, b) => a + b) / results.length
};
}
</script>
{status}
<div class="wrapper" class:running={isRunning}> <div class="wrapper" class:running={isRunning}>
{#if isRunning} {#if result}
{#if result} <h3>Finished ({humanizeDuration(result.duration)})</h3>
<h3>Finished ({humanizeDuration(result.duration)})</h3> <div class="monitor-wrapper">
<div class="monitor-wrapper"> <Monitor points={result.samples} />
<Monitor points={result.samples} /> </div>
</div> <label for="bench-avg">Average </label>
<label for="bench-avg">Average </label> <button
<button id="bench-avg"
id="bench-avg" onkeydown={(ev) => ev.key === 'Enter' && copyContent(result?.avg)}
on:keydown={(ev) => ev.key === "Enter" && copyContent(result?.avg)} onclick={() => copyContent(result?.avg)}
on:click={() => copyContent(result?.avg)} >
>{Math.floor(result.avg * 100) / 100}</button {Math.floor(result.avg * 100) / 100}
> </button>
<i <i
role="button" role="button"
tabindex="0" tabindex="0"
on:keydown={(ev) => ev.key === "Enter" && copyContent(result?.avg)} onkeydown={(ev) => ev.key === 'Enter' && copyContent(result?.avg)}
on:click={() => copyContent(result?.avg)}>(click to copy)</i onclick={() => copyContent(result?.avg)}
> >(click to copy)</i>
<label for="bench-stdev">Standard Deviation σ</label> <label for="bench-stdev">Standard Deviation σ</label>
<button id="bench-stdev" on:click={() => copyContent(result?.stdev)} <button id="bench-stdev" onclick={() => copyContent(result?.stdev)}>
>{Math.floor(result.stdev * 100) / 100}</button {Math.floor(result.stdev * 100) / 100}
> </button>
<i <i
role="button" role="button"
tabindex="0" tabindex="0"
on:keydown={(ev) => ev.key === "Enter" && copyContent(result?.avg)} onkeydown={(ev) => ev.key === 'Enter' && copyContent(result?.avg)}
on:click={() => copyContent(result?.stdev + "")}>(click to copy)</i onclick={() => copyContent(result?.stdev + '')}
> >(click to copy)</i>
<div> <div>
<button on:click={() => (isRunning = false)}>reset</button> <button onclick={() => (isRunning = false)}>reset</button>
</div> </div>
{:else} {:else if isRunning}
<p>WarmUp ({$warmUp}/{warmUpAmount})</p> <p>WarmUp ({$warmUp}/{warmUpAmount})</p>
<progress value={$warmUp} max={warmUpAmount} <progress value={$warmUp} max={warmUpAmount}>
>{Math.floor(($warmUp / warmUpAmount) * 100)}%</progress {Math.floor(($warmUp / warmUpAmount) * 100)}%
> </progress>
<p>Progress ({samples}/{$amount})</p> <p>Progress ({samples}/{amount.value})</p>
<progress value={samples} max={$amount} <progress value={samples} max={amount.value}>
>{Math.floor((samples / $amount) * 100)}%</progress {Math.floor((samples / amount.value) * 100)}%
> </progress>
{/if} {:else}
{:else} <label for="bench-samples">Samples</label>
<label for="bench-samples">Samples</label> <Number id="bench-sample" bind:value={amount.value} max={1000} />
<Integer id="bench-sample" bind:value={$amount} max={1000} /> <button onclick={benchmark} disabled={isRunning}>start</button>
<button on:click={benchmark} disabled={isRunning}> start </button> {/if}
{/if}
</div> </div>
<style> <style>
.wrapper { .wrapper {
padding: 1em; padding: 1em;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1em; gap: 1em;
} }
.monitor-wrapper { .monitor-wrapper {
border: solid thin var(--outline); border: solid thin var(--outline);
border-bottom: none; border-bottom: none;
} }
i { i {
opacity: 0.5; opacity: 0.5;
font-size: 0.8em; font-size: 0.8em;
} }
</style> </style>

View File

@@ -15,7 +15,7 @@
FileSaver.saveAs(blob, name + "." + extension); FileSaver.saveAs(blob, name + "." + extension);
}; };
export let scene: Group; const { scene } = $props<{ scene: Group }>();
let gltfExporter: GLTFExporter; let gltfExporter: GLTFExporter;
async function exportGltf() { async function exportGltf() {
@@ -53,7 +53,7 @@
} }
</script> </script>
<div class="p-2"> <div class="p-4">
<button on:click={exportObj}> export obj </button> <button onclick={exportObj}> export obj </button>
<button on:click={exportGltf}> export gltf </button> <button onclick={exportGltf}> export gltf </button>
</div> </div>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { Graph } from "$lib/types"; import type { Graph } from "$lib/types";
const { graph }: { graph: Graph } = $props(); const { graph }: { graph?: Graph } = $props();
function convert(g: Graph): string { function convert(g: Graph): string {
return JSON.stringify( return JSON.stringify(
@@ -16,5 +16,5 @@
</script> </script>
<pre> <pre>
{convert(graph)} {graph ? convert(graph) : 'No graph loaded'}
</pre> </pre>

View File

@@ -13,32 +13,34 @@
let { keymaps }: Props = $props(); let { keymaps }: Props = $props();
</script> </script>
<table class="wrapper"> <div class="p-4">
<tbody> <table class="wrapper">
{#each keymaps as keymap} <tbody>
<tr> {#each keymaps as keymap}
<td colspan="2">
<h3>{keymap.title}</h3>
</td>
</tr>
{#each get(keymap.keymap?.keys) as key}
<tr> <tr>
{#if key.description} <td colspan="2">
<td class="command-wrapper"> <h3>{keymap.title}</h3>
<ShortCut </td>
alt={key.alt}
ctrl={key.ctrl}
shift={key.shift}
key={key.key}
/>
</td>
<td>{key.description}</td>
{/if}
</tr> </tr>
{#each get(keymap.keymap?.keys) as key}
<tr>
{#if key.description}
<td class="command-wrapper">
<ShortCut
alt={key.alt}
ctrl={key.ctrl}
shift={key.shift}
key={key.key}
/>
</td>
<td>{key.description}</td>
{/if}
</tr>
{/each}
{/each} {/each}
{/each} </tbody>
</tbody> </table>
</table> </div>
<style> <style>
.wrapper { .wrapper {

View File

@@ -1,7 +1,15 @@
<script lang="ts"> <script lang="ts">
import "@nodarium/ui/app.css"; import "@nodarium/ui/app.css";
import "virtual:uno.css"; import "../app.css";
import "@unocss/reset/normalize.css"; import type { Snippet } from "svelte";
import * as config from "$lib/config";
const { children } = $props<{ children?: Snippet }>();
</script> </script>
<slot /> {@render children?.()}
<svelte:head>
{#if config.ANALYTIC_SCRIPT}
{@html config.ANALYTIC_SCRIPT}
{/if}
</svelte:head>

View File

@@ -2,7 +2,7 @@
import Grid from "$lib/grid"; import Grid from "$lib/grid";
import GraphInterface from "$lib/graph-interface"; import GraphInterface from "$lib/graph-interface";
import * as templates from "$lib/graph-templates"; import * as templates from "$lib/graph-templates";
import type { Graph, Node } from "@nodarium/types"; import type { Graph, NodeInstance } from "@nodarium/types";
import Viewer from "$lib/result-viewer/Viewer.svelte"; import Viewer from "$lib/result-viewer/Viewer.svelte";
import { import {
appSettings, appSettings,
@@ -28,6 +28,8 @@
import BenchmarkPanel from "$lib/sidebar/panels/BenchmarkPanel.svelte"; import BenchmarkPanel from "$lib/sidebar/panels/BenchmarkPanel.svelte";
import { debounceAsyncFunction } from "$lib/helpers"; import { debounceAsyncFunction } from "$lib/helpers";
import GraphSource from "$lib/sidebar/panels/GraphSource.svelte"; import GraphSource from "$lib/sidebar/panels/GraphSource.svelte";
import { ProjectManager } from "$lib/project-manager/project-manager.svelte";
import ProjectManagerEl from "$lib/project-manager/ProjectManager.svelte";
let performanceStore = createPerformanceStore(); let performanceStore = createPerformanceStore();
@@ -37,22 +39,33 @@
const runtimeCache = new MemoryRuntimeCache(); const runtimeCache = new MemoryRuntimeCache();
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache); const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
memoryRuntime.perf = performanceStore; memoryRuntime.perf = performanceStore;
const pm = new ProjectManager();
const runtime = $derived( const runtime = $derived(
appSettings.value.debug.useWorker ? workerRuntime : memoryRuntime, appSettings.value.debug.useWorker ? workerRuntime : memoryRuntime,
); );
let activeNode = $state<Node | undefined>(undefined); $effect(() => {
let scene = $state<Group>(null!); workerRuntime.useRegistryCache =
appSettings.value.debug.cache.useRuntimeCache;
workerRuntime.useRuntimeCache =
appSettings.value.debug.cache.useRegistryCache;
let graph = $state( if (appSettings.value.debug.cache.useRegistryCache) {
localStorage.getItem("graph") nodeRegistry.cache = registryCache;
? JSON.parse(localStorage.getItem("graph")!) } else {
: templates.defaultPlant, nodeRegistry.cache = undefined;
); }
function handleSave(graph: Graph) {
localStorage.setItem("graph", JSON.stringify(graph)); if (appSettings.value.debug.cache.useRuntimeCache) {
} memoryRuntime.cache = runtimeCache;
} else {
memoryRuntime.cache = undefined;
}
});
let activeNode = $state<NodeInstance | undefined>(undefined);
let scene = $state<Group>(null!);
let graphInterface = $state<ReturnType<typeof GraphInterface>>(null!); let graphInterface = $state<ReturnType<typeof GraphInterface>>(null!);
let viewerComponent = $state<ReturnType<typeof Viewer>>(); let viewerComponent = $state<ReturnType<typeof Viewer>>();
@@ -69,29 +82,24 @@
{ {
key: "r", key: "r",
description: "Regenerate the plant model", description: "Regenerate the plant model",
callback: randomGenerate, callback: () => randomGenerate(),
}, },
]); ]);
let graphSettings = $state<Record<string, any>>({}); let graphSettings = $state<Record<string, any>>({});
let graphSettingTypes = $state({
randomSeed: { type: "boolean", value: false },
});
$effect(() => { $effect(() => {
if (graphSettings) { if (graphSettings && graphSettingTypes) {
manager?.setSettings($state.snapshot(graphSettings)); manager?.setSettings($state.snapshot(graphSettings));
} }
}); });
type BooleanSchema = {
[key: string]: {
type: "boolean";
value: false;
};
};
let graphSettingTypes = $state<BooleanSchema>({
randomSeed: { type: "boolean", value: false },
});
let runIndex = 0; async function update(
g: Graph,
async function update(g: Graph, s: Record<string, any> = graphSettings) { s: Record<string, any> = $state.snapshot(graphSettings),
runIndex++; ) {
performanceStore.startRun(); performanceStore.startRun();
try { try {
let a = performance.now(); let a = performance.now();
@@ -164,21 +172,23 @@
/> />
</Grid.Cell> </Grid.Cell>
<Grid.Cell> <Grid.Cell>
<GraphInterface {#if pm.graph}
{graph} <GraphInterface
bind:this={graphInterface} graph={pm.graph}
registry={nodeRegistry} bind:this={graphInterface}
showGrid={appSettings.value.nodeInterface.showNodeGrid} registry={nodeRegistry}
snapToGrid={appSettings.value.nodeInterface.snapToGrid} showGrid={appSettings.value.nodeInterface.showNodeGrid}
bind:activeNode snapToGrid={appSettings.value.nodeInterface.snapToGrid}
bind:showHelp={appSettings.value.nodeInterface.showHelp} bind:activeNode
bind:settings={graphSettings} bind:showHelp={appSettings.value.nodeInterface.showHelp}
bind:settingTypes={graphSettingTypes} bind:settings={graphSettings}
onresult={(result) => handleUpdate(result)} bind:settingTypes={graphSettingTypes}
onsave={(graph) => handleSave(graph)} onsave={(g) => pm.saveGraph(g)}
/> onresult={(result) => handleUpdate(result)}
/>
{/if}
<Sidebar> <Sidebar>
<Panel id="general" title="General" icon="i-tabler-settings"> <Panel id="general" title="General" icon="i-[tabler--settings]">
<NestedSettings <NestedSettings
id="general" id="general"
bind:value={appSettings.value} bind:value={appSettings.value}
@@ -188,7 +198,7 @@
<Panel <Panel
id="shortcuts" id="shortcuts"
title="Keyboard Shortcuts" title="Keyboard Shortcuts"
icon="i-tabler-keyboard" icon="i-[tabler--keyboard]"
> >
<Keymap <Keymap
keymaps={[ keymaps={[
@@ -197,50 +207,49 @@
]} ]}
/> />
</Panel> </Panel>
<Panel id="exports" title="Exporter" icon="i-tabler-package-export"> <Panel id="exports" title="Exporter" icon="i-[tabler--package-export]">
<ExportSettings {scene} /> <ExportSettings {scene} />
</Panel> </Panel>
<Panel <Panel
id="node-store" id="node-store"
classes="text-green-400"
title="Node Store" title="Node Store"
icon="i-tabler-database" icon="i-[tabler--database] bg-green-400"
> >
<NodeStore registry={nodeRegistry} /> <NodeStore registry={nodeRegistry} />
</Panel> </Panel>
<Panel <Panel
id="performance" id="performance"
title="Performance" title="Performance"
classes="text-red-400"
hidden={!appSettings.value.debug.showPerformancePanel} hidden={!appSettings.value.debug.showPerformancePanel}
icon="i-tabler-brand-speedtest" icon="i-[tabler--brand-speedtest] bg-red-400"
> >
{#if $performanceStore} {#if $performanceStore}
<PerformanceViewer data={$performanceStore} /> <PerformanceViewer data={$performanceStore} />
{/if} {/if}
</Panel> </Panel>
<Panel id="projects" icon="i-[tabler--folder-open]">
<ProjectManagerEl projectManager={pm} />
</Panel>
<Panel <Panel
id="graph-source" id="graph-source"
title="Graph Source" title="Graph Source"
hidden={!appSettings.value.debug.showGraphJson} hidden={!appSettings.value.debug.showGraphJson}
icon="i-tabler-code" icon="i-[tabler--code]"
> >
<GraphSource {graph} /> <GraphSource graph={pm.graph ?? manager?.serialize()} />
</Panel> </Panel>
<Panel <Panel
id="benchmark" id="benchmark"
title="Benchmark" title="Benchmark"
classes="text-red-400"
hidden={!appSettings.value.debug.showBenchmarkPanel} hidden={!appSettings.value.debug.showBenchmarkPanel}
icon="i-tabler-graph" icon="i-[tabler--graph] bg-red-400"
> >
<BenchmarkPanel run={randomGenerate} /> <BenchmarkPanel run={randomGenerate} />
</Panel> </Panel>
<Panel <Panel
id="graph-settings" id="graph-settings"
title="Graph Settings" title="Graph Settings"
classes="text-blue-400" icon="i-[custom--graph] bg-blue-400"
icon="i-custom-graph"
> >
<NestedSettings <NestedSettings
id="graph-settings" id="graph-settings"
@@ -251,10 +260,9 @@
<Panel <Panel
id="active-node" id="active-node"
title="Node Settings" title="Node Settings"
classes="text-blue-400" icon="i-[tabler--adjustments] bg-blue-400"
icon="i-tabler-adjustments"
> >
<ActiveNodeSettings {manager} node={activeNode} /> <ActiveNodeSettings {manager} bind:node={activeNode} />
</Panel> </Panel>
</Sidebar> </Sidebar>
</Grid.Cell> </Grid.Cell>

View File

@@ -0,0 +1,8 @@
<script lang="ts">
import type { Snippet } from "svelte";
const { children } = $props<{ children?: Snippet }>();
</script>
<main class="w-screen overflow-x-hidden">
{@render children()}
</main>

View File

@@ -0,0 +1,119 @@
<script lang="ts">
import NodeHTML from "$lib/graph-interface/node/NodeHTML.svelte";
import { localState } from "$lib/helpers/localState.svelte";
import Panel from "$lib/sidebar/Panel.svelte";
import Sidebar from "$lib/sidebar/Sidebar.svelte";
import { IndexDBCache, RemoteNodeRegistry } from "@nodarium/registry";
import { type NodeId, type NodeInstance } from "@nodarium/types";
import Code from "./Code.svelte";
import Grid from "$lib/grid";
import {
concatEncodedArrays,
createWasmWrapper,
encodeNestedArray,
} from "@nodarium/utils";
const registryCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry("", registryCache);
let activeNode = localState<NodeId | undefined>(
"node.dev.activeNode",
undefined,
);
let nodeWasm = $state<ArrayBuffer>();
let nodeInstance = $state<NodeInstance>();
let nodeWasmWrapper = $state<ReturnType<typeof createWasmWrapper>>();
async function fetchNodeData(nodeId?: NodeId) {
nodeWasm = undefined;
nodeInstance = undefined;
if (!nodeId) return;
const data = await nodeRegistry.fetchNodeDefinition(nodeId);
nodeWasm = await nodeRegistry.fetchArrayBuffer("nodes/" + nodeId + ".wasm");
nodeInstance = {
id: 0,
type: nodeId,
position: [0, 0] as [number, number],
props: {},
state: {
type: data,
},
};
nodeWasmWrapper = createWasmWrapper(nodeWasm);
}
$effect(() => {
fetchNodeData(activeNode.value);
});
$effect(() => {
if (nodeInstance?.props && nodeWasmWrapper) {
const keys = Object.keys(nodeInstance.state.type?.inputs || {});
let ins = Object.values(nodeInstance.props) as number[];
if (keys[0] === "plant") {
ins = [[0, 0, 0, 0, 0, 0, 0, 0], ...ins];
}
const inputs = concatEncodedArrays(encodeNestedArray(ins));
nodeWasmWrapper?.execute(inputs);
}
});
</script>
<div class="node-wrapper absolute bottom-8 left-8">
{#if nodeInstance}
<NodeHTML inView position="relative" z={5} bind:node={nodeInstance} />
{/if}
</div>
<Grid.Row>
<Grid.Cell>
<pre>
<code>
{JSON.stringify(nodeInstance?.props)}
</code>
</pre>
</Grid.Cell>
<Grid.Cell>
<div class="h-screen w-[80vw] overflow-y-auto">
{#if nodeWasm}
<Code wasm={nodeWasm} />
{/if}
</div>
</Grid.Cell>
</Grid.Row>
<Sidebar>
<Panel
id="node-store"
classes="text-green-400"
title="Node Store"
icon="i-[tabler--database]"
>
<div class="p-4 flex flex-col gap-2">
{#await nodeRegistry.fetchCollection("max/plantarium")}
<p>Loading Nodes...</p>
{:then result}
{#each result.nodes as n}
<button
class="cursor-pointer p-2 bg-layer-1 {activeNode.value === n.id
? 'outline outline-offset-1'
: ''}"
onclick={() => (activeNode.value = n.id)}>{n.id}</button
>
{/each}
{/await}
</div>
</Panel>
</Sidebar>
<style>
:global body {
height: 100vh;
width: 100vw;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import wabtInit from "wabt";
const { wasm } = $props<{ wasm: ArrayBuffer }>();
async function toWat(arrayBuffer: ArrayBuffer) {
const wabt = await wabtInit();
const module = wabt.readWasm(new Uint8Array(arrayBuffer), {
readDebugNames: true,
});
module.generateNames();
module.applyNames();
return module.toText({ foldExprs: false, inlineExport: false });
}
</script>
{#await toWat(wasm)}
<p>Converting to WAT</p>
{:then c}
<pre>
<code class="text-gray-50">{c}</code>
</pre>
{/await}

1
app/static/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
nodes/

View File

@@ -1,20 +0,0 @@
// uno.config.ts
import { defineConfig } from 'unocss'
import presetIcons from '@unocss/preset-icons'
import { presetUno } from 'unocss'
import fs from 'fs'
const icons = Object.fromEntries(fs.readdirSync('./src/lib/icons')
.map(name => [name.replace(".svg", ""), fs.readFileSync(`./src/lib/icons/${name}`, 'utf-8')]))
export default defineConfig({
presets: [
presetUno(),
presetIcons({
collections: {
custom: icons
}
}),
]
})

View File

@@ -1,14 +1,14 @@
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite';
import UnoCSS from 'unocss/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite'
import comlink from 'vite-plugin-comlink'; import comlink from 'vite-plugin-comlink';
import glsl from "vite-plugin-glsl"; import glsl from "vite-plugin-glsl";
import wasm from "vite-plugin-wasm"; import wasm from "vite-plugin-wasm";
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
tailwindcss(),
comlink(), comlink(),
UnoCSS(),
sveltekit(), sveltekit(),
glsl(), glsl(),
wasm() wasm()

View File

@@ -4,7 +4,7 @@ This guide will help you developing your first Nodarium Node written in Rust. As
## Prerequesites ## Prerequesites
You need to have [Rust](https://www.rust-lang.org/tools/install) and [wasm-pack](https://rustwasm.github.io/wasm-pack/book/) installed. Rust is the language we are going to develop our node in and wasm-pack helps us compile our rust code into a webassembly file. You need to have [Rust](https://www.rust-lang.org/tools/install) and [wasm-pack](https://rustwasm.github.io/docs/wasm-pack/) installed. Rust is the language we are going to develop our node in and wasm-pack helps us compile our rust code into a webassembly file.
```bash ```bash
# install rust # install rust
@@ -26,6 +26,7 @@ Now we create the definition file of the node.
Here we define what kind of inputs our node will expect and what kind of output it produces. If you want to dive deeper into this topic, have a look at [NODE_DEFINITION.md](./NODE_DEFINITION.md). Here we define what kind of inputs our node will expect and what kind of output it produces. If you want to dive deeper into this topic, have a look at [NODE_DEFINITION.md](./NODE_DEFINITION.md).
`src/definition.json` `src/definition.json`
```json ```json
{ {
"id": "my-name/my-namespace/zylinder-node", "id": "my-name/my-namespace/zylinder-node",
@@ -35,7 +36,7 @@ Here we define what kind of inputs our node will expect and what kind of output
"inputs": { "inputs": {
"height": { "height": {
"type": "float", "type": "float",
"value": 2, "value": 2
}, },
"radius": { "radius": {
"type": "float", "type": "float",
@@ -44,6 +45,7 @@ Here we define what kind of inputs our node will expect and what kind of output
} }
} }
``` ```
If we take a look at the `src/lib.rs` file we see that `src/definition.json` is included with the following line: If we take a look at the `src/lib.rs` file we see that `src/definition.json` is included with the following line:
```rust ```rust

27
flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1768564909,
"narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

46
flake.nix Normal file
View File

@@ -0,0 +1,46 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
};
outputs = {nixpkgs, ...}: let
systems = ["aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux"];
eachSystem = function:
nixpkgs.lib.genAttrs systems (system:
function {
inherit system;
pkgs = nixpkgs.legacyPackages.${system};
});
in {
devShells = eachSystem ({pkgs, ...}: {
default = pkgs.mkShellNoCC {
packages = [
# general deps
pkgs.nodejs_24
pkgs.pnpm_10
# wasm stuff
pkgs.rustc
pkgs.cargo
pkgs.rust-analyzer
pkgs.rustfmt
pkgs.binaryen
pkgs.lld
pkgs.zig
pkgs.zls
# frontend
pkgs.vscode-langservers-extracted
pkgs.typescript-language-server
pkgs.prettier
pkgs.tailwindcss-language-server
pkgs.svelte-language-server
];
shellHook = ''
unset ZIG_GLOBAL_CACHE_DIR
'';
};
});
};
}

View File

@@ -11,18 +11,8 @@ crate-type = ["cdylib", "rlib"]
default = ["console_error_panic_hook"] default = ["console_error_panic_hook"]
[dependencies] [dependencies]
wasm-bindgen = "0.2.84"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
utils = { version = "0.1.0", path = "../../../../packages/utils" } utils = { version = "0.1.0", path = "../../../../packages/utils" }
macros = { version = "0.1.0", path = "../../../../packages/macros" } macros = { version = "0.1.0", path = "../../../../packages/macros" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.4"
console_error_panic_hook = { version = "0.1.7", optional = true } console_error_panic_hook = { version = "0.1.7", optional = true }
web-sys = { version = "0.3.69", features = ["console"] } web-sys = { version = "0.3.69", features = ["console"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.34"

View File

@@ -1,6 +0,0 @@
{
"scripts": {
"build": "wasm-pack build --release --out-name index --no-default-features",
"dev": "cargo watch -s 'wasm-pack build --dev --out-name index --no-default-features'"
}
}

View File

@@ -1,13 +0,0 @@
//! Test suite for the Web and headless browsers.
#![cfg(target_arch = "wasm32")]
extern crate wasm_bindgen_test;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn pass() {
assert_eq!(1 + 1, 2);
}

View File

@@ -7,22 +7,6 @@ edition = "2018"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies] [dependencies]
wasm-bindgen = "0.2.84"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" } nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
serde = { version = "1.0", features = ["derive"] } nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
serde-wasm-bindgen = "0.4"
console_error_panic_hook = { version = "0.1.7", optional = true }
web-sys = { version = "0.3.69", features = ["console"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.34"

View File

@@ -1,6 +0,0 @@
{
"scripts": {
"build": "wasm-pack build --release --out-name index --no-default-features",
"dev": "cargo watch -s 'wasm-pack build --dev --out-name index --no-default-features'"
}
}

View File

@@ -1,18 +1,15 @@
use nodarium_macros::include_definition_file; use nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute;
use nodarium_utils::{ use nodarium_utils::{
concat_args, encode_float, evaluate_float, geometry::calculate_normals, log, set_panic_hook, encode_float, evaluate_float, geometry::calculate_normals,log,
split_args, wrap_arg, split_args, wrap_arg,
}; };
use wasm_bindgen::prelude::*;
include_definition_file!("src/input.json"); nodarium_definition_file!("src/input.json");
#[rustfmt::skip] #[nodarium_execute]
#[wasm_bindgen]
pub fn execute(input: &[i32]) -> Vec<i32> { pub fn execute(input: &[i32]) -> Vec<i32> {
set_panic_hook();
let args = split_args(input); let args = split_args(input);
log!("WASM(cube): input: {:?} -> {:?}", input, args); log!("WASM(cube): input: {:?} -> {:?}", input, args);
@@ -22,7 +19,6 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
let p = encode_float(size); let p = encode_float(size);
let n = encode_float(-size); let n = encode_float(-size);
// [[1,3, x, y, z, x, y,z,x,y,z]]; // [[1,3, x, y, z, x, y,z,x,y,z]];
let mut cube_geometry = [ let mut cube_geometry = [

View File

@@ -1,13 +0,0 @@
//! Test suite for the Web and headless browsers.
#![cfg(target_arch = "wasm32")]
extern crate wasm_bindgen_test;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn pass() {
assert_eq!(1 + 1, 2);
}

View File

@@ -7,23 +7,6 @@ edition = "2018"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies] [dependencies]
wasm-bindgen = "0.2.84"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" } nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" } nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.4"
console_error_panic_hook = { version = "0.1.7", optional = true }
web-sys = { version = "0.3.69", features = ["console"] }
glam = "0.27.0"
[dev-dependencies]
wasm-bindgen-test = "0.3.34"

View File

@@ -1,6 +0,0 @@
{
"scripts": {
"build": "wasm-pack build --release --out-name index --no-default-features",
"dev": "cargo watch -s 'wasm-pack build --dev --out-name index --no-default-features'"
}
}

View File

@@ -1,20 +1,19 @@
use nodarium_macros::include_definition_file; use nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute;
use nodarium_utils::{ use nodarium_utils::{
concat_arg_vecs, evaluate_float, evaluate_int, concat_arg_vecs, evaluate_float, evaluate_int,
geometry::{ geometry::{
create_path, interpolate_along_path, rotate_vector_by_angle, wrap_path, wrap_path_mut, create_path, interpolate_along_path, rotate_vector_by_angle, wrap_path, wrap_path_mut,
}, },
log, set_panic_hook, split_args, log, split_args,
}; };
use std::f32::consts::PI; use std::f32::consts::PI;
use wasm_bindgen::prelude::*;
include_definition_file!("src/input.json"); nodarium_definition_file!("src/input.json");
#[wasm_bindgen] #[nodarium_execute]
pub fn execute(input: &[i32]) -> Vec<i32> { pub fn execute(input: &[i32]) -> Vec<i32> {
set_panic_hook();
let args = split_args(input); let args = split_args(input);
let paths = split_args(args[0]); let paths = split_args(args[0]);

View File

@@ -1,13 +0,0 @@
//! Test suite for the Web and headless browsers.
#![cfg(target_arch = "wasm32")]
extern crate wasm_bindgen_test;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn pass() {
assert_eq!(1 + 1, 2);
}

View File

@@ -7,21 +7,6 @@ edition = "2018"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies] [dependencies]
wasm-bindgen = "0.2.84"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" } nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" } nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.4"
console_error_panic_hook = { version = "0.1.7", optional = true }
[dev-dependencies]
wasm-bindgen-test = "0.3.34"

View File

@@ -1,6 +0,0 @@
{
"scripts": {
"build": "wasm-pack build --release --out-name index --no-default-features",
"dev": "cargo watch -s 'wasm-pack build --dev --out-name index --no-default-features'"
}
}

View File

@@ -1,9 +1,9 @@
use nodarium_macros::include_definition_file; use nodarium_macros::nodarium_definition_file;
use wasm_bindgen::prelude::*; use nodarium_macros::nodarium_execute;
include_definition_file!("src/input.json"); nodarium_definition_file!("src/input.json");
#[wasm_bindgen] #[nodarium_execute]
pub fn execute(args: &[i32]) -> Vec<i32> { pub fn execute(args: &[i32]) -> Vec<i32> {
args.into() args.into()
} }

View File

@@ -1,13 +0,0 @@
//! Test suite for the Web and headless browsers.
#![cfg(target_arch = "wasm32")]
extern crate wasm_bindgen_test;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn pass() {
assert_eq!(1 + 1, 2);
}

View File

@@ -7,24 +7,7 @@ edition = "2018"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies] [dependencies]
wasm-bindgen = "0.2.84"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" } nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" } nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
serde = { version = "1.0", features = ["derive"] } glam = "0.30.10"
serde-wasm-bindgen = "0.4"
console_error_panic_hook = { version = "0.1.7", optional = true }
web-sys = { version = "0.3.69", features = ["console"] }
noise = "0.9.0"
glam = "0.27.0"
[dev-dependencies]
wasm-bindgen-test = "0.3.34"

View File

@@ -1,6 +0,0 @@
{
"scripts": {
"build": "wasm-pack build --release --out-name index --no-default-features",
"dev": "cargo watch -s 'wasm-pack build --dev --out-name index --no-default-features'"
}
}

Some files were not shown because too many files have changed in this diff Show More