89 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
Max Richter
6229becfd8 fix: display add menu at correct position
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m58s
2025-12-01 22:39:43 +01:00
Max Richter
af944cefaa chore: disable cache from runtime executor 2025-12-01 22:39:06 +01:00
Max Richter
a1ea56093c fix: correctly handle node wrapper resizing
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m57s
2025-12-01 19:48:40 +01:00
Max Richter
1850e21810 fix: make clipboard work 2025-12-01 19:30:44 +01:00
Max Richter
7e51cc5ea1 chore: some updates
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m58s
2025-12-01 18:29:47 +01:00
Max Richter
1ea544e765 chore: rename @nodes -> @nodarium for everything
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 3m33s
2025-12-01 17:03:14 +01:00
e5658b8a7e feat: initial auto connect nodes
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m35s
2025-11-26 17:27:32 +01:00
d3a9b3f056 fix: make node wasm loading work
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m32s
2025-11-26 12:10:25 +01:00
249 changed files with 8446 additions and 10892 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
/target
.direnv/

367
Cargo.lock generated
View File

@@ -1,176 +1,80 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "autocfg"
version = "1.2.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "box"
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 = "branch"
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]]
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]]
name = "float"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"nodarium_macros",
"nodarium_utils",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
]
[[package]]
name = "glam"
version = "0.27.0"
version = "0.30.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9"
checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9"
[[package]]
name = "gravity"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"glam",
"nodarium_macros",
"nodarium_utils",
"noise",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
]
[[package]]
name = "instance"
version = "0.1.0"
dependencies = [
"glam",
"nodarium_macros",
"nodarium_utils",
]
[[package]]
name = "itoa"
version = "1.0.11"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[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"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "math"
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 = "max-plantarium-triangle"
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 = "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",
]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "nodarium_macros"
@@ -180,7 +84,7 @@ dependencies = [
"quote",
"serde",
"serde_json",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -195,29 +99,19 @@ dependencies = [
name = "nodarium_utils"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"glam",
"noise",
"noise 0.9.0",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "nodes-noise"
name = "noise"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"glam",
"nodarium_macros",
"nodarium_utils",
"noise",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
"noise 0.9.0",
]
[[package]]
@@ -233,48 +127,35 @@ dependencies = [
[[package]]
name = "num-traits"
version = "0.2.18"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "output"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"glam",
"nodarium_macros",
"nodarium_utils",
"serde",
"serde_json",
"wasm-bindgen",
"wasm-bindgen-test",
"web-sys",
]
[[package]]
name = "proc-macro2"
version = "1.0.81"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [
"proc-macro2",
]
@@ -307,103 +188,76 @@ dependencies = [
name = "random"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"nodarium_macros",
"nodarium_utils",
"serde",
"serde-wasm-bindgen",
"wasm-bindgen",
"wasm-bindgen-test",
]
[[package]]
name = "rotate"
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]]
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]]
name = "serde"
version = "1.0.198"
version = "1.0.228"
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 = [
"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]]
name = "serde_derive"
version = "1.0.198"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.60",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.116"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"ryu",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "stem"
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 = "syn"
version = "1.0.109"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
@@ -411,119 +265,30 @@ dependencies = [
]
[[package]]
name = "syn"
version = "2.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
name = "triangle"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
"nodarium_macros",
"nodarium_utils",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "wasm-bindgen"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
name = "vec3"
version = "0.1.0"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
"nodarium_macros",
"nodarium_utils",
"serde",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.92"
name = "zmij"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
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",
]
checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2"

View File

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

View File

@@ -2,10 +2,6 @@ FROM node:24-alpine
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 \
CARGO_HOME=/usr/local/cargo \
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 \
&& sh /tmp/rustup-init.sh -y --no-modify-path --profile minimal \
&& rm /tmp/rustup-init.sh \
&& rustup default stable \
&& rustup target add wasm32-unknown-unknown \
&& cargo install wasm-pack \
&& 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
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).
# Nodarium App

View File

