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
/target
.direnv/

357
Cargo.lock generated
View File

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

View File

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

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"
},
"dependencies": {
"@nodarium/registry": "workspace:*",
"@nodarium/ui": "workspace:*",
"@nodarium/utils": "workspace:*",
"@nodarium/registry": "link:../packages/registry",
"@nodarium/ui": "link:../packages/ui",
"@nodarium/utils": "link:../packages/utils",
"@sveltejs/kit": "^2.50.0",
"@tailwindcss/vite": "^4.1.18",
"@threlte/core": "8.3.1",
"@threlte/extras": "9.7.1",
"@types/three": "^0.182.0",
"@unocss/reset": "^66.6.0",
"comlink": "^4.4.2",
"file-saver": "^2.0.5",
"idb": "^8.0.3",
"jsondiffpatch": "^0.7.3",
"tailwindcss": "^4.1.18",
"three": "^0.182.0",
"wabt": "^1.0.39"
"three": "^0.182.0"
},
"devDependencies": {
"@iconify-json/tabler": "^1.2.26",
"@iconify/tailwind4": "^1.2.1",
"@nodarium/types": "workspace:",
"@nodarium/types": "link:../packages/types",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tsconfig/svelte": "^5.0.6",
"@types/file-saver": "^2.0.7",
"@types/three": "^0.182.0",
"@unocss/preset-icons": "^66.6.0",
"svelte": "^5.46.4",
"svelte-check": "^4.3.5",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"unocss": "^66.6.0",
"vite": "^7.3.1",
"vite-plugin-comlink": "^5.3.0",
"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" />
<link rel="icon" href="%sveltekit.assets%/svelte.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script defer src="https://umami.max-richter.dev/script.js" data-website-id="585c442b-0524-4874-8955-f9853b44b17e"></script>
%sveltekit.head%
<title>Nodes</title>
<script>

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

View File

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

View File

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

View File

@@ -1,8 +1,6 @@
import type { Box } from '@nodarium/types';
import { writable } from 'svelte/store';
import type { Color } from 'three';
import { Vector3 } from 'three/src/math/Vector3.js';
import { writable } from "svelte/store";
import { Vector3 } from "three/src/math/Vector3.js";
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 { Vector2 } from "three/src/math/Vector2.js";
import { appSettings } from "$lib/settings/app-settings.svelte";
import { getGraphState } from "../graph-state.svelte";
import { onDestroy } from "svelte";
const graphState = getGraphState();
type Props = {
x1: number;
@@ -42,21 +38,20 @@
x2: number;
y2: 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)));
let points = $state<Vector3[]>([]);
let lastId: string | null = null;
const curveId = $derived(`${x1}-${y1}-${x2}-${y2}`);
function update() {
const new_x = x2 - x1;
const new_y = y2 - y1;
const curveId = `${x1}-${y1}-${x2}-${y2}`;
if (lastId === curveId) {
return;
}
@@ -77,15 +72,6 @@
.getPoints(samples)
.map((p) => new Vector3(p.x, 0, p.y))
.flat();
if (id) {
graphState.setEdgeGeometry(
id,
x1,
y1,
$state.snapshot(points) as unknown as Vector3[],
);
}
}
$effect(() => {
@@ -93,10 +79,6 @@
update();
}
});
onDestroy(() => {
if (id) graphState.removeEdgeGeometry(id);
});
</script>
<T.Mesh
@@ -119,18 +101,6 @@
<T.CircleGeometry args={[0.5, 16]} />
</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}>
<MeshLineGeometry {points} />
<MeshLineMaterial width={thickness} color={lineColor} />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,10 +14,7 @@
<div class="wrapper">
<div class="bars">
{#each values as value, i}
<div
class="bar bg-{colors[i]}-400"
style="width: {(value / total) * 100}%;"
>
<div class="bar bg-{colors[i]}" style="width: {(value / total) * 100}%;">
{Math.round(value)}ms
</div>
{/each}
@@ -25,12 +22,10 @@
<div class="labels mt-2">
{#each values as _label, i}
<div class="text-{colors[i]}-400">{labels[i]}</div>
<div class="text-{colors[i]}">{labels[i]}</div>
{/each}
</div>
<span
class="bg-red-400 bg-green-400 bg-blue-400 text-red-400 text-green-400 text-blue-400"
></span>
<span class="bg-red bg-green bg-blue text-red text-green text-blue"></span>
</div>
<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],
] as Vector3Tuple;
}
// $effect(() => {
// console.log({
// geometries: $state.snapshot(geometries),
// indices: appSettings.value.debug.showIndices,
// });
// });
</script>
<Camera {center} {centerCamera} />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,34 +13,32 @@
let { keymaps }: Props = $props();
</script>
<div class="p-4">
<table class="wrapper">
<tbody>
{#each keymaps as keymap}
<table class="wrapper">
<tbody>
{#each keymaps as keymap}
<tr>
<td colspan="2">
<h3>{keymap.title}</h3>
</td>
</tr>
{#each get(keymap.keymap?.keys) as key}
<tr>
<td colspan="2">
<h3>{keymap.title}</h3>
</td>
{#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 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}
</tbody>
</table>
</div>
{/each}
</tbody>
</table>
<style>
.wrapper {

View File

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

View File

@@ -28,8 +28,6 @@
import BenchmarkPanel from "$lib/sidebar/panels/BenchmarkPanel.svelte";
import { debounceAsyncFunction } from "$lib/helpers";
import GraphSource from "$lib/sidebar/panels/GraphSource.svelte";
import { ProjectManager } from "$lib/project-manager/project-manager.svelte";
import ProjectManagerEl from "$lib/project-manager/ProjectManager.svelte";
let performanceStore = createPerformanceStore();
@@ -39,34 +37,23 @@
const runtimeCache = new MemoryRuntimeCache();
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
memoryRuntime.perf = performanceStore;
const pm = new ProjectManager();
const runtime = $derived(
appSettings.value.debug.useWorker ? workerRuntime : memoryRuntime,
);
$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 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 viewerComponent = $state<ReturnType<typeof Viewer>>();
const manager = $derived(graphInterface?.manager);
@@ -85,16 +72,21 @@
callback: () => randomGenerate(),
},
]);
let graphSettings = $state<Record<string, any>>({});
let graphSettingTypes = $state({
randomSeed: { type: "boolean", value: false },
});
$effect(() => {
if (graphSettings && graphSettingTypes) {
if (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(
g: Graph,
@@ -172,23 +164,21 @@
/>
</Grid.Cell>
<Grid.Cell>
{#if pm.graph}
<GraphInterface
graph={pm.graph}
bind:this={graphInterface}
registry={nodeRegistry}
showGrid={appSettings.value.nodeInterface.showNodeGrid}
snapToGrid={appSettings.value.nodeInterface.snapToGrid}
bind:activeNode
bind:showHelp={appSettings.value.nodeInterface.showHelp}
bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes}
onsave={(g) => pm.saveGraph(g)}
onresult={(result) => handleUpdate(result)}
/>
{/if}
<GraphInterface
{graph}
bind:this={graphInterface}
registry={nodeRegistry}
showGrid={appSettings.value.nodeInterface.showNodeGrid}
snapToGrid={appSettings.value.nodeInterface.snapToGrid}
bind:activeNode
bind:showHelp={appSettings.value.nodeInterface.showHelp}
bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes}
onresult={(result) => handleUpdate(result)}
onsave={(graph) => handleSave(graph)}
/>
<Sidebar>
<Panel id="general" title="General" icon="i-[tabler--settings]">
<Panel id="general" title="General" icon="i-tabler-settings">
<NestedSettings
id="general"
bind:value={appSettings.value}
@@ -198,7 +188,7 @@
<Panel
id="shortcuts"
title="Keyboard Shortcuts"
icon="i-[tabler--keyboard]"
icon="i-tabler-keyboard"
>
<Keymap
keymaps={[
@@ -207,49 +197,50 @@
]}
/>
</Panel>
<Panel id="exports" title="Exporter" icon="i-[tabler--package-export]">
<Panel id="exports" title="Exporter" icon="i-tabler-package-export">
<ExportSettings {scene} />
</Panel>
<Panel
id="node-store"
classes="text-green-400"
title="Node Store"
icon="i-[tabler--database] bg-green-400"
icon="i-tabler-database"
>
<NodeStore registry={nodeRegistry} />
</Panel>
<Panel
id="performance"
title="Performance"
classes="text-red-400"
hidden={!appSettings.value.debug.showPerformancePanel}
icon="i-[tabler--brand-speedtest] bg-red-400"
icon="i-tabler-brand-speedtest"
>
{#if $performanceStore}
<PerformanceViewer data={$performanceStore} />
{/if}
</Panel>
<Panel id="projects" icon="i-[tabler--folder-open]">
<ProjectManagerEl projectManager={pm} />
</Panel>
<Panel
id="graph-source"
title="Graph Source"
hidden={!appSettings.value.debug.showGraphJson}
icon="i-[tabler--code]"
icon="i-tabler-code"
>
<GraphSource graph={pm.graph ?? manager?.serialize()} />
<GraphSource {graph} />
</Panel>
<Panel
id="benchmark"
title="Benchmark"
classes="text-red-400"
hidden={!appSettings.value.debug.showBenchmarkPanel}
icon="i-[tabler--graph] bg-red-400"
icon="i-tabler-graph"
>
<BenchmarkPanel run={randomGenerate} />
</Panel>
<Panel
id="graph-settings"
title="Graph Settings"
icon="i-[custom--graph] bg-blue-400"
classes="text-blue-400"
icon="i-custom-graph"
>
<NestedSettings
id="graph-settings"
@@ -260,7 +251,8 @@
<Panel
id="active-node"
title="Node Settings"
icon="i-[tabler--adjustments] bg-blue-400"
classes="text-blue-400"
icon="i-tabler-adjustments"
>
<ActiveNodeSettings {manager} bind:node={activeNode} />
</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">
import NodeHTML from "$lib/graph-interface/node/NodeHTML.svelte";
import { localState } from "$lib/helpers/localState.svelte";
import Grid from "$lib/grid";
import Panel from "$lib/sidebar/Panel.svelte";
import Sidebar from "$lib/sidebar/Sidebar.svelte";
import { IndexDBCache, RemoteNodeRegistry } from "@nodarium/registry";
import { type NodeId, type NodeInstance } from "@nodarium/types";
import Code from "./Code.svelte";
import Grid from "$lib/grid";
import {
concatEncodedArrays,
createWasmWrapper,
encodeNestedArray,
} from "@nodarium/utils";
const registryCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry("", registryCache);
let activeNode = localState<NodeId | undefined>(
"node.dev.activeNode",
undefined,
);
let nodeWasm = $state<ArrayBuffer>();
let nodeInstance = $state<NodeInstance>();
let nodeWasmWrapper = $state<ReturnType<typeof createWasmWrapper>>();
async function fetchNodeData(nodeId?: NodeId) {
nodeWasm = undefined;
nodeInstance = undefined;
if (!nodeId) return;
const data = await nodeRegistry.fetchNodeDefinition(nodeId);
nodeWasm = await nodeRegistry.fetchArrayBuffer("nodes/" + nodeId + ".wasm");
nodeInstance = {
id: 0,
type: nodeId,
position: [0, 0] as [number, number],
props: {},
state: {
type: data,
},
};
nodeWasmWrapper = createWasmWrapper(nodeWasm);
}
$effect(() => {
fetchNodeData(activeNode.value);
});
$effect(() => {
if (nodeInstance?.props && nodeWasmWrapper) {
const keys = Object.keys(nodeInstance.state.type?.inputs || {});
let ins = Object.values(nodeInstance.props) as number[];
if (keys[0] === "plant") {
ins = [[0, 0, 0, 0, 0, 0, 0, 0], ...ins];
}
const inputs = concatEncodedArrays(encodeNestedArray(ins));
nodeWasmWrapper?.execute(inputs);
}
});
</script>
<div class="node-wrapper absolute bottom-8 left-8">
{#if nodeInstance}
<NodeHTML inView position="relative" z={5} bind:node={nodeInstance} />
{/if}
</div>
<Grid.Row>
<Grid.Cell></Grid.Cell>
<Grid.Cell>
<pre>
<code>
{JSON.stringify(nodeInstance?.props)}
</code>
</pre>
</Grid.Cell>
<Grid.Cell>
<div class="h-screen w-[80vw] overflow-y-auto">
{#if nodeWasm}
<Code wasm={nodeWasm} />
{/if}
</div>
<Sidebar>
<Panel
id="node-store"
classes="text-green-400"
title="Node Store"
icon="i-tabler-database"
></Panel>
</Sidebar>
</Grid.Cell>
</Grid.Row>
<Sidebar>
<Panel
id="node-store"
classes="text-green-400"
title="Node Store"
icon="i-[tabler--database]"
>
<div class="p-4 flex flex-col gap-2">
{#await nodeRegistry.fetchCollection("max/plantarium")}
<p>Loading Nodes...</p>
{:then result}
{#each result.nodes as n}
<button
class="cursor-pointer p-2 bg-layer-1 {activeNode.value === n.id
? 'outline outline-offset-1'
: ''}"
onclick={() => (activeNode.value = n.id)}>{n.id}</button
>
{/each}
{/await}
</div>
</Panel>
</Sidebar>
<style>
:global body {
height: 100vh;
width: 100vw;
overflow: hidden;
}
</style>

View File

@@ -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 }) {
const namespaces = await registry.getUser(params.user)
return json(namespaces);
}

View File

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

View File

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

View File

@@ -6,9 +6,6 @@ import * as registry from "$lib/node-registry";
export const prerender = true;
export const GET: RequestHandler = async function GET() {
const users = await registry.getUsers();
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 UnoCSS from 'unocss/vite';
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite'
import comlink from 'vite-plugin-comlink';
import glsl from "vite-plugin-glsl";
import wasm from "vite-plugin-wasm";
export default defineConfig({
plugins: [
tailwindcss(),
comlink(),
UnoCSS(),
sveltekit(),
glsl(),
wasm()

View File

@@ -4,7 +4,7 @@ This guide will help you developing your first Nodarium Node written in Rust. As
## Prerequesites
You need to have [Rust](https://www.rust-lang.org/tools/install) and [wasm-pack](https://rustwasm.github.io/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
# install rust
@@ -22,12 +22,11 @@ cd my-new-node
## Setup Definition
Now we create the definition file of the node.
Now we create the definition file of the node.
Here we define what kind of inputs our node will expect and what kind of output it produces. If you want to dive deeper into this topic, have a look at [NODE_DEFINITION.md](./NODE_DEFINITION.md).
`src/definition.json`
```json
```json
{
"id": "my-name/my-namespace/zylinder-node",
"outputs": [
@@ -36,7 +35,7 @@ Here we define what kind of inputs our node will expect and what kind of output
"inputs": {
"height": {
"type": "float",
"value": 2
"value": 2,
},
"radius": {
"type": "float",
@@ -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:
```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"]
[dependencies]
wasm-bindgen = "0.2.84"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
utils = { version = "0.1.0", path = "../../../../packages/utils" }
macros = { version = "0.1.0", path = "../../../../packages/macros" }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.4"
console_error_panic_hook = { version = "0.1.7", optional = true }
web-sys = { version = "0.3.69", features = ["console"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.34"

View File

@@ -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]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[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_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::nodarium_execute;
use nodarium_macros::include_definition_file;
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,
};
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> {
set_panic_hook();
let args = split_args(input);
log!("WASM(cube): input: {:?} -> {:?}", input, args);
@@ -19,6 +22,7 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
let p = encode_float(size);
let n = encode_float(-size);
// [[1,3, x, y, z, x, y,z,x,y,z]];
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]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2.84"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.4"
console_error_panic_hook = { version = "0.1.7", optional = true }
web-sys = { version = "0.3.69", features = ["console"] }
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::nodarium_execute;
use nodarium_macros::include_definition_file;
use nodarium_utils::{
concat_arg_vecs, evaluate_float, evaluate_int,
geometry::{
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 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> {
set_panic_hook();
let args = split_args(input);
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]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2.84"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.4"
console_error_panic_hook = { version = "0.1.7", optional = true }
[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::nodarium_execute;
use nodarium_macros::include_definition_file;
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> {
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]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2.84"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
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 nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute;
use nodarium_macros::include_definition_file;
use nodarium_utils::{
concat_args, evaluate_float, evaluate_int,
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 {
a + (b - a) * t
}
#[nodarium_execute]
#[wasm_bindgen]
pub fn execute(input: &[i32]) -> Vec<i32> {
set_panic_hook();
reset_call_count();
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]
name = "instance"
name = "nodarium_instance"
version = "0.1.0"
authors = ["Max Richter <jim-x@web.de>"]
edition = "2018"
@@ -7,7 +7,23 @@ edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2.84"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
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 nodarium_macros::nodarium_execute;
use nodarium_macros::nodarium_definition_file;
use nodarium_macros::include_definition_file;
use nodarium_utils::{
concat_args, evaluate_float, evaluate_int,
concat_args, encode_float, evaluate_float, evaluate_int,
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> {
set_panic_hook();
let args = split_args(input);
let mut inputs = split_args(args[0]);
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]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[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_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::nodarium_execute;
use nodarium_utils::{
concat_args, split_args
};
use nodarium_macros::include_definition_file;
use nodarium_utils::{concat_args, set_panic_hook, split_args};
use wasm_bindgen::prelude::*;
#[nodarium_execute]
include_definition_file!("src/input.json");
#[wasm_bindgen]
pub fn execute(args: &[i32]) -> Vec<i32> {
set_panic_hook();
let args = split_args(args);
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]
name = "noise"
name = "nodes-noise"
version = "0.1.0"
authors = ["Max Richter <jim-x@web.de>"]
edition = "2018"
@@ -7,8 +7,24 @@ edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2.84"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.4"
console_error_panic_hook = { version = "0.1.7", optional = true }
web-sys = { version = "0.3.69", features = ["console"] }
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::nodarium_execute;
use nodarium_macros::include_definition_file;
use nodarium_utils::{
concat_args, evaluate_float, evaluate_int, evaluate_vec3, geometry::wrap_path_mut,
reset_call_count, split_args,
concat_args, evaluate_float, evaluate_int, evaluate_vec3, geometry::wrap_path_mut, log,
reset_call_count, set_panic_hook, split_args,
};
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 {
a + t * (b - a)
}
#[nodarium_execute]
#[wasm_bindgen]
pub fn execute(input: &[i32]) -> Vec<i32> {
set_panic_hook();
reset_call_count();
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]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
console_error_panic_hook = ["dep:console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2.84"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
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_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::nodarium_execute;
use nodarium_macros::include_definition_file;
use nodarium_utils::{
concat_args, evaluate_int,
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> {
set_panic_hook();
log!("WASM(output): input: {:?}", 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]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2.84"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.4"
console_error_panic_hook = { version = "0.1.7", optional = true }
[dev-dependencies]
wasm-bindgen-test = "0.3.34"

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