41 Commits

Author SHA1 Message Date
Max Richter
548e445eb7 fix: correctly show hide geometries in geometrypool
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m4s
2025-12-03 22:59:06 +01:00
db77a4fd94 Merge pull request 'refactor: split ui/runtime/serialized node types' (#10) from refactor/split-node-runtime-types into main
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m3s
Reviewed-on: #10
2025-12-03 19:19:17 +01:00
Max Richter
7ae1fae3b9 refactor: split ui/runtime/serialized node types
Closes #6
2025-12-03 19:18:56 +01:00
1126cf8f9f feat: dont use custom edge geometry
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m55s
2025-12-03 10:33:24 +01:00
Max Richter
ef479d0557 chore: update
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 3m50s
2025-12-02 17:31:58 +01:00
Max Richter
a1c926c3cf fix: better handle randomGeneration 2025-12-02 17:27:34 +01:00
ca8b1e15ac chore: cleanup edge and node code
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m8s
2025-12-02 16:59:43 +01:00
4878d02705 refactor: remove unneeded random var in node 2025-12-02 16:59:29 +01:00
2b4c81f557 fix: make sure new nodes are reactive
Closes #7
2025-12-02 16:59:11 +01:00
d178f812fb refactor: move event handlers to own classes 2025-12-02 16:58:31 +01:00
669a2c7991 docs: remove placeholder content in readme 2025-12-02 15:20:26 +01:00
becd7a1eb3 fix: make sure we do not pass svelte state into comlink
cant clone proxies
2025-12-02 15:20:13 +01:00
d140f42468 feat: better a18n for node parameters
Dunno of a18n would even be possible for the node graph
2025-12-02 15:19:48 +01:00
be835e5cff fix: better stroke width and color for edges 2025-12-02 15:00:41 +01:00
Max Richter
6229becfd8 fix: display add menu at correct position
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m58s
2025-12-01 22:39:43 +01:00
Max Richter
af944cefaa chore: disable cache from runtime executor 2025-12-01 22:39:06 +01:00
Max Richter
a1ea56093c fix: correctly handle node wrapper resizing
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m57s
2025-12-01 19:48:40 +01:00
Max Richter
1850e21810 fix: make clipboard work 2025-12-01 19:30:44 +01:00
Max Richter
7e51cc5ea1 chore: some updates
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m58s
2025-12-01 18:29:47 +01:00
Max Richter
1ea544e765 chore: rename @nodes -> @nodarium for everything
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 3m33s
2025-12-01 17:03:14 +01:00
e5658b8a7e feat: initial auto connect nodes
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m35s
2025-11-26 17:27:32 +01:00
d3a9b3f056 fix: make node wasm loading work
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m32s
2025-11-26 12:10:25 +01:00
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
95 changed files with 5766 additions and 6858 deletions

View File

@@ -2,7 +2,7 @@ name: Deploy to GitHub Pages
on:
push:
branches: 'main'
branches: "main"
jobs:
build_site:
@@ -13,7 +13,7 @@ jobs:
uses: actions/checkout@v4
- name: Install dependencies
run: pnpm install
run: pnpm install --frozen-lockfile
- name: build
run: pnpm run build:deploy
@@ -23,7 +23,7 @@ jobs:
echo "$SSH_PRIVATE_KEY" > /tmp/id_rsa
chmod 600 /tmp/id_rsa
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:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ vars.SSH_HOST }}

View File

@@ -1,18 +1,21 @@
FROM node:22
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" \
&& 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 \
&& curl https://rclone.org/install.sh --output /tmp/rclone-install.sh \
&& bash /tmp/rclone-install.sh
&& sh /tmp/rustup-linux-install.sh -y
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 \
&& npm i -g pnpm

View File

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

View File

@@ -1,5 +1,5 @@
{
"name": "@nodes/app",
"name": "@nodarium/app",
"private": true,
"version": "0.0.0",
"type": "module",
@@ -10,37 +10,37 @@
"preview": "vite preview"
},
"dependencies": {
"@nodes/registry": "link:../packages/registry",
"@nodes/ui": "link:../packages/ui",
"@nodes/utils": "link:../packages/utils",
"@sveltejs/kit": "^2.12.2",
"@threlte/core": "8.0.0-next.23",
"@threlte/extras": "9.0.0-next.33",
"@types/three": "^0.171.0",
"@unocss/reset": "^0.65.2",
"@nodarium/registry": "link:../packages/registry",
"@nodarium/ui": "link:../packages/ui",
"@nodarium/utils": "link:../packages/utils",
"@sveltejs/kit": "^2.49.0",
"@threlte/core": "8.3.0",
"@threlte/extras": "9.7.0",
"@types/three": "^0.181.0",
"@unocss/reset": "^66.5.9",
"comlink": "^4.4.2",
"file-saver": "^2.0.5",
"idb": "^8.0.1",
"jsondiffpatch": "^0.6.0",
"three": "^0.171.0"
"idb": "^8.0.3",
"jsondiffpatch": "^0.7.3",
"three": "^0.181.2"
},
"devDependencies": {
"@iconify-json/tabler": "^1.2.13",
"@nodes/types": "link:../packages/types",
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tsconfig/svelte": "^5.0.4",
"@iconify-json/tabler": "^1.2.23",
"@nodarium/types": "link:../packages/types",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tsconfig/svelte": "^5.0.6",
"@types/file-saver": "^2.0.7",
"@unocss/preset-icons": "^0.65.2",
"svelte": "^5.14.4",
"svelte-check": "^4.1.1",
"@unocss/preset-icons": "^66.5.9",
"svelte": "^5.43.14",
"svelte-check": "^4.3.4",
"tslib": "^2.8.1",
"typescript": "^5.7.2",
"unocss": "^0.65.2",
"vite": "^6.0.4",
"vite-plugin-comlink": "^5.1.0",
"vite-plugin-glsl": "^1.3.1",
"vite-plugin-wasm": "^3.3.0",
"vitest": "^2.1.8"
"typescript": "^5.9.3",
"unocss": "^66.5.9",
"vite": "^7.2.4",
"vite-plugin-comlink": "^5.3.0",
"vite-plugin-glsl": "^1.5.4",
"vite-plugin-wasm": "^3.5.0",
"vitest": "^4.0.13"
}
}

View File