@@ -1,5 +1,5 @@
{
"name": "@nodes/app",
"name": "@nodarium/app",
"private": true,
"version": "0.0.0",
"type": "module",
@@ -10,37 +10,38 @@
"preview": "vite preview"
},
"dependencies": {
"@nodes/registry": "link:../packages/registry",
"@nodes/ui": "link:../packages/ui",
"@nodes/utils": "link:../packages/utils",
"@sveltejs/kit": "^2.49.0",
"@threlte/core": "8.3.0",
"@threlte/extras": "9.7.0",
"@types/three": "^0.181.0",
"@unocss/reset": "^66.5.9",
"@nodarium/registry": "workspace:*",
"@nodarium/ui": "workspace:*",
"@nodarium/utils": "workspace:*",
"@sveltejs/kit": "^2.50.0",
"@tailwindcss/vite": "^4.1.18",
"@threlte/core": "8.3.1",
"@threlte/extras": "9.7.1",
"comlink": "^4.4.2",
"file-saver": "^2.0.5",
"idb": "^8.0.3",
"jsondiffpatch": "^0.7.3",
"three": "^0.181.2"
"tailwindcss": "^4.1.18",
"three": "^0.182.0",
"wabt": "^1.0.39"
},
"devDependencies": {
"@iconify-json/tabler": "^1.2.23",
"@nodes/types": "link:../packages/types",
"@iconify-json/tabler": "^1.2.26",
"@iconify/tailwind4": "^1.2.1",
"@nodarium/types": "workspace:",
"@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",
"@types/file-saver": "^2.0.7",
"@unocss/preset-icons": "^66.5.9",
"svelte": "^5.43.14",
"svelte-check": "^4.3.4",
"@types/three": "^0.182.0",
"svelte": "^5.46.4",
"svelte-check": "^4.3.5",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"unocss": "^66.5.9",
"vite": "^7.2.4",
"vite": "^7.3.1",
"vite-plugin-comlink": "^5.3.0",
"vite-plugin-glsl": "^1.5.4",
"vite-plugin-glsl": "^1.5.5",
"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" />
<link rel="icon" href="%sveltekit.assets%/svelte.svg" />
<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%
<title>Nodes</title>
<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,31 +1,36 @@
<script lang="ts">
import type { GraphManager } from "./graph-manager.svelte";
import { HTML } from "@threlte/extras";
import { onMount } from "svelte";
import type { NodeType } from "@nodes/types";
import type { NodeId, NodeInstance } from '@nodarium/types';
import { HTML } from '@threlte/extras';
import { onMount } from 'svelte';
import { getGraphManager, getGraphState } from '../graph-state.svelte';
type Props = {
position: [x: number, y: number] | null;
graph: GraphManager;
onnode: (n: NodeInstance) => void;
};
let { position = $bindable(), graph }: Props = $props();
const { onnode }: Props = $props();
const graph = getGraphManager();
const graphState = getGraphState();
let input: HTMLInputElement;
let wrapper: HTMLDivElement;
let value = $state<string>();
let activeNodeId = $state<NodeType>();
let activeNodeId = $state<NodeId>();
const allNodes = graph.getNodeDefinitions();
const allNodes = graphState.activeSocket
? graph.getPossibleNodes(graphState.activeSocket)
: graph.getNodeDefinitions();
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(() => {
if (nodes) {
if (activeNodeId === undefined) {
activeNodeId = nodes[0].id;
activeNodeId = nodes?.[0]?.id;
} else if (nodes.length) {
const node = nodes.find((node) => node.id === activeNodeId);
if (!node) {
@@ -35,30 +40,40 @@
}
});
function handleNodeCreation(nodeType: NodeInstance['type']) {
if (!graphState.addMenuPosition) return;
onnode?.({
id: -1,
type: nodeType,
position: [...graphState.addMenuPosition],
props: {},
state: {}
});
}
function handleKeyDown(event: KeyboardEvent) {
event.stopImmediatePropagation();
if (event.key === "Escape") {
position = null;
if (event.key === 'Escape') {
graphState.addMenuPosition = null;
return;
}
if (event.key === "ArrowDown") {
if (event.key === 'ArrowDown') {
const index = nodes.findIndex((node) => node.id === activeNodeId);
activeNodeId = nodes[(index + 1) % nodes.length].id;
return;
}
if (event.key === "ArrowUp") {
if (event.key === 'ArrowUp') {
const index = nodes.findIndex((node) => node.id === activeNodeId);
activeNodeId = nodes[(index - 1 + nodes.length) % nodes.length].id;
return;
}
if (event.key === "Enter") {
if (activeNodeId && position) {
graph.createNode({ type: activeNodeId, position, props: {} });
position = null;
if (event.key === 'Enter') {
if (activeNodeId && graphState.addMenuPosition) {
handleNodeCreation(activeNodeId);
}
return;
}
@@ -67,11 +82,25 @@
onMount(() => {
input.disabled = false;
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>
<HTML position.x={position?.[0]} position.z={position?.[1]} transform={false}>
<div class="add-menu-wrapper">
<HTML
position.x={graphState.addMenuPosition?.[0]}
position.z={graphState.addMenuPosition?.[1]}
transform={false}
>
<div class="add-menu-wrapper" bind:this={wrapper}>
<div class="header">
<input
id="add-menu"
@@ -94,19 +123,11 @@
tabindex="0"
aria-selected={node.id === activeNodeId}
onkeydown={(event) => {
if (event.key === "Enter") {
if (position) {
graph.createNode({ type: node.id, position, props: {} });
position = null;
}
}
}}
onmousedown={() => {
if (position) {
graph.createNode({ type: node.id, position, props: {} });
position = null;
if (event.key === 'Enter') {
handleNodeCreation(node.id);
}
}}
onmousedown={() => handleNodeCreation(node.id)}
onfocus={() => {
activeNodeId = node.id;
}}
@@ -115,7 +136,7 @@
activeNodeId = node.id;
}}
>
{node.id.split("/").at(-1)}
{node.id.split('/').at(-1)}
</div>
{/each}
</div>
@@ -157,6 +178,8 @@
min-height: none;
width: 100%;
color: var(--text-color);
max-height: 300px;
overflow-y: auto;
}
.result {

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { NodeDefinition, NodeRegistry } from "@nodes/types";
import { onDestroy, onMount } from "svelte";
import type { NodeDefinition, NodeRegistry } from "@nodarium/types";
import { onMount } from "svelte";
let mx = $state(0);
let my = $state(0);

View File

@@ -1,19 +1,44 @@
<script lang="ts">
import { MeshLineGeometry, MeshLineMaterial } from '@threlte/extras';
import { points, lines } from './store.js';
import { T } from '@threlte/core';
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
import { points, lines, rects } from "./store.js";
import { T } from "@threlte/core";
import { Color } from "three";
</script>
{#each $points as point}
<T.Mesh position.x={point.x} position.y={point.y} position.z={point.z} rotation.x={-Math.PI / 2}>
<T.CircleGeometry args={[0.2, 32]} />
<T.MeshBasicMaterial color="red" />
</T.Mesh>
<T.Mesh
position.x={point.x}
position.y={point.y}
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 $lines as line}
<T.Mesh>
<MeshLineGeometry points={line} />
<MeshLineMaterial color="red" linewidth={1} attenuate={false} />
</T.Mesh>
<T.Mesh position.y={1}>
<MeshLineGeometry points={line.points} />
<MeshLineMaterial
color={line.color || "red"}
linewidth={1}
attenuate={false}
/>
</T.Mesh>
{/each}

View File

@@ -1,5 +1,8 @@
import { Vector3 } from "three/src/math/Vector3.js";
import { lines, points } from "./store";
import type { Box } from '@nodarium/types';
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) {
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() {
points.set([]);
lines.set([]);
rects.set([]);
}
export function debugLine(line: Vector3[]) {
export function debugLine(points: Vector3[], color?: Color) {
lines.update((l) => {
l.push(line);
l.push({ points, color });
return l;
});
}
import Component from "./Debug.svelte";
export default Component
export default Component;
export function clearLines() {
lines.set([]);
}

View File

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

View File

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

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,116 +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,30 +1,33 @@
import throttle from '$lib/helpers/throttle';
import { RemoteNodeRegistry } from '@nodarium/registry';
import type {
Edge,
Graph,
Node,
NodeDefinition,
NodeId,
NodeInput,
NodeInstance,
NodeRegistry,
NodeType,
Socket,
} from "@nodes/types";
import { fastHashString } from "@nodes/utils";
import { SvelteMap } from "svelte/reactivity";
import EventEmitter from "./helpers/EventEmitter";
import { createLogger } from "@nodes/utils";
import throttle from "$lib/helpers/throttle";
import { HistoryManager } from "./history-manager";
Socket
} from '@nodarium/types';
import { fastHashString } from '@nodarium/utils';
import { createLogger } from '@nodarium/utils';
import { SvelteMap } from 'svelte/reactivity';
import EventEmitter from './helpers/EventEmitter';
import { HistoryManager } from './history-manager';
const logger = createLogger("graph-manager");
// logger.mute();
const logger = createLogger('graph-manager');
logger.mute();
const clone =
"structuredClone" in self
? self.structuredClone
: (args: any) => JSON.parse(JSON.stringify(args));
const remoteRegistry = new RemoteNodeRegistry('');
const clone = 'structuredClone' in self
? self.structuredClone
: (args: any) => JSON.parse(JSON.stringify(args));
function areSocketsCompatible(
output: string | undefined,
inputs: string | (string | undefined)[] | undefined,
inputs: string | (string | undefined)[] | undefined
) {
if (Array.isArray(inputs) && output) {
return inputs.includes(output);
@@ -32,6 +35,26 @@ function areSocketsCompatible(
return inputs === output;
}
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
if (firstEdge[0].id !== secondEdge[0].id) {
return false;
}
if (firstEdge[1] !== secondEdge[1]) {
return false;
}
if (firstEdge[2].id !== secondEdge[2].id) {
return false;
}
if (firstEdge[3] !== secondEdge[3]) {
return false;
}
return true;
}
export class GraphManager extends EventEmitter<{
save: Graph;
result: any;
@@ -40,13 +63,13 @@ export class GraphManager extends EventEmitter<{
values: Record<string, unknown>;
};
}> {
status = $state<"loading" | "idle" | "error">();
status = $state<'loading' | 'idle' | 'error'>();
loaded = false;
graph: Graph = { id: 0, nodes: [], edges: [] };
id = $state(0);
nodes = new SvelteMap<number, Node>();
nodes = new SvelteMap<number, NodeInstance>();
edges = $state<Edge[]>([]);
@@ -66,7 +89,7 @@ export class GraphManager extends EventEmitter<{
history: HistoryManager = new HistoryManager();
execute = throttle(() => {
if (this.loaded === false) return;
this.emit("result", this.serialize());
this.emit('result', this.serialize());
}, 10);
constructor(public registry: NodeRegistry) {
@@ -78,21 +101,22 @@ export class GraphManager extends EventEmitter<{
id: node.id,
position: [...node.position],
type: node.type,
props: node.props,
})) as Node[];
props: node.props
})) as NodeInstance[];
const edges = this.edges.map((edge) => [
edge[0].id,
edge[1],
edge[2].id,
edge[3],
]) as Graph["edges"];
edge[3]
]) as Graph['edges'];
const serialized = {
id: this.graph.id,
settings: this.settings,
settings: $state.snapshot(this.settings),
meta: $state.snapshot(this.graph.meta),
nodes,
edges,
edges
};
logger.log("serializing graph", serialized);
logger.log('serializing graph', serialized);
return clone($state.snapshot(serialized));
}
@@ -111,14 +135,14 @@ export class GraphManager extends EventEmitter<{
return this.registry.getAllNodes();
}
getLinkedNodes(node: Node) {
const nodes = new Set<Node>();
getLinkedNodes(node: NodeInstance) {
const nodes = new Set<NodeInstance>();
const stack = [node];
while (stack.length) {
const n = stack.pop();
if (!n) continue;
nodes.add(n);
const children = this.getChildrenOfNode(n);
const children = this.getChildren(n);
const parents = this.getParentsOfNode(n);
const newNodes = [...children, ...parents].filter((n) => !nodes.has(n));
stack.push(...newNodes);
@@ -126,21 +150,112 @@ export class GraphManager extends EventEmitter<{
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 = [];
for (const node of nodes) {
const children = node.tmp?.children || [];
const children = node.state?.children || [];
for (const child of children) {
if (nodes.includes(child)) {
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) {
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [
number,
number,
number,
string,
string
]);
}
}
@@ -152,35 +267,31 @@ export class GraphManager extends EventEmitter<{
private _init(graph: Graph) {
const nodes = new Map(
graph.nodes.map((node: Node) => {
graph.nodes.map((node) => {
const nodeType = this.registry.getNode(node.type);
const n = node as NodeInstance;
if (nodeType) {
node.tmp = {
random: (Math.random() - 0.5) * 2,
type: nodeType,
n.state = {
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 to = nodes.get(edge[2]);
if (!from || !to) {
throw new Error("Edge references non-existing node");
throw new Error('Edge references non-existing node');
}
from.tmp = from.tmp || {};
from.tmp.children = from.tmp.children || [];
from.tmp.children.push(to);
to.tmp = to.tmp || {};
to.tmp.parents = to.tmp.parents || [];
to.tmp.parents.push(from);
from.state.children = from.state.children || [];
from.state.children.push(to);
to.state.parents = to.state.parents || [];
to.state.parents.push(from);
return [from, edge[1], to, edge[3]] as Edge;
});
this.edges = [...edges];
this.nodes.clear();
for (const [id, node] of nodes) {
this.nodes.set(id, node);
@@ -194,26 +305,42 @@ export class GraphManager extends EventEmitter<{
this.loaded = false;
this.graph = graph;
this.status = "loading";
this.status = 'loading';
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)]));
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) {
const nodeType = this.registry.getNode(node.type);
if (!nodeType) {
logger.error(`Node type not found: ${node.type}`);
this.status = "error";
this.status = 'error';
return;
}
node.tmp = node.tmp || {};
node.tmp.random = (Math.random() - 0.5) * 2;
node.tmp.type = nodeType;
// Turn into runtime node
const n = node as NodeInstance;
n.state = {};
n.state.type = nodeType;
}
// load settings
@@ -232,11 +359,11 @@ export class GraphManager extends EventEmitter<{
settingTypes[settingId] = {
__node_type: type.id,
__node_input: key,
...type.inputs[key],
...type.inputs[key]
};
if (
settingValues[settingId] === undefined &&
"value" in type.inputs[key]
settingValues[settingId] === undefined
&& 'value' in type.inputs[key]
) {
settingValues[settingId] = type.inputs[key].value;
}
@@ -246,14 +373,14 @@ export class GraphManager extends EventEmitter<{
}
this.settings = settingValues;
this.emit("settings", { types: settingTypes, values: settingValues });
this.emit('settings', { types: settingTypes, values: settingValues });
this.history.reset();
this._init(this.graph);
this.save();
this.status = "idle";
this.status = 'idle';
this.loaded = true;
logger.log(`Graph loaded in ${performance.now() - a}ms`);
@@ -272,7 +399,7 @@ export class GraphManager extends EventEmitter<{
return this.registry.getNode(id);
}
async loadNode(id: NodeType) {
async loadNodeType(id: NodeId) {
await this.registry.load([id]);
const nodeType = this.registry.getNode(id);
@@ -286,9 +413,9 @@ export class GraphManager extends EventEmitter<{
if (settingId) {
settingTypes[settingId] = nodeType.inputs[key];
if (
settingValues &&
settingValues?.[settingId] === undefined &&
"value" in nodeType.inputs[key]
settingValues
&& settingValues?.[settingId] === undefined
&& 'value' in nodeType.inputs[key]
) {
settingValues[settingId] = nodeType.inputs[key].value;
}
@@ -297,33 +424,32 @@ export class GraphManager extends EventEmitter<{
}
this.settings = settingValues;
console.log("GraphManager.setSettings", settingValues);
this.settingTypes = settingTypes;
this.emit("settings", { types: settingTypes, values: settingValues });
this.emit('settings', { types: settingTypes, values: settingValues });
}
getChildrenOfNode(node: Node) {
getChildren(node: NodeInstance) {
const children = [];
const stack = node.tmp?.children?.slice(0);
const stack = node.state?.children?.slice(0);
while (stack?.length) {
const child = stack.pop();
if (!child) continue;
children.push(child);
stack.push(...(child.tmp?.children || []));
stack.push(...(child.state?.children || []));
}
return children;
}
getNodesBetween(from: Node, to: Node): Node[] | undefined {
getNodesBetween(from: NodeInstance, to: NodeInstance): NodeInstance[] | undefined {
// < - - - - from
const toParents = this.getParentsOfNode(to);
// < - - - - from - - - - to
const fromParents = this.getParentsOfNode(from);
if (toParents.includes(from)) {
const fromChildren = this.getChildrenOfNode(from);
const fromChildren = this.getChildren(from);
return toParents.filter((n) => fromChildren.includes(n));
} else if (fromParents.includes(to)) {
const toChildren = this.getChildrenOfNode(to);
const toChildren = this.getChildren(to);
return fromParents.filter((n) => toChildren.includes(n));
} else {
// these two nodes are not connected
@@ -331,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 edgesFromNode = this.edges.filter((edge) => edge[0].id === node.id);
for (const edge of [...edgesToNode, ...edgesFromNode]) {
@@ -344,11 +470,11 @@ export class GraphManager extends EventEmitter<{
for (const [to, toSocket] of inputSockets) {
for (const [from, fromSocket] of outputSockets) {
const outputType = from.tmp?.type?.outputs?.[fromSocket];
const inputType = to?.tmp?.type?.inputs?.[toSocket]?.type;
const outputType = from.state?.type?.outputs?.[fromSocket];
const inputType = to?.state?.type?.inputs?.[toSocket]?.type;
if (outputType === inputType) {
this.createEdge(from, fromSocket, to, toSocket, {
applyUpdate: false,
applyUpdate: false
});
continue;
}
@@ -361,19 +487,32 @@ export class GraphManager extends EventEmitter<{
this.save();
}
createNodeId() {
const max = Math.max(0, ...this.nodes.keys());
return max + 1;
smartConnect(from: NodeInstance, to: NodeInstance): Edge | undefined {
const inputs = Object.entries(to.state?.type?.inputs ?? {});
const outputs = from.state?.type?.outputs ?? [];
for (let i = 0; i < inputs.length; i++) {
const [inputName, input] = inputs[0];
for (let o = 0; o < outputs.length; o++) {
const output = outputs[0];
if (input.type === output) {
return this.createEdge(from, o, to, inputName);
}
}
}
}
createGraph(nodes: Node[], edges: [number, number, number, string][]) {
createNodeId() {
return Math.max(0, ...this.nodes.keys()) + 1;
}
createGraph(nodes: NodeInstance[], edges: [number, number, number, string][]) {
// map old ids to new ids
const idMap = new Map<number, number>();
const startId = this.createNodeId();
let startId = this.createNodeId();
nodes = nodes.map((node, i) => {
const id = startId + i;
nodes = nodes.map((node) => {
const id = startId++;
idMap.set(node.id, id);
const type = this.registry.getNode(node.type);
if (!type) {
@@ -387,16 +526,14 @@ export class GraphManager extends EventEmitter<{
const to = nodes.find((n) => n.id === idMap.get(edge[2]));
if (!from || !to) {
throw new Error("Edge references non-existing node");
throw new Error('Edge references non-existing node');
}
to.tmp = to.tmp || {};
to.tmp.parents = to.tmp.parents || [];
to.tmp.parents.push(from);
to.state.parents = to.state.parents || [];
to.state.parents.push(from);
from.tmp = from.tmp || {};
from.tmp.children = from.tmp.children || [];
from.tmp.children.push(to);
from.state.children = from.state.children || [];
from.state.children.push(to);
return [from, edge[1], to, edge[3]] as Edge;
});
@@ -414,11 +551,11 @@ export class GraphManager extends EventEmitter<{
createNode({
type,
position,
props = {},
props = {}
}: {
type: Node["type"];
position: Node["position"];
props: Node["props"];
type: NodeInstance['type'];
position: NodeInstance['position'];
props: NodeInstance['props'];
}) {
const nodeType = this.registry.getNode(type);
if (!nodeType) {
@@ -426,83 +563,82 @@ export class GraphManager extends EventEmitter<{
return;
}
const node: Node = {
const node: NodeInstance = $state({
id: this.createNodeId(),
type,
position,
tmp: { type: nodeType },
props,
};
state: { type: nodeType },
props
});
this.nodes.set(node.id, node);
this.save();
return node;
}
createEdge(
from: Node,
from: NodeInstance,
fromSocket: number,
to: Node,
to: NodeInstance,
toSocket: string,
{ applyUpdate = true } = {},
) {
{ applyUpdate = true } = {}
): Edge | undefined {
const existingEdges = this.getEdgesToNode(to);
// check if this exact edge already exists
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) {
logger.error("Edge already exists", existingEdge);
logger.error('Edge already exists', existingEdge);
return;
}
// check if socket types match
const fromSocketType = from.tmp?.type?.outputs?.[fromSocket];
const toSocketType = [to.tmp?.type?.inputs?.[toSocket]?.type];
if (to.tmp?.type?.inputs?.[toSocket]?.accepts) {
toSocketType.push(...(to?.tmp?.type?.inputs?.[toSocket]?.accepts || []));
const fromSocketType = from.state?.type?.outputs?.[fromSocket];
const toSocketType = [to.state?.type?.inputs?.[toSocket]?.type];
if (to.state?.type?.inputs?.[toSocket]?.accepts) {
toSocketType.push(...(to?.state?.type?.inputs?.[toSocket]?.accepts || []));
}
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
logger.error(
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`,
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`
);
return;
}
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) {
this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
}
if (applyUpdate) {
this.edges.push([from, fromSocket, to, toSocket]);
} else {
this.edges.push([from, fromSocket, to, toSocket]);
}
const edge = [from, fromSocket, to, toSocket] as Edge;
from.tmp = from.tmp || {};
from.tmp.children = from.tmp.children || [];
from.tmp.children.push(to);
this.edges.push(edge);
to.tmp = to.tmp || {};
to.tmp.parents = to.tmp.parents || [];
to.tmp.parents.push(from);
from.state.children = from.state.children || [];
from.state.children.push(to);
to.state.parents = to.state.parents || [];
to.state.parents.push(from);
if (applyUpdate) {
this.save();
}
this.execute();
return edge;
}
undo() {
const nextState = this.history.undo();
if (nextState) {
this._init(nextState);
this.emit("save", this.serialize());
this.emit('save', this.serialize());
}
}
@@ -510,7 +646,7 @@ export class GraphManager extends EventEmitter<{
const nextState = this.history.redo();
if (nextState) {
this._init(nextState);
this.emit("save", this.serialize());
this.emit('save', this.serialize());
}
}
@@ -527,44 +663,83 @@ export class GraphManager extends EventEmitter<{
if (this.currentUndoGroup) return;
const state = this.serialize();
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 stack = node.tmp?.parents?.slice(0);
const stack = node.state?.parents?.slice(0);
while (stack?.length) {
if (parents.length > 1000000) {
logger.warn("Infinite loop detected");
logger.warn('Infinite loop detected');
break;
}
const parent = stack.pop();
if (!parent) continue;
parents.push(parent);
stack.push(...(parent.tmp?.parents || []));
stack.push(...(parent.state?.parents || []));
}
return parents.reverse();
}
getPossibleSockets({ node, index }: Socket): [Node, string | number][] {
const nodeType = node?.tmp?.type;
getPossibleNodes(socket: Socket): NodeDefinition[] {
const allDefinitions = this.getNodeDefinitions();
const nodeType = socket.node.state?.type;
if (!nodeType) {
return [];
}
const definitions = typeof socket.index === 'string'
? allDefinitions.filter(s => {
return s.outputs?.find(_s =>
Object
.values(nodeType?.inputs || {})
.map(s => s.type)
.includes(_s as NodeInput['type'])
);
})
: allDefinitions.filter(s =>
Object
.values(s.inputs ?? {})
.find(s => {
if (s.hidden) return false;
if (nodeType.outputs?.includes(s.type)) {
return true;
}
return s.accepts?.find(a => nodeType.outputs?.includes(a));
})
);
return definitions;
}
getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
const nodeType = node?.state?.type;
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 (typeof index === "string") {
if (typeof index === 'string') {
// filter out self and child nodes
const children = new Set(this.getChildrenOfNode(node).map((n) => n.id));
const children = new Set(this.getChildren(node).map((n) => n.id));
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;
for (const node of nodes) {
const nodeType = node?.tmp?.type;
const nodeType = node?.state?.type;
const inputs = nodeType?.outputs;
if (!inputs) continue;
for (let index = 0; index < inputs.length; index++) {
@@ -573,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
// filter out self and parent nodes
const parents = new Set(this.getParentsOfNode(node).map((n) => n.id));
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
const edges = new Map(
this.getEdgesFromNode(node)
.filter((e) => e[1] === index)
.map((e) => [e[2].id, e[3]]),
.map((e) => [e[2].id, e[3]])
);
const ownType = nodeType.outputs?.[index];
for (const node of nodes) {
const inputs = node?.tmp?.type?.inputs;
const inputs = node?.state?.type?.inputs;
if (!inputs) continue;
for (const key in inputs) {
const otherType = [inputs[key].type];
otherType.push(...(inputs[key].accepts || []));
if (
areSocketsCompatible(ownType, otherType) &&
edges.get(node.id) !== key
areSocketsCompatible(ownType, otherType)
&& edges.get(node.id) !== key
) {
sockets.push([node, key]);
}
@@ -613,7 +788,7 @@ export class GraphManager extends EventEmitter<{
removeEdge(
edge: Edge,
{ applyDeletion = true }: { applyDeletion?: boolean } = {},
{ applyDeletion = true }: { applyDeletion?: boolean } = {}
) {
const id0 = edge[0].id;
const sid0 = edge[1];
@@ -621,35 +796,31 @@ export class GraphManager extends EventEmitter<{
const sid2 = edge[3];
const _edge = this.edges.find(
(e) =>
e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2,
(e) => e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2
);
if (!_edge) return;
edge[0].tmp = edge[0].tmp || {};
if (edge[0].tmp.children) {
edge[0].tmp.children = edge[0].tmp.children.filter(
(n: Node) => n.id !== id2,
if (edge[0].state.children) {
edge[0].state.children = edge[0].state.children.filter(
(n: NodeInstance) => n.id !== id2
);
}
edge[2].tmp = edge[2].tmp || {};
if (edge[2].tmp.parents) {
edge[2].tmp.parents = edge[2].tmp.parents.filter(
(n: Node) => n.id !== id0,
if (edge[2].state.parents) {
edge[2].state.parents = edge[2].state.parents.filter(
(n: NodeInstance) => n.id !== id0
);
}
this.edges = this.edges.filter((e) => !areEdgesEqual(e, edge));
if (applyDeletion) {
this.edges = this.edges.filter((e) => e !== _edge);
this.execute();
this.save();
} else {
this.edges = this.edges.filter((e) => e !== _edge);
}
}
getEdgesToNode(node: Node) {
getEdgesToNode(node: NodeInstance) {
return this.edges
.filter((edge) => edge[2].id === node.id)
.map((edge) => {
@@ -658,10 +829,10 @@ export class GraphManager extends EventEmitter<{
if (!from || !to) return;
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
.filter((edge) => edge[0].id === node.id)
.map((edge) => {
@@ -670,6 +841,6 @@ export class GraphManager extends EventEmitter<{
if (!from || !to) return;
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

@@ -0,0 +1,342 @@
import type { NodeInstance, Socket } from '@nodarium/types';
import { getContext, setContext } from 'svelte';
import { SvelteSet } from 'svelte/reactivity';
import type { OrthographicCamera, Vector3 } from 'three';
import type { GraphManager } from './graph-manager.svelte';
const graphStateKey = Symbol('graph-state');
export function getGraphState() {
return getContext<GraphState>(graphStateKey);
}
export function setGraphState(graphState: GraphState) {
return setContext(graphStateKey, graphState);
}
const graphManagerKey = Symbol('graph-manager');
export function getGraphManager() {
return getContext<GraphManager>(graphManagerKey);
}
export function setGraphManager(manager: GraphManager) {
return setContext(graphManagerKey, manager);
}
type EdgeData = {
x1: number;
y1: number;
points: Vector3[];
};
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);
height = $state(100);
hoveredEdgeId = $state<string | null>(null);
edges = new Map<string, EdgeData>();
wrapper = $state<HTMLDivElement>(null!);
rect: DOMRect = $derived(
(this.wrapper && this.width && this.height)
? this.wrapper.getBoundingClientRect()
: new DOMRect(0, 0, 0, 0)
);
camera = $state<OrthographicCamera>(null!);
cameraPosition: [number, number, number] = $state([140, 100, 3.5]);
clipboard: null | {
nodes: NodeInstance[];
edges: [number, number, number, string][];
} = null;
cameraBounds = $derived([
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
]);
boxSelection = $state(false);
edgeEndPosition = $state<[number, number] | 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]);
mouseDown = $state<[number, number] | null>(null);
activeNodeId = $state(-1);
selectedNodes = new SvelteSet<number>();
activeSocket = $state<Socket | null>(null);
hoveredSocket = $state<Socket | null>(null);
possibleSockets = $state<Socket[]>([]);
possibleSocketIds = $derived(
new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`))
);
getEdges() {
return $state.snapshot(this.edges);
}
clearSelection() {
this.selectedNodes.clear();
}
isBodyFocused = () => document?.activeElement?.nodeName !== 'INPUT';
setEdgeGeometry(edgeId: string, x1: number, y1: number, points: Vector3[]) {
this.edges.set(edgeId, { x1, y1, points });
}
removeEdgeGeometry(edgeId: string) {
this.edges.delete(edgeId);
}
getEdgeData() {
return this.edges;
}
updateNodePosition(node: NodeInstance) {
if (
node.state.x === node.position[0]
&& node.state.y === node.position[1]
) {
delete node.state.x;
delete node.state.y;
}
if (node.state['x'] !== undefined && node.state['y'] !== undefined) {
if (node.state.ref) {
node.state.ref.style.setProperty('--nx', `${node.state.x * 10}px`);
node.state.ref.style.setProperty('--ny', `${node.state.y * 10}px`);
}
} else {
if (node.state.ref) {
node.state.ref.style.setProperty('--nx', `${node.position[0] * 10}px`);
node.state.ref.style.setProperty('--ny', `${node.position[1] * 10}px`);
}
}
}
getSnapLevel() {
const z = this.cameraPosition[2];
if (z > 66) {
return 8;
} else if (z > 55) {
return 4;
} else if (z > 11) {
return 2;
} else {
}
return 1;
}
getSocketPosition(
node: NodeInstance,
index: string | number
): [number, number] {
if (typeof index === 'number') {
return [
(node?.state?.x ?? node.position[0]) + 20,
(node?.state?.y ?? node.position[1]) + 2.5 + 10 * index
];
} else {
const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index);
return [
node?.state?.x ?? node.position[0],
(node?.state?.y ?? node.position[1]) + 10 + 10 * _index
];
}
}
private nodeHeightCache: Record<string, number> = {};
getNodeHeight(nodeTypeId: string) {
if (nodeTypeId in this.nodeHeightCache) {
return this.nodeHeightCache[nodeTypeId];
}
const node = this.graph.getNodeType(nodeTypeId);
if (!node?.inputs) {
return 5;
}
const height = 5
+ 10
* Object.keys(node.inputs).filter(
(p) =>
p !== 'seed'
&& node?.inputs
&& !('setting' in node?.inputs?.[p])
&& node.inputs[p].hidden !== true
).length;
this.nodeHeightCache[nodeTypeId] = height;
return height;
}
copyNodes() {
if (this.activeNodeId === -1 && !this.selectedNodes?.size) {
return;
}
let nodes = [
this.activeNodeId,
...(this.selectedNodes?.values() || [])
]
.map((id) => this.graph.getNode(id))
.filter(b => !!b);
const edges = this.graph.getEdgesBetweenNodes(nodes);
nodes = nodes.map((node) => ({
...node,
position: [
this.mousePosition[0] - node.position[0],
this.mousePosition[1] - node.position[1]
],
tmp: undefined
}));
this.clipboard = {
nodes: nodes,
edges: edges
};
}
pasteNodes() {
if (!this.clipboard) return;
const nodes = this.clipboard.nodes
.map((node) => {
node.position[0] = this.mousePosition[0] - node.position[0];
node.position[1] = this.mousePosition[1] - node.position[1];
return node;
})
.filter(Boolean) as NodeInstance[];
const newNodes = this.graph.createGraph(nodes, this.clipboard.edges);
this.selectedNodes.clear();
for (const node of newNodes) {
this.selectedNodes.add(node.id);
}
}
setDownSocket(socket: Socket) {
this.activeSocket = socket;
let { node, index, position } = socket;
// remove existing edge
if (typeof index === 'string') {
const edges = this.graph.getEdgesToNode(node);
for (const edge of edges) {
if (edge[3] === index) {
node = edge[0];
index = edge[1];
position = this.getSocketPosition(node, index);
this.graph.removeEdge(edge);
break;
}
}
}
this.mouseDown = position;
this.activeSocket = {
node,
index,
position
};
this.possibleSockets = this.graph
.getPossibleSockets(this.activeSocket)
.map(([node, index]) => {
return {
node,
index,
position: this.getSocketPosition(node, index)
};
});
}
projectScreenToWorld(x: number, y: number): [number, number] {
return [
this.cameraPosition[0]
+ (x - this.width / 2) / this.cameraPosition[2],
this.cameraPosition[1]
+ (y - this.height / 2) / this.cameraPosition[2]
];
}
getNodeIdFromEvent(event: MouseEvent) {
let clickedNodeId = -1;
let mx = event.clientX - this.rect.x;
let my = event.clientY - this.rect.y;
if (event.button === 0) {
// check if the clicked element is a node
if (event.target instanceof HTMLElement) {
const nodeElement = event.target.closest('.node');
const nodeId = nodeElement?.getAttribute?.('data-node-id');
if (nodeId) {
clickedNodeId = parseInt(nodeId, 10);
}
}
// if we do not have an active node,
// we are going to check if we clicked on a node by coordinates
if (clickedNodeId === -1) {
const [downX, downY] = this.projectScreenToWorld(mx, my);
for (const node of this.graph.nodes.values()) {
const x = node.position[0];
const y = node.position[1];
const height = this.getNodeHeight(node.type);
if (downX > x && downX < x + 20 && downY > y && downY < y + height) {
clickedNodeId = node.id;
break;
}
}
}
}
return clickedNodeId;
}
isNodeInView(node: NodeInstance) {
const height = this.getNodeHeight(node.type);
const width = 20;
return (
node.position[0] > this.cameraBounds[0] - width
&& node.position[0] < this.cameraBounds[1]
&& node.position[1] > this.cameraBounds[2] - height
&& node.position[1] < this.cameraBounds[3]
);
}
openNodePalette() {
this.addMenuPosition = [this.mousePosition[0], this.mousePosition[1]];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,102 +0,0 @@
<script lang="ts">
import type { Edge as EdgeType, Node as NodeType } from "@nodes/types";
import { HTML } from "@threlte/extras";
import Edge from "../edges/Edge.svelte";
import Node from "../node/Node.svelte";
import { getContext, onMount } from "svelte";
import { getGraphState } from "./state.svelte";
import { useThrelte } from "@threlte/core";
import { appSettings } from "$lib/settings/app-settings.svelte";
type Props = {
nodes: Map<number, NodeType>;
edges: EdgeType[];
cameraPosition: [number, number, number];
};
const { nodes, edges, cameraPosition = [0, 0, 4] }: Props = $props();
const { invalidate } = useThrelte();
$effect(() => {
appSettings.value.theme;
invalidate();
});
const graphState = getGraphState();
const isNodeInView = getContext<(n: NodeType) => boolean>("isNodeInView");
const getSocketPosition =
getContext<(node: NodeType, index: string | number) => [number, number]>(
"getSocketPosition",
);
const edgePositions = $derived(
edges.map((edge) => {
const fromNode = nodes.get(edge[0].id);
const toNode = nodes.get(edge[2].id);
// This check is important because nodes might not be there during some transitions.
if (!fromNode || !toNode) {
return [0, 0, 0, 0];
}
const pos1 = getSocketPosition(fromNode, edge[1]);
const pos2 = getSocketPosition(toNode, edge[3]);
return [pos1[0], pos1[1], pos2[0], pos2[1]];
}),
);
onMount(() => {
for (const node of nodes.values()) {
if (node?.tmp?.ref) {
node.tmp.ref.style.setProperty("--nx", `${node.position[0] * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.position[1] * 10}px`);
}
}
});
</script>
{#each edgePositions as edge (`${edge.join("-")}`)}
{@const [x1, y1, x2, y2] = edge}
<Edge
z={cameraPosition[2]}
from={{
x: x1,
y: y1,
}}
to={{
x: x2,
y: y2,
}}
/>
{/each}
<HTML transform={false}>
<div
role="tree"
id="graph"
tabindex="0"
class="wrapper"
style:transform={`scale(${cameraPosition[2] * 0.1})`}
class:hovering-sockets={graphState.activeSocket}
>
{#each nodes.values() as node (node.id)}
<Node
{node}
inView={cameraPosition && isNodeInView(node)}
z={cameraPosition[2]}
/>
{/each}
</div>
</HTML>
<style>
.wrapper {
position: absolute;
z-index: 100;
width: 0px;
height: 0px;
}
</style>

View File

@@ -1,22 +1,22 @@
<script lang="ts">
import type { Graph, Node, NodeRegistry } from "@nodes/types";
import type { Graph, NodeInstance, NodeRegistry } from "@nodarium/types";
import GraphEl from "./Graph.svelte";
import { GraphManager } from "../graph-manager.svelte";
import { setContext } from "svelte";
import { debounce } from "$lib/helpers";
import { createKeyMap } from "$lib/helpers/createKeyMap";
import { GraphState } from "./state.svelte";
const graphState = new GraphState();
setContext("graphState", graphState);
import {
GraphState,
setGraphManager,
setGraphState,
} from "../graph-state.svelte";
import { setupKeymaps } from "../keymaps";
type Props = {
graph: Graph;
graph?: Graph;
registry: NodeRegistry;
settings?: Record<string, any>;
activeNode?: Node;
activeNode?: NodeInstance;
showGrid?: boolean;
snapToGrid?: boolean;
showHelp?: boolean;
@@ -31,8 +31,8 @@
registry,
settings = $bindable(),
activeNode = $bindable(),
showGrid,
snapToGrid,
showGrid = $bindable(true),
snapToGrid = $bindable(true),
showHelp = $bindable(false),
settingTypes = $bindable(),
onsave,
@@ -40,10 +40,20 @@
}: Props = $props();
export const keymap = createKeyMap([]);
setContext("keymap", keymap);
export const manager = new GraphManager(registry);
setContext("graphManager", manager);
setGraphManager(manager);
const graphState = new GraphState(manager);
$effect(() => {
graphState.showGrid = showGrid;
graphState.snapToGrid = snapToGrid;
graphState.showHelp = showHelp;
});
setGraphState(graphState);
setupKeymaps(keymap, manager, graphState);
$effect(() => {
if (graphState.activeNodeId !== -1) {
@@ -53,13 +63,10 @@
}
});
const updateSettings = debounce((s: Record<string, any>) => {
manager.setSettings(s);
}, 200);
$effect(() => {
if (settingTypes && settings) {
updateSettings(settings);
if (!graphState.addMenuPosition) {
graphState.edgeEndPosition = null;
graphState.activeSocket = null;
}
});
@@ -72,7 +79,11 @@
manager.on("save", (save) => onsave?.(save));
manager.load(graph);
$effect(() => {
if (graph) {
manager.load(graph);
}
});
</script>
<GraphEl 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

@@ -1,6 +0,0 @@
import type { GraphManager } from "../graph-manager.svelte";
import { getContext } from "svelte";
export function getGraphManager(): GraphManager {
return getContext("graphManager");
}

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

@@ -1,21 +0,0 @@
import type { Socket } from "@nodes/types";
import { getContext } from "svelte";
import { SvelteSet } from "svelte/reactivity";
export function getGraphState() {
return getContext<GraphState>("graphState");
}
export class GraphState {
activeNodeId = $state(-1);
selectedNodes = new SvelteSet<number>();
activeSocket = $state<Socket | null>(null);
hoveredSocket = $state<Socket | null>(null);
possibleSockets = $state<Socket[]>([]);
possibleSocketIds = $derived(
new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`)),
);
clearSelection() {
this.selectedNodes.clear();
}
}

View File

@@ -8,7 +8,7 @@ export function lerp(a: number, b: number, t: number) {
export function animate(
duration: number,
callback: (progress: number) => void | false,
callback: (progress: number) => void | false
) {
const start = performance.now();
const loop = (time: number) => {
@@ -33,41 +33,37 @@ export function createNodePath({
cornerBottom = 0,
leftBump = false,
rightBump = false,
aspectRatio = 1,
aspectRatio = 1
} = {}) {
return `M0,${cornerTop}
${
cornerTop
? ` V${cornerTop}
${cornerTop
? ` V${cornerTop}
Q0,0 ${cornerTop * aspectRatio},0
H${100 - cornerTop * aspectRatio}
Q100,0 100,${cornerTop}
`
: ` V0
: ` V0
H100
`
}
}
V${y - height / 2}
${
rightBump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100`
}
${
cornerBottom
? ` V${100 - cornerBottom}
${rightBump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100`
}
${cornerBottom
? ` V${100 - cornerBottom}
Q100,100 ${100 - cornerBottom * aspectRatio},100
H${cornerBottom * aspectRatio}
Q0,100 0,${100 - cornerBottom}
`
: `${leftBump ? `V100 H0` : `V100`}`
}
${
leftBump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
: ` H0`
}
Z`.replace(/\s+/g, " ");
: `${leftBump ? `V100 H0` : `V100`}`
}
${leftBump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
: ` H0`
}
Z`.replace(/\s+/g, ' ');
}
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 =
"structedClone" in globalThis
? globalThis.structuredClone
: (obj) => JSON.parse(JSON.stringify(obj));
export const clone: <T>(v: T) => T = 'structedClone' in globalThis
? globalThis.structuredClone
: (obj) => JSON.parse(JSON.stringify(obj));
export function withSubComponents<A, B extends Record<string, any>>(
component: A,
subcomponents: B,
subcomponents: B
): A & B {
Object.keys(subcomponents).forEach((key) => {
// 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;
}
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,7 +1,7 @@
import { create, type Delta } from "jsondiffpatch";
import type { Graph } from "@nodes/types";
import type { Graph } from "@nodarium/types";
import { clone } from "./helpers/index.js";
import { createLogger } from "@nodes/utils";
import { createLogger } from "@nodarium/utils";
const diff = create({
objectHash: function (obj, index) {
@@ -16,7 +16,7 @@ const diff = create({
});
const log = createLogger("history");
// log.mute();
log.mute();
export class HistoryManager {
index: number = -1;

View File

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

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import type { Node } from "@nodes/types";
import { getContext, onMount } from "svelte";
import { getGraphState } from "../graph/state.svelte";
import type { NodeInstance } from "@nodarium/types";
import { getGraphState } from "../graph-state.svelte";
import { T } from "@threlte/core";
import { type Mesh } from "three";
import NodeFrag from "./Node.frag";
@@ -13,7 +12,7 @@
const graphState = getGraphState();
type Props = {
node: Node;
node: NodeInstance;
inView: boolean;
z: number;
};
@@ -21,40 +20,30 @@
const isActive = $derived(graphState.activeNodeId === node.id);
const isSelected = $derived(graphState.selectedNodes.has(node.id));
let strokeColor = $state(colors.selected);
$effect(() => {
appSettings.value.theme;
strokeColor = isSelected
? colors.selected
: isActive
? colors.active
: colors.outline;
});
const updateNodePosition =
getContext<(n: Node) => void>("updateNodePosition");
const getNodeHeight = getContext<(n: string) => number>("getNodeHeight");
const strokeColor = $derived(
appSettings.value.theme &&
(isSelected
? colors.selected
: isActive
? colors.active
: colors.outline),
);
let meshRef: Mesh | undefined = $state();
const height = getNodeHeight?.(node.type);
const height = graphState.getNodeHeight(node.type);
$effect(() => {
if (!node?.tmp) node.tmp = {};
node.tmp.mesh = meshRef;
});
onMount(() => {
if (!node.tmp) node.tmp = {};
node.tmp.mesh = meshRef;
updateNodePosition?.(node);
if (meshRef && !node.state?.mesh) {
node.state.mesh = meshRef;
graphState.updateNodePosition(node);
}
});
</script>
<T.Mesh
position.x={node.position[0] + 10}
position.z={node.position[1] + height / 2}
position.x={(node.state.x ?? node.position[0]) + 10}
position.z={(node.state.y ?? node.position[1]) + height / 2}
position.y={0.8}
rotation.x={-Math.PI / 2}
bind:ref={meshRef}

View File

@@ -1,13 +1,15 @@
<script lang="ts">
import type { Node } from "@nodes/types";
import type { NodeInstance } from "@nodarium/types";
import NodeHeader from "./NodeHeader.svelte";
import NodeParameter from "./NodeParameter.svelte";
import { getContext, onMount } from "svelte";
import { getGraphState } from "../graph-state.svelte";
let ref: HTMLDivElement;
const graphState = getGraphState();
type Props = {
node: Node;
node: NodeInstance;
position?: "absolute" | "fixed" | "relative";
isActive?: boolean;
isSelected?: boolean;
@@ -24,21 +26,20 @@
z = 2,
}: 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 parameters = Object.entries(node?.tmp?.type?.inputs || {}).filter(
const parameters = Object.entries(node.state?.type?.inputs || {}).filter(
(p) =>
p[1].type !== "seed" && !("setting" in p[1]) && p[1]?.hidden !== true,
);
const updateNodePosition =
getContext<(n: Node) => void>("updateNodePosition");
onMount(() => {
node.tmp = node.tmp || {};
node.tmp.ref = ref;
updateNodePosition?.(node);
$effect(() => {
if ("state" in node && !node.state.ref) {
node.state.ref = ref;
graphState?.updateNodePosition(node);
}
});
</script>

View File

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

View File

@@ -1,13 +1,14 @@
<script lang="ts">
import type { Node, NodeInput } from "@nodes/types";
import { getGraphManager } from "../graph/context.js";
import { Input } from "@nodes/ui";
import type { NodeInstance, NodeInput } from "@nodarium/types";
import { Input } from "@nodarium/ui";
import type { GraphManager } from "../graph-manager.svelte";
type Props = {
node: Node;
node: NodeInstance;
input: NodeInput;
id: string;
elementId?: string;
graph?: GraphManager;
};
const {
@@ -15,10 +16,9 @@
input,
id,
elementId = `input-${Math.random().toString(36).substring(7)}`,
graph,
}: Props = $props();
const graph = getGraphManager();
function getDefaultValue() {
if (node?.props?.[id] !== undefined) return node?.props?.[id] as number;
if ("value" in input && input?.value !== undefined)

View File

@@ -1,51 +1,40 @@
<script lang="ts">
import type {
NodeInput as NodeInputType,
Socket,
Node as NodeType,
} from "@nodes/types";
import { getContext } from "svelte";
import { createNodePath } from "../helpers/index.js";
import { getGraphManager } from "../graph/context.js";
import NodeInput from "./NodeInput.svelte";
import { getGraphState } from "../graph/state.svelte.js";
import type { NodeInput, NodeInstance } from "@nodarium/types";
import { createNodePath } from "../helpers";
import NodeInputEl from "./NodeInput.svelte";
import { getGraphManager, getGraphState } from "../graph-state.svelte";
type Props = {
node: NodeType;
input: NodeInputType;
node: NodeInstance;
input: NodeInput;
id: string;
isLast?: boolean;
};
const graph = getGraphManager();
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 graph = getGraphManager();
const graphState = getGraphState();
const graphId = graph?.id;
const elementId = `input-${Math.random().toString(36).substring(7)}`;
const setDownSocket = getContext<(socket: Socket) => void>("setDownSocket");
const getSocketPosition =
getContext<(node: NodeType, index: string) => [number, number]>(
"getSocketPosition",
);
function handleMouseDown(ev: MouseEvent) {
ev.preventDefault();
ev.stopPropagation();
setDownSocket?.({
graphState.setDownSocket({
node,
index: id,
position: getSocketPosition?.(node, id),
position: graphState.getSocketPosition?.(node, id),
});
}
const leftBump = node.tmp?.type?.inputs?.[id].internal !== true;
const leftBump = node.state?.type?.inputs?.[id].internal !== true;
const cornerBottom = isLast ? 5 : 0;
const aspectRatio = 0.5;
@@ -84,21 +73,21 @@
{#key id && graphId}
<div class="content" class:disabled={graph?.inputSockets?.has(socketId)}>
{#if inputType.label !== ""}
<label for={elementId}>{input.label || id}</label>
<label for={elementId} title={input.description}
>{input.label || id}</label
>
{/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}
<NodeInput {elementId} bind:node {input} {id} />
<NodeInputEl {graph} {elementId} bind:node {input} {id} />
{/if}
</div>
{#if node?.tmp?.type?.inputs?.[id]?.internal !== true}
<div
data-node-socket
class="large target"
onmousedown={handleMouseDown}
role="button"
tabindex="0"
></div>
{#if node?.state?.type?.inputs?.[id]?.internal !== true}
<div data-node-socket class="large target"></div>
<div
data-node-socket
class="small target"
@@ -198,9 +187,6 @@
.content.disabled {
opacity: 0.2;
}
.content.disabled > * {
pointer-events: none;
}
.disabled svg path {
d: var(--hover-path-disabled) !important;

View File

@@ -1,20 +0,0 @@
import type { Node, NodeDefinition } from "@nodes/types";
export type GraphNode = Node & {
tmp?: {
depth?: number;
mesh?: any;
random?: number;
parents?: Node[];
children?: Node[];
inputNodes?: Record<string, Node>;
type?: NodeDefinition;
downX?: number;
downY?: number;
x?: number;
y?: number;
ref?: HTMLElement;
visible?: boolean;
isMoving?: boolean;
};
};

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
<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);
</script>
<slot {id} />
{@render children({ id })}

View File

@@ -126,7 +126,7 @@ export function humanizeDuration(durationInMilliseconds: number) {
}
if (millis > 0 || durationString === '') {
durationString += millis + 'ms';
durationString += Math.floor(millis) + 'ms';
}
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="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"/>

Before

Width:  |  Height:  |  Size: 519 B

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -1,9 +1,9 @@
import { createWasmWrapper } from "@nodes/utils";
import fs from "fs/promises";
import path from "path";
import { createWasmWrapper } from '@nodarium/utils';
import fs from 'fs/promises';
import path from 'path';
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 {
await fs.access(filePath);
@@ -36,12 +36,12 @@ export async function getNode(id: `${string}/${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
.filter((n) => n !== "pkg" && n !== ".template")
.filter((n) => n !== 'pkg' && n !== '.template')
.map((n) => {
return {
id: `${userId}/${n}`,
id: `${userId}/${n}`
};
});
}
@@ -50,20 +50,20 @@ export async function getCollection(userId: `${string}/${string}`) {
const nodes = await getCollectionNodes(userId);
return {
id: userId,
nodes,
nodes
};
}
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(
collections.map(async (n) => {
const nodes = await getCollectionNodes(`${userId}/${n}`);
return {
id: `${userId}/${n}`,
nodes,
nodes
};
}),
})
);
}
@@ -71,20 +71,20 @@ export async function getUser(userId: string) {
const collections = await getUserCollections(userId);
return {
id: userId,
collections,
collections
};
}
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(
nodes.map(async (n) => {
const collections = await getUserCollections(n);
return {
id: n,
collections,
collections
};
}),
})
);
return users;
}

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,3 @@
<script lang="ts">
</script>
<span class="spinner"></span>
<style>

View File

@@ -1,8 +1,12 @@
<script lang="ts">
export let labels: string[] = [];
export let values: number[] = [];
type Props = {
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"];
</script>
@@ -10,7 +14,10 @@
<div class="wrapper">
<div class="bars">
{#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
</div>
{/each}
@@ -18,12 +25,11 @@
<div class="labels mt-2">
{#each values as _label, i}
<div class="text-{colors[i]}">{labels[i]}</div>
<div class="text-{colors[i]}-400">{labels[i]}</div>
{/each}
</div>
<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>
</div>

View File

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

View File

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

View File

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

View File

@@ -1,35 +1,36 @@
<script lang="ts">
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 type { PerformanceData, PerformanceStore } from "@nodes/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,
fps: false,
});
$: vertices = $store?.at(-1)?.["total-vertices"]?.[0] || 0;
$: faces = $store?.at(-1)?.["total-faces"]?.[0] || 0;
$: runtime = $store?.at(-1)?.["runtime"]?.[0] || 0;
const vertices = $derived($store?.at(-1)?.["total-vertices"]?.[0] || 0);
const faces = $derived($store?.at(-1)?.["total-faces"]?.[0] || 0);
const runtime = $derived($store?.at(-1)?.["runtime"]?.[0] || 0);
function getPoints(data: PerformanceData, key: string) {
return data?.map((run) => run[key]?.[0] || 0) || [];
}
export let fps: number[] = [];
</script>
<div class="wrapper">
<table>
<tbody>
<tr on:click={() => ($open.runtime = !$open.runtime)}>
<td>{$open.runtime ? "-" : "+"} runtime </td>
<tr
style="cursor:pointer;"
onclick={() => (open.value.runtime = !open.value.runtime)}
>
<td>{open.value.runtime ? "-" : "+"} runtime </td>
<td>{humanizeDuration(runtime || 1000)}</td>
</tr>
{#if $open.runtime}
{#if open.value.runtime}
<tr>
<td colspan="2">
<SmallGraph points={getPoints($store, "runtime")} />
@@ -37,13 +38,16 @@
</tr>
{/if}
<tr on:click={() => ($open.fps = !$open.fps)}>
<td>{$open.fps ? "-" : "+"} fps </td>
<tr
style="cursor:pointer;"
onclick={() => (open.value.fps = !open.value.fps)}
>
<td>{open.value.fps ? "-" : "+"} fps </td>
<td>
{Math.floor(fps[fps.length - 1])}fps
</td>
</tr>
{#if $open.fps}
{#if open.value.fps}
<tr>
<td colspan="2">
<SmallGraph points={fps} />
@@ -74,9 +78,6 @@
border: solid thin var(--outline);
border-collapse: collapse;
}
tr {
cursor: pointer;
}
td {
padding: 4px;
padding-inline: 8px;

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

View File

@@ -2,8 +2,8 @@
import { Canvas } from "@threlte/core";
import Scene from "./Scene.svelte";
import { Vector3 } from "three";
import { decodeFloat, splitNestedArray } from "@nodes/utils";
import type { PerformanceStore } from "@nodes/utils";
import { decodeFloat, splitNestedArray } from "@nodarium/utils";
import type { PerformanceStore } from "@nodarium/utils";
import { appSettings } from "$lib/settings/app-settings.svelte";
import SmallPerformanceViewer from "$lib/performance/SmallPerformanceViewer.svelte";
import { MeshMatcapMaterial, TextureLoader, type Group } from "three";
@@ -95,12 +95,14 @@
<SmallPerformanceViewer {fps} store={perf} />
{/if}
<Canvas>
<Scene
bind:this={sceneComponent}
{lines}
{centerCamera}
bind:scene
bind:fps
/>
</Canvas>
<div style="height: 100%">
<Canvas>
<Scene
bind:this={sceneComponent}
{lines}
{centerCamera}
bind:scene
bind:fps
/>
</Canvas>
</div>

View File

@@ -1,4 +1,4 @@
import { fastHashArrayBuffer } from "@nodes/utils";
import { fastHashArrayBuffer } from "@nodarium/utils";
import {
BufferAttribute,
BufferGeometry,
@@ -11,7 +11,7 @@ import {
} from "three";
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);
let hash = new Int32Array(sampleCount);
@@ -40,6 +40,9 @@ export function createGeometryPool(parentScene: Group, material: Material) {
let hash = fastArrayHash(data);
let geometry = existingMesh ? existingMesh.geometry : new BufferGeometry();
if (existingMesh) {
existingMesh.visible = true;
}
// Extract data from the encoded array
// const geometryType = encodedData[index++];
@@ -121,7 +124,6 @@ export function createGeometryPool(parentScene: Group, material: Material) {
updateSingleGeometry(input, existingMesh || null);
} else if (existingMesh) {
existingMesh.visible = false;
scene.remove(existingMesh);
}
}
return { totalVertices, totalFaces };
@@ -206,19 +208,16 @@ export function createInstancedGeometryPool(
existingInstance &&
instanceCount > existingInstance.geometry.userData.count
) {
console.log("recreating instance");
scene.remove(existingInstance);
instances.splice(instances.indexOf(existingInstance), 1);
existingInstance = new InstancedMesh(geometry, material, instanceCount);
scene.add(existingInstance);
instances.push(existingInstance);
} else if (!existingInstance) {
console.log("creating instance");
existingInstance = new InstancedMesh(geometry, material, instanceCount);
scene.add(existingInstance);
instances.push(existingInstance);
} else {
console.log("updating instance");
existingInstance.count = instanceCount;
}
@@ -261,7 +260,6 @@ export function createInstancedGeometryPool(
updateSingleInstance(input, existingMesh || null);
} else if (existingMesh) {
existingMesh.visible = false;
scene.remove(existingMesh);
}
}
return { totalVertices, totalFaces };

View File

@@ -1,4 +1,4 @@
import type { Graph, RuntimeExecutor } from "@nodes/types";
import type { Graph, RuntimeExecutor } from "@nodarium/types";
export class RemoteRuntimeExecutor implements RuntimeExecutor {

View File

@@ -1,19 +1,33 @@
import { type SyncCache } from "@nodes/types";
import { type SyncCache } from "@nodarium/types";
export class MemoryRuntimeCache implements SyncCache {
private map = new Map<string, unknown>();
size: number;
private cache: [string, unknown][] = [];
size = 50;
constructor(size = 50) {
this.size = size;
}
get<T>(key: string): T | undefined {
return this.cache.find(([k]) => k === key)?.[1] as T;
}
set<T>(key: string, value: T): void {
this.cache.push([key, value]);
this.cache = this.cache.slice(-this.size);
}
clear(): void {
this.cache = [];
if (!this.map.has(key)) return undefined;
const value = this.map.get(key) as T;
this.map.delete(key);
this.map.set(key, value);
return value;
}
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,22 +1,22 @@
import type {
Graph,
Node,
NodeDefinition,
NodeInput,
NodeRegistry,
RuntimeExecutor,
SyncCache,
} from "@nodes/types";
} from "@nodarium/types";
import {
concatEncodedArrays,
createLogger,
encodeFloat,
fastHashArrayBuffer,
type PerformanceStore,
} from "@nodes/utils";
} from "@nodarium/utils";
import type { RuntimeNode } from "./types";
const log = createLogger("runtime-executor");
// log.mute();
log.mute();
function getValue(input: NodeInput, value?: unknown) {
if (value === undefined && "value" in input) {
@@ -58,14 +58,16 @@ function getValue(input: NodeInput, value?: unknown) {
export class MemoryRuntimeExecutor implements RuntimeExecutor {
private definitionMap: Map<string, NodeDefinition> = new Map();
private randomSeed = Math.floor(Math.random() * 100000000);
private seed = Math.floor(Math.random() * 100000000);
perf?: PerformanceStore;
constructor(
private registry: NodeRegistry,
private cache?: SyncCache<Int32Array>,
) {}
public cache?: SyncCache<Int32Array>,
) {
this.cache = undefined;
}
private async getNodeDefinitions(graph: Graph) {
if (this.registry.status !== "ready") {
@@ -90,18 +92,27 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
// First, lets check if all nodes have a definition
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"),
) as Node;
);
if (!outputNode) {
throw new Error("No output node found");
}
outputNode.tmp = outputNode.tmp || {};
outputNode.tmp.depth = 0;
const nodeMap = new Map<number, Node>(
graph.nodes.map((node) => [node.id, node]),
const nodeMap = new Map(
graphNodes.map((node) => [node.id, node]),
);
// loop through all edges and assign the parent and child nodes to each node
@@ -110,14 +121,9 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
const parent = nodeMap.get(parentId);
const child = nodeMap.get(childId);
if (parent && child) {
parent.tmp = parent.tmp || {};
parent.tmp.children = parent.tmp.children || [];
parent.tmp.children.push(child);
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;
parent.state.children.push(child);
child.state.parents.push(parent);
child.state.inputNodes[childInput] = parent;
}
}
@@ -128,20 +134,10 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
while (stack.length) {
const node = stack.pop();
if (!node) continue;
node.tmp = node.tmp || {};
if (node?.tmp?.depth === undefined) {
node.tmp.depth = 0;
}
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);
}
}
for (const parent of node.state.parents) {
parent.state = parent.state || {};
parent.state.depth = node.state.depth + 1;
stack.push(parent);
}
nodes.push(node);
}
@@ -173,16 +169,20 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
// we execute the nodes from the bottom up
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
const results: Record<string, Int32Array> = {};
if (settings["randomSeed"]) {
this.seed = Math.floor(Math.random() * 100000000);
}
for (const node of sortedNodes) {
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`);
continue;
}
@@ -193,11 +193,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
const inputs = Object.entries(node_type.inputs || {}).map(
([key, input]) => {
if (input.type === "seed") {
if (settings["randomSeed"] === true) {
return Math.floor(Math.random() * 100000000);
} else {
return this.randomSeed;
}
return this.seed;
}
// If the input is linked to a setting, we use that value
@@ -206,7 +202,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
}
// check if the input is connected to another node
const inputNode = node.tmp?.inputNodes?.[key];
const inputNode = node.state.inputNodes[key];
if (inputNode) {
if (results[inputNode.id] === undefined) {
throw new Error(
@@ -248,13 +244,14 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
}
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);
a = performance.now();
results[node.id] = node_type.execute(encoded_inputs);
log.log("Executed", node.type, node.id)
b = performance.now();
if (this.cache) {
if (this.cache && node.id !== outputNode.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

@@ -1,17 +1,34 @@
import { MemoryRuntimeExecutor } from "./runtime-executor";
import { RemoteNodeRegistry, IndexDBCache } from "@nodes/registry";
import type { Graph } from "@nodes/types";
import { createPerformanceStore } from "@nodes/utils";
import { RemoteNodeRegistry, IndexDBCache } from "@nodarium/registry";
import type { Graph } from "@nodarium/types";
import { createPerformanceStore } from "@nodarium/utils";
import { MemoryRuntimeCache } from "./runtime-executor-cache";
const cache = new MemoryRuntimeCache();
const indexDbCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry("", indexDbCache);
const cache = new MemoryRuntimeCache()
const executor = new MemoryRuntimeExecutor(nodeRegistry, cache);
const performanceStore = createPerformanceStore();
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(
graph: Graph,
settings: Record<string, unknown>,

View File

@@ -1,5 +1,5 @@
/// <reference types="vite-plugin-comlink/client" />
import type { Graph, RuntimeExecutor } from "@nodes/types";
import type { Graph, RuntimeExecutor } from "@nodarium/types";
export class WorkerRuntimeExecutor implements RuntimeExecutor {
@@ -11,5 +11,11 @@ export class WorkerRuntimeExecutor implements RuntimeExecutor {
async getPerformanceData() {
return this.worker.getPerformanceData();
}
set useRuntimeCache(useCache: boolean) {
this.worker.setUseRuntimeCache(useCache);
}
set useRegistryCache(useCache: boolean) {
this.worker.setUseRegistryCache(useCache);
}
}

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import NestedSettings from "./NestedSettings.svelte";
import { localState } from "$lib/helpers/localState.svelte";
import type { NodeInput } from "@nodes/types";
import Input from "@nodes/ui";
import type { NodeInput } from "@nodarium/types";
import Input from "@nodarium/ui";
type Button = { type: "button"; label?: string };
type Button = { type: "button"; callback: () => void; label?: string };
type InputType = NodeInput | Button;
@@ -99,7 +99,7 @@
Array.isArray((node as any).options) &&
typeof internalValue === "number"
) {
value[key] = (node as any).options[internalValue] as any;
value[key] = (node as any)?.options?.[internalValue] as any;
} else {
value[key] = internalValue as any;
}
@@ -110,11 +110,13 @@
<!-- Leaf input -->
<div class="input input-{type[key].type}" class:first-level={depth === 1}>
{#if type[key].type === "button"}
<button onclick={() => type[key].callback()}>
<button onclick={() => "callback" in type[key] && type[key].callback()}>
{type[key].label || key}
</button>
{:else}
<label for={id}>{type[key].label || key}</label>
{#if type[key].label !== ""}
<label for={id}>{type[key].label || key}</label>
{/if}
<Input {id} input={type[key]} bind:value={internalValue} />
{/if}
</div>
@@ -194,6 +196,10 @@
padding-bottom: 1px;
}
button {
cursor: pointer;
}
hr {
position: absolute;
margin: 0;

View File

@@ -1,6 +1,4 @@
import { localState } from "$lib/helpers/localState.svelte";
import type { NodeInput } from "@nodes/types";
import type { SettingsType } from ".";
const themes = [
"dark",
@@ -10,7 +8,7 @@ const themes = [
"high-contrast",
"nord",
"dracula",
];
] as const;
export const AppSettingTypes = {
theme: {
@@ -49,11 +47,6 @@ export const AppSettingTypes = {
},
debug: {
title: "Debug",
amount: {
type: "number",
label: "Amount",
value: 4,
},
wireframe: {
type: "boolean",
label: "Wireframe",
@@ -61,7 +54,7 @@ export const AppSettingTypes = {
},
useWorker: {
type: "boolean",
label: "Execute runtime in worker",
label: "Execute in WebWorker",
value: true,
},
showIndices: {
@@ -89,6 +82,24 @@ export const AppSettingTypes = {
label: "Show Stem Lines",
value: false,
},
showGraphJson: {
type: "boolean",
label: "Show Graph Source",
value: false,
},
cache: {
title: "Cache",
useRuntimeCache: {
type: "boolean",
label: "Node Results",
value: true,
},
useRegistryCache: {
type: "boolean",
label: "Node Source",
value: true,
},
},
stressTest: {
title: "Stress Test",
amount: {
@@ -119,32 +130,23 @@ export const AppSettingTypes = {
},
},
},
} as const satisfies SettingsType;
} as const;
type IsInputDefinition<T> = T extends NodeInput ? T : never;
type HasTitle = { title: string };
type SettingsToStore<T> =
T extends { value: infer V }
? V extends readonly string[]
? V[number]
: V
: T extends any[]
? {}
: T extends object
? {
[K in keyof T as T[K] extends object ? K : never]:
SettingsToStore<T[K]>
}
: never;
type Widen<T> = T extends boolean
? boolean
: T extends number
? number
: T extends string
? string
: T;
type ExtractSettingsValues<T> = {
-readonly [K in keyof T]: T[K] extends HasTitle
? ExtractSettingsValues<Omit<T[K], "title">>
: T[K] extends IsInputDefinition<T[K]>
? T[K] extends { value: infer V }
? Widen<V>
: never
: T[K] extends Record<string, any>
? ExtractSettingsValues<T[K]>
: never;
};
export function settingsToStore<T>(settings: T): ExtractSettingsValues<T> {
export function settingsToStore<T>(settings: T): SettingsToStore<T> {
const result = {} as any;
for (const key in settings) {
const value = settings[key];

View File

@@ -1,4 +1,4 @@
import type { NodeInput } from "@nodes/types";
import type { NodeInput } from "@nodarium/types";
type Button = { type: "button"; label?: string };

View File

@@ -1,48 +1,46 @@
<script lang="ts">
import { getContext } from "svelte";
import type { Readable } from "svelte/store";
import { type Snippet } from "svelte";
import { panelState } from "./PanelState.svelte";
export let id: string;
export let icon: string = "";
export let title = "";
export let classes = "";
export let hidden: boolean | undefined = undefined;
const {
id,
icon = "",
title = "",
classes = "",
hidden,
children,
} = $props<{
id: string;
icon?: string;
title?: string;
classes?: string;
hidden?: boolean;
children?: Snippet;
}>();
const setVisibility =
getContext<(id: string, visible: boolean) => void>("setVisibility");
$: 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);
const panel = panelState.registerPanel(id, icon, classes, hidden);
$effect(() => {
panel.hidden = hidden;
});
</script>
{#if $visible}
{#if panelState.activePanel.value === id}
<div class="wrapper" class:hidden>
{#if title}
<header>
<h3>{title}</h3>
</header>
{/if}
<slot />
{@render children?.()}
</div>
{/if}
<style>
header {
border-bottom: solid thin var(--outline);
height: 69px;
height: 70px;
display: flex;
align-items: center;
padding-left: 1em;
}
h3 {
margin: 0px;
}
</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">
import localStore from "$lib/helpers/localStore";
import { setContext } from "svelte";
import { derived } from "svelte/store";
import { type Snippet } from "svelte";
import { panelState as state } from "./PanelState.svelte";
let panels: Record<
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;
}
}
const { children } = $props<{ children?: Snippet }>();
</script>
<div class="wrapper" class:visible={$activePanel}>
<div class="wrapper" class:visible={state.activePanel.value}>
<div class="tabs">
<button
aria-label="Close"
on:click={() => {
setActivePanel($activePanel ? false : keys[0]);
}}
>
<span class="absolute i-tabler-chevron-left w-6 h-6 block"></span>
<button aria-label="Close" onclick={() => state.toggleOpen()}>
<span class="icon-[tabler--settings]"></span>
<span class="absolute i-[tabler--chevron-left] w-6 h-6 block"></span>
</button>
{#each keys as panel (panels[panel].id)}
{#if panels[panel].visible !== false}
{#each state.keys as panelId (panelId)}
{#if !state.panels[panelId].hidden}
<button
aria-label={panel}
class="tab {panels[panel].classes}"
class:active={panel === $activePanel}
on:click={() => setActivePanel(panel)}
aria-label={panelId}
class="tab {state.panels[panelId].classes}"
class:active={panelId === state.activePanel.value}
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>
{/if}
{/each}
</div>
<div class="content">
<slot />
{@render children?.()}
</div>
</div>
@@ -116,7 +71,7 @@
align-items: center;
border-bottom: solid thin var(--outline);
border-left: solid thin var(--outline);
background: var(--layer-0);
background: var(--layer-1);
}
.tabs > button > span {
@@ -124,7 +79,7 @@
}
.tabs > button.active {
background: var(--layer-1);
background: var(--layer-2);
}
.tabs > button.active span {

View File

@@ -1,16 +1,15 @@
<script lang="ts">
import type { Node, NodeInput } from "@nodes/types";
import type { NodeInstance, NodeInput } from "@nodarium/types";
import NestedSettings from "$lib/settings/NestedSettings.svelte";
import type { GraphManager } from "$lib/graph-interface/graph-manager.svelte";
type Props = {
manager: GraphManager;
node: Node;
node: NodeInstance;
};
const { manager, node }: Props = $props();
const { manager, node = $bindable() }: Props = $props();
const nodeDefinition = filterInputs(node.tmp?.type?.inputs);
function filterInputs(inputs?: Record<string, NodeInput>) {
const _inputs = $state.snapshot(inputs);
return Object.fromEntries(
@@ -20,18 +19,19 @@
})
.map(([key, value]) => {
//@ts-ignore
value.__node_type = node?.tmp?.type.id;
value.__node_type = node.state?.type.id;
//@ts-ignore
value.__node_input = key;
return [key, value];
}),
);
}
const nodeDefinition = filterInputs(node.state?.type?.inputs);
type Store = Record<string, number | number[]>;
let store = $state<Store>(createStore(node?.props, nodeDefinition));
function createStore(
props: Node["props"],
props: NodeInstance["props"],
inputs: Record<string, NodeInput>,
): Store {
const store: Store = {};
@@ -64,6 +64,7 @@
lastPropsHash = propsHash;
if (needsUpdate) {
manager.save();
manager.execute();
}
}
@@ -75,8 +76,12 @@
});
</script>
<NestedSettings
id="activeNodeSettings"
bind:value={store}
type={nodeDefinition}
/>
{#if Object.keys(nodeDefinition).length}
<NestedSettings
id="activeNodeSettings"
bind:value={store}
type={nodeDefinition}
/>
{:else}
<p class="mx-4">Node has no settings</p>
{/if}

View File

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

View File

@@ -1,143 +1,148 @@
<script lang="ts">
import localStore from "$lib/helpers/localStore";
import { Integer } from "@nodes/ui";
import { writable } from "svelte/store";
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 lang="ts" module>
let result:
| { stdev: number; avg: number; duration: number; samples: number[] }
| undefined = $state();
</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}>
{#if isRunning}
{#if result}
<h3>Finished ({humanizeDuration(result.duration)})</h3>
<div class="monitor-wrapper">
<Monitor points={result.samples} />
</div>
<label for="bench-avg">Average </label>
<button
id="bench-avg"
on:keydown={(ev) => ev.key === "Enter" && copyContent(result?.avg)}
on:click={() => copyContent(result?.avg)}
>{Math.floor(result.avg * 100) / 100}</button
>
<i
role="button"
tabindex="0"
on:keydown={(ev) => ev.key === "Enter" && copyContent(result?.avg)}
on:click={() => copyContent(result?.avg)}>(click to copy)</i
>
<label for="bench-stdev">Standard Deviation σ</label>
<button id="bench-stdev" on:click={() => copyContent(result?.stdev)}
>{Math.floor(result.stdev * 100) / 100}</button
>
<i
role="button"
tabindex="0"
on:keydown={(ev) => ev.key === "Enter" && copyContent(result?.avg)}
on:click={() => copyContent(result?.stdev + "")}>(click to copy)</i
>
<div>
<button on:click={() => (isRunning = false)}>reset</button>
</div>
{:else}
<p>WarmUp ({$warmUp}/{warmUpAmount})</p>
<progress value={$warmUp} max={warmUpAmount}
>{Math.floor(($warmUp / warmUpAmount) * 100)}%</progress
>
<p>Progress ({samples}/{$amount})</p>
<progress value={samples} max={$amount}
>{Math.floor((samples / $amount) * 100)}%</progress
>
{/if}
{:else}
<label for="bench-samples">Samples</label>
<Integer id="bench-sample" bind:value={$amount} max={1000} />
<button on:click={benchmark} disabled={isRunning}> start </button>
{/if}
{#if result}
<h3>Finished ({humanizeDuration(result.duration)})</h3>
<div class="monitor-wrapper">
<Monitor points={result.samples} />
</div>
<label for="bench-avg">Average </label>
<button
id="bench-avg"
onkeydown={(ev) => ev.key === 'Enter' && copyContent(result?.avg)}
onclick={() => copyContent(result?.avg)}
>
{Math.floor(result.avg * 100) / 100}
</button>
<i
role="button"
tabindex="0"
onkeydown={(ev) => ev.key === 'Enter' && copyContent(result?.avg)}
onclick={() => copyContent(result?.avg)}
>(click to copy)</i>
<label for="bench-stdev">Standard Deviation σ</label>
<button id="bench-stdev" onclick={() => copyContent(result?.stdev)}>
{Math.floor(result.stdev * 100) / 100}
</button>
<i
role="button"
tabindex="0"
onkeydown={(ev) => ev.key === 'Enter' && copyContent(result?.avg)}
onclick={() => copyContent(result?.stdev + '')}
>(click to copy)</i>
<div>
<button onclick={() => (isRunning = false)}>reset</button>
</div>
{:else if isRunning}
<p>WarmUp ({$warmUp}/{warmUpAmount})</p>
<progress value={$warmUp} max={warmUpAmount}>
{Math.floor(($warmUp / warmUpAmount) * 100)}%
</progress>
<p>Progress ({samples}/{amount.value})</p>
<progress value={samples} max={amount.value}>
{Math.floor((samples / amount.value) * 100)}%
</progress>
{:else}
<label for="bench-samples">Samples</label>
<Number id="bench-sample" bind:value={amount.value} max={1000} />
<button onclick={benchmark} disabled={isRunning}>start</button>
{/if}
</div>
<style>
.wrapper {
padding: 1em;
display: flex;
flex-direction: column;
gap: 1em;
}
.monitor-wrapper {
border: solid thin var(--outline);
border-bottom: none;
}
i {
opacity: 0.5;
font-size: 0.8em;
}
.wrapper {
padding: 1em;
display: flex;
flex-direction: column;
gap: 1em;
}
.monitor-wrapper {
border: solid thin var(--outline);
border-bottom: none;
}
i {
opacity: 0.5;
font-size: 0.8em;
}
</style>

View File

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

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { Graph } from "$lib/types";
const { graph }: { graph?: Graph } = $props();
function convert(g: Graph): string {
return JSON.stringify(
{
...g,
nodes: g.nodes.map((n: any) => ({ ...n, tmp: undefined })),
},
null,
2,
);
}
</script>
<pre>
{graph ? convert(graph) : 'No graph loaded'}
</pre>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { createKeyMap } from "$lib/helpers/createKeyMap";
import { ShortCut } from "@nodes/ui";
import { ShortCut } from "@nodarium/ui";
import { get } from "svelte/store";
type Props = {
@@ -11,35 +11,36 @@
};
let { keymaps }: Props = $props();
console.log({ keymaps });
</script>
<table class="wrapper">
<tbody>
{#each keymaps as keymap}
<tr>
<td colspan="2">
<h3>{keymap.title}</h3>
</td>
</tr>
{#each get(keymap.keymap?.keys) as key}
<div class="p-4">
<table class="wrapper">
<tbody>
{#each keymaps as keymap}
<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}
<td colspan="2">
<h3>{keymap.title}</h3>
</td>
</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}
</tbody>
</table>
</tbody>
</table>
</div>
<style>
.wrapper {

View File

@@ -1,8 +1,6 @@
import type {
Graph,
Node as NodeType,
NodeDefinition,
NodeInput,
RuntimeExecutor,
} from "@nodes/types";
} from "@nodarium/types";
export type { Graph, NodeDefinition, NodeInput };

View File

@@ -1,7 +1,15 @@
<script lang="ts">
import "@nodes/ui/app.css";
import "virtual:uno.css";
import "@unocss/reset/normalize.css";
import "@nodarium/ui/app.css";
import "../app.css";
import type { Snippet } from "svelte";
import * as config from "$lib/config";
const { children } = $props<{ children?: Snippet }>();
</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 GraphInterface from "$lib/graph-interface";
import * as templates from "$lib/graph-templates";
import type { Graph, Node } from "@nodes/types";
import type { Graph, NodeInstance } from "@nodarium/types";
import Viewer from "$lib/result-viewer/Viewer.svelte";
import {
appSettings,
@@ -23,10 +23,13 @@
WorkerRuntimeExecutor,
MemoryRuntimeExecutor,
} from "$lib/runtime";
import { IndexDBCache, RemoteNodeRegistry } from "@nodes/registry";
import { createPerformanceStore } from "@nodes/utils";
import { IndexDBCache, RemoteNodeRegistry } from "@nodarium/registry";
import { createPerformanceStore } from "@nodarium/utils";
import BenchmarkPanel from "$lib/sidebar/panels/BenchmarkPanel.svelte";
import { debounceAsyncFunction } from "$lib/helpers";
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();
@@ -36,19 +39,33 @@
const runtimeCache = new MemoryRuntimeCache();
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
memoryRuntime.perf = performanceStore;
const pm = new ProjectManager();
const runtime = $derived(
appSettings.value.debug.useWorker ? workerRuntime : memoryRuntime,
);
let activeNode = $state<Node | undefined>(undefined);
let scene = $state<Group>(null!);
$effect(() => {
workerRuntime.useRegistryCache =
appSettings.value.debug.cache.useRuntimeCache;
workerRuntime.useRuntimeCache =
appSettings.value.debug.cache.useRegistryCache;
let graph = $state(
localStorage.getItem("graph")
? JSON.parse(localStorage.getItem("graph")!)
: templates.defaultPlant,
);
if (appSettings.value.debug.cache.useRegistryCache) {
nodeRegistry.cache = registryCache;
} else {
nodeRegistry.cache = undefined;
}
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 viewerComponent = $state<ReturnType<typeof Viewer>>();
@@ -65,89 +82,80 @@
{
key: "r",
description: "Regenerate the plant model",
callback: randomGenerate,
callback: () => randomGenerate(),
},
]);
let graphSettings = $state<Record<string, any>>({});
let graphSettingTypes = $state({
randomSeed: { type: "boolean", value: false },
});
$effect(() => {
if (graphSettings) {
if (graphSettings && graphSettingTypes) {
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;
const handleUpdate = debounceAsyncFunction(
async (g: Graph, s: Record<string, any> = graphSettings) => {
runIndex++;
performanceStore.startRun();
try {
let a = performance.now();
const graphResult = await runtime.execute(
$state.snapshot(g),
$state.snapshot(s),
);
let b = performance.now();
async function update(
g: Graph,
s: Record<string, any> = $state.snapshot(graphSettings),
) {
performanceStore.startRun();
try {
let a = performance.now();
const graphResult = await runtime.execute(g, s);
let b = performance.now();
if (appSettings.value.debug.useWorker) {
let perfData = await runtime.getPerformanceData();
let lastRun = perfData?.at(-1);
if (lastRun?.total) {
lastRun.runtime = lastRun.total;
delete lastRun.total;
performanceStore.mergeData(lastRun);
performanceStore.addPoint(
"worker-transfer",
b - a - lastRun.runtime[0],
);
}
if (appSettings.value.debug.useWorker) {
let perfData = await runtime.getPerformanceData();
let lastRun = perfData?.at(-1);
if (lastRun?.total) {
lastRun.runtime = lastRun.total;
delete lastRun.total;
performanceStore.mergeData(lastRun);
performanceStore.addPoint(
"worker-transfer",
b - a - lastRun.runtime[0],
);
}
viewerComponent?.update(graphResult);
} catch (error) {
console.log("errors", error);
} finally {
performanceStore.stopRun();
}
},
);
viewerComponent?.update(graphResult);
} catch (error) {
console.log("errors", error);
} finally {
performanceStore.stopRun();
}
}
const handleUpdate = debounceAsyncFunction(update);
$effect(() => {
//@ts-ignore
AppSettingTypes.debug.stressTest.loadGrid.callback = () => {
graph = templates.grid(
appSettings.value.debug.amount.value,
appSettings.value.debug.amount.value,
manager.load(
templates.grid(
appSettings.value.debug.stressTest.amount,
appSettings.value.debug.stressTest.amount,
),
);
};
//@ts-ignore
AppSettingTypes.debug.stressTest.loadTree.callback = () => {
graph = templates.tree(appSettings.value.debug.amount.value);
manager.load(templates.tree(appSettings.value.debug.stressTest.amount));
};
//@ts-ignore
AppSettingTypes.debug.stressTest.lottaFaces.callback = () => {
graph = templates.lottaFaces;
manager.load(templates.lottaFaces as unknown as Graph);
};
//@ts-ignore
AppSettingTypes.debug.stressTest.lottaNodes.callback = () => {
graph = templates.lottaNodes;
manager.load(templates.lottaNodes as unknown as Graph);
};
//@ts-ignore
AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => {
graph = templates.lottaNodesAndFaces;
manager.load(templates.lottaNodesAndFaces as unknown as Graph);
};
});
function handleSave(graph: Graph) {
localStorage.setItem("graph", JSON.stringify(graph));
}
</script>
<svelte:document onkeydown={applicationKeymap.handleKeyboardEvent} />
@@ -164,21 +172,23 @@
/>
</Grid.Cell>
<Grid.Cell>
<GraphInterface
{graph}
bind:this={graphInterface}
registry={nodeRegistry}
showGrid={appSettings.value.nodeInterface.showNodeGrid}
snapToGrid={appSettings.value.nodeInterface.snapToGrid}
bind:activeNode
bind:showHelp={appSettings.value.nodeInterface.showHelp}
bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes}
onresult={(result) => handleUpdate(result)}
onsave={(graph) => handleSave(graph)}
/>
{#if pm.graph}
<GraphInterface
graph={pm.graph}
bind:this={graphInterface}
registry={nodeRegistry}
showGrid={appSettings.value.nodeInterface.showNodeGrid}
snapToGrid={appSettings.value.nodeInterface.snapToGrid}
bind:activeNode
bind:showHelp={appSettings.value.nodeInterface.showHelp}
bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes}
onsave={(g) => pm.saveGraph(g)}
onresult={(result) => handleUpdate(result)}
/>
{/if}
<Sidebar>
<Panel id="general" title="General" icon="i-tabler-settings">
<Panel id="general" title="General" icon="i-[tabler--settings]">
<NestedSettings
id="general"
bind:value={appSettings.value}
@@ -188,51 +198,58 @@
<Panel
id="shortcuts"
title="Keyboard Shortcuts"
icon="i-tabler-keyboard"
icon="i-[tabler--keyboard]"
>
<Keymap
keymaps={[
{ keymap: applicationKeymap, title: "Application" },
{ keymap: graphInterface.keymap, title: "Node-Editor" },
{ keymap: graphInterface?.keymap, title: "Node-Editor" },
]}
/>
</Panel>
<Panel id="exports" title="Exporter" icon="i-tabler-package-export">
<Panel id="exports" title="Exporter" icon="i-[tabler--package-export]">
<ExportSettings {scene} />
</Panel>
<Panel
id="node-store"
classes="text-green-400"
title="Node Store"
icon="i-tabler-database"
icon="i-[tabler--database] bg-green-400"
>
<NodeStore registry={nodeRegistry} />
</Panel>
<Panel
id="performance"
title="Performance"
classes="text-red-400"
hidden={!appSettings.value.debug.showPerformancePanel}
icon="i-tabler-brand-speedtest"
icon="i-[tabler--brand-speedtest] bg-red-400"
>
{#if $performanceStore}
<PerformanceViewer data={$performanceStore} />
{/if}
</Panel>
<Panel id="projects" icon="i-[tabler--folder-open]">
<ProjectManagerEl projectManager={pm} />
</Panel>
<Panel
id="graph-source"
title="Graph Source"
hidden={!appSettings.value.debug.showGraphJson}
icon="i-[tabler--code]"
>
<GraphSource graph={pm.graph ?? manager?.serialize()} />
</Panel>
<Panel
id="benchmark"
title="Benchmark"
classes="text-red-400"
hidden={!appSettings.value.debug.showBenchmarkPanel}
icon="i-tabler-graph"
icon="i-[tabler--graph] bg-red-400"
>
<BenchmarkPanel run={randomGenerate} />
</Panel>
<Panel
id="graph-settings"
title="Graph Settings"
classes="text-blue-400"
icon="i-custom-graph"
icon="i-[custom--graph] bg-blue-400"
>
<NestedSettings
id="graph-settings"
@@ -243,10 +260,9 @@
<Panel
id="active-node"
title="Node Settings"
classes="text-blue-400"
icon="i-tabler-adjustments"
icon="i-[tabler--adjustments] bg-blue-400"
>
<ActiveNodeSettings {manager} node={activeNode} />
<ActiveNodeSettings {manager} bind:node={activeNode} />
</Panel>
</Sidebar>
</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 UnoCSS from 'unocss/vite';
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite'
import comlink from 'vite-plugin-comlink';
import glsl from "vite-plugin-glsl";
import wasm from "vite-plugin-wasm";
export default defineConfig({
plugins: [
tailwindcss(),
comlink(),
UnoCSS(),
sveltekit(),
glsl(),
wasm()

View File

@@ -4,7 +4,7 @@ This guide will help you developing your first Nodarium Node written in Rust. As
## 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
# install rust
@@ -22,11 +22,12 @@ cd my-new-node
## Setup Definition
Now we create the definition file of the node.
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).
`src/definition.json`
```json
```json
{
"id": "my-name/my-namespace/zylinder-node",
"outputs": [
@@ -35,7 +36,7 @@ Here we define what kind of inputs our node will expect and what kind of output
"inputs": {
"height": {
"type": "float",
"value": 2,
"value": 2
},
"radius": {
"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:
```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"]
[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" }
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"] }
[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]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[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" }
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"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.34"
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }

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