4 Commits

Author SHA1 Message Date
Max Richter
aea2cbceba feat: updating some things
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 10m47s
2026-01-18 15:39:10 +01:00
Max Richter
cb89b16dd8 chore: cleanup some code 2026-01-18 15:18:11 +01:00
Max Richter
4217621574 feat(ui): remove storybook 2026-01-18 15:17:56 +01:00
Max Richter
22f285eaff chore: pnpm update 2026-01-18 15:17:27 +01:00
202 changed files with 7083 additions and 5577 deletions

View File

@@ -1,70 +0,0 @@
{
"$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
View File

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

1
.gitignore vendored
View File

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

357
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

@@ -1,43 +0,0 @@
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

@@ -10,34 +10,33 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@nodarium/registry": "workspace:*", "@nodarium/registry": "link:../packages/registry",
"@nodarium/ui": "workspace:*", "@nodarium/ui": "link:../packages/ui",
"@nodarium/utils": "workspace:*", "@nodarium/utils": "link:../packages/utils",
"@sveltejs/kit": "^2.50.0", "@sveltejs/kit": "^2.50.0",
"@tailwindcss/vite": "^4.1.18",
"@threlte/core": "8.3.1", "@threlte/core": "8.3.1",
"@threlte/extras": "9.7.1", "@threlte/extras": "9.7.1",
"@types/three": "^0.182.0",
"@unocss/reset": "^66.6.0",
"comlink": "^4.4.2", "comlink": "^4.4.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"idb": "^8.0.3", "idb": "^8.0.3",
"jsondiffpatch": "^0.7.3", "jsondiffpatch": "^0.7.3",
"tailwindcss": "^4.1.18", "three": "^0.182.0"
"three": "^0.182.0",
"wabt": "^1.0.39"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/tabler": "^1.2.26", "@iconify-json/tabler": "^1.2.26",
"@iconify/tailwind4": "^1.2.1", "@nodarium/types": "link:../packages/types",
"@nodarium/types": "workspace:",
"@sveltejs/adapter-static": "^3.0.10", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/vite-plugin-svelte": "^6.2.4", "@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tsconfig/svelte": "^5.0.6", "@tsconfig/svelte": "^5.0.6",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/three": "^0.182.0", "@unocss/preset-icons": "^66.6.0",
"svelte": "^5.46.4", "svelte": "^5.46.4",
"svelte-check": "^4.3.5", "svelte-check": "^4.3.5",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"unocss": "^66.6.0",
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-plugin-comlink": "^5.3.0", "vite-plugin-comlink": "^5.3.0",
"vite-plugin-glsl": "^1.5.5", "vite-plugin-glsl": "^1.5.5",

1576
app/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,10 +31,6 @@
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js"; import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
import { Vector2 } from "three/src/math/Vector2.js"; import { Vector2 } from "three/src/math/Vector2.js";
import { appSettings } from "$lib/settings/app-settings.svelte"; import { appSettings } from "$lib/settings/app-settings.svelte";
import { getGraphState } from "../graph-state.svelte";
import { onDestroy } from "svelte";
const graphState = getGraphState();
type Props = { type Props = {
x1: number; x1: number;
@@ -42,21 +38,20 @@
x2: number; x2: number;
y2: number; y2: number;
z: number; z: number;
id?: string;
}; };
const { x1, y1, x2, y2, z, id }: Props = $props(); const { x1, y1, x2, y2, z }: Props = $props();
const thickness = $derived(Math.max(0.001, 0.00082 * Math.exp(0.055 * z))); const thickness = $derived(Math.max(0.001, 0.00082 * Math.exp(0.055 * z)));
let points = $state<Vector3[]>([]); let points = $state<Vector3[]>([]);
let lastId: string | null = null; let lastId: string | null = null;
const curveId = $derived(`${x1}-${y1}-${x2}-${y2}`);
function update() { function update() {
const new_x = x2 - x1; const new_x = x2 - x1;
const new_y = y2 - y1; const new_y = y2 - y1;
const curveId = `${x1}-${y1}-${x2}-${y2}`;
if (lastId === curveId) { if (lastId === curveId) {
return; return;
} }
@@ -77,15 +72,6 @@
.getPoints(samples) .getPoints(samples)
.map((p) => new Vector3(p.x, 0, p.y)) .map((p) => new Vector3(p.x, 0, p.y))
.flat(); .flat();
if (id) {
graphState.setEdgeGeometry(
id,
x1,
y1,
$state.snapshot(points) as unknown as Vector3[],
);
}
} }
$effect(() => { $effect(() => {
@@ -93,10 +79,6 @@
update(); update();
} }
}); });
onDestroy(() => {
if (id) graphState.removeEdgeGeometry(id);
});
</script> </script>
<T.Mesh <T.Mesh
@@ -119,18 +101,6 @@
<T.CircleGeometry args={[0.5, 16]} /> <T.CircleGeometry args={[0.5, 16]} />
</T.Mesh> </T.Mesh>
{#if graphState.hoveredEdgeId === id}
<T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
<MeshLineGeometry {points} />
<MeshLineMaterial
width={thickness * 5}
color={lineColor}
opacity={0.5}
transparent
/>
</T.Mesh>
{/if}
<T.Mesh position.x={x1} position.z={y1} position.y={0.1}> <T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
<MeshLineGeometry {points} /> <MeshLineGeometry {points} />
<MeshLineMaterial width={thickness} color={lineColor} /> <MeshLineMaterial width={thickness} color={lineColor} />

View File

@@ -1,33 +1,31 @@
import throttle from '$lib/helpers/throttle';
import { RemoteNodeRegistry } from '@nodarium/registry';
import type { import type {
Edge, Edge,
Graph, Graph,
NodeDefinition,
NodeId,
NodeInput,
NodeInstance, NodeInstance,
NodeDefinition,
NodeInput,
NodeRegistry, NodeRegistry,
Socket NodeId,
} from '@nodarium/types'; Socket,
import { fastHashString } from '@nodarium/utils'; } from "@nodarium/types";
import { createLogger } from '@nodarium/utils'; import { fastHashString } from "@nodarium/utils";
import { SvelteMap } from 'svelte/reactivity'; import { SvelteMap } from "svelte/reactivity";
import EventEmitter from './helpers/EventEmitter'; import EventEmitter from "./helpers/EventEmitter";
import { HistoryManager } from './history-manager'; import { createLogger } from "@nodarium/utils";
import throttle from "$lib/helpers/throttle";
import { HistoryManager } from "./history-manager";
const logger = createLogger('graph-manager'); const logger = createLogger("graph-manager");
logger.mute(); logger.mute();
const remoteRegistry = new RemoteNodeRegistry(''); const clone =
"structuredClone" in self
const clone = 'structuredClone' in self ? self.structuredClone
? self.structuredClone : (args: any) => JSON.parse(JSON.stringify(args));
: (args: any) => JSON.parse(JSON.stringify(args));
function areSocketsCompatible( function areSocketsCompatible(
output: string | undefined, output: string | undefined,
inputs: string | (string | undefined)[] | undefined inputs: string | (string | undefined)[] | undefined,
) { ) {
if (Array.isArray(inputs) && output) { if (Array.isArray(inputs) && output) {
return inputs.includes(output); return inputs.includes(output);
@@ -36,23 +34,24 @@ function areSocketsCompatible(
} }
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) { function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
if (firstEdge[0].id !== secondEdge[0].id) { if (firstEdge[0].id !== secondEdge[0].id) {
return false; return false;
} }
if (firstEdge[1] !== secondEdge[1]) { if (firstEdge[1] !== secondEdge[1]) {
return false; return false
} }
if (firstEdge[2].id !== secondEdge[2].id) { if (firstEdge[2].id !== secondEdge[2].id) {
return false; return false
} }
if (firstEdge[3] !== secondEdge[3]) { if (firstEdge[3] !== secondEdge[3]) {
return false; return false
} }
return true; return true
} }
export class GraphManager extends EventEmitter<{ export class GraphManager extends EventEmitter<{
@@ -63,7 +62,7 @@ export class GraphManager extends EventEmitter<{
values: Record<string, unknown>; values: Record<string, unknown>;
}; };
}> { }> {
status = $state<'loading' | 'idle' | 'error'>(); status = $state<"loading" | "idle" | "error">();
loaded = false; loaded = false;
graph: Graph = { id: 0, nodes: [], edges: [] }; graph: Graph = { id: 0, nodes: [], edges: [] };
@@ -89,7 +88,7 @@ export class GraphManager extends EventEmitter<{
history: HistoryManager = new HistoryManager(); history: HistoryManager = new HistoryManager();
execute = throttle(() => { execute = throttle(() => {
if (this.loaded === false) return; if (this.loaded === false) return;
this.emit('result', this.serialize()); this.emit("result", this.serialize());
}, 10); }, 10);
constructor(public registry: NodeRegistry) { constructor(public registry: NodeRegistry) {
@@ -101,22 +100,21 @@ export class GraphManager extends EventEmitter<{
id: node.id, id: node.id,
position: [...node.position], position: [...node.position],
type: node.type, type: node.type,
props: node.props props: node.props,
})) as NodeInstance[]; })) as NodeInstance[];
const edges = this.edges.map((edge) => [ const edges = this.edges.map((edge) => [
edge[0].id, edge[0].id,
edge[1], edge[1],
edge[2].id, edge[2].id,
edge[3] edge[3],
]) as Graph['edges']; ]) as Graph["edges"];
const serialized = { const serialized = {
id: this.graph.id, id: this.graph.id,
settings: $state.snapshot(this.settings), settings: $state.snapshot(this.settings),
meta: $state.snapshot(this.graph.meta),
nodes, nodes,
edges edges,
}; };
logger.log('serializing graph', serialized); logger.log("serializing graph", serialized);
return clone($state.snapshot(serialized)); return clone($state.snapshot(serialized));
} }
@@ -150,97 +148,6 @@ export class GraphManager extends EventEmitter<{
return [...nodes.values()]; return [...nodes.values()];
} }
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][] { getEdgesBetweenNodes(nodes: NodeInstance[]): [number, number, number, string][] {
const edges = []; const edges = [];
for (const node of nodes) { for (const node of nodes) {
@@ -248,14 +155,14 @@ export class GraphManager extends EventEmitter<{
for (const child of children) { for (const child of children) {
if (nodes.includes(child)) { if (nodes.includes(child)) {
const edge = this.edges.find( const edge = this.edges.find(
(e) => e[0].id === node.id && e[2].id === child.id (e) => e[0].id === node.id && e[2].id === child.id,
); );
if (edge) { if (edge) {
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [ edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [
number, number,
number, number,
number, number,
string string,
]); ]);
} }
} }
@@ -272,18 +179,18 @@ export class GraphManager extends EventEmitter<{
const n = node as NodeInstance; const n = node as NodeInstance;
if (nodeType) { if (nodeType) {
n.state = { n.state = {
type: nodeType type: nodeType,
}; };
} }
return [node.id, n]; return [node.id, n];
}) }),
); );
this.edges = graph.edges.map((edge) => { const edges = graph.edges.map((edge) => {
const from = nodes.get(edge[0]); const from = nodes.get(edge[0]);
const to = nodes.get(edge[2]); const to = nodes.get(edge[2]);
if (!from || !to) { if (!from || !to) {
throw new Error('Edge references non-existing node'); throw new Error("Edge references non-existing node");
} }
from.state.children = from.state.children || []; from.state.children = from.state.children || [];
from.state.children.push(to); from.state.children.push(to);
@@ -292,6 +199,8 @@ export class GraphManager extends EventEmitter<{
return [from, edge[1], to, edge[3]] as Edge; return [from, edge[1], to, edge[3]] as Edge;
}); });
this.edges = [...edges];
this.nodes.clear(); this.nodes.clear();
for (const [id, node] of nodes) { for (const [id, node] of nodes) {
this.nodes.set(id, node); this.nodes.set(id, node);
@@ -305,36 +214,21 @@ export class GraphManager extends EventEmitter<{
this.loaded = false; this.loaded = false;
this.graph = graph; this.graph = graph;
this.status = 'loading'; this.status = "loading";
this.id = graph.id; this.id = graph.id;
logger.info('loading graph', { nodes: graph.nodes, edges: graph.edges, id: graph.id }); logger.info("loading graph", $state.snapshot(graph));
const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)])); const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)]));
await this.registry.load(nodeIds); await this.registry.load(nodeIds);
// Fetch all nodes from all collections of the loaded nodes logger.info("loaded node types", this.registry.getAllNodes());
const allCollections = new Set<`${string}/${string}`>();
for (const id of nodeIds) {
const [user, collection] = id.split('/');
allCollections.add(`${user}/${collection}`);
}
for (const collection of allCollections) {
remoteRegistry
.fetchCollection(collection)
.then((collection: { nodes: { id: NodeId }[] }) => {
const ids = collection.nodes.map((n) => n.id);
return this.registry.load(ids);
});
}
logger.info('loaded node types', this.registry.getAllNodes());
for (const node of this.graph.nodes) { for (const node of this.graph.nodes) {
const nodeType = this.registry.getNode(node.type); const nodeType = this.registry.getNode(node.type);
if (!nodeType) { if (!nodeType) {
logger.error(`Node type not found: ${node.type}`); logger.error(`Node type not found: ${node.type}`);
this.status = 'error'; this.status = "error";
return; return;
} }
// Turn into runtime node // Turn into runtime node
@@ -359,11 +253,11 @@ export class GraphManager extends EventEmitter<{
settingTypes[settingId] = { settingTypes[settingId] = {
__node_type: type.id, __node_type: type.id,
__node_input: key, __node_input: key,
...type.inputs[key] ...type.inputs[key],
}; };
if ( if (
settingValues[settingId] === undefined settingValues[settingId] === undefined &&
&& 'value' in type.inputs[key] "value" in type.inputs[key]
) { ) {
settingValues[settingId] = type.inputs[key].value; settingValues[settingId] = type.inputs[key].value;
} }
@@ -373,14 +267,14 @@ export class GraphManager extends EventEmitter<{
} }
this.settings = settingValues; this.settings = settingValues;
this.emit('settings', { types: settingTypes, values: settingValues }); this.emit("settings", { types: settingTypes, values: settingValues });
this.history.reset(); this.history.reset();
this._init(this.graph); this._init(this.graph);
this.save(); this.save();
this.status = 'idle'; this.status = "idle";
this.loaded = true; this.loaded = true;
logger.log(`Graph loaded in ${performance.now() - a}ms`); logger.log(`Graph loaded in ${performance.now() - a}ms`);
@@ -413,9 +307,9 @@ export class GraphManager extends EventEmitter<{
if (settingId) { if (settingId) {
settingTypes[settingId] = nodeType.inputs[key]; settingTypes[settingId] = nodeType.inputs[key];
if ( if (
settingValues settingValues &&
&& settingValues?.[settingId] === undefined settingValues?.[settingId] === undefined &&
&& 'value' in nodeType.inputs[key] "value" in nodeType.inputs[key]
) { ) {
settingValues[settingId] = nodeType.inputs[key].value; settingValues[settingId] = nodeType.inputs[key].value;
} }
@@ -425,7 +319,7 @@ export class GraphManager extends EventEmitter<{
this.settings = settingValues; this.settings = settingValues;
this.settingTypes = settingTypes; this.settingTypes = settingTypes;
this.emit('settings', { types: settingTypes, values: settingValues }); this.emit("settings", { types: settingTypes, values: settingValues });
} }
getChildren(node: NodeInstance) { getChildren(node: NodeInstance) {
@@ -474,7 +368,7 @@ export class GraphManager extends EventEmitter<{
const inputType = to?.state?.type?.inputs?.[toSocket]?.type; const inputType = to?.state?.type?.inputs?.[toSocket]?.type;
if (outputType === inputType) { if (outputType === inputType) {
this.createEdge(from, fromSocket, to, toSocket, { this.createEdge(from, fromSocket, to, toSocket, {
applyUpdate: false applyUpdate: false,
}); });
continue; continue;
} }
@@ -509,7 +403,7 @@ export class GraphManager extends EventEmitter<{
// map old ids to new ids // map old ids to new ids
const idMap = new Map<number, number>(); const idMap = new Map<number, number>();
let startId = this.createNodeId(); let startId = this.createNodeId()
nodes = nodes.map((node) => { nodes = nodes.map((node) => {
const id = startId++; const id = startId++;
@@ -526,7 +420,7 @@ export class GraphManager extends EventEmitter<{
const to = nodes.find((n) => n.id === idMap.get(edge[2])); const to = nodes.find((n) => n.id === idMap.get(edge[2]));
if (!from || !to) { if (!from || !to) {
throw new Error('Edge references non-existing node'); throw new Error("Edge references non-existing node");
} }
to.state.parents = to.state.parents || []; to.state.parents = to.state.parents || [];
@@ -551,11 +445,11 @@ export class GraphManager extends EventEmitter<{
createNode({ createNode({
type, type,
position, position,
props = {} props = {},
}: { }: {
type: NodeInstance['type']; type: NodeInstance["type"];
position: NodeInstance['position']; position: NodeInstance["position"];
props: NodeInstance['props']; props: NodeInstance["props"];
}) { }) {
const nodeType = this.registry.getNode(type); const nodeType = this.registry.getNode(type);
if (!nodeType) { if (!nodeType) {
@@ -568,14 +462,14 @@ export class GraphManager extends EventEmitter<{
type, type,
position, position,
state: { type: nodeType }, state: { type: nodeType },
props props,
}); });
this.nodes.set(node.id, node); this.nodes.set(node.id, node);
this.save(); this.save();
return node; return node
} }
createEdge( createEdge(
@@ -583,16 +477,17 @@ export class GraphManager extends EventEmitter<{
fromSocket: number, fromSocket: number,
to: NodeInstance, to: NodeInstance,
toSocket: string, toSocket: string,
{ applyUpdate = true } = {} { applyUpdate = true } = {},
): Edge | undefined { ): Edge | undefined {
const existingEdges = this.getEdgesToNode(to); const existingEdges = this.getEdgesToNode(to);
// check if this exact edge already exists // check if this exact edge already exists
const existingEdge = existingEdges.find( const existingEdge = existingEdges.find(
(e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket (e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket,
); );
if (existingEdge) { if (existingEdge) {
logger.error('Edge already exists', existingEdge); logger.error("Edge already exists", existingEdge);
return; return;
} }
@@ -605,13 +500,13 @@ export class GraphManager extends EventEmitter<{
if (!areSocketsCompatible(fromSocketType, toSocketType)) { if (!areSocketsCompatible(fromSocketType, toSocketType)) {
logger.error( logger.error(
`Socket types do not match: ${fromSocketType} !== ${toSocketType}` `Socket types do not match: ${fromSocketType} !== ${toSocketType}`,
); );
return; return;
} }
const edgeToBeReplaced = this.edges.find( const edgeToBeReplaced = this.edges.find(
(e) => e[2].id === to.id && e[3] === toSocket (e) => e[2].id === to.id && e[3] === toSocket,
); );
if (edgeToBeReplaced) { if (edgeToBeReplaced) {
this.removeEdge(edgeToBeReplaced, { applyDeletion: false }); this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
@@ -638,7 +533,7 @@ export class GraphManager extends EventEmitter<{
const nextState = this.history.undo(); const nextState = this.history.undo();
if (nextState) { if (nextState) {
this._init(nextState); this._init(nextState);
this.emit('save', this.serialize()); this.emit("save", this.serialize());
} }
} }
@@ -646,7 +541,7 @@ export class GraphManager extends EventEmitter<{
const nextState = this.history.redo(); const nextState = this.history.redo();
if (nextState) { if (nextState) {
this._init(nextState); this._init(nextState);
this.emit('save', this.serialize()); this.emit("save", this.serialize());
} }
} }
@@ -663,15 +558,8 @@ export class GraphManager extends EventEmitter<{
if (this.currentUndoGroup) return; if (this.currentUndoGroup) return;
const state = this.serialize(); const state = this.serialize();
this.history.save(state); this.history.save(state);
this.emit("save", state);
// This is some stupid race condition where the graph-manager emits a save event logger.log("saving graphs", state);
// 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: NodeInstance) { getParentsOfNode(node: NodeInstance) {
@@ -679,7 +567,7 @@ export class GraphManager extends EventEmitter<{
const stack = node.state?.parents?.slice(0); const stack = node.state?.parents?.slice(0);
while (stack?.length) { while (stack?.length) {
if (parents.length > 1000000) { if (parents.length > 1000000) {
logger.warn('Infinite loop detected'); logger.warn("Infinite loop detected");
break; break;
} }
const parent = stack.pop(); const parent = stack.pop();
@@ -698,28 +586,26 @@ export class GraphManager extends EventEmitter<{
return []; return [];
} }
const definitions = typeof socket.index === 'string' const definitions = typeof socket.index === "string"
? allDefinitions.filter(s => { ? allDefinitions.filter(s => {
return s.outputs?.find(_s => return s.outputs?.find(_s => Object
Object .values(nodeType?.inputs || {})
.values(nodeType?.inputs || {}) .map(s => s.type)
.map(s => s.type) .includes(_s as NodeInput["type"])
.includes(_s as NodeInput['type']) )
);
}) })
: allDefinitions.filter(s => : allDefinitions.filter(s => Object
Object .values(s.inputs ?? {})
.values(s.inputs ?? {}) .find(s => {
.find(s => { if (s.hidden) return false;
if (s.hidden) return false; if (nodeType.outputs?.includes(s.type)) {
if (nodeType.outputs?.includes(s.type)) { return true
return true; }
} return s.accepts?.find(a => nodeType.outputs?.includes(a))
return s.accepts?.find(a => nodeType.outputs?.includes(a)); }))
})
); return definitions
return definitions;
} }
getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] { getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
@@ -729,11 +615,11 @@ export class GraphManager extends EventEmitter<{
const sockets: [NodeInstance, string | number][] = []; const sockets: [NodeInstance, string | number][] = [];
// if index is a string, we are an input looking for outputs // if index is a string, we are an input looking for outputs
if (typeof index === 'string') { if (typeof index === "string") {
// filter out self and child nodes // filter out self and child nodes
const children = new Set(this.getChildren(node).map((n) => n.id)); const children = new Set(this.getChildren(node).map((n) => n.id));
const nodes = this.getAllNodes().filter( const nodes = this.getAllNodes().filter(
(n) => n.id !== node.id && !children.has(n.id) (n) => n.id !== node.id && !children.has(n.id),
); );
const ownType = nodeType?.inputs?.[index].type; const ownType = nodeType?.inputs?.[index].type;
@@ -748,20 +634,20 @@ export class GraphManager extends EventEmitter<{
} }
} }
} }
} else if (typeof index === 'number') { } else if (typeof index === "number") {
// if index is a number, we are an output looking for inputs // if index is a number, we are an output looking for inputs
// filter out self and parent nodes // filter out self and parent nodes
const parents = new Set(this.getParentsOfNode(node).map((n) => n.id)); const parents = new Set(this.getParentsOfNode(node).map((n) => n.id));
const nodes = this.getAllNodes().filter( const nodes = this.getAllNodes().filter(
(n) => n.id !== node.id && !parents.has(n.id) (n) => n.id !== node.id && !parents.has(n.id),
); );
// get edges from this socket // get edges from this socket
const edges = new Map( const edges = new Map(
this.getEdgesFromNode(node) this.getEdgesFromNode(node)
.filter((e) => e[1] === index) .filter((e) => e[1] === index)
.map((e) => [e[2].id, e[3]]) .map((e) => [e[2].id, e[3]]),
); );
const ownType = nodeType.outputs?.[index]; const ownType = nodeType.outputs?.[index];
@@ -774,8 +660,8 @@ export class GraphManager extends EventEmitter<{
otherType.push(...(inputs[key].accepts || [])); otherType.push(...(inputs[key].accepts || []));
if ( if (
areSocketsCompatible(ownType, otherType) areSocketsCompatible(ownType, otherType) &&
&& edges.get(node.id) !== key edges.get(node.id) !== key
) { ) {
sockets.push([node, key]); sockets.push([node, key]);
} }
@@ -788,7 +674,7 @@ export class GraphManager extends EventEmitter<{
removeEdge( removeEdge(
edge: Edge, edge: Edge,
{ applyDeletion = true }: { applyDeletion?: boolean } = {} { applyDeletion = true }: { applyDeletion?: boolean } = {},
) { ) {
const id0 = edge[0].id; const id0 = edge[0].id;
const sid0 = edge[1]; const sid0 = edge[1];
@@ -796,20 +682,21 @@ export class GraphManager extends EventEmitter<{
const sid2 = edge[3]; const sid2 = edge[3];
const _edge = this.edges.find( const _edge = this.edges.find(
(e) => e[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; if (!_edge) return;
if (edge[0].state.children) { if (edge[0].state.children) {
edge[0].state.children = edge[0].state.children.filter( edge[0].state.children = edge[0].state.children.filter(
(n: NodeInstance) => n.id !== id2 (n: NodeInstance) => n.id !== id2,
); );
} }
if (edge[2].state.parents) { if (edge[2].state.parents) {
edge[2].state.parents = edge[2].state.parents.filter( edge[2].state.parents = edge[2].state.parents.filter(
(n: NodeInstance) => n.id !== id0 (n: NodeInstance) => n.id !== id0,
); );
} }
@@ -818,6 +705,7 @@ export class GraphManager extends EventEmitter<{
this.execute(); this.execute();
this.save(); this.save();
} }
} }
getEdgesToNode(node: NodeInstance) { getEdgesToNode(node: NodeInstance) {

View File

@@ -1,43 +1,36 @@
import type { NodeInstance, Socket } from '@nodarium/types'; import type { NodeInstance, Socket } from "@nodarium/types";
import { getContext, setContext } from 'svelte'; import { getContext, setContext } from "svelte";
import { SvelteSet } from 'svelte/reactivity'; import { SvelteSet } from "svelte/reactivity";
import type { OrthographicCamera, Vector3 } from 'three'; import type { GraphManager } from "./graph-manager.svelte";
import type { GraphManager } from './graph-manager.svelte'; import type { OrthographicCamera } from "three";
const graphStateKey = Symbol('graph-state');
const graphStateKey = Symbol("graph-state");
export function getGraphState() { export function getGraphState() {
return getContext<GraphState>(graphStateKey); return getContext<GraphState>(graphStateKey);
} }
export function setGraphState(graphState: GraphState) { export function setGraphState(graphState: GraphState) {
return setContext(graphStateKey, graphState); return setContext(graphStateKey, graphState)
} }
const graphManagerKey = Symbol('graph-manager'); const graphManagerKey = Symbol("graph-manager");
export function getGraphManager() { export function getGraphManager() {
return getContext<GraphManager>(graphManagerKey); return getContext<GraphManager>(graphManagerKey)
} }
export function setGraphManager(manager: GraphManager) { export function setGraphManager(manager: GraphManager) {
return setContext(graphManagerKey, manager); return setContext(graphManagerKey, manager);
} }
type EdgeData = {
x1: number;
y1: number;
points: Vector3[];
};
export class GraphState { export class GraphState {
constructor(private graph: GraphManager) { constructor(private graph: GraphManager) {
$effect.root(() => { $effect.root(() => {
$effect(() => { $effect(() => {
localStorage.setItem( localStorage.setItem("cameraPosition", `[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]`)
'cameraPosition', })
`[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]` })
); const storedPosition = localStorage.getItem("cameraPosition")
});
});
const storedPosition = localStorage.getItem('cameraPosition');
if (storedPosition) { if (storedPosition) {
try { try {
const d = JSON.parse(storedPosition); const d = JSON.parse(storedPosition);
@@ -45,7 +38,7 @@ export class GraphState {
this.cameraPosition[1] = d[1]; this.cameraPosition[1] = d[1];
this.cameraPosition[2] = d[2]; this.cameraPosition[2] = d[2];
} catch (e) { } catch (e) {
console.log('Failed to parsed stored camera position', e); console.log("Failed to parsed stored camera position", e);
} }
} }
} }
@@ -53,18 +46,13 @@ export class GraphState {
width = $state(100); width = $state(100);
height = $state(100); height = $state(100);
hoveredEdgeId = $state<string | null>(null);
edges = new Map<string, EdgeData>();
wrapper = $state<HTMLDivElement>(null!); wrapper = $state<HTMLDivElement>(null!);
rect: DOMRect = $derived( rect: DOMRect = $derived(
(this.wrapper && this.width && this.height) (this.wrapper && this.width && this.height) ? this.wrapper.getBoundingClientRect() : new DOMRect(0, 0, 0, 0),
? this.wrapper.getBoundingClientRect()
: new DOMRect(0, 0, 0, 0)
); );
camera = $state<OrthographicCamera>(null!); camera = $state<OrthographicCamera>(null!);
cameraPosition: [number, number, number] = $state([140, 100, 3.5]); cameraPosition: [number, number, number] = $state([0, 0, 4]);
clipboard: null | { clipboard: null | {
nodes: NodeInstance[]; nodes: NodeInstance[];
@@ -75,7 +63,7 @@ export class GraphState {
this.cameraPosition[0] - this.width / this.cameraPosition[2] / 2, this.cameraPosition[0] - this.width / this.cameraPosition[2] / 2,
this.cameraPosition[0] + this.width / this.cameraPosition[2] / 2, this.cameraPosition[0] + this.width / this.cameraPosition[2] / 2,
this.cameraPosition[1] - this.height / this.cameraPosition[2] / 2, this.cameraPosition[1] - this.height / this.cameraPosition[2] / 2,
this.cameraPosition[1] + this.height / this.cameraPosition[2] / 2 this.cameraPosition[1] + this.height / this.cameraPosition[2] / 2,
]); ]);
boxSelection = $state(false); boxSelection = $state(false);
@@ -83,8 +71,8 @@ export class GraphState {
addMenuPosition = $state<[number, number] | null>(null); addMenuPosition = $state<[number, number] | null>(null);
snapToGrid = $state(false); snapToGrid = $state(false);
showGrid = $state(true); showGrid = $state(true)
showHelp = $state(false); showHelp = $state(false)
cameraDown = [0, 0]; cameraDown = [0, 0];
mouseDownNodeId = -1; mouseDownNodeId = -1;
@@ -100,49 +88,33 @@ export class GraphState {
hoveredSocket = $state<Socket | null>(null); hoveredSocket = $state<Socket | null>(null);
possibleSockets = $state<Socket[]>([]); possibleSockets = $state<Socket[]>([]);
possibleSocketIds = $derived( possibleSocketIds = $derived(
new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`)) new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`)),
); );
getEdges() {
return $state.snapshot(this.edges);
}
clearSelection() { clearSelection() {
this.selectedNodes.clear(); this.selectedNodes.clear();
} }
isBodyFocused = () => document?.activeElement?.nodeName !== 'INPUT'; isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT";
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) { updateNodePosition(node: NodeInstance) {
if ( if (
node.state.x === node.position[0] node.state.x === node.position[0] &&
&& node.state.y === node.position[1] node.state.y === node.position[1]
) { ) {
delete node.state.x; delete node.state.x;
delete node.state.y; delete node.state.y;
} }
if (node.state['x'] !== undefined && node.state['y'] !== undefined) { if (node.state["x"] !== undefined && node.state["y"] !== undefined) {
if (node.state.ref) { if (node.state.ref) {
node.state.ref.style.setProperty('--nx', `${node.state.x * 10}px`); node.state.ref.style.setProperty("--nx", `${node.state.x * 10}px`);
node.state.ref.style.setProperty('--ny', `${node.state.y * 10}px`); node.state.ref.style.setProperty("--ny", `${node.state.y * 10}px`);
} }
} else { } else {
if (node.state.ref) { if (node.state.ref) {
node.state.ref.style.setProperty('--nx', `${node.position[0] * 10}px`); node.state.ref.style.setProperty("--nx", `${node.position[0] * 10}px`);
node.state.ref.style.setProperty('--ny', `${node.position[1] * 10}px`); node.state.ref.style.setProperty("--ny", `${node.position[1] * 10}px`);
} }
} }
} }
@@ -162,18 +134,18 @@ export class GraphState {
getSocketPosition( getSocketPosition(
node: NodeInstance, node: NodeInstance,
index: string | number index: string | number,
): [number, number] { ): [number, number] {
if (typeof index === 'number') { if (typeof index === "number") {
return [ return [
(node?.state?.x ?? node.position[0]) + 20, (node?.state?.x ?? node.position[0]) + 20,
(node?.state?.y ?? node.position[1]) + 2.5 + 10 * index (node?.state?.y ?? node.position[1]) + 2.5 + 10 * index,
]; ];
} else { } else {
const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index); const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index);
return [ return [
node?.state?.x ?? node.position[0], node?.state?.x ?? node.position[0],
(node?.state?.y ?? node.position[1]) + 10 + 10 * _index (node?.state?.y ?? node.position[1]) + 10 + 10 * _index,
]; ];
} }
} }
@@ -187,26 +159,26 @@ export class GraphState {
if (!node?.inputs) { if (!node?.inputs) {
return 5; return 5;
} }
const height = 5 const height =
+ 10 5 +
* Object.keys(node.inputs).filter( 10 *
(p) => Object.keys(node.inputs).filter(
p !== 'seed' (p) =>
&& node?.inputs p !== "seed" &&
&& !('setting' in node?.inputs?.[p]) node?.inputs &&
&& node.inputs[p].hidden !== true !("setting" in node?.inputs?.[p]) &&
).length; node.inputs[p].hidden !== true,
).length;
this.nodeHeightCache[nodeTypeId] = height; this.nodeHeightCache[nodeTypeId] = height;
return height; return height;
} }
copyNodes() { copyNodes() {
if (this.activeNodeId === -1 && !this.selectedNodes?.size) { if (this.activeNodeId === -1 && !this.selectedNodes?.size)
return; return;
}
let nodes = [ let nodes = [
this.activeNodeId, this.activeNodeId,
...(this.selectedNodes?.values() || []) ...(this.selectedNodes?.values() || []),
] ]
.map((id) => this.graph.getNode(id)) .map((id) => this.graph.getNode(id))
.filter(b => !!b); .filter(b => !!b);
@@ -216,14 +188,14 @@ export class GraphState {
...node, ...node,
position: [ position: [
this.mousePosition[0] - node.position[0], this.mousePosition[0] - node.position[0],
this.mousePosition[1] - node.position[1] this.mousePosition[1] - node.position[1],
], ],
tmp: undefined tmp: undefined,
})); }));
this.clipboard = { this.clipboard = {
nodes: nodes, nodes: nodes,
edges: edges edges: edges,
}; };
} }
@@ -245,13 +217,14 @@ export class GraphState {
} }
} }
setDownSocket(socket: Socket) { setDownSocket(socket: Socket) {
this.activeSocket = socket; this.activeSocket = socket;
let { node, index, position } = socket; let { node, index, position } = socket;
// remove existing edge // remove existing edge
if (typeof index === 'string') { if (typeof index === "string") {
const edges = this.graph.getEdgesToNode(node); const edges = this.graph.getEdgesToNode(node);
for (const edge of edges) { for (const edge of edges) {
if (edge[3] === index) { if (edge[3] === index) {
@@ -268,7 +241,7 @@ export class GraphState {
this.activeSocket = { this.activeSocket = {
node, node,
index, index,
position position,
}; };
this.possibleSockets = this.graph this.possibleSockets = this.graph
@@ -277,17 +250,18 @@ export class GraphState {
return { return {
node, node,
index, index,
position: this.getSocketPosition(node, index) position: this.getSocketPosition(node, index),
}; };
}); });
} };
projectScreenToWorld(x: number, y: number): [number, number] { projectScreenToWorld(x: number, y: number): [number, number] {
return [ return [
this.cameraPosition[0] this.cameraPosition[0] +
+ (x - this.width / 2) / this.cameraPosition[2], (x - this.width / 2) / this.cameraPosition[2],
this.cameraPosition[1] this.cameraPosition[1] +
+ (y - this.height / 2) / this.cameraPosition[2] (y - this.height / 2) / this.cameraPosition[2],
]; ];
} }
@@ -300,8 +274,8 @@ export class GraphState {
if (event.button === 0) { if (event.button === 0) {
// check if the clicked element is a node // check if the clicked element is a node
if (event.target instanceof HTMLElement) { if (event.target instanceof HTMLElement) {
const nodeElement = event.target.closest('.node'); const nodeElement = event.target.closest(".node");
const nodeId = nodeElement?.getAttribute?.('data-node-id'); const nodeId = nodeElement?.getAttribute?.("data-node-id");
if (nodeId) { if (nodeId) {
clickedNodeId = parseInt(nodeId, 10); clickedNodeId = parseInt(nodeId, 10);
} }
@@ -329,14 +303,10 @@ export class GraphState {
const height = this.getNodeHeight(node.type); const height = this.getNodeHeight(node.type);
const width = 20; const width = 20;
return ( return (
node.position[0] > this.cameraBounds[0] - width node.position[0] > this.cameraBounds[0] - width &&
&& node.position[0] < this.cameraBounds[1] node.position[0] < this.cameraBounds[1] &&
&& node.position[1] > this.cameraBounds[2] - height node.position[1] > this.cameraBounds[2] - height &&
&& node.position[1] < this.cameraBounds[3] node.position[1] < this.cameraBounds[3]
); );
} };
openNodePalette() {
this.addMenuPosition = [this.mousePosition[0], this.mousePosition[1]];
}
} }

View File

@@ -1,23 +1,21 @@
<script lang="ts"> <script lang="ts">
import type { Edge, NodeInstance } from '@nodarium/types'; import type { Edge, NodeInstance } from "@nodarium/types";
import { Canvas } from '@threlte/core'; import { createKeyMap } from "../../helpers/createKeyMap";
import { HTML } from '@threlte/extras'; import AddMenu from "../components/AddMenu.svelte";
import { createKeyMap } from '../../helpers/createKeyMap'; import Background from "../background/Background.svelte";
import Background from '../background/Background.svelte'; import BoxSelection from "../components/BoxSelection.svelte";
import AddMenu from '../components/AddMenu.svelte'; import EdgeEl from "../edges/Edge.svelte";
import BoxSelection from '../components/BoxSelection.svelte'; import NodeEl from "../node/Node.svelte";
import Camera from '../components/Camera.svelte'; import Camera from "../components/Camera.svelte";
import HelpView from '../components/HelpView.svelte'; import { Canvas } from "@threlte/core";
import Debug from '../debug/Debug.svelte'; import HelpView from "../components/HelpView.svelte";
import EdgeEl from '../edges/Edge.svelte'; import { getGraphManager, getGraphState } from "../graph-state.svelte";
import { getGraphManager, getGraphState } from '../graph-state.svelte'; import { HTML } from "@threlte/extras";
import NodeEl from '../node/Node.svelte'; import { FileDropEventManager, MouseEventManager } from "./events";
import { maxZoom, minZoom } from './constants'; import { maxZoom, minZoom } from "./constants";
import { FileDropEventManager } from './drop.events';
import { MouseEventManager } from './mouse.events';
const { const {
keymap keymap,
}: { }: {
keymap: ReturnType<typeof createKeyMap>; keymap: ReturnType<typeof createKeyMap>;
} = $props(); } = $props();
@@ -45,18 +43,19 @@
const newNode = graph.createNode({ const newNode = graph.createNode({
type: node.type, type: node.type,
position: node.position, position: node.position,
props: node.props props: node.props,
}); });
if (!newNode) return; if (!newNode) return;
if (graphState.activeSocket) { if (graphState.activeSocket) {
if (typeof graphState.activeSocket.index === 'number') { if (typeof graphState.activeSocket.index === "number") {
const socketType = graphState.activeSocket.node.state?.type?.outputs?.[ const socketType =
graphState.activeSocket.index graphState.activeSocket.node.state?.type?.outputs?.[
]; graphState.activeSocket.index
];
const input = Object.entries(newNode?.state?.type?.inputs || {}).find( const input = Object.entries(newNode?.state?.type?.inputs || {}).find(
(inp) => inp[1].type === socketType (inp) => inp[1].type === socketType,
); );
if (input) { if (input) {
@@ -64,13 +63,14 @@
graphState.activeSocket.node, graphState.activeSocket.node,
graphState.activeSocket.index, graphState.activeSocket.index,
newNode, newNode,
input[0] input[0],
); );
} }
} else { } else {
const socketType = graphState.activeSocket.node.state?.type?.inputs?.[ const socketType =
graphState.activeSocket.index graphState.activeSocket.node.state?.type?.inputs?.[
]; graphState.activeSocket.index
];
const output = newNode.state?.type?.outputs?.find((out) => { const output = newNode.state?.type?.outputs?.find((out) => {
if (socketType?.type === out) return true; if (socketType?.type === out) return true;
@@ -83,7 +83,7 @@
newNode, newNode,
output.indexOf(output), output.indexOf(output),
graphState.activeSocket.node, graphState.activeSocket.node,
graphState.activeSocket.index graphState.activeSocket.index,
); );
} }
} }
@@ -95,15 +95,14 @@
</script> </script>
<svelte:window <svelte:window
onmousemove={(ev) => mouseEvents.handleWindowMouseMove(ev)} onmousemove={(ev) => mouseEvents.handleMouseMove(ev)}
onmouseup={(ev) => mouseEvents.handleWindowMouseUp(ev)} onmouseup={(ev) => mouseEvents.handleMouseUp(ev)}
/> />
<div <div
onwheel={(ev) => mouseEvents.handleMouseScroll(ev)} onwheel={(ev) => mouseEvents.handleMouseScroll(ev)}
bind:this={graphState.wrapper} bind:this={graphState.wrapper}
class="graph-wrapper" class="graph-wrapper"
style="height: 100%"
class:is-panning={graphState.isPanning} class:is-panning={graphState.isPanning}
class:is-hovering={graphState.hoveredNodeId !== -1} class:is-hovering={graphState.hoveredNodeId !== -1}
aria-label="Graph" aria-label="Graph"
@@ -113,7 +112,6 @@
bind:clientHeight={graphState.height} bind:clientHeight={graphState.height}
onkeydown={(ev) => keymap.handleKeyboardEvent(ev)} onkeydown={(ev) => keymap.handleKeyboardEvent(ev)}
onmousedown={(ev) => mouseEvents.handleMouseDown(ev)} onmousedown={(ev) => mouseEvents.handleMouseDown(ev)}
oncontextmenu={(ev) => mouseEvents.handleContextMenu(ev)}
{...fileDropEvents.getEventListenerProps()} {...fileDropEvents.getEventListenerProps()}
> >
<input <input
@@ -146,18 +144,20 @@
<BoxSelection <BoxSelection
cameraPosition={graphState.cameraPosition} cameraPosition={graphState.cameraPosition}
p1={{ p1={{
x: graphState.cameraPosition[0] x:
+ (graphState.mouseDown[0] - graphState.width / 2) graphState.cameraPosition[0] +
/ graphState.cameraPosition[2], (graphState.mouseDown[0] - graphState.width / 2) /
y: graphState.cameraPosition[1] graphState.cameraPosition[2],
+ (graphState.mouseDown[1] - graphState.height / 2) y:
/ graphState.cameraPosition[2] graphState.cameraPosition[1] +
(graphState.mouseDown[1] - graphState.height / 2) /
graphState.cameraPosition[2],
}} }}
p2={{ x: graphState.mousePosition[0], y: graphState.mousePosition[1] }} p2={{ x: graphState.mousePosition[0], y: graphState.mousePosition[1] }}
/> />
{/if} {/if}
{#if graph.status === 'idle'} {#if graph.status === "idle"}
{#if graphState.addMenuPosition} {#if graphState.addMenuPosition}
<AddMenu onnode={handleNodeCreation} /> <AddMenu onnode={handleNodeCreation} />
{/if} {/if}
@@ -174,23 +174,12 @@
{#each graph.edges as edge} {#each graph.edges as edge}
{@const [x1, y1, x2, y2] = getEdgePosition(edge)} {@const [x1, y1, x2, y2] = getEdgePosition(edge)}
<EdgeEl <EdgeEl z={graphState.cameraPosition[2]} {x1} {y1} {x2} {y2} />
id={graph.getEdgeId(edge)}
z={graphState.cameraPosition[2]}
{x1}
{y1}
{x2}
{y2}
/>
{/each} {/each}
<Debug /> <HTML>
<HTML transform={false}>
<div <div
role="tree"
id="graph" id="graph"
tabindex="0"
class="wrapper" class="wrapper"
style:transform={`scale(${graphState.cameraPosition[2] * 0.1})`} style:transform={`scale(${graphState.cameraPosition[2] * 0.1})`}
class:hovering-sockets={graphState.activeSocket} class:hovering-sockets={graphState.activeSocket}
@@ -204,9 +193,9 @@
{/each} {/each}
</div> </div>
</HTML> </HTML>
{:else if graph.status === 'loading'} {:else if graph.status === "loading"}
<span>Loading</span> <span>Loading</span>
{:else if graph.status === 'error'} {:else if graph.status === "error"}
<span>Error</span> <span>Error</span>
{/if} {/if}
</Canvas> </Canvas>

View File

@@ -11,7 +11,7 @@
import { setupKeymaps } from "../keymaps"; import { setupKeymaps } from "../keymaps";
type Props = { type Props = {
graph?: Graph; graph: Graph;
registry: NodeRegistry; registry: NodeRegistry;
settings?: Record<string, any>; settings?: Record<string, any>;
@@ -70,6 +70,12 @@
} }
}); });
$effect(() => {
if (settingTypes && settings) {
manager.setSettings(settings);
}
});
manager.on("settings", (_settings) => { manager.on("settings", (_settings) => {
settingTypes = { ...settingTypes, ..._settings.types }; settingTypes = { ...settingTypes, ..._settings.types };
settings = _settings.values; settings = _settings.values;
@@ -79,11 +85,7 @@
manager.on("save", (save) => onsave?.(save)); manager.on("save", (save) => onsave?.(save));
$effect(() => { manager.load(graph);
if (graph) {
manager.load(graph);
}
});
</script> </script>
<GraphEl {keymap} /> <GraphEl {keymap} />

View File

@@ -1,107 +0,0 @@
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

@@ -1,110 +0,0 @@
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

@@ -1,23 +1,127 @@
import { animate, lerp } from '$lib/helpers'; import { GraphSchema, type NodeId, type NodeInstance } from "@nodarium/types";
import { type NodeInstance } from '@nodarium/types'; import type { GraphManager } from "../graph-manager.svelte";
import type { GraphManager } from '../graph-manager.svelte'; import type { GraphState } from "../graph-state.svelte";
import { type GraphState } from '../graph-state.svelte'; import { animate, lerp } from "$lib/helpers";
import { snapToGrid as snapPointToGrid } from '../helpers'; import { snapToGrid as snapPointToGrid } from "../helpers";
import { maxZoom, minZoom, zoomSpeed } from './constants'; import { maxZoom, minZoom, zoomSpeed } from "./constants";
import { EdgeInteractionManager } from './edge.events';
export class MouseEventManager {
edgeInteractionManager: EdgeInteractionManager; export class FileDropEventManager {
constructor( constructor(
private graph: GraphManager, private graph: GraphManager,
private state: GraphState private state: GraphState
) { ) { }
this.edgeInteractionManager = new EdgeInteractionManager(graph, state);
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);
}
}
} }
handleWindowMouseUp(event: MouseEvent) { handleMouseLeave() {
this.edgeInteractionManager.handleMouseUp(); 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(),
}
}
}
export class MouseEventManager {
constructor(
private graph: GraphManager,
private state: GraphState
) { }
handleMouseUp(event: MouseEvent) {
this.state.isPanning = false; this.state.isPanning = false;
if (!this.state.mouseDown) return; if (!this.state.mouseDown) return;
@@ -41,23 +145,25 @@ export class MouseEventManager {
const snapLevel = this.state.getSnapLevel(); const snapLevel = this.state.getSnapLevel();
activeNode.position[0] = snapPointToGrid( activeNode.position[0] = snapPointToGrid(
activeNode?.state?.x ?? activeNode.position[0], activeNode?.state?.x ?? activeNode.position[0],
5 / snapLevel 5 / snapLevel,
); );
activeNode.position[1] = snapPointToGrid( activeNode.position[1] = snapPointToGrid(
activeNode?.state?.y ?? activeNode.position[1], activeNode?.state?.y ?? activeNode.position[1],
5 / snapLevel 5 / snapLevel,
); );
} else { } else {
activeNode.position[0] = activeNode?.state?.x ?? activeNode.position[0]; activeNode.position[0] = activeNode?.state?.x ?? activeNode.position[0];
activeNode.position[1] = activeNode?.state?.y ?? activeNode.position[1]; activeNode.position[1] = activeNode?.state?.y ?? activeNode.position[1];
} }
const nodes = [ const nodes = [
...[...(this.state.selectedNodes?.values() || [])].map((id) => this.graph.getNode(id)) ...[...(this.state.selectedNodes?.values() || [])].map((id) =>
this.graph.getNode(id),
),
] as NodeInstance[]; ] as NodeInstance[];
const vec = [ const vec = [
activeNode.position[0] - (activeNode?.state.x || 0), activeNode.position[0] - (activeNode?.state.x || 0),
activeNode.position[1] - (activeNode?.state.y || 0) activeNode.position[1] - (activeNode?.state.y || 0),
]; ];
for (const node of nodes) { for (const node of nodes) {
@@ -73,9 +179,9 @@ export class MouseEventManager {
animate(500, (a: number) => { animate(500, (a: number) => {
for (const node of nodes) { for (const node of nodes) {
if ( if (
node?.state node?.state &&
&& node.state['x'] !== undefined node.state["x"] !== undefined &&
&& node.state['y'] !== undefined node.state["y"] !== undefined
) { ) {
node.state.x = lerp(node.state.x, node.position[0], a); node.state.x = lerp(node.state.x, node.position[0], a);
node.state.y = lerp(node.state.y, node.position[1], a); node.state.y = lerp(node.state.y, node.position[1], a);
@@ -89,24 +195,24 @@ export class MouseEventManager {
this.graph.save(); this.graph.save();
} else if (this.state.hoveredSocket && this.state.activeSocket) { } else if (this.state.hoveredSocket && this.state.activeSocket) {
if ( if (
typeof this.state.hoveredSocket.index === 'number' typeof this.state.hoveredSocket.index === "number" &&
&& typeof this.state.activeSocket.index === 'string' typeof this.state.activeSocket.index === "string"
) { ) {
this.graph.createEdge( this.graph.createEdge(
this.state.hoveredSocket.node, this.state.hoveredSocket.node,
this.state.hoveredSocket.index || 0, this.state.hoveredSocket.index || 0,
this.state.activeSocket.node, this.state.activeSocket.node,
this.state.activeSocket.index this.state.activeSocket.index,
); );
} else if ( } else if (
typeof this.state.activeSocket.index == 'number' typeof this.state.activeSocket.index == "number" &&
&& typeof this.state.hoveredSocket.index === 'string' typeof this.state.hoveredSocket.index === "string"
) { ) {
this.graph.createEdge( this.graph.createEdge(
this.state.activeSocket.node, this.state.activeSocket.node,
this.state.activeSocket.index || 0, this.state.activeSocket.index || 0,
this.state.hoveredSocket.node, this.state.hoveredSocket.node,
this.state.hoveredSocket.index this.state.hoveredSocket.index,
); );
} }
this.graph.save(); this.graph.save();
@@ -114,18 +220,18 @@ export class MouseEventManager {
// Handle automatic adding of nodes on ctrl+mouseUp // Handle automatic adding of nodes on ctrl+mouseUp
this.state.edgeEndPosition = [ this.state.edgeEndPosition = [
this.state.mousePosition[0], this.state.mousePosition[0],
this.state.mousePosition[1] this.state.mousePosition[1],
]; ];
if (typeof this.state.activeSocket.index === 'number') { if (typeof this.state.activeSocket.index === "number") {
this.state.addMenuPosition = [ this.state.addMenuPosition = [
this.state.mousePosition[0], this.state.mousePosition[0],
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2] this.state.mousePosition[1] - 25 / this.state.cameraPosition[2],
]; ];
} else { } else {
this.state.addMenuPosition = [ this.state.addMenuPosition = [
this.state.mousePosition[0] - 155 / this.state.cameraPosition[2], this.state.mousePosition[0] - 155 / this.state.cameraPosition[2],
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2] this.state.mousePosition[1] - 25 / this.state.cameraPosition[2],
]; ];
} }
return; return;
@@ -133,11 +239,11 @@ export class MouseEventManager {
// check if camera moved // check if camera moved
if ( if (
clickedNodeId === -1 clickedNodeId === -1 &&
&& !this.state.boxSelection !this.state.boxSelection &&
&& this.state.cameraDown[0] === this.state.cameraPosition[0] this.state.cameraDown[0] === this.state.cameraPosition[0] &&
&& this.state.cameraDown[1] === this.state.cameraPosition[1] this.state.cameraDown[1] === this.state.cameraPosition[1] &&
&& this.state.isBodyFocused() this.state.isBodyFocused()
) { ) {
this.state.activeNodeId = -1; this.state.activeNodeId = -1;
this.state.clearSelection(); this.state.clearSelection();
@@ -151,27 +257,16 @@ export class MouseEventManager {
this.state.addMenuPosition = null; this.state.addMenuPosition = null;
} }
handleContextMenu(event: MouseEvent) {
if (!this.state.addMenuPosition) {
event.preventDefault();
this.state.openNodePalette();
}
}
handleMouseDown(event: MouseEvent) { handleMouseDown(event: MouseEvent) {
// Right click
if (event.button === 2) {
return;
}
if (this.state.mouseDown) return; if (this.state.mouseDown) return;
this.state.edgeEndPosition = null; this.state.edgeEndPosition = null;
if (event.target instanceof HTMLElement) { if (event.target instanceof HTMLElement) {
if ( if (
event.target.nodeName !== 'CANVAS' event.target.nodeName !== "CANVAS" &&
&& !event.target.classList.contains('node') !event.target.classList.contains("node") &&
&& !event.target.classList.contains('content') !event.target.classList.contains("content")
) { ) {
return; return;
} }
@@ -193,7 +288,7 @@ export class MouseEventManager {
this.state.activeNodeId = clickedNodeId; this.state.activeNodeId = clickedNodeId;
// if the selected node is the same as the clicked node // if the selected node is the same as the clicked node
} else if (this.state.activeNodeId === clickedNodeId) { } else if (this.state.activeNodeId === clickedNodeId) {
// $activeNodeId = -1; //$activeNodeId = -1;
// if the clicked node is different from the selected node and secondary // if the clicked node is different from the selected node and secondary
} else if (event.ctrlKey) { } else if (event.ctrlKey) {
this.state.selectedNodes.add(this.state.activeNodeId); this.state.selectedNodes.add(this.state.activeNodeId);
@@ -217,7 +312,6 @@ export class MouseEventManager {
this.state.activeNodeId = clickedNodeId; this.state.activeNodeId = clickedNodeId;
this.state.clearSelection(); this.state.clearSelection();
} }
this.edgeInteractionManager.handleMouseDown();
} else if (event.ctrlKey) { } else if (event.ctrlKey) {
this.state.boxSelection = true; this.state.boxSelection = true;
} }
@@ -241,7 +335,8 @@ export class MouseEventManager {
this.state.edgeEndPosition = null; this.state.edgeEndPosition = null;
} }
handleWindowMouseMove(event: MouseEvent) {
handleMouseMove(event: MouseEvent) {
let mx = event.clientX - this.state.rect.x; let mx = event.clientX - this.state.rect.x;
let my = event.clientY - this.state.rect.y; let my = event.clientY - this.state.rect.y;
@@ -256,8 +351,8 @@ export class MouseEventManager {
let _socket; let _socket;
for (const socket of this.state.possibleSockets) { for (const socket of this.state.possibleSockets) {
const dist = Math.sqrt( const dist = Math.sqrt(
(socket.position[0] - this.state.mousePosition[0]) ** 2 (socket.position[0] - this.state.mousePosition[0]) ** 2 +
+ (socket.position[1] - this.state.mousePosition[1]) ** 2 (socket.position[1] - this.state.mousePosition[1]) ** 2,
); );
if (dist < smallestDist) { if (dist < smallestDist) {
smallestDist = dist; smallestDist = dist;
@@ -280,7 +375,7 @@ export class MouseEventManager {
event.stopPropagation(); event.stopPropagation();
const mouseD = this.state.projectScreenToWorld( const mouseD = this.state.projectScreenToWorld(
this.state.mouseDown[0], this.state.mouseDown[0],
this.state.mouseDown[1] this.state.mouseDown[1],
); );
const x1 = Math.min(mouseD[0], this.state.mousePosition[0]); const x1 = Math.min(mouseD[0], this.state.mousePosition[0]);
const x2 = Math.max(mouseD[0], this.state.mousePosition[0]); const x2 = Math.max(mouseD[0], this.state.mousePosition[0]);
@@ -302,7 +397,6 @@ export class MouseEventManager {
// here we are handling dragging of nodes // here we are handling dragging of nodes
if (this.state.activeNodeId !== -1 && this.state.mouseDownNodeId !== -1) { if (this.state.activeNodeId !== -1 && this.state.mouseDownNodeId !== -1) {
this.edgeInteractionManager.handleMouseMove();
const node = this.graph.getNode(this.state.activeNodeId); const node = this.graph.getNode(this.state.activeNodeId);
if (!node || event.buttons !== 1) return; if (!node || event.buttons !== 1) return;
@@ -311,8 +405,10 @@ export class MouseEventManager {
const oldX = node.state.downX || 0; const oldX = node.state.downX || 0;
const oldY = node.state.downY || 0; const oldY = node.state.downY || 0;
let newX = oldX + (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2]; let newX =
let newY = oldY + (my - this.state.mouseDown[1]) / this.state.cameraPosition[2]; 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) { if (event.ctrlKey) {
const snapLevel = this.state.getSnapLevel(); const snapLevel = this.state.getSnapLevel();
@@ -352,19 +448,23 @@ export class MouseEventManager {
// here we are handling panning of camera // here we are handling panning of camera
this.state.isPanning = true; this.state.isPanning = true;
let newX = this.state.cameraDown[0] let newX =
- (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2]; this.state.cameraDown[0] -
let newY = this.state.cameraDown[1] (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
- (my - this.state.mouseDown[1]) / 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[0] = newX;
this.state.cameraPosition[1] = newY; this.state.cameraPosition[1] = newY;
} }
handleMouseScroll(event: WheelEvent) { handleMouseScroll(event: WheelEvent) {
const bodyIsFocused = document.activeElement === document.body const bodyIsFocused =
|| document.activeElement === this.state.wrapper document.activeElement === document.body ||
|| document?.activeElement?.id === 'graph'; document.activeElement === this.state.wrapper ||
document?.activeElement?.id === "graph";
if (!bodyIsFocused) return; if (!bodyIsFocused) return;
// Define zoom speed and clamp it between -1 and 1 // Define zoom speed and clamp it between -1 and 1
@@ -379,19 +479,21 @@ export class MouseEventManager {
maxZoom, maxZoom,
isNegative isNegative
? this.state.cameraPosition[2] / delta ? this.state.cameraPosition[2] / delta
: this.state.cameraPosition[2] * delta : this.state.cameraPosition[2] * delta,
) ),
); );
// Calculate the ratio of the new zoom to the original zoom // Calculate the ratio of the new zoom to the original zoom
const zoomRatio = newZoom / this.state.cameraPosition[2]; const zoomRatio = newZoom / this.state.cameraPosition[2];
// Update camera position and zoom level // Update camera position and zoom level
this.state.cameraPosition[0] = this.state.mousePosition[0] this.state.cameraPosition[0] = this.state.mousePosition[0] -
- (this.state.mousePosition[0] - this.state.cameraPosition[0]) (this.state.mousePosition[0] - this.state.cameraPosition[0]) /
/ zoomRatio; zoomRatio;
this.state.cameraPosition[1] = this.state.mousePosition[1] this.state.cameraPosition[1] = this.state.mousePosition[1] -
- (this.state.mousePosition[1] - this.state.cameraPosition[1]) (this.state.mousePosition[1] - this.state.cameraPosition[1]) /
/ zoomRatio, this.state.cameraPosition[2] = newZoom; zoomRatio,
this.state.cameraPosition[2] = newZoom;
} }
} }

View File

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

View File

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

View File

@@ -73,14 +73,8 @@
{#key id && graphId} {#key id && graphId}
<div class="content" class:disabled={graph?.inputSockets?.has(socketId)}> <div class="content" class:disabled={graph?.inputSockets?.has(socketId)}>
{#if inputType.label !== ""} {#if inputType.label !== ""}
<label for={elementId} title={input.description} <label for={elementId}>{input.label || id}</label>
>{input.label || id}</label
>
{/if} {/if}
<span
class="absolute i-[tabler--help-circle] size-4 block top-2 right-2 opacity-30"
title={JSON.stringify(input, null, 2)}
></span>
{#if inputType.external !== true} {#if inputType.external !== true}
<NodeInputEl {graph} {elementId} bind:node {input} {id} /> <NodeInputEl {graph} {elementId} bind:node {input} {id} />
{/if} {/if}
@@ -187,6 +181,9 @@
.content.disabled { .content.disabled {
opacity: 0.2; opacity: 0.2;
} }
.content.disabled > * {
pointer-events: none;
}
.disabled svg path { .disabled svg path {
d: var(--hover-path-disabled) !important; d: var(--hover-path-disabled) !important;

View File

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

View File

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

View File

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

View File

@@ -14,10 +14,7 @@
<div class="wrapper"> <div class="wrapper">
<div class="bars"> <div class="bars">
{#each values as value, i} {#each values as value, i}
<div <div class="bar bg-{colors[i]}" style="width: {(value / total) * 100}%;">
class="bar bg-{colors[i]}-400"
style="width: {(value / total) * 100}%;"
>
{Math.round(value)}ms {Math.round(value)}ms
</div> </div>
{/each} {/each}
@@ -25,12 +22,10 @@
<div class="labels mt-2"> <div class="labels mt-2">
{#each values as _label, i} {#each values as _label, i}
<div class="text-{colors[i]}-400">{labels[i]}</div> <div class="text-{colors[i]}">{labels[i]}</div>
{/each} {/each}
</div> </div>
<span <span class="bg-red bg-green bg-blue text-red text-green text-blue"></span>
class="bg-red-400 bg-green-400 bg-blue-400 text-red-400 text-green-400 text-blue-400"
></span>
</div> </div>
<style> <style>

View File

@@ -1,108 +0,0 @@
<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

@@ -1,52 +0,0 @@
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

@@ -1,85 +0,0 @@
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

@@ -92,6 +92,13 @@
geo.attributes.position.array[i + 2], geo.attributes.position.array[i + 2],
] as Vector3Tuple; ] as Vector3Tuple;
} }
// $effect(() => {
// console.log({
// geometries: $state.snapshot(geometries),
// indices: appSettings.value.debug.showIndices,
// });
// });
</script> </script>
<Camera {center} {centerCamera} /> <Camera {center} {centerCamera} />

View File

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

View File

@@ -64,7 +64,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
constructor( constructor(
private registry: NodeRegistry, private registry: NodeRegistry,
public cache?: SyncCache<Int32Array>, private cache?: SyncCache<Int32Array>,
) { ) {
this.cache = undefined; this.cache = undefined;
} }
@@ -244,14 +244,13 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
} }
this.perf?.addPoint("cache-hit", 0); this.perf?.addPoint("cache-hit", 0);
log.group(`executing ${node_type.id}-${node.id}`); log.group(`executing ${node_type.id || node.id}`);
log.log(`Inputs:`, inputs); log.log(`Inputs:`, inputs);
a = performance.now(); a = performance.now();
results[node.id] = node_type.execute(encoded_inputs); results[node.id] = node_type.execute(encoded_inputs);
log.log("Executed", node.type, node.id)
b = performance.now(); b = performance.now();
if (this.cache && node.id !== outputNode.id) { if (this.cache) {
this.cache.set(inputHash, results[node.id]); this.cache.set(inputHash, results[node.id]);
} }

View File

@@ -13,22 +13,6 @@ const executor = new MemoryRuntimeExecutor(nodeRegistry, cache);
const performanceStore = createPerformanceStore(); const performanceStore = createPerformanceStore();
executor.perf = performanceStore; executor.perf = performanceStore;
export async function setUseRegistryCache(useCache: boolean) {
if (useCache) {
nodeRegistry.cache = indexDbCache;
} else {
nodeRegistry.cache = undefined;
}
}
export async function setUseRuntimeCache(useCache: boolean) {
if (useCache) {
executor.cache = cache;
} else {
executor.cache = undefined;
}
}
export async function executeGraph( export async function executeGraph(
graph: Graph, graph: Graph,
settings: Record<string, unknown>, settings: Record<string, unknown>,

View File

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

View File

@@ -2,7 +2,7 @@
import NestedSettings from "./NestedSettings.svelte"; import NestedSettings from "./NestedSettings.svelte";
import { localState } from "$lib/helpers/localState.svelte"; import { localState } from "$lib/helpers/localState.svelte";
import type { NodeInput } from "@nodarium/types"; import type { NodeInput } from "@nodarium/types";
import Input from "@nodarium/ui"; import { Input } from "@nodarium/ui";
type Button = { type: "button"; callback: () => void; label?: string }; type Button = { type: "button"; callback: () => void; label?: string };

View File

@@ -87,19 +87,6 @@ export const AppSettingTypes = {
label: "Show Graph Source", label: "Show Graph Source",
value: false, value: false,
}, },
cache: {
title: "Cache",
useRuntimeCache: {
type: "boolean",
label: "Node Results",
value: true,
},
useRegistryCache: {
type: "boolean",
label: "Node Source",
value: true,
},
},
stressTest: { stressTest: {
title: "Stress Test", title: "Stress Test",
amount: { amount: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
<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

@@ -1,119 +1,25 @@
<script lang="ts"> <script lang="ts">
import NodeHTML from "$lib/graph-interface/node/NodeHTML.svelte"; import Grid from "$lib/grid";
import { localState } from "$lib/helpers/localState.svelte";
import Panel from "$lib/sidebar/Panel.svelte"; import Panel from "$lib/sidebar/Panel.svelte";
import Sidebar from "$lib/sidebar/Sidebar.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> </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.Row>
<Grid.Cell></Grid.Cell>
<Grid.Cell> <Grid.Cell>
<pre> <Sidebar>
<code> <Panel
{JSON.stringify(nodeInstance?.props)} id="node-store"
</code> classes="text-green-400"
</pre> title="Node Store"
</Grid.Cell> icon="i-tabler-database"
></Panel>
<Grid.Cell> </Sidebar>
<div class="h-screen w-[80vw] overflow-y-auto">
{#if nodeWasm}
<Code wasm={nodeWasm} />
{/if}
</div>
</Grid.Cell> </Grid.Cell>
</Grid.Row> </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> <style>
:global body { :global body {
height: 100vh; height: 100vh;
width: 100vw;
overflow: hidden;
} }
</style> </style>

View File

@@ -1,26 +0,0 @@
<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}

View File

@@ -14,9 +14,6 @@ export const entries: EntryGenerator = async () => {
export const GET: RequestHandler = async function GET({ params }) { export const GET: RequestHandler = async function GET({ params }) {
const namespaces = await registry.getUser(params.user) const namespaces = await registry.getUser(params.user)
return json(namespaces); return json(namespaces);
} }

View File

@@ -14,9 +14,6 @@ export const entries: EntryGenerator = async () => {
} }
export const GET: RequestHandler = async function GET({ params }) { export const GET: RequestHandler = async function GET({ params }) {
const namespaces = await registry.getCollection(`${params.user}/${params.collection}`); const namespaces = await registry.getCollection(`${params.user}/${params.collection}`);
return json(namespaces); return json(namespaces);
} }

View File

@@ -5,7 +5,6 @@ import * as registry from "$lib/node-registry";
export const prerender = true; export const prerender = true;
export const entries: EntryGenerator = async () => { export const entries: EntryGenerator = async () => {
const users = await registry.getUsers(); const users = await registry.getUsers();
return users.map(user => { return users.map(user => {
@@ -18,9 +17,7 @@ export const entries: EntryGenerator = async () => {
} }
export const GET: RequestHandler = async function GET({ params }) { export const GET: RequestHandler = async function GET({ params }) {
const nodeId = `${params.user}/${params.collection}/${params.node}` as const; const nodeId = `${params.user}/${params.collection}/${params.node}` as const;
try { try {
const node = await getNode(nodeId); const node = await getNode(nodeId);
return json(node); return json(node);

View File

@@ -6,9 +6,6 @@ import * as registry from "$lib/node-registry";
export const prerender = true; export const prerender = true;
export const GET: RequestHandler = async function GET() { export const GET: RequestHandler = async function GET() {
const users = await registry.getUsers(); const users = await registry.getUsers();
return json(users); return json(users);
} }

View File

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

18
app/uno.config.ts Normal file
View File

@@ -0,0 +1,18 @@
// uno.config.ts
import { defineConfig } from 'unocss'
import presetIcons from '@unocss/preset-icons'
import fs from 'node: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: [
presetIcons({
collections: {
custom: icons
}
}),
]
})

View File

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

View File

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

27
flake.lock generated
View File

@@ -1,27 +0,0 @@
{
"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
}

View File

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

View File

@@ -0,0 +1,6 @@
{
"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

@@ -0,0 +1,13 @@
//! 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,6 +7,22 @@ edition = "2018"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies] [dependencies]
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" } wasm-bindgen = "0.2.84"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" } nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,22 @@
use glam::Vec3; use glam::Vec3;
use nodarium_macros::nodarium_definition_file; use nodarium_macros::include_definition_file;
use nodarium_macros::nodarium_execute;
use nodarium_utils::{ use nodarium_utils::{
concat_args, evaluate_float, evaluate_int, concat_args, evaluate_float, evaluate_int,
geometry::{wrap_path, wrap_path_mut}, geometry::{wrap_path, wrap_path_mut},
log, reset_call_count, split_args, log, reset_call_count, set_panic_hook, split_args,
}; };
use wasm_bindgen::prelude::*;
nodarium_definition_file!("src/input.json"); include_definition_file!("src/input.json");
fn lerp_vec3(a: Vec3, b: Vec3, t: f32) -> Vec3 { fn lerp_vec3(a: Vec3, b: Vec3, t: f32) -> Vec3 {
a + (b - a) * t a + (b - a) * t
} }
#[nodarium_execute] #[wasm_bindgen]
pub fn execute(input: &[i32]) -> Vec<i32> { pub fn execute(input: &[i32]) -> Vec<i32> {
set_panic_hook();
reset_call_count(); reset_call_count();
let args = split_args(input); let args = split_args(input);

View File

@@ -0,0 +1,13 @@
//! 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

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

View File

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

View File

@@ -1,18 +1,20 @@
use glam::{Mat4, Quat, Vec3}; use glam::{Mat4, Quat, Vec3};
use nodarium_macros::nodarium_execute; use nodarium_macros::include_definition_file;
use nodarium_macros::nodarium_definition_file;
use nodarium_utils::{ use nodarium_utils::{
concat_args, evaluate_float, evaluate_int, concat_args, encode_float, evaluate_float, evaluate_int,
geometry::{ geometry::{
create_instance_data, wrap_geometry_data, wrap_instance_data, wrap_path, calculate_normals, create_instance_data, wrap_geometry_data, wrap_instance_data, wrap_path,
}, },
log, split_args, log, set_panic_hook, split_args, wrap_arg,
}; };
use wasm_bindgen::prelude::*;
nodarium_definition_file!("src/input.json"); include_definition_file!("src/input.json");
#[nodarium_execute] #[wasm_bindgen]
pub fn execute(input: &[i32]) -> Vec<i32> { pub fn execute(input: &[i32]) -> Vec<i32> {
set_panic_hook();
let args = split_args(input); let args = split_args(input);
let mut inputs = split_args(args[0]); let mut inputs = split_args(args[0]);
log!("WASM(instance): inputs: {:?}", inputs); log!("WASM(instance): inputs: {:?}", inputs);

View File

@@ -0,0 +1,13 @@
//! 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,6 +7,17 @@ edition = "2018"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies] [dependencies]
wasm-bindgen = "0.2.84"
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.4"
console_error_panic_hook = { version = "0.1.7", optional = true }
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" } nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" } nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
web-sys = { version = "0.3.69", features = ["console"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.34"

View File

@@ -0,0 +1,6 @@
{
"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 +1,12 @@
use nodarium_macros::nodarium_definition_file; use nodarium_macros::include_definition_file;
use nodarium_macros::nodarium_execute; use nodarium_utils::{concat_args, set_panic_hook, split_args};
use nodarium_utils::{ use wasm_bindgen::prelude::*;
concat_args, split_args
};
#[nodarium_execute] include_definition_file!("src/input.json");
#[wasm_bindgen]
pub fn execute(args: &[i32]) -> Vec<i32> { pub fn execute(args: &[i32]) -> Vec<i32> {
set_panic_hook();
let args = split_args(args); let args = split_args(args);
concat_args(vec![&[0], args[0], args[1], args[2]]) concat_args(vec![&[0], args[0], args[1], args[2]])
} }
nodarium_definition_file!("src/input.json");

View File

@@ -0,0 +1,13 @@
//! 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

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

View File

@@ -0,0 +1,6 @@
{
"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,19 +1,21 @@
use nodarium_macros::nodarium_definition_file; use nodarium_macros::include_definition_file;
use nodarium_macros::nodarium_execute;
use nodarium_utils::{ use nodarium_utils::{
concat_args, evaluate_float, evaluate_int, evaluate_vec3, geometry::wrap_path_mut, concat_args, evaluate_float, evaluate_int, evaluate_vec3, geometry::wrap_path_mut, log,
reset_call_count, split_args, reset_call_count, set_panic_hook, split_args,
}; };
use noise::{HybridMulti, MultiFractal, NoiseFn, OpenSimplex}; use noise::{HybridMulti, MultiFractal, NoiseFn, OpenSimplex};
use wasm_bindgen::prelude::*;
nodarium_definition_file!("src/input.json"); include_definition_file!("src/input.json");
fn lerp(a: f32, b: f32, t: f32) -> f32 { fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + t * (b - a) a + t * (b - a)
} }
#[nodarium_execute] #[wasm_bindgen]
pub fn execute(input: &[i32]) -> Vec<i32> { pub fn execute(input: &[i32]) -> Vec<i32> {
set_panic_hook();
reset_call_count(); reset_call_count();
let args = split_args(input); let args = split_args(input);

View File

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

View File

@@ -0,0 +1,6 @@
{
"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,15 +1,17 @@
use nodarium_macros::nodarium_definition_file; use nodarium_macros::include_definition_file;
use nodarium_macros::nodarium_execute;
use nodarium_utils::{ use nodarium_utils::{
concat_args, evaluate_int, concat_args, evaluate_int,
geometry::{extrude_path, wrap_path}, geometry::{extrude_path, wrap_path},
log, split_args, log, set_panic_hook, split_args,
}; };
use wasm_bindgen::prelude::*;
nodarium_definition_file!("src/inputs.json"); include_definition_file!("src/inputs.json");
#[nodarium_execute] #[wasm_bindgen]
pub fn execute(input: &[i32]) -> Vec<i32> { pub fn execute(input: &[i32]) -> Vec<i32> {
set_panic_hook();
log!("WASM(output): input: {:?}", input); log!("WASM(output): input: {:?}", input);
let args = split_args(input); let args = split_args(input);

View File

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

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