@@ -1,4 +1,6 @@
precision highp float;
// For WebGL1 make sure this extension is enabled in your material:
// #extension GL_OES_standard_derivatives : enable
varying vec2 vUv;
@@ -10,33 +12,45 @@ uniform vec2 zoomLimits;
uniform vec3 backgroundColor;
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) {
x = fract(x * divisions);
x = min(x, 1.0 - x);
// Continuous grid coordinates
float gx = x * divisions;
float gy = y * divisions;
float xdelta = fwidth(x);
x = smoothstep(x - xdelta, x + xdelta, thickness);
// Distance to nearest grid line (0 at the line)
float fx = fract(gx);
fx = min(fx, 1.0 - fx);
float fy = fract(gy);
fy = min(fy, 1.0 - fy);
y = fract(y * divisions);
y = min(y, 1.0 - y);
// Derivatives in screen space use the continuous coords here
float dx = fwidth(gx);
float dy = fwidth(gy);
float ydelta = fwidth(y);
y = smoothstep(y - ydelta, y + ydelta, thickness);
// Keep the original semantics: thickness is the threshold in the [0, 0.5] distance domain
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 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;
float gridY = mod(y + divisions/2.0, divisions) - divisions / 2.0;
vec2 g = vec2(gridX, gridY);
float d = length(g);
// Calculate the distance from the center of the grid
float gridDistance = length(vec2(gridX, gridY));
// 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);
// Screen-space derivative for AA on the circle edge
float w = fwidth(d);
float circle = 1.0 - smoothstep(circleRadius - w, circleRadius + w, d);
return circle;
}
@@ -58,14 +72,12 @@ void main(void) {
float divisions = 0.1 / cz;
float thickness = 0.05 / cz;
float delta = 0.1 / 2.0;
float nz = (cz - minZ) / (maxZ - minZ);
float ux = (vUv.x - 0.5) * width + cx * cz;
float uy = (vUv.y - 0.5) * height - cy * cz;
// extra small grid
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;
@@ -76,7 +88,7 @@ void main(void) {
// small grid
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 s1 = circle_grid(ux, uy, cz * 10.0, 2.0) * 0.5;
@@ -91,9 +103,10 @@ void main(void) {
large = max(large, s2);
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);
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 BackgroundFrag from "./Background.frag";
import { colors } from "../graph/colors.svelte";
import { Color } from "three";
import { appSettings } from "$lib/settings/app-settings.svelte";
type Props = {
@@ -42,10 +41,10 @@
value: [0, 1, 0],
},
backgroundColor: {
value: colors["layer-0"].clone(),
value: colors["layer-0"],
},
lineColor: {
value: colors["outline"].clone(),
value: colors["outline"],
},
zoomLimits: {
value: [2, 50],
@@ -55,9 +54,9 @@
},
}}
uniforms.camPos.value={cameraPosition}
uniforms.backgroundColor.value={appSettings.theme &&
colors["layer-0"].clone()}
uniforms.lineColor.value={appSettings.theme && colors["outline"].clone()}
uniforms.backgroundColor.value={appSettings.value.theme &&
colors["layer-0"]}
uniforms.lineColor.value={appSettings.value.theme && colors["outline"]}
uniforms.zoomLimits.value={[minZoom, maxZoom]}
uniforms.dimensions.value={[width, height]}
/>

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,734 @@
import type {
Edge,
Graph,
NodeInstance,
NodeDefinition,
NodeInput,
NodeRegistry,
NodeId,
Socket,
} from "@nodarium/types";
import { fastHashString } from "@nodarium/utils";
import { SvelteMap } from "svelte/reactivity";
import EventEmitter from "./helpers/EventEmitter";
import { createLogger } from "@nodarium/utils";
import throttle from "$lib/helpers/throttle";
import { HistoryManager } from "./history-manager";
const logger = createLogger("graph-manager");
logger.mute();
const clone =
"structuredClone" in self
? self.structuredClone
: (args: any) => JSON.parse(JSON.stringify(args));
function areSocketsCompatible(
output: string | undefined,
inputs: string | (string | undefined)[] | undefined,
) {
if (Array.isArray(inputs) && output) {
return inputs.includes(output);
}
return inputs === output;
}
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
if (firstEdge[0].id !== secondEdge[0].id) {
return false;
}
if (firstEdge[1] !== secondEdge[1]) {
return false
}
if (firstEdge[2].id !== secondEdge[2].id) {
return false
}
if (firstEdge[3] !== secondEdge[3]) {
return false
}
return true
}
export class GraphManager extends EventEmitter<{
save: Graph;
result: any;
settings: {
types: Record<string, NodeInput>;
values: Record<string, unknown>;
};
}> {
status = $state<"loading" | "idle" | "error">();
loaded = false;
graph: Graph = { id: 0, nodes: [], edges: [] };
id = $state(0);
nodes = new SvelteMap<number, NodeInstance>();
edges = $state<Edge[]>([]);
settingTypes: Record<string, NodeInput> = {};
settings = $state<Record<string, unknown>>();
currentUndoGroup: number | null = null;
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();
execute = throttle(() => {
if (this.loaded === false) return;
this.emit("result", this.serialize());
}, 10);
constructor(public registry: NodeRegistry) {
super();
}
serialize(): Graph {
const nodes = Array.from(this.nodes.values()).map((node) => ({
id: node.id,
position: [...node.position],
type: node.type,
props: node.props,
})) as NodeInstance[];
const edges = this.edges.map((edge) => [
edge[0].id,
edge[1],
edge[2].id,
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;
setSettings(settings: Record<string, unknown>) {
let hash = fastHashString(JSON.stringify(settings));
if (hash === this.lastSettingsHash) return;
this.lastSettingsHash = hash;
this.settings = settings;
this.save();
this.execute();
}
getNodeDefinitions() {
return this.registry.getAllNodes();
}
getLinkedNodes(node: NodeInstance) {
const nodes = new Set<NodeInstance>();
const stack = [node];
while (stack.length) {
const n = stack.pop();
if (!n) continue;
nodes.add(n);
const children = this.getChildren(n);
const parents = this.getParentsOfNode(n);
const newNodes = [...children, ...parents].filter((n) => !nodes.has(n));
stack.push(...newNodes);
}
return [...nodes.values()];
}
getEdgesBetweenNodes(nodes: NodeInstance[]): [number, number, number, string][] {
const edges = [];
for (const node of nodes) {
const children = node.state?.children || [];
for (const child of children) {
if (nodes.includes(child)) {
const edge = this.edges.find(
(e) => e[0].id === node.id && e[2].id === child.id,
);
if (edge) {
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [
number,
number,
number,
string,
]);
}
}
}
}
return edges;
}
private _init(graph: Graph) {
const nodes = new Map(
graph.nodes.map((node) => {
const nodeType = this.registry.getNode(node.type);
const n = node as NodeInstance;
if (nodeType) {
n.state = {
type: nodeType,
};
}
return [node.id, n];
}),
);
const edges = graph.edges.map((edge) => {
const from = nodes.get(edge[0]);
const to = nodes.get(edge[2]);
if (!from || !to) {
throw new Error("Edge references non-existing node");
}
from.state.children = from.state.children || [];
from.state.children.push(to);
to.state.parents = to.state.parents || [];
to.state.parents.push(from);
return [from, edge[1], to, edge[3]] as Edge;
});
this.edges = [...edges];
this.nodes.clear();
for (const [id, node] of nodes) {
this.nodes.set(id, node);
}
this.execute();
}
async load(graph: Graph) {
const a = performance.now();
this.loaded = false;
this.graph = graph;
this.status = "loading";
this.id = graph.id;
logger.info("loading graph", $state.snapshot(graph));
const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)]));
await this.registry.load(nodeIds);
logger.info("loaded node types", this.registry.getAllNodes());
for (const node of this.graph.nodes) {
const nodeType = this.registry.getNode(node.type);
if (!nodeType) {
logger.error(`Node type not found: ${node.type}`);
this.status = "error";
return;
}
// Turn into runtime node
const n = node as NodeInstance;
n.state = {};
n.state.type = nodeType;
}
// load settings
const settingTypes: Record<
string,
// Optional metadata to map settings to specific nodes
NodeInput & { __node_type: string; __node_input: string }
> = {};
const settingValues = graph.settings || {};
const types = this.getNodeDefinitions();
for (const type of types) {
if (type.inputs) {
for (const key in type.inputs) {
let settingId = type.inputs[key].setting;
if (settingId) {
settingTypes[settingId] = {
__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;
}
}
}
}
}
this.settings = settingValues;
this.emit("settings", { types: settingTypes, values: settingValues });
this.history.reset();
this._init(this.graph);
this.save();
this.status = "idle";
this.loaded = true;
logger.log(`Graph loaded in ${performance.now() - a}ms`);
setTimeout(() => this.execute(), 100);
}
getAllNodes() {
return Array.from(this.nodes.values());
}
getNode(id: number) {
return this.nodes.get(id);
}
getNodeType(id: string) {
return this.registry.getNode(id);
}
async loadNodeType(id: NodeId) {
await this.registry.load([id]);
const nodeType = this.registry.getNode(id);
if (!nodeType) return;
const settingTypes = this.settingTypes;
const settingValues = this.settings;
if (nodeType.inputs) {
for (const key in nodeType.inputs) {
let settingId = nodeType.inputs[key].setting;
if (settingId) {
settingTypes[settingId] = nodeType.inputs[key];
if (
settingValues &&
settingValues?.[settingId] === undefined &&
"value" in nodeType.inputs[key]
) {
settingValues[settingId] = nodeType.inputs[key].value;
}
}
}
}
this.settings = settingValues;
this.settingTypes = settingTypes;
this.emit("settings", { types: settingTypes, values: settingValues });
}
getChildren(node: NodeInstance) {
const children = [];
const stack = node.state?.children?.slice(0);
while (stack?.length) {
const child = stack.pop();
if (!child) continue;
children.push(child);
stack.push(...(child.state?.children || []));
}
return children;
}
getNodesBetween(from: NodeInstance, to: NodeInstance): NodeInstance[] | undefined {
// < - - - - from
const toParents = this.getParentsOfNode(to);
// < - - - - from - - - - to
const fromParents = this.getParentsOfNode(from);
if (toParents.includes(from)) {
const fromChildren = this.getChildren(from);
return toParents.filter((n) => fromChildren.includes(n));
} else if (fromParents.includes(to)) {
const toChildren = this.getChildren(to);
return fromParents.filter((n) => toChildren.includes(n));
} else {
// these two nodes are not connected
return;
}
}
removeNode(node: NodeInstance, { restoreEdges = false } = {}) {
const edgesToNode = this.edges.filter((edge) => edge[2].id === node.id);
const edgesFromNode = this.edges.filter((edge) => edge[0].id === node.id);
for (const edge of [...edgesToNode, ...edgesFromNode]) {
this.removeEdge(edge, { applyDeletion: false });
}
if (restoreEdges) {
const outputSockets = edgesToNode.map((e) => [e[0], e[1]] as const);
const inputSockets = edgesFromNode.map((e) => [e[2], e[3]] as const);
for (const [to, toSocket] of inputSockets) {
for (const [from, fromSocket] of outputSockets) {
const outputType = from.state?.type?.outputs?.[fromSocket];
const inputType = to?.state?.type?.inputs?.[toSocket]?.type;
if (outputType === inputType) {
this.createEdge(from, fromSocket, to, toSocket, {
applyUpdate: false,
});
continue;
}
}
}
}
this.nodes.delete(node.id);
this.execute();
this.save();
}
smartConnect(from: NodeInstance, to: NodeInstance): Edge | undefined {
const inputs = Object.entries(to.state?.type?.inputs ?? {});
const outputs = from.state?.type?.outputs ?? [];
for (let i = 0; i < inputs.length; i++) {
const [inputName, input] = inputs[0];
for (let o = 0; o < outputs.length; o++) {
const output = outputs[0];
if (input.type === output) {
return this.createEdge(from, o, to, inputName);
}
}
}
}
createNodeId() {
return Math.max(0, ...this.nodes.keys()) + 1;
}
createGraph(nodes: NodeInstance[], edges: [number, number, number, string][]) {
// map old ids to new ids
const idMap = new Map<number, number>();
let startId = this.createNodeId()
nodes = nodes.map((node) => {
const id = startId++;
idMap.set(node.id, id);
const type = this.registry.getNode(node.type);
if (!type) {
throw new Error(`Node type not found: ${node.type}`);
}
return { ...node, id, tmp: { type } };
});
const _edges = edges.map((edge) => {
const from = nodes.find((n) => n.id === idMap.get(edge[0]));
const to = nodes.find((n) => n.id === idMap.get(edge[2]));
if (!from || !to) {
throw new Error("Edge references non-existing node");
}
to.state.parents = to.state.parents || [];
to.state.parents.push(from);
from.state.children = from.state.children || [];
from.state.children.push(to);
return [from, edge[1], to, edge[3]] as Edge;
});
for (const node of nodes) {
this.nodes.set(node.id, node);
}
this.edges.push(..._edges);
this.save();
return nodes;
}
createNode({
type,
position,
props = {},
}: {
type: NodeInstance["type"];
position: NodeInstance["position"];
props: NodeInstance["props"];
}) {
const nodeType = this.registry.getNode(type);
if (!nodeType) {
logger.error(`Node type not found: ${type}`);
return;
}
const node: NodeInstance = $state({
id: this.createNodeId(),
type,
position,
state: { type: nodeType },
props,
});
this.nodes.set(node.id, node);
this.save();
return node
}
createEdge(
from: NodeInstance,
fromSocket: number,
to: NodeInstance,
toSocket: string,
{ applyUpdate = true } = {},
): Edge | undefined {
const existingEdges = this.getEdgesToNode(to);
// check if this exact edge already exists
const existingEdge = existingEdges.find(
(e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket,
);
if (existingEdge) {
logger.error("Edge already exists", existingEdge);
return;
}
// check if socket types match
const fromSocketType = from.state?.type?.outputs?.[fromSocket];
const toSocketType = [to.state?.type?.inputs?.[toSocket]?.type];
if (to.state?.type?.inputs?.[toSocket]?.accepts) {
toSocketType.push(...(to?.state?.type?.inputs?.[toSocket]?.accepts || []));
}
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
logger.error(
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`,
);
return;
}
const edgeToBeReplaced = this.edges.find(
(e) => e[2].id === to.id && e[3] === toSocket,
);
if (edgeToBeReplaced) {
this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
}
const edge = [from, fromSocket, to, toSocket] as Edge;
this.edges.push(edge);
from.state.children = from.state.children || [];
from.state.children.push(to);
to.state.parents = to.state.parents || [];
to.state.parents.push(from);
if (applyUpdate) {
this.save();
}
this.execute();
return edge;
}
undo() {
const nextState = this.history.undo();
if (nextState) {
this._init(nextState);
this.emit("save", this.serialize());
}
}
redo() {
const nextState = this.history.redo();
if (nextState) {
this._init(nextState);
this.emit("save", this.serialize());
}
}
startUndoGroup() {
this.currentUndoGroup = 1;
}
saveUndoGroup() {
this.currentUndoGroup = null;
this.save();
}
save() {
if (this.currentUndoGroup) return;
const state = this.serialize();
this.history.save(state);
this.emit("save", state);
logger.log("saving graphs", state);
}
getParentsOfNode(node: NodeInstance) {
const parents = [];
const stack = node.state?.parents?.slice(0);
while (stack?.length) {
if (parents.length > 1000000) {
logger.warn("Infinite loop detected");
break;
}
const parent = stack.pop();
if (!parent) continue;
parents.push(parent);
stack.push(...(parent.state?.parents || []));
}
return parents.reverse();
}
getPossibleNodes(socket: Socket): NodeDefinition[] {
const allDefinitions = this.getNodeDefinitions();
const nodeType = socket.node.state?.type;
if (!nodeType) {
return [];
}
const definitions = typeof socket.index === "string"
? allDefinitions.filter(s => {
return s.outputs?.find(_s => Object
.values(nodeType?.inputs || {})
.map(s => s.type)
.includes(_s as NodeInput["type"])
)
})
: allDefinitions.filter(s => Object
.values(s.inputs ?? {})
.find(s => {
if (s.hidden) return false;
if (nodeType.outputs?.includes(s.type)) {
return true
}
return s.accepts?.find(a => nodeType.outputs?.includes(a))
}))
return definitions
}
getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
const nodeType = node?.state?.type;
if (!nodeType) return [];
const sockets: [NodeInstance, string | number][] = [];
// if index is a string, we are an input looking for outputs
if (typeof index === "string") {
// filter out self and child nodes
const children = new Set(this.getChildren(node).map((n) => n.id));
const nodes = this.getAllNodes().filter(
(n) => n.id !== node.id && !children.has(n.id),
);
const ownType = nodeType?.inputs?.[index].type;
for (const node of nodes) {
const nodeType = node?.state?.type;
const inputs = nodeType?.outputs;
if (!inputs) continue;
for (let index = 0; index < inputs.length; index++) {
if (inputs[index] === ownType) {
sockets.push([node, index]);
}
}
}
} else if (typeof index === "number") {
// if index is a number, we are an output looking for inputs
// filter out self and parent nodes
const parents = new Set(this.getParentsOfNode(node).map((n) => n.id));
const nodes = this.getAllNodes().filter(
(n) => n.id !== node.id && !parents.has(n.id),
);
// 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 ownType = nodeType.outputs?.[index];
for (const node of nodes) {
const inputs = node?.state?.type?.inputs;
if (!inputs) continue;
for (const key in inputs) {
const otherType = [inputs[key].type];
otherType.push(...(inputs[key].accepts || []));
if (
areSocketsCompatible(ownType, otherType) &&
edges.get(node.id) !== key
) {
sockets.push([node, key]);
}
}
}
}
return sockets;
}
removeEdge(
edge: Edge,
{ applyDeletion = true }: { applyDeletion?: boolean } = {},
) {
const id0 = edge[0].id;
const sid0 = edge[1];
const id2 = edge[2].id;
const sid2 = edge[3];
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[0].state.children) {
edge[0].state.children = edge[0].state.children.filter(
(n: NodeInstance) => n.id !== id2,
);
}
if (edge[2].state.parents) {
edge[2].state.parents = edge[2].state.parents.filter(
(n: NodeInstance) => n.id !== id0,
);
}
this.edges = this.edges.filter((e) => !areEdgesEqual(e, edge));
if (applyDeletion) {
this.execute();
this.save();
}
}
getEdgesToNode(node: NodeInstance) {
return this.edges
.filter((edge) => edge[2].id === node.id)
.map((edge) => {
const from = this.getNode(edge[0].id);
const to = this.getNode(edge[2].id);
if (!from || !to) return;
return [from, edge[1], to, edge[3]] as const;
})
.filter(Boolean) as unknown as [NodeInstance, number, NodeInstance, string][];
}
getEdgesFromNode(node: NodeInstance) {
return this.edges
.filter((edge) => edge[0].id === node.id)
.map((edge) => {
const from = this.getNode(edge[0].id);
const to = this.getNode(edge[2].id);
if (!from || !to) return;
return [from, edge[1], to, edge[3]] as const;
})
.filter(Boolean) as unknown as [NodeInstance, number, NodeInstance, string][];
}
}

View File

@@ -1,611 +0,0 @@
import type { Edge, Graph, Node, NodeInput, NodeRegistry, Socket, } from "@nodes/types";
import { fastHashString } from "@nodes/utils";
import { writable, type Writable } from "svelte/store";
import EventEmitter from "./helpers/EventEmitter.js";
import { createLogger } from "./helpers/index.js";
import throttle from "./helpers/throttle.js";
import { HistoryManager } from "./history-manager.js";
const logger = createLogger("graph-manager");
logger.mute();
const clone = "structuredClone" in self ? self.structuredClone : (args: any) => JSON.parse(JSON.stringify(args));
function areSocketsCompatible(output: string | undefined, inputs: string | string[] | undefined) {
if (Array.isArray(inputs) && output) {
return inputs.includes(output);
}
return inputs === output;
}
export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "settings": { types: Record<string, NodeInput>, values: Record<string, unknown> } }> {
status: Writable<"loading" | "idle" | "error"> = writable("loading");
loaded = false;
graph: Graph = { id: 0, nodes: [], edges: [] };
id = writable(0);
private _nodes: Map<number, Node> = new Map();
nodes: Writable<Map<number, Node>> = writable(new Map());
private _edges: Edge[] = [];
edges: Writable<Edge[]> = writable([]);
settingTypes: Record<string, NodeInput> = {};
settings: Record<string, unknown> = {};
currentUndoGroup: number | null = null;
inputSockets: Writable<Set<string>> = writable(new Set());
history: HistoryManager = new HistoryManager();
execute = throttle(() => {
if (this.loaded === false) return;
this.emit("result", this.serialize());
}, 10);
constructor(public registry: NodeRegistry) {
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 {
logger.group("serializing graph")
const nodes = Array.from(this._nodes.values()).map(node => ({
id: node.id,
position: [...node.position],
type: node.type,
props: node.props,
})) as Node[];
const edges = this._edges.map(edge => [edge[0].id, edge[1], edge[2].id, edge[3]]) as Graph["edges"];
const serialized = { id: this.graph.id, settings: this.settings, nodes, edges };
logger.groupEnd();
return clone(serialized);
}
private lastSettingsHash = 0;
setSettings(settings: Record<string, unknown>) {
let hash = fastHashString(JSON.stringify(settings));
if (hash === this.lastSettingsHash) return;
this.lastSettingsHash = hash;
this.settings = settings;
this.save();
this.execute();
}
getNodeDefinitions() {
return this.registry.getAllNodes();
}
getLinkedNodes(node: Node) {
const nodes = new Set<Node>();
const stack = [node];
while (stack.length) {
const n = stack.pop();
if (!n) continue;
nodes.add(n);
const children = this.getChildrenOfNode(n);
const parents = this.getParentsOfNode(n);
const newNodes = [...children, ...parents].filter(n => !nodes.has(n));
stack.push(...newNodes);
}
return [...nodes.values()];
}
getEdgesBetweenNodes(nodes: Node[]): [number, number, number, string][] {
const edges = [];
for (const node of nodes) {
const children = node.tmp?.children || [];
for (const child of children) {
if (nodes.includes(child)) {
const edge = this._edges.find(e => e[0].id === node.id && e[2].id === child.id);
if (edge) {
edges.push([edge[0].id, edge[1], edge[2].id, edge[3]] as [number, number, number, string]);
}
}
}
}
return edges;
}
private _init(graph: Graph) {
const nodes = new Map(graph.nodes.map(node => {
const nodeType = this.registry.getNode(node.type);
if (nodeType) {
node.tmp = {
random: (Math.random() - 0.5) * 2,
type: nodeType
};
}
return [node.id, node]
}));
const edges = graph.edges.map((edge) => {
const from = nodes.get(edge[0]);
const to = nodes.get(edge[2]);
if (!from || !to) {
throw new Error("Edge references non-existing node");
};
from.tmp = from.tmp || {};
from.tmp.children = from.tmp.children || [];
from.tmp.children.push(to);
to.tmp = to.tmp || {};
to.tmp.parents = to.tmp.parents || [];
to.tmp.parents.push(from);
return [from, edge[1], to, edge[3]] as Edge;
})
this.edges.set(edges);
this.nodes.set(nodes);
this.execute();
}
async load(graph: Graph) {
const a = performance.now();
this.loaded = false;
this.graph = graph;
this.status.set("loading");
this.id.set(graph.id);
const nodeIds = Array.from(new Set([...graph.nodes.map(n => n.type)]));
await this.registry.load(nodeIds);
for (const node of this.graph.nodes) {
const nodeType = this.registry.getNode(node.type);
if (!nodeType) {
logger.error(`Node type not found: ${node.type}`);
this.status.set("error");
return;
}
node.tmp = node.tmp || {};
node.tmp.random = (Math.random() - 0.5) * 2;
node.tmp.type = nodeType;
}
// load settings
const settingTypes: Record<string, NodeInput> = {};
const settingValues = graph.settings || {};
const types = this.getNodeDefinitions();
for (const type of types) {
if (type.inputs) {
for (const key in type.inputs) {
let settingId = type.inputs[key].setting;
if (settingId) {
settingTypes[settingId] = { __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;
}
}
}
}
}
this.settings = settingValues;
this.emit("settings", { types: settingTypes, values: settingValues });
this.history.reset();
this._init(this.graph);
this.save();
this.status.set("idle");
this.loaded = true;
logger.log(`Graph loaded in ${performance.now() - a}ms`);
setTimeout(() => this.execute(), 100);
}
getAllNodes() {
return Array.from(this._nodes.values());
}
getNode(id: number) {
return this._nodes.get(id);
}
getNodeType(id: string) {
return this.registry.getNode(id);
}
async loadNode(id: string) {
await this.registry.load([id]);
const nodeType = this.registry.getNode(id);
if (!nodeType) return;
const settingTypes = this.settingTypes;
const settingValues = this.settings;
if (nodeType.inputs) {
for (const key in nodeType.inputs) {
let settingId = nodeType.inputs[key].setting;
if (settingId) {
settingTypes[settingId] = nodeType.inputs[key];
if (settingValues[settingId] === undefined && "value" in nodeType.inputs[key]) {
settingValues[settingId] = nodeType.inputs[key].value;
}
}
}
}
this.settings = settingValues;
this.settingTypes = settingTypes;
this.emit("settings", { types: settingTypes, values: settingValues });
}
getChildrenOfNode(node: Node) {
const children = [];
const stack = node.tmp?.children?.slice(0);
while (stack?.length) {
const child = stack.pop();
if (!child) continue;
children.push(child);
stack.push(...child.tmp?.children || []);
}
return children;
}
getNodesBetween(from: Node, to: Node): Node[] | undefined {
// < - - - - from
const toParents = this.getParentsOfNode(to);
// < - - - - from - - - - to
const fromParents = this.getParentsOfNode(from);
if (toParents.includes(from)) {
const fromChildren = this.getChildrenOfNode(from);
return toParents.filter(n => fromChildren.includes(n));
} else if (fromParents.includes(to)) {
const toChildren = this.getChildrenOfNode(to);
return fromParents.filter(n => toChildren.includes(n));
} else {
// these two nodes are not connected
return;
}
}
removeNode(node: Node, { restoreEdges = false } = {}) {
const edgesToNode = this._edges.filter((edge) => edge[2].id === node.id);
const edgesFromNode = this._edges.filter((edge) => edge[0].id === node.id);
for (const edge of [...edgesToNode, ...edgesFromNode]) {
this.removeEdge(edge, { applyDeletion: false });
}
if (restoreEdges) {
const outputSockets = edgesToNode.map(e => [e[0], e[1]] as const);
const inputSockets = edgesFromNode.map(e => [e[2], e[3]] as const);
for (const [to, toSocket] of inputSockets) {
for (const [from, fromSocket] of outputSockets) {
const outputType = from.tmp?.type?.outputs?.[fromSocket];
const inputType = to?.tmp?.type?.inputs?.[toSocket]?.type;
if (outputType === inputType) {
this.createEdge(from, fromSocket, to, toSocket, { applyUpdate: false });
continue;
}
}
}
}
this.edges.set(this._edges);
this.nodes.update((nodes) => {
nodes.delete(node.id);
return nodes;
});
this.execute()
this.save();
}
createNodeId() {
const max = Math.max(...this._nodes.keys());
return max + 1;
}
createGraph(nodes: Node[], edges: [number, number, number, string][]) {
// map old ids to new ids
const idMap = new Map<number, number>();
const startId = this.createNodeId();
nodes = nodes.map((node, i) => {
const id = startId + i;
idMap.set(node.id, id);
const type = this.registry.getNode(node.type);
if (!type) {
throw new Error(`Node type not found: ${node.type}`);
}
return { ...node, id, tmp: { type } };
});
const _edges = edges.map(edge => {
const from = nodes.find(n => n.id === idMap.get(edge[0]));
const to = nodes.find(n => n.id === idMap.get(edge[2]));
if (!from || !to) {
throw new Error("Edge references non-existing node");
}
to.tmp = to.tmp || {};
to.tmp.parents = to.tmp.parents || [];
to.tmp.parents.push(from);
from.tmp = from.tmp || {};
from.tmp.children = from.tmp.children || [];
from.tmp.children.push(to);
return [from, edge[1], to, edge[3]] as Edge;
});
for (const node of nodes) {
this._nodes.set(node.id, node);
}
this._edges.push(..._edges);
this.nodes.set(this._nodes);
this.edges.set(this._edges);
this.save();
return nodes;
}
createNode({ type, position, props = {} }: { type: Node["type"], position: Node["position"], props: Node["props"] }) {
const nodeType = this.registry.getNode(type);
if (!nodeType) {
logger.error(`Node type not found: ${type}`);
return;
}
const node: Node = { id: this.createNodeId(), type, position, tmp: { type: nodeType }, props };
this.nodes.update((nodes) => {
nodes.set(node.id, node);
return nodes;
});
this.save();
}
createEdge(from: Node, fromSocket: number, to: Node, toSocket: string, { applyUpdate = true } = {}) {
const existingEdges = this.getEdgesToNode(to);
// check if this exact edge already exists
const existingEdge = existingEdges.find(e => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket);
if (existingEdge) {
logger.error("Edge already exists", existingEdge);
return;
};
// check if socket types match
const fromSocketType = from.tmp?.type?.outputs?.[fromSocket];
const toSocketType = [to.tmp?.type?.inputs?.[toSocket]?.type];
if (to.tmp?.type?.inputs?.[toSocket]?.accepts) {
toSocketType.push(...(to?.tmp?.type?.inputs?.[toSocket]?.accepts || []));
}
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
logger.error(`Socket types do not match: ${fromSocketType} !== ${toSocketType}`);
return;
}
const edgeToBeReplaced = this._edges.find(e => e[2].id === to.id && e[3] === toSocket);
if (edgeToBeReplaced) {
this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
}
if (applyUpdate) {
this._edges.push([from, fromSocket, to, toSocket]);
} else {
this._edges.push([from, fromSocket, to, toSocket]);
}
from.tmp = from.tmp || {};
from.tmp.children = from.tmp.children || [];
from.tmp.children.push(to);
to.tmp = to.tmp || {};
to.tmp.parents = to.tmp.parents || [];
to.tmp.parents.push(from);
if (applyUpdate) {
this.edges.set(this._edges);
this.save();
}
this.execute();
}
undo() {
const nextState = this.history.undo();
if (nextState) {
this._init(nextState);
this.emit("save", this.serialize());
}
}
redo() {
const nextState = this.history.redo();
if (nextState) {
this._init(nextState);
this.emit("save", this.serialize());
}
}
startUndoGroup() {
this.currentUndoGroup = 1;
}
saveUndoGroup() {
this.currentUndoGroup = null;
this.save();
}
save() {
if (this.currentUndoGroup) return;
const state = this.serialize();
this.history.save(state);
this.emit("save", state);
logger.log("saving graphs", state);
}
getParentsOfNode(node: Node) {
const parents = [];
const stack = node.tmp?.parents?.slice(0);
while (stack?.length) {
if (parents.length > 1000000) {
logger.warn("Infinite loop detected")
break;
}
const parent = stack.pop();
if (!parent) continue;
parents.push(parent);
stack.push(...parent.tmp?.parents || []);
}
return parents.reverse();
}
getPossibleSockets({ node, index }: Socket): [Node, string | number][] {
const nodeType = node?.tmp?.type;
if (!nodeType) return [];
const sockets: [Node, string | number][] = []
// if index is a string, we are an input looking for outputs
if (typeof index === "string") {
// filter out self and child nodes
const children = new Set(this.getChildrenOfNode(node).map(n => n.id));
const nodes = this.getAllNodes().filter(n => n.id !== node.id && !children.has(n.id));
const ownType = nodeType?.inputs?.[index].type;
for (const node of nodes) {
const nodeType = node?.tmp?.type;
const inputs = nodeType?.outputs;
if (!inputs) continue;
for (let index = 0; index < inputs.length; index++) {
if (inputs[index] === ownType) {
sockets.push([node, index]);
}
}
}
} else if (typeof index === "number") {
// if index is a number, we are an output looking for inputs
// filter out self and parent nodes
const parents = new Set(this.getParentsOfNode(node).map(n => n.id));
const nodes = this.getAllNodes().filter(n => n.id !== node.id && !parents.has(n.id));
// 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 ownType = nodeType.outputs?.[index];
for (const node of nodes) {
const inputs = node?.tmp?.type?.inputs;
if (!inputs) continue;
for (const key in inputs) {
const otherType = [inputs[key].type];
otherType.push(...(inputs[key].accepts || []));
if (areSocketsCompatible(ownType, otherType) && edges.get(node.id) !== key) {
sockets.push([node, key]);
}
}
}
}
return sockets;
}
removeEdge(edge: Edge, { applyDeletion = true }: { applyDeletion?: boolean } = {}) {
const id0 = edge[0].id;
const sid0 = edge[1];
const id2 = edge[2].id;
const sid2 = edge[3];
const _edge = this._edges.find((e) => e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2);
if (!_edge) return;
edge[0].tmp = edge[0].tmp || {};
if (edge[0].tmp.children) {
edge[0].tmp.children = edge[0].tmp.children.filter(n => n.id !== id2);
}
edge[2].tmp = edge[2].tmp || {};
if (edge[2].tmp.parents) {
edge[2].tmp.parents = edge[2].tmp.parents.filter(n => n.id !== id0);
}
if (applyDeletion) {
this.edges.update((edges) => {
return edges.filter(e => e !== _edge);
});
this.execute();
this.save();
} else {
this._edges = this._edges.filter(e => e !== _edge);
}
}
getEdgesToNode(node: Node) {
return this._edges
.filter((edge) => edge[2].id === node.id)
.map((edge) => {
const from = this.getNode(edge[0].id);
const to = this.getNode(edge[2].id);
if (!from || !to) return;
return [from, edge[1], to, edge[3]] as const;
})
.filter(Boolean) as unknown as [Node, number, Node, string][];
}
getEdgesFromNode(node: Node) {
return this._edges
.filter((edge) => edge[0].id === node.id)
.map((edge) => {
const from = this.getNode(edge[0].id);
const to = this.getNode(edge[2].id);
if (!from || !to) return;
return [from, edge[1], to, edge[3]] as const;
})
.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">
import type { Graph, Node, NodeRegistry } from "@nodes/types";
import type { Graph, NodeInstance, NodeRegistry } from "@nodarium/types";
import GraphEl from "./Graph.svelte";
import { GraphManager } from "../graph-manager.js";
import { setContext } from "svelte";
import { debounce } from "$lib/helpers";
import { GraphManager } from "../graph-manager.svelte";
import { createKeyMap } from "$lib/helpers/createKeyMap";
import { GraphState } from "./state.svelte";
const graphState = new GraphState();
setContext("graphState", graphState);
import { GraphState, setGraphManager, setGraphState } from "./state.svelte";
import { setupKeymaps } from "../keymaps";
type Props = {
graph: Graph;
@@ -16,7 +12,7 @@
settings?: Record<string, any>;
activeNode?: Node;
activeNode?: NodeInstance;
showGrid?: boolean;
snapToGrid?: boolean;
showHelp?: boolean;
@@ -31,8 +27,8 @@
registry,
settings = $bindable(),
activeNode = $bindable(),
showGrid,
snapToGrid,
showGrid = $bindable(true),
snapToGrid = $bindable(true),
showHelp = $bindable(false),
settingTypes = $bindable(),
onsave,
@@ -40,10 +36,20 @@
}: Props = $props();
export const keymap = createKeyMap([]);
setContext("keymap", keymap);
export const manager = new GraphManager(registry);
setContext("graphManager", manager);
setGraphManager(manager);
const graphState = new GraphState(manager);
$effect(() => {
graphState.showGrid = showGrid;
graphState.snapToGrid = snapToGrid;
graphState.showHelp = showHelp;
});
setGraphState(graphState);
setupKeymaps(keymap, manager, graphState);
$effect(() => {
if (graphState.activeNodeId !== -1) {
@@ -53,13 +59,16 @@
}
});
const updateSettings = debounce((s) => {
manager.setSettings(s);
}, 200);
$effect(() => {
if (!graphState.addMenuPosition) {
graphState.edgeEndPosition = null;
graphState.activeSocket = null;
}
});
$effect(() => {
if (settingTypes && settings) {
updateSettings($state.snapshot(settings));
manager.setSettings(settings);
}
});
@@ -75,4 +84,4 @@
manager.load(graph);
</script>
<GraphEl bind:showGrid bind:snapToGrid bind:showHelp />
<GraphEl {keymap} />

View File

@@ -12,21 +12,23 @@ const variables = [
"edge",
] as const;
function getColor(variable: typeof variables[number]) {
function getColor(variable: (typeof variables)[number]) {
const style = getComputedStyle(document.body.parentElement!);
let color = style.getPropertyValue(`--${variable}`);
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(() => {
if (!appSettings.theme || !("getComputedStyle" in globalThis)) return;
if (!appSettings.value.theme || !("getComputedStyle" in globalThis)) return;
const style = getComputedStyle(document.body.parentElement!);
for (const v of variables) {
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 NodeId, type NodeInstance } 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 NodeId;
let mx = event.clientX - this.state.rect.x;
let my = event.clientY - this.state.rect.y;
if (nodeId) {
let nodeOffsetX = event.dataTransfer.getData("data/node-offset-x");
let nodeOffsetY = event.dataTransfer.getData("data/node-offset-y");
if (nodeOffsetX && nodeOffsetY) {
mx += parseInt(nodeOffsetX);
my += parseInt(nodeOffsetY);
}
let props = {};
let rawNodeProps = event.dataTransfer.getData("data/node-props");
if (rawNodeProps) {
try {
props = JSON.parse(rawNodeProps);
} catch (e) { }
}
const pos = this.state.projectScreenToWorld(mx, my);
this.graph.registry.load([nodeId]).then(() => {
this.graph.createNode({
type: nodeId,
props,
position: pos,
});
});
} else if (event.dataTransfer.files.length) {
const file = event.dataTransfer.files[0];
if (file.type === "application/wasm") {
const reader = new FileReader();
reader.onload = async (e) => {
const buffer = e.target?.result;
if (buffer?.constructor === ArrayBuffer) {
const nodeType = await this.graph.registry.register(buffer);
this.graph.createNode({
type: nodeType.id,
props: {},
position: this.state.projectScreenToWorld(mx, my),
});
}
};
reader.readAsArrayBuffer(file);
} else if (file.type === "application/json") {
const reader = new FileReader();
reader.onload = (e) => {
const buffer = e.target?.result as ArrayBuffer;
if (buffer) {
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
this.graph.load(state);
}
};
reader.readAsText(file);
}
}
}
handleMouseLeave() {
this.state.isDragging = false;
this.state.isPanning = false;
}
handleDragEnter(e: DragEvent) {
e.preventDefault();
this.state.isDragging = true;
this.state.isPanning = false;
}
handleDragOver(e: DragEvent) {
e.preventDefault();
this.state.isDragging = true;
this.state.isPanning = false;
}
handleDragEnd(e: DragEvent) {
e.preventDefault();
this.state.isDragging = true;
this.state.isPanning = false;
}
getEventListenerProps() {
return {
ondragenter: (ev: DragEvent) => this.handleDragEnter(ev),
ondragover: (ev: DragEvent) => this.handleDragOver(ev),
ondragexit: (ev: DragEvent) => this.handleDragEnd(ev),
ondrop: (ev: DragEvent) => this.handleFileDrop(ev),
onmouseleave: () => this.handleMouseLeave(),
}
}
}
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?.state?.isMoving && !event.ctrlKey && !event.shiftKey) {
this.state.activeNodeId = clickedNodeId;
this.state.clearSelection();
}
}
}
if (activeNode?.state?.isMoving) {
activeNode.state = activeNode.state || {};
activeNode.state.isMoving = false;
if (this.state.snapToGrid) {
const snapLevel = this.state.getSnapLevel();
activeNode.position[0] = snapPointToGrid(
activeNode?.state?.x ?? activeNode.position[0],
5 / snapLevel,
);
activeNode.position[1] = snapPointToGrid(
activeNode?.state?.y ?? activeNode.position[1],
5 / snapLevel,
);
} else {
activeNode.position[0] = activeNode?.state?.x ?? activeNode.position[0];
activeNode.position[1] = activeNode?.state?.y ?? activeNode.position[1];
}
const nodes = [
...[...(this.state.selectedNodes?.values() || [])].map((id) =>
this.graph.getNode(id),
),
] as NodeInstance[];
const vec = [
activeNode.position[0] - (activeNode?.state.x || 0),
activeNode.position[1] - (activeNode?.state.y || 0),
];
for (const node of nodes) {
if (!node) continue;
node.state = node.state || {};
const { x, y } = node.state;
if (x !== undefined && y !== undefined) {
node.position[0] = x + vec[0];
node.position[1] = y + vec[1];
}
}
nodes.push(activeNode);
animate(500, (a: number) => {
for (const node of nodes) {
if (
node?.state &&
node.state["x"] !== undefined &&
node.state["y"] !== undefined
) {
node.state.x = lerp(node.state.x, node.position[0], a);
node.state.y = lerp(node.state.y, node.position[1], a);
this.state.updateNodePosition(node);
if (node?.state?.isMoving) {
return false;
}
}
}
});
this.graph.save();
} else if (this.state.hoveredSocket && this.state.activeSocket) {
if (
typeof this.state.hoveredSocket.index === "number" &&
typeof this.state.activeSocket.index === "string"
) {
this.graph.createEdge(
this.state.hoveredSocket.node,
this.state.hoveredSocket.index || 0,
this.state.activeSocket.node,
this.state.activeSocket.index,
);
} else if (
typeof this.state.activeSocket.index == "number" &&
typeof this.state.hoveredSocket.index === "string"
) {
this.graph.createEdge(
this.state.activeSocket.node,
this.state.activeSocket.index || 0,
this.state.hoveredSocket.node,
this.state.hoveredSocket.index,
);
}
this.graph.save();
} else if (this.state.activeSocket && event.ctrlKey) {
// Handle automatic adding of nodes on ctrl+mouseUp
this.state.edgeEndPosition = [
this.state.mousePosition[0],
this.state.mousePosition[1],
];
if (typeof this.state.activeSocket.index === "number") {
this.state.addMenuPosition = [
this.state.mousePosition[0],
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2],
];
} else {
this.state.addMenuPosition = [
this.state.mousePosition[0] - 155 / this.state.cameraPosition[2],
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2],
];
}
return;
}
// check if camera moved
if (
clickedNodeId === -1 &&
!this.state.boxSelection &&
this.state.cameraDown[0] === this.state.cameraPosition[0] &&
this.state.cameraDown[1] === this.state.cameraPosition[1] &&
this.state.isBodyFocused()
) {
this.state.activeNodeId = -1;
this.state.clearSelection();
}
this.state.mouseDown = null;
this.state.boxSelection = false;
this.state.activeSocket = null;
this.state.possibleSockets = [];
this.state.hoveredSocket = null;
this.state.addMenuPosition = null;
}
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.state = node.state || {};
node.state.downX = node.position[0];
node.state.downY = node.position[1];
if (this.state.selectedNodes) {
for (const nodeId of this.state.selectedNodes) {
const n = this.graph.getNode(nodeId);
if (!n) continue;
n.state = n.state || {};
n.state.downX = n.position[0];
n.state.downY = n.position[1];
}
}
this.state.edgeEndPosition = null;
}
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?.state) continue;
const x = node.position[0];
const y = node.position[1];
const height = this.state.getNodeHeight(node.type);
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
this.state.selectedNodes?.add(node.id);
} else {
this.state.selectedNodes?.delete(node.id);
}
}
return;
}
// here we are handling dragging of nodes
if (this.state.activeNodeId !== -1 && this.state.mouseDownNodeId !== -1) {
const node = this.graph.getNode(this.state.activeNodeId);
if (!node || event.buttons !== 1) return;
node.state = node.state || {};
const oldX = node.state.downX || 0;
const oldY = node.state.downY || 0;
let newX =
oldX + (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
let newY =
oldY + (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
if (event.ctrlKey) {
const snapLevel = this.state.getSnapLevel();
if (this.state.snapToGrid) {
newX = snapPointToGrid(newX, 5 / snapLevel);
newY = snapPointToGrid(newY, 5 / snapLevel);
}
}
if (!node.state.isMoving) {
const dist = Math.sqrt((oldX - newX) ** 2 + (oldY - newY) ** 2);
if (dist > 0.2) {
node.state.isMoving = true;
}
}
const vecX = oldX - newX;
const vecY = oldY - newY;
if (this.state.selectedNodes?.size) {
for (const nodeId of this.state.selectedNodes) {
const n = this.graph.getNode(nodeId);
if (!n?.state) continue;
n.state.x = (n?.state?.downX || 0) - vecX;
n.state.y = (n?.state?.downY || 0) - vecY;
this.state.updateNodePosition(n);
}
}
node.state.x = newX;
node.state.y = newY;
this.state.updateNodePosition(node);
return;
}
// here we are handling panning of camera
this.state.isPanning = true;
let newX =
this.state.cameraDown[0] -
(mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
let newY =
this.state.cameraDown[1] -
(my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
this.state.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,313 @@
import type { Socket } from "@nodes/types";
import { getContext } from "svelte";
import { SvelteSet } from 'svelte/reactivity';
import type { NodeInstance, Socket } from "@nodarium/types";
import { getContext, setContext } from "svelte";
import { SvelteSet } from "svelte/reactivity";
import type { GraphManager } from "../graph-manager.svelte";
import type { OrthographicCamera } from "three";
const graphStateKey = Symbol("graph-state");
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 {
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: NodeInstance[];
edges: [number, number, number, string][];
} = null;
cameraBounds = $derived([
this.cameraPosition[0] - this.width / this.cameraPosition[2] / 2,
this.cameraPosition[0] + this.width / this.cameraPosition[2] / 2,
this.cameraPosition[1] - this.height / this.cameraPosition[2] / 2,
this.cameraPosition[1] + this.height / this.cameraPosition[2] / 2,
]);
boxSelection = $state(false);
edgeEndPosition = $state<[number, number] | null>();
addMenuPosition = $state<[number, number] | null>(null);
snapToGrid = $state(false);
showGrid = $state(true)
showHelp = $state(false)
cameraDown = [0, 0];
mouseDownNodeId = -1;
isPanning = $state(false);
isDragging = $state(false);
hoveredNodeId = $state(-1);
mousePosition = $state([0, 0]);
mouseDown = $state<[number, number] | null>(null);
activeNodeId = $state(-1);
selectedNodes = new SvelteSet<number>();
activeSocket = $state<Socket | null>(null);
hoveredSocket = $state<Socket | null>(null);
possibleSockets = $state<Socket[]>([]);
possibleSocketIds = $derived(new Set(
this.possibleSockets.map((s) => `${s.node.id}-${s.index}`),
));
possibleSocketIds = $derived(
new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`)),
);
clearSelection() {
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: NodeInstance) {
if (node.state.ref && node.state.mesh) {
if (node.state["x"] !== undefined && node.state["y"] !== undefined) {
node.state.ref.style.setProperty("--nx", `${node.state.x * 10}px`);
node.state.ref.style.setProperty("--ny", `${node.state.y * 10}px`);
node.state.mesh.position.x = node.state.x + 10;
node.state.mesh.position.z = node.state.y + this.getNodeHeight(node.type) / 2;
if (
node.state.x === node.position[0] &&
node.state.y === node.position[1]
) {
delete node.state.x;
delete node.state.y;
}
this.graph.edges = [...this.graph.edges];
} else {
node.state.ref.style.setProperty("--nx", `${node.position[0] * 10}px`);
node.state.ref.style.setProperty("--ny", `${node.position[1] * 10}px`);
node.state.mesh.position.x = node.position[0] + 10;
node.state.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: NodeInstance,
index: string | number,
): [number, number] {
if (typeof index === "number") {
return [
(node?.state?.x ?? node.position[0]) + 20,
(node?.state?.y ?? node.position[1]) + 2.5 + 10 * index,
];
} else {
const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index);
return [
node?.state?.x ?? node.position[0],
(node?.state?.y ?? node.position[1]) + 10 + 10 * _index,
];
}
}
private nodeHeightCache: Record<string, number> = {};
getNodeHeight(nodeTypeId: string) {
if (nodeTypeId in this.nodeHeightCache) {
return this.nodeHeightCache[nodeTypeId];
}
const node = this.graph.getNodeType(nodeTypeId);
if (!node?.inputs) {
return 5;
}
const height =
5 +
10 *
Object.keys(node.inputs).filter(
(p) =>
p !== "seed" &&
node?.inputs &&
!("setting" in node?.inputs?.[p]) &&
node.inputs[p].hidden !== true,
).length;
this.nodeHeightCache[nodeTypeId] = height;
return height;
}
copyNodes() {
if (this.activeNodeId === -1 && !this.selectedNodes?.size)
return;
let nodes = [
this.activeNodeId,
...(this.selectedNodes?.values() || []),
]
.map((id) => this.graph.getNode(id))
.filter(b => !!b);
const edges = this.graph.getEdgesBetweenNodes(nodes);
nodes = nodes.map((node) => ({
...node,
position: [
this.mousePosition[0] - node.position[0],
this.mousePosition[1] - node.position[1],
],
tmp: undefined,
}));
this.clipboard = {
nodes: nodes,
edges: edges,
};
}
pasteNodes() {
if (!this.clipboard) return;
const nodes = this.clipboard.nodes
.map((node) => {
node.position[0] = this.mousePosition[0] - node.position[0];
node.position[1] = this.mousePosition[1] - node.position[1];
return node;
})
.filter(Boolean) as NodeInstance[];
const newNodes = this.graph.createGraph(nodes, this.clipboard.edges);
this.selectedNodes.clear();
for (const node of newNodes) {
this.selectedNodes.add(node.id);
}
}
setDownSocket(socket: Socket) {
this.activeSocket = socket;
let { node, index, position } = socket;
// remove existing edge
if (typeof index === "string") {
const edges = this.graph.getEdgesToNode(node);
for (const edge of edges) {
if (edge[3] === index) {
node = edge[0];
index = edge[1];
position = this.getSocketPosition(node, index);
this.graph.removeEdge(edge);
break;
}
}
}
this.mouseDown = position;
this.activeSocket = {
node,
index,
position,
};
this.possibleSockets = this.graph
.getPossibleSockets(this.activeSocket)
.map(([node, index]) => {
return {
node,
index,
position: this.getSocketPosition(node, index),
};
});
};
projectScreenToWorld(x: number, y: number): [number, number] {
return [
this.cameraPosition[0] +
(x - this.width / 2) / this.cameraPosition[2],
this.cameraPosition[1] +
(y - this.height / 2) / this.cameraPosition[2],
];
}
getNodeIdFromEvent(event: MouseEvent) {
let clickedNodeId = -1;
let mx = event.clientX - this.rect.x;
let my = event.clientY - this.rect.y;
if (event.button === 0) {
// check if the clicked element is a node
if (event.target instanceof HTMLElement) {
const nodeElement = event.target.closest(".node");
const nodeId = nodeElement?.getAttribute?.("data-node-id");
if (nodeId) {
clickedNodeId = parseInt(nodeId, 10);
}
}
// if we do not have an active node,
// we are going to check if we clicked on a node by coordinates
if (clickedNodeId === -1) {
const [downX, downY] = this.projectScreenToWorld(mx, my);
for (const node of this.graph.nodes.values()) {
const x = node.position[0];
const y = node.position[1];
const height = this.getNodeHeight(node.type);
if (downX > x && downX < x + 20 && downY > y && downY < y + height) {
clickedNodeId = node.id;
break;
}
}
}
}
return clickedNodeId;
}
isNodeInView(node: NodeInstance) {
const height = this.getNodeHeight(node.type);
const width = 20;
return (
node.position[0] > this.cameraBounds[0] - width &&
node.position[0] < this.cameraBounds[1] &&
node.position[1] > this.cameraBounds[2] - height &&
node.position[1] < this.cameraBounds[3]
);
};
}

