24 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
73 changed files with 1909 additions and 1994 deletions

View File

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

View File

@@ -1,5 +1,5 @@
{ {
"name": "@nodes/app", "name": "@nodarium/app",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
@@ -10,9 +10,9 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@nodes/registry": "link:../packages/registry", "@nodarium/registry": "link:../packages/registry",
"@nodes/ui": "link:../packages/ui", "@nodarium/ui": "link:../packages/ui",
"@nodes/utils": "link:../packages/utils", "@nodarium/utils": "link:../packages/utils",
"@sveltejs/kit": "^2.49.0", "@sveltejs/kit": "^2.49.0",
"@threlte/core": "8.3.0", "@threlte/core": "8.3.0",
"@threlte/extras": "9.7.0", "@threlte/extras": "9.7.0",
@@ -26,7 +26,7 @@
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/tabler": "^1.2.23", "@iconify-json/tabler": "^1.2.23",
"@nodes/types": "link:../packages/types", "@nodarium/types": "link:../packages/types",
"@sveltejs/adapter-static": "^3.0.10", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/vite-plugin-svelte": "^6.2.1", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tsconfig/svelte": "^5.0.6", "@tsconfig/svelte": "^5.0.6",

View File

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

View File

@@ -1,21 +1,25 @@
<script lang="ts"> <script lang="ts">
import type { GraphManager } from "./graph-manager.svelte";
import { HTML } from "@threlte/extras"; import { HTML } from "@threlte/extras";
import { onMount } from "svelte"; import { onMount } from "svelte";
import type { NodeType } from "@nodes/types"; import type { NodeInstance, NodeId } from "@nodarium/types";
import { getGraphManager, getGraphState } from "../graph/state.svelte";
type Props = { type Props = {
position: [x: number, y: number] | null; onnode: (n: NodeInstance) => void;
graph: GraphManager;
}; };
let { position = $bindable(), graph }: Props = $props(); const { onnode }: Props = $props();
const graph = getGraphManager();
const graphState = getGraphState();
let input: HTMLInputElement; let input: HTMLInputElement;
let value = $state<string>(); let value = $state<string>();
let activeNodeId = $state<NodeType>(); let activeNodeId = $state<NodeId>();
const allNodes = graph.getNodeDefinitions(); const allNodes = graphState.activeSocket
? graph.getPossibleNodes(graphState.activeSocket)
: graph.getNodeDefinitions();
function filterNodes() { function filterNodes() {
return allNodes.filter((node) => node.id.includes(value ?? "")); return allNodes.filter((node) => node.id.includes(value ?? ""));
@@ -25,7 +29,7 @@
$effect(() => { $effect(() => {
if (nodes) { if (nodes) {
if (activeNodeId === undefined) { if (activeNodeId === undefined) {
activeNodeId = nodes[0].id; activeNodeId = nodes?.[0]?.id;
} else if (nodes.length) { } else if (nodes.length) {
const node = nodes.find((node) => node.id === activeNodeId); const node = nodes.find((node) => node.id === activeNodeId);
if (!node) { if (!node) {
@@ -35,11 +39,22 @@
} }
}); });
function handleNodeCreation(nodeType: NodeInstance["type"]) {
if (!graphState.addMenuPosition) return;
onnode?.({
id: -1,
type: nodeType,
position: [...graphState.addMenuPosition],
props: {},
state: {},
});
}
function handleKeyDown(event: KeyboardEvent) { function handleKeyDown(event: KeyboardEvent) {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
if (event.key === "Escape") { if (event.key === "Escape") {
position = null; graphState.addMenuPosition = null;
return; return;
} }
@@ -56,9 +71,8 @@
} }
if (event.key === "Enter") { if (event.key === "Enter") {
if (activeNodeId && position) { if (activeNodeId && graphState.addMenuPosition) {
graph.createNode({ type: activeNodeId, position, props: {} }); handleNodeCreation(activeNodeId);
position = null;
} }
return; return;
} }
@@ -70,7 +84,11 @@
}); });
</script> </script>
<HTML position.x={position?.[0]} position.z={position?.[1]} transform={false}> <HTML
position.x={graphState.addMenuPosition?.[0]}
position.z={graphState.addMenuPosition?.[1]}
transform={false}
>
<div class="add-menu-wrapper"> <div class="add-menu-wrapper">
<div class="header"> <div class="header">
<input <input
@@ -95,18 +113,10 @@
aria-selected={node.id === activeNodeId} aria-selected={node.id === activeNodeId}
onkeydown={(event) => { onkeydown={(event) => {
if (event.key === "Enter") { if (event.key === "Enter") {
if (position) { handleNodeCreation(node.id);
graph.createNode({ type: node.id, position, props: {} });
position = null;
}
}
}}
onmousedown={() => {
if (position) {
graph.createNode({ type: node.id, position, props: {} });
position = null;
} }
}} }}
onmousedown={() => handleNodeCreation(node.id)}
onfocus={() => { onfocus={() => {
activeNodeId = node.id; activeNodeId = node.id;
}} }}

View File

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

View File

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

@@ -1,21 +1,22 @@
import type { import type {
Edge, Edge,
Graph, Graph,
Node, NodeInstance,
NodeDefinition,
NodeInput, NodeInput,
NodeRegistry, NodeRegistry,
NodeType, NodeId,
Socket, Socket,
} from "@nodes/types"; } from "@nodarium/types";
import { fastHashString } from "@nodes/utils"; import { fastHashString } from "@nodarium/utils";
import { SvelteMap } from "svelte/reactivity"; import { SvelteMap } from "svelte/reactivity";
import EventEmitter from "./helpers/EventEmitter"; import EventEmitter from "./helpers/EventEmitter";
import { createLogger } from "@nodes/utils"; import { createLogger } from "@nodarium/utils";
import throttle from "$lib/helpers/throttle"; import throttle from "$lib/helpers/throttle";
import { HistoryManager } from "./history-manager"; import { HistoryManager } from "./history-manager";
const logger = createLogger("graph-manager"); const logger = createLogger("graph-manager");
// logger.mute(); logger.mute();
const clone = const clone =
"structuredClone" in self "structuredClone" in self
@@ -32,6 +33,27 @@ function areSocketsCompatible(
return inputs === 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<{ export class GraphManager extends EventEmitter<{
save: Graph; save: Graph;
result: any; result: any;
@@ -46,7 +68,7 @@ export class GraphManager extends EventEmitter<{
graph: Graph = { id: 0, nodes: [], edges: [] }; graph: Graph = { id: 0, nodes: [], edges: [] };
id = $state(0); id = $state(0);
nodes = new SvelteMap<number, Node>(); nodes = new SvelteMap<number, NodeInstance>();
edges = $state<Edge[]>([]); edges = $state<Edge[]>([]);
@@ -79,7 +101,7 @@ export class GraphManager extends EventEmitter<{
position: [...node.position], position: [...node.position],
type: node.type, type: node.type,
props: node.props, props: node.props,
})) as Node[]; })) as NodeInstance[];
const edges = this.edges.map((edge) => [ const edges = this.edges.map((edge) => [
edge[0].id, edge[0].id,
edge[1], edge[1],
@@ -88,7 +110,7 @@ export class GraphManager extends EventEmitter<{
]) as Graph["edges"]; ]) as Graph["edges"];
const serialized = { const serialized = {
id: this.graph.id, id: this.graph.id,
settings: this.settings, settings: $state.snapshot(this.settings),
nodes, nodes,
edges, edges,
}; };
@@ -111,14 +133,14 @@ export class GraphManager extends EventEmitter<{
return this.registry.getAllNodes(); return this.registry.getAllNodes();
} }
getLinkedNodes(node: Node) { getLinkedNodes(node: NodeInstance) {
const nodes = new Set<Node>(); const nodes = new Set<NodeInstance>();
const stack = [node]; const stack = [node];
while (stack.length) { while (stack.length) {
const n = stack.pop(); const n = stack.pop();
if (!n) continue; if (!n) continue;
nodes.add(n); nodes.add(n);
const children = this.getChildrenOfNode(n); const children = this.getChildren(n);
const parents = this.getParentsOfNode(n); const parents = this.getParentsOfNode(n);
const newNodes = [...children, ...parents].filter((n) => !nodes.has(n)); const newNodes = [...children, ...parents].filter((n) => !nodes.has(n));
stack.push(...newNodes); stack.push(...newNodes);
@@ -126,10 +148,10 @@ export class GraphManager extends EventEmitter<{
return [...nodes.values()]; return [...nodes.values()];
} }
getEdgesBetweenNodes(nodes: Node[]): [number, number, number, string][] { getEdgesBetweenNodes(nodes: NodeInstance[]): [number, number, number, string][] {
const edges = []; const edges = [];
for (const node of nodes) { for (const node of nodes) {
const children = node.tmp?.children || []; const children = node.state?.children || [];
for (const child of children) { for (const child of children) {
if (nodes.includes(child)) { if (nodes.includes(child)) {
const edge = this.edges.find( const edge = this.edges.find(
@@ -152,15 +174,15 @@ export class GraphManager extends EventEmitter<{
private _init(graph: Graph) { private _init(graph: Graph) {
const nodes = new Map( const nodes = new Map(
graph.nodes.map((node: Node) => { graph.nodes.map((node) => {
const nodeType = this.registry.getNode(node.type); const nodeType = this.registry.getNode(node.type);
const n = node as NodeInstance;
if (nodeType) { if (nodeType) {
node.tmp = { n.state = {
random: (Math.random() - 0.5) * 2,
type: nodeType, type: nodeType,
}; };
} }
return [node.id, node]; return [node.id, n];
}), }),
); );
@@ -170,12 +192,10 @@ export class GraphManager extends EventEmitter<{
if (!from || !to) { if (!from || !to) {
throw new Error("Edge references non-existing node"); throw new Error("Edge references non-existing node");
} }
from.tmp = from.tmp || {}; from.state.children = from.state.children || [];
from.tmp.children = from.tmp.children || []; from.state.children.push(to);
from.tmp.children.push(to); to.state.parents = to.state.parents || [];
to.tmp = to.tmp || {}; to.state.parents.push(from);
to.tmp.parents = to.tmp.parents || [];
to.tmp.parents.push(from);
return [from, edge[1], to, edge[3]] as Edge; return [from, edge[1], to, edge[3]] as Edge;
}); });
@@ -211,9 +231,10 @@ export class GraphManager extends EventEmitter<{
this.status = "error"; this.status = "error";
return; return;
} }
node.tmp = node.tmp || {}; // Turn into runtime node
node.tmp.random = (Math.random() - 0.5) * 2; const n = node as NodeInstance;
node.tmp.type = nodeType; n.state = {};
n.state.type = nodeType;
} }
// load settings // load settings
@@ -272,7 +293,7 @@ export class GraphManager extends EventEmitter<{
return this.registry.getNode(id); return this.registry.getNode(id);
} }
async loadNode(id: NodeType) { async loadNodeType(id: NodeId) {
await this.registry.load([id]); await this.registry.load([id]);
const nodeType = this.registry.getNode(id); const nodeType = this.registry.getNode(id);
@@ -297,33 +318,32 @@ export class GraphManager extends EventEmitter<{
} }
this.settings = settingValues; this.settings = settingValues;
console.log("GraphManager.setSettings", settingValues);
this.settingTypes = settingTypes; this.settingTypes = settingTypes;
this.emit("settings", { types: settingTypes, values: settingValues }); this.emit("settings", { types: settingTypes, values: settingValues });
} }
getChildrenOfNode(node: Node) { getChildren(node: NodeInstance) {
const children = []; const children = [];
const stack = node.tmp?.children?.slice(0); const stack = node.state?.children?.slice(0);
while (stack?.length) { while (stack?.length) {
const child = stack.pop(); const child = stack.pop();
if (!child) continue; if (!child) continue;
children.push(child); children.push(child);
stack.push(...(child.tmp?.children || [])); stack.push(...(child.state?.children || []));
} }
return children; return children;
} }
getNodesBetween(from: Node, to: Node): Node[] | undefined { getNodesBetween(from: NodeInstance, to: NodeInstance): NodeInstance[] | undefined {
// < - - - - from // < - - - - from
const toParents = this.getParentsOfNode(to); const toParents = this.getParentsOfNode(to);
// < - - - - from - - - - to // < - - - - from - - - - to
const fromParents = this.getParentsOfNode(from); const fromParents = this.getParentsOfNode(from);
if (toParents.includes(from)) { if (toParents.includes(from)) {
const fromChildren = this.getChildrenOfNode(from); const fromChildren = this.getChildren(from);
return toParents.filter((n) => fromChildren.includes(n)); return toParents.filter((n) => fromChildren.includes(n));
} else if (fromParents.includes(to)) { } else if (fromParents.includes(to)) {
const toChildren = this.getChildrenOfNode(to); const toChildren = this.getChildren(to);
return fromParents.filter((n) => toChildren.includes(n)); return fromParents.filter((n) => toChildren.includes(n));
} else { } else {
// these two nodes are not connected // these two nodes are not connected
@@ -331,7 +351,7 @@ export class GraphManager extends EventEmitter<{
} }
} }
removeNode(node: Node, { restoreEdges = false } = {}) { removeNode(node: NodeInstance, { restoreEdges = false } = {}) {
const edgesToNode = this.edges.filter((edge) => edge[2].id === node.id); const edgesToNode = this.edges.filter((edge) => edge[2].id === node.id);
const edgesFromNode = this.edges.filter((edge) => edge[0].id === node.id); const edgesFromNode = this.edges.filter((edge) => edge[0].id === node.id);
for (const edge of [...edgesToNode, ...edgesFromNode]) { for (const edge of [...edgesToNode, ...edgesFromNode]) {
@@ -344,8 +364,8 @@ export class GraphManager extends EventEmitter<{
for (const [to, toSocket] of inputSockets) { for (const [to, toSocket] of inputSockets) {
for (const [from, fromSocket] of outputSockets) { for (const [from, fromSocket] of outputSockets) {
const outputType = from.tmp?.type?.outputs?.[fromSocket]; const outputType = from.state?.type?.outputs?.[fromSocket];
const inputType = to?.tmp?.type?.inputs?.[toSocket]?.type; const inputType = to?.state?.type?.inputs?.[toSocket]?.type;
if (outputType === inputType) { if (outputType === inputType) {
this.createEdge(from, fromSocket, to, toSocket, { this.createEdge(from, fromSocket, to, toSocket, {
applyUpdate: false, applyUpdate: false,
@@ -361,19 +381,32 @@ export class GraphManager extends EventEmitter<{
this.save(); this.save();
} }
createNodeId() { smartConnect(from: NodeInstance, to: NodeInstance): Edge | undefined {
const max = Math.max(0, ...this.nodes.keys()); const inputs = Object.entries(to.state?.type?.inputs ?? {});
return max + 1; const outputs = from.state?.type?.outputs ?? [];
for (let i = 0; i < inputs.length; i++) {
const [inputName, input] = inputs[0];
for (let o = 0; o < outputs.length; o++) {
const output = outputs[0];
if (input.type === output) {
return this.createEdge(from, o, to, inputName);
}
}
}
} }
createGraph(nodes: Node[], edges: [number, number, number, string][]) { createNodeId() {
return Math.max(0, ...this.nodes.keys()) + 1;
}
createGraph(nodes: NodeInstance[], edges: [number, number, number, string][]) {
// map old ids to new ids // map old ids to new ids
const idMap = new Map<number, number>(); const idMap = new Map<number, number>();
const startId = this.createNodeId(); let startId = this.createNodeId()
nodes = nodes.map((node, i) => { nodes = nodes.map((node) => {
const id = startId + i; const id = startId++;
idMap.set(node.id, id); idMap.set(node.id, id);
const type = this.registry.getNode(node.type); const type = this.registry.getNode(node.type);
if (!type) { if (!type) {
@@ -390,13 +423,11 @@ export class GraphManager extends EventEmitter<{
throw new Error("Edge references non-existing node"); throw new Error("Edge references non-existing node");
} }
to.tmp = to.tmp || {}; to.state.parents = to.state.parents || [];
to.tmp.parents = to.tmp.parents || []; to.state.parents.push(from);
to.tmp.parents.push(from);
from.tmp = from.tmp || {}; from.state.children = from.state.children || [];
from.tmp.children = from.tmp.children || []; from.state.children.push(to);
from.tmp.children.push(to);
return [from, edge[1], to, edge[3]] as Edge; return [from, edge[1], to, edge[3]] as Edge;
}); });
@@ -416,9 +447,9 @@ export class GraphManager extends EventEmitter<{
position, position,
props = {}, props = {},
}: { }: {
type: Node["type"]; type: NodeInstance["type"];
position: Node["position"]; position: NodeInstance["position"];
props: Node["props"]; props: NodeInstance["props"];
}) { }) {
const nodeType = this.registry.getNode(type); const nodeType = this.registry.getNode(type);
if (!nodeType) { if (!nodeType) {
@@ -426,26 +457,29 @@ export class GraphManager extends EventEmitter<{
return; return;
} }
const node: Node = { const node: NodeInstance = $state({
id: this.createNodeId(), id: this.createNodeId(),
type, type,
position, position,
tmp: { type: nodeType }, state: { type: nodeType },
props, props,
}; });
this.nodes.set(node.id, node); this.nodes.set(node.id, node);
this.save(); this.save();
return node
} }
createEdge( createEdge(
from: Node, from: NodeInstance,
fromSocket: number, fromSocket: number,
to: Node, to: NodeInstance,
toSocket: string, toSocket: string,
{ applyUpdate = true } = {}, { applyUpdate = true } = {},
) { ): Edge | undefined {
const existingEdges = this.getEdgesToNode(to); const existingEdges = this.getEdgesToNode(to);
// check if this exact edge already exists // check if this exact edge already exists
@@ -458,10 +492,10 @@ export class GraphManager extends EventEmitter<{
} }
// check if socket types match // check if socket types match
const fromSocketType = from.tmp?.type?.outputs?.[fromSocket]; const fromSocketType = from.state?.type?.outputs?.[fromSocket];
const toSocketType = [to.tmp?.type?.inputs?.[toSocket]?.type]; const toSocketType = [to.state?.type?.inputs?.[toSocket]?.type];
if (to.tmp?.type?.inputs?.[toSocket]?.accepts) { if (to.state?.type?.inputs?.[toSocket]?.accepts) {
toSocketType.push(...(to?.tmp?.type?.inputs?.[toSocket]?.accepts || [])); toSocketType.push(...(to?.state?.type?.inputs?.[toSocket]?.accepts || []));
} }
if (!areSocketsCompatible(fromSocketType, toSocketType)) { if (!areSocketsCompatible(fromSocketType, toSocketType)) {
@@ -478,24 +512,21 @@ export class GraphManager extends EventEmitter<{
this.removeEdge(edgeToBeReplaced, { applyDeletion: false }); this.removeEdge(edgeToBeReplaced, { applyDeletion: false });
} }
if (applyUpdate) { const edge = [from, fromSocket, to, toSocket] as Edge;
this.edges.push([from, fromSocket, to, toSocket]);
} else {
this.edges.push([from, fromSocket, to, toSocket]);
}
from.tmp = from.tmp || {}; this.edges.push(edge);
from.tmp.children = from.tmp.children || [];
from.tmp.children.push(to);
to.tmp = to.tmp || {}; from.state.children = from.state.children || [];
to.tmp.parents = to.tmp.parents || []; from.state.children.push(to);
to.tmp.parents.push(from); to.state.parents = to.state.parents || [];
to.state.parents.push(from);
if (applyUpdate) { if (applyUpdate) {
this.save(); this.save();
} }
this.execute(); this.execute();
return edge;
} }
undo() { undo() {
@@ -531,9 +562,9 @@ export class GraphManager extends EventEmitter<{
logger.log("saving graphs", state); logger.log("saving graphs", state);
} }
getParentsOfNode(node: Node) { getParentsOfNode(node: NodeInstance) {
const parents = []; const parents = [];
const stack = node.tmp?.parents?.slice(0); const stack = node.state?.parents?.slice(0);
while (stack?.length) { while (stack?.length) {
if (parents.length > 1000000) { if (parents.length > 1000000) {
logger.warn("Infinite loop detected"); logger.warn("Infinite loop detected");
@@ -542,21 +573,51 @@ export class GraphManager extends EventEmitter<{
const parent = stack.pop(); const parent = stack.pop();
if (!parent) continue; if (!parent) continue;
parents.push(parent); parents.push(parent);
stack.push(...(parent.tmp?.parents || [])); stack.push(...(parent.state?.parents || []));
} }
return parents.reverse(); return parents.reverse();
} }
getPossibleSockets({ node, index }: Socket): [Node, string | number][] { getPossibleNodes(socket: Socket): NodeDefinition[] {
const nodeType = node?.tmp?.type; 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 []; if (!nodeType) return [];
const sockets: [Node, string | number][] = []; const sockets: [NodeInstance, string | number][] = [];
// if index is a string, we are an input looking for outputs // if index is a string, we are an input looking for outputs
if (typeof index === "string") { if (typeof index === "string") {
// filter out self and child nodes // filter out self and child nodes
const children = new Set(this.getChildrenOfNode(node).map((n) => n.id)); const children = new Set(this.getChildren(node).map((n) => n.id));
const nodes = this.getAllNodes().filter( const nodes = this.getAllNodes().filter(
(n) => n.id !== node.id && !children.has(n.id), (n) => n.id !== node.id && !children.has(n.id),
); );
@@ -564,7 +625,7 @@ export class GraphManager extends EventEmitter<{
const ownType = nodeType?.inputs?.[index].type; const ownType = nodeType?.inputs?.[index].type;
for (const node of nodes) { for (const node of nodes) {
const nodeType = node?.tmp?.type; const nodeType = node?.state?.type;
const inputs = nodeType?.outputs; const inputs = nodeType?.outputs;
if (!inputs) continue; if (!inputs) continue;
for (let index = 0; index < inputs.length; index++) { for (let index = 0; index < inputs.length; index++) {
@@ -592,7 +653,7 @@ export class GraphManager extends EventEmitter<{
const ownType = nodeType.outputs?.[index]; const ownType = nodeType.outputs?.[index];
for (const node of nodes) { for (const node of nodes) {
const inputs = node?.tmp?.type?.inputs; const inputs = node?.state?.type?.inputs;
if (!inputs) continue; if (!inputs) continue;
for (const key in inputs) { for (const key in inputs) {
const otherType = [inputs[key].type]; const otherType = [inputs[key].type];
@@ -624,32 +685,30 @@ export class GraphManager extends EventEmitter<{
(e) => (e) =>
e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2, e[0].id === id0 && e[1] === sid0 && e[2].id === id2 && e[3] === sid2,
); );
if (!_edge) return; if (!_edge) return;
edge[0].tmp = edge[0].tmp || {}; if (edge[0].state.children) {
if (edge[0].tmp.children) { edge[0].state.children = edge[0].state.children.filter(
edge[0].tmp.children = edge[0].tmp.children.filter( (n: NodeInstance) => n.id !== id2,
(n: Node) => n.id !== id2,
); );
} }
edge[2].tmp = edge[2].tmp || {}; if (edge[2].state.parents) {
if (edge[2].tmp.parents) { edge[2].state.parents = edge[2].state.parents.filter(
edge[2].tmp.parents = edge[2].tmp.parents.filter( (n: NodeInstance) => n.id !== id0,
(n: Node) => n.id !== id0,
); );
} }
this.edges = this.edges.filter((e) => !areEdgesEqual(e, edge));
if (applyDeletion) { if (applyDeletion) {
this.edges = this.edges.filter((e) => e !== _edge);
this.execute(); this.execute();
this.save(); this.save();
} else {
this.edges = this.edges.filter((e) => e !== _edge);
} }
} }
getEdgesToNode(node: Node) { getEdgesToNode(node: NodeInstance) {
return this.edges return this.edges
.filter((edge) => edge[2].id === node.id) .filter((edge) => edge[2].id === node.id)
.map((edge) => { .map((edge) => {
@@ -658,10 +717,10 @@ export class GraphManager extends EventEmitter<{
if (!from || !to) return; if (!from || !to) return;
return [from, edge[1], to, edge[3]] as const; return [from, edge[1], to, edge[3]] as const;
}) })
.filter(Boolean) as unknown as [Node, number, Node, string][]; .filter(Boolean) as unknown as [NodeInstance, number, NodeInstance, string][];
} }
getEdgesFromNode(node: Node) { getEdgesFromNode(node: NodeInstance) {
return this.edges return this.edges
.filter((edge) => edge[0].id === node.id) .filter((edge) => edge[0].id === node.id)
.map((edge) => { .map((edge) => {
@@ -670,6 +729,6 @@ export class GraphManager extends EventEmitter<{
if (!from || !to) return; if (!from || !to) return;
return [from, edge[1], to, edge[3]] as const; return [from, edge[1], to, edge[3]] as const;
}) })
.filter(Boolean) as unknown as [Node, number, Node, string][]; .filter(Boolean) as unknown as [NodeInstance, number, NodeInstance, string][];
} }
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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,12 +1,70 @@
import type { Socket } from "@nodes/types"; import type { NodeInstance, Socket } from "@nodarium/types";
import { getContext } from "svelte"; import { getContext, setContext } from "svelte";
import { SvelteSet } from "svelte/reactivity"; import { SvelteSet } from "svelte/reactivity";
import type { GraphManager } from "../graph-manager.svelte";
import type { OrthographicCamera } from "three";
const graphStateKey = Symbol("graph-state");
export function getGraphState() { export function getGraphState() {
return getContext<GraphState>("graphState"); return getContext<GraphState>(graphStateKey);
}
export function setGraphState(graphState: GraphState) {
return setContext(graphStateKey, graphState)
}
const graphManagerKey = Symbol("graph-manager");
export function getGraphManager() {
return getContext<GraphManager>(graphManagerKey)
}
export function setGraphManager(manager: GraphManager) {
return setContext(graphManagerKey, manager);
} }
export class GraphState { export class GraphState {
constructor(private graph: GraphManager) { }
width = $state(100);
height = $state(100);
wrapper = $state<HTMLDivElement>(null!);
rect: DOMRect = $derived(
(this.wrapper && this.width && this.height) ? this.wrapper.getBoundingClientRect() : new DOMRect(0, 0, 0, 0),
);
camera = $state<OrthographicCamera>(null!);
cameraPosition: [number, number, number] = $state([0, 0, 4]);
clipboard: null | {
nodes: 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); activeNodeId = $state(-1);
selectedNodes = new SvelteSet<number>(); selectedNodes = new SvelteSet<number>();
activeSocket = $state<Socket | null>(null); activeSocket = $state<Socket | null>(null);
@@ -15,7 +73,241 @@ export class GraphState {
possibleSocketIds = $derived( possibleSocketIds = $derived(
new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`)), new Set(this.possibleSockets.map((s) => `${s.node.id}-${s.index}`)),
); );
clearSelection() { clearSelection() {
this.selectedNodes.clear(); this.selectedNodes.clear();
} }
isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT";
setCameraTransform(
x = this.cameraPosition[0],
y = this.cameraPosition[1],
z = this.cameraPosition[2],
) {
if (this.camera) {
this.camera.position.x = x;
this.camera.position.z = y;
this.camera.zoom = z;
}
this.cameraPosition = [x, y, z];
localStorage.setItem("cameraPosition", JSON.stringify(this.cameraPosition));
}
updateNodePosition(node: 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,7 +1,7 @@
import { create, type Delta } from "jsondiffpatch"; import { create, type Delta } from "jsondiffpatch";
import type { Graph } from "@nodes/types"; import type { Graph } from "@nodarium/types";
import { clone } from "./helpers/index.js"; import { clone } from "./helpers/index.js";
import { createLogger } from "@nodes/utils"; import { createLogger } from "@nodarium/utils";
const diff = create({ const diff = create({
objectHash: function (obj, index) { objectHash: function (obj, index) {
@@ -16,7 +16,7 @@ const diff = create({
}); });
const log = createLogger("history"); const log = createLogger("history");
// log.mute(); log.mute();
export class HistoryManager { export class HistoryManager {
index: number = -1; index: number = -1;

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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) { export function grid(width: number, height: number) {
@@ -16,9 +16,6 @@ export function grid(width: number, height: number) {
graph.nodes.push({ graph.nodes.push({
id: i, id: i,
tmp: {
visible: false,
},
position: [x * 30, y * 40], position: [x * 30, y * 40],
props: i == 0 ? { value: 0 } : { op_type: 0, a: 1, b: 0.05 }, props: i == 0 ? { value: 0 } : { op_type: 0, a: 1, b: 0.05 },
type: i == 0 ? "max/plantarium/float" : "max/plantarium/math", type: i == 0 ? "max/plantarium/float" : "max/plantarium/math",
@@ -29,9 +26,6 @@ export function grid(width: number, height: number) {
graph.nodes.push({ graph.nodes.push({
id: amount, id: amount,
tmp: {
visible: false,
},
position: [width * 30, (height - 1) * 40], position: [width * 30, (height - 1) * 40],
type: "max/plantarium/output", type: "max/plantarium/output",
props: {}, 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 { export function tree(depth: number): Graph {
const nodes: Node[] = [ const nodes: SerializedNode[] = [
{ {
id: 0, id: 0,
type: "max/plantarium/output", type: "max/plantarium/output",

View File

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

View File

@@ -1,4 +1,4 @@
import { createWasmWrapper } from "@nodes/utils"; import { createWasmWrapper } from "@nodarium/utils";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,8 @@
import { Canvas } from "@threlte/core"; import { Canvas } from "@threlte/core";
import Scene from "./Scene.svelte"; import Scene from "./Scene.svelte";
import { Vector3 } from "three"; import { Vector3 } from "three";
import { decodeFloat, splitNestedArray } from "@nodes/utils"; import { decodeFloat, splitNestedArray } from "@nodarium/utils";
import type { PerformanceStore } from "@nodes/utils"; import type { PerformanceStore } from "@nodarium/utils";
import { appSettings } from "$lib/settings/app-settings.svelte"; import { appSettings } from "$lib/settings/app-settings.svelte";
import SmallPerformanceViewer from "$lib/performance/SmallPerformanceViewer.svelte"; import SmallPerformanceViewer from "$lib/performance/SmallPerformanceViewer.svelte";
import { MeshMatcapMaterial, TextureLoader, type Group } from "three"; import { MeshMatcapMaterial, TextureLoader, type Group } from "three";

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
import Grid from "$lib/grid"; import Grid from "$lib/grid";
import GraphInterface from "$lib/graph-interface"; import GraphInterface from "$lib/graph-interface";
import * as templates from "$lib/graph-templates"; import * as templates from "$lib/graph-templates";
import type { Graph, Node } from "@nodes/types"; import type { Graph, NodeInstance } from "@nodarium/types";
import Viewer from "$lib/result-viewer/Viewer.svelte"; import Viewer from "$lib/result-viewer/Viewer.svelte";
import { import {
appSettings, appSettings,
@@ -23,10 +23,11 @@
WorkerRuntimeExecutor, WorkerRuntimeExecutor,
MemoryRuntimeExecutor, MemoryRuntimeExecutor,
} from "$lib/runtime"; } from "$lib/runtime";
import { IndexDBCache, RemoteNodeRegistry } from "@nodes/registry"; import { IndexDBCache, RemoteNodeRegistry } from "@nodarium/registry";
import { createPerformanceStore } from "@nodes/utils"; import { createPerformanceStore } from "@nodarium/utils";
import BenchmarkPanel from "$lib/sidebar/panels/BenchmarkPanel.svelte"; import BenchmarkPanel from "$lib/sidebar/panels/BenchmarkPanel.svelte";
import { debounceAsyncFunction } from "$lib/helpers"; import { debounceAsyncFunction } from "$lib/helpers";
import GraphSource from "$lib/sidebar/panels/GraphSource.svelte";
let performanceStore = createPerformanceStore(); let performanceStore = createPerformanceStore();
@@ -41,7 +42,7 @@
appSettings.value.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 scene = $state<Group>(null!);
let graph = $state( let graph = $state(
@@ -49,6 +50,9 @@
? JSON.parse(localStorage.getItem("graph")!) ? JSON.parse(localStorage.getItem("graph")!)
: templates.defaultPlant, : templates.defaultPlant,
); );
function handleSave(graph: Graph) {
localStorage.setItem("graph", JSON.stringify(graph));
}
let graphInterface = $state<ReturnType<typeof GraphInterface>>(null!); let graphInterface = $state<ReturnType<typeof GraphInterface>>(null!);
let viewerComponent = $state<ReturnType<typeof Viewer>>(); let viewerComponent = $state<ReturnType<typeof Viewer>>();
@@ -65,7 +69,7 @@
{ {
key: "r", key: "r",
description: "Regenerate the plant model", description: "Regenerate the plant model",
callback: randomGenerate, callback: () => randomGenerate(),
}, },
]); ]);
let graphSettings = $state<Record<string, any>>({}); let graphSettings = $state<Record<string, any>>({});
@@ -84,70 +88,66 @@
randomSeed: { type: "boolean", value: false }, randomSeed: { type: "boolean", value: false },
}); });
let runIndex = 0; async function update(
const handleUpdate = debounceAsyncFunction( g: Graph,
async (g: Graph, s: Record<string, any> = graphSettings) => { s: Record<string, any> = $state.snapshot(graphSettings),
runIndex++; ) {
performanceStore.startRun(); performanceStore.startRun();
try { try {
let a = performance.now(); let a = performance.now();
const graphResult = await runtime.execute( const graphResult = await runtime.execute(g, s);
$state.snapshot(g), let b = performance.now();
$state.snapshot(s),
);
let b = performance.now();
if (appSettings.value.debug.useWorker) { if (appSettings.value.debug.useWorker) {
let perfData = await runtime.getPerformanceData(); let perfData = await runtime.getPerformanceData();
let lastRun = perfData?.at(-1); let lastRun = perfData?.at(-1);
if (lastRun?.total) { if (lastRun?.total) {
lastRun.runtime = lastRun.total; lastRun.runtime = lastRun.total;
delete lastRun.total; delete lastRun.total;
performanceStore.mergeData(lastRun); performanceStore.mergeData(lastRun);
performanceStore.addPoint( performanceStore.addPoint(
"worker-transfer", "worker-transfer",
b - a - lastRun.runtime[0], b - a - lastRun.runtime[0],
); );
}
} }
viewerComponent?.update(graphResult);
} catch (error) {
console.log("errors", error);
} finally {
performanceStore.stopRun();
} }
}, viewerComponent?.update(graphResult);
); } catch (error) {
console.log("errors", error);
} finally {
performanceStore.stopRun();
}
}
const handleUpdate = debounceAsyncFunction(update);
$effect(() => { $effect(() => {
//@ts-ignore //@ts-ignore
AppSettingTypes.debug.stressTest.loadGrid.callback = () => { AppSettingTypes.debug.stressTest.loadGrid.callback = () => {
graph = templates.grid( manager.load(
appSettings.value.debug.amount.value, templates.grid(
appSettings.value.debug.amount.value, appSettings.value.debug.stressTest.amount,
appSettings.value.debug.stressTest.amount,
),
); );
}; };
//@ts-ignore //@ts-ignore
AppSettingTypes.debug.stressTest.loadTree.callback = () => { AppSettingTypes.debug.stressTest.loadTree.callback = () => {
graph = templates.tree(appSettings.value.debug.amount.value); manager.load(templates.tree(appSettings.value.debug.stressTest.amount));
}; };
//@ts-ignore //@ts-ignore
AppSettingTypes.debug.stressTest.lottaFaces.callback = () => { AppSettingTypes.debug.stressTest.lottaFaces.callback = () => {
graph = templates.lottaFaces; manager.load(templates.lottaFaces as unknown as Graph);
}; };
//@ts-ignore //@ts-ignore
AppSettingTypes.debug.stressTest.lottaNodes.callback = () => { AppSettingTypes.debug.stressTest.lottaNodes.callback = () => {
graph = templates.lottaNodes; manager.load(templates.lottaNodes as unknown as Graph);
}; };
//@ts-ignore //@ts-ignore
AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => { AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => {
graph = templates.lottaNodesAndFaces; manager.load(templates.lottaNodesAndFaces as unknown as Graph);
}; };
}); });
function handleSave(graph: Graph) {
localStorage.setItem("graph", JSON.stringify(graph));
}
</script> </script>
<svelte:document onkeydown={applicationKeymap.handleKeyboardEvent} /> <svelte:document onkeydown={applicationKeymap.handleKeyboardEvent} />
@@ -193,7 +193,7 @@
<Keymap <Keymap
keymaps={[ keymaps={[
{ keymap: applicationKeymap, title: "Application" }, { keymap: applicationKeymap, title: "Application" },
{ keymap: graphInterface.keymap, title: "Node-Editor" }, { keymap: graphInterface?.keymap, title: "Node-Editor" },
]} ]}
/> />
</Panel> </Panel>
@@ -219,6 +219,14 @@
<PerformanceViewer data={$performanceStore} /> <PerformanceViewer data={$performanceStore} />
{/if} {/if}
</Panel> </Panel>
<Panel
id="graph-source"
title="Graph Source"
hidden={!appSettings.value.debug.showGraphJson}
icon="i-tabler-code"
>
<GraphSource {graph} />
</Panel>
<Panel <Panel
id="benchmark" id="benchmark"
title="Benchmark" title="Benchmark"

View File

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

View File

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

View File

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

View File

@@ -3,11 +3,11 @@ import {
type AsyncCache, type AsyncCache,
type NodeDefinition, type NodeDefinition,
type NodeRegistry, type NodeRegistry,
} from "@nodes/types"; } from "@nodarium/types";
import { createLogger, createWasmWrapper } from "@nodes/utils"; import { createLogger, createWasmWrapper } from "@nodarium/utils";
const log = createLogger("node-registry"); const log = createLogger("node-registry");
// log.mute(); log.mute();
export class RemoteNodeRegistry implements NodeRegistry { export class RemoteNodeRegistry implements NodeRegistry {
status: "loading" | "ready" | "error" = "loading"; status: "loading" | "ready" | "error" = "loading";
@@ -37,7 +37,7 @@ export class RemoteNodeRegistry implements NodeRegistry {
constructor( constructor(
private url: string, private url: string,
private cache?: AsyncCache<ArrayBuffer>, private cache?: AsyncCache<ArrayBuffer>,
) {} ) { }
async fetchUsers() { async fetchUsers() {
return this.fetchJson(`nodes/users.json`); return this.fetchJson(`nodes/users.json`);
@@ -56,16 +56,17 @@ export class RemoteNodeRegistry implements NodeRegistry {
} }
private async fetchNodeWasm(nodeId: `${string}/${string}/${string}`) { private async fetchNodeWasm(nodeId: `${string}/${string}/${string}`) {
const res = await Promise.race([ const cachedNode = await this.cache?.get(nodeId);
this.fetchArrayBuffer(`nodes/${nodeId}.wasm`), if (cachedNode) {
this.cache?.get(nodeId), return cachedNode;
]); }
if (!res) { const node = await this.fetchArrayBuffer(`nodes/${nodeId}.wasm`);
if (!node) {
throw new Error(`Failed to load node wasm ${nodeId}`); throw new Error(`Failed to load node wasm ${nodeId}`);
} }
return res; return node;
} }
async load(nodeIds: `${string}/${string}/${string}`[]) { async load(nodeIds: `${string}/${string}/${string}`[]) {

View File

@@ -1,5 +1,5 @@
{ {
"name": "@nodes/types", "name": "@nodarium/types",
"version": "0.0.0", "version": "0.0.0",
"description": "", "description": "",
"main": "src/index.ts", "main": "src/index.ts",

View File

@@ -1,4 +1,4 @@
import type { Graph, NodeDefinition, NodeType } from "./types"; import type { Graph, NodeDefinition, NodeId } from "./types";
export interface NodeRegistry { export interface NodeRegistry {
/** /**
@@ -13,13 +13,13 @@ export interface NodeRegistry {
* @throws An error if the nodes could not be loaded * @throws An error if the nodes could not be loaded
* @remarks This method should be called before calling getNode or getAllNodes * @remarks This method should be called before calling getNode or getAllNodes
*/ */
load: (nodeIds: NodeType[]) => Promise<NodeDefinition[]>; load: (nodeIds: NodeId[]) => Promise<NodeDefinition[]>;
/** /**
* Get a node by id * Get a node by id
* @param id - The id of the node to get * @param id - The id of the node to get
* @returns The node with the given id, or undefined if no such node exists * @returns The node with the given id, or undefined if no such node exists
*/ */
getNode: (id: NodeType | string) => NodeDefinition | undefined; getNode: (id: NodeId | string) => NodeDefinition | undefined;
/** /**
* Get all nodes * Get all nodes
* @returns An array of all nodes * @returns An array of all nodes

View File

@@ -6,10 +6,11 @@ export type {
AsyncCache, AsyncCache,
} from "./components"; } from "./components";
export type { export type {
Node, SerializedNode,
NodeInstance,
NodeDefinition, NodeDefinition,
Socket, Socket,
NodeType, NodeId,
Edge, Edge,
Graph, Graph,
} from "./types"; } from "./types";

View File

@@ -1,34 +1,35 @@
import { z } from "zod"; import { z } from "zod";
import { NodeInputSchema } from "./inputs"; import { NodeInputSchema } from "./inputs";
export const NodeTypeSchema = z export const NodeIdSchema = z
.string() .string()
.regex(/^[^/]+\/[^/]+\/[^/]+$/, "Invalid NodeId format") .regex(/^[^/]+\/[^/]+\/[^/]+$/, "Invalid NodeId format")
.transform((value) => value as `${string}/${string}/${string}`); .transform((value) => value as `${string}/${string}/${string}`);
export type NodeType = z.infer<typeof NodeTypeSchema>; export type NodeId = z.infer<typeof NodeIdSchema>;
export type Node = { export type NodeRuntimeState = {
tmp?: { depth?: number;
depth?: number; mesh?: any;
mesh?: any; parents?: NodeInstance[];
random?: number; children?: NodeInstance[];
parents?: Node[]; inputNodes?: Record<string, NodeInstance>;
children?: Node[]; type?: NodeDefinition;
inputNodes?: Record<string, Node>; downX?: number;
type?: NodeDefinition; downY?: number;
downX?: number; x?: number;
downY?: number; y?: number;
x?: number; ref?: HTMLElement;
y?: number; visible?: boolean;
ref?: HTMLElement; isMoving?: boolean;
visible?: boolean; };
isMoving?: boolean;
}; export type NodeInstance = {
} & z.infer<typeof NodeSchema>; state: NodeRuntimeState;
} & SerializedNode;
export const NodeDefinitionSchema = z.object({ export const NodeDefinitionSchema = z.object({
id: NodeTypeSchema, id: NodeIdSchema,
inputs: z.record(z.string(), NodeInputSchema).optional(), inputs: z.record(z.string(), NodeInputSchema).optional(),
outputs: z.array(z.string()).optional(), outputs: z.array(z.string()).optional(),
meta: z meta: z
@@ -41,7 +42,7 @@ export const NodeDefinitionSchema = z.object({
export const NodeSchema = z.object({ export const NodeSchema = z.object({
id: z.number(), id: z.number(),
type: NodeTypeSchema, type: NodeIdSchema,
props: z props: z
.record(z.string(), z.union([z.number(), z.array(z.number())])) .record(z.string(), z.union([z.number(), z.array(z.number())]))
.optional(), .optional(),
@@ -54,17 +55,20 @@ export const NodeSchema = z.object({
position: z.tuple([z.number(), z.number()]), position: z.tuple([z.number(), z.number()]),
}); });
export type SerializedNode = z.infer<typeof NodeSchema>;
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & { export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & {
execute(input: Int32Array): Int32Array; execute(input: Int32Array): Int32Array;
}; };
export type Socket = { export type Socket = {
node: Node; node: NodeInstance;
index: number | string; index: number | string;
position: [number, number]; position: [number, number];
}; };
export type Edge = [Node, number, Node, string]; export type Edge = [NodeInstance, number, NodeInstance, string];
export const GraphSchema = z.object({ export const GraphSchema = z.object({
id: z.number(), id: z.number(),
@@ -79,4 +83,4 @@ export const GraphSchema = z.object({
edges: z.array(z.tuple([z.number(), z.number(), z.number(), z.string()])), edges: z.array(z.tuple([z.number(), z.number(), z.number(), z.string()])),
}); });
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", "version": "0.0.1",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@@ -53,7 +53,7 @@
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.2.4", "vite": "^7.2.4",
"vitest": "^4.0.13", "vitest": "^4.0.13",
"@nodes/types": "link:../types" "@nodarium/types": "link:../types"
}, },
"svelte": "./dist/index.js", "svelte": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",

View File

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

View File

@@ -1,44 +1,39 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
ctrl?: boolean; ctrl?: boolean;
shift?: boolean; shift?: boolean;
alt?: boolean; alt?: boolean;
key: string | string[]; key: string | string[];
} }
let { let { ctrl = false, shift = false, alt = false, key }: Props = $props();
ctrl = false,
shift = false,
alt = false,
key
}: Props = $props();
</script> </script>
<div class="command"> <div class="command">
{#if ctrl} {#if ctrl}
<span>Ctrl</span> <span>Ctrl</span>
{/if} {/if}
{#if shift} {#if shift}
<span>Shift</span> <span>Shift</span>
{/if} {/if}
{#if alt} {#if alt}
<span>Alt</span> <span>Alt</span>
{/if} {/if}
{key} {key}
</div> </div>
<style> <style>
.command { .command {
background: var(--layer-2); background: var(--layer-2);
padding: 0.4em; padding: 0.4em;
font-size: 0.8em; font-size: 0.8em;
border-radius: 0.3em; border-radius: 0.3em;
white-space: nowrap; white-space: nowrap;
width: fit-content; width: fit-content;
} }
span::after { span::after {
content: " +"; content: ' +';
opacity: 0.5; opacity: 0.5;
} }
</style> </style>

View File

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

View File

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

View File

@@ -1,156 +1,45 @@
// https://github.com/6502/sha256/blob/main/sha256.js export function fastHashArrayBuffer(input: string | Int32Array): string {
function sha256(data?: string | Int32Array) { const mask = (1n << 64n) - 1n
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: number, n: number) => (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 = (input: string | Int32Array) => {
const data =
typeof input === "string"
? typeof TextEncoder === "undefined"
? //@ts-ignore
Buffer.from(input)
: new TextEncoder().encode(input)
: input;
for (let i = 0; i < data.length; i++) { // FNV-1a 64-bit constants
buf[bp++] = data[i]; let h = 0xcbf29ce484222325n // offset basis
if (bp === 64) process(); const FNV_PRIME = 0x100000001b3n
}
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;
};
if (data) add(data); // 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
}
}
return { add, 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)
} }
export function fastHashArrayBuffer(buffer: string | Int32Array): string {
return sha256(buffer).digest();
}
// Shamelessly copied from
// https://stackoverflow.com/a/8831937
export function fastHashString(input: string) { export function fastHashString(input: string) {
if (input.length === 0) return 0; if (input.length === 0) return 0;
@@ -162,20 +51,3 @@ export function fastHashString(input: string) {
return hash; 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(v);
} else {
s.add(v.toString());
}
}
return s.digest();
}

View File

@@ -1,5 +1,5 @@
//@ts-nocheck //@ts-nocheck
import { NodeDefinition } from "@nodes/types"; import { NodeDefinition } from "@nodarium/types";
const cachedTextDecoder = new TextDecoder("utf-8", { const cachedTextDecoder = new TextDecoder("utf-8", {
ignoreBOM: true, ignoreBOM: true,
@@ -10,16 +10,16 @@ const cachedTextEncoder = new TextEncoder();
const encodeString = const encodeString =
typeof cachedTextEncoder.encodeInto === "function" typeof cachedTextEncoder.encodeInto === "function"
? function (arg, view) { ? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view); return cachedTextEncoder.encodeInto(arg, view);
} }
: function (arg, view) { : function (arg, view) {
const buf = cachedTextEncoder.encode(arg); const buf = cachedTextEncoder.encode(arg);
view.set(buf); view.set(buf);
return { return {
read: arg.length, read: arg.length,
written: buf.length, written: buf.length,
};
}; };
};
function createWrapper() { function createWrapper() {
let wasm: any; let wasm: any;

16
pnpm-lock.yaml generated
View File

@@ -10,13 +10,13 @@ importers:
app: app:
dependencies: dependencies:
'@nodes/registry': '@nodarium/registry':
specifier: link:../packages/registry specifier: link:../packages/registry
version: link:../packages/registry version: link:../packages/registry
'@nodes/ui': '@nodarium/ui':
specifier: link:../packages/ui specifier: link:../packages/ui
version: link:../packages/ui version: link:../packages/ui
'@nodes/utils': '@nodarium/utils':
specifier: link:../packages/utils specifier: link:../packages/utils
version: link:../packages/utils version: link:../packages/utils
'@sveltejs/kit': '@sveltejs/kit':
@@ -53,7 +53,7 @@ importers:
'@iconify-json/tabler': '@iconify-json/tabler':
specifier: ^1.2.23 specifier: ^1.2.23
version: 1.2.23 version: 1.2.23
'@nodes/types': '@nodarium/types':
specifier: link:../packages/types specifier: link:../packages/types
version: link:../packages/types version: link:../packages/types
'@sveltejs/adapter-static': '@sveltejs/adapter-static':
@@ -130,10 +130,10 @@ importers:
packages/registry: packages/registry:
dependencies: dependencies:
'@nodes/types': '@nodarium/types':
specifier: link:../types specifier: link:../types
version: link:../types version: link:../types
'@nodes/utils': '@nodarium/utils':
specifier: link:../utils specifier: link:../utils
version: link:../utils version: link:../utils
idb: idb:
@@ -165,7 +165,7 @@ importers:
specifier: ^9.7.0 specifier: ^9.7.0
version: 9.7.0(@types/three@0.181.0)(svelte@5.43.14)(three@0.181.2) version: 9.7.0(@types/three@0.181.0)(svelte@5.43.14)(three@0.181.2)
devDependencies: devDependencies:
'@nodes/types': '@nodarium/types':
specifier: link:../types specifier: link:../types
version: link:../types version: link:../types
'@storybook/addon-essentials': '@storybook/addon-essentials':
@@ -240,7 +240,7 @@ importers:
packages/utils: packages/utils:
dependencies: dependencies:
'@nodes/types': '@nodarium/types':
specifier: link:../types specifier: link:../types
version: link:../types version: link:../types
devDependencies: devDependencies: