66 Commits

Author SHA1 Message Date
Max Richter
ef479d0557 chore: update
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 3m50s
2025-12-02 17:31:58 +01:00
Max Richter
a1c926c3cf fix: better handle randomGeneration 2025-12-02 17:27:34 +01:00
ca8b1e15ac chore: cleanup edge and node code
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m8s
2025-12-02 16:59:43 +01:00
4878d02705 refactor: remove unneeded random var in node 2025-12-02 16:59:29 +01:00
2b4c81f557 fix: make sure new nodes are reactive
Closes #7
2025-12-02 16:59:11 +01:00
d178f812fb refactor: move event handlers to own classes 2025-12-02 16:58:31 +01:00
669a2c7991 docs: remove placeholder content in readme 2025-12-02 15:20:26 +01:00
becd7a1eb3 fix: make sure we do not pass svelte state into comlink
cant clone proxies
2025-12-02 15:20:13 +01:00
d140f42468 feat: better a18n for node parameters
Dunno of a18n would even be possible for the node graph
2025-12-02 15:19:48 +01:00
be835e5cff fix: better stroke width and color for edges 2025-12-02 15:00:41 +01:00
Max Richter
6229becfd8 fix: display add menu at correct position
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m58s
2025-12-01 22:39:43 +01:00
Max Richter
af944cefaa chore: disable cache from runtime executor 2025-12-01 22:39:06 +01:00
Max Richter
a1ea56093c fix: correctly handle node wrapper resizing
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m57s
2025-12-01 19:48:40 +01:00
Max Richter
1850e21810 fix: make clipboard work 2025-12-01 19:30:44 +01:00
Max Richter
7e51cc5ea1 chore: some updates
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m58s
2025-12-01 18:29:47 +01:00
Max Richter
1ea544e765 chore: rename @nodes -> @nodarium for everything
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 3m33s
2025-12-01 17:03:14 +01:00
e5658b8a7e feat: initial auto connect nodes
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m35s
2025-11-26 17:27:32 +01:00
d3a9b3f056 fix: make node wasm loading work
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m32s
2025-11-26 12:10:25 +01:00
0894141d3e fix: correctly load nodes from cache/fetch
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 4m9s
2025-11-26 11:09:19 +01:00
925167d9f2 feat: setup antialising on grids 2025-11-26 11:08:57 +01:00
Max Richter
9c4554a1f0 chore: log some more stuff during registry
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m31s
2025-11-24 22:42:08 +01:00
Max Richter
67a104ff84 chore: add some more logs
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m34s
2025-11-24 22:25:01 +01:00
Max Richter
1212c28152 feat: enable some debug logs
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m45s
2025-11-24 22:09:54 +01:00
Max Richter
cfcb447784 feat: update some more components to svelte 5
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m48s
2025-11-24 21:11:16 +01:00
Max Richter
d64877666b fix: 120 type errors
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m47s
2025-11-24 00:10:38 +01:00
Max Richter
0fa1b64d49 feat: update ci to cache pnpm and rust
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m40s
2025-11-23 19:40:50 +01:00
Max Richter
6ca1ff2a34 chore: move some more components to svelte 5
Some checks failed
Deploy to GitHub Pages / build_site (push) Has been cancelled
2025-11-23 19:35:33 +01:00
Max Richter
716df245ab fix: trying to correctly setup sftp
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m40s
2025-11-23 16:09:57 +01:00
Max Richter
2e76202c63 fix: trying to correctly setup sftp 2025-11-23 16:07:26 +01:00
Max Richter
7818148b12 chore: pnpm up -r latest
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 4m6s
2025-11-23 16:01:59 +01:00
Max Richter
566b287550 chore: upgrade ci docker image
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 2m30s
2025-11-23 15:39:16 +01:00
Max Richter
62d3f58d86 chore: update pnpm to latest 2025-11-23 15:19:27 +01:00
Max Richter
c868818ba2 feat: use local node registry again
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 2m56s
2025-11-23 15:15:50 +01:00
Max Richter
64ea7ac349 chore: make some things a bit more typesafe 2025-11-23 15:15:38 +01:00
Max Richter
2dcd797762 chore: pnpm upgrade 2025-11-23 15:15:15 +01:00
05b192e7ab commit to trigger deploy 2025-01-15 19:30:12 +01:00
edcaab4bd4 fix: use correct url 2025-01-15 18:17:04 +01:00
a99040f42e feat: some shit
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 47s
2024-12-20 16:35:23 +01:00
fca59e87e5 feat: some shit 2024-12-20 16:35:16 +01:00
05e8970475 feat: use remote registry
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m4s
2024-12-20 16:11:30 +01:00
385d1dd831 feat: use remote registry
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m2s
2024-12-20 16:06:18 +01:00
dc46c4b64c feat: some stuff
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m0s
2024-12-20 15:55:45 +01:00
15ff1cc52d feat: some shit
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m4s
2024-12-20 15:24:54 +01:00
a70e8195a2 feat: add some more versining stuff 2024-12-20 14:06:33 +01:00
4ca36b324b fix: some shit
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m55s
2024-12-20 13:40:14 +01:00
221817fc16 fix: some node
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m1s
2024-12-20 12:51:56 +01:00
7060b37df5 fix: error in schema
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 36s
2024-12-20 12:49:25 +01:00
ec037a3bbd fix: error in schema
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 36s
2024-12-20 12:46:44 +01:00
2814165ee6 fix: run migrations in code
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 36s
2024-12-20 12:42:45 +01:00
c6badff1ee fix: dockerfile
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 36s
2024-12-20 12:36:11 +01:00
a0d420517c feat: some shit
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 36s
2024-12-20 12:21:50 +01:00
eadd37bfa4 feat: some shit
Some checks failed
Deploy to GitHub Pages / build_site (push) Has been cancelled
2024-12-20 12:21:46 +01:00
9d698be86f fix: dockerfile
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 38s
2024-12-20 12:09:30 +01:00
540d0549d7 feat: some shit
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 37s
2024-12-19 23:55:07 +01:00
a740da1099 feat: merge svelte-5
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 38s
2024-12-19 18:31:19 +01:00
33d5ed14dd WIP 2024-12-19 18:28:17 +01:00
53f400a4f6 fix: some svelte 5 issues 2024-12-19 15:35:22 +01:00
74b7cc4232 WIP 2024-12-19 15:09:17 +01:00
972fd39da2 feat: disable remote node registry
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m59s
2024-12-18 18:19:46 +01:00
00a9d5b532 feat: add analytics
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m3s
2024-12-18 18:17:10 +01:00
c7d1c28c83 fix: disable story building (for now)
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m13s
2024-12-18 18:14:56 +01:00
43d525f1d9 fix(ci): build scripts
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 1m29s
2024-12-18 18:12:06 +01:00
48175aade0 chore: update lockfile
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 1m31s
2024-12-18 18:07:21 +01:00
6d6ca6f888 feat: some update
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 7s
2024-12-18 18:06:48 +01:00
0c9a9269c4 feat: some shit
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 6s
2024-12-18 17:26:24 +01:00
9d4d67f086 feat: initial backend store prototype
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 13s
2024-12-17 18:15:21 +01:00
130 changed files with 8575 additions and 5975 deletions

View File

@@ -2,7 +2,7 @@ name: Deploy to GitHub Pages
on: on:
push: push:
branches: 'main' branches: "main"
jobs: jobs:
build_site: build_site:
@@ -13,7 +13,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install --frozen-lockfile
- name: build - name: build
run: pnpm run build:deploy run: pnpm run build:deploy
@@ -23,7 +23,7 @@ jobs:
echo "$SSH_PRIVATE_KEY" > /tmp/id_rsa echo "$SSH_PRIVATE_KEY" > /tmp/id_rsa
chmod 600 /tmp/id_rsa chmod 600 /tmp/id_rsa
mkdir -p ~/.config/rclone mkdir -p ~/.config/rclone
echo "[sftp-remote]\ntype = sftp\nhost = ${SSH_HOST}\nuser = ${SSH_USER}\nport = ${SSH_PORT}\nkey_file = /tmp/id_rsa" > ~/.config/rclone/rclone.conf echo -e "[sftp-remote]\ntype = sftp\nhost = ${SSH_HOST}\nuser = ${SSH_USER}\nport = ${SSH_PORT}\nkey_file = /tmp/id_rsa" > ~/.config/rclone/rclone.conf
env: env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ vars.SSH_HOST }} SSH_HOST: ${{ vars.SSH_HOST }}

View File

@@ -1,18 +1,21 @@
FROM node:21 FROM node:24-alpine
# IMAGE CUSTOMISATIONS RUN apk add --no-cache --update curl rclone g++
# Install rust
# https://github.com/rust-lang/rustup/issues/1085
RUN RUSTUP_URL="https://sh.rustup.rs" \ 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 \ && curl --silent --show-error --location --fail --retry 3 --proto '=https' --tlsv1.2 --output /tmp/rustup-linux-install.sh $RUSTUP_URL \
&& bash /tmp/rustup-linux-install.sh -y \ && sh /tmp/rustup-linux-install.sh -y
&& curl https://rclone.org/install.sh --output /tmp/rclone-install.sh \
&& bash /tmp/rclone-install.sh
ENV PATH=/root/.cargo/bin:$PATH ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH
RUN rustup target add wasm32-unknown-unknown \ RUN curl --silent --show-error --location --fail --retry 3 \
--proto '=https' --tlsv1.2 \
--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 \ && cargo install wasm-pack \
&& npm i -g pnpm && npm i -g pnpm

View File

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

View File

@@ -1,5 +1,5 @@
{ {
"name": "@nodes/app", "name": "@nodarium/app",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
@@ -10,37 +10,37 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@nodes/registry": "link:../packages/registry", "@nodarium/registry": "link:../packages/registry",
"@nodes/ui": "link:../packages/ui", "@nodarium/ui": "link:../packages/ui",
"@nodes/utils": "link:../packages/utils", "@nodarium/utils": "link:../packages/utils",
"@sveltejs/kit": "^2.7.4", "@sveltejs/kit": "^2.49.0",
"@threlte/core": "8.0.0-next.23", "@threlte/core": "8.3.0",
"@threlte/extras": "9.0.0-next.33", "@threlte/extras": "9.7.0",
"@types/three": "^0.169.0", "@types/three": "^0.181.0",
"@unocss/reset": "^0.63.6", "@unocss/reset": "^66.5.9",
"comlink": "^4.4.1", "comlink": "^4.4.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"idb": "^8.0.0", "idb": "^8.0.3",
"jsondiffpatch": "^0.6.0", "jsondiffpatch": "^0.7.3",
"three": "^0.170.0" "three": "^0.181.2"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/tabler": "^1.2.7", "@iconify-json/tabler": "^1.2.23",
"@nodes/types": "link:../packages/types", "@nodarium/types": "link:../packages/types",
"@sveltejs/adapter-static": "^3.0.6", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tsconfig/svelte": "^5.0.4", "@tsconfig/svelte": "^5.0.6",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@unocss/preset-icons": "^0.63.6", "@unocss/preset-icons": "^66.5.9",
"svelte": "^5.1.9", "svelte": "^5.43.14",
"svelte-check": "^4.0.5", "svelte-check": "^4.3.4",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.6.3", "typescript": "^5.9.3",
"unocss": "^0.63.6", "unocss": "^66.5.9",
"vite": "^5.4.10", "vite": "^7.2.4",
"vite-plugin-comlink": "^5.1.0", "vite-plugin-comlink": "^5.3.0",
"vite-plugin-glsl": "^1.3.0", "vite-plugin-glsl": "^1.5.4",
"vite-plugin-wasm": "^3.3.0", "vite-plugin-wasm": "^3.5.0",
"vitest": "^2.1.4" "vitest": "^4.0.13"
} }
} }

View File

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

View File

@@ -1,4 +1,6 @@
precision highp float; precision highp float;
// For WebGL1 make sure this extension is enabled in your material:
// #extension GL_OES_standard_derivatives : enable
varying vec2 vUv; varying vec2 vUv;
@@ -10,33 +12,45 @@ uniform vec2 zoomLimits;
uniform vec3 backgroundColor; uniform vec3 backgroundColor;
uniform vec3 lineColor; uniform vec3 lineColor;
// Anti-aliased step: threshold in the same units as `value`
float aaStep(float threshold, float value, float deriv) {
float w = deriv * 0.5; // ~one pixel
return smoothstep(threshold - w, threshold + w, value);
}
float grid(float x, float y, float divisions, float thickness) { float grid(float x, float y, float divisions, float thickness) {
x = fract(x * divisions); // Continuous grid coordinates
x = min(x, 1.0 - x); float gx = x * divisions;
float gy = y * divisions;
float xdelta = fwidth(x); // Distance to nearest grid line (0 at the line)
x = smoothstep(x - xdelta, x + xdelta, thickness); float fx = fract(gx);
fx = min(fx, 1.0 - fx);
float fy = fract(gy);
fy = min(fy, 1.0 - fy);
y = fract(y * divisions); // Derivatives in screen space use the continuous coords here
y = min(y, 1.0 - y); float dx = fwidth(gx);
float dy = fwidth(gy);
float ydelta = fwidth(y); // Keep the original semantics: thickness is the threshold in the [0, 0.5] distance domain
y = smoothstep(y - ydelta, y + ydelta, thickness); float lineX = 1.0 - aaStep(thickness, fx, dx);
float lineY = 1.0 - aaStep(thickness, fy, dy);
return clamp(x + y, 0.0, 1.0); return clamp(lineX + lineY, 0.0, 1.0);
} }
float circle_grid(float x, float y, float divisions, float circleRadius) { float circle_grid(float x, float y, float divisions, float circleRadius) {
float gridX = mod(x + divisions * 0.5, divisions) - divisions * 0.5;
float gridY = mod(y + divisions * 0.5, divisions) - divisions * 0.5;
float gridX = mod(x + divisions/2.0, divisions) - divisions / 2.0; vec2 g = vec2(gridX, gridY);
float gridY = mod(y + divisions/2.0, divisions) - divisions / 2.0; float d = length(g);
// Calculate the distance from the center of the grid // Screen-space derivative for AA on the circle edge
float gridDistance = length(vec2(gridX, gridY)); float w = fwidth(d);
// Use smoothstep to create a smooth transition at the edges of the circle
float circle = 1.0 - smoothstep(circleRadius - 0.5, circleRadius + 0.5, gridDistance);
float circle = 1.0 - smoothstep(circleRadius - w, circleRadius + w, d);
return circle; return circle;
} }
@@ -56,44 +70,43 @@ void main(void) {
float minZ = zoomLimits.x; float minZ = zoomLimits.x;
float maxZ = zoomLimits.y; float maxZ = zoomLimits.y;
float divisions = 0.1/cz; float divisions = 0.1 / cz;
float thickness = 0.05/cz; float thickness = 0.05 / cz;
float delta = 0.1 / 2.0;
float nz = (cz - minZ) / (maxZ - minZ); float nz = (cz - minZ) / (maxZ - minZ);
float ux = (vUv.x-0.5) * width + cx*cz; float ux = (vUv.x - 0.5) * width + cx * cz;
float uy = (vUv.y-0.5) * height - cy*cz; float uy = (vUv.y - 0.5) * height - cy * cz;
// extra small grid
//extra small grid float m1 = grid(ux, uy, divisions * 4.0, thickness * 4.0) * 0.9;
float m1 = grid(ux, uy, divisions*4.0, thickness*4.0) * 0.9; float m2 = grid(ux, uy, divisions * 16.0, thickness * 16.0) * 0.5;
float m2 = grid(ux, uy, divisions*16.0, thickness*16.0) * 0.5;
float xsmall = max(m1, m2); float xsmall = max(m1, m2);
float s3 = circle_grid(ux, uy, cz/1.6, 1.0) * 0.5; float s3 = circle_grid(ux, uy, cz / 1.6, 1.0) * 0.5;
xsmall = max(xsmall, s3); xsmall = max(xsmall, s3);
// small grid // small grid
float c1 = grid(ux, uy, divisions, thickness) * 0.6; float c1 = grid(ux, uy, divisions, thickness) * 0.6;
float c2 = grid(ux, uy, divisions*2.0, thickness) * 0.5; float c2 = grid(ux, uy, divisions * 2.0, thickness * 2.0) * 0.5;
float small = max(c1, c2); float small = max(c1, c2);
float s1 = circle_grid(ux, uy, cz*10.0, 2.0) * 0.5; float s1 = circle_grid(ux, uy, cz * 10.0, 2.0) * 0.5;
small = max(small, s1); small = max(small, s1);
// large grid // large grid
float c3 = grid(ux, uy, divisions/8.0, thickness/8.0) * 0.5; float c3 = grid(ux, uy, divisions / 8.0, thickness / 8.0) * 0.5;
float c4 = grid(ux, uy, divisions/2.0, thickness/4.0) * 0.4; float c4 = grid(ux, uy, divisions / 2.0, thickness / 4.0) * 0.4;
float large = max(c3, c4); float large = max(c3, c4);
float s2 = circle_grid(ux, uy, cz*20.0, 1.0) * 0.4; float s2 = circle_grid(ux, uy, cz * 20.0, 1.0) * 0.4;
large = max(large, s2); large = max(large, s2);
float c = mix(large, small, min(nz*2.0+0.05, 1.0)); float c = mix(large, small, min(nz * 2.0 + 0.05, 1.0));
c = mix(c, xsmall, max(min((nz-0.3)/0.7, 1.0), 0.0)); c = mix(c, xsmall, clamp((nz - 0.3) / 0.7, 0.0, 1.0));
vec3 color = mix(backgroundColor, lineColor, c); vec3 color = mix(backgroundColor, lineColor, c);
gl_FragColor = vec4(color, 1.0); gl_FragColor = vec4(color, 1.0);
} }

View File

@@ -1,21 +0,0 @@
<script lang="ts">
import type { Hst } from "@histoire/plugin-svelte";
export let Hst: Hst;
import Background from "./Background.svelte";
import { Canvas } from "@threlte/core";
import Camera from "../Camera.svelte";
let width = globalThis.innerWidth || 100;
let height = globalThis.innerHeight || 100;
let cameraPosition: [number, number, number] = [0, 1, 0];
</script>
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />
<Hst.Story>
<Canvas shadows={false}>
<Camera bind:position={cameraPosition} />
<Background {cameraPosition} {width} {height} />
</Canvas>
</Hst.Story>

View File

@@ -3,7 +3,6 @@
import BackgroundVert from "./Background.vert"; import BackgroundVert from "./Background.vert";
import BackgroundFrag from "./Background.frag"; import BackgroundFrag from "./Background.frag";
import { colors } from "../graph/colors.svelte"; import { colors } from "../graph/colors.svelte";
import { Color } from "three";
import { appSettings } from "$lib/settings/app-settings.svelte"; import { appSettings } from "$lib/settings/app-settings.svelte";
type Props = { type Props = {
@@ -42,10 +41,10 @@
value: [0, 1, 0], value: [0, 1, 0],
}, },
backgroundColor: { backgroundColor: {
value: colors["layer-0"].clone(), value: colors["layer-0"],
}, },
lineColor: { lineColor: {
value: colors["outline"].clone(), value: colors["outline"],
}, },
zoomLimits: { zoomLimits: {
value: [2, 50], value: [2, 50],
@@ -55,9 +54,9 @@
}, },
}} }}
uniforms.camPos.value={cameraPosition} uniforms.camPos.value={cameraPosition}
uniforms.backgroundColor.value={appSettings.theme && uniforms.backgroundColor.value={appSettings.value.theme &&
colors["layer-0"].clone()} colors["layer-0"]}
uniforms.lineColor.value={appSettings.theme && colors["outline"].clone()} uniforms.lineColor.value={appSettings.value.theme && colors["outline"]}
uniforms.zoomLimits.value={[minZoom, maxZoom]} uniforms.zoomLimits.value={[minZoom, maxZoom]}
uniforms.dimensions.value={[width, height]} uniforms.dimensions.value={[width, height]}
/> />

View File

@@ -1,26 +1,35 @@
<script lang="ts"> <script lang="ts">
import type { GraphManager } from "./graph-manager.js";
import { HTML } from "@threlte/extras"; import { HTML } from "@threlte/extras";
import { onMount } from "svelte"; import { onMount } from "svelte";
import type { Node, NodeType } from "@nodarium/types";
import { getGraphManager, getGraphState } from "../graph/state.svelte";
export let position: [x: number, y: number] | null; type Props = {
onnode: (n: Node) => void;
};
export let graph: GraphManager; const { onnode }: Props = $props();
const graph = getGraphManager();
const graphState = getGraphState();
let input: HTMLInputElement; let input: HTMLInputElement;
let value: string = ""; let value = $state<string>();
let activeNodeId: string = ""; let activeNodeId = $state<NodeType>();
const allNodes = graph.getNodeDefinitions(); const allNodes = graphState.activeSocket
? graph.getPossibleNodes(graphState.activeSocket)
: graph.getNodeDefinitions();
function filterNodes() { function filterNodes() {
return allNodes.filter((node) => node.id.includes(value)); return allNodes.filter((node) => node.id.includes(value ?? ""));
} }
$: nodes = value === "" ? allNodes : filterNodes(); const nodes = $derived(value === "" ? allNodes : filterNodes());
$: if (nodes) { $effect(() => {
if (activeNodeId === "") { if (nodes) {
activeNodeId = nodes[0].id; if (activeNodeId === undefined) {
activeNodeId = nodes?.[0]?.id;
} else if (nodes.length) { } else if (nodes.length) {
const node = nodes.find((node) => node.id === activeNodeId); const node = nodes.find((node) => node.id === activeNodeId);
if (!node) { if (!node) {
@@ -28,12 +37,23 @@
} }
} }
} }
});
function handleNodeCreation(nodeType: Node["type"]) {
if (!graphState.addMenuPosition) return;
onnode?.({
id: -1,
type: nodeType,
position: [...graphState.addMenuPosition],
props: {},
});
}
function handleKeyDown(event: KeyboardEvent) { function handleKeyDown(event: KeyboardEvent) {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
if (event.key === "Escape") { if (event.key === "Escape") {
position = null; graphState.addMenuPosition = null;
return; return;
} }
@@ -50,9 +70,8 @@
} }
if (event.key === "Enter") { if (event.key === "Enter") {
if (activeNodeId && position) { if (activeNodeId && graphState.addMenuPosition) {
graph.createNode({ type: activeNodeId, position }); handleNodeCreation(activeNodeId);
position = null;
} }
return; return;
} }
@@ -64,7 +83,11 @@
}); });
</script> </script>
<HTML position.x={position?.[0]} position.z={position?.[1]} transform={false}> <HTML
position.x={graphState.addMenuPosition?.[0]}
position.z={graphState.addMenuPosition?.[1]}
transform={false}
>
<div class="add-menu-wrapper"> <div class="add-menu-wrapper">
<div class="header"> <div class="header">
<input <input
@@ -74,7 +97,7 @@
role="searchbox" role="searchbox"
placeholder="Search..." placeholder="Search..."
disabled={false} disabled={false}
on:keydown={handleKeyDown} onkeydown={handleKeyDown}
bind:value bind:value
bind:this={input} bind:this={input}
/> />
@@ -87,25 +110,17 @@
role="treeitem" role="treeitem"
tabindex="0" tabindex="0"
aria-selected={node.id === activeNodeId} aria-selected={node.id === activeNodeId}
on:keydown={(event) => { onkeydown={(event) => {
if (event.key === "Enter") { if (event.key === "Enter") {
if (position) { handleNodeCreation(node.id);
graph.createNode({ type: node.id, position, props: {} });
position = null;
}
} }
}} }}
on:mousedown={() => { onmousedown={() => handleNodeCreation(node.id)}
if (position) { onfocus={() => {
graph.createNode({ type: node.id, position, props: {} });
position = null;
}
}}
on:focus={() => {
activeNodeId = node.id; activeNodeId = node.id;
}} }}
class:selected={node.id === activeNodeId} class:selected={node.id === activeNodeId}
on:mouseover={() => { onmouseover={() => {
activeNodeId = node.id; activeNodeId = node.id;
}} }}
> >

View File

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

View File

@@ -5,14 +5,16 @@
color: colors.edge.clone(), color: colors.edge.clone(),
toneMapped: false, toneMapped: false,
}); });
let lineColor = $state(colors.edge.clone().convertSRGBToLinear());
$effect.root(() => { $effect.root(() => {
$effect(() => { $effect(() => {
appSettings.theme; appSettings.value.theme;
circleMaterial.color = colors.edge.clone().convertSRGBToLinear(); circleMaterial.color = colors.edge.clone().convertSRGBToLinear();
}) lineColor = colors.edge.clone().convertSRGBToLinear();
});
}); });
const lineCache = new Map<number, BufferGeometry>();
const curve = new CubicBezierCurve( const curve = new CubicBezierCurve(
new Vector2(0, 0), new Vector2(0, 0),
@@ -25,43 +27,36 @@
<script lang="ts"> <script lang="ts">
import { T } from "@threlte/core"; import { T } from "@threlte/core";
import { MeshLineMaterial } from "@threlte/extras"; import { MeshLineMaterial } from "@threlte/extras";
import { BufferGeometry, MeshBasicMaterial, Vector3 } from "three"; import { Mesh, MeshBasicMaterial, Vector3 } from "three";
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js"; import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
import { Vector2 } from "three/src/math/Vector2.js"; import { Vector2 } from "three/src/math/Vector2.js";
import { createEdgeGeometry } from "./createEdgeGeometry.js"; import { createEdgeGeometry } from "./createEdgeGeometry.js";
import { appSettings } from "$lib/settings/app-settings.svelte"; import { appSettings } from "$lib/settings/app-settings.svelte";
type Props = { type Props = {
from: { x: number; y: number }; x1: number;
to: { x: number; y: number }; y1: number;
z:number; x2: number;
y2: number;
z: number;
}; };
const { from, to, z }: Props = $props(); const { x1, y1, x2, y2, z }: Props = $props();
let geometry: BufferGeometry|null = $state(null); const thickness = $derived(Math.max(0.001, 0.00082 * Math.exp(0.055 * z)));
const lineColor = $derived(appSettings.theme && colors.edge.clone().convertSRGBToLinear()); let mesh = $state<Mesh>();
let lastId: number | null = null; let lastId: string | null = null;
const primeA = 31;
const primeB = 37;
function update() { function update() {
const new_x = to.x - from.x; const new_x = x2 - x1;
const new_y = to.y - from.y; const new_y = y2 - y1;
const curveId = new_x * primeA + new_y * primeB; const curveId = `${x1}-${y1}-${x2}-${y2}`;
if (lastId === curveId) { if (lastId === curveId) {
return; return;
} }
lastId = curveId;
const mid = new Vector2(new_x / 2, new_y / 2);
if (lineCache.has(curveId)) {
geometry = lineCache.get(curveId)!;
return;
}
const length = Math.floor( const length = Math.floor(
Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4, Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4,
@@ -70,8 +65,8 @@
const samples = Math.max(length * 16, 10); const samples = Math.max(length * 16, 10);
curve.v0.set(0, 0); curve.v0.set(0, 0);
curve.v1.set(mid.x, 0); curve.v1.set(new_x / 2, 0);
curve.v2.set(mid.x, new_y); curve.v2.set(new_x / 2, new_y);
curve.v3.set(new_x, new_y); curve.v3.set(new_x, new_y);
const points = curve const points = curve
@@ -79,21 +74,21 @@
.map((p) => new Vector3(p.x, 0, p.y)) .map((p) => new Vector3(p.x, 0, p.y))
.flat(); .flat();
geometry = createEdgeGeometry(points); if (mesh) {
lineCache.set(curveId, geometry); mesh.geometry = createEdgeGeometry(points);
}; }
}
$effect(() => { $effect(() => {
if (from || to) { if (x1 || x2 || y1 || y2) {
update(); update();
} }
}); });
</script> </script>
<T.Mesh <T.Mesh
position.x={from.x} position.x={x1}
position.z={from.y} position.z={y1}
position.y={0.8} position.y={0.8}
rotation.x={-Math.PI / 2} rotation.x={-Math.PI / 2}
material={circleMaterial} material={circleMaterial}
@@ -102,8 +97,8 @@
</T.Mesh> </T.Mesh>
<T.Mesh <T.Mesh
position.x={to.x} position.x={x2}
position.z={to.y} position.z={y2}
position.y={0.8} position.y={0.8}
rotation.x={-Math.PI / 2} rotation.x={-Math.PI / 2}
material={circleMaterial} material={circleMaterial}
@@ -111,8 +106,6 @@
<T.CircleGeometry args={[0.5, 16]} /> <T.CircleGeometry args={[0.5, 16]} />
</T.Mesh> </T.Mesh>
{#if geometry} <T.Mesh bind:ref={mesh} position.x={x1} position.z={y1} position.y={0.1}>
<T.Mesh position.x={from.x} position.z={from.y} position.y={0.1} {geometry}> <MeshLineMaterial width={thickness} color={lineColor} />
<MeshLineMaterial width={Math.max(z*0.0001,0.00001)} color={lineColor} /> </T.Mesh>
</T.Mesh>
{/if}

View File

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

View File

@@ -6,7 +6,7 @@ export function createEdgeGeometry(points: Vector3[]) {
const length = points[0].distanceTo(points[points.length - 1]); const length = points[0].distanceTo(points[points.length - 1]);
const startRadius = 10.5; const startRadius = 8;
const constantWidth = 2; const constantWidth = 2;
const taperFraction = 0.8 / length; const taperFraction = 0.8 / length;
@@ -37,8 +37,6 @@ export function createEdgeGeometry(points: Vector3[]) {
let indices: number[] = [] let indices: number[] = []
let indicesIndex = 0 let indicesIndex = 0
for (let j = 0; j < pointCount; j++) { for (let j = 0; j < pointCount; j++) {
const c = j / points.length const c = j / points.length
counters[counterIndex + 0] = c counters[counterIndex + 0] = c
@@ -73,8 +71,6 @@ export function createEdgeGeometry(points: Vector3[]) {
geometry.setAttribute('uv', new BufferAttribute(new Float32Array(uvArray), 2)) geometry.setAttribute('uv', new BufferAttribute(new Float32Array(uvArray), 2))
geometry.setIndex(new BufferAttribute(new Uint16Array(indices), 1)) geometry.setIndex(new BufferAttribute(new Uint16Array(indices), 1))
let positions: number[] = [] let positions: number[] = []
let previous: number[] = [] let previous: number[] = []
let next: number[] = [] let next: number[] = []

View File

@@ -1,86 +1,125 @@
import type { Edge, Graph, Node, NodeInput, NodeRegistry, Socket, } from "@nodes/types"; import type {
import { fastHashString } from "@nodes/utils"; Edge,
import { get, writable, type Writable } from "svelte/store"; Graph,
import EventEmitter from "./helpers/EventEmitter.js"; Node,
import { createLogger } from "./helpers/index.js"; NodeDefinition,
import throttle from "./helpers/throttle.js"; NodeInput,
import { HistoryManager } from "./history-manager.js"; NodeRegistry,
NodeType,
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(); logger.mute();
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) { function areSocketsCompatible(
output: string | undefined,
inputs: string | (string | undefined)[] | undefined,
) {
if (Array.isArray(inputs) && output) { if (Array.isArray(inputs) && output) {
return inputs.includes(output); return inputs.includes(output);
} }
return inputs === output; return inputs === output;
} }
export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "settings": { types: Record<string, NodeInput>, values: Record<string, unknown> } }> { function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
status: Writable<"loading" | "idle" | "error"> = writable("loading"); if (firstEdge[0].id !== secondEdge[0].id) {
return false;
}
if (firstEdge[1] !== secondEdge[1]) {
return false
}
if (firstEdge[2].id !== secondEdge[2].id) {
return false
}
if (firstEdge[3] !== secondEdge[3]) {
return false
}
return true
}
export class GraphManager extends EventEmitter<{
save: Graph;
result: any;
settings: {
types: Record<string, NodeInput>;
values: Record<string, unknown>;
};
}> {
status = $state<"loading" | "idle" | "error">();
loaded = false; loaded = false;
graph: Graph = { id: 0, nodes: [], edges: [] }; graph: Graph = { id: 0, nodes: [], edges: [] };
id = writable(0); id = $state(0);
private _nodes: Map<number, Node> = new Map(); nodes = new SvelteMap<number, Node>();
nodes: Writable<Map<number, Node>> = writable(new Map());
private _edges: Edge[] = []; edges = $state<Edge[]>([]);
edges: Writable<Edge[]> = writable([]);
settingTypes: Record<string, NodeInput> = {}; settingTypes: Record<string, NodeInput> = {};
settings: Record<string, unknown> = {}; settings = $state<Record<string, unknown>>();
currentUndoGroup: number | null = null; currentUndoGroup: number | null = null;
inputSockets: Writable<Set<string>> = writable(new Set()); inputSockets = $derived.by(() => {
const s = new Set<string>();
for (const edge of this.edges) {
s.add(`${edge[2].id}-${edge[3]}`);
}
return s;
});
history: HistoryManager = new HistoryManager(); history: HistoryManager = new HistoryManager();
execute = throttle(() => { execute = throttle(() => {
console.log("Props", get(this.nodes).values().find(n => n.type === "max/plantarium/gravity")?.props);
if (this.loaded === false) return; if (this.loaded === false) return;
this.emit("result", this.serialize()); this.emit("result", this.serialize());
}, 10); }, 10);
constructor(public registry: NodeRegistry) { constructor(public registry: NodeRegistry) {
super(); super();
this.nodes.subscribe((nodes) => {
this._nodes = nodes;
});
this.edges.subscribe((edges) => {
this._edges = edges;
const s = new Set<string>();
for (const edge of edges) {
s.add(`${edge[2].id}-${edge[3]}`);
}
this.inputSockets.set(s);
});
} }
serialize(): Graph { serialize(): Graph {
logger.group("serializing graph") const nodes = Array.from(this.nodes.values()).map((node) => ({
const nodes = Array.from(this._nodes.values()).map(node => ({
id: node.id, id: node.id,
position: [...node.position], position: [...node.position],
type: node.type, type: node.type,
props: node.props, props: node.props,
})) as Node[]; })) as Node[];
const edges = this._edges.map(edge => [edge[0].id, edge[1], edge[2].id, edge[3]]) as Graph["edges"]; const edges = this.edges.map((edge) => [
const serialized = { id: this.graph.id, settings: this.settings, nodes, edges }; edge[0].id,
logger.groupEnd(); edge[1],
edge[2].id,
return clone(serialized); edge[3],
]) as Graph["edges"];
const serialized = {
id: this.graph.id,
settings: $state.snapshot(this.settings),
nodes,
edges,
};
logger.log("serializing graph", serialized);
return clone($state.snapshot(serialized));
} }
private lastSettingsHash = 0; private lastSettingsHash = 0;
setSettings(settings: Record<string, unknown>) { setSettings(settings: Record<string, unknown>) {
let hash = fastHashString(JSON.stringify(settings)); let hash = fastHashString(JSON.stringify(settings));
if (hash === this.lastSettingsHash) return; if (hash === this.lastSettingsHash) return;
this.lastSettingsHash = hash; this.lastSettingsHash = hash;
@@ -90,8 +129,6 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
this.execute(); this.execute();
} }
getNodeDefinitions() { getNodeDefinitions() {
return this.registry.getAllNodes(); return this.registry.getAllNodes();
} }
@@ -103,9 +140,9 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
const n = stack.pop(); const n = stack.pop();
if (!n) continue; if (!n) continue;
nodes.add(n); nodes.add(n);
const children = this.getChildrenOfNode(n); const children = this.getChildren(n);
const parents = this.getParentsOfNode(n); const parents = this.getParentsOfNode(n);
const newNodes = [...children, ...parents].filter(n => !nodes.has(n)); const newNodes = [...children, ...parents].filter((n) => !nodes.has(n));
stack.push(...newNodes); stack.push(...newNodes);
} }
return [...nodes.values()]; return [...nodes.values()];
@@ -117,9 +154,16 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
const children = node.tmp?.children || []; const children = node.tmp?.children || [];
for (const child of children) { for (const child of children) {
if (nodes.includes(child)) { if (nodes.includes(child)) {
const edge = this._edges.find(e => e[0].id === node.id && e[2].id === child.id); const edge = this.edges.find(
(e) => e[0].id === node.id && e[2].id === child.id,
);
if (edge) { if (edge) {
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [number, number, number, string]); edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [
number,
number,
number,
string,
]);
} }
} }
} }
@@ -128,25 +172,25 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
return edges; return edges;
} }
private _init(graph: Graph) { private _init(graph: Graph) {
const nodes = new Map(graph.nodes.map(node => { const nodes = new Map(
graph.nodes.map((node: Node) => {
const nodeType = this.registry.getNode(node.type); const nodeType = this.registry.getNode(node.type);
if (nodeType) { if (nodeType) {
node.tmp = { node.tmp = {
random: (Math.random() - 0.5) * 2, type: nodeType,
type: nodeType
}; };
} }
return [node.id, node] return [node.id, node];
})); }),
);
const edges = graph.edges.map((edge) => { const edges = graph.edges.map((edge) => {
const from = nodes.get(edge[0]); const from = nodes.get(edge[0]);
const to = nodes.get(edge[2]); const to = nodes.get(edge[2]);
if (!from || !to) { if (!from || !to) {
throw new Error("Edge references non-existing node"); throw new Error("Edge references non-existing node");
}; }
from.tmp = from.tmp || {}; from.tmp = from.tmp || {};
from.tmp.children = from.tmp.children || []; from.tmp.children = from.tmp.children || [];
from.tmp.children.push(to); from.tmp.children.push(to);
@@ -154,42 +198,50 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
to.tmp.parents = to.tmp.parents || []; to.tmp.parents = to.tmp.parents || [];
to.tmp.parents.push(from); to.tmp.parents.push(from);
return [from, edge[1], to, edge[3]] as Edge; return [from, edge[1], to, edge[3]] as Edge;
}) });
this.edges.set(edges); this.edges = [...edges];
this.nodes.set(nodes);
this.nodes.clear();
for (const [id, node] of nodes) {
this.nodes.set(id, node);
}
this.execute(); this.execute();
} }
async load(graph: Graph) { async load(graph: Graph) {
const a = performance.now(); const a = performance.now();
this.loaded = false; this.loaded = false;
this.graph = graph; this.graph = graph;
this.status.set("loading"); this.status = "loading";
this.id.set(graph.id); this.id = graph.id;
const nodeIds = Array.from(new Set([...graph.nodes.map(n => n.type)])); logger.info("loading graph", $state.snapshot(graph));
const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)]));
await this.registry.load(nodeIds); await this.registry.load(nodeIds);
logger.info("loaded node types", this.registry.getAllNodes());
for (const node of this.graph.nodes) { for (const node of this.graph.nodes) {
const nodeType = this.registry.getNode(node.type); const nodeType = this.registry.getNode(node.type);
if (!nodeType) { if (!nodeType) {
logger.error(`Node type not found: ${node.type}`); logger.error(`Node type not found: ${node.type}`);
this.status.set("error"); this.status = "error";
return; return;
} }
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.random = (Math.random() - 0.5) * 2;
node.tmp.type = nodeType; node.tmp.type = nodeType;
} }
// load settings // load settings
const settingTypes: Record<string, NodeInput> = {}; const settingTypes: Record<
string,
// Optional metadata to map settings to specific nodes
NodeInput & { __node_type: string; __node_input: string }
> = {};
const settingValues = graph.settings || {}; const settingValues = graph.settings || {};
const types = this.getNodeDefinitions(); const types = this.getNodeDefinitions();
for (const type of types) { for (const type of types) {
@@ -197,8 +249,15 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
for (const key in type.inputs) { for (const key in type.inputs) {
let settingId = type.inputs[key].setting; let settingId = type.inputs[key].setting;
if (settingId) { if (settingId) {
settingTypes[settingId] = { __node_type: type.id, __node_input: key, ...type.inputs[key] }; settingTypes[settingId] = {
if (settingValues[settingId] === undefined && "value" in type.inputs[key]) { __node_type: type.id,
__node_input: key,
...type.inputs[key],
};
if (
settingValues[settingId] === undefined &&
"value" in type.inputs[key]
) {
settingValues[settingId] = type.inputs[key].value; settingValues[settingId] = type.inputs[key].value;
} }
} }
@@ -214,28 +273,26 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
this.save(); this.save();
this.status.set("idle"); this.status = "idle";
this.loaded = true; this.loaded = true;
logger.log(`Graph loaded in ${performance.now() - a}ms`); logger.log(`Graph loaded in ${performance.now() - a}ms`);
setTimeout(() => this.execute(), 100); setTimeout(() => this.execute(), 100);
} }
getAllNodes() { getAllNodes() {
return Array.from(this._nodes.values()); return Array.from(this.nodes.values());
} }
getNode(id: number) { getNode(id: number) {
return this._nodes.get(id); return this.nodes.get(id);
} }
getNodeType(id: string) { getNodeType(id: string) {
return this.registry.getNode(id); return this.registry.getNode(id);
} }
async loadNode(id: string) { async loadNodeType(id: NodeType) {
await this.registry.load([id]); await this.registry.load([id]);
const nodeType = this.registry.getNode(id); const nodeType = this.registry.getNode(id);
@@ -248,7 +305,11 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
let settingId = nodeType.inputs[key].setting; let settingId = nodeType.inputs[key].setting;
if (settingId) { if (settingId) {
settingTypes[settingId] = nodeType.inputs[key]; settingTypes[settingId] = nodeType.inputs[key];
if (settingValues[settingId] === undefined && "value" in nodeType.inputs[key]) { if (
settingValues &&
settingValues?.[settingId] === undefined &&
"value" in nodeType.inputs[key]
) {
settingValues[settingId] = nodeType.inputs[key].value; settingValues[settingId] = nodeType.inputs[key].value;
} }
} }
@@ -260,14 +321,14 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
this.emit("settings", { types: settingTypes, values: settingValues }); this.emit("settings", { types: settingTypes, values: settingValues });
} }
getChildrenOfNode(node: Node) { getChildren(node: Node) {
const children = []; const children = [];
const stack = node.tmp?.children?.slice(0); const stack = node.tmp?.children?.slice(0);
while (stack?.length) { while (stack?.length) {
const child = stack.pop(); const child = stack.pop();
if (!child) continue; if (!child) continue;
children.push(child); children.push(child);
stack.push(...child.tmp?.children || []); stack.push(...(child.tmp?.children || []));
} }
return children; return children;
} }
@@ -278,11 +339,11 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
// < - - - - from - - - - to // < - - - - from - - - - to
const fromParents = this.getParentsOfNode(from); const fromParents = this.getParentsOfNode(from);
if (toParents.includes(from)) { if (toParents.includes(from)) {
const fromChildren = this.getChildrenOfNode(from); const fromChildren = this.getChildren(from);
return toParents.filter(n => fromChildren.includes(n)); return toParents.filter((n) => fromChildren.includes(n));
} else if (fromParents.includes(to)) { } else if (fromParents.includes(to)) {
const toChildren = this.getChildrenOfNode(to); const toChildren = this.getChildren(to);
return fromParents.filter(n => toChildren.includes(n)); return fromParents.filter((n) => toChildren.includes(n));
} else { } else {
// these two nodes are not connected // these two nodes are not connected
return; return;
@@ -290,53 +351,61 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
} }
removeNode(node: Node, { restoreEdges = false } = {}) { removeNode(node: Node, { restoreEdges = false } = {}) {
const edgesToNode = this.edges.filter((edge) => edge[2].id === node.id);
const edgesToNode = this._edges.filter((edge) => edge[2].id === node.id); const edgesFromNode = this.edges.filter((edge) => edge[0].id === node.id);
const edgesFromNode = this._edges.filter((edge) => edge[0].id === node.id);
for (const edge of [...edgesToNode, ...edgesFromNode]) { for (const edge of [...edgesToNode, ...edgesFromNode]) {
this.removeEdge(edge, { applyDeletion: false }); this.removeEdge(edge, { applyDeletion: false });
} }
if (restoreEdges) { if (restoreEdges) {
const outputSockets = edgesToNode.map(e => [e[0], e[1]] as const); const outputSockets = edgesToNode.map((e) => [e[0], e[1]] as const);
const inputSockets = edgesFromNode.map(e => [e[2], e[3]] as const); const inputSockets = edgesFromNode.map((e) => [e[2], e[3]] as const);
for (const [to, toSocket] of inputSockets) { for (const [to, toSocket] of inputSockets) {
for (const [from, fromSocket] of outputSockets) { for (const [from, fromSocket] of outputSockets) {
const outputType = from.tmp?.type?.outputs?.[fromSocket]; const outputType = from.tmp?.type?.outputs?.[fromSocket];
const inputType = to?.tmp?.type?.inputs?.[toSocket]?.type; const inputType = to?.tmp?.type?.inputs?.[toSocket]?.type;
if (outputType === inputType) { if (outputType === inputType) {
this.createEdge(from, fromSocket, to, toSocket, { applyUpdate: false }); this.createEdge(from, fromSocket, to, toSocket, {
applyUpdate: false,
});
continue; continue;
} }
} }
} }
} }
this.edges.set(this._edges); this.nodes.delete(node.id);
this.execute();
this.nodes.update((nodes) => {
nodes.delete(node.id);
return nodes;
});
this.execute()
this.save(); this.save();
} }
smartConnect(from: Node, to: Node): Edge | undefined {
const inputs = Object.entries(to.tmp?.type?.inputs ?? {});
const outputs = from.tmp?.type?.outputs ?? [];
for (let i = 0; i < inputs.length; i++) {
const [inputName, input] = inputs[0];
for (let o = 0; o < outputs.length; o++) {
const output = outputs[0];
if (input.type === output) {
return this.createEdge(from, o, to, inputName);
}
}
}
}
createNodeId() { createNodeId() {
const max = Math.max(...this._nodes.keys()); return Math.max(0, ...this.nodes.keys()) + 1;
return max + 1;
} }
createGraph(nodes: Node[], edges: [number, number, number, string][]) { createGraph(nodes: Node[], edges: [number, number, number, string][]) {
// map old ids to new ids // map old ids to new ids
const idMap = new Map<number, number>(); const idMap = new Map<number, number>();
const startId = this.createNodeId(); let startId = this.createNodeId()
nodes = nodes.map((node, i) => { nodes = nodes.map((node) => {
const id = startId + i; const id = startId++;
idMap.set(node.id, id); idMap.set(node.id, id);
const type = this.registry.getNode(node.type); const type = this.registry.getNode(node.type);
if (!type) { if (!type) {
@@ -345,9 +414,9 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
return { ...node, id, tmp: { type } }; return { ...node, id, tmp: { type } };
}); });
const _edges = edges.map(edge => { const _edges = edges.map((edge) => {
const from = nodes.find(n => n.id === idMap.get(edge[0])); const from = nodes.find((n) => n.id === idMap.get(edge[0]));
const to = nodes.find(n => n.id === idMap.get(edge[2])); const to = nodes.find((n) => n.id === idMap.get(edge[2]));
if (!from || !to) { if (!from || !to) {
throw new Error("Edge references non-existing node"); throw new Error("Edge references non-existing node");
@@ -365,45 +434,63 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
}); });
for (const node of nodes) { for (const node of nodes) {
this._nodes.set(node.id, node); this.nodes.set(node.id, node);
} }
this._edges.push(..._edges); this.edges.push(..._edges);
this.nodes.set(this._nodes);
this.edges.set(this._edges);
this.save(); this.save();
return nodes; return nodes;
} }
createNode({ type, position, props = {} }: { type: Node["type"], position: Node["position"], props: Node["props"] }) { createNode({
type,
position,
props = {},
}: {
type: Node["type"];
position: Node["position"];
props: Node["props"];
}) {
const nodeType = this.registry.getNode(type); const nodeType = this.registry.getNode(type);
if (!nodeType) { if (!nodeType) {
logger.error(`Node type not found: ${type}`); logger.error(`Node type not found: ${type}`);
return; return;
} }
const node: Node = { id: this.createNodeId(), type, position, tmp: { type: nodeType }, props }; const node: Node = $state({
id: this.createNodeId(),
this.nodes.update((nodes) => { type,
nodes.set(node.id, node); position,
return nodes; tmp: { type: nodeType },
props,
}); });
this.nodes.set(node.id, node);
this.save(); this.save();
return node
} }
createEdge(from: Node, fromSocket: number, to: Node, toSocket: string, { applyUpdate = true } = {}) { createEdge(
from: Node,
fromSocket: number,
to: Node,
toSocket: string,
{ applyUpdate = true } = {},
): Edge | undefined {
const existingEdges = this.getEdgesToNode(to); const existingEdges = this.getEdgesToNode(to);
// check if this exact edge already exists // check if this exact edge already exists
const existingEdge = existingEdges.find(e => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket); const existingEdge = existingEdges.find(
(e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket,
);
if (existingEdge) { if (existingEdge) {
logger.error("Edge already exists", existingEdge); logger.error("Edge already exists", existingEdge);
return; return;
}; }
// check if socket types match // check if socket types match
const fromSocketType = from.tmp?.type?.outputs?.[fromSocket]; const fromSocketType = from.tmp?.type?.outputs?.[fromSocket];
@@ -413,20 +500,22 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
} }
if (!areSocketsCompatible(fromSocketType, toSocketType)) { if (!areSocketsCompatible(fromSocketType, toSocketType)) {
logger.error(`Socket types do not match: ${fromSocketType} !== ${toSocketType}`); logger.error(
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`,
);
return; return;
} }
const edgeToBeReplaced = this._edges.find(e => e[2].id === to.id && e[3] === toSocket); const edgeToBeReplaced = this.edges.find(
(e) => e[2].id === to.id && e[3] === toSocket,
);
if (edgeToBeReplaced) { if (edgeToBeReplaced) {
this.removeEdge(edgeToBeReplaced, { applyDeletion: false }); this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
} }
if (applyUpdate) { const edge = [from, fromSocket, to, toSocket] as Edge;
this._edges.push([from, fromSocket, to, toSocket]);
} else { this.edges.push(edge);
this._edges.push([from, fromSocket, to, toSocket]);
}
from.tmp = from.tmp || {}; from.tmp = from.tmp || {};
from.tmp.children = from.tmp.children || []; from.tmp.children = from.tmp.children || [];
@@ -437,10 +526,11 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
to.tmp.parents.push(from); to.tmp.parents.push(from);
if (applyUpdate) { if (applyUpdate) {
this.edges.set(this._edges);
this.save(); this.save();
} }
this.execute(); this.execute();
return edge;
} }
undo() { undo() {
@@ -451,14 +541,12 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
} }
} }
redo() { redo() {
const nextState = this.history.redo(); const nextState = this.history.redo();
if (nextState) { if (nextState) {
this._init(nextState); this._init(nextState);
this.emit("save", this.serialize()); this.emit("save", this.serialize());
} }
} }
startUndoGroup() { startUndoGroup() {
@@ -483,30 +571,60 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
const stack = node.tmp?.parents?.slice(0); const stack = node.tmp?.parents?.slice(0);
while (stack?.length) { while (stack?.length) {
if (parents.length > 1000000) { if (parents.length > 1000000) {
logger.warn("Infinite loop detected") logger.warn("Infinite loop detected");
break; break;
} }
const parent = stack.pop(); const parent = stack.pop();
if (!parent) continue; if (!parent) continue;
parents.push(parent); parents.push(parent);
stack.push(...parent.tmp?.parents || []); stack.push(...(parent.tmp?.parents || []));
} }
return parents.reverse(); return parents.reverse();
} }
getPossibleSockets({ node, index }: Socket): [Node, string | number][] { getPossibleNodes(socket: Socket): NodeDefinition[] {
const allDefinitions = this.getNodeDefinitions();
const nodeType = socket.node.tmp?.type;
if (!nodeType) {
return [];
}
const definitions = typeof socket.index === "string"
? allDefinitions.filter(s => {
return s.outputs?.find(_s => Object
.values(nodeType?.inputs || {})
.map(s => s.type)
.includes(_s as NodeInput["type"])
)
})
: allDefinitions.filter(s => Object
.values(s.inputs ?? {})
.find(s => {
if (s.hidden) return false;
if (nodeType.outputs?.includes(s.type)) {
return true
}
return s.accepts?.find(a => nodeType.outputs?.includes(a))
}))
return definitions
}
getPossibleSockets({ node, index }: Socket): [Node, string | number][] {
const nodeType = node?.tmp?.type; const nodeType = node?.tmp?.type;
if (!nodeType) return []; if (!nodeType) return [];
const sockets: [Node, string | number][] = [] const sockets: [Node, string | number][] = [];
// if index is a string, we are an input looking for outputs // if index is a string, we are an input looking for outputs
if (typeof index === "string") { if (typeof index === "string") {
// filter out self and child nodes // filter out self and child nodes
const children = new Set(this.getChildrenOfNode(node).map(n => n.id)); const children = new Set(this.getChildren(node).map((n) => n.id));
const nodes = this.getAllNodes().filter(n => n.id !== node.id && !children.has(n.id)); const nodes = this.getAllNodes().filter(
(n) => n.id !== node.id && !children.has(n.id),
);
const ownType = nodeType?.inputs?.[index].type; const ownType = nodeType?.inputs?.[index].type;
@@ -520,16 +638,21 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
} }
} }
} }
} else if (typeof index === "number") { } else if (typeof index === "number") {
// if index is a number, we are an output looking for inputs // if index is a number, we are an output looking for inputs
// filter out self and parent nodes // filter out self and parent nodes
const parents = new Set(this.getParentsOfNode(node).map(n => n.id)); const parents = new Set(this.getParentsOfNode(node).map((n) => n.id));
const nodes = this.getAllNodes().filter(n => n.id !== node.id && !parents.has(n.id)); const nodes = this.getAllNodes().filter(
(n) => n.id !== node.id && !parents.has(n.id),
);
// get edges from this socket // get edges from this socket
const edges = new Map(this.getEdgesFromNode(node).filter(e => e[1] === index).map(e => [e[2].id, e[3]])); const edges = new Map(
this.getEdgesFromNode(node)
.filter((e) => e[1] === index)
.map((e) => [e[2].id, e[3]]),
);
const ownType = nodeType.outputs?.[index]; const ownType = nodeType.outputs?.[index];
@@ -537,11 +660,13 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
const inputs = node?.tmp?.type?.inputs; const inputs = node?.tmp?.type?.inputs;
if (!inputs) continue; if (!inputs) continue;
for (const key in inputs) { for (const key in inputs) {
const otherType = [inputs[key].type]; const otherType = [inputs[key].type];
otherType.push(...(inputs[key].accepts || [])); otherType.push(...(inputs[key].accepts || []));
if (areSocketsCompatible(ownType, otherType) && edges.get(node.id) !== key) { if (
areSocketsCompatible(ownType, otherType) &&
edges.get(node.id) !== key
) {
sockets.push([node, key]); sockets.push([node, key]);
} }
} }
@@ -549,42 +674,48 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
} }
return sockets; return sockets;
} }
removeEdge(edge: Edge, { applyDeletion = true }: { applyDeletion?: boolean } = {}) { removeEdge(
edge: Edge,
{ applyDeletion = true }: { applyDeletion?: boolean } = {},
) {
const id0 = edge[0].id; const id0 = edge[0].id;
const sid0 = edge[1]; const sid0 = edge[1];
const id2 = edge[2].id; const id2 = edge[2].id;
const sid2 = edge[3]; const sid2 = edge[3];
const _edge = this._edges.find((e) => e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2); const _edge = this.edges.find(
(e) =>
e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2,
);
if (!_edge) return; if (!_edge) return;
edge[0].tmp = edge[0].tmp || {}; edge[0].tmp = edge[0].tmp || {};
if (edge[0].tmp.children) { if (edge[0].tmp.children) {
edge[0].tmp.children = edge[0].tmp.children.filter(n => n.id !== id2); edge[0].tmp.children = edge[0].tmp.children.filter(
(n: Node) => n.id !== id2,
);
} }
edge[2].tmp = edge[2].tmp || {}; edge[2].tmp = edge[2].tmp || {};
if (edge[2].tmp.parents) { if (edge[2].tmp.parents) {
edge[2].tmp.parents = edge[2].tmp.parents.filter(n => n.id !== id0); edge[2].tmp.parents = edge[2].tmp.parents.filter(
(n: Node) => n.id !== id0,
);
} }
this.edges = this.edges.filter((e) => !areEdgesEqual(e, edge));
if (applyDeletion) { if (applyDeletion) {
this.edges.update((edges) => {
return edges.filter(e => e !== _edge);
});
this.execute(); this.execute();
this.save(); this.save();
} else {
this._edges = this._edges.filter(e => e !== _edge);
} }
} }
getEdgesToNode(node: Node) { getEdgesToNode(node: Node) {
return this._edges return this.edges
.filter((edge) => edge[2].id === node.id) .filter((edge) => edge[2].id === node.id)
.map((edge) => { .map((edge) => {
const from = this.getNode(edge[0].id); const from = this.getNode(edge[0].id);
@@ -596,7 +727,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
} }
getEdgesFromNode(node: Node) { getEdgesFromNode(node: Node) {
return this._edges return this.edges
.filter((edge) => edge[0].id === node.id) .filter((edge) => edge[0].id === node.id)
.map((edge) => { .map((edge) => {
const from = this.getNode(edge[0].id); const from = this.getNode(edge[0].id);
@@ -606,7 +737,4 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
}) })
.filter(Boolean) as unknown as [Node, number, Node, string][]; .filter(Boolean) as unknown as [Node, number, Node, string][];
} }
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,14 +1,10 @@
<script lang="ts"> <script lang="ts">
import type { Graph, Node, NodeRegistry } from "@nodes/types"; import type { Graph, Node, NodeRegistry } from "@nodarium/types";
import GraphEl from "./Graph.svelte"; import GraphEl from "./Graph.svelte";
import { GraphManager } from "../graph-manager.js"; import { GraphManager } from "../graph-manager.svelte";
import { setContext } from "svelte";
import { debounce } from "$lib/helpers";
import { createKeyMap } from "$lib/helpers/createKeyMap"; import { createKeyMap } from "$lib/helpers/createKeyMap";
import { GraphState } from "./state.svelte"; import { GraphState, setGraphManager, setGraphState } from "./state.svelte";
import { setupKeymaps } from "../keymaps";
const graphState = new GraphState();
setContext("graphState", graphState);
type Props = { type Props = {
graph: Graph; graph: Graph;
@@ -31,8 +27,8 @@
registry, registry,
settings = $bindable(), settings = $bindable(),
activeNode = $bindable(), activeNode = $bindable(),
showGrid, showGrid = $bindable(true),
snapToGrid, snapToGrid = $bindable(true),
showHelp = $bindable(false), showHelp = $bindable(false),
settingTypes = $bindable(), settingTypes = $bindable(),
onsave, onsave,
@@ -40,31 +36,44 @@
}: Props = $props(); }: Props = $props();
export const keymap = createKeyMap([]); export const keymap = createKeyMap([]);
setContext("keymap", keymap);
export const manager = new GraphManager(registry); export const manager = new GraphManager(registry);
setContext("graphManager", manager); setGraphManager(manager);
const graphState = new GraphState(manager);
$effect(() => {
graphState.showGrid = showGrid;
graphState.snapToGrid = snapToGrid;
graphState.showHelp = showHelp;
});
setGraphState(graphState);
setupKeymaps(keymap, manager, graphState);
$effect(() => { $effect(() => {
if (graphState.activeNodeId !== -1) { if (graphState.activeNodeId !== -1) {
activeNode = manager.getNode(graphState.activeNodeId); activeNode = manager.getNode(graphState.activeNodeId);
} else { } else if (activeNode) {
activeNode = undefined; activeNode = undefined;
} }
}); });
const updateSettings = debounce((s) => { $effect(() => {
manager.setSettings(s); if (!graphState.addMenuPosition) {
}, 200); graphState.edgeEndPosition = null;
graphState.activeSocket = null;
}
});
$effect(() => { $effect(() => {
if (settingTypes && settings) { if (settingTypes && settings) {
updateSettings($state.snapshot(settings)); manager.setSettings(settings);
} }
}); });
manager.on("settings", (_settings) => { manager.on("settings", (_settings) => {
settingTypes = _settings.types; settingTypes = { ...settingTypes, ..._settings.types };
settings = _settings.values; settings = _settings.values;
}); });
@@ -75,4 +84,4 @@
manager.load(graph); manager.load(graph);
</script> </script>
<GraphEl bind:showGrid bind:snapToGrid bind:showHelp /> <GraphEl {keymap} />

View File

@@ -1,5 +1,5 @@
import { appSettings } from "$lib/settings/app-settings.svelte"; import { appSettings } from "$lib/settings/app-settings.svelte";
import { Color } from "three"; import { Color, LinearSRGBColorSpace } from "three";
const variables = [ const variables = [
"layer-0", "layer-0",
@@ -12,20 +12,23 @@ const variables = [
"edge", "edge",
] as const; ] as const;
function getColor(variable: typeof variables[number]) { function getColor(variable: (typeof variables)[number]) {
const style = getComputedStyle(document.body.parentElement!); const style = getComputedStyle(document.body.parentElement!);
let color = style.getPropertyValue(`--${variable}`); let color = style.getPropertyValue(`--${variable}`);
return new Color().setStyle(color); return new Color().setStyle(color, LinearSRGBColorSpace);
} }
export const colors = Object.fromEntries(variables.map(v => [v, getColor(v)])) as Record<typeof variables[number], Color>; export const colors = Object.fromEntries(
variables.map((v) => [v, getColor(v)]),
) as Record<(typeof variables)[number], Color>;
$effect.root(() => { $effect.root(() => {
$effect(() => { $effect(() => {
if (!appSettings.theme || !("getComputedStyle" in globalThis)) return; if (!appSettings.value.theme || !("getComputedStyle" in globalThis)) return;
const style = getComputedStyle(document.body.parentElement!); const style = getComputedStyle(document.body.parentElement!);
for (const v of variables) { for (const v of variables) {
colors[v].setStyle(style.getPropertyValue(`--${v}`)); const hex = style.getPropertyValue(`--${v}`);
colors[v].setStyle(hex, LinearSRGBColorSpace);
} }
}); });
}) });

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,15 @@
import throttle from './throttle.js'; import throttle from "$lib/helpers/throttle";
type EventMap = Record<string, unknown>; type EventMap = Record<string, unknown>;
type EventKey<T extends EventMap> = string & keyof T; type EventKey<T extends EventMap> = string & keyof T;
type EventReceiver<T> = (params: T, stuff?: Record<string, unknown>) => unknown; type EventReceiver<T> = (params: T, stuff?: Record<string, unknown>) => unknown;
export default class EventEmitter<
export default class EventEmitter<T extends EventMap = { [key: string]: unknown }> { T extends EventMap = { [key: string]: unknown },
> {
index = 0; index = 0;
public eventMap: T = {} as T; public eventMap: T = {} as T;
constructor() { constructor() {}
}
private cbs: { [key: string]: ((data?: unknown) => unknown)[] } = {}; private cbs: { [key: string]: ((data?: unknown) => unknown)[] } = {};
private cbsOnce: { [key: string]: ((data?: unknown) => unknown)[] } = {}; private cbsOnce: { [key: string]: ((data?: unknown) => unknown)[] } = {};
@@ -29,7 +29,11 @@ export default class EventEmitter<T extends EventMap = { [key: string]: unknown
} }
} }
public on<K extends EventKey<T>>(event: K, cb: EventReceiver<T[K]>, throttleTimer = 0) { public on<K extends EventKey<T>>(
event: K,
cb: EventReceiver<T[K]>,
throttleTimer = 0,
) {
if (throttleTimer > 0) cb = throttle(cb, throttleTimer); if (throttleTimer > 0) cb = throttle(cb, throttleTimer);
const cbs = Object.assign(this.cbs, { const cbs = Object.assign(this.cbs, {
[event]: [...(this.cbs[event] || []), cb], [event]: [...(this.cbs[event] || []), cb],
@@ -38,7 +42,7 @@ export default class EventEmitter<T extends EventMap = { [key: string]: unknown
// console.log('New EventEmitter ', this.constructor.name); // console.log('New EventEmitter ', this.constructor.name);
return () => { return () => {
cbs[event]?.splice(cbs[event].indexOf(cb), 1); this.cbs[event]?.splice(cbs[event].indexOf(cb), 1);
}; };
} }
@@ -48,10 +52,17 @@ export default class EventEmitter<T extends EventMap = { [key: string]: unknown
* @param {function} cb Listener, gets called everytime the event is emitted * @param {function} cb Listener, gets called everytime the event is emitted
* @returns {function} Returns a function which removes the listener when called * @returns {function} Returns a function which removes the listener when called
*/ */
public once<K extends EventKey<T>>(event: K, cb: EventReceiver<T[K]>): () => void { public once<K extends EventKey<T>>(
this.cbsOnce[event] = [...(this.cbsOnce[event] || []), cb]; event: K,
cb: EventReceiver<T[K]>,
): () => void {
const cbsOnce = Object.assign(this.cbsOnce, {
[event]: [...(this.cbsOnce[event] || []), cb],
});
this.cbsOnce = cbsOnce;
return () => { return () => {
this.cbsOnce[event].splice(this.cbsOnce[event].indexOf(cb), 1); cbsOnce[event]?.splice(cbsOnce[event].indexOf(cb), 1);
}; };
} }

View File

@@ -6,7 +6,10 @@ export function lerp(a: number, b: number, t: number) {
return a + (b - a) * t; return a + (b - a) * t;
} }
export function animate(duration: number, callback: (progress: number) => void | false) { export function animate(
duration: number,
callback: (progress: number) => void | false,
) {
const start = performance.now(); const start = performance.now();
const loop = (time: number) => { const loop = (time: number) => {
const progress = (time - start) / duration; const progress = (time - start) / duration;
@@ -18,7 +21,7 @@ export function animate(duration: number, callback: (progress: number) => void |
} else { } else {
callback(1); callback(1);
} }
} };
requestAnimationFrame(loop); requestAnimationFrame(loop);
} }
@@ -33,7 +36,8 @@ export function createNodePath({
aspectRatio = 1, aspectRatio = 1,
} = {}) { } = {}) {
return `M0,${cornerTop} return `M0,${cornerTop}
${cornerTop ${
cornerTop
? ` V${cornerTop} ? ` V${cornerTop}
Q0,0 ${cornerTop * aspectRatio},0 Q0,0 ${cornerTop * aspectRatio},0
H${100 - cornerTop * aspectRatio} H${100 - cornerTop * aspectRatio}
@@ -44,11 +48,13 @@ export function createNodePath({
` `
} }
V${y - height / 2} V${y - height / 2}
${rightBump ${
rightBump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}` ? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100` : ` H100`
} }
${cornerBottom ${
cornerBottom
? ` V${100 - cornerBottom} ? ` V${100 - cornerBottom}
Q100,100 ${100 - cornerBottom * aspectRatio},100 Q100,100 ${100 - cornerBottom * aspectRatio},100
H${cornerBottom * aspectRatio} H${cornerBottom * aspectRatio}
@@ -56,7 +62,8 @@ export function createNodePath({
` `
: `${leftBump ? `V100 H0` : `V100`}` : `${leftBump ? `V100 H0` : `V100`}`
} }
${leftBump ${
leftBump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}` ? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
: ` H0` : ` H0`
} }
@@ -71,35 +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
export const createLogger = (() => { ? globalThis.structuredClone
let maxLength = 5; : (obj) => JSON.parse(JSON.stringify(obj));
return (scope: string) => {
maxLength = Math.max(maxLength, scope.length);
let muted = false;
return {
log: (...args: any[]) => !muted && console.log(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #888", ...args),
group: (...args: any[]) => !muted && console.groupCollapsed(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #888", ...args),
groupEnd: () => !muted && console.groupEnd(),
info: (...args: any[]) => !muted && console.info(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #888", ...args),
warn: (...args: any[]) => !muted && console.warn(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #888", ...args),
error: (...args: any[]) => console.error(`[%c${scope.padEnd(maxLength, " ")}]:`, "color: #f88", ...args),
mute() {
muted = true;
},
unmute() {
muted = false;
}
}
}
})();
export function withSubComponents<A, B extends Record<string, any>>( export function withSubComponents<A, B extends Record<string, any>>(
component: A, component: A,
subcomponents: B subcomponents: B,
): A & B { ): A & B {
Object.keys(subcomponents).forEach((key) => { Object.keys(subcomponents).forEach((key) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -1,20 +0,0 @@
export default <R, A extends any[]>(
fn: (...args: A) => R,
delay: number
): ((...args: A) => R) => {
let wait = false;
return (...args: A) => {
if (wait) return undefined;
const val = fn(...args);
wait = true;
setTimeout(() => {
wait = false;
}, delay);
return val;
}
};

View File

@@ -1,24 +1,24 @@
import { create, type Delta } from "jsondiffpatch"; import { create, type Delta } from "jsondiffpatch";
import type { Graph } from "@nodes/types"; import type { Graph } from "@nodarium/types";
import { createLogger, clone } from "./helpers/index.js"; import { clone } from "./helpers/index.js";
import { createLogger } from "@nodarium/utils";
const diff = create({ const diff = create({
objectHash: function (obj, index) { objectHash: function (obj, index) {
if (obj === null) return obj; if (obj === null) return obj;
if ("id" in obj) return obj.id; if ("id" in obj) return obj.id as string;
if ("_id" in obj) return obj._id as string;
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
return obj.join("-") return obj.join("-");
} }
return obj?.id || obj._id || '$$index:' + index; return "$$index:" + index;
} },
}) });
const log = createLogger("history") const log = createLogger("history");
log.mute(); log.mute();
export class HistoryManager { export class HistoryManager {
index: number = -1; index: number = -1;
history: Delta[] = []; history: Delta[] = [];
private initialState: Graph | undefined; private initialState: Graph | undefined;
@@ -27,26 +27,25 @@ export class HistoryManager {
private opts = { private opts = {
debounce: 400, debounce: 400,
maxHistory: 100, maxHistory: 100,
} };
constructor({ maxHistory = 100, debounce = 100 } = {}) { constructor({ maxHistory = 100, debounce = 100 } = {}) {
this.history = []; this.history = [];
this.index = -1; this.index = -1;
this.opts.debounce = debounce; this.opts.debounce = debounce;
this.opts.maxHistory = maxHistory; this.opts.maxHistory = maxHistory;
globalThis["_history"] = this;
} }
save(state: Graph) { save(state: Graph) {
if (!this.state) { if (!this.state) {
this.state = clone(state); this.state = clone(state);
this.initialState = this.state; this.initialState = this.state;
log.log("initial state saved") log.log("initial state saved");
} else { } else {
const newState = state; const newState = state;
const delta = diff.diff(this.state, newState); const delta = diff.diff(this.state, newState);
if (delta) { if (delta) {
log.log("saving state") log.log("saving state");
// Add the delta to history // Add the delta to history
if (this.index < this.history.length - 1) { if (this.index < this.history.length - 1) {
// Clear the history after the current index if new changes are made // Clear the history after the current index if new changes are made
@@ -62,7 +61,7 @@ export class HistoryManager {
} }
this.state = newState; this.state = newState;
} else { } else {
log.log("no changes") log.log("no changes");
} }
} }
} }
@@ -76,7 +75,7 @@ export class HistoryManager {
undo() { undo() {
if (this.index === -1 && this.initialState) { if (this.index === -1 && this.initialState) {
log.log("reached start, loading initial state") log.log("reached start, loading initial state");
return clone(this.initialState); return clone(this.initialState);
} else { } else {
const delta = this.history[this.index]; const delta = this.history[this.index];
@@ -96,7 +95,7 @@ export class HistoryManager {
this.state = nextState; this.state = nextState;
return clone(nextState); return clone(nextState);
} else { } else {
log.log("reached end") log.log("reached end");
} }
} }
} }

View File

@@ -0,0 +1,192 @@
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",
callback: () => {
const activeNode = graph.getNode(graphState.activeNodeId);
if (activeNode) {
const nodes = graph.getLinkedNodes(activeNode);
graphState.selectedNodes.clear();
for (const node of nodes) {
graphState.selectedNodes.add(node.id);
}
}
},
});
keymap.addShortcut({
key: "?",
description: "Toggle Help",
callback: () => {
// TODO: fix this
// showHelp = !showHelp;
},
});
keymap.addShortcut({
key: "c",
ctrl: true,
description: "Copy active nodes",
callback: () => graphState.copyNodes(),
});
keymap.addShortcut({
key: "v",
ctrl: true,
description: "Paste nodes",
callback: () => graphState.pasteNodes(),
});
keymap.addShortcut({
key: "Escape",
description: "Deselect nodes",
callback: () => {
graphState.activeNodeId = -1;
graphState.clearSelection();
graphState.edgeEndPosition = null;
(document.activeElement as HTMLElement)?.blur();
},
});
keymap.addShortcut({
key: "A",
shift: true,
description: "Add new Node",
callback: () => {
graphState.addMenuPosition = [graphState.mousePosition[0], graphState.mousePosition[1]];
},
});
keymap.addShortcut({
key: ".",
description: "Center camera",
callback: () => {
if (!graphState.isBodyFocused()) return;
const average = [0, 0];
for (const node of graph.nodes.values()) {
average[0] += node.position[0];
average[1] += node.position[1];
}
average[0] = average[0] ? average[0] / graph.nodes.size : 0;
average[1] = average[1] ? average[1] / graph.nodes.size : 0;
const camX = graphState.cameraPosition[0];
const camY = graphState.cameraPosition[1];
const camZ = graphState.cameraPosition[2];
const ease = (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t);
animate(500, (a: number) => {
graphState.setCameraTransform(
lerp(camX, average[0], ease(a)),
lerp(camY, average[1], ease(a)),
lerp(camZ, 2, ease(a)),
);
if (graphState.mouseDown) return false;
});
},
});
keymap.addShortcut({
key: "a",
ctrl: true,
preventDefault: true,
description: "Select all nodes",
callback: () => {
if (!graphState.isBodyFocused()) return;
for (const node of graph.nodes.keys()) {
graphState.selectedNodes.add(node);
}
},
});
keymap.addShortcut({
key: "z",
ctrl: true,
description: "Undo",
callback: () => {
if (!graphState.isBodyFocused()) return;
graph.undo();
for (const node of graph.nodes.values()) {
graphState.updateNodePosition(node);
}
},
});
keymap.addShortcut({
key: "y",
ctrl: true,
description: "Redo",
callback: () => {
graph.redo();
for (const node of graph.nodes.values()) {
graphState.updateNodePosition(node);
}
},
});
keymap.addShortcut({
key: "s",
ctrl: true,
description: "Save",
preventDefault: true,
callback: () => {
const state = graph.serialize();
const blob = new Blob([JSON.stringify(state)], {
type: "application/json;charset=utf-8",
});
FileSaver.saveAs(blob, "nodarium-graph.json");
},
});
keymap.addShortcut({
key: ["Delete", "Backspace", "x"],
description: "Delete selected nodes",
callback: (event) => {
if (!graphState.isBodyFocused()) return;
graph.startUndoGroup();
if (graphState.activeNodeId !== -1) {
const node = graph.getNode(graphState.activeNodeId);
if (node) {
graph.removeNode(node, { restoreEdges: event.ctrlKey });
graphState.activeNodeId = -1;
}
}
if (graphState.selectedNodes) {
for (const nodeId of graphState.selectedNodes) {
const node = graph.getNode(nodeId);
if (node) {
graph.removeNode(node, { restoreEdges: event.ctrlKey });
}
}
graphState.clearSelection();
}
graph.saveUndoGroup();
},
});
keymap.addShortcut({
key: "f",
description: "Smart Connect Nodes",
callback: () => {
const nodes = [...graphState.selectedNodes.values()]
.map((g) => graph.getNode(g))
.filter((n) => !!n);
const edge = graph.smartConnect(nodes[0], nodes[1]);
if (!edge) graph.smartConnect(nodes[1], nodes[0]);
},
});
}

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { Node } from "@nodes/types"; import type { Node } from "@nodarium/types";
import { getContext, onMount } from "svelte"; import { onMount } from "svelte";
import { getGraphState } from "../graph/state.svelte"; import { getGraphState } from "../graph/state.svelte";
import { T } from "@threlte/core"; import { T } from "@threlte/core";
import { type Mesh } from "three"; import { type Mesh } from "three";
@@ -17,39 +17,29 @@
inView: boolean; inView: boolean;
z: number; z: number;
}; };
const { node, inView, z }: Props = $props(); let { node, inView, z }: Props = $props();
const isActive = $derived(graphState.activeNodeId === node.id); const isActive = $derived(graphState.activeNodeId === node.id);
const isSelected = $derived(graphState.selectedNodes.has(node.id)); const isSelected = $derived(graphState.selectedNodes.has(node.id));
let strokeColor = $state(colors.selected); const strokeColor = $derived(
$effect(() => { appSettings.value.theme &&
appSettings.theme; (isSelected
strokeColor = isSelected ? colors.selected
? colors.selected.clone()
: isActive : isActive
? colors.active.clone() ? colors.active
: colors.outline.clone(); : colors.outline),
}); );
const updateNodePosition =
getContext<(n: Node) => void>("updateNodePosition");
const getNodeHeight = getContext<(n: string) => number>("getNodeHeight");
let meshRef: Mesh | undefined = $state(); let meshRef: Mesh | undefined = $state();
const height = getNodeHeight?.(node.type); const height = graphState.getNodeHeight(node.type);
$effect(() => { $effect(() => {
node.tmp = node.tmp || {}; if (!node.tmp) node.tmp = {};
if (meshRef && !node.tmp?.mesh) {
node.tmp.mesh = meshRef; node.tmp.mesh = meshRef;
updateNodePosition?.(node); graphState.updateNodePosition(node);
}); }
onMount(() => {
node.tmp = node.tmp || {};
node.tmp.mesh = meshRef;
updateNodePosition?.(node);
}); });
</script> </script>

View File

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

View File

@@ -1,23 +1,19 @@
<script lang="ts"> <script lang="ts">
import { getGraphState } from "../graph/state.svelte.js";
import { createNodePath } from "../helpers/index.js"; import { createNodePath } from "../helpers/index.js";
import type { Node, Socket } from "@nodes/types"; import type { Node } from "@nodarium/types";
import { getContext } from "svelte";
export let node: Node; const graphState = getGraphState();
const setDownSocket = getContext<(socket: Socket) => void>("setDownSocket"); const { node }: { node: Node } = $props();
const getSocketPosition =
getContext<(node: Node, index: number) => [number, number]>(
"getSocketPosition",
);
function handleMouseDown(event: MouseEvent) { function handleMouseDown(event: MouseEvent) {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
setDownSocket?.({ graphState.setDownSocket?.({
node, node,
index: 0, index: 0,
position: getSocketPosition?.(node, 0), position: graphState.getSocketPosition?.(node, 0),
}); });
} }
@@ -33,14 +29,14 @@
rightBump, rightBump,
aspectRatio, aspectRatio,
}); });
const pathDisabled = createNodePath({ // const pathDisabled = createNodePath({
depth: 0, // depth: 0,
height: 15, // height: 15,
y: 50, // y: 50,
cornerTop, // cornerTop,
rightBump, // rightBump,
aspectRatio, // aspectRatio,
}); // });
const pathHover = createNodePath({ const pathHover = createNodePath({
depth: 8.5, depth: 8.5,
height: 50, height: 50,
@@ -59,7 +55,7 @@
class="click-target" class="click-target"
role="button" role="button"
tabindex="0" tabindex="0"
on:mousedown={handleMouseDown} onmousedown={handleMouseDown}
></div> ></div>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,25 +1,36 @@
<script lang="ts"> <script lang="ts">
import type { Node, NodeInput } from "@nodes/types"; import type { Node, NodeInput } from "@nodarium/types";
import { getGraphManager } from "../graph/context.js"; import { Input } from "@nodarium/ui";
import { Input } from "@nodes/ui"; import type { GraphManager } from "../graph-manager.svelte";
type Props = { type Props = {
node: Node; node: Node;
input: NodeInput; input: NodeInput;
id: string; id: string;
elementId?: string; elementId?: string;
graph?: GraphManager;
}; };
const { const {
node, node = $bindable(),
input, input,
id, id,
elementId = `input-${Math.random().toString(36).substring(7)}`, elementId = `input-${Math.random().toString(36).substring(7)}`,
graph,
}: Props = $props(); }: Props = $props();
const graph = getGraphManager(); function getDefaultValue() {
if (node?.props?.[id] !== undefined) return node?.props?.[id] as number;
if ("value" in input && input?.value !== undefined)
return input?.value as number;
if (input.type === "boolean") return 0;
if (input.type === "float") return 0.5;
if (input.type === "integer") return 0;
if (input.type === "select") return 0;
return 0;
}
let value = $state(node?.props?.[id] ?? input.value); let value = $state(getDefaultValue());
$effect(() => { $effect(() => {
if (value !== undefined && node?.props?.[id] !== value) { if (value !== undefined && node?.props?.[id] !== value) {

View File

@@ -1,14 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { import type {
NodeInput as NodeInputType, NodeInput as NodeInputType,
Socket,
Node as NodeType, Node as NodeType,
} from "@nodes/types"; } from "@nodarium/types";
import { getContext } from "svelte";
import { createNodePath } from "../helpers/index.js"; import { createNodePath } from "../helpers/index.js";
import { getGraphManager } from "../graph/context.js";
import NodeInput from "./NodeInput.svelte"; import NodeInput from "./NodeInput.svelte";
import { getGraphState } from "../graph/state.svelte.js"; import { getGraphManager, getGraphState } from "../graph/state.svelte.js";
type Props = { type Props = {
node: NodeType; node: NodeType;
@@ -17,32 +14,26 @@
isLast?: boolean; isLast?: boolean;
}; };
const { node = $bindable(), input, id, isLast }: Props = $props(); const graph = getGraphManager();
let { node = $bindable(), input, id, isLast }: Props = $props();
const inputType = node?.tmp?.type?.inputs?.[id]!; const inputType = node?.tmp?.type?.inputs?.[id]!;
const socketId = `${node.id}-${id}`; const socketId = `${node.id}-${id}`;
const graph = getGraphManager();
const graphState = getGraphState(); const graphState = getGraphState();
const graphId = graph?.id; const graphId = graph?.id;
const inputSockets = graph?.inputSockets;
const elementId = `input-${Math.random().toString(36).substring(7)}`; const elementId = `input-${Math.random().toString(36).substring(7)}`;
const setDownSocket = getContext<(socket: Socket) => void>("setDownSocket");
const getSocketPosition =
getContext<(node: NodeType, index: string) => [number, number]>(
"getSocketPosition",
);
function handleMouseDown(ev: MouseEvent) { function handleMouseDown(ev: MouseEvent) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
setDownSocket?.({ graphState.setDownSocket({
node, node,
index: id, index: id,
position: getSocketPosition?.(node, id), position: graphState.getSocketPosition?.(node, id),
}); });
} }
@@ -83,23 +74,17 @@
class:disabled={!graphState?.possibleSocketIds.has(socketId)} class:disabled={!graphState?.possibleSocketIds.has(socketId)}
> >
{#key id && graphId} {#key id && graphId}
<div class="content" class:disabled={$inputSockets?.has(socketId)}> <div class="content" class:disabled={graph?.inputSockets?.has(socketId)}>
{#if inputType.label !== ""} {#if inputType.label !== ""}
<label for={elementId}>{input.label || id}</label> <label for={elementId}>{input.label || id}</label>
{/if} {/if}
{#if inputType.external !== true} {#if inputType.external !== true}
<NodeInput {elementId} {node} {input} {id} /> <NodeInput {graph} {elementId} bind:node {input} {id} />
{/if} {/if}
</div> </div>
{#if node?.tmp?.type?.inputs?.[id]?.internal !== true} {#if node?.tmp?.type?.inputs?.[id]?.internal !== true}
<div <div data-node-socket class="large target"></div>
data-node-socket
class="large target"
onmousedown={handleMouseDown}
role="button"
tabindex="0"
></div>
<div <div
data-node-socket data-node-socket
class="small target" class="small target"

View File

@@ -1,4 +1,4 @@
import type { Graph } from "@nodes/types"; import type { Graph } from "@nodarium/types";
export function grid(width: number, height: number) { export function grid(width: number, height: number) {

View File

@@ -1,4 +1,4 @@
import type { Graph, Node } from "@nodes/types"; import type { Graph, Node } from "@nodarium/types";
export function tree(depth: number): Graph { export function tree(depth: number): Graph {

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { getContext } from "svelte"; import { getContext } from "svelte";
import type { Writable } from "svelte/store";
let index = -1; let index = -1;
let wrapper: HTMLDivElement; let wrapper: HTMLDivElement;
@@ -9,7 +8,7 @@
index = getContext<() => number>("registerCell")(); index = getContext<() => number>("registerCell")();
} }
const sizes = getContext<Writable<string[]>>("sizes"); const sizes = getContext<{ value: string[] }>("sizes");
let downSizes: string[] = []; let downSizes: string[] = [];
let downWidth = 0; let downWidth = 0;
@@ -17,7 +16,7 @@
let startX = 0; let startX = 0;
function handleMouseDown(event: MouseEvent) { function handleMouseDown(event: MouseEvent) {
downSizes = [...$sizes]; downSizes = [...sizes.value];
mouseDown = true; mouseDown = true;
startX = event.clientX; startX = event.clientX;
downWidth = wrapper.getBoundingClientRect().width; downWidth = wrapper.getBoundingClientRect().width;
@@ -26,8 +25,7 @@
function handleMouseMove(event: MouseEvent) { function handleMouseMove(event: MouseEvent) {
if (mouseDown) { if (mouseDown) {
const width = downWidth + startX - event.clientX; const width = downWidth + startX - event.clientX;
$sizes[index] = `${width}px`; sizes.value[index] = `${width}px`;
$sizes = $sizes;
} }
} }
</script> </script>

View File

@@ -1,27 +1,31 @@
<script lang="ts"> <script lang="ts">
import { setContext, getContext } from "svelte"; import { setContext, getContext } from "svelte";
import localStore from "$lib/helpers/localStore"; import { localState } from "$lib/helpers/localState.svelte";
const gridId = getContext<string>("grid-id") || "grid-0"; const gridId = getContext<string>("grid-id") || "grid-0";
let sizes = localStore<string[]>(gridId, []); let sizes = localState<string[]>(gridId, []);
const { children } = $props();
let registerIndex = 0; let registerIndex = 0;
setContext("registerCell", function () { setContext("registerCell", function () {
let index = registerIndex; let index = registerIndex;
registerIndex++; registerIndex++;
if (registerIndex > $sizes.length) { if (registerIndex > sizes.value.length) {
$sizes = [...$sizes, "1fr"]; sizes.value = [...sizes.value, "1fr"];
} }
return index; return index;
}); });
setContext("sizes", sizes); setContext("sizes", sizes);
$: cols = $sizes.map((size, i) => `${i > 0 ? "1px " : ""}` + size).join(" "); const cols = $derived(
sizes.value.map((size, i) => `${i > 0 ? "1px " : ""}` + size).join(" "),
);
</script> </script>
<div class="wrapper" style={`grid-template-columns: ${cols};`}> <div class="wrapper" style={`grid-template-columns: ${cols};`}>
<slot /> {@render children()}
</div> </div>
<style> <style>

View File

@@ -65,7 +65,7 @@ export function createNodePath({
export const debounce = (fn: Function, ms = 300) => { export const debounce = (fn: Function, ms = 300) => {
let timeoutId: ReturnType<typeof setTimeout>; let timeoutId: ReturnType<typeof setTimeout>;
return function(this: any, ...args: any[]) { return function (this: any, ...args: any[]) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), ms); timeoutId = setTimeout(() => fn.apply(this, args), ms);
}; };
@@ -131,41 +131,100 @@ export function humanizeDuration(durationInMilliseconds: number) {
return durationString.trim(); return durationString.trim();
} }
export function debounceAsyncFunction<T extends any[], R>(func: (...args: T) => Promise<R>): (...args: T) => Promise<R> { // export function debounceAsyncFunction<T extends any[], R>(
let currentPromise: Promise<R> | null = null; // func: (...args: T) => Promise<R>
let nextArgs: T | null = null; // ): (...args: T) => Promise<R> {
let resolveNext: ((result: R) => void) | null = null; // let timeoutId: ReturnType<typeof setTimeout> | null = null;
// let lastPromise: Promise<R> | null = null;
// let lastReject: ((reason?: any) => void) | null = null;
//
// return (...args: T): Promise<R> => {
// if (timeoutId) {
// clearTimeout(timeoutId);
// if (lastReject) {
// lastReject(new Error("Debounced: Previous call was canceled."));
// }
// }
//
// return new Promise<R>((resolve, reject) => {
// lastReject = reject;
// timeoutId = setTimeout(() => {
// timeoutId = null;
// lastReject = null;
// lastPromise = func(...args).then(resolve, reject);
// }, 300); // Default debounce time is 300ms; you can make this configurable.
// });
// };
// }
export function debounceAsyncFunction<T extends (...args: any[]) => Promise<any>>(asyncFn: T): T {
let isRunning = false;
let latestArgs: Parameters<T> | null = null;
let resolveNext: (() => void) | null = null;
const debouncedFunction = async (...args: T): Promise<R> => { return (async function serializedFunction(...args: Parameters<T>): Promise<ReturnType<T>> {
if (currentPromise) { latestArgs = args;
// Store the latest arguments and create a new promise to resolve them later
nextArgs = args; if (isRunning) {
return new Promise<R>((resolve) => { // Wait for the current execution to finish
await new Promise<void>((resolve) => {
resolveNext = resolve; resolveNext = resolve;
}); });
} else { }
// Execute the function immediately
// Indicate the function is running
isRunning = true;
try { try {
currentPromise = func(...args); // Execute with the latest arguments
const result = await currentPromise; const result = await asyncFn(...latestArgs!);
return result; return result;
} finally { } finally {
currentPromise = null; // Allow the next execution
// If there are stored arguments, call the function again with the latest arguments isRunning = false;
if (nextArgs) {
const argsToUse = nextArgs;
const resolver = resolveNext;
nextArgs = null;
resolveNext = null;
resolver!(await debouncedFunction(...argsToUse));
}
}
}
};
return debouncedFunction; if (resolveNext) {
resolveNext();
resolveNext = null;
}
}
}) as T;
} }
// export function debounceAsyncFunction<T extends any[], R>(func: (...args: T) => Promise<R>): (...args: T) => Promise<R> {
// let currentPromise: Promise<R> | null = null;
// let nextArgs: T | null = null;
// let resolveNext: ((result: R) => void) | null = null;
//
// const debouncedFunction = async (...args: T): Promise<R> => {
// if (currentPromise) {
// // Store the latest arguments and create a new promise to resolve them later
// nextArgs = args;
// return new Promise<R>((resolve) => {
// resolveNext = resolve;
// });
// } else {
// // Execute the function immediately
// try {
// currentPromise = func(...args);
// const result = await currentPromise;
// return result;
// } finally {
// currentPromise = null;
// // If there are stored arguments, call the function again with the latest arguments
// if (nextArgs) {
// const argsToUse = nextArgs;
// const resolver = resolveNext;
// nextArgs = null;
// resolveNext = null;
// resolver!(await debouncedFunction(...argsToUse));
// }
// }
// }
// };
//
// return debouncedFunction;
// }
export function withArgsChangeOnly<T extends any[], R>(func: (...args: T) => R): (...args: T) => R { export function withArgsChangeOnly<T extends any[], R>(func: (...args: T) => R): (...args: T) => R {
let lastArgs: T | undefined = undefined; let lastArgs: T | undefined = undefined;
let lastResult: R; let lastResult: R;

View File

@@ -1,11 +1,34 @@
export function localState<T>(key: string, defaultValue: T): T { import { browser } from "$app/environment";
const stored = localStorage.getItem(key)
const state = $state(stored ? JSON.parse(stored) : defaultValue) export class LocalStore<T> {
value = $state<T>() as T;
key = "";
constructor(key: string, value: T) {
this.key = key;
this.value = value;
if (browser) {
const item = localStorage.getItem(key);
if (item) this.value = this.deserialize(item);
}
$effect.root(() => { $effect.root(() => {
$effect(() => { $effect(() => {
const value = $state.snapshot(state); localStorage.setItem(this.key, this.serialize(this.value));
localStorage.setItem(key, JSON.stringify(value));
}); });
}); });
return state; }
serialize(value: T): string {
return JSON.stringify(value);
}
deserialize(item: string): T {
return JSON.parse(item);
}
}
export function localState<T>(key: string, value: T) {
return new LocalStore(key, value);
} }

View File

@@ -1,20 +1,19 @@
export default <R, A extends any[]>( export default <T extends unknown[]>(
fn: (...args: A) => R, callback: (...args: T) => void,
delay: number delay: number,
): ((...args: A) => R) => { ) => {
let wait = false; let isWaiting = false;
return (...args: A) => { return (...args: T) => {
if (wait) return undefined; if (isWaiting) {
return;
}
const val = fn(...args); callback(...args);
isWaiting = true;
wait = true;
setTimeout(() => { setTimeout(() => {
wait = false; isWaiting = false;
}, delay); }, delay);
};
return val;
}
}; };

View File

@@ -1,6 +1,6 @@
import { createWasmWrapper } from "@nodes/utils" import { createWasmWrapper } from "@nodarium/utils";
import fs from "fs/promises" import fs from "fs/promises";
import path from "path" import path from "path";
export async function getWasm(id: `${string}/${string}/${string}`) { export async function getWasm(id: `${string}/${string}/${string}`) {
const filePath = path.resolve(`../nodes/${id}/pkg/index_bg.wasm`); const filePath = path.resolve(`../nodes/${id}/pkg/index_bg.wasm`);
@@ -8,17 +8,15 @@ export async function getWasm(id: `${string}/${string}/${string}`) {
try { try {
await fs.access(filePath); await fs.access(filePath);
} catch (e) { } catch (e) {
return null return null;
} }
const file = await fs.readFile(filePath); const file = await fs.readFile(filePath);
return new Uint8Array(file); return new Uint8Array(file);
} }
export async function getNodeWasm(id: `${string}/${string}/${string}`) { export async function getNodeWasm(id: `${string}/${string}/${string}`) {
const wasmBytes = await getWasm(id); const wasmBytes = await getWasm(id);
if (!wasmBytes) return null; if (!wasmBytes) return null;
@@ -27,9 +25,7 @@ export async function getNodeWasm(id: `${string}/${string}/${string}`) {
return wrapper; return wrapper;
} }
export async function getNode(id: `${string}/${string}/${string}`) { export async function getNode(id: `${string}/${string}/${string}`) {
const wrapper = await getNodeWasm(id); const wrapper = await getNodeWasm(id);
const definition = wrapper?.get_definition?.(); const definition = wrapper?.get_definition?.();
@@ -37,18 +33,17 @@ export async function getNode(id: `${string}/${string}/${string}`) {
if (!definition) return null; if (!definition) return null;
return definition; return definition;
} }
export async function getCollectionNodes(userId: `${string}/${string}`) { export async function getCollectionNodes(userId: `${string}/${string}`) {
const nodes = await fs.readdir(path.resolve(`../nodes/${userId}`)); const nodes = await fs.readdir(path.resolve(`../nodes/${userId}`));
return nodes return nodes
.filter(n => n !== "pkg" && n !== ".template") .filter((n) => n !== "pkg" && n !== ".template")
.map(n => { .map((n) => {
return { return {
id: `${userId}/${n}`, id: `${userId}/${n}`,
} };
}) });
} }
export async function getCollection(userId: `${string}/${string}`) { export async function getCollection(userId: `${string}/${string}`) {
@@ -56,36 +51,40 @@ export async function getCollection(userId: `${string}/${string}`) {
return { return {
id: userId, id: userId,
nodes, nodes,
} };
} }
export async function getUserCollections(userId: string) { export async function getUserCollections(userId: string) {
const collections = await fs.readdir(path.resolve(`../nodes/${userId}`)); const collections = await fs.readdir(path.resolve(`../nodes/${userId}`));
return Promise.all(collections.map(async n => { return Promise.all(
collections.map(async (n) => {
const nodes = await getCollectionNodes(`${userId}/${n}`); const nodes = await getCollectionNodes(`${userId}/${n}`);
return { return {
id: `${userId}/${n}`, id: `${userId}/${n}`,
nodes, nodes,
} };
})); }),
);
} }
export async function getUser(userId: string) { export async function getUser(userId: string) {
const collections = await getUserCollections(userId); const collections = await getUserCollections(userId);
return { return {
id: userId, id: userId,
collections collections,
} };
} }
export async function getUsers() { export async function getUsers() {
const nodes = await fs.readdir(path.resolve("../nodes")); const nodes = await fs.readdir(path.resolve("../nodes"));
const users = await Promise.all(nodes.map(async n => { const users = await Promise.all(
nodes.map(async (n) => {
const collections = await getUserCollections(n); const collections = await getUserCollections(n);
return { return {
id: n, id: n,
collections collections,
} };
})) }),
);
return users; return users;
} }

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Select } from "@nodes/ui"; import { Select } from "@nodarium/ui";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
let activeStore = 0; let activeStore = 0;

View File

@@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
import NodeHtml from "$lib/graph-interface/node/NodeHTML.svelte"; import NodeHtml from "$lib/graph-interface/node/NodeHTML.svelte";
import type { NodeDefinition } from "@nodes/types"; import type { NodeDefinition } from "@nodarium/types";
export let node: NodeDefinition; export let node: NodeDefinition;
console.log(node);
let dragging = false; let dragging = false;

View File

@@ -2,7 +2,7 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import BreadCrumbs from "./BreadCrumbs.svelte"; import BreadCrumbs from "./BreadCrumbs.svelte";
import DraggableNode from "./DraggableNode.svelte"; import DraggableNode from "./DraggableNode.svelte";
import type { RemoteNodeRegistry } from "@nodes/registry"; import type { RemoteNodeRegistry } from "@nodarium/registry";
export let registry: RemoteNodeRegistry; export let registry: RemoteNodeRegistry;
@@ -23,7 +23,7 @@
<div class="wrapper"> <div class="wrapper">
{#if !activeUser} {#if !activeUser}
{#await registry.fetchUsers()} {#await registry.fetchUsers()}
<div>Loading...</div> <div>Loading Users...</div>
{:then users} {:then users}
{#each users as user} {#each users as user}
<button <button
@@ -37,7 +37,7 @@
{/await} {/await}
{:else if !activeCollection} {:else if !activeCollection}
{#await registry.fetchUser(activeUser)} {#await registry.fetchUser(activeUser)}
<div>Loading...</div> <div>Loading User...</div>
{:then user} {:then user}
{#each user.collections as collection} {#each user.collections as collection}
<button <button
@@ -53,11 +53,11 @@
{/await} {/await}
{:else if !activeNode} {:else if !activeNode}
{#await registry.fetchCollection(`${activeUser}/${activeCollection}`)} {#await registry.fetchCollection(`${activeUser}/${activeCollection}`)}
<div>Loading...</div> <div>Loading Collection...</div>
{:then collection} {:then collection}
{#each collection.nodes as node} {#each collection.nodes as node}
{#await registry.fetchNodeDefinition(node.id)} {#await registry.fetchNodeDefinition(node.id)}
<div>Loading... {node.id}</div> <div>Loading Node... {node.id}</div>
{:then node} {:then node}
{#if node} {#if node}
<DraggableNode {node} /> <DraggableNode {node} />

View File

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

View File

@@ -27,10 +27,12 @@
function constructPath() { function constructPath() {
max = max !== undefined ? max : Math.max(...points); max = max !== undefined ? max : Math.max(...points);
min = min !== undefined ? min : Math.min(...points); min = min !== undefined ? min : Math.min(...points);
const mi = min as number;
const ma = max as number;
return points return points
.map((point, i) => { .map((point, i) => {
const x = (i / (points.length - 1)) * 100; const x = (i / (points.length - 1)) * 100;
const y = 100 - ((point - min) / (max - min)) * 100; const y = 100 - ((point - mi) / (ma - mi)) * 100;
return `${x},${y}`; return `${x},${y}`;
}) })
.join(" "); .join(" ");

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import Monitor from "./Monitor.svelte"; import Monitor from "./Monitor.svelte";
import { humanizeNumber } from "$lib/helpers"; import { humanizeNumber } from "$lib/helpers";
import { Checkbox } from "@nodes/ui"; import { Checkbox } from "@nodarium/ui";
import localStore from "$lib/helpers/localStore"; import localStore from "$lib/helpers/localStore";
import { type PerformanceData } from "@nodes/utils"; import { type PerformanceData } from "@nodarium/utils";
import BarSplit from "./BarSplit.svelte"; import BarSplit from "./BarSplit.svelte";
export let data: PerformanceData; export let data: PerformanceData;

View File

@@ -2,7 +2,7 @@
import { humanizeDuration, humanizeNumber } from "$lib/helpers"; import { humanizeDuration, humanizeNumber } from "$lib/helpers";
import localStore from "$lib/helpers/localStore"; import localStore from "$lib/helpers/localStore";
import SmallGraph from "./SmallGraph.svelte"; import SmallGraph from "./SmallGraph.svelte";
import type { PerformanceData, PerformanceStore } from "@nodes/utils"; import type { PerformanceData, PerformanceStore } from "@nodarium/utils";
export let store: PerformanceStore; export let store: PerformanceStore;
@@ -25,7 +25,10 @@
<div class="wrapper"> <div class="wrapper">
<table> <table>
<tbody> <tbody>
<tr on:click={() => ($open.runtime = !$open.runtime)}> <tr
style="cursor:pointer;"
on:click={() => ($open.runtime = !$open.runtime)}
>
<td>{$open.runtime ? "-" : "+"} runtime </td> <td>{$open.runtime ? "-" : "+"} runtime </td>
<td>{humanizeDuration(runtime || 1000)}</td> <td>{humanizeDuration(runtime || 1000)}</td>
</tr> </tr>
@@ -37,7 +40,7 @@
</tr> </tr>
{/if} {/if}
<tr on:click={() => ($open.fps = !$open.fps)}> <tr style="cursor:pointer;" on:click={() => ($open.fps = !$open.fps)}>
<td>{$open.fps ? "-" : "+"} fps </td> <td>{$open.fps ? "-" : "+"} fps </td>
<td> <td>
{Math.floor(fps[fps.length - 1])}fps {Math.floor(fps[fps.length - 1])}fps
@@ -74,9 +77,6 @@
border: solid thin var(--outline); border: solid thin var(--outline);
border-collapse: collapse; border-collapse: collapse;
} }
tr {
cursor: pointer;
}
td { td {
padding: 4px; padding: 4px;
padding-inline: 8px; padding-inline: 8px;

View File

@@ -7,11 +7,15 @@
import type { PerspectiveCamera, Vector3Tuple } from "three"; import type { PerspectiveCamera, Vector3Tuple } from "three";
import type { OrbitControls as OrbitControlsType } from "three/examples/jsm/controls/OrbitControls.js"; import type { OrbitControls as OrbitControlsType } from "three/examples/jsm/controls/OrbitControls.js";
let camera: PerspectiveCamera; let camera = $state<PerspectiveCamera>();
let controls: OrbitControlsType; let controls = $state<OrbitControlsType>();
export let center: Vector3; type Props = {
export let centerCamera: boolean = true; center: Vector3;
centerCamera: boolean;
};
const { center, centerCamera }: Props = $props();
const cameraTransform = localStore<{ const cameraTransform = localStore<{
camera: Vector3Tuple; camera: Vector3Tuple;
@@ -22,7 +26,7 @@
}); });
function saveCameraState() { function saveCameraState() {
if (!camera) return; if (!camera || !controls) return;
let cPos = camera.position.toArray(); let cPos = camera.position.toArray();
let tPos = controls.target.toArray(); let tPos = controls.target.toArray();
// check if tPos is NaN or tPos is NaN // check if tPos is NaN or tPos is NaN
@@ -35,6 +39,7 @@
let isRunning = false; let isRunning = false;
const task = useTask(() => { const task = useTask(() => {
if (!controls) return;
let length = center.clone().sub(controls.target).length(); let length = center.clone().sub(controls.target).length();
if (length < 0.01 || !centerCamera) { if (length < 0.01 || !centerCamera) {
isRunning = false; isRunning = false;
@@ -47,7 +52,8 @@
}); });
task.stop(); task.stop();
$: if ( $effect(() => {
if (
center && center &&
controls && controls &&
centerCamera && centerCamera &&
@@ -59,10 +65,11 @@
isRunning = true; isRunning = true;
task.start(); task.start();
} }
});
onMount(() => { onMount(() => {
controls.target.fromArray($cameraTransform.target); controls?.target.fromArray($cameraTransform.target);
controls.update(); controls?.update();
}); });
</script> </script>

View File

@@ -1,6 +1,11 @@
<script lang="ts"> <script lang="ts">
import { T, useTask, useThrelte } from "@threlte/core"; import { T, useTask, useThrelte } from "@threlte/core";
import { MeshLineGeometry, MeshLineMaterial, Text } from "@threlte/extras"; import {
Grid,
MeshLineGeometry,
MeshLineMaterial,
Text,
} from "@threlte/extras";
import { import {
type Group, type Group,
type BufferGeometry, type BufferGeometry,
@@ -9,7 +14,6 @@
Box3, Box3,
Mesh, Mesh,
MeshBasicMaterial, MeshBasicMaterial,
Color,
} from "three"; } from "three";
import { appSettings } from "../settings/app-settings.svelte"; import { appSettings } from "../settings/app-settings.svelte";
import Camera from "./Camera.svelte"; import Camera from "./Camera.svelte";
@@ -42,11 +46,9 @@
export const invalidate = function () { export const invalidate = function () {
if (scene) { if (scene) {
geometries = scene.children geometries = scene.children
.filter( .filter((child) => "geometry" in child && child.isObject3D)
(child) => "geometry" in child && child.isObject3D && child.geometry,
)
.map((child) => { .map((child) => {
return child.geometry; return (child as Mesh).geometry;
}); });
} }
@@ -72,7 +74,7 @@
} }
$effect(() => { $effect(() => {
const wireframe = appSettings.debug.wireframe; const wireframe = appSettings.value.debug.wireframe;
scene.traverse(function (child) { scene.traverse(function (child) {
if (isMesh(child) && isMatCapMaterial(child.material)) { if (isMesh(child) && isMatCapMaterial(child.material)) {
child.material.wireframe = wireframe; child.material.wireframe = wireframe;
@@ -92,18 +94,24 @@
<Camera {center} {centerCamera} /> <Camera {center} {centerCamera} />
{#if appSettings.showGrid} {#if appSettings.value.showGrid}
<T.GridHelper <Grid
args={[20, 20]} cellColor={colors["outline"]}
colorGrid={colors["outline"]} cellThickness={0.7}
colorCenterLine={new Color("red")} infiniteGrid
sectionThickness={0.7}
sectionDistance={2}
sectionColor={colors["outline"]}
fadeDistance={50}
fadeStrength={10}
fadeOrigin={new Vector3(0, 0, 0)}
/> />
{/if} {/if}
<T.Group> <T.Group>
{#if geometries} {#if geometries}
{#each geometries as geo} {#each geometries as geo}
{#if appSettings.debug.showIndices} {#if appSettings.value.debug.showIndices}
{#each geo.attributes.position.array as _, i} {#each geo.attributes.position.array as _, i}
{#if i % 3 === 0} {#if i % 3 === 0}
<Text fontSize={0.25} position={getPosition(geo, i)} /> <Text fontSize={0.25} position={getPosition(geo, i)} />
@@ -111,7 +119,7 @@
{/each} {/each}
{/if} {/if}
{#if appSettings.debug.showVertices} {#if appSettings.value.debug.showVertices}
<T.Points visible={true}> <T.Points visible={true}>
<T is={geo} /> <T is={geo} />
<T.PointsMaterial size={0.25} /> <T.PointsMaterial size={0.25} />
@@ -123,7 +131,7 @@
<T.Group bind:ref={scene}></T.Group> <T.Group bind:ref={scene}></T.Group>
</T.Group> </T.Group>
{#if appSettings.debug.showStemLines && lines} {#if appSettings.value.debug.showStemLines && lines}
{#each lines as line} {#each lines as line}
<T.Mesh> <T.Mesh>
<MeshLineGeometry points={line} /> <MeshLineGeometry points={line} />

View File

@@ -2,8 +2,8 @@
import { Canvas } from "@threlte/core"; import { Canvas } from "@threlte/core";
import Scene from "./Scene.svelte"; import Scene from "./Scene.svelte";
import { Vector3 } from "three"; import { Vector3 } from "three";
import { decodeFloat, splitNestedArray } from "@nodes/utils"; import { decodeFloat, splitNestedArray } from "@nodarium/utils";
import type { PerformanceStore } from "@nodes/utils"; import type { PerformanceStore } from "@nodarium/utils";
import { appSettings } from "$lib/settings/app-settings.svelte"; import { appSettings } from "$lib/settings/app-settings.svelte";
import SmallPerformanceViewer from "$lib/performance/SmallPerformanceViewer.svelte"; import SmallPerformanceViewer from "$lib/performance/SmallPerformanceViewer.svelte";
import { MeshMatcapMaterial, TextureLoader, type Group } from "three"; import { MeshMatcapMaterial, TextureLoader, type Group } from "three";
@@ -68,7 +68,7 @@
const inputs = splitNestedArray(result); const inputs = splitNestedArray(result);
perf.endPoint(); perf.endPoint();
if (appSettings.debug.showStemLines) { if (appSettings.value.debug.showStemLines) {
perf.addPoint("create-lines"); perf.addPoint("create-lines");
lines = inputs lines = inputs
.map((input) => { .map((input) => {
@@ -91,7 +91,7 @@
}; };
</script> </script>
{#if appSettings.debug.showPerformancePanel} {#if appSettings.value.debug.showPerformancePanel}
<SmallPerformanceViewer {fps} store={perf} /> <SmallPerformanceViewer {fps} store={perf} />
{/if} {/if}

View File

@@ -1,20 +1,27 @@
import { fastHashArrayBuffer } from "@nodes/utils"; import { fastHashArrayBuffer } from "@nodarium/utils";
import { BufferAttribute, BufferGeometry, Float32BufferAttribute, Group, InstancedMesh, Material, Matrix4, Mesh } from "three"; import {
BufferAttribute,
BufferGeometry,
Float32BufferAttribute,
Group,
InstancedMesh,
Material,
Matrix4,
Mesh,
} from "three";
function fastArrayHash(arr: ArrayBuffer) { function fastArrayHash(arr: Int32Array) {
let ints = new Uint8Array(arr); const sampleDistance = Math.max(Math.floor(arr.length / 100), 1);
const sampleCount = Math.floor(arr.length / sampleDistance);
const sampleDistance = Math.max(Math.floor(ints.length / 100), 1); let hash = new Int32Array(sampleCount);
const sampleCount = Math.floor(ints.length / sampleDistance);
let hash = new Uint8Array(sampleCount);
for (let i = 0; i < sampleCount; i++) { for (let i = 0; i < sampleCount; i++) {
const index = i * sampleDistance; const index = i * sampleDistance;
hash[i] = ints[index]; hash[i] = arr[index];
} }
return fastHashArrayBuffer(hash.buffer); return fastHashArrayBuffer(hash);
} }
export function createGeometryPool(parentScene: Group, material: Material) { export function createGeometryPool(parentScene: Group, material: Material) {
@@ -26,8 +33,10 @@ export function createGeometryPool(parentScene: Group, material: Material) {
let totalVertices = 0; let totalVertices = 0;
let totalFaces = 0; let totalFaces = 0;
function updateSingleGeometry(data: Int32Array, existingMesh: Mesh | null = null) { function updateSingleGeometry(
data: Int32Array,
existingMesh: Mesh | null = null,
) {
let hash = fastArrayHash(data); let hash = fastArrayHash(data);
let geometry = existingMesh ? existingMesh.geometry : new BufferGeometry(); let geometry = existingMesh ? existingMesh.geometry : new BufferGeometry();
@@ -50,11 +59,7 @@ export function createGeometryPool(parentScene: Group, material: Material) {
index = indicesEnd; index = indicesEnd;
// Vertices // Vertices
const vertices = new Float32Array( const vertices = new Float32Array(data.buffer, index * 4, vertexCount * 3);
data.buffer,
index * 4,
vertexCount * 3,
);
index = index + vertexCount * 3; index = index + vertexCount * 3;
let posAttribute = geometry.getAttribute( let posAttribute = geometry.getAttribute(
@@ -71,11 +76,7 @@ export function createGeometryPool(parentScene: Group, material: Material) {
); );
} }
const normals = new Float32Array( const normals = new Float32Array(data.buffer, index * 4, vertexCount * 3);
data.buffer,
index * 4,
vertexCount * 3,
);
index = index + vertexCount * 3; index = index + vertexCount * 3;
if ( if (
@@ -109,11 +110,8 @@ export function createGeometryPool(parentScene: Group, material: Material) {
} }
} }
return { return {
update( update(newData: Int32Array[]) {
newData: Int32Array[],
) {
totalVertices = 0; totalVertices = 0;
totalFaces = 0; totalFaces = 0;
for (let i = 0; i < Math.max(newData.length, meshes.length); i++) { for (let i = 0; i < Math.max(newData.length, meshes.length); i++) {
@@ -127,11 +125,14 @@ export function createGeometryPool(parentScene: Group, material: Material) {
} }
} }
return { totalVertices, totalFaces }; return { totalVertices, totalFaces };
} },
} };
} }
export function createInstancedGeometryPool(parentScene: Group, material: Material) { export function createInstancedGeometryPool(
parentScene: Group,
material: Material,
) {
const scene = new Group(); const scene = new Group();
parentScene.add(scene); parentScene.add(scene);
@@ -139,19 +140,25 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
let totalVertices = 0; let totalVertices = 0;
let totalFaces = 0; let totalFaces = 0;
function updateSingleInstance(data: Int32Array, existingInstance: InstancedMesh | null = null) { function updateSingleInstance(
data: Int32Array,
existingInstance: InstancedMesh | null = null,
) {
let hash = fastArrayHash(data); let hash = fastArrayHash(data);
let geometry = existingInstance ? existingInstance.geometry : new BufferGeometry(); let geometry = existingInstance
? existingInstance.geometry
: new BufferGeometry();
// Extract data from the encoded array // Extract data from the encoded array
let index = 0; let index = 0;
const geometryType = data[index++]; // const geometryType = data[index++];
index++;
const vertexCount = data[index++]; const vertexCount = data[index++];
const faceCount = data[index++]; const faceCount = data[index++];
const instanceCount = data[index++]; const instanceCount = data[index++];
const stemDepth = data[index++]; // const stemDepth = data[index++];
index++;
totalVertices += vertexCount * instanceCount; totalVertices += vertexCount * instanceCount;
totalFaces += faceCount * instanceCount; totalFaces += faceCount * instanceCount;
@@ -168,11 +175,7 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
} }
// Vertices // Vertices
const vertices = new Float32Array( const vertices = new Float32Array(data.buffer, index * 4, vertexCount * 3);
data.buffer,
index * 4,
vertexCount * 3,
);
index = index + vertexCount * 3; index = index + vertexCount * 3;
let posAttribute = geometry.getAttribute( let posAttribute = geometry.getAttribute(
"position", "position",
@@ -187,11 +190,7 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
); );
} }
const normals = new Float32Array( const normals = new Float32Array(data.buffer, index * 4, vertexCount * 3);
data.buffer,
index * 4,
vertexCount * 3,
);
index = index + vertexCount * 3; index = index + vertexCount * 3;
const normalsAttribute = geometry.getAttribute( const normalsAttribute = geometry.getAttribute(
"normal", "normal",
@@ -203,20 +202,20 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3)); geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3));
} }
if (existingInstance && instanceCount > existingInstance.geometry.userData.count) { if (
console.log("recreating instance") existingInstance &&
instanceCount > existingInstance.geometry.userData.count
) {
scene.remove(existingInstance); scene.remove(existingInstance);
instances.splice(instances.indexOf(existingInstance), 1); instances.splice(instances.indexOf(existingInstance), 1);
existingInstance = new InstancedMesh(geometry, material, instanceCount); existingInstance = new InstancedMesh(geometry, material, instanceCount);
scene.add(existingInstance) scene.add(existingInstance);
instances.push(existingInstance) instances.push(existingInstance);
} else if (!existingInstance) { } else if (!existingInstance) {
console.log("creating instance")
existingInstance = new InstancedMesh(geometry, material, instanceCount); existingInstance = new InstancedMesh(geometry, material, instanceCount);
scene.add(existingInstance) scene.add(existingInstance);
instances.push(existingInstance) instances.push(existingInstance);
} else { } else {
console.log("updating instance")
existingInstance.count = instanceCount; existingInstance.count = instanceCount;
} }
@@ -225,28 +224,31 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
const matrices = new Float32Array( const matrices = new Float32Array(
data.buffer, data.buffer,
index * 4, index * 4,
instanceCount * 16); instanceCount * 16,
);
for (let i = 0; i < instanceCount; i++) { for (let i = 0; i < instanceCount; i++) {
const matrix = new Matrix4().fromArray(matrices.subarray(i * 16, i * 16 + 16)); const matrix = new Matrix4().fromArray(
matrices.subarray(i * 16, i * 16 + 16),
);
existingInstance.setMatrixAt(i, matrix); existingInstance.setMatrixAt(i, matrix);
} }
geometry.userData = { geometry.userData = {
vertexCount, vertexCount,
faceCount, faceCount,
count: Math.max(instanceCount, existingInstance.geometry.userData.count || 0), count: Math.max(
instanceCount,
existingInstance.geometry.userData.count || 0,
),
hash, hash,
}; };
existingInstance.instanceMatrix.needsUpdate = true; existingInstance.instanceMatrix.needsUpdate = true;
} }
return { return {
update( update(newData: Int32Array[]) {
newData: Int32Array[],
) {
totalVertices = 0; totalVertices = 0;
totalFaces = 0; totalFaces = 0;
for (let i = 0; i < Math.max(newData.length, instances.length); i++) { for (let i = 0; i < Math.max(newData.length, instances.length); i++) {
@@ -260,6 +262,6 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
} }
} }
return { totalVertices, totalFaces }; return { totalVertices, totalFaces };
} },
} };
} }

View File

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

View File

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

View File

@@ -1,12 +1,26 @@
import type { Graph, NodeDefinition, NodeInput, NodeRegistry, RuntimeExecutor, SyncCache } from "@nodes/types"; import type {
import { concatEncodedArrays, createLogger, encodeFloat, fastHashArrayBuffer, type PerformanceStore } from "@nodes/utils"; Graph,
Node,
NodeDefinition,
NodeInput,
NodeRegistry,
RuntimeExecutor,
SyncCache,
} from "@nodarium/types";
import {
concatEncodedArrays,
createLogger,
encodeFloat,
fastHashArrayBuffer,
type PerformanceStore,
} from "@nodarium/utils";
const log = createLogger("runtime-executor"); const log = createLogger("runtime-executor");
log.mute() log.mute();
function getValue(input: NodeInput, value?: unknown) { function getValue(input: NodeInput, value?: unknown) {
if (value === undefined && "value" in input) { if (value === undefined && "value" in input) {
value = input.value value = input.value;
} }
if (input.type === "float") { if (input.type === "float") {
@@ -15,7 +29,13 @@ function getValue(input: NodeInput, value?: unknown) {
if (Array.isArray(value)) { if (Array.isArray(value)) {
if (input.type === "vec3") { if (input.type === "vec3") {
return [0, value.length + 1, ...value.map(v => encodeFloat(v)), 1, 1] as number[]; return [
0,
value.length + 1,
...value.map((v) => encodeFloat(v)),
1,
1,
] as number[];
} }
return [0, value.length + 1, ...value, 1, 1] as number[]; return [0, value.length + 1, ...value, 1, 1] as number[];
} }
@@ -36,22 +56,25 @@ function getValue(input: NodeInput, value?: unknown) {
} }
export class MemoryRuntimeExecutor implements RuntimeExecutor { export class MemoryRuntimeExecutor implements RuntimeExecutor {
private definitionMap: Map<string, NodeDefinition> = new Map(); private definitionMap: Map<string, NodeDefinition> = new Map();
private randomSeed = Math.floor(Math.random() * 100000000); private seed = Math.floor(Math.random() * 100000000);
perf?: PerformanceStore; perf?: PerformanceStore;
constructor(private registry: NodeRegistry, private cache?: SyncCache<Int32Array>) { } constructor(
private registry: NodeRegistry,
private cache?: SyncCache<Int32Array>,
) {
this.cache = undefined;
}
private async getNodeDefinitions(graph: Graph) { private async getNodeDefinitions(graph: Graph) {
if (this.registry.status !== "ready") { if (this.registry.status !== "ready") {
throw new Error("Node registry is not ready"); throw new Error("Node registry is not ready");
} }
await this.registry.load(graph.nodes.map(node => node.type)); await this.registry.load(graph.nodes.map((node) => node.type));
const typeMap = new Map<string, NodeDefinition>(); const typeMap = new Map<string, NodeDefinition>();
for (const node of graph.nodes) { for (const node of graph.nodes) {
@@ -66,18 +89,22 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
} }
private async addMetaData(graph: Graph) { private async addMetaData(graph: Graph) {
// First, lets check if all nodes have a definition // First, lets check if all nodes have a definition
this.definitionMap = await this.getNodeDefinitions(graph); this.definitionMap = await this.getNodeDefinitions(graph);
const outputNode = graph.nodes.find(node => node.type.endsWith("/output")); const outputNode = graph.nodes.find((node) =>
node.type.endsWith("/output"),
) as Node;
if (!outputNode) { if (!outputNode) {
throw new Error("No output node found"); throw new Error("No output node found");
} }
outputNode.tmp = outputNode.tmp || {}; outputNode.tmp = outputNode.tmp || {};
outputNode.tmp.depth = 0; outputNode.tmp.depth = 0;
const nodeMap = new Map(graph.nodes.map(node => [node.id, node])); const nodeMap = new Map<number, Node>(
graph.nodes.map((node) => [node.id, node]),
);
// loop through all edges and assign the parent and child nodes to each node // loop through all edges and assign the parent and child nodes to each node
for (const edge of graph.edges) { for (const edge of graph.edges) {
@@ -96,7 +123,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
} }
} }
const nodes = [] const nodes = [];
// loop through all the nodes and assign each nodes its depth // loop through all the nodes and assign each nodes its depth
const stack = [outputNode]; const stack = [outputNode];
@@ -125,7 +152,6 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
} }
async execute(graph: Graph, settings: Record<string, unknown>) { async execute(graph: Graph, settings: Record<string, unknown>) {
this.perf?.addPoint("runtime"); this.perf?.addPoint("runtime");
let a = performance.now(); let a = performance.now();
@@ -148,31 +174,32 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
*/ */
// we execute the nodes from the bottom up // we execute the nodes from the bottom up
const sortedNodes = nodes.sort((a, b) => (b.tmp?.depth || 0) - (a.tmp?.depth || 0)); const sortedNodes = nodes.sort(
(a, b) => (b.tmp?.depth || 0) - (a.tmp?.depth || 0),
);
// here we store the intermediate results of the nodes // here we store the intermediate results of the nodes
const results: Record<string, Int32Array> = {}; const results: Record<string, Int32Array> = {};
for (const node of sortedNodes) { if (settings["randomSeed"]) {
this.seed = Math.floor(Math.random() * 100000000);
}
for (const node of sortedNodes) {
const node_type = this.definitionMap.get(node.type)!; const node_type = this.definitionMap.get(node.type)!;
if (!node_type || !node.tmp || !node_type.execute) { if (!node_type || !node.tmp || !node_type.execute) {
log.warn(`Node ${node.id} has no definition`); log.warn(`Node ${node.id} has no definition`);
continue; continue;
}; }
a = performance.now(); a = performance.now();
// Collect the inputs for the node // Collect the inputs for the node
const inputs = Object.entries(node_type.inputs || {}).map(([key, input]) => { const inputs = Object.entries(node_type.inputs || {}).map(
([key, input]) => {
if (input.type === "seed") { if (input.type === "seed") {
if (settings["randomSeed"] === true) { return this.seed;
return Math.floor(Math.random() * 100000000)
} else {
return this.randomSeed
}
} }
// If the input is linked to a setting, we use that value // If the input is linked to a setting, we use that value
@@ -184,7 +211,9 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
const inputNode = node.tmp?.inputNodes?.[key]; const inputNode = node.tmp?.inputNodes?.[key];
if (inputNode) { if (inputNode) {
if (results[inputNode.id] === undefined) { if (results[inputNode.id] === undefined) {
throw new Error(`Node ${node.type} is missing input from node ${inputNode.type}`); throw new Error(
`Node ${node.type} is missing input from node ${inputNode.type}`,
);
} }
return results[inputNode.id]; return results[inputNode.id];
} }
@@ -195,13 +224,13 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
} }
return getValue(input); return getValue(input);
}); },
);
b = performance.now(); b = performance.now();
this.perf?.addPoint("collected-inputs", b - a); this.perf?.addPoint("collected-inputs", b - a);
try { try {
a = performance.now(); a = performance.now();
const encoded_inputs = concatEncodedArrays(inputs); const encoded_inputs = concatEncodedArrays(inputs);
b = performance.now(); b = performance.now();
@@ -234,13 +263,10 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
this.perf?.addPoint("node/" + node_type.id, b - a); this.perf?.addPoint("node/" + node_type.id, b - a);
log.log("Result:", results[node.id]); log.log("Result:", results[node.id]);
log.groupEnd(); log.groupEnd();
} catch (e) { } catch (e) {
log.groupEnd(); log.groupEnd();
log.error(`Error executing node ${node_type.id || node.id}`, e); log.error(`Error executing node ${node_type.id || node.id}`, e);
} }
} }
// return the result of the parent of the output node // return the result of the parent of the output node
@@ -253,11 +279,9 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
this.perf?.endPoint("runtime"); this.perf?.endPoint("runtime");
return res as unknown as Int32Array; return res as unknown as Int32Array;
} }
getPerformanceData() { getPerformanceData() {
return this.perf?.get(); return this.perf?.get();
} }
} }

View File

@@ -1,19 +1,20 @@
import { MemoryRuntimeExecutor } from "./runtime-executor"; import { MemoryRuntimeExecutor } from "./runtime-executor";
import { RemoteNodeRegistry, IndexDBCache } from "@nodes/registry"; import { RemoteNodeRegistry, IndexDBCache } from "@nodarium/registry";
import type { Graph } from "@nodes/types"; import type { Graph } from "@nodarium/types";
import { createPerformanceStore } from "@nodes/utils"; import { createPerformanceStore } from "@nodarium/utils";
import { MemoryRuntimeCache } from "./runtime-executor-cache";
const cache = new MemoryRuntimeCache();
const indexDbCache = new IndexDBCache("node-registry"); const indexDbCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry(""); const nodeRegistry = new RemoteNodeRegistry("", indexDbCache);
nodeRegistry.cache = indexDbCache;
const executor = new MemoryRuntimeExecutor(nodeRegistry, cache); const executor = new MemoryRuntimeExecutor(nodeRegistry);
const performanceStore = createPerformanceStore(); const performanceStore = createPerformanceStore();
executor.perf = performanceStore; executor.perf = performanceStore;
export async function executeGraph(graph: Graph, settings: Record<string, unknown>): Promise<Int32Array> { export async function executeGraph(
graph: Graph,
settings: Record<string, unknown>,
): Promise<Int32Array> {
await nodeRegistry.load(graph.nodes.map((n) => n.type)); await nodeRegistry.load(graph.nodes.map((n) => n.type));
performanceStore.startRun(); performanceStore.startRun();
let res = await executor.execute(graph, settings); let res = await executor.execute(graph, settings);

View File

@@ -1,5 +1,5 @@
/// <reference types="vite-plugin-comlink/client" /> /// <reference types="vite-plugin-comlink/client" />
import type { Graph, RuntimeExecutor } from "@nodes/types"; import type { Graph, RuntimeExecutor } from "@nodarium/types";
export class WorkerRuntimeExecutor implements RuntimeExecutor { export class WorkerRuntimeExecutor implements RuntimeExecutor {

View File

@@ -0,0 +1,211 @@
<script lang="ts">
import NestedSettings from "./NestedSettings.svelte";
import { localState } from "$lib/helpers/localState.svelte";
import type { NodeInput } from "@nodarium/types";
import Input from "@nodarium/ui";
type Button = { type: "button"; callback: () => void; label?: string };
type InputType = NodeInput | Button;
type SettingsNode = InputType | SettingsGroup;
interface SettingsGroup {
title?: string;
[key: string]: any;
}
type SettingsType = Record<string, SettingsNode>;
type SettingsValue = Record<
string,
Record<string, unknown> | string | number | boolean | number[]
>;
type Props = {
id: string;
key?: string;
value: SettingsValue;
type: SettingsType;
depth?: number;
};
// Local persistent state for <details> sections
const openSections = localState<Record<string, boolean>>("open-details", {});
let { id, key = "", value = $bindable(), type, depth = 0 }: Props = $props();
function isNodeInput(v: SettingsNode | undefined): v is InputType {
return !!v && typeof v === "object" && "type" in v;
}
function getDefaultValue(): unknown {
if (key === "" || key === "title") return;
const node = type[key];
if (!isNodeInput(node)) return;
const anyNode = node as any;
// select input: use index into options
if (Array.isArray(anyNode.options)) {
if (value?.[key] !== undefined) {
return anyNode.options.indexOf(value[key]);
}
return 0;
}
if (value?.[key] !== undefined) return value[key];
if ("value" in node && anyNode.value !== undefined) {
return anyNode.value;
}
switch (node.type) {
case "boolean":
return 0;
case "float":
return 0.5;
case "integer":
case "select":
return 0;
default:
return 0;
}
}
let internalValue = $state(getDefaultValue());
let open = $state(openSections.value[id]);
// Persist <details> open/closed state for groups
if (depth > 0 && !isNodeInput(type[key!])) {
$effect(() => {
if (open !== undefined) {
openSections.value[id] = open;
}
});
}
// Sync internalValue back into `value`
$effect(() => {
if (key === "" || internalValue === undefined) return;
const node = type[key];
if (
isNodeInput(node) &&
Array.isArray((node as any).options) &&
typeof internalValue === "number"
) {
value[key] = (node as any)?.options?.[internalValue] as any;
} else {
value[key] = internalValue as any;
}
});
</script>
{#if key && isNodeInput(type?.[key])}
<!-- Leaf input -->
<div class="input input-{type[key].type}" class:first-level={depth === 1}>
{#if type[key].type === "button"}
<button onclick={() => "callback" in type[key] && type[key].callback()}>
{type[key].label || key}
</button>
{:else}
{#if type[key].label !== ""}
<label for={id}>{type[key].label || key}</label>
{/if}
<Input {id} input={type[key]} bind:value={internalValue} />
{/if}
</div>
{:else if depth === 0}
<!-- Root: iterate over top-level keys -->
{#each Object.keys(type ?? {}).filter((k) => k !== "title") as childKey}
<NestedSettings
id={`${id}.${childKey}`}
key={childKey}
bind:value
{type}
depth={depth + 1}
/>
{/each}
<hr />
{:else if key && type?.[key]}
<!-- Group -->
{#if depth > 0}
<hr />
{/if}
<details bind:open>
<summary><p>{(type[key] as SettingsGroup).title || key}</p></summary>
<div class="content">
{#each Object.keys(type[key] as SettingsGroup).filter((k) => k !== "title") as childKey}
<NestedSettings
id={`${id}.${childKey}`}
key={childKey}
bind:value={value[key] as SettingsValue}
type={type[key] as unknown as SettingsType}
depth={depth + 1}
/>
{/each}
</div>
</details>
{/if}
<style>
summary {
cursor: pointer;
user-select: none;
margin-bottom: 1em;
}
summary > p {
display: inline;
padding-left: 6px;
}
details {
padding: 1em;
padding-bottom: 0;
padding-left: 21px;
}
.input {
margin-top: 15px;
margin-bottom: 15px;
display: flex;
flex-direction: column;
gap: 10px;
padding-left: 20px;
}
.input-boolean {
display: flex;
flex-direction: row;
align-items: center;
}
.input-boolean > label {
order: 2;
}
.first-level.input {
padding-left: 1em;
padding-right: 1em;
padding-bottom: 1px;
}
button {
cursor: pointer;
}
hr {
position: absolute;
margin: 0;
left: 0;
right: 0;
border: none;
border-bottom: solid thin var(--outline);
}
</style>

View File

@@ -1,7 +1,14 @@
import { localState } from "$lib/helpers/localState.svelte"; import { localState } from "$lib/helpers/localState.svelte";
import type { NodeInput } from "@nodes/types";
const themes = ["dark", "light", "catppuccin", "solarized", "high-contrast", "nord", "dracula"]; const themes = [
"dark",
"light",
"catppuccin",
"solarized",
"high-contrast",
"nord",
"dracula",
] as const;
export const AppSettingTypes = { export const AppSettingTypes = {
theme: { theme: {
@@ -18,25 +25,25 @@ export const AppSettingTypes = {
centerCamera: { centerCamera: {
type: "boolean", type: "boolean",
label: "Center Camera", label: "Center Camera",
value: true value: true,
}, },
nodeInterface: { nodeInterface: {
title: "Node Interface", title: "Node Interface",
showNodeGrid: { showNodeGrid: {
type: "boolean", type: "boolean",
label: "Show Grid", label: "Show Grid",
value: true value: true,
}, },
snapToGrid: { snapToGrid: {
type: "boolean", type: "boolean",
label: "Snap to Grid", label: "Snap to Grid",
value: true value: true,
}, },
showHelp: { showHelp: {
type: "boolean", type: "boolean",
label: "Show Help", label: "Show Help",
value: false value: false,
} },
}, },
debug: { debug: {
title: "Debug", title: "Debug",
@@ -75,58 +82,63 @@ export const AppSettingTypes = {
label: "Show Stem Lines", label: "Show Stem Lines",
value: false, value: false,
}, },
showGraphJson: {
type: "boolean",
label: "Show Graph Source",
value: false,
},
stressTest: { stressTest: {
title: "Stress Test", title: "Stress Test",
amount: { amount: {
type: "integer", type: "integer",
min: 2, min: 2,
max: 15, max: 15,
value: 4 value: 4,
}, },
loadGrid: { loadGrid: {
type: "button", type: "button",
label: "Load Grid" label: "Load Grid",
}, },
loadTree: { loadTree: {
type: "button", type: "button",
label: "Load Tree" label: "Load Tree",
}, },
lottaFaces: { lottaFaces: {
type: "button", type: "button",
label: "Load 'lots of faces'" label: "Load 'lots of faces'",
}, },
lottaNodes: { lottaNodes: {
type: "button", type: "button",
label: "Load 'lots of nodes'" label: "Load 'lots of nodes'",
}, },
lottaNodesAndFaces: { lottaNodesAndFaces: {
type: "button", type: "button",
label: "Load 'lots of nodes and faces'" label: "Load 'lots of nodes and faces'",
}
}, },
},
},
} as const;
type SettingsToStore<T> =
T extends { value: infer V }
? V extends readonly string[]
? V[number]
: V
: T extends any[]
? {}
: T extends object
? {
[K in keyof T as T[K] extends object ? K : never]:
SettingsToStore<T[K]>
} }
} as const
type IsInputDefinition<T> = T extends NodeInput ? T : never;
type HasTitle = { title: string };
type ExtractSettingsValues<T> = {
[K in keyof T]: T[K] extends HasTitle
? ExtractSettingsValues<Omit<T[K], 'title'>>
: T[K] extends IsInputDefinition<T[K]>
? T[K] extends { value: any }
? T[K]['value']
: never
: T[K] extends Record<string, any>
? ExtractSettingsValues<T[K]>
: never; : never;
};
function settingsToStore<T>(settings: T): ExtractSettingsValues<T> { export function settingsToStore<T>(settings: T): SettingsToStore<T> {
const result = {} as any; const result = {} as any;
for (const key in settings) { for (const key in settings) {
const value = settings[key]; const value = settings[key];
if (value && typeof value === 'object') { if (value && typeof value === "object") {
if ('value' in value) { if ("value" in value) {
result[key] = value.value; result[key] = value.value;
} else { } else {
result[key] = settingsToStore(value); result[key] = settingsToStore(value);
@@ -136,11 +148,14 @@ function settingsToStore<T>(settings: T): ExtractSettingsValues<T> {
return result; return result;
} }
export const appSettings = localState("app-settings", settingsToStore(AppSettingTypes)); export let appSettings = localState(
"app-settings",
settingsToStore(AppSettingTypes),
);
$effect.root(() => { $effect.root(() => {
$effect(() => { $effect(() => {
const theme = appSettings.theme; const theme = appSettings.value.theme;
const classes = document.documentElement.classList; const classes = document.documentElement.classList;
const newClassName = `theme-${theme}`; const newClassName = `theme-${theme}`;
if (classes) { if (classes) {

View File

@@ -0,0 +1,27 @@
import type { NodeInput } from "@nodarium/types";
type Button = { type: "button"; label?: string };
export type SettingsStore = {
[key: string]: SettingsStore | string | number | boolean;
};
type InputType = NodeInput | Button;
type SettingsNode = InputType | SettingsGroup;
export interface SettingsGroup {
title?: string;
[key: string]: SettingsNode | string | number | undefined;
}
export type SettingsType = Record<string, SettingsNode>;
export type SettingsValue = Record<
string,
Record<string, unknown> | string | number | boolean | number[]
>;
export function isNodeInput(v: SettingsNode | undefined): v is InputType {
return !!v && "type" in v;
}

View File

@@ -1,40 +0,0 @@
<script lang="ts">
import type { NodeInput } from "@nodes/types";
import NestedSettings from "./NestedSettings.svelte";
import type { Writable } from "svelte/store";
interface Nested {
[key: string]: NodeInput | Nested;
}
export let type: Record<string, NodeInput>;
export let store: Writable<Record<string, any>>;
function constructNested(type: Record<string, NodeInput>) {
const nested: Nested = {};
for (const key in type) {
const parts = key.split(".");
let current = nested;
for (let i = 0; i < parts.length; i++) {
if (i === parts.length - 1) {
current[parts[i]] = type[key];
} else {
current[parts[i]] = current[parts[i]] || {};
current = current[parts[i]] as Nested;
}
}
}
return nested;
}
$: settings = constructNested({
randomSeed: { type: "boolean", value: false },
...type,
});
</script>
{#key settings}
<NestedSettings id="graph-settings" {settings} {store} />
{/key}

View File

@@ -1,157 +0,0 @@
<script module lang="ts">
let openSections = localState<Record<string,boolean>>("open-details", {});
</script>
<script lang="ts">
import NestedSettings from "./NestedSettings.svelte";
import {localState} from "$lib/helpers/localState.svelte";
import type { NodeInput } from "@nodes/types";
import Input from "@nodes/ui";
type Button = { type: "button"; label?: string };
type InputType = NodeInput | Button;
interface Nested {
[key: string]: (Nested & { title?: string }) | InputType;
}
type SettingsType = Record<string, Nested>;
type SettingsValue = Record<string, Record<string, unknown> | string | number | boolean>;
type Props = {
id: string;
key?: string;
value: SettingsValue;
type: SettingsType;
depth?: number;
};
let { id, key = "", value = $bindable(), type, depth = 0 }: Props = $props();
function isNodeInput(v: InputType | Nested): v is InputType {
return v && "type" in v;
}
let internalValue = $state(Array.isArray(type?.[key]?.options) ? type[key]?.options?.indexOf(value?.[key]) : value?.[key]);
let open = $state(openSections[id]);
if(depth > 0 && !isNodeInput(type[key])){
$effect(() => {
if(open !== undefined){
openSections[id] = open;
};
});
}
$effect(() => {
if(key === "" || internalValue === undefined) return;
if(isNodeInput(type[key]) && Array.isArray(type[key]?.options) && typeof internalValue === "number"){
value[key] = type[key].options?.[internalValue];
}else{
value[key] = internalValue;
}
})
</script>
{#if key && isNodeInput(type?.[key]) }
<div class="input input-{type[key].type}" class:first-level={depth === 1}>
{#if type[key].type === "button"}
<button onclick={() => console.log(type[key])}>
{type[key].label || key}
</button>
{:else}
<label for={id}>{type[key].label || key}</label>
<Input id={id} input={type[key]} bind:value={internalValue} />
{/if}
</div>
{:else}
{#if depth === 0}
{#each Object.keys(type).filter((key) => key !== "title") as childKey}
<NestedSettings
id={`${id}.${childKey}`}
key={childKey}
value={value}
type={type}
depth={depth + 1}
/>
{/each}
<hr />
{:else if key && type?.[key]}
{#if depth > 0}
<hr />
{/if}
<details bind:open>
<summary><p>{type[key]?.title||key}</p></summary>
<div class="content">
{#each Object.keys(type[key]).filter((key) => key !== "title") as childKey}
<NestedSettings
id={`${id}.${childKey}`}
key={childKey}
value={value[key] as SettingsValue}
type={type[key] as SettingsType}
depth={depth + 1}
/>
{/each}
</div>
</details>
{/if}
{/if}
<style>
summary {
cursor: pointer;
user-select: none;
margin-bottom: 1em;
}
summary::marker { }
summary > p {
display: inline;
padding-left: 6px;
}
details {
padding: 1em;
padding-bottom: 0;
padding-left: 21px;
}
.input {
margin-top: 15px;
margin-bottom: 15px;
display: flex;
flex-direction: column;
gap: 10px;
padding-left: 20px;
}
.input-boolean {
display: flex;
flex-direction: row;
align-items: center;
}
.input-boolean > label {
order: 2;
}
.first-level.input {
padding-left: 1em;
padding-right: 1em;
padding-bottom: 1px;
}
hr {
position: absolute;
margin: 0;
left: 0;
right: 0;
border: none;
border-bottom: solid thin var(--outline);
}
</style>

View File

@@ -116,7 +116,7 @@
align-items: center; align-items: center;
border-bottom: solid thin var(--outline); border-bottom: solid thin var(--outline);
border-left: solid thin var(--outline); border-left: solid thin var(--outline);
background: var(--layer-0); background: var(--layer-1);
} }
.tabs > button > span { .tabs > button > span {
@@ -124,7 +124,7 @@
} }
.tabs > button.active { .tabs > button.active {
background: var(--layer-1); background: var(--layer-2);
} }
.tabs > button.active span { .tabs > button.active span {

View File

@@ -1,16 +1,15 @@
<script lang="ts"> <script lang="ts">
import type { Node, NodeInput } from "@nodes/types"; import type { Node, NodeInput } from "@nodarium/types";
import NestedSettings from "./NestedSettings.svelte"; import NestedSettings from "$lib/settings/NestedSettings.svelte";
import type { GraphManager } from "$lib/graph-interface/graph-manager"; import type { GraphManager } from "$lib/graph-interface/graph-manager.svelte";
type Props = { type Props = {
manager: GraphManager; manager: GraphManager;
node: Node; node: Node;
}; };
const { manager, node }: Props = $props(); const { manager, node = $bindable() }: Props = $props();
const nodeDefinition = filterInputs(node.tmp?.type?.inputs);
function filterInputs(inputs?: Record<string, NodeInput>) { function filterInputs(inputs?: Record<string, NodeInput>) {
const _inputs = $state.snapshot(inputs); const _inputs = $state.snapshot(inputs);
return Object.fromEntries( return Object.fromEntries(
@@ -27,6 +26,7 @@
}), }),
); );
} }
const nodeDefinition = filterInputs(node.tmp?.type?.inputs);
type Store = Record<string, number | number[]>; type Store = Record<string, number | number[]>;
let store = $state<Store>(createStore(node?.props, nodeDefinition)); let store = $state<Store>(createStore(node?.props, nodeDefinition));
@@ -69,24 +69,18 @@
} }
$effect(() => { $effect(() => {
if (store && store) { if (store) {
updateNode(); updateNode();
} }
}); });
</script> </script>
{#if node} {#if Object.keys(nodeDefinition).length}
{#key node.id}
{#if nodeDefinition && store && Object.keys(nodeDefinition).length > 0}
<NestedSettings <NestedSettings
id="activeNodeSettings" id="activeNodeSettings"
bind:value={store} bind:value={store}
type={nodeDefinition} type={nodeDefinition}
/> />
{:else}
<p class="mx-4">Active Node has no Settings</p>
{/if}
{/key}
{:else} {:else}
<p class="mx-4">No active node</p> <p class="mx-4">Node has no settings</p>
{/if} {/if}

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { Node } from "@nodes/types"; import type { Node } from "@nodarium/types";
import type { GraphManager } from "$lib/graph-interface/graph-manager"; import type { GraphManager } from "$lib/graph-interface/graph-manager.svelte";
import ActiveNodeSelected from "./ActiveNodeSelected.svelte"; import ActiveNodeSelected from "./ActiveNodeSelected.svelte";
type Props = { type Props = {

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import localStore from "$lib/helpers/localStore"; import localStore from "$lib/helpers/localStore";
import { Integer } from "@nodes/ui"; import { Integer } from "@nodarium/ui";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { humanizeDuration } from "$lib/helpers"; import { humanizeDuration } from "$lib/helpers";
import Monitor from "$lib/performance/Monitor.svelte"; import Monitor from "$lib/performance/Monitor.svelte";

View File

@@ -3,7 +3,6 @@
import type { OBJExporter } from "three/addons/exporters/OBJExporter.js"; import type { OBJExporter } from "three/addons/exporters/OBJExporter.js";
import type { GLTFExporter } from "three/addons/exporters/GLTFExporter.js"; import type { GLTFExporter } from "three/addons/exporters/GLTFExporter.js";
import FileSaver from "file-saver"; import FileSaver from "file-saver";
import { appSettings } from "../app-settings.svelte";
// Download // Download
const download = ( const download = (
@@ -52,8 +51,6 @@
// download .obj file // download .obj file
download(result, "plant", "text/plain", "obj"); download(result, "plant", "text/plain", "obj");
} }
</script> </script>
<div class="p-2"> <div class="p-2">

View File

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

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { createKeyMap } from "$lib/helpers/createKeyMap"; import type { createKeyMap } from "$lib/helpers/createKeyMap";
import { ShortCut } from "@nodes/ui"; import { ShortCut } from "@nodarium/ui";
import { get } from "svelte/store"; import { get } from "svelte/store";
type Props = { type Props = {
@@ -11,7 +11,6 @@
}; };
let { keymaps }: Props = $props(); let { keymaps }: Props = $props();
console.log({ keymaps });
</script> </script>
<table class="wrapper"> <table class="wrapper">

6
app/src/lib/types.ts Normal file
View File

@@ -0,0 +1,6 @@
import type {
Graph,
NodeDefinition,
NodeInput,
} from "@nodarium/types";
export type { Graph, NodeDefinition, NodeInput };

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import "@nodes/ui/app.css"; import "@nodarium/ui/app.css";
import "virtual:uno.css"; import "virtual:uno.css";
import "@unocss/reset/normalize.css"; import "@unocss/reset/normalize.css";
</script> </script>

View File

@@ -2,59 +2,61 @@
import Grid from "$lib/grid"; import Grid from "$lib/grid";
import GraphInterface from "$lib/graph-interface"; import GraphInterface from "$lib/graph-interface";
import * as templates from "$lib/graph-templates"; import * as templates from "$lib/graph-templates";
import type { Graph, Node } from "@nodes/types"; import type { Graph, Node } from "@nodarium/types";
import Viewer from "$lib/result-viewer/Viewer.svelte"; import Viewer from "$lib/result-viewer/Viewer.svelte";
import Settings from "$lib/settings/Settings.svelte";
import { import {
appSettings, appSettings,
AppSettingTypes, AppSettingTypes,
} from "$lib/settings/app-settings.svelte"; } from "$lib/settings/app-settings.svelte";
import Keymap from "$lib/settings/panels/Keymap.svelte"; import Keymap from "$lib/sidebar/panels/Keymap.svelte";
import Sidebar from "$lib/sidebar/Sidebar.svelte";
import { createKeyMap } from "$lib/helpers/createKeyMap"; import { createKeyMap } from "$lib/helpers/createKeyMap";
import NodeStore from "$lib/node-store/NodeStore.svelte"; import NodeStore from "$lib/node-store/NodeStore.svelte";
import ActiveNodeSettings from "$lib/settings/panels/ActiveNodeSettings.svelte"; import ActiveNodeSettings from "$lib/sidebar/panels/ActiveNodeSettings.svelte";
import PerformanceViewer from "$lib/performance/PerformanceViewer.svelte"; import PerformanceViewer from "$lib/performance/PerformanceViewer.svelte";
import Panel from "$lib/settings/Panel.svelte"; import Panel from "$lib/sidebar/Panel.svelte";
import GraphSettings from "$lib/settings/panels/GraphSettings.svelte"; import NestedSettings from "$lib/settings/NestedSettings.svelte";
import NestedSettings from "$lib/settings/panels/NestedSettings.svelte";
import type { Group } from "three"; import type { Group } from "three";
import ExportSettings from "$lib/settings/panels/ExportSettings.svelte"; import ExportSettings from "$lib/sidebar/panels/ExportSettings.svelte";
import { import {
MemoryRuntimeCache, MemoryRuntimeCache,
WorkerRuntimeExecutor, WorkerRuntimeExecutor,
MemoryRuntimeExecutor, MemoryRuntimeExecutor,
} from "$lib/runtime"; } from "$lib/runtime";
import { IndexDBCache, RemoteNodeRegistry } from "@nodes/registry"; import { IndexDBCache, RemoteNodeRegistry } from "@nodarium/registry";
import { createPerformanceStore } from "@nodes/utils"; import { createPerformanceStore } from "@nodarium/utils";
import BenchmarkPanel from "$lib/settings/panels/BenchmarkPanel.svelte"; import BenchmarkPanel from "$lib/sidebar/panels/BenchmarkPanel.svelte";
import { debounceAsyncFunction } from "$lib/helpers"; import { debounceAsyncFunction } from "$lib/helpers";
import { onMount } from "svelte"; import GraphSource from "$lib/sidebar/panels/GraphSource.svelte";
let performanceStore = createPerformanceStore(); let performanceStore = createPerformanceStore();
const registryCache = new IndexDBCache("node-registry"); const registryCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry(""); const nodeRegistry = new RemoteNodeRegistry("", registryCache);
nodeRegistry.cache = registryCache;
const workerRuntime = new WorkerRuntimeExecutor(); const workerRuntime = new WorkerRuntimeExecutor();
const runtimeCache = new MemoryRuntimeCache(); const runtimeCache = new MemoryRuntimeCache();
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache); const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
memoryRuntime.perf = performanceStore; memoryRuntime.perf = performanceStore;
const runtime = $derived( const runtime = $derived(
appSettings.debug.useWorker ? workerRuntime : memoryRuntime, appSettings.value.debug.useWorker ? workerRuntime : memoryRuntime,
); );
let activeNode = $state<Node | undefined>(undefined); let activeNode = $state<Node | undefined>(undefined);
let scene = $state<Group>(null!); let scene = $state<Group>(null!);
let graph = localStorage.getItem("graph") let graph = $state(
localStorage.getItem("graph")
? JSON.parse(localStorage.getItem("graph")!) ? JSON.parse(localStorage.getItem("graph")!)
: templates.defaultPlant; : templates.defaultPlant,
);
function handleSave(graph: Graph) {
localStorage.setItem("graph", JSON.stringify(graph));
}
let graphInterface = $state<ReturnType<typeof GraphInterface>>(null!); let graphInterface = $state<ReturnType<typeof GraphInterface>>(null!);
let viewerComponent = $state<ReturnType<typeof Viewer>>(); let viewerComponent = $state<ReturnType<typeof Viewer>>();
const manager = $derived(graphInterface?.manager); const manager = $derived(graphInterface?.manager);
const managerStatus = $derived(manager?.status);
async function randomGenerate() { async function randomGenerate() {
if (!manager) return; if (!manager) return;
@@ -67,21 +69,39 @@
{ {
key: "r", key: "r",
description: "Regenerate the plant model", description: "Regenerate the plant model",
callback: randomGenerate, callback: () => randomGenerate(),
}, },
]); ]);
let graphSettings = $state<Record<string, any>>({}); let graphSettings = $state<Record<string, any>>({});
let graphSettingTypes = $state({}); $effect(() => {
if (graphSettings) {
manager?.setSettings($state.snapshot(graphSettings));
}
});
type BooleanSchema = {
[key: string]: {
type: "boolean";
value: false;
};
};
let graphSettingTypes = $state<BooleanSchema>({
randomSeed: { type: "boolean", value: false },
});
const handleUpdate = debounceAsyncFunction( let runIndex = 0;
async (g: Graph, s: Record<string, any> = graphSettings) => {
async function update(
g: Graph,
s: Record<string, any> = $state.snapshot(graphSettings),
) {
runIndex++;
performanceStore.startRun(); performanceStore.startRun();
try { try {
let a = performance.now(); let a = performance.now();
const graphResult = await runtime.execute(g, $state.snapshot(s)); const graphResult = await runtime.execute(g, s);
let b = performance.now(); let b = performance.now();
if (appSettings.debug.useWorker) { if (appSettings.value.debug.useWorker) {
let perfData = await runtime.getPerformanceData(); let perfData = await runtime.getPerformanceData();
let lastRun = perfData?.at(-1); let lastRun = perfData?.at(-1);
if (lastRun?.total) { if (lastRun?.total) {
@@ -94,49 +114,48 @@
); );
} }
} }
viewerComponent?.update(graphResult); viewerComponent?.update(graphResult);
} catch (error) { } catch (error) {
console.log("errors", error); console.log("errors", error);
} finally { } finally {
performanceStore.stopRun(); performanceStore.stopRun();
} }
},
);
// $ if (AppSettings) {
// //@ts-ignore
// AppSettingTypes.debug.stressTest.loadGrid.callback = () => {
// graph = templates.grid($AppSettings.amount, $AppSettings.amount);
// };
// //@ts-ignore
// AppSettingTypes.debug.stressTest.loadTree.callback = () => {
// graph = templates.tree($AppSettings.amount);
// };
// //@ts-ignore
// AppSettingTypes.debug.stressTest.lottaFaces.callback = () => {
// graph = templates.lottaFaces;
// };
// //@ts-ignore
// AppSettingTypes.debug.stressTest.lottaNodes.callback = () => {
// graph = templates.lottaNodes;
// };
// //@ts-ignore
// AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => {
// graph = templates.lottaNodesAndFaces;
// };
// }
function handleSave(graph: Graph) {
localStorage.setItem("graph", JSON.stringify(graph));
} }
onMount(() => {
handleUpdate(graph); const handleUpdate = debounceAsyncFunction(update);
$effect(() => {
//@ts-ignore
AppSettingTypes.debug.stressTest.loadGrid.callback = () => {
manager.load(
templates.grid(
appSettings.value.debug.stressTest.amount,
appSettings.value.debug.stressTest.amount,
),
);
};
//@ts-ignore
AppSettingTypes.debug.stressTest.loadTree.callback = () => {
manager.load(templates.tree(appSettings.value.debug.stressTest.amount));
};
//@ts-ignore
AppSettingTypes.debug.stressTest.lottaFaces.callback = () => {
manager.load(templates.lottaFaces as unknown as Graph);
};
//@ts-ignore
AppSettingTypes.debug.stressTest.lottaNodes.callback = () => {
manager.load(templates.lottaNodes as unknown as Graph);
};
//@ts-ignore
AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => {
manager.load(templates.lottaNodesAndFaces as unknown as Graph);
};
}); });
</script> </script>
<svelte:document on:keydown={applicationKeymap.handleKeyboardEvent} /> <svelte:document onkeydown={applicationKeymap.handleKeyboardEvent} />
<div class="wrapper manager-{$managerStatus}">
<div class="wrapper manager-{manager?.status}">
<header></header> <header></header>
<Grid.Row> <Grid.Row>
<Grid.Cell> <Grid.Cell>
@@ -144,29 +163,28 @@
bind:scene bind:scene
bind:this={viewerComponent} bind:this={viewerComponent}
perf={performanceStore} perf={performanceStore}
centerCamera={appSettings.centerCamera} centerCamera={appSettings.value.centerCamera}
/> />
</Grid.Cell> </Grid.Cell>
<Grid.Cell> <Grid.Cell>
{#key graph}
<GraphInterface <GraphInterface
bind:this={graphInterface}
{graph} {graph}
bind:this={graphInterface}
registry={nodeRegistry} registry={nodeRegistry}
showGrid={appSettings.nodeInterface.showNodeGrid} showGrid={appSettings.value.nodeInterface.showNodeGrid}
snapToGrid={appSettings.nodeInterface.snapToGrid} snapToGrid={appSettings.value.nodeInterface.snapToGrid}
bind:activeNode bind:activeNode
bind:showHelp={appSettings.nodeInterface.showHelp} bind:showHelp={appSettings.value.nodeInterface.showHelp}
bind:settings={graphSettings} bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes} bind:settingTypes={graphSettingTypes}
onresult={(result) => handleUpdate(result)} onresult={(result) => handleUpdate(result)}
onsave={(graph) => handleSave(graph)} onsave={(graph) => handleSave(graph)}
/> />
<Settings> <Sidebar>
<Panel id="general" title="General" icon="i-tabler-settings"> <Panel id="general" title="General" icon="i-tabler-settings">
<NestedSettings <NestedSettings
id="general" id="general"
value={appSettings} bind:value={appSettings.value}
type={AppSettingTypes} type={AppSettingTypes}
/> />
</Panel> </Panel>
@@ -178,7 +196,7 @@
<Keymap <Keymap
keymaps={[ keymaps={[
{ keymap: applicationKeymap, title: "Application" }, { keymap: applicationKeymap, title: "Application" },
{ keymap: graphInterface.keymap, title: "Node-Editor" }, { keymap: graphInterface?.keymap, title: "Node-Editor" },
]} ]}
/> />
</Panel> </Panel>
@@ -197,18 +215,26 @@
id="performance" id="performance"
title="Performance" title="Performance"
classes="text-red-400" classes="text-red-400"
hidden={!appSettings.debug.showPerformancePanel} hidden={!appSettings.value.debug.showPerformancePanel}
icon="i-tabler-brand-speedtest" icon="i-tabler-brand-speedtest"
> >
{#if $performanceStore} {#if $performanceStore}
<PerformanceViewer data={$performanceStore} /> <PerformanceViewer data={$performanceStore} />
{/if} {/if}
</Panel> </Panel>
<Panel
id="graph-source"
title="Graph Source"
hidden={!appSettings.value.debug.showGraphJson}
icon="i-tabler-code"
>
<GraphSource {graph} />
</Panel>
<Panel <Panel
id="benchmark" id="benchmark"
title="Benchmark" title="Benchmark"
classes="text-red-400" classes="text-red-400"
hidden={!appSettings.debug.showBenchmarkPanel} hidden={!appSettings.value.debug.showBenchmarkPanel}
icon="i-tabler-graph" icon="i-tabler-graph"
> >
<BenchmarkPanel run={randomGenerate} /> <BenchmarkPanel run={randomGenerate} />
@@ -219,9 +245,11 @@
classes="text-blue-400" classes="text-blue-400"
icon="i-custom-graph" icon="i-custom-graph"
> >
{#if Object.keys(graphSettingTypes).length > 0} <NestedSettings
<GraphSettings type={graphSettingTypes} store={graphSettings} /> id="graph-settings"
{/if} type={graphSettingTypes}
bind:value={graphSettings}
/>
</Panel> </Panel>
<Panel <Panel
id="active-node" id="active-node"
@@ -231,15 +259,13 @@
> >
<ActiveNodeSettings {manager} node={activeNode} /> <ActiveNodeSettings {manager} node={activeNode} />
</Panel> </Panel>
</Settings> </Sidebar>
{/key}
</Grid.Cell> </Grid.Cell>
</Grid.Row> </Grid.Row>
</div> </div>
<style> <style>
header { header {
/* border-bottom: solid thin var(--outline); */
background-color: var(--layer-1); background-color: var(--layer-1);
} }

View File

@@ -1,27 +1,36 @@
import type { RequestHandler } from "./$types"; import type { EntryGenerator, RequestHandler } from "./$types";
import * as registry from "$lib/node-registry"; import * as registry from "$lib/node-registry";
import type { EntryGenerator } from "../$types";
export const prerender = true; export const prerender = true;
export const entries: EntryGenerator = async () => { export const entries: EntryGenerator = async () => {
const users = await registry.getUsers(); const users = await registry.getUsers();
return users.map(user => { return users
return user.collections.map(collection => { .map((user) => {
return collection.nodes.map(node => { return user.collections.map((collection) => {
return { user: user.id, collection: collection.id.split("/")[1], node: node.id.split("/")[2] } return collection.nodes.map((node) => {
return {
user: user.id,
collection: collection.id.split("/")[1],
node: node.id.split("/")[2],
};
});
}); });
}) })
}).flat(2); .flat(2);
} };
export const GET: RequestHandler = async function GET({ params }) { export const GET: RequestHandler = async function GET({ params }) {
const wasm = await registry.getWasm(
const wasm = await registry.getWasm(`${params.user}/${params.collection}/${params.node}`); `${params.user}/${params.collection}/${params.node}`,
);
if (!wasm) { if (!wasm) {
return new Response("Not found", { status: 404 }); return new Response("Not found", { status: 404 });
} }
return new Response(wasm, { status: 200, headers: { "Content-Type": "application/wasm" } }); return new Response(wasm, {
} status: 200,
headers: { "Content-Type": "application/wasm" },
});
};

View File

@@ -1,100 +0,0 @@
<script lang="ts">
import {
decodeFloat,
encodeFloat,
decodeNestedArray,
encodeNestedArray,
concatEncodedArrays,
} from "@nodes/utils";
console.clear();
{
const encodedPositions = new Int32Array([
encodeFloat(1.1),
encodeFloat(2.0),
encodeFloat(3.0),
encodeFloat(4.0),
encodeFloat(5.0),
encodeFloat(6.0),
encodeFloat(7.0),
encodeFloat(8.0),
encodeFloat(9.0),
]);
// Create a Float32Array using the same buffer that backs the Int32Array
const floatView = new Float32Array(encodedPositions.buffer);
console.log({ encodedPositions, floatView });
}
if (false) {
const input_a = encodeNestedArray([1, 2, 3]);
const input_b = 2;
const input_c = 89;
const input_d = encodeNestedArray([4, 5, 6]);
const output = concatNestedArrays([input_a, input_b, input_c, input_d]);
const decoded = decodeNestedArray(output);
console.log("CONCAT", [input_a, input_b, input_c, input_d]);
console.log(output);
console.log(decoded);
}
if (false) {
let maxError = 0;
new Array(10_000).fill(null).forEach((v, i) => {
const input = i < 5_000 ? i : Math.random() * 100;
const encoded = encodeFloat(input);
const output = decodeFloat(encoded[0], encoded[1]);
const error = Math.abs(input - output);
if (error > maxError) {
maxError = error;
}
});
console.log("DECODE FLOAT");
console.log(maxError);
console.log(encodeFloat(2.0));
console.log("----");
}
if (false) {
console.log("Turning Int32Array into Array");
const test_size = 2_000_000;
const a = new Int32Array(test_size);
let t0 = performance.now();
for (let i = 0; i < test_size; i++) {
a[i] = Math.floor(Math.random() * 100);
}
console.log("TIME", performance.now() - t0);
t0 = performance.now();
const b = [...a.slice(0, test_size)];
console.log("TIME", performance.now() - t0);
console.log(typeof b, Array.isArray(b), b instanceof Int32Array);
}
if (false) {
// const input = [5, [6, 1], [7, 2, [5, 1]]];
// const input = [5, [], [6, []], []];
// const input = [52];
const input = [0, 0, [0, 2, 0, 128, 0, 128], 0, 128];
console.log("INPUT");
console.log(input);
let encoded = encodeNestedArray(input);
// encoded = [];
console.log("ENCODED");
console.log(encoded);
encoded = [0, 2, 1, 0, 4, 4, 2, 4, 1, 2, 2, 0, 3, 2, 3, 1, 1, 1, 1];
const decoded = decodeNestedArray(encoded);
console.log("DECODED");
console.log(decoded);
console.log("EQUALS", JSON.stringify(input) === JSON.stringify(decoded));
}
</script>

View File

@@ -10,22 +10,22 @@
"strength": { "strength": {
"type": "float", "type": "float",
"min": 0, "min": 0,
"max": 1 "max": 1,
"value": 1, "value": 1
}, },
"curviness": { "curviness": {
"type": "float", "type": "float",
"hidden": true, "hidden": true,
"min": 0, "min": 0,
"max": 1, "max": 1,
"value": 0.5, "value": 0.5
}, },
"depth": { "depth": {
"type": "integer", "type": "integer",
"min": 1, "min": 1,
"max": 10, "max": 10,
"hidden": true, "hidden": true,
"value": 1, "value": 1
} }
} }
} }

View File

@@ -1,10 +1,12 @@
{ {
"scripts": { "scripts": {
"build": "pnpm build:nodes && pnpm build:app", "build": "pnpm build:nodes && pnpm build:app",
"build:story": "pnpm -r --filter 'ui' story:build",
"build:app": "BASE_PATH=/ui pnpm -r --filter 'ui' build && pnpm -r --filter 'app' build", "build:app": "BASE_PATH=/ui pnpm -r --filter 'ui' build && pnpm -r --filter 'app' build",
"build:nodes": "pnpm -r --filter './nodes/**' build", "build:nodes": "pnpm -r --filter './nodes/**' build",
"dev:nodes": "pnpm -r --parallel --filter './nodes/**' dev", "dev:nodes": "pnpm -r --parallel --filter './nodes/**' dev",
"build:deploy": "pnpm build && cp -r ./packages/ui/storybook-static ./app/build/ui", "build:deploy": "pnpm build",
"dev": "pnpm -r --filter 'app' --filter './packages/node-registry' dev" "dev": "pnpm -r --filter 'app' --filter './packages/node-registry' dev"
} },
"packageManager": "pnpm@10.24.0"
} }

View File

@@ -1,5 +1,5 @@
{ {
"name": "@nodes/registry", "name": "@nodarium/registry",
"version": "0.0.0", "version": "0.0.0",
"description": "", "description": "",
"main": "src/index.ts", "main": "src/index.ts",
@@ -10,8 +10,8 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@nodes/types": "link:../types", "@nodarium/types": "link:../types",
"@nodes/utils": "link:../utils", "@nodarium/utils": "link:../utils",
"idb": "^8.0.0" "idb": "^8.0.3"
} }
} }

View File

@@ -1,4 +1,4 @@
import type { AsyncCache } from '@nodes/types'; import type { AsyncCache } from '@nodarium/types';
import { openDB, type IDBPDatabase } from 'idb'; import { openDB, type IDBPDatabase } from 'idb';
export class IndexDBCache implements AsyncCache<ArrayBuffer> { export class IndexDBCache implements AsyncCache<ArrayBuffer> {

View File

@@ -1,73 +1,79 @@
import { type NodeRegistry, type NodeDefinition, NodeDefinitionSchema, type AsyncCache } from "@nodes/types"; import {
import { createWasmWrapper, createLogger } from "@nodes/utils"; NodeDefinitionSchema,
type AsyncCache,
type NodeDefinition,
type NodeRegistry,
} from "@nodarium/types";
import { createLogger, createWasmWrapper } from "@nodarium/utils";
const log = createLogger("node-registry"); const log = createLogger("node-registry");
log.mute(); log.mute();
export class RemoteNodeRegistry implements NodeRegistry { export class RemoteNodeRegistry implements NodeRegistry {
status: "loading" | "ready" | "error" = "loading"; status: "loading" | "ready" | "error" = "loading";
private nodes: Map<string, NodeDefinition> = new Map(); private nodes: Map<string, NodeDefinition> = new Map();
cache?: AsyncCache<ArrayBuffer>; async fetchJson(url: string) {
const response = await fetch(`${this.url}/${url}`);
fetch: typeof fetch = globalThis.fetch.bind(globalThis);
constructor(private url: string) { }
async fetchUsers() {
const response = await this.fetch(`${this.url}/nodes/users.json`);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to load users`); log.error(`Failed to load ${url}`, { response, url, host: this.url });
throw new Error(`Failed to load ${url}`);
} }
return response.json(); return response.json();
} }
async fetchUser(userId: `${string}`) { async fetchArrayBuffer(url: string) {
const response = await this.fetch(`${this.url}/nodes/${userId}.json`); const response = await fetch(`${this.url}/${url}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to load user ${userId}`); log.error(`Failed to load ${url}`, { response, url, host: this.url });
} throw new Error(`Failed to load ${url}`);
return response.json();
}
async fetchCollection(userCollectionId: `${string}/${string}`) {
const response = await this.fetch(`${this.url}/nodes/${userCollectionId}.json`);
if (!response.ok) {
throw new Error(`Failed to load collection ${userCollectionId}`);
}
return response.json();
}
async fetchNodeDefinition(nodeId: `${string}/${string}/${string}`) {
const response = await this.fetch(`${this.url}/nodes/${nodeId}.json`);
if (!response.ok) {
throw new Error(`Failed to load node definition ${nodeId}`);
}
return response.json()
}
private async fetchNodeWasm(nodeId: `${string}/${string}/${string}`) {
const response = await this.fetch(`${this.url}/nodes/${nodeId}.wasm`);
if (!response.ok) {
if (this.cache) {
let value = await this.cache.get(nodeId);
if (value) {
return value;
}
}
throw new Error(`Failed to load node wasm ${nodeId}`);
} }
return response.arrayBuffer(); return response.arrayBuffer();
} }
constructor(
private url: string,
private cache?: AsyncCache<ArrayBuffer>,
) { }
async fetchUsers() {
return this.fetchJson(`nodes/users.json`);
}
async fetchUser(userId: `${string}`) {
return this.fetchJson(`user/${userId}.json`);
}
async fetchCollection(userCollectionId: `${string}/${string}`) {
return this.fetchJson(`nodes/${userCollectionId}.json`);
}
async fetchNodeDefinition(nodeId: `${string}/${string}/${string}`) {
return this.fetchJson(`nodes/${nodeId}.json`);
}
private async fetchNodeWasm(nodeId: `${string}/${string}/${string}`) {
const cachedNode = await this.cache?.get(nodeId);
if (cachedNode) {
return cachedNode;
}
const node = await this.fetchArrayBuffer(`nodes/${nodeId}.wasm`);
if (!node) {
throw new Error(`Failed to load node wasm ${nodeId}`);
}
return node;
}
async load(nodeIds: `${string}/${string}/${string}`[]) { async load(nodeIds: `${string}/${string}/${string}`[]) {
const a = performance.now(); const a = performance.now();
const nodes = await Promise.all([...new Set(nodeIds).values()].map(async id => { const nodes = await Promise.all(
[...new Set(nodeIds).values()].map(async (id) => {
if (this.nodes.has(id)) { if (this.nodes.has(id)) {
return this.nodes.get(id)!; return this.nodes.get(id)!;
} }
@@ -75,9 +81,8 @@ export class RemoteNodeRegistry implements NodeRegistry {
const wasmBuffer = await this.fetchNodeWasm(id); const wasmBuffer = await this.fetchNodeWasm(id);
return this.register(wasmBuffer); return this.register(wasmBuffer);
}),
})); );
const duration = performance.now() - a; const duration = performance.now() - a;
@@ -87,11 +92,10 @@ export class RemoteNodeRegistry implements NodeRegistry {
log.groupEnd(); log.groupEnd();
this.status = "ready"; this.status = "ready";
return nodes return nodes;
} }
async register(wasmBuffer: ArrayBuffer) { async register(wasmBuffer: ArrayBuffer) {
const wrapper = createWasmWrapper(wasmBuffer); const wrapper = createWasmWrapper(wasmBuffer);
const definition = NodeDefinitionSchema.safeParse(wrapper.get_definition()); const definition = NodeDefinitionSchema.safeParse(wrapper.get_definition());
@@ -107,8 +111,8 @@ export class RemoteNodeRegistry implements NodeRegistry {
let node = { let node = {
...definition.data, ...definition.data,
execute: wrapper.execute execute: wrapper.execute,
} };
this.nodes.set(definition.data.id, node); this.nodes.set(definition.data.id, node);

View File

@@ -0,0 +1,19 @@
{
"name": "store-client",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"generate": "openapi-ts -i ../../store/openapi.json -o src/client -c @hey-api/client-fetch"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@hey-api/client-fetch": "^0.13.1"
},
"devDependencies": {
"@hey-api/openapi-ts": "^0.88.0"
}
}

View File

@@ -0,0 +1,3 @@
// This file is auto-generated by @hey-api/openapi-ts
export * from './sdk.gen';
export * from './types.gen';

View File

@@ -0,0 +1,55 @@
// This file is auto-generated by @hey-api/openapi-ts
import { createClient, createConfig, type OptionsLegacyParser } from '@hey-api/client-fetch';
import type { GetV1NodesByUserJsonData, GetV1NodesByUserJsonError, GetV1NodesByUserJsonResponse, GetV1NodesByUserBySystemJsonData, GetV1NodesByUserBySystemJsonError, GetV1NodesByUserBySystemJsonResponse, GetV1NodesByUserBySystemByNodeIdby-.+jsonData, GetV1NodesByUserBySystemByNodeIdby-.+jsonError, GetV1NodesByUserBySystemByNodeIdby-.+jsonResponse, GetV1NodesByUserBySystemByNodeIdby-.+WasmData, GetV1NodesByUserBySystemByNodeIdby-.+WasmError, GetV1NodesByUserBySystemByNodeIdby-.+WasmResponse, PostV1NodesError, PostV1NodesResponse, GetV1UsersUsersJsonError, GetV1UsersUsersJsonResponse, GetV1UsersByUserIdJsonData, GetV1UsersByUserIdJsonError, GetV1UsersByUserIdJsonResponse } from './types.gen';
export const client = createClient(createConfig());
export const getV1NodesByUserJson = <ThrowOnError extends boolean = false>(options: OptionsLegacyParser<GetV1NodesByUserJsonData, ThrowOnError>) => {
return (options?.client ?? client).get<GetV1NodesByUserJsonResponse, GetV1NodesByUserJsonError, ThrowOnError>({
...options,
url: '/v1/nodes/{user}.json'
});
};
export const getV1NodesByUserBySystemJson = <ThrowOnError extends boolean = false>(options: OptionsLegacyParser<GetV1NodesByUserBySystemJsonData, ThrowOnError>) => {
return (options?.client ?? client).get<GetV1NodesByUserBySystemJsonResponse, GetV1NodesByUserBySystemJsonError, ThrowOnError>({
...options,
url: '/v1/nodes/{user}/{system}.json'
});
};
export const getV1NodesByUserBySystemByNodeIdby-.+Json = <ThrowOnError extends boolean = false>(options: OptionsLegacyParser<GetV1NodesByUserBySystemByNodeIdby-.+jsonData, ThrowOnError>) => {
return (options?.client ?? client).get<GetV1NodesByUserBySystemByNodeIdby-.+jsonResponse, GetV1NodesByUserBySystemByNodeIdby-.+jsonError, ThrowOnError>({
...options,
url: '/v1/nodes/{user}/{system}/{nodeId}{.+\\.json}'
});
};
export const getV1NodesByUserBySystemByNodeIdby-.+Wasm = <ThrowOnError extends boolean = false>(options: OptionsLegacyParser<GetV1NodesByUserBySystemByNodeIdby-.+WasmData, ThrowOnError>) => {
return (options?.client ?? client).get<GetV1NodesByUserBySystemByNodeIdby-.+WasmResponse, GetV1NodesByUserBySystemByNodeIdby-.+WasmError, ThrowOnError>({
...options,
url: '/v1/nodes/{user}/{system}/{nodeId}{.+\\.wasm}'
});
};
export const postV1Nodes = <ThrowOnError extends boolean = false>(options?: OptionsLegacyParser<unknown, ThrowOnError>) => {
return (options?.client ?? client).post<PostV1NodesResponse, PostV1NodesError, ThrowOnError>({
...options,
url: '/v1/nodes'
});
};
export const getV1UsersUsersJson = <ThrowOnError extends boolean = false>(options?: OptionsLegacyParser<unknown, ThrowOnError>) => {
return (options?.client ?? client).get<GetV1UsersUsersJsonResponse, GetV1UsersUsersJsonError, ThrowOnError>({
...options,
url: '/v1/users/users.json'
});
};
export const getV1UsersByUserIdJson = <ThrowOnError extends boolean = false>(options?: OptionsLegacyParser<GetV1UsersByUserIdJsonData, ThrowOnError>) => {
return (options?.client ?? client).get<GetV1UsersByUserIdJsonResponse, GetV1UsersByUserIdJsonError, ThrowOnError>({
...options,
url: '/v1/users/{userId}.json'
});
};

View File

@@ -0,0 +1,173 @@
// This file is auto-generated by @hey-api/openapi-ts
export type NodeDefinition = {
id: string;
inputs?: {
[key: string]: NodeInput;
};
outputs?: Array<(string)>;
meta?: {
description?: string;
title?: string;
};
};
export type NodeInput = {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'seed';
value?: number;
} | {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'boolean';
value?: boolean;
} | {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'float';
element?: 'slider';
value?: number;
min?: number;
max?: number;
step?: number;
} | {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'integer';
element?: 'slider';
value?: number;
min?: number;
max?: number;
} | {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'select';
options?: Array<(string)>;
value?: number;
} | {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'vec3';
value?: Array<(number)>;
} | {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'geometry';
} | {
internal?: boolean;
external?: boolean;
setting?: string;
label?: string;
description?: string;
accepts?: Array<(string)>;
hidden?: boolean;
type: 'path';
};
export type type = 'seed';
export type element = 'slider';
export type User = {
id: string;
name: string;
};
export type GetV1NodesByUserJsonData = {
path: {
user: string;
};
};
export type GetV1NodesByUserJsonResponse = (Array<NodeDefinition>);
export type GetV1NodesByUserJsonError = unknown;
export type GetV1NodesByUserBySystemJsonData = {
path: {
system?: string;
user: string;
};
};
export type GetV1NodesByUserBySystemJsonResponse = (Array<NodeDefinition>);
export type GetV1NodesByUserBySystemJsonError = unknown;
export type GetV1NodesByUserBySystemByNodeIdby-.+jsonData = {
path: {
nodeId: string;
system: string;
user: string;
};
};
export type GetV1NodesByUserBySystemByNodeIdby-.+jsonResponse = (NodeDefinition);
export type GetV1NodesByUserBySystemByNodeIdby-.+jsonError = unknown;
export type GetV1NodesByUserBySystemByNodeIdby-.+WasmData = {
path: {
nodeId: string;
system: string;
user: string;
};
};
export type GetV1NodesByUserBySystemByNodeIdby-.+WasmResponse = (unknown);
export type GetV1NodesByUserBySystemByNodeIdby-.+WasmError = unknown;
export type PostV1NodesResponse = (NodeDefinition);
export type PostV1NodesError = unknown;
export type GetV1UsersUsersJsonResponse = (Array<User>);
export type GetV1UsersUsersJsonError = unknown;
export type GetV1UsersByUserIdJsonData = {
path?: {
userId?: string;
};
};
export type GetV1UsersByUserIdJsonResponse = (User);
export type GetV1UsersByUserIdJsonError = unknown;

View File

@@ -1,5 +1,5 @@
{ {
"name": "@nodes/types", "name": "@nodarium/types",
"version": "0.0.0", "version": "0.0.0",
"description": "", "description": "",
"main": "src/index.ts", "main": "src/index.ts",
@@ -13,6 +13,6 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"zod": "^3.23.8" "zod": "^4.1.12"
} }
} }

View File

@@ -1,4 +1,4 @@
import { Graph, NodeDefinition, NodeId } from "./types"; import type { Graph, NodeDefinition, NodeType } from "./types";
export interface NodeRegistry { export interface NodeRegistry {
/** /**
@@ -13,13 +13,13 @@ export interface NodeRegistry {
* @throws An error if the nodes could not be loaded * @throws An error if the nodes could not be loaded
* @remarks This method should be called before calling getNode or getAllNodes * @remarks This method should be called before calling getNode or getAllNodes
*/ */
load: (nodeIds: NodeId[]) => Promise<NodeDefinition[]>; load: (nodeIds: NodeType[]) => Promise<NodeDefinition[]>;
/** /**
* Get a node by id * Get a node by id
* @param id - The id of the node to get * @param id - The id of the node to get
* @returns The node with the given id, or undefined if no such node exists * @returns The node with the given id, or undefined if no such node exists
*/ */
getNode: (id: NodeId | string) => NodeDefinition | undefined; getNode: (id: NodeType | string) => NodeDefinition | undefined;
/** /**
* Get all nodes * Get all nodes
* @returns An array of all nodes * @returns An array of all nodes
@@ -32,9 +32,6 @@ export interface NodeRegistry {
* @returns The node definition * @returns The node definition
*/ */
register: (wasmBuffer: ArrayBuffer) => Promise<NodeDefinition>; register: (wasmBuffer: ArrayBuffer) => Promise<NodeDefinition>;
cache?: AsyncCache<ArrayBuffer>;
} }
export interface RuntimeExecutor { export interface RuntimeExecutor {
@@ -43,7 +40,10 @@ export interface RuntimeExecutor {
* @param graph - The graph to execute * @param graph - The graph to execute
* @returns The result of the execution * @returns The result of the execution
*/ */
execute: (graph: Graph, settings: Record<string, unknown>) => Promise<Int32Array>; execute: (
graph: Graph,
settings: Record<string, unknown>,
) => Promise<Int32Array>;
} }
export interface SyncCache<T = unknown> { export interface SyncCache<T = unknown> {
@@ -69,7 +69,6 @@ export interface SyncCache<T = unknown> {
* Clear the cache * Clear the cache
*/ */
clear: () => void; clear: () => void;
} }
export interface AsyncCache<T = unknown> { export interface AsyncCache<T = unknown> {

View File

@@ -1,6 +1,17 @@
export type { NodeInput } from "./inputs"; export type { NodeInput } from "./inputs";
export type { NodeRegistry, RuntimeExecutor, SyncCache, AsyncCache } from "./components"; export type {
export type { Node, NodeDefinition, Socket, NodeId, Edge, Graph } from "./types"; NodeRegistry,
RuntimeExecutor,
SyncCache,
AsyncCache,
} from "./components";
export type {
Node,
NodeDefinition,
Socket,
NodeType,
Edge,
Graph,
} from "./types";
export { NodeSchema, GraphSchema } from "./types"; export { NodeSchema, GraphSchema } from "./types";
export { NodeDefinitionSchema } from "./types"; export { NodeDefinitionSchema } from "./types";

View File

@@ -6,11 +6,23 @@ const DefaultOptionsSchema = z.object({
setting: z.string().optional(), setting: z.string().optional(),
label: z.string().optional(), label: z.string().optional(),
description: z.string().optional(), description: z.string().optional(),
accepts: z.array(z.string()).optional(), accepts: z
.array(
z.union([
z.literal("float"),
z.literal("integer"),
z.literal("boolean"),
z.literal("select"),
z.literal("seed"),
z.literal("vec3"),
z.literal("geometry"),
z.literal("path"),
]),
)
.optional(),
hidden: z.boolean().optional(), hidden: z.boolean().optional(),
}); });
export const NodeInputFloatSchema = z.object({ export const NodeInputFloatSchema = z.object({
...DefaultOptionsSchema.shape, ...DefaultOptionsSchema.shape,
type: z.literal("float"), type: z.literal("float"),
@@ -40,7 +52,7 @@ export const NodeInputSelectSchema = z.object({
...DefaultOptionsSchema.shape, ...DefaultOptionsSchema.shape,
type: z.literal("select"), type: z.literal("select"),
options: z.array(z.string()).optional(), options: z.array(z.string()).optional(),
value: z.number().optional(), value: z.string().optional(),
}); });
export const NodeInputSeedSchema = z.object({ export const NodeInputSeedSchema = z.object({
@@ -74,7 +86,7 @@ export const NodeInputSchema = z.union([
NodeInputSeedSchema, NodeInputSeedSchema,
NodeInputVec3Schema, NodeInputVec3Schema,
NodeInputGeometrySchema, NodeInputGeometrySchema,
NodeInputPathSchema NodeInputPathSchema,
]); ]);
export type NodeInput = z.infer<typeof NodeInputSchema>; export type NodeInput = z.infer<typeof NodeInputSchema>;

View File

@@ -1,27 +1,23 @@
import { z } from "zod"; import { z } from "zod";
import { NodeInputSchema } from "./inputs"; import { NodeInputSchema } from "./inputs";
export type NodeId = `${string}/${string}/${string}`; export const NodeTypeSchema = z
.string()
.regex(/^[^/]+\/[^/]+\/[^/]+$/, "Invalid NodeId format")
.transform((value) => value as `${string}/${string}/${string}`);
export const NodeSchema = z.object({ export type NodeType = z.infer<typeof NodeTypeSchema>;
id: z.number(),
type: z.string(),
props: z.record(z.union([z.number(), z.array(z.number())])).optional(),
meta: z.object({
title: z.string().optional(),
lastModified: z.string().optional(),
}).optional(),
position: z.tuple([z.number(), z.number()])
});
export type Node = { export type Node = {
/**
* .tmp only exists at runtime
*/
tmp?: { tmp?: {
depth?: number; depth?: number;
mesh?: any; mesh?: any;
random?: number; parents?: Node[];
parents?: Node[], children?: Node[];
children?: Node[], inputNodes?: Record<string, Node>;
inputNodes?: Record<string, Node>
type?: NodeDefinition; type?: NodeDefinition;
downX?: number; downX?: number;
downY?: number; downY?: number;
@@ -30,17 +26,34 @@ export type Node = {
ref?: HTMLElement; ref?: HTMLElement;
visible?: boolean; visible?: boolean;
isMoving?: boolean; isMoving?: boolean;
} };
} & z.infer<typeof NodeSchema>; } & z.infer<typeof NodeSchema>;
export const NodeDefinitionSchema = z.object({ export const NodeDefinitionSchema = z.object({
id: z.string(), id: NodeTypeSchema,
inputs: z.record(NodeInputSchema).optional(), inputs: z.record(z.string(), NodeInputSchema).optional(),
outputs: z.array(z.string()).optional(), outputs: z.array(z.string()).optional(),
meta: z.object({ meta: z
.object({
description: z.string().optional(), description: z.string().optional(),
title: z.string().optional(), title: z.string().optional(),
}).optional(), })
.optional(),
});
export const NodeSchema = z.object({
id: z.number(),
type: NodeTypeSchema,
props: z
.record(z.string(), z.union([z.number(), z.array(z.number())]))
.optional(),
meta: z
.object({
title: z.string().optional(),
lastModified: z.string().optional(),
})
.optional(),
position: z.tuple([z.number(), z.number()]),
}); });
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & { export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & {
@@ -56,12 +69,14 @@ export type Socket = {
export type Edge = [Node, number, Node, string]; export type Edge = [Node, number, Node, string];
export const GraphSchema = z.object({ export const GraphSchema = z.object({
id: z.number().optional(), id: z.number(),
meta: z.object({ meta: z
.object({
title: z.string().optional(), title: z.string().optional(),
lastModified: z.string().optional(), lastModified: z.string().optional(),
}).optional(), })
settings: z.record(z.any()).optional(), .optional(),
settings: z.record(z.string(), z.any()).optional(),
nodes: z.array(NodeSchema), nodes: z.array(NodeSchema),
edges: z.array(z.tuple([z.number(), z.number(), z.number(), z.string()])), edges: z.array(z.tuple([z.number(), z.number(), z.number(), z.string()])),
}); });

View File

@@ -1,5 +1,5 @@
{ {
"name": "@nodes/ui", "name": "@nodarium/ui",
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@@ -30,36 +30,36 @@
"svelte": "^4.0.0" "svelte": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^8.4.1", "@storybook/addon-essentials": "^8.6.14",
"@storybook/addon-svelte-csf": "5.0.0-next.10", "@storybook/addon-svelte-csf": "5.0.10",
"@storybook/addon-themes": "^8.4.1", "@storybook/addon-themes": "^10.0.8",
"@storybook/svelte": "^8.4.1", "@storybook/svelte": "^10.0.8",
"@storybook/sveltekit": "^8.4.1", "@storybook/sveltekit": "^10.0.8",
"@sveltejs/adapter-static": "^3.0.6", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.7.4", "@sveltejs/kit": "^2.49.0",
"@sveltejs/package": "^2.3.7", "@sveltejs/package": "^2.5.6",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@typescript-eslint/eslint-plugin": "^8.12.2", "@typescript-eslint/eslint-plugin": "^8.47.0",
"@typescript-eslint/parser": "^8.12.2", "@typescript-eslint/parser": "^8.47.0",
"eslint": "^9.14.0", "eslint": "^9.39.1",
"eslint-plugin-storybook": "^0.10.2", "eslint-plugin-storybook": "^10.0.8",
"eslint-plugin-svelte": "^2.46.0", "eslint-plugin-svelte": "^3.13.0",
"publint": "^0.2.12", "publint": "^0.3.15",
"storybook": "^8.4.1", "storybook": "^10.0.8",
"svelte": "^5.1.9", "svelte": "^5.43.14",
"svelte-check": "^4.0.5", "svelte-check": "^4.3.4",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.6.3", "typescript": "^5.9.3",
"vite": "^5.4.10", "vite": "^7.2.4",
"vitest": "^2.1.4" "vitest": "^4.0.13",
"@nodarium/types": "link:../types"
}, },
"svelte": "./dist/index.js", "svelte": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@nodes/types": "link:../types", "@threlte/core": "^8.3.0",
"@threlte/core": "^7.3.1", "@threlte/extras": "^9.7.0"
"@threlte/extras": "^8.12.0"
} }
} }

View File

@@ -3,12 +3,13 @@
title?: string; title?: string;
transparent?: boolean; transparent?: boolean;
children?: import('svelte').Snippet; children?: import('svelte').Snippet;
open?: boolean;
} }
let { title = "Details", transparent = false, children }: Props = $props(); let { title = "Details", transparent = false, children, open = $bindable(false) }: Props = $props();
</script> </script>
<details class:transparent> <details class:transparent bind:open>
<summary>{title}</summary> <summary>{title}</summary>
<div class="content"> <div class="content">
{@render children?.()} {@render children?.()}
@@ -33,7 +34,4 @@
outline: none; outline: none;
} }
.content {
/* padding-left: 12px; */
}
</style> </style>

View File

@@ -4,7 +4,7 @@
import Integer from './elements/Integer.svelte'; import Integer from './elements/Integer.svelte';
import Select from './elements/Select.svelte'; import Select from './elements/Select.svelte';
import type { NodeInput } from '@nodes/types'; import type { NodeInput } from '@nodarium/types';
import Vec3 from './elements/Vec3.svelte'; import Vec3 from './elements/Vec3.svelte';
interface Props { interface Props {
@@ -27,4 +27,3 @@
{:else if input.type === 'vec3'} {:else if input.type === 'vec3'}
<Vec3 {id} bind:value /> <Vec3 {id} bind:value />
{/if} {/if}

View File

@@ -3,7 +3,7 @@
ctrl?: boolean; ctrl?: boolean;
shift?: boolean; shift?: boolean;
alt?: boolean; alt?: boolean;
key: string; key: string | string[];
} }
let { let {

View File

@@ -1,5 +1,5 @@
{ {
"name": "@nodes/utils", "name": "@nodarium/utils",
"version": "0.0.1", "version": "0.0.1",
"description": "", "description": "",
"main": "src/index.ts", "main": "src/index.ts",
@@ -10,10 +10,10 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@nodes/types": "link:../types" "@nodarium/types": "link:../types"
}, },
"devDependencies": { "devDependencies": {
"vite": "^5.4.10", "vite": "^7.2.4",
"vitest": "^2.1.4" "vitest": "^4.0.13"
} }
} }

View File

@@ -1,47 +1,84 @@
// https://github.com/6502/sha256/blob/main/sha256.js // https://github.com/6502/sha256/blob/main/sha256.js
function sha256(data?: string | Uint8Array) { function sha256(data?: string | Int32Array) {
let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a, let h0 = 0x6a09e667,
h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19, h1 = 0xbb67ae85,
tsz = 0, bp = 0; h2 = 0x3c6ef372,
const k = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, h3 = 0xa54ff53a,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, h4 = 0x510e527f,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, h5 = 0x9b05688c,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, h6 = 0x1f83d9ab,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, h7 = 0x5be0cd19,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, tsz = 0,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, bp = 0;
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2], const k = [
rrot = (x, n) => (x >>> n) | (x << (32 - n)), 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
],
rrot = (x: number, n: number) => (x >>> n) | (x << (32 - n)),
w = new Uint32Array(64), w = new Uint32Array(64),
buf = new Uint8Array(64), buf = new Uint8Array(64),
process = () => { process = () => {
for (let j = 0, r = 0; j < 16; j++, r += 4) { for (let j = 0, r = 0; j < 16; j++, r += 4) {
w[j] = (buf[r] << 24) | (buf[r + 1] << 16) | (buf[r + 2] << 8) | buf[r + 3]; w[j] =
(buf[r] << 24) | (buf[r + 1] << 16) | (buf[r + 2] << 8) | buf[r + 3];
} }
for (let j = 16; j < 64; j++) { for (let j = 16; j < 64; j++) {
let s0 = rrot(w[j - 15], 7) ^ rrot(w[j - 15], 18) ^ (w[j - 15] >>> 3); let s0 = rrot(w[j - 15], 7) ^ rrot(w[j - 15], 18) ^ (w[j - 15] >>> 3);
let s1 = rrot(w[j - 2], 17) ^ rrot(w[j - 2], 19) ^ (w[j - 2] >>> 10); let s1 = rrot(w[j - 2], 17) ^ rrot(w[j - 2], 19) ^ (w[j - 2] >>> 10);
w[j] = (w[j - 16] + s0 + w[j - 7] + s1) | 0; w[j] = (w[j - 16] + s0 + w[j - 7] + s1) | 0;
} }
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7; let a = h0,
b = h1,
c = h2,
d = h3,
e = h4,
f = h5,
g = h6,
h = h7;
for (let j = 0; j < 64; j++) { for (let j = 0; j < 64; j++) {
let S1 = rrot(e, 6) ^ rrot(e, 11) ^ rrot(e, 25), let S1 = rrot(e, 6) ^ rrot(e, 11) ^ rrot(e, 25),
ch = (e & f) ^ ((~e) & g), ch = (e & f) ^ (~e & g),
t1 = (h + S1 + ch + k[j] + w[j]) | 0, t1 = (h + S1 + ch + k[j] + w[j]) | 0,
S0 = rrot(a, 2) ^ rrot(a, 13) ^ rrot(a, 22), S0 = rrot(a, 2) ^ rrot(a, 13) ^ rrot(a, 22),
maj = (a & b) ^ (a & c) ^ (b & c), maj = (a & b) ^ (a & c) ^ (b & c),
t2 = (S0 + maj) | 0; t2 = (S0 + maj) | 0;
h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0; h = g;
g = f;
f = e;
e = (d + t1) | 0;
d = c;
c = b;
b = a;
a = (t1 + t2) | 0;
} }
h0 = (h0 + a) | 0; h1 = (h1 + b) | 0; h2 = (h2 + c) | 0; h3 = (h3 + d) | 0; h0 = (h0 + a) | 0;
h4 = (h4 + e) | 0; h5 = (h5 + f) | 0; h6 = (h6 + g) | 0; h7 = (h7 + h) | 0; h1 = (h1 + b) | 0;
h2 = (h2 + c) | 0;
h3 = (h3 + d) | 0;
h4 = (h4 + e) | 0;
h5 = (h5 + f) | 0;
h6 = (h6 + g) | 0;
h7 = (h7 + h) | 0;
bp = 0; bp = 0;
}, },
add = data => { add = (input: string | Int32Array) => {
if (typeof data === "string") { const data =
data = typeof TextEncoder === "undefined" ? Buffer.from(data) : (new TextEncoder).encode(data); typeof input === "string"
} ? typeof TextEncoder === "undefined"
? //@ts-ignore
Buffer.from(input)
: new TextEncoder().encode(input)
: input;
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
buf[bp++] = data[i]; buf[bp++] = data[i];
if (bp === 64) process(); if (bp === 64) process();
@@ -49,7 +86,8 @@ function sha256(data?: string | Uint8Array) {
tsz += data.length; tsz += data.length;
}, },
digest = () => { digest = () => {
buf[bp++] = 0x80; if (bp == 64) process(); buf[bp++] = 0x80;
if (bp == 64) process();
if (bp + 8 > 64) { if (bp + 8 > 64) {
while (bp < 64) buf[bp++] = 0x00; while (bp < 64) buf[bp++] = 0x00;
process(); process();
@@ -57,24 +95,48 @@ function sha256(data?: string | Uint8Array) {
while (bp < 58) buf[bp++] = 0x00; while (bp < 58) buf[bp++] = 0x00;
// Max number of bytes is 35,184,372,088,831 // Max number of bytes is 35,184,372,088,831
let L = tsz * 8; let L = tsz * 8;
buf[bp++] = (L / 1099511627776.) & 255; buf[bp++] = (L / 1099511627776) & 255;
buf[bp++] = (L / 4294967296.) & 255; buf[bp++] = (L / 4294967296) & 255;
buf[bp++] = L >>> 24; buf[bp++] = L >>> 24;
buf[bp++] = (L >>> 16) & 255; buf[bp++] = (L >>> 16) & 255;
buf[bp++] = (L >>> 8) & 255; buf[bp++] = (L >>> 8) & 255;
buf[bp++] = L & 255; buf[bp++] = L & 255;
process(); process();
let reply = new Uint8Array(32); let reply = new Uint8Array(32);
reply[0] = h0 >>> 24; reply[1] = (h0 >>> 16) & 255; reply[2] = (h0 >>> 8) & 255; reply[3] = h0 & 255; reply[0] = h0 >>> 24;
reply[4] = h1 >>> 24; reply[5] = (h1 >>> 16) & 255; reply[6] = (h1 >>> 8) & 255; reply[7] = h1 & 255; reply[1] = (h0 >>> 16) & 255;
reply[8] = h2 >>> 24; reply[9] = (h2 >>> 16) & 255; reply[10] = (h2 >>> 8) & 255; reply[11] = h2 & 255; reply[2] = (h0 >>> 8) & 255;
reply[12] = h3 >>> 24; reply[13] = (h3 >>> 16) & 255; reply[14] = (h3 >>> 8) & 255; reply[15] = h3 & 255; reply[3] = h0 & 255;
reply[16] = h4 >>> 24; reply[17] = (h4 >>> 16) & 255; reply[18] = (h4 >>> 8) & 255; reply[19] = h4 & 255; reply[4] = h1 >>> 24;
reply[20] = h5 >>> 24; reply[21] = (h5 >>> 16) & 255; reply[22] = (h5 >>> 8) & 255; reply[23] = h5 & 255; reply[5] = (h1 >>> 16) & 255;
reply[24] = h6 >>> 24; reply[25] = (h6 >>> 16) & 255; reply[26] = (h6 >>> 8) & 255; reply[27] = h6 & 255; reply[6] = (h1 >>> 8) & 255;
reply[28] = h7 >>> 24; reply[29] = (h7 >>> 16) & 255; reply[30] = (h7 >>> 8) & 255; reply[31] = h7 & 255; reply[7] = h1 & 255;
reply[8] = h2 >>> 24;
reply[9] = (h2 >>> 16) & 255;
reply[10] = (h2 >>> 8) & 255;
reply[11] = h2 & 255;
reply[12] = h3 >>> 24;
reply[13] = (h3 >>> 16) & 255;
reply[14] = (h3 >>> 8) & 255;
reply[15] = h3 & 255;
reply[16] = h4 >>> 24;
reply[17] = (h4 >>> 16) & 255;
reply[18] = (h4 >>> 8) & 255;
reply[19] = h4 & 255;
reply[20] = h5 >>> 24;
reply[21] = (h5 >>> 16) & 255;
reply[22] = (h5 >>> 8) & 255;
reply[23] = h5 & 255;
reply[24] = h6 >>> 24;
reply[25] = (h6 >>> 16) & 255;
reply[26] = (h6 >>> 8) & 255;
reply[27] = h6 & 255;
reply[28] = h7 >>> 24;
reply[29] = (h7 >>> 16) & 255;
reply[30] = (h7 >>> 8) & 255;
reply[31] = h7 & 255;
let res = ""; let res = "";
reply.forEach(x => res += ("0" + x.toString(16)).slice(-2)); reply.forEach((x) => (res += ("0" + x.toString(16)).slice(-2)));
return res; return res;
}; };
@@ -83,8 +145,8 @@ function sha256(data?: string | Uint8Array) {
return { add, digest }; return { add, digest };
} }
export function fastHashArrayBuffer(buffer: ArrayBuffer): string { export function fastHashArrayBuffer(buffer: string | Int32Array): string {
return sha256(new Uint8Array(buffer)).digest(); return sha256(buffer).digest();
} }
// Shamelessly copied from // Shamelessly copied from
@@ -101,22 +163,19 @@ export function fastHashString(input: string) {
return hash; return hash;
} }
export function fastHash(input: (string | Int32Array | number)[]) { export function fastHash(input: (string | Int32Array | number)[]) {
const s = sha256(); const s = sha256();
for (let i = 0; i < input.length; i++) { for (let i = 0; i < input.length; i++) {
const v = input[i] const v = input[i];
if (typeof v === "string") { if (typeof v === "string") {
s.add(v); s.add(v);
} else if (v instanceof Int32Array) { } else if (v instanceof Int32Array) {
s.add(new Uint8Array(v.buffer)); s.add(v);
} else { } else {
s.add(v.toString()); s.add(v.toString());
} }
} }
return s.digest() return s.digest();
} }

View File

@@ -1,7 +1,8 @@
type SparseArray<T = number> = (T | T[] | SparseArray<T>)[]; type SparseArray<T = number> = (T | T[] | SparseArray<T>)[];
export function concatEncodedArrays(input: (number | number[] | Int32Array)[]): Int32Array { export function concatEncodedArrays(
input: (number | number[] | Int32Array)[],
): Int32Array {
let totalLength = 4; let totalLength = 4;
for (let i = 0; i < input.length; i++) { for (let i = 0; i < input.length; i++) {
const item = input[i]; const item = input[i];
@@ -36,7 +37,7 @@ export function concatEncodedArrays(input: (number | number[] | Int32Array)[]):
result[totalLength - 2] = 1; result[totalLength - 2] = 1;
result[totalLength - 1] = 1; result[totalLength - 1] = 1;
return result return result;
} }
// Encodes a nested array into a flat array with bracket and distance notation // Encodes a nested array into a flat array with bracket and distance notation
@@ -68,12 +69,11 @@ export function encodeNestedArray(array: SparseArray): number[] {
} }
return [...encoded, 1, 1]; return [...encoded, 1, 1];
}; }
function decode_recursive(dense: number[] | Int32Array, index = 0) { function decode_recursive(dense: number[] | Int32Array, index = 0) {
if (dense instanceof Int32Array) { if (dense instanceof Int32Array) {
dense = Array.from(dense) dense = Array.from(dense);
} }
const decoded: (number | number[])[] = []; const decoded: (number | number[])[] = [];
@@ -82,12 +82,17 @@ function decode_recursive(dense: number[] | Int32Array, index = 0) {
index += 2; // Skip the initial bracket notation index += 2; // Skip the initial bracket notation
while (index < dense.length) { while (index < dense.length) {
if (index === nextBracketIndex) { if (index === nextBracketIndex) {
if (dense[index] === 0) { // Opening bracket detected if (dense[index] === 0) {
const [p, nextIndex, _nextBracketIndex] = decode_recursive(dense, index); // Opening bracket detected
decoded.push(p); const [p, nextIndex, _nextBracketIndex] = decode_recursive(
dense,
index,
);
decoded.push(...p);
index = nextIndex + 1; index = nextIndex + 1;
nextBracketIndex = _nextBracketIndex; nextBracketIndex = _nextBracketIndex;
} else { // Closing bracket detected } else {
// Closing bracket detected
nextBracketIndex = dense[index + 1] + index + 1; nextBracketIndex = dense[index + 1] + index + 1;
return [decoded, index, nextBracketIndex] as const; return [decoded, index, nextBracketIndex] as const;
} }
@@ -103,7 +108,6 @@ export function decodeNestedArray(dense: number[] | Int32Array) {
return decode_recursive(dense, 0)[0]; return decode_recursive(dense, 0)[0];
} }
export function splitNestedArray(input: Int32Array) { export function splitNestedArray(input: Int32Array) {
let index = 0; let index = 0;
const length = input.length; const length = input.length;

View File

@@ -1,10 +1,14 @@
import { NodeDefinition } from "@nodes/types"; //@ts-nocheck
import { NodeDefinition } from "@nodarium/types";
const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); const cachedTextDecoder = new TextDecoder("utf-8", {
ignoreBOM: true,
fatal: true,
});
const cachedTextEncoder = new TextEncoder(); const cachedTextEncoder = new TextEncoder();
const encodeString =
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' typeof cachedTextEncoder.encodeInto === "function"
? function (arg, view) { ? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view); return cachedTextEncoder.encodeInto(arg, view);
} }
@@ -13,9 +17,9 @@ const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
view.set(buf); view.set(buf);
return { return {
read: arg.length, read: arg.length,
written: buf.length written: buf.length,
};
}; };
});
function createWrapper() { function createWrapper() {
let wasm: any; let wasm: any;
@@ -53,7 +57,9 @@ function createWrapper() {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
} }
function getObject(idx: number) { return heap[idx]; } function getObject(idx: number) {
return heap[idx];
}
function addHeapObject(obj: any) { function addHeapObject(obj: any) {
if (heap_next === heap.length) heap.push(heap.length + 1); if (heap_next === heap.length) heap.push(heap.length + 1);
@@ -63,9 +69,11 @@ function createWrapper() {
return idx; return idx;
} }
let WASM_VECTOR_LEN = 0; let WASM_VECTOR_LEN = 0;
function passArray32ToWasm0(arg: ArrayLike<number>, malloc: (arg0: number, arg1: number) => number) { function passArray32ToWasm0(
arg: ArrayLike<number>,
malloc: (arg0: number, arg1: number) => number,
) {
const ptr = malloc(arg.length * 4, 4) >>> 0; const ptr = malloc(arg.length * 4, 4) >>> 0;
getUint32Memory0().set(arg, ptr / 4); getUint32Memory0().set(arg, ptr / 4);
WASM_VECTOR_LEN = arg.length; WASM_VECTOR_LEN = arg.length;
@@ -89,21 +97,10 @@ function createWrapper() {
return ret; return ret;
} }
function getArrayJsValueFromWasm0(ptr: number, len: number) {
ptr = ptr >>> 0;
const mem = getUint32Memory0();
const slice = mem.subarray(ptr / 4, ptr / 4 + len);
const result = [];
for (let i = 0; i < slice.length; i++) {
result.push(takeObject(slice[i]));
}
return result;
}
function __wbindgen_string_new(arg0: number, arg1: number) { function __wbindgen_string_new(arg0: number, arg1: number) {
const ret = getStringFromWasm0(arg0, arg1); const ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret); return addHeapObject(ret);
}; }
// Additional methods and their internal helpers can also be refactored in a similar manner. // Additional methods and their internal helpers can also be refactored in a similar manner.
function get_definition() { function get_definition() {
@@ -124,7 +121,6 @@ function createWrapper() {
} }
} }
function execute(args: Int32Array) { function execute(args: Int32Array) {
try { try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
@@ -141,12 +137,19 @@ function createWrapper() {
} }
} }
function passStringToWasm0(arg: string, malloc: (arg0: any, arg1: number) => number, realloc: ((arg0: number, arg1: any, arg2: number, arg3: number) => number) | undefined) { function passStringToWasm0(
arg: string,
malloc: (arg0: any, arg1: number) => number,
realloc:
| ((arg0: number, arg1: any, arg2: number, arg3: number) => number)
| undefined,
) {
if (realloc === undefined) { if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg); const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0; const ptr = malloc(buf.length, 1) >>> 0;
getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); getUint8Memory0()
.subarray(ptr, ptr + buf.length)
.set(buf);
WASM_VECTOR_LEN = buf.length; WASM_VECTOR_LEN = buf.length;
return ptr; return ptr;
} }
@@ -160,7 +163,7 @@ function createWrapper() {
for (; offset < len; offset++) { for (; offset < len; offset++) {
const code = arg.charCodeAt(offset); const code = arg.charCodeAt(offset);
if (code > 0x7F) break; if (code > 0x7f) break;
mem[ptr + offset] = code; mem[ptr + offset] = code;
} }
@@ -168,7 +171,7 @@ function createWrapper() {
if (offset !== 0) { if (offset !== 0) {
arg = arg.slice(offset); arg = arg.slice(offset);
} }
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; ptr = realloc(ptr, len, (len = offset + arg.length * 3), 1) >>> 0;
const view = getUint8Memory0().subarray(ptr + offset, ptr + len); const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view); const ret = encodeString(arg, view);
@@ -183,15 +186,19 @@ function createWrapper() {
function __wbg_new_abda76e883ba8a5f() { function __wbg_new_abda76e883ba8a5f() {
const ret = new Error(); const ret = new Error();
return addHeapObject(ret); return addHeapObject(ret);
}; }
function __wbg_stack_658279fe44541cf6(arg0, arg1) { function __wbg_stack_658279fe44541cf6(arg0, arg1) {
const ret = getObject(arg1).stack; const ret = getObject(arg1).stack;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const ptr1 = passStringToWasm0(
ret,
wasm.__wbindgen_malloc,
wasm.__wbindgen_realloc,
);
const len1 = WASM_VECTOR_LEN; const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1; getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}; }
function __wbg_error_f851667af71bcfc6(arg0, arg1) { function __wbg_error_f851667af71bcfc6(arg0, arg1) {
let deferred0_0; let deferred0_0;
@@ -203,27 +210,25 @@ function createWrapper() {
} finally { } finally {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
} }
}; }
function __wbindgen_object_drop_ref(arg0) { function __wbindgen_object_drop_ref(arg0) {
takeObject(arg0); takeObject(arg0);
}; }
function __wbg_log_5bb5f88f245d7762(arg0) { function __wbg_log_5bb5f88f245d7762(arg0) {
console.log(getObject(arg0)); console.log(getObject(arg0));
}; }
function __wbindgen_throw(arg0, arg1) { function __wbindgen_throw(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1)); throw new Error(getStringFromWasm0(arg0, arg1));
}; }
return { return {
setInstance(instance: WebAssembly.Instance) { setInstance(instance: WebAssembly.Instance) {
wasm = instance.exports; wasm = instance.exports;
}, },
exports: { exports: {
// Expose other methods that interact with the wasm instance // Expose other methods that interact with the wasm instance
execute, execute,
@@ -240,11 +245,12 @@ function createWrapper() {
}; };
} }
export function createWasmWrapper(wasmBuffer: ArrayBuffer | Uint8Array) {
export function createWasmWrapper(wasmBuffer: ArrayBuffer) {
const wrapper = createWrapper(); const wrapper = createWrapper();
const module = new WebAssembly.Module(wasmBuffer); const module = new WebAssembly.Module(wasmBuffer);
const instance = new WebAssembly.Instance(module, { ["./index_bg.js"]: wrapper }); const instance = new WebAssembly.Instance(module, {
["./index_bg.js"]: wrapper,
});
wrapper.setInstance(instance); wrapper.setInstance(instance);
return wrapper.exports; return wrapper.exports;
} }

5854
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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