View File

@@ -1,15 +1,15 @@
import throttle from './throttle.js';
import throttle from "$lib/helpers/throttle";
type EventMap = Record<string, unknown>;
type EventKey<T extends EventMap> = string & keyof T;
type EventReceiver<T> = (params: T, stuff?: Record<string, unknown>) => unknown;
export default class EventEmitter<T extends EventMap = { [key: string]: unknown }> {
export default class EventEmitter<
T extends EventMap = { [key: string]: unknown },
> {
index = 0;
public eventMap: T = {} as T;
constructor() {
}
constructor() {}
private cbs: { [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);
const cbs = Object.assign(this.cbs, {
[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);
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
* @returns {function} Returns a function which removes the listener when called
*/
public once<K extends EventKey<T>>(event: K, cb: EventReceiver<T[K]>): () => void {
this.cbsOnce[event] = [...(this.cbsOnce[event] || []), cb];
public once<K extends EventKey<T>>(
event: K,
cb: EventReceiver<T[K]>,
): () => void {
const cbsOnce = Object.assign(this.cbsOnce, {
[event]: [...(this.cbsOnce[event] || []), cb],
});
this.cbsOnce = cbsOnce;
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;
}
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 loop = (time: number) => {
const progress = (time - start) / duration;
@@ -18,7 +21,7 @@ export function animate(duration: number, callback: (progress: number) => void |
} else {
callback(1);
}
}
};
requestAnimationFrame(loop);
}
@@ -33,7 +36,8 @@ export function createNodePath({
aspectRatio = 1,
} = {}) {
return `M0,${cornerTop}
${cornerTop
${
cornerTop
? ` V${cornerTop}
Q0,0 ${cornerTop * aspectRatio},0
H${100 - cornerTop * aspectRatio}
@@ -44,11 +48,13 @@ export function createNodePath({
`
}
V${y - height / 2}
${rightBump
${
rightBump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100`
}
${cornerBottom
${
cornerBottom
? ` V${100 - cornerBottom}
Q100,100 ${100 - cornerBottom * aspectRatio},100
H${cornerBottom * aspectRatio}
@@ -56,7 +62,8 @@ export function createNodePath({
`
: `${leftBump ? `V100 H0` : `V100`}`
}
${leftBump
${
leftBump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
: ` 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 createLogger = (() => {
let maxLength = 5;
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 const clone: <T>(v: T) => T =
"structedClone" in globalThis
? globalThis.structuredClone
: (obj) => JSON.parse(JSON.stringify(obj));
export function withSubComponents<A, B extends Record<string, any>>(
component: A,
subcomponents: B
subcomponents: B,
): A & B {
Object.keys(subcomponents).forEach((key) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any

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 type { Graph } from "@nodes/types";
import { createLogger, clone } from "./helpers/index.js";
import type { Graph } from "@nodarium/types";
import { clone } from "./helpers/index.js";
import { createLogger } from "@nodarium/utils";
const diff = create({
objectHash: function (obj, index) {
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)) {
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();
export class HistoryManager {
index: number = -1;
history: Delta[] = [];
private initialState: Graph | undefined;
@@ -27,26 +27,25 @@ export class HistoryManager {
private opts = {
debounce: 400,
maxHistory: 100,
}
};
constructor({ maxHistory = 100, debounce = 100 } = {}) {
this.history = [];
this.index = -1;
this.opts.debounce = debounce;
this.opts.maxHistory = maxHistory;
globalThis["_history"] = this;
}
save(state: Graph) {
if (!this.state) {
this.state = clone(state);
this.initialState = this.state;
log.log("initial state saved")
log.log("initial state saved");
} else {
const newState = state;
const delta = diff.diff(this.state, newState);
if (delta) {
log.log("saving state")
log.log("saving state");
// Add the delta to history
if (this.index < this.history.length - 1) {
// Clear the history after the current index if new changes are made
@@ -62,7 +61,7 @@ export class HistoryManager {
}
this.state = newState;
} else {
log.log("no changes")
log.log("no changes");
}
}
}
@@ -76,7 +75,7 @@ export class HistoryManager {
undo() {
if (this.index === -1 && this.initialState) {
log.log("reached start, loading initial state")
log.log("reached start, loading initial state");
return clone(this.initialState);
} else {
const delta = this.history[this.index];
@@ -96,7 +95,7 @@ export class HistoryManager {
this.state = nextState;
return clone(nextState);
} 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,5 @@
<script lang="ts">
import type { Node } from "@nodes/types";
import { getContext, onMount } from "svelte";
import type { NodeInstance } from "@nodarium/types";
import { getGraphState } from "../graph/state.svelte";
import { T } from "@threlte/core";
import { type Mesh } from "three";
@@ -13,43 +12,32 @@
const graphState = getGraphState();
type Props = {
node: Node;
node: NodeInstance;
inView: boolean;
z: number;
};
const { node, inView, z }: Props = $props();
let { node = $bindable(), inView, z }: Props = $props();
const isActive = $derived(graphState.activeNodeId === node.id);
const isSelected = $derived(graphState.selectedNodes.has(node.id));
let strokeColor = $state(colors.selected);
$effect(() => {
appSettings.theme;
strokeColor = isSelected
const strokeColor = $derived(
appSettings.value.theme &&
(isSelected
? colors.selected
: isActive
? colors.active
: colors.outline;
});
const updateNodePosition =
getContext<(n: Node) => void>("updateNodePosition");
const getNodeHeight = getContext<(n: string) => number>("getNodeHeight");
: colors.outline),
);
let meshRef: Mesh | undefined = $state();
const height = getNodeHeight?.(node.type);
const height = graphState.getNodeHeight(node.type);
$effect(() => {
node.tmp = node.tmp || {};
node.tmp.mesh = meshRef;
updateNodePosition?.(node);
});
onMount(() => {
node.tmp = node.tmp || {};
node.tmp.mesh = meshRef;
updateNodePosition?.(node);
if (meshRef && !node.state?.mesh) {
node.state.mesh = meshRef;
graphState.updateNodePosition(node);
}
});
</script>
@@ -79,4 +67,4 @@
/>
</T.Mesh>
<NodeHtml {node} {inView} {isActive} {isSelected} {z} />
<NodeHtml bind:node {inView} {isActive} {isSelected} {z} />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,27 +1,31 @@
<script lang="ts">
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";
let sizes = localStore<string[]>(gridId, []);
let sizes = localState<string[]>(gridId, []);
const { children } = $props();
let registerIndex = 0;
setContext("registerCell", function () {
let index = registerIndex;
registerIndex++;
if (registerIndex > $sizes.length) {
$sizes = [...$sizes, "1fr"];
if (registerIndex > sizes.value.length) {
sizes.value = [...sizes.value, "1fr"];
}
return index;
});
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>
<div class="wrapper" style={`grid-template-columns: ${cols};`}>
<slot />
{@render children()}
</div>
<style>

View File

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

View File

@@ -1,11 +1,34 @@
export function localState<T>(key: string, defaultValue: T): T {
const stored = localStorage.getItem(key)
const state = $state(stored ? JSON.parse(stored) : defaultValue)
import { browser } from "$app/environment";
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(() => {
const value = $state.snapshot(state);
localStorage.setItem(key, JSON.stringify(value));
localStorage.setItem(this.key, this.serialize(this.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[]>(
fn: (...args: A) => R,
delay: number
): ((...args: A) => R) => {
let wait = false;
export default <T extends unknown[]>(
callback: (...args: T) => void,
delay: number,
) => {
let isWaiting = false;
return (...args: A) => {
if (wait) return undefined;
return (...args: T) => {
if (isWaiting) {
return;
}
const val = fn(...args);
wait = true;
callback(...args);
isWaiting = true;
setTimeout(() => {
wait = false;
isWaiting = false;
}, delay);
return val;
}
};
};

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
import { writable } from "svelte/store";
import BreadCrumbs from "./BreadCrumbs.svelte";
import DraggableNode from "./DraggableNode.svelte";
import type { RemoteNodeRegistry } from "@nodes/registry";
import type { RemoteNodeRegistry } from "@nodarium/registry";
export let registry: RemoteNodeRegistry;

View File

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

View File

@@ -27,10 +27,12 @@
function constructPath() {
max = max !== undefined ? max : Math.max(...points);
min = min !== undefined ? min : Math.min(...points);
const mi = min as number;
const ma = max as number;
return points
.map((point, i) => {
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}`;
})
.join(" ");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,27 @@
import { fastHashArrayBuffer } from "@nodes/utils";
import { BufferAttribute, BufferGeometry, Float32BufferAttribute, Group, InstancedMesh, Material, Matrix4, Mesh } from "three";
import { fastHashArrayBuffer } from "@nodarium/utils";
import {
BufferAttribute,
BufferGeometry,
Float32BufferAttribute,
Group,
InstancedMesh,
Material,
Matrix4,
Mesh,
} from "three";
function fastArrayHash(arr: ArrayBuffer) {
let ints = new Uint8Array(arr);
function fastArrayHash(arr: Int32Array) {
const sampleDistance = Math.max(Math.floor(arr.length / 1000), 1);
const sampleCount = Math.floor(arr.length / sampleDistance);
const sampleDistance = Math.max(Math.floor(ints.length / 100), 1);
const sampleCount = Math.floor(ints.length / sampleDistance);
let hash = new Uint8Array(sampleCount);
let hash = new Int32Array(sampleCount);
for (let i = 0; i < sampleCount; i++) {
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) {
@@ -26,11 +33,16 @@ export function createGeometryPool(parentScene: Group, material: Material) {
let totalVertices = 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 geometry = existingMesh ? existingMesh.geometry : new BufferGeometry();
if (existingMesh) {
existingMesh.visible = true;
}
// Extract data from the encoded array
// const geometryType = encodedData[index++];
@@ -50,11 +62,7 @@ export function createGeometryPool(parentScene: Group, material: Material) {
index = indicesEnd;
// Vertices
const vertices = new Float32Array(
data.buffer,
index * 4,
vertexCount * 3,
);
const vertices = new Float32Array(data.buffer, index * 4, vertexCount * 3);
index = index + vertexCount * 3;
let posAttribute = geometry.getAttribute(
@@ -71,11 +79,7 @@ export function createGeometryPool(parentScene: Group, material: Material) {
);
}
const normals = new Float32Array(
data.buffer,
index * 4,
vertexCount * 3,
);
const normals = new Float32Array(data.buffer, index * 4, vertexCount * 3);
index = index + vertexCount * 3;
if (
@@ -109,11 +113,8 @@ export function createGeometryPool(parentScene: Group, material: Material) {
}
}
return {
update(
newData: Int32Array[],
) {
update(newData: Int32Array[]) {
totalVertices = 0;
totalFaces = 0;
for (let i = 0; i < Math.max(newData.length, meshes.length); i++) {
@@ -123,15 +124,17 @@ export function createGeometryPool(parentScene: Group, material: Material) {
updateSingleGeometry(input, existingMesh || null);
} else if (existingMesh) {
existingMesh.visible = false;
scene.remove(existingMesh);
}
}
return { totalVertices, totalFaces };
}
}
},
};
}
export function createInstancedGeometryPool(parentScene: Group, material: Material) {
export function createInstancedGeometryPool(
parentScene: Group,
material: Material,
) {
const scene = new Group();
parentScene.add(scene);
@@ -139,19 +142,25 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
let totalVertices = 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 geometry = existingInstance ? existingInstance.geometry : new BufferGeometry();
let geometry = existingInstance
? existingInstance.geometry
: new BufferGeometry();
// Extract data from the encoded array
let index = 0;
const geometryType = data[index++];
// const geometryType = data[index++];
index++;
const vertexCount = data[index++];
const faceCount = data[index++];
const instanceCount = data[index++];
const stemDepth = data[index++];
// const stemDepth = data[index++];
index++;
totalVertices += vertexCount * instanceCount;
totalFaces += faceCount * instanceCount;
@@ -168,11 +177,7 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
}
// Vertices
const vertices = new Float32Array(
data.buffer,
index * 4,
vertexCount * 3,
);
const vertices = new Float32Array(data.buffer, index * 4, vertexCount * 3);
index = index + vertexCount * 3;
let posAttribute = geometry.getAttribute(
"position",
@@ -187,11 +192,7 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
);
}
const normals = new Float32Array(
data.buffer,
index * 4,
vertexCount * 3,
);
const normals = new Float32Array(data.buffer, index * 4, vertexCount * 3);
index = index + vertexCount * 3;
const normalsAttribute = geometry.getAttribute(
"normal",
@@ -203,20 +204,20 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
geometry.setAttribute("normal", new Float32BufferAttribute(normals, 3));
}
if (existingInstance && instanceCount > existingInstance.geometry.userData.count) {
console.log("recreating instance")
if (
existingInstance &&
instanceCount > existingInstance.geometry.userData.count
) {
scene.remove(existingInstance);
instances.splice(instances.indexOf(existingInstance), 1);
existingInstance = new InstancedMesh(geometry, material, instanceCount);
scene.add(existingInstance)
instances.push(existingInstance)
scene.add(existingInstance);
instances.push(existingInstance);
} else if (!existingInstance) {
console.log("creating instance")
existingInstance = new InstancedMesh(geometry, material, instanceCount);
scene.add(existingInstance)
instances.push(existingInstance)
scene.add(existingInstance);
instances.push(existingInstance);
} else {
console.log("updating instance")
existingInstance.count = instanceCount;
}
@@ -225,28 +226,31 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
const matrices = new Float32Array(
data.buffer,
index * 4,
instanceCount * 16);
instanceCount * 16,
);
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);
}
geometry.userData = {
vertexCount,
faceCount,
count: Math.max(instanceCount, existingInstance.geometry.userData.count || 0),
count: Math.max(
instanceCount,
existingInstance.geometry.userData.count || 0,
),
hash,
};
existingInstance.instanceMatrix.needsUpdate = true;
}
return {
update(
newData: Int32Array[],
) {
update(newData: Int32Array[]) {
totalVertices = 0;
totalFaces = 0;
for (let i = 0; i < Math.max(newData.length, instances.length); i++) {
@@ -256,10 +260,9 @@ export function createInstancedGeometryPool(parentScene: Group, material: Materi
updateSingleInstance(input, existingMesh || null);
} else if (existingMesh) {
existingMesh.visible = false;
scene.remove(existingMesh);
}
}
return { totalVertices, totalFaces };
}
}
},
};
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,22 @@
import { MemoryRuntimeExecutor } from "./runtime-executor";
import { RemoteNodeRegistry, IndexDBCache } from "@nodes/registry";
import type { Graph } from "@nodes/types";
import { createPerformanceStore } from "@nodes/utils";
import { RemoteNodeRegistry, IndexDBCache } from "@nodarium/registry";
import type { Graph } from "@nodarium/types";
import { createPerformanceStore } from "@nodarium/utils";
import { MemoryRuntimeCache } from "./runtime-executor-cache";
const cache = new MemoryRuntimeCache();
const indexDbCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry("");
nodeRegistry.cache = indexDbCache;
const nodeRegistry = new RemoteNodeRegistry("", indexDbCache);
const cache = new MemoryRuntimeCache()
const executor = new MemoryRuntimeExecutor(nodeRegistry, cache);
const performanceStore = createPerformanceStore();
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));
performanceStore.startRun();
let res = await executor.execute(graph, settings);

View File

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

View File

@@ -1,21 +1,22 @@
<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";
import type { NodeInput } from "@nodarium/types";
import Input from "@nodarium/ui";
type Button = { type: "button"; label?: string };
type Button = { type: "button"; callback: () => void; label?: string };
type InputType = NodeInput | Button;
interface Nested {
[key: string]: (Nested & { title?: string }) | InputType;
type SettingsNode = InputType | SettingsGroup;
interface SettingsGroup {
title?: string;
[key: string]: any;
}
type SettingsType = Record<string, Nested>;
type SettingsType = Record<string, SettingsNode>;
type SettingsValue = Record<
string,
Record<string, unknown> | string | number | boolean | number[]
@@ -29,94 +30,122 @@
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: InputType | Nested): v is InputType {
return v && "type" in v;
function isNodeInput(v: SettingsNode | undefined): v is InputType {
return !!v && typeof v === "object" && "type" in v;
}
function getDefaultValue() {
if (key === "") return;
if (key === "title") return;
if (Array.isArray(type[key]?.options)) {
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 type[key]?.options?.indexOf(value?.[key]);
} else {
return anyNode.options.indexOf(value[key]);
}
return 0;
}
}
if (value?.[key] !== undefined) return value?.[key];
if (type[key]?.value !== undefined) return type[key]?.value;
if (isNodeInput(type[key])) {
if (type[key].type === "boolean") return 0;
if (type[key].type === "float") return 0.5;
if (type[key].type === "integer") return 0;
if (type[key].type === "select") 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[id]);
if (depth > 0 && !isNodeInput(type[key])) {
let open = $state(openSections.value[id]);
// Persist <details> open/closed state for groups
if (depth > 0 && !isNodeInput(type[key!])) {
$effect(() => {
if (open !== undefined) {
openSections[id] = open;
openSections.value[id] = open;
}
});
}
// Sync internalValue back into `value`
$effect(() => {
if (key === "" || internalValue === undefined) return;
const node = type[key];
if (
isNodeInput(type[key]) &&
Array.isArray(type[key]?.options) &&
isNodeInput(node) &&
Array.isArray((node as any).options) &&
typeof internalValue === "number"
) {
value[key] = type[key].options?.[internalValue];
value[key] = (node as any)?.options?.[internalValue] as any;
} else {
value[key] = internalValue;
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={() => console.log(type[key])}>
<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}
{#each Object.keys(type ?? {}).filter((key) => key !== "title") as childKey}
<!-- Root: iterate over top-level keys -->
{#each Object.keys(type ?? {}).filter((k) => k !== "title") as childKey}
<NestedSettings
id={`${id}.${childKey}`}
key={childKey}
{value}
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]?.title || key}</p></summary>
<summary><p>{(type[key] as SettingsGroup).title || key}</p></summary>
<div class="content">
{#each Object.keys(type[key]).filter((key) => key !== "title") as childKey}
{#each Object.keys(type[key] as SettingsGroup).filter((k) => k !== "title") as childKey}
<NestedSettings
id={`${id}.${childKey}`}
key={childKey}
value={value[key] as SettingsValue}
type={type[key] as SettingsType}
bind:value={value[key] as SettingsValue}
type={type[key] as unknown as SettingsType}
depth={depth + 1}
/>
{/each}
@@ -156,6 +185,7 @@
flex-direction: row;
align-items: center;
}
.input-boolean > label {
order: 2;
}
@@ -166,6 +196,10 @@
padding-bottom: 1px;
}
button {
cursor: pointer;
}
hr {
position: absolute;
margin: 0;

View File

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

View File

@@ -1,13 +1,27 @@
import type { NodeInput } from "@nodes/types";
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;
export interface SettingsType {
[key: string]: (SettingsType & { title?: string }) | InputType;
type SettingsNode = InputType | SettingsGroup;
export interface SettingsGroup {
title?: string;
[key: string]: SettingsNode | string | number | undefined;
}
export type SettingsStore = {
[key: string]: SettingsStore | string | number | boolean
};
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

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

View File

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

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import type { Node } from "@nodes/types";
import type { GraphManager } from "$lib/graph-interface/graph-manager";
import type { NodeInstance } from "@nodarium/types";
import type { GraphManager } from "$lib/graph-interface/graph-manager.svelte";
import ActiveNodeSelected from "./ActiveNodeSelected.svelte";
type Props = {
manager: GraphManager;
node: Node | undefined;
node: NodeInstance | undefined;
};
const { manager, node }: Props = $props();

View File

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

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
import Grid from "$lib/grid";
import GraphInterface from "$lib/graph-interface";
import * as templates from "$lib/graph-templates";
import type { Graph, Node } from "@nodes/types";
import type { Graph, NodeInstance } from "@nodarium/types";
import Viewer from "$lib/result-viewer/Viewer.svelte";
import {
appSettings,
@@ -23,40 +23,40 @@
WorkerRuntimeExecutor,
MemoryRuntimeExecutor,
} from "$lib/runtime";
import { IndexDBCache, RemoteNodeRegistry } from "@nodes/registry";
import { createPerformanceStore } from "@nodes/utils";
import { IndexDBCache, RemoteNodeRegistry } from "@nodarium/registry";
import { createPerformanceStore } from "@nodarium/utils";
import BenchmarkPanel from "$lib/sidebar/panels/BenchmarkPanel.svelte";
import { debounceAsyncFunction } from "$lib/helpers";
import { onMount } from "svelte";
import GraphSource from "$lib/sidebar/panels/GraphSource.svelte";
let performanceStore = createPerformanceStore();
const registryCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry(
"https://node-store.app.max-richter.dev",
/// "http://localhost:8000",
registryCache,
);
const nodeRegistry = new RemoteNodeRegistry("", registryCache);
const workerRuntime = new WorkerRuntimeExecutor();
const runtimeCache = new MemoryRuntimeCache();
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
memoryRuntime.perf = performanceStore;
const runtime = $derived(
appSettings.debug.useWorker ? workerRuntime : memoryRuntime,
appSettings.value.debug.useWorker ? workerRuntime : memoryRuntime,
);
let activeNode = $state<Node | undefined>(undefined);
let activeNode = $state<NodeInstance | undefined>(undefined);
let scene = $state<Group>(null!);
let graph = localStorage.getItem("graph")
let graph = $state(
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 viewerComponent = $state<ReturnType<typeof Viewer>>();
const manager = $derived(graphInterface?.manager);
const managerStatus = $derived(manager?.status);
async function randomGenerate() {
if (!manager) return;
@@ -69,23 +69,36 @@
{
key: "r",
description: "Regenerate the plant model",
callback: randomGenerate,
callback: () => randomGenerate(),
},
]);
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(
async (g: Graph, s: Record<string, any> = graphSettings) => {
async function update(
g: Graph,
s: Record<string, any> = $state.snapshot(graphSettings),
) {
performanceStore.startRun();
try {
let a = performance.now();
const graphResult = await runtime.execute(g, $state.snapshot(s));
const graphResult = await runtime.execute(g, s);
let b = performance.now();
if (appSettings.debug.useWorker) {
if (appSettings.value.debug.useWorker) {
let perfData = await runtime.getPerformanceData();
let lastRun = perfData?.at(-1);
if (lastRun?.total) {
@@ -98,49 +111,48 @@
);
}
}
viewerComponent?.update(graphResult);
} catch (error) {
console.log("errors", error);
} finally {
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>
<svelte:document on:keydown={applicationKeymap.handleKeyboardEvent} />
<div class="wrapper manager-{$managerStatus}">
<svelte:document onkeydown={applicationKeymap.handleKeyboardEvent} />
<div class="wrapper manager-{manager?.status}">
<header></header>
<Grid.Row>
<Grid.Cell>
@@ -148,19 +160,18 @@
bind:scene
bind:this={viewerComponent}
perf={performanceStore}
centerCamera={appSettings.centerCamera}
centerCamera={appSettings.value.centerCamera}
/>
</Grid.Cell>
<Grid.Cell>
{#key graph}
<GraphInterface
bind:this={graphInterface}
{graph}
bind:this={graphInterface}
registry={nodeRegistry}
showGrid={appSettings.nodeInterface.showNodeGrid}
snapToGrid={appSettings.nodeInterface.snapToGrid}
showGrid={appSettings.value.nodeInterface.showNodeGrid}
snapToGrid={appSettings.value.nodeInterface.snapToGrid}
bind:activeNode
bind:showHelp={appSettings.nodeInterface.showHelp}
bind:showHelp={appSettings.value.nodeInterface.showHelp}
bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes}
onresult={(result) => handleUpdate(result)}
@@ -170,7 +181,7 @@
<Panel id="general" title="General" icon="i-tabler-settings">
<NestedSettings
id="general"
value={appSettings}
bind:value={appSettings.value}
type={AppSettingTypes}
/>
</Panel>
@@ -182,7 +193,7 @@
<Keymap
keymaps={[
{ keymap: applicationKeymap, title: "Application" },
{ keymap: graphInterface.keymap, title: "Node-Editor" },
{ keymap: graphInterface?.keymap, title: "Node-Editor" },
]}
/>
</Panel>
@@ -201,18 +212,26 @@
id="performance"
title="Performance"
classes="text-red-400"
hidden={!appSettings.debug.showPerformancePanel}
hidden={!appSettings.value.debug.showPerformancePanel}
icon="i-tabler-brand-speedtest"
>
{#if $performanceStore}
<PerformanceViewer data={$performanceStore} />
{/if}
</Panel>
<Panel
id="graph-source"
title="Graph Source"
hidden={!appSettings.value.debug.showGraphJson}
icon="i-tabler-code"
>
<GraphSource {graph} />
</Panel>
<Panel
id="benchmark"
title="Benchmark"
classes="text-red-400"
hidden={!appSettings.debug.showBenchmarkPanel}
hidden={!appSettings.value.debug.showBenchmarkPanel}
icon="i-tabler-graph"
>
<BenchmarkPanel run={randomGenerate} />
@@ -238,14 +257,12 @@
<ActiveNodeSettings {manager} node={activeNode} />
</Panel>
</Sidebar>
{/key}
</Grid.Cell>
</Grid.Row>
</div>
<style>
header {
/* border-bottom: solid thin var(--outline); */
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 type { EntryGenerator } from "../$types";
export const prerender = true;
export const entries: EntryGenerator = async () => {
const users = await registry.getUsers();
return users.map(user => {
return user.collections.map(collection => {
return collection.nodes.map(node => {
return { user: user.id, collection: collection.id.split("/")[1], node: node.id.split("/")[2] }
return users
.map((user) => {
return user.collections.map((collection) => {
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 }) {
const wasm = await registry.getWasm(`${params.user}/${params.collection}/${params.node}`);
const wasm = await registry.getWasm(
`${params.user}/${params.collection}/${params.node}`,
);
if (!wasm) {
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

@@ -8,5 +8,5 @@
"build:deploy": "pnpm build",
"dev": "pnpm -r --filter 'app' --filter './packages/node-registry' dev"
},
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c"
"packageManager": "pnpm@10.24.0"
}

View File

@@ -1,5 +1,5 @@
{
"name": "@nodes/registry",
"name": "@nodarium/registry",
"version": "0.0.0",
"description": "",
"main": "src/index.ts",
@@ -10,8 +10,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"@nodes/types": "link:../types",
"@nodes/utils": "link:../utils",
"idb": "^8.0.0"
"@nodarium/types": "link:../types",
"@nodarium/utils": "link:../utils",
"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';
export class IndexDBCache implements AsyncCache<ArrayBuffer> {

View File

@@ -1,76 +1,79 @@
import { NodeDefinitionSchema, type AsyncCache, type NodeDefinition, type NodeRegistry } from "@nodes/types";
import { createLogger, createWasmWrapper } from "@nodes/utils";
import {
NodeDefinitionSchema,
type AsyncCache,
type NodeDefinition,
type NodeRegistry,
} from "@nodarium/types";
import { createLogger, createWasmWrapper } from "@nodarium/utils";
const log = createLogger("node-registry");
log.mute();
export class RemoteNodeRegistry implements NodeRegistry {
status: "loading" | "ready" | "error" = "loading";
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, private cache?: AsyncCache<ArrayBuffer>) { }
async fetchUsers() {
const response = await this.fetch(`${this.url}/nodes/users.json`);
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();
}
async fetchUser(userId: `${string}`) {
const response = await this.fetch(`${this.url}/user/${userId}.json`);
async fetchArrayBuffer(url: string) {
const response = await fetch(`${this.url}/${url}`);
if (!response.ok) {
throw new Error(`Failed to load user ${userId}`);
}
return response.json();
log.error(`Failed to load ${url}`, { response, url, host: this.url });
throw new Error(`Failed to load ${url}`);
}
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 fetchNode = async () => {
const response = await this.fetch(`${this.url}/nodes/${nodeId}.wasm`);
return response.arrayBuffer();
}
const res = await Promise.race([
fetchNode(),
this.cache?.get(nodeId)
]);
constructor(
private url: string,
private cache?: AsyncCache<ArrayBuffer>,
) { }
if (!res) {
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 res;
return node;
}
async load(nodeIds: `${string}/${string}/${string}`[]) {
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)) {
return this.nodes.get(id)!;
}
@@ -78,9 +81,8 @@ export class RemoteNodeRegistry implements NodeRegistry {
const wasmBuffer = await this.fetchNodeWasm(id);
return this.register(wasmBuffer);
}));
}),
);
const duration = performance.now() - a;
@@ -90,11 +92,10 @@ export class RemoteNodeRegistry implements NodeRegistry {
log.groupEnd();
this.status = "ready";
return nodes
return nodes;
}
async register(wasmBuffer: ArrayBuffer) {
const wrapper = createWasmWrapper(wasmBuffer);
const definition = NodeDefinitionSchema.safeParse(wrapper.get_definition());
@@ -110,8 +111,8 @@ export class RemoteNodeRegistry implements NodeRegistry {
let node = {
...definition.data,
execute: wrapper.execute
}
execute: wrapper.execute,
};
this.nodes.set(definition.data.id, node);

View File

@@ -11,9 +11,9 @@
"author": "",
"license": "ISC",
"dependencies": {
"@hey-api/client-fetch": "^0.5.6"
"@hey-api/client-fetch": "^0.13.1"
},
"devDependencies": {
"@hey-api/openapi-ts": "^0.60.0"
"@hey-api/openapi-ts": "^0.88.0"
}
}

View File

@@ -1,5 +1,5 @@
{
"name": "@nodes/types",
"name": "@nodarium/types",
"version": "0.0.0",
"description": "",
"main": "src/index.ts",
@@ -13,6 +13,6 @@
"author": "",
"license": "ISC",
"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, NodeId } from "./types";
export interface NodeRegistry {
/**
@@ -32,9 +32,6 @@ export interface NodeRegistry {
* @returns The node definition
*/
register: (wasmBuffer: ArrayBuffer) => Promise<NodeDefinition>;
cache?: AsyncCache<ArrayBuffer>;
}
export interface RuntimeExecutor {
@@ -43,7 +40,10 @@ export interface RuntimeExecutor {
* @param graph - The graph to execute
* @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> {
@@ -69,7 +69,6 @@ export interface SyncCache<T = unknown> {
* Clear the cache
*/
clear: () => void;
}
export interface AsyncCache<T = unknown> {

View File

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

View File

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

View File

@@ -1,27 +1,19 @@
import { z } from "zod";
import { NodeInputSchema } from "./inputs";
export type NodeId = `${string}/${string}/${string}`;
export const NodeIdSchema = z
.string()
.regex(/^[^/]+\/[^/]+\/[^/]+$/, "Invalid NodeId format")
.transform((value) => value as `${string}/${string}/${string}`);
export const NodeSchema = z.object({
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 NodeId = z.infer<typeof NodeIdSchema>;
export type Node = {
tmp?: {
export type NodeRuntimeState = {
depth?: number;
mesh?: any;
random?: number;
parents?: Node[],
children?: Node[],
inputNodes?: Record<string, Node>
parents?: NodeInstance[];
children?: NodeInstance[];
inputNodes?: Record<string, NodeInstance>;
type?: NodeDefinition;
downX?: number;
downY?: number;
@@ -30,40 +22,65 @@ export type Node = {
ref?: HTMLElement;
visible?: boolean;
isMoving?: boolean;
}
} & z.infer<typeof NodeSchema>;
};
export type NodeInstance = {
state: NodeRuntimeState;
} & SerializedNode;
export const NodeDefinitionSchema = z.object({
id: z.string(),
inputs: z.record(NodeInputSchema).optional(),
id: NodeIdSchema,
inputs: z.record(z.string(), NodeInputSchema).optional(),
outputs: z.array(z.string()).optional(),
meta: z.object({
meta: z
.object({
description: z.string().optional(),
title: z.string().optional(),
}).optional(),
})
.optional(),
});
export const NodeSchema = z.object({
id: z.number(),
type: NodeIdSchema,
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 SerializedNode = z.infer<typeof NodeSchema>;
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & {
execute(input: Int32Array): Int32Array;
};
export type Socket = {
node: Node;
node: NodeInstance;
index: number | string;
position: [number, number];
};
export type Edge = [Node, number, Node, string];
export type Edge = [NodeInstance, number, NodeInstance, string];
export const GraphSchema = z.object({
id: z.number().optional(),
meta: z.object({
id: z.number(),
meta: z
.object({
title: 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),
edges: z.array(z.tuple([z.number(), z.number(), z.number(), z.string()])),
});
export type Graph = z.infer<typeof GraphSchema> & { nodes: Node[] };
export type Graph = z.infer<typeof GraphSchema>;

View File

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

View File

@@ -3,12 +3,13 @@
title?: string;
transparent?: boolean;
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>
<details class:transparent>
<details class:transparent bind:open>
<summary>{title}</summary>
<div class="content">
{@render children?.()}
@@ -33,7 +34,4 @@
outline: none;
}
.content {
/* padding-left: 12px; */
}
</style>

View File

@@ -4,7 +4,7 @@
import Integer from './elements/Integer.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';
interface Props {
@@ -27,4 +27,3 @@
{:else if input.type === 'vec3'}
<Vec3 {id} bind:value />
{/if}

View File

@@ -3,15 +3,10 @@
ctrl?: boolean;
shift?: boolean;
alt?: boolean;
key: string;
key: string | string[];
}
let {
ctrl = false,
shift = false,
alt = false,
key
}: Props = $props();
let { ctrl = false, shift = false, alt = false, key }: Props = $props();
</script>
<div class="command">
@@ -38,7 +33,7 @@
}
span::after {
content: " +";
content: ' +';
opacity: 0.5;
}
</style>

View File

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

View File

@@ -1,5 +1,5 @@
import { test, expect } from 'vitest';
import { fastHashArray, fastHashString } from './fastHash';
import { fastHashArrayBuffer, fastHashString } from './fastHash';
test('fastHashString doesnt produce clashes', () => {
const hashA = fastHashString('abcdef');
@@ -14,10 +14,10 @@ test("fastHashArray doesnt product collisions", () => {
const a = new Int32Array(1000);
const hash_a = fastHashArray(a.buffer);
const hash_a = fastHashArrayBuffer(a);
a[0] = 1;
const hash_b = fastHashArray(a.buffer);
const hash_b = fastHashArrayBuffer(a);
expect(hash_a).not.toEqual(hash_b);
@@ -28,13 +28,13 @@ test('fastHashArray is fast(ish) < 20ms', () => {
const a = new Int32Array(10_000);
const t0 = performance.now();
fastHashArray(a.buffer);
fastHashArrayBuffer(a);
const t1 = performance.now();
a[0] = 1;
fastHashArray(a.buffer);
fastHashArrayBuffer(a);
const t2 = performance.now();
@@ -48,7 +48,7 @@ test('fastHashArray is deterministic', () => {
a[42] = 69;
const b = new Int32Array(1000);
b[42] = 69;
const hashA = fastHashArray(a.buffer);
const hashB = fastHashArray(b.buffer);
const hashA = fastHashArrayBuffer(a);
const hashB = fastHashArrayBuffer(b);
expect(hashA).toEqual(hashB);
});

View File

@@ -1,94 +1,45 @@
export function fastHashArrayBuffer(input: string | Int32Array): string {
const mask = (1n << 64n) - 1n
// https://github.com/6502/sha256/blob/main/sha256.js
function sha256(data?: string | Uint8Array) {
let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a,
h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19,
tsz = 0, bp = 0;
const k = [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, n) => (x >>> n) | (x << (32 - n)),
w = new Uint32Array(64),
buf = new Uint8Array(64),
process = () => {
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];
}
for (let j = 16; j < 64; j++) {
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);
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;
for (let j = 0; j < 64; j++) {
let S1 = rrot(e, 6) ^ rrot(e, 11) ^ rrot(e, 25),
ch = (e & f) ^ ((~e) & g),
t1 = (h + S1 + ch + k[j] + w[j]) | 0,
S0 = rrot(a, 2) ^ rrot(a, 13) ^ rrot(a, 22),
maj = (a & b) ^ (a & c) ^ (b & c),
t2 = (S0 + maj) | 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;
h4 = (h4 + e) | 0; h5 = (h5 + f) | 0; h6 = (h6 + g) | 0; h7 = (h7 + h) | 0;
bp = 0;
},
add = data => {
if (typeof data === "string") {
data = typeof TextEncoder === "undefined" ? Buffer.from(data) : (new TextEncoder).encode(data);
}
for (let i = 0; i < data.length; i++) {
buf[bp++] = data[i];
if (bp === 64) process();
}
tsz += data.length;
},
digest = () => {
buf[bp++] = 0x80; if (bp == 64) process();
if (bp + 8 > 64) {
while (bp < 64) buf[bp++] = 0x00;
process();
}
while (bp < 58) buf[bp++] = 0x00;
// Max number of bytes is 35,184,372,088,831
let L = tsz * 8;
buf[bp++] = (L / 1099511627776.) & 255;
buf[bp++] = (L / 4294967296.) & 255;
buf[bp++] = L >>> 24;
buf[bp++] = (L >>> 16) & 255;
buf[bp++] = (L >>> 8) & 255;
buf[bp++] = L & 255;
process();
let reply = new Uint8Array(32);
reply[0] = h0 >>> 24; reply[1] = (h0 >>> 16) & 255; reply[2] = (h0 >>> 8) & 255; reply[3] = h0 & 255;
reply[4] = h1 >>> 24; reply[5] = (h1 >>> 16) & 255; reply[6] = (h1 >>> 8) & 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 = "";
reply.forEach(x => res += ("0" + x.toString(16)).slice(-2));
return res;
};
// FNV-1a 64-bit constants
let h = 0xcbf29ce484222325n // offset basis
const FNV_PRIME = 0x100000001b3n
if (data) add(data);
return { add, digest };
// get bytes for string or Int32Array
let bytes: Uint8Array
if (typeof input === "string") {
// utf-8 encoding
bytes = new TextEncoder().encode(input)
} else {
// Int32Array -> bytes (little-endian)
bytes = new Uint8Array(input.length * 4)
for (let i = 0; i < input.length; i++) {
const v = input[i] >>> 0 // ensure unsigned 32-bit
const base = i * 4
bytes[base] = v & 0xff
bytes[base + 1] = (v >>> 8) & 0xff
bytes[base + 2] = (v >>> 16) & 0xff
bytes[base + 3] = (v >>> 24) & 0xff
}
}
export function fastHashArrayBuffer(buffer: ArrayBuffer): string {
return sha256(new Uint8Array(buffer)).digest();
// FNV-1a byte-wise
for (let i = 0; i < bytes.length; i++) {
h = (h ^ BigInt(bytes[i])) & mask
h = (h * FNV_PRIME) & mask
}
// MurmurHash3's fmix64 finalizer (good avalanche)
h ^= h >> 33n
h = (h * 0xff51afd7ed558ccdn) & mask
h ^= h >> 33n
h = (h * 0xc4ceb9fe1a85ec53n) & mask
h ^= h >> 33n
// to 16-char hex
return h.toString(16).padStart(16, "0").slice(-16)
}
// Shamelessly copied from
// https://stackoverflow.com/a/8831937
export function fastHashString(input: string) {
if (input.length === 0) return 0;
@@ -100,23 +51,3 @@ export function fastHashString(input: string) {
return hash;
}
export function fastHash(input: (string | Int32Array | number)[]) {
const s = sha256();
for (let i = 0; i < input.length; i++) {
const v = input[i]
if (typeof v === "string") {
s.add(v);
} else if (v instanceof Int32Array) {
s.add(new Uint8Array(v.buffer));
} else {
s.add(v.toString());
}
}
return s.digest()
}

View File

@@ -1,7 +1,8 @@
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;
for (let i = 0; i < input.length; i++) {
const item = input[i];
@@ -36,7 +37,7 @@ export function concatEncodedArrays(input: (number | number[] | Int32Array)[]):
result[totalLength - 2] = 1;
result[totalLength - 1] = 1;
return result
return result;
}
// 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];
};
}
function decode_recursive(dense: number[] | Int32Array, index = 0) {
if (dense instanceof Int32Array) {
dense = Array.from(dense)
dense = Array.from(dense);
}
const decoded: (number | number[])[] = [];
@@ -82,12 +82,17 @@ function decode_recursive(dense: number[] | Int32Array, index = 0) {
index += 2; // Skip the initial bracket notation
while (index < dense.length) {
if (index === nextBracketIndex) {
if (dense[index] === 0) { // Opening bracket detected
const [p, nextIndex, _nextBracketIndex] = decode_recursive(dense, index);
decoded.push(p);
if (dense[index] === 0) {
// Opening bracket detected
const [p, nextIndex, _nextBracketIndex] = decode_recursive(
dense,
index,
);
decoded.push(...p);
index = nextIndex + 1;
nextBracketIndex = _nextBracketIndex;
} else { // Closing bracket detected
} else {
// Closing bracket detected
nextBracketIndex = dense[index + 1] + index + 1;
return [decoded, index, nextBracketIndex] as const;
}
@@ -103,7 +108,6 @@ export function decodeNestedArray(dense: number[] | Int32Array) {
return decode_recursive(dense, 0)[0];
}
export function splitNestedArray(input: Int32Array) {
let index = 0;
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 encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
const encodeString =
typeof cachedTextEncoder.encodeInto === "function"
? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
}
@@ -13,9 +17,9 @@ const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
view.set(buf);
return {
read: arg.length,
written: buf.length
written: buf.length,
};
};
});
function createWrapper() {
let wasm: any;
@@ -53,7 +57,9 @@ function createWrapper() {
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) {
if (heap_next === heap.length) heap.push(heap.length + 1);
@@ -63,9 +69,11 @@ function createWrapper() {
return idx;
}
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;
getUint32Memory0().set(arg, ptr / 4);
WASM_VECTOR_LEN = arg.length;
@@ -89,21 +97,10 @@ function createWrapper() {
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) {
const ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
};
}
// Additional methods and their internal helpers can also be refactored in a similar manner.
function get_definition() {
@@ -124,7 +121,6 @@ function createWrapper() {
}
}
function execute(args: Int32Array) {
try {
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) {
const buf = cachedTextEncoder.encode(arg);
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;
return ptr;
}
@@ -160,7 +163,7 @@ function createWrapper() {
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
if (code > 0x7f) break;
mem[ptr + offset] = code;
}
@@ -168,7 +171,7 @@ function createWrapper() {
if (offset !== 0) {
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 ret = encodeString(arg, view);
@@ -183,15 +186,19 @@ function createWrapper() {
function __wbg_new_abda76e883ba8a5f() {
const ret = new Error();
return addHeapObject(ret);
};
}
function __wbg_stack_658279fe44541cf6(arg0, arg1) {
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;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
};
}
function __wbg_error_f851667af71bcfc6(arg0, arg1) {
let deferred0_0;
@@ -203,27 +210,25 @@ function createWrapper() {
} finally {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
};
}
function __wbindgen_object_drop_ref(arg0) {
takeObject(arg0);
};
}
function __wbg_log_5bb5f88f245d7762(arg0) {
console.log(getObject(arg0));
};
}
function __wbindgen_throw(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
}
return {
setInstance(instance: WebAssembly.Instance) {
wasm = instance.exports;
},
exports: {
// Expose other methods that interact with the wasm instance
execute,
@@ -240,11 +245,12 @@ function createWrapper() {
};
}
export function createWasmWrapper(wasmBuffer: ArrayBuffer) {
export function createWasmWrapper(wasmBuffer: ArrayBuffer | Uint8Array) {
const wrapper = createWrapper();
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);
return wrapper.exports;
}

5968
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff