4 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
32 changed files with 400 additions and 676 deletions

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { HTML } from "@threlte/extras"; import { HTML } from "@threlte/extras";
import { onMount } from "svelte"; import { onMount } from "svelte";
import type { Node, NodeType } from "@nodarium/types"; import type { NodeInstance, NodeId } from "@nodarium/types";
import { getGraphManager, getGraphState } from "../graph/state.svelte"; import { getGraphManager, getGraphState } from "../graph/state.svelte";
type Props = { type Props = {
onnode: (n: Node) => void; onnode: (n: NodeInstance) => void;
}; };
const { onnode }: Props = $props(); const { onnode }: Props = $props();
@@ -15,7 +15,7 @@
let input: HTMLInputElement; let input: HTMLInputElement;
let value = $state<string>(); let value = $state<string>();
let activeNodeId = $state<NodeType>(); let activeNodeId = $state<NodeId>();
const allNodes = graphState.activeSocket const allNodes = graphState.activeSocket
? graph.getPossibleNodes(graphState.activeSocket) ? graph.getPossibleNodes(graphState.activeSocket)
@@ -39,13 +39,14 @@
} }
}); });
function handleNodeCreation(nodeType: Node["type"]) { function handleNodeCreation(nodeType: NodeInstance["type"]) {
if (!graphState.addMenuPosition) return; if (!graphState.addMenuPosition) return;
onnode?.({ onnode?.({
id: -1, id: -1,
type: nodeType, type: nodeType,
position: [...graphState.addMenuPosition], position: [...graphState.addMenuPosition],
props: {}, props: {},
state: {},
}); });
} }

View File

@@ -26,11 +26,10 @@
<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 { Mesh, 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 = {
@@ -45,7 +44,7 @@
const thickness = $derived(Math.max(0.001, 0.00082 * Math.exp(0.055 * z))); const thickness = $derived(Math.max(0.001, 0.00082 * Math.exp(0.055 * z)));
let mesh = $state<Mesh>(); let points = $state<Vector3[]>([]);
let lastId: string | null = null; let lastId: string | null = null;
@@ -69,14 +68,10 @@
curve.v2.set(new_x / 2, 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();
if (mesh) {
mesh.geometry = createEdgeGeometry(points);
}
} }
$effect(() => { $effect(() => {
@@ -106,6 +101,7 @@
<T.CircleGeometry args={[0.5, 16]} /> <T.CircleGeometry args={[0.5, 16]} />
</T.Mesh> </T.Mesh>
<T.Mesh bind:ref={mesh} position.x={x1} position.z={y1} position.y={0.1}> <T.Mesh position.x={x1} position.z={y1} position.y={0.1}>
<MeshLineGeometry {points} />
<MeshLineMaterial width={thickness} color={lineColor} /> <MeshLineMaterial width={thickness} color={lineColor} />
</T.Mesh> </T.Mesh>

View File

@@ -1,112 +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,11 +1,11 @@
import type { import type {
Edge, Edge,
Graph, Graph,
Node, NodeInstance,
NodeDefinition, NodeDefinition,
NodeInput, NodeInput,
NodeRegistry, NodeRegistry,
NodeType, NodeId,
Socket, Socket,
} from "@nodarium/types"; } from "@nodarium/types";
import { fastHashString } from "@nodarium/utils"; import { fastHashString } from "@nodarium/utils";
@@ -68,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[]>([]);
@@ -101,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],
@@ -133,8 +133,8 @@ 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();
@@ -148,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(
@@ -174,14 +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 = {
type: nodeType, type: nodeType,
}; };
} }
return [node.id, node]; return [node.id, n];
}), }),
); );
@@ -191,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;
}); });
@@ -232,8 +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.type = nodeType; const n = node as NodeInstance;
n.state = {};
n.state.type = nodeType;
} }
// load settings // load settings
@@ -292,7 +293,7 @@ export class GraphManager extends EventEmitter<{
return this.registry.getNode(id); return this.registry.getNode(id);
} }
async loadNodeType(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);
@@ -321,19 +322,19 @@ export class GraphManager extends EventEmitter<{
this.emit("settings", { types: settingTypes, values: settingValues }); this.emit("settings", { types: settingTypes, values: settingValues });
} }
getChildren(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
@@ -350,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]) {
@@ -363,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,
@@ -380,9 +381,9 @@ export class GraphManager extends EventEmitter<{
this.save(); this.save();
} }
smartConnect(from: Node, to: Node): Edge | undefined { smartConnect(from: NodeInstance, to: NodeInstance): Edge | undefined {
const inputs = Object.entries(to.tmp?.type?.inputs ?? {}); const inputs = Object.entries(to.state?.type?.inputs ?? {});
const outputs = from.tmp?.type?.outputs ?? []; const outputs = from.state?.type?.outputs ?? [];
for (let i = 0; i < inputs.length; i++) { for (let i = 0; i < inputs.length; i++) {
const [inputName, input] = inputs[0]; const [inputName, input] = inputs[0];
for (let o = 0; o < outputs.length; o++) { for (let o = 0; o < outputs.length; o++) {
@@ -398,7 +399,7 @@ export class GraphManager extends EventEmitter<{
return Math.max(0, ...this.nodes.keys()) + 1; return Math.max(0, ...this.nodes.keys()) + 1;
} }
createGraph(nodes: Node[], edges: [number, number, number, string][]) { 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>();
@@ -422,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;
}); });
@@ -448,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) {
@@ -458,11 +457,11 @@ export class GraphManager extends EventEmitter<{
return; return;
} }
const node: Node = $state({ const node: NodeInstance = $state({
id: this.createNodeId(), id: this.createNodeId(),
type, type,
position, position,
tmp: { type: nodeType }, state: { type: nodeType },
props, props,
}); });
@@ -474,9 +473,9 @@ export class GraphManager extends EventEmitter<{
} }
createEdge( createEdge(
from: Node, from: NodeInstance,
fromSocket: number, fromSocket: number,
to: Node, to: NodeInstance,
toSocket: string, toSocket: string,
{ applyUpdate = true } = {}, { applyUpdate = true } = {},
): Edge | undefined { ): Edge | undefined {
@@ -493,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)) {
@@ -517,13 +516,10 @@ export class GraphManager extends EventEmitter<{
this.edges.push(edge); this.edges.push(edge);
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.state.parents.push(from);
to.tmp = to.tmp || {};
to.tmp.parents = to.tmp.parents || [];
to.tmp.parents.push(from);
if (applyUpdate) { if (applyUpdate) {
this.save(); this.save();
@@ -566,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");
@@ -577,7 +573,7 @@ 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();
} }
@@ -585,7 +581,7 @@ export class GraphManager extends EventEmitter<{
getPossibleNodes(socket: Socket): NodeDefinition[] { getPossibleNodes(socket: Socket): NodeDefinition[] {
const allDefinitions = this.getNodeDefinitions(); const allDefinitions = this.getNodeDefinitions();
const nodeType = socket.node.tmp?.type; const nodeType = socket.node.state?.type;
if (!nodeType) { if (!nodeType) {
return []; return [];
} }
@@ -612,11 +608,11 @@ export class GraphManager extends EventEmitter<{
} }
getPossibleSockets({ node, index }: Socket): [Node, string | number][] { getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
const nodeType = node?.tmp?.type; 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") {
@@ -629,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++) {
@@ -657,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];
@@ -692,17 +688,15 @@ export class GraphManager extends EventEmitter<{
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,
); );
} }
@@ -714,7 +708,7 @@ export class GraphManager extends EventEmitter<{
} }
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) => {
@@ -723,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) => {
@@ -735,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][];
} }
} }

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { Edge, Node } from "@nodarium/types"; import type { Edge, NodeInstance } from "@nodarium/types";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { createKeyMap } from "../../helpers/createKeyMap"; import { createKeyMap } from "../../helpers/createKeyMap";
import AddMenu from "../components/AddMenu.svelte"; import AddMenu from "../components/AddMenu.svelte";
@@ -40,7 +40,7 @@
return [pos1[0], pos1[1], pos2[0], pos2[1]]; return [pos1[0], pos1[1], pos2[0], pos2[1]];
} }
function handleNodeCreation(node: Node) { function handleNodeCreation(node: NodeInstance) {
const newNode = graph.createNode({ const newNode = graph.createNode({
type: node.type, type: node.type,
position: node.position, position: node.position,
@@ -51,11 +51,11 @@
if (graphState.activeSocket) { if (graphState.activeSocket) {
if (typeof graphState.activeSocket.index === "number") { if (typeof graphState.activeSocket.index === "number") {
const socketType = const socketType =
graphState.activeSocket.node.tmp?.type?.outputs?.[ graphState.activeSocket.node.state?.type?.outputs?.[
graphState.activeSocket.index graphState.activeSocket.index
]; ];
const input = Object.entries(newNode?.tmp?.type?.inputs || {}).find( const input = Object.entries(newNode?.state?.type?.inputs || {}).find(
(inp) => inp[1].type === socketType, (inp) => inp[1].type === socketType,
); );
@@ -69,11 +69,11 @@
} }
} else { } else {
const socketType = const socketType =
graphState.activeSocket.node.tmp?.type?.inputs?.[ graphState.activeSocket.node.state?.type?.inputs?.[
graphState.activeSocket.index graphState.activeSocket.index
]; ];
const output = newNode.tmp?.type?.outputs?.find((out) => { const output = newNode.state?.type?.outputs?.find((out) => {
if (socketType?.type === out) return true; if (socketType?.type === out) return true;
if (socketType?.accepts?.includes(out as any)) return true; if (socketType?.accepts?.includes(out as any)) return true;
return false; return false;

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { Graph, Node, NodeRegistry } from "@nodarium/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 { createKeyMap } from "$lib/helpers/createKeyMap"; import { createKeyMap } from "$lib/helpers/createKeyMap";
@@ -12,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;

View File

@@ -1,4 +1,4 @@
import { GraphSchema, type NodeType, type Node } from "@nodarium/types"; import { GraphSchema, type NodeId, type NodeInstance } from "@nodarium/types";
import type { GraphManager } from "../graph-manager.svelte"; import type { GraphManager } from "../graph-manager.svelte";
import type { GraphState } from "./state.svelte"; import type { GraphState } from "./state.svelte";
import { animate, lerp } from "$lib/helpers"; import { animate, lerp } from "$lib/helpers";
@@ -17,7 +17,7 @@ export class FileDropEventManager {
event.preventDefault(); event.preventDefault();
this.state.isDragging = false; this.state.isDragging = false;
if (!event.dataTransfer) return; if (!event.dataTransfer) return;
const nodeId = event.dataTransfer.getData("data/node-id") as NodeType; const nodeId = event.dataTransfer.getData("data/node-id") as NodeId;
let mx = event.clientX - this.state.rect.x; let mx = event.clientX - this.state.rect.x;
let my = event.clientY - this.state.rect.y; let my = event.clientY - this.state.rect.y;
@@ -131,45 +131,45 @@ export class MouseEventManager {
if (clickedNodeId !== -1) { if (clickedNodeId !== -1) {
if (activeNode) { if (activeNode) {
if (!activeNode?.tmp?.isMoving && !event.ctrlKey && !event.shiftKey) { if (!activeNode?.state?.isMoving && !event.ctrlKey && !event.shiftKey) {
this.state.activeNodeId = clickedNodeId; this.state.activeNodeId = clickedNodeId;
this.state.clearSelection(); this.state.clearSelection();
} }
} }
} }
if (activeNode?.tmp?.isMoving) { if (activeNode?.state?.isMoving) {
activeNode.tmp = activeNode.tmp || {}; activeNode.state = activeNode.state || {};
activeNode.tmp.isMoving = false; activeNode.state.isMoving = false;
if (this.state.snapToGrid) { if (this.state.snapToGrid) {
const snapLevel = this.state.getSnapLevel(); const snapLevel = this.state.getSnapLevel();
activeNode.position[0] = snapPointToGrid( activeNode.position[0] = snapPointToGrid(
activeNode?.tmp?.x ?? activeNode.position[0], activeNode?.state?.x ?? activeNode.position[0],
5 / snapLevel, 5 / snapLevel,
); );
activeNode.position[1] = snapPointToGrid( activeNode.position[1] = snapPointToGrid(
activeNode?.tmp?.y ?? activeNode.position[1], activeNode?.state?.y ?? activeNode.position[1],
5 / snapLevel, 5 / snapLevel,
); );
} else { } else {
activeNode.position[0] = activeNode?.tmp?.x ?? activeNode.position[0]; activeNode.position[0] = activeNode?.state?.x ?? activeNode.position[0];
activeNode.position[1] = activeNode?.tmp?.y ?? activeNode.position[1]; activeNode.position[1] = activeNode?.state?.y ?? activeNode.position[1];
} }
const nodes = [ const nodes = [
...[...(this.state.selectedNodes?.values() || [])].map((id) => ...[...(this.state.selectedNodes?.values() || [])].map((id) =>
this.graph.getNode(id), this.graph.getNode(id),
), ),
] as Node[]; ] as NodeInstance[];
const vec = [ const vec = [
activeNode.position[0] - (activeNode?.tmp.x || 0), activeNode.position[0] - (activeNode?.state.x || 0),
activeNode.position[1] - (activeNode?.tmp.y || 0), activeNode.position[1] - (activeNode?.state.y || 0),
]; ];
for (const node of nodes) { for (const node of nodes) {
if (!node) continue; if (!node) continue;
node.tmp = node.tmp || {}; node.state = node.state || {};
const { x, y } = node.tmp; const { x, y } = node.state;
if (x !== undefined && y !== undefined) { if (x !== undefined && y !== undefined) {
node.position[0] = x + vec[0]; node.position[0] = x + vec[0];
node.position[1] = y + vec[1]; node.position[1] = y + vec[1];
@@ -179,14 +179,14 @@ export class MouseEventManager {
animate(500, (a: number) => { animate(500, (a: number) => {
for (const node of nodes) { for (const node of nodes) {
if ( if (
node?.tmp && node?.state &&
node.tmp["x"] !== undefined && node.state["x"] !== undefined &&
node.tmp["y"] !== undefined node.state["y"] !== undefined
) { ) {
node.tmp.x = lerp(node.tmp.x, node.position[0], a); node.state.x = lerp(node.state.x, node.position[0], a);
node.tmp.y = lerp(node.tmp.y, node.position[1], a); node.state.y = lerp(node.state.y, node.position[1], a);
this.state.updateNodePosition(node); this.state.updateNodePosition(node);
if (node?.tmp?.isMoving) { if (node?.state?.isMoving) {
return false; return false;
} }
} }
@@ -318,17 +318,17 @@ export class MouseEventManager {
const node = this.graph.getNode(this.state.activeNodeId); const node = this.graph.getNode(this.state.activeNodeId);
if (!node) return; if (!node) return;
node.tmp = node.tmp || {}; node.state = node.state || {};
node.tmp.downX = node.position[0]; node.state.downX = node.position[0];
node.tmp.downY = node.position[1]; node.state.downY = node.position[1];
if (this.state.selectedNodes) { if (this.state.selectedNodes) {
for (const nodeId of this.state.selectedNodes) { for (const nodeId of this.state.selectedNodes) {
const n = this.graph.getNode(nodeId); const n = this.graph.getNode(nodeId);
if (!n) continue; if (!n) continue;
n.tmp = n.tmp || {}; n.state = n.state || {};
n.tmp.downX = n.position[0]; n.state.downX = n.position[0];
n.tmp.downY = n.position[1]; n.state.downY = n.position[1];
} }
} }
@@ -382,7 +382,7 @@ export class MouseEventManager {
const y1 = Math.min(mouseD[1], this.state.mousePosition[1]); const y1 = Math.min(mouseD[1], this.state.mousePosition[1]);
const y2 = Math.max(mouseD[1], this.state.mousePosition[1]); const y2 = Math.max(mouseD[1], this.state.mousePosition[1]);
for (const node of this.graph.nodes.values()) { for (const node of this.graph.nodes.values()) {
if (!node?.tmp) continue; if (!node?.state) continue;
const x = node.position[0]; const x = node.position[0];
const y = node.position[1]; const y = node.position[1];
const height = this.state.getNodeHeight(node.type); const height = this.state.getNodeHeight(node.type);
@@ -400,10 +400,10 @@ export class MouseEventManager {
const node = this.graph.getNode(this.state.activeNodeId); const node = this.graph.getNode(this.state.activeNodeId);
if (!node || event.buttons !== 1) return; if (!node || event.buttons !== 1) return;
node.tmp = node.tmp || {}; node.state = node.state || {};
const oldX = node.tmp.downX || 0; const oldX = node.state.downX || 0;
const oldY = node.tmp.downY || 0; const oldY = node.state.downY || 0;
let newX = let newX =
oldX + (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2]; oldX + (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
@@ -418,10 +418,10 @@ export class MouseEventManager {
} }
} }
if (!node.tmp.isMoving) { if (!node.state.isMoving) {
const dist = Math.sqrt((oldX - newX) ** 2 + (oldY - newY) ** 2); const dist = Math.sqrt((oldX - newX) ** 2 + (oldY - newY) ** 2);
if (dist > 0.2) { if (dist > 0.2) {
node.tmp.isMoving = true; node.state.isMoving = true;
} }
} }
@@ -431,15 +431,15 @@ export class MouseEventManager {
if (this.state.selectedNodes?.size) { if (this.state.selectedNodes?.size) {
for (const nodeId of this.state.selectedNodes) { for (const nodeId of this.state.selectedNodes) {
const n = this.graph.getNode(nodeId); const n = this.graph.getNode(nodeId);
if (!n?.tmp) continue; if (!n?.state) continue;
n.tmp.x = (n?.tmp?.downX || 0) - vecX; n.state.x = (n?.state?.downX || 0) - vecX;
n.tmp.y = (n?.tmp?.downY || 0) - vecY; n.state.y = (n?.state?.downY || 0) - vecY;
this.state.updateNodePosition(n); this.state.updateNodePosition(n);
} }
} }
node.tmp.x = newX; node.state.x = newX;
node.tmp.y = newY; node.state.y = newY;
this.state.updateNodePosition(node); this.state.updateNodePosition(node);

View File

@@ -1,4 +1,4 @@
import type { Node, Socket } from "@nodarium/types"; import type { NodeInstance, Socket } from "@nodarium/types";
import { getContext, setContext } from "svelte"; import { getContext, setContext } from "svelte";
import { SvelteSet } from "svelte/reactivity"; import { SvelteSet } from "svelte/reactivity";
import type { GraphManager } from "../graph-manager.svelte"; import type { GraphManager } from "../graph-manager.svelte";
@@ -38,7 +38,7 @@ export class GraphState {
cameraPosition: [number, number, number] = $state([0, 0, 4]); cameraPosition: [number, number, number] = $state([0, 0, 4]);
clipboard: null | { clipboard: null | {
nodes: Node[]; nodes: NodeInstance[];
edges: [number, number, number, string][]; edges: [number, number, number, string][];
} = null; } = null;
@@ -95,26 +95,26 @@ export class GraphState {
} }
updateNodePosition(node: Node) { updateNodePosition(node: NodeInstance) {
if (node?.tmp?.ref && node?.tmp?.mesh) { if (node.state.ref && node.state.mesh) {
if (node.tmp["x"] !== undefined && node.tmp["y"] !== undefined) { if (node.state["x"] !== undefined && node.state["y"] !== undefined) {
node.tmp.ref.style.setProperty("--nx", `${node.tmp.x * 10}px`); node.state.ref.style.setProperty("--nx", `${node.state.x * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.tmp.y * 10}px`); node.state.ref.style.setProperty("--ny", `${node.state.y * 10}px`);
node.tmp.mesh.position.x = node.tmp.x + 10; node.state.mesh.position.x = node.state.x + 10;
node.tmp.mesh.position.z = node.tmp.y + this.getNodeHeight(node.type) / 2; node.state.mesh.position.z = node.state.y + this.getNodeHeight(node.type) / 2;
if ( if (
node.tmp.x === node.position[0] && node.state.x === node.position[0] &&
node.tmp.y === node.position[1] node.state.y === node.position[1]
) { ) {
delete node.tmp.x; delete node.state.x;
delete node.tmp.y; delete node.state.y;
} }
this.graph.edges = [...this.graph.edges]; this.graph.edges = [...this.graph.edges];
} else { } else {
node.tmp.ref.style.setProperty("--nx", `${node.position[0] * 10}px`); node.state.ref.style.setProperty("--nx", `${node.position[0] * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.position[1] * 10}px`); node.state.ref.style.setProperty("--ny", `${node.position[1] * 10}px`);
node.tmp.mesh.position.x = node.position[0] + 10; node.state.mesh.position.x = node.position[0] + 10;
node.tmp.mesh.position.z = node.state.mesh.position.z =
node.position[1] + this.getNodeHeight(node.type) / 2; node.position[1] + this.getNodeHeight(node.type) / 2;
} }
} }
@@ -134,19 +134,19 @@ export class GraphState {
} }
getSocketPosition( getSocketPosition(
node: Node, node: NodeInstance,
index: string | number, index: string | number,
): [number, number] { ): [number, number] {
if (typeof index === "number") { if (typeof index === "number") {
return [ return [
(node?.tmp?.x ?? node.position[0]) + 20, (node?.state?.x ?? node.position[0]) + 20,
(node?.tmp?.y ?? node.position[1]) + 2.5 + 10 * index, (node?.state?.y ?? node.position[1]) + 2.5 + 10 * index,
]; ];
} else { } else {
const _index = Object.keys(node.tmp?.type?.inputs || {}).indexOf(index); const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index);
return [ return [
node?.tmp?.x ?? node.position[0], node?.state?.x ?? node.position[0],
(node?.tmp?.y ?? node.position[1]) + 10 + 10 * _index, (node?.state?.y ?? node.position[1]) + 10 + 10 * _index,
]; ];
} }
} }
@@ -174,32 +174,6 @@ export class GraphState {
return height; return height;
} }
setNodePosition(node: Node) {
if (node?.tmp?.ref && node?.tmp?.mesh) {
if (node.tmp["x"] !== undefined && node.tmp["y"] !== undefined) {
node.tmp.ref.style.setProperty("--nx", `${node.tmp.x * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.tmp.y * 10}px`);
node.tmp.mesh.position.x = node.tmp.x + 10;
node.tmp.mesh.position.z = node.tmp.y + this.getNodeHeight(node.type) / 2;
if (
node.tmp.x === node.position[0] &&
node.tmp.y === node.position[1]
) {
delete node.tmp.x;
delete node.tmp.y;
}
this.graph.edges = [...this.graph.edges];
} else {
node.tmp.ref.style.setProperty("--nx", `${node.position[0] * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.position[1] * 10}px`);
node.tmp.mesh.position.x = node.position[0] + 10;
node.tmp.mesh.position.z =
node.position[1] + this.getNodeHeight(node.type) / 2;
}
}
}
copyNodes() { copyNodes() {
if (this.activeNodeId === -1 && !this.selectedNodes?.size) if (this.activeNodeId === -1 && !this.selectedNodes?.size)
return; return;
@@ -231,12 +205,11 @@ export class GraphState {
const nodes = this.clipboard.nodes const nodes = this.clipboard.nodes
.map((node) => { .map((node) => {
node.tmp = node.tmp || {};
node.position[0] = this.mousePosition[0] - node.position[0]; node.position[0] = this.mousePosition[0] - node.position[0];
node.position[1] = this.mousePosition[1] - node.position[1]; node.position[1] = this.mousePosition[1] - node.position[1];
return node; return node;
}) })
.filter(Boolean) as Node[]; .filter(Boolean) as NodeInstance[];
const newNodes = this.graph.createGraph(nodes, this.clipboard.edges); const newNodes = this.graph.createGraph(nodes, this.clipboard.edges);
this.selectedNodes.clear(); this.selectedNodes.clear();
@@ -327,7 +300,7 @@ export class GraphState {
return clickedNodeId; return clickedNodeId;
} }
isNodeInView(node: Node) { isNodeInView(node: NodeInstance) {
const height = this.getNodeHeight(node.type); const height = this.getNodeHeight(node.type);
const width = 20; const width = 20;
return ( return (

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { Node } from "@nodarium/types"; import type { NodeInstance } from "@nodarium/types";
import { onMount } from "svelte";
import { getGraphState } from "../graph/state.svelte"; import { getGraphState } from "../graph/state.svelte";
import { T } from "@threlte/core"; import { T } from "@threlte/core";
import { type Mesh } from "three"; import { type Mesh } from "three";
@@ -13,11 +12,11 @@
const graphState = getGraphState(); const graphState = getGraphState();
type Props = { type Props = {
node: Node; node: NodeInstance;
inView: boolean; inView: boolean;
z: number; z: number;
}; };
let { node, inView, z }: Props = $props(); let { node = $bindable(), inView, z }: Props = $props();
const isActive = $derived(graphState.activeNodeId === node.id); const isActive = $derived(graphState.activeNodeId === node.id);
const isSelected = $derived(graphState.selectedNodes.has(node.id)); const isSelected = $derived(graphState.selectedNodes.has(node.id));
@@ -35,9 +34,8 @@
const height = graphState.getNodeHeight(node.type); const height = graphState.getNodeHeight(node.type);
$effect(() => { $effect(() => {
if (!node.tmp) node.tmp = {}; if (meshRef && !node.state?.mesh) {
if (meshRef && !node.tmp?.mesh) { node.state.mesh = meshRef;
node.tmp.mesh = meshRef;
graphState.updateNodePosition(node); graphState.updateNodePosition(node);
} }
}); });
@@ -69,4 +67,4 @@
/> />
</T.Mesh> </T.Mesh>
<NodeHtml {node} {inView} {isActive} {isSelected} {z} /> <NodeHtml bind:node {inView} {isActive} {isSelected} {z} />

View File

@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { Node } from "@nodarium/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 { onMount } from "svelte";
import { getGraphState } from "../graph/state.svelte"; import { getGraphState } from "../graph/state.svelte";
let ref: HTMLDivElement; let ref: HTMLDivElement;
@@ -10,7 +9,7 @@
const graphState = getGraphState(); 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;
@@ -31,15 +30,16 @@
const zOffset = Math.random() - 0.5; 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,
); );
onMount(() => { $effect(() => {
node.tmp = node.tmp || {}; if ("state" in node && !node.state.ref) {
node.tmp.ref = ref; node.state.ref = ref;
graphState?.updateNodePosition(node); graphState?.updateNodePosition(node);
}
}); });
</script> </script>

View File

@@ -1,24 +1,26 @@
<script lang="ts"> <script lang="ts">
import { getGraphState } from "../graph/state.svelte.js"; import { getGraphState } from "../graph/state.svelte.js";
import { createNodePath } from "../helpers/index.js"; import { createNodePath } from "../helpers/index.js";
import type { Node } from "@nodarium/types"; import type { NodeInstance } from "@nodarium/types";
const graphState = getGraphState(); const graphState = getGraphState();
const { node }: { node: Node } = $props(); const { node }: { node: NodeInstance } = $props();
function handleMouseDown(event: MouseEvent) { function handleMouseDown(event: MouseEvent) {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
if ("state" in node) {
graphState.setDownSocket?.({ graphState.setDownSocket?.({
node, node,
index: 0, index: 0,
position: graphState.getSocketPosition?.(node, 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({
@@ -29,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,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import type { Node, NodeInput } from "@nodarium/types"; import type { NodeInstance, NodeInput } from "@nodarium/types";
import { Input } from "@nodarium/ui"; import { Input } from "@nodarium/ui";
import type { GraphManager } from "../graph-manager.svelte"; 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;

View File

@@ -1,15 +1,12 @@
<script lang="ts"> <script lang="ts">
import type { import type { NodeInput, NodeInstance } from "@nodarium/types";
NodeInput as NodeInputType,
Node as NodeType,
} from "@nodarium/types";
import { createNodePath } from "../helpers/index.js"; import { createNodePath } from "../helpers/index.js";
import NodeInput from "./NodeInput.svelte"; import NodeInputEl from "./NodeInput.svelte";
import { getGraphManager, getGraphState } from "../graph/state.svelte.js"; import { getGraphManager, 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;
}; };
@@ -18,7 +15,7 @@
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}`;
@@ -37,7 +34,7 @@
}); });
} }
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;
@@ -79,11 +76,11 @@
<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 {graph} {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 data-node-socket class="large target"></div> <div data-node-socket class="large target"></div>
<div <div
data-node-socket data-node-socket

View File

@@ -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 "@nodarium/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,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 "@nodarium/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

@@ -1,25 +1,23 @@
<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 "@nodarium/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">
@@ -27,12 +25,12 @@
<tbody> <tbody>
<tr <tr
style="cursor:pointer;" style="cursor:pointer;"
on:click={() => ($open.runtime = !$open.runtime)} onclick={() => (open.value.runtime = !open.value.runtime)}
> >
<td>{$open.runtime ? "-" : "+"} runtime </td> <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")} />
@@ -40,13 +38,16 @@
</tr> </tr>
{/if} {/if}
<tr style="cursor:pointer;" 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} />

View File

@@ -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 };
@@ -258,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,19 +1,33 @@
import { type SyncCache } from "@nodarium/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,6 +1,5 @@
import type { import type {
Graph, Graph,
Node,
NodeDefinition, NodeDefinition,
NodeInput, NodeInput,
NodeRegistry, NodeRegistry,
@@ -14,6 +13,7 @@ import {
fastHashArrayBuffer, fastHashArrayBuffer,
type PerformanceStore, type PerformanceStore,
} from "@nodarium/utils"; } from "@nodarium/utils";
import type { RuntimeNode } from "./types";
const log = createLogger("runtime-executor"); const log = createLogger("runtime-executor");
log.mute(); log.mute();
@@ -92,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
@@ -112,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;
} }
} }
@@ -130,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;
}
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); stack.push(parent);
} else {
parent.tmp.depth = Math.max(parent.tmp.depth, node.tmp.depth + 1);
}
}
} }
nodes.push(node); nodes.push(node);
} }
@@ -175,7 +169,7 @@ 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
@@ -188,7 +182,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
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;
} }
@@ -208,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

@@ -2,11 +2,13 @@ import { MemoryRuntimeExecutor } from "./runtime-executor";
import { RemoteNodeRegistry, IndexDBCache } from "@nodarium/registry"; import { RemoteNodeRegistry, IndexDBCache } from "@nodarium/registry";
import type { Graph } from "@nodarium/types"; import type { Graph } from "@nodarium/types";
import { createPerformanceStore } from "@nodarium/utils"; import { createPerformanceStore } from "@nodarium/utils";
import { MemoryRuntimeCache } from "./runtime-executor-cache";
const indexDbCache = new IndexDBCache("node-registry"); const indexDbCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry("", indexDbCache); const nodeRegistry = new RemoteNodeRegistry("", indexDbCache);
const executor = new MemoryRuntimeExecutor(nodeRegistry); const cache = new MemoryRuntimeCache()
const executor = new MemoryRuntimeExecutor(nodeRegistry, cache);
const performanceStore = createPerformanceStore(); const performanceStore = createPerformanceStore();
executor.perf = performanceStore; executor.perf = performanceStore;

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { Node, NodeInput } from "@nodarium/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 = $bindable() }: Props = $props(); const { manager, node = $bindable() }: Props = $props();
@@ -19,19 +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.tmp?.type?.inputs); 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 = {};

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { Node } from "@nodarium/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

@@ -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 "@nodarium/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,
@@ -42,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(
@@ -88,13 +88,10 @@
randomSeed: { type: "boolean", value: false }, randomSeed: { type: "boolean", value: false },
}); });
let runIndex = 0;
async function update( async function update(
g: Graph, g: Graph,
s: Record<string, any> = $state.snapshot(graphSettings), s: Record<string, any> = $state.snapshot(graphSettings),
) { ) {
runIndex++;
performanceStore.startRun(); performanceStore.startRun();
try { try {
let a = performance.now(); let a = performance.now();

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,23 +1,19 @@
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 only exists at runtime
*/
tmp?: {
depth?: number; depth?: number;
mesh?: any; mesh?: any;
parents?: Node[]; parents?: NodeInstance[];
children?: Node[]; children?: NodeInstance[];
inputNodes?: Record<string, Node>; inputNodes?: Record<string, NodeInstance>;
type?: NodeDefinition; type?: NodeDefinition;
downX?: number; downX?: number;
downY?: number; downY?: number;
@@ -26,11 +22,14 @@ export type Node = {
ref?: HTMLElement; ref?: HTMLElement;
visible?: boolean; visible?: boolean;
isMoving?: boolean; isMoving?: boolean;
}; };
} & z.infer<typeof NodeSchema>;
export type NodeInstance = {
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
@@ -43,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(),
@@ -56,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(),
@@ -81,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

@@ -6,12 +6,7 @@
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">
@@ -38,7 +33,7 @@
} }
span::after { span::after {
content: " +"; content: ' +';
opacity: 0.5; opacity: 0.5;
} }
</style> </style>

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();
}