Merge pull request 'feat/debug-node' (#41) from feat/debug-node into main
All checks were successful
🚀 Lint & Test & Deploy / release (push) Successful in 3m58s

Reviewed-on: #41
This commit was merged in pull request #41.
This commit is contained in:
2026-02-12 23:20:58 +01:00
31 changed files with 525 additions and 252 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -29,8 +29,9 @@ function areSocketsCompatible(
output: string | undefined,
inputs: string | (string | undefined)[] | undefined
) {
if (output === '*') return true;
if (Array.isArray(inputs) && output) {
return inputs.includes(output);
return inputs.includes('*') || inputs.includes(output);
}
return inputs === output;
}

View File

@@ -3,6 +3,7 @@ import { getContext, setContext } from 'svelte';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import type { OrthographicCamera, Vector3 } from 'three';
import type { GraphManager } from './graph-manager.svelte';
import { getNodeHeight, getSocketPosition } from './helpers/nodeHelpers';
const graphStateKey = Symbol('graph-state');
export function getGraphState() {
@@ -159,54 +160,27 @@ export class GraphState {
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;
}
let height = 5;
for (const key of Object.keys(node.inputs)) {
if (key === 'seed') continue;
if (!node.inputs) continue;
if (node?.inputs?.[key] === undefined) continue;
if ('setting' in node.inputs[key]) continue;
if (node.inputs[key].hidden) continue;
if (
node.inputs[key].type === 'shape'
&& node.inputs[key].external !== true
&& node.inputs[key].internal !== false
) {
height += 20;
continue;
tryConnectToDebugNode(nodeId: number) {
const node = this.graph.nodes.get(nodeId);
if (!node) return;
if (node.type.endsWith('/debug')) return;
if (!node.state.type?.outputs?.length) return;
for (const _node of this.graph.nodes.values()) {
if (_node.type.endsWith('/debug')) {
this.graph.createEdge(node, 0, _node, 'input');
return;
}
height += 10;
}
this.nodeHeightCache[nodeTypeId] = height;
return height;
const debugNode = this.graph.createNode({
type: 'max/plantarium/debug',
position: [node.position[0] + 30, node.position[1]],
props: {}
});
if (debugNode) {
this.graph.createEdge(node, 0, debugNode, 'input');
}
}
copyNodes() {
@@ -266,7 +240,7 @@ export class GraphState {
if (edge[3] === index) {
node = edge[0];
index = edge[1];
position = this.getSocketPosition(node, index);
position = getSocketPosition(node, index);
this.graph.removeEdge(edge);
break;
}
@@ -286,7 +260,7 @@ export class GraphState {
return {
node,
index,
position: this.getSocketPosition(node, index)
position: getSocketPosition(node, index)
};
});
}
@@ -323,7 +297,7 @@ export class GraphState {
for (const node of this.graph.nodes.values()) {
const x = node.position[0];
const y = node.position[1];
const height = this.getNodeHeight(node.type);
const height = getNodeHeight(node.state.type!);
if (downX > x && downX < x + 20 && downY > y && downY < y + height) {
clickedNodeId = node.id;
break;
@@ -335,14 +309,12 @@ export class GraphState {
}
isNodeInView(node: NodeInstance) {
const height = this.getNodeHeight(node.type);
const height = getNodeHeight(node.state.type!);
const width = 20;
return (
node.position[0] > this.cameraBounds[0] - width
return node.position[0] > this.cameraBounds[0] - width
&& node.position[0] < this.cameraBounds[1]
&& node.position[1] > this.cameraBounds[2] - height
&& node.position[1] < this.cameraBounds[3]
);
&& node.position[1] < this.cameraBounds[3];
}
openNodePalette() {

View File

@@ -11,6 +11,7 @@
import Debug from '../debug/Debug.svelte';
import EdgeEl from '../edges/Edge.svelte';
import { getGraphManager, getGraphState } from '../graph-state.svelte';
import { getSocketPosition } from '../helpers/nodeHelpers';
import NodeEl from '../node/Node.svelte';
import { maxZoom, minZoom } from './constants';
import { FileDropEventManager } from './drop.events';
@@ -38,8 +39,8 @@
return [0, 0, 0, 0];
}
const pos1 = graphState.getSocketPosition(fromNode, edge[1]);
const pos2 = graphState.getSocketPosition(toNode, edge[3]);
const pos1 = getSocketPosition(fromNode, edge[1]);
const pos2 = getSocketPosition(toNode, edge[3]);
return [pos1[0], pos1[1], pos2[0], pos2[1]];
}
@@ -208,7 +209,6 @@
<NodeEl
{node}
inView={graphState.isNodeInView(node)}
z={graphState.cameraPosition[2]}
/>
{/each}
</div>

View File

@@ -3,6 +3,7 @@ import { type NodeInstance } from '@nodarium/types';
import type { GraphManager } from '../graph-manager.svelte';
import { type GraphState } from '../graph-state.svelte';
import { snapToGrid as snapPointToGrid } from '../helpers';
import { getNodeHeight } from '../helpers/nodeHelpers';
import { maxZoom, minZoom, zoomSpeed } from './constants';
import { EdgeInteractionManager } from './edge.events';
@@ -188,6 +189,10 @@ export class MouseEventManager {
// if we clicked on a node
if (clickedNodeId !== -1) {
if (event.ctrlKey && event.shiftKey) {
this.state.tryConnectToDebugNode(clickedNodeId);
return;
}
if (this.state.activeNodeId === -1) {
this.state.activeNodeId = clickedNodeId;
// if the selected node is the same as the clicked node
@@ -289,7 +294,7 @@ export class MouseEventManager {
if (!node?.state) continue;
const x = node.position[0];
const y = node.position[1];
const height = this.state.getNodeHeight(node.type);
const height = getNodeHeight(node.state.type!);
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
this.state.selectedNodes?.add(node.id);
} else {

View File

@@ -35,6 +35,9 @@ export function createNodePath({
rightBump = false,
aspectRatio = 1
} = {}) {
const leftBumpTopY = y + height / 2;
const leftBumpBottomY = y - height / 2;
return `M0,${cornerTop}
${
cornerTop
@@ -64,9 +67,7 @@ export function createNodePath({
}
${
leftBump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${
y - height / 2
}`
? ` V${leftBumpTopY} C${depth},${leftBumpTopY} ${depth},${leftBumpBottomY} 0,${leftBumpBottomY}`
: ` H0`
}
Z`.replace(/\s+/g, ' ');

View File

@@ -0,0 +1,71 @@
import type { NodeDefinition, NodeInstance } from '@nodarium/types';
export function getParameterHeight(node: NodeDefinition, inputKey: string) {
const input = node.inputs?.[inputKey];
if (!input) {
return 0;
}
if (inputKey === 'seed') return 0;
if (!node.inputs) return 0;
if ('setting' in input) return 0;
if (input.hidden) return 0;
if (input.type === 'shape' && input.external !== true) {
return 200;
}
if (
input?.label !== '' && !input.external && input.type !== 'path'
&& input.type !== 'geometry'
) {
return 100;
}
return 50;
}
export function 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 {
let height = 5;
const nodeType = node.state.type!;
const inputs = nodeType.inputs || {};
for (const inputKey in inputs) {
const h = getParameterHeight(nodeType, inputKey) / 10;
if (inputKey === index) {
height += h / 2;
break;
}
height += h;
}
return [
node?.state?.x ?? node.position[0],
(node?.state?.y ?? node.position[1]) + height
];
}
}
const nodeHeightCache: Record<string, number> = {};
export function getNodeHeight(node: NodeDefinition) {
if (node.id in nodeHeightCache) {
return nodeHeightCache[node.id];
}
if (!node?.inputs) {
return 5;
}
let height = 5;
for (const key in node.inputs) {
const h = getParameterHeight(node, key) / 10;
height += h;
}
nodeHeightCache[node.id] = height;
return height;
}

View File

@@ -1,56 +1,88 @@
varying vec2 vUv;
uniform float uWidth;
uniform float uHeight;
uniform float uZoom;
uniform vec3 uColorDark;
uniform vec3 uColorBright;
uniform vec3 uStrokeColor;
uniform float uStrokeWidth;
const float uHeaderHeight = 5.0;
uniform float uSectionHeights[16];
uniform int uNumSections;
float msign(in float x) { return (x < 0.0) ? -1.0 : 1.0; }
float sdCircle(vec2 p, float r) { return length(p) - r; }
vec4 roundedBoxSDF( in vec2 p, in vec2 b, in float r, in float s) {
vec2 q = abs(p) - b + r;
float l = b.x + b.y + 1.570796 * r;
float k1 = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
float k2 = ((q.x > 0.0) ? atan(q.y, q.x) : 1.570796);
float k3 = 3.0 + 2.0 * msign(min(p.x, -p.y)) - msign(p.x);
float k4 = msign(p.x * p.y);
float k5 = r * k2 + max(-q.x, 0.0);
float ra = s * round(k1 / s);
float l2 = l + 1.570796 * ra;
return vec4(k1 - ra, k3 * l2 + k4 * (b.y + ((q.y > 0.0) ? k5 + k2 * ra : q.y)), 4.0 * l2, k1);
}
void main(){
float strokeWidth = mix(2.0, 0.5, uZoom);
float borderRadius = 0.5;
float dentRadius = 0.8;
float y = (1.0-vUv.y) * uHeight;
float y = (1.0 - vUv.y) * uHeight;
float x = vUv.x * uWidth;
vec2 size = vec2(uWidth, uHeight);
vec2 uv = (vUv - 0.5) * 2.0;
vec2 uvCenter = (vUv - 0.5) * 2.0;
float u_border_radius = 0.4;
vec4 distance = roundedBoxSDF(uv * size, size, u_border_radius*2.0, 0.0);
vec4 boxData = roundedBoxSDF(uvCenter * size, size, borderRadius * 2.0, 0.0);
float sceneSDF = boxData.w;
if (distance.w > 0.0 ) {
// outside
gl_FragColor = vec4(0.0,0.0,0.0, 0.0);
}else{
if (distance.w > -uStrokeWidth || mod(y+5.0, 10.0) < uStrokeWidth/2.0) {
// draw the outer stroke
gl_FragColor = vec4(uStrokeColor, 1.0);
}else if (y<5.0){
// draw the header
gl_FragColor = vec4(uColorBright, 1.0);
}else{
gl_FragColor = vec4(uColorDark, 1.0);
}
vec2 headerDentPos = vec2(uWidth, uHeaderHeight * 0.5);
float headerDentDist = sdCircle(vec2(x, y) - headerDentPos, dentRadius);
sceneSDF = max(sceneSDF, -headerDentDist*2.0);
float currentYBoundary = uHeaderHeight;
float previousYBoundary = uHeaderHeight;
for (int i = 0; i < 16; i++) {
if (i >= uNumSections) break;
float sectionHeight = uSectionHeights[i];
currentYBoundary += sectionHeight;
float centerY = previousYBoundary + (sectionHeight * 0.5);
vec2 circlePos = vec2(0.0, centerY);
float circleDist = sdCircle(vec2(x, y) - circlePos, dentRadius);
sceneSDF = max(sceneSDF, -circleDist*2.0);
previousYBoundary = currentYBoundary;
}
if (sceneSDF > 0.05) {
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
return;
}
vec3 finalColor = (y < uHeaderHeight) ? uColorBright : uColorDark;
bool isDivider = false;
float dividerY = uHeaderHeight;
if (abs(y - dividerY) < strokeWidth * 0.25) isDivider = true;
for (int i = 0; i < 16; i++) {
if (i >= uNumSections - 1) break;
dividerY += uSectionHeights[i];
if (abs(y - dividerY) < strokeWidth * 0.25) isDivider = true;
}
if (sceneSDF > -strokeWidth || isDivider) {
gl_FragColor = vec4(uStrokeColor, 1.0);
} else {
gl_FragColor = vec4(finalColor, 1.0);
}
}

View File

@@ -5,6 +5,7 @@
import { type Mesh } from 'three';
import { getGraphState } from '../graph-state.svelte';
import { colors } from '../graph/colors.svelte';
import { getNodeHeight, getParameterHeight } from '../helpers/nodeHelpers';
import NodeFrag from './Node.frag';
import NodeVert from './Node.vert';
import NodeHtml from './NodeHTML.svelte';
@@ -14,9 +15,10 @@
type Props = {
node: NodeInstance;
inView: boolean;
z: number;
};
let { node = $bindable(), inView, z }: Props = $props();
let { node = $bindable(), inView }: Props = $props();
const nodeType = $derived(node.state.type!);
const isActive = $derived(graphState.activeNodeId === node.id);
const isSelected = $derived(graphState.selectedNodes.has(node.id));
@@ -29,9 +31,18 @@
: colors.outline)
);
const sectionHeights = $derived(
Object
.keys(nodeType.inputs || {})
.map(key => getParameterHeight(nodeType, key) / 10)
.filter(b => !!b)
);
let meshRef: Mesh | undefined = $state();
const height = graphState.getNodeHeight(node.type);
const height = getNodeHeight(node.state.type!);
const zoom = $derived(graphState.cameraPosition[2]);
$effect(() => {
if (meshRef && !node.state?.mesh) {
@@ -39,6 +50,10 @@
graphState.updateNodePosition(node);
}
});
const zoomValue = $derived(
(Math.log(graphState.cameraPosition[2]) - Math.log(1)) / (Math.log(40) - Math.log(1))
);
// const zoomValue = (graphState.cameraPosition[2] - 1) / 39;
</script>
<T.Mesh
@@ -47,7 +62,7 @@
position.y={0.8}
rotation.x={-Math.PI / 2}
bind:ref={meshRef}
visible={inView && z < 7}
visible={inView && zoom < 7}
>
<T.PlaneGeometry args={[20, height]} radius={1} />
<T.ShaderMaterial
@@ -57,14 +72,19 @@
uniforms={{
uColorBright: { value: colors['layer-2'] },
uColorDark: { value: colors['layer-1'] },
uStrokeColor: { value: colors['layer-2'].clone() },
uStrokeWidth: { value: 1.0 },
uStrokeColor: { value: colors.outline.clone() },
uSectionHeights: { value: [5, 10] },
uNumSections: { value: 2 },
uWidth: { value: 20 },
uHeight: { value: height }
uHeight: { value: 200 },
uZoom: { value: 1.0 }
}}
uniforms.uStrokeColor.value={strokeColor.clone()}
uniforms.uStrokeWidth.value={(7 - z) / 3}
uniforms.uZoom.value={zoomValue}
uniforms.uHeight.value={height}
uniforms.uSectionHeights.value={sectionHeights}
uniforms.uNumSections.value={sectionHeights.length}
uniforms.uStrokeColor.value={strokeColor}
/>
</T.Mesh>
<NodeHtml bind:node {inView} {isActive} {isSelected} {z} />
<NodeHtml bind:node {inView} {isActive} {isSelected} z={zoom} />

View File

@@ -1,7 +1,9 @@
<script lang="ts">
import { appSettings } from '$lib/settings/app-settings.svelte';
import type { NodeInstance } from '@nodarium/types';
import { getGraphState } from '../graph-state.svelte';
import { createNodePath } from '../helpers/index.js';
import { getSocketPosition } from '../helpers/nodeHelpers';
const graphState = getGraphState();
@@ -14,7 +16,7 @@
graphState.setDownSocket?.({
node,
index: 0,
position: graphState.getSocketPosition?.(node, 0)
position: getSocketPosition?.(node, 0)
});
}
}
@@ -47,6 +49,9 @@
<div class="wrapper" data-node-id={node.id} data-node-type={node.type}>
<div class="content">
{#if appSettings.value.debug.advancedMode}
<span class="bg-white text-black! mr-2 px-1 rounded-sm opacity-30">{node.id}</span>
{/if}
{node.type.split('/').pop()}
</div>
<div

View File

@@ -2,6 +2,7 @@
import type { NodeInput, NodeInstance } from '@nodarium/types';
import { getGraphManager, getGraphState } from '../graph-state.svelte';
import { createNodePath } from '../helpers';
import { getParameterHeight, getSocketPosition } from '../helpers/nodeHelpers';
import NodeInputEl from './NodeInput.svelte';
type Props = {
@@ -12,19 +13,18 @@
};
const graph = getGraphManager();
const graphState = getGraphState();
const graphId = graph?.id;
const elementId = `input-${Math.random().toString(36).substring(7)}`;
let { node = $bindable(), input, id, isLast }: Props = $props();
const inputType = $derived(node?.state?.type?.inputs?.[id]);
const nodeType = $derived(node.state.type!);
const inputType = $derived(nodeType.inputs?.[id]);
const socketId = $derived(`${node.id}-${id}`);
const isShape = $derived(input.type === 'shape' && input.external !== true);
const height = $derived(isShape ? 200 : 100);
const graphState = getGraphState();
const graphId = graph?.id;
const elementId = `input-${Math.random().toString(36).substring(7)}`;
const height = $derived(getParameterHeight(nodeType, id));
function handleMouseDown(ev: MouseEvent) {
ev.preventDefault();
@@ -32,18 +32,18 @@
graphState.setDownSocket({
node,
index: id,
position: graphState.getSocketPosition?.(node, id)
position: getSocketPosition(node, id)
});
}
const leftBump = $derived(node.state?.type?.inputs?.[id].internal !== true);
const leftBump = $derived(nodeType.inputs?.[id].internal !== true);
const cornerBottom = $derived(isLast ? 5 : 0);
const aspectRatio = 0.5;
const path = $derived(
createNodePath({
depth: 6,
height: 18,
height: 2000 / height,
y: 50.5,
cornerBottom,
leftBump,
@@ -53,7 +53,7 @@
const pathHover = $derived(
createNodePath({
depth: 7,
height: 20,
height: 2200 / height,
y: 50.5,
cornerBottom,
leftBump,
@@ -74,10 +74,6 @@
{#if inputType?.label !== ''}
<label for={elementId} title={input.description}>{input.label || id}</label>
{/if}
<span
class="absolute i-[tabler--help-circle] size-4 block top-2 right-2 opacity-30"
title={JSON.stringify(input, null, 2)}
></span>
{#if inputType?.external !== true}
<NodeInputEl {graph} {elementId} bind:node {input} {id} />
{/if}

View File

@@ -0,0 +1,11 @@
export const debugNode = {
id: 'max/plantarium/debug',
inputs: {
input: {
type: '*'
}
},
execute(_data: Int32Array): Int32Array {
return _data;
}
} as const;

View File

@@ -15,8 +15,15 @@ export class RemoteNodeRegistry implements NodeRegistry {
constructor(
private url: string,
public cache?: AsyncCache<ArrayBuffer | string>
) {}
public cache?: AsyncCache<ArrayBuffer | string>,
nodes?: NodeDefinition[]
) {
if (nodes?.length) {
for (const node of nodes) {
this.nodes.set(node.id, node);
}
}
}
async fetchJson(url: string, skipCache = false) {
const finalUrl = `${this.url}/${url}`;

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { T } from '@threlte/core';
import type { Group } from 'three';
import { updateDebugScene } from './debug';
type Props = {
debugData?: Record<number, { type: string; data: Int32Array }>;
};
let group = $state<Group>(null!);
const { debugData }: Props = $props();
$effect(() => {
if (!group || !debugData) return;
updateDebugScene(group, $state.snapshot(debugData));
});
</script>
<T.Group bind:ref={group} />

View File

@@ -1,33 +1,26 @@
<script lang="ts">
import { colors } from '$lib/graph-interface/graph/colors.svelte';
import { T, useTask, useThrelte } from '@threlte/core';
import { Grid, MeshLineGeometry, MeshLineMaterial, Text } from '@threlte/extras';
import {
Box3,
type BufferGeometry,
type Group,
Mesh,
MeshBasicMaterial,
Vector3,
type Vector3Tuple
} from 'three';
import { Grid } from '@threlte/extras';
import { Box3, type BufferGeometry, type Group, Mesh, MeshBasicMaterial, Vector3 } from 'three';
import { appSettings } from '../settings/app-settings.svelte';
import Camera from './Camera.svelte';
import Debug from './Debug.svelte';
const { renderStage, invalidate: _invalidate } = useThrelte();
type Props = {
fps: number[];
lines: Vector3[][];
debugData?: Record<number, { type: string; data: Int32Array }>;
scene: Group;
centerCamera: boolean;
};
let {
lines,
centerCamera,
fps = $bindable(),
scene = $bindable()
scene = $bindable(),
debugData
}: Props = $props();
let geometries = $state.raw<BufferGeometry[]>([]);
@@ -91,18 +84,12 @@
});
_invalidate();
});
function getPosition(geo: BufferGeometry, i: number) {
return [
geo.attributes.position.array[i],
geo.attributes.position.array[i + 1],
geo.attributes.position.array[i + 2]
] as Vector3Tuple;
}
</script>
<Camera {center} {centerCamera} />
<Debug {debugData} />
{#if appSettings.value.showGrid}
<Grid
cellColor={colors['outline']}
@@ -116,35 +103,4 @@
fadeOrigin={new Vector3(0, 0, 0)}
/>
{/if}
<T.Group>
{#if geometries}
{#each geometries as geo (geo.id)}
{#if appSettings.value.debug.showIndices}
{#each geo.attributes.position.array, i (i)}
{#if i % 3 === 0}
<Text fontSize={0.25} position={getPosition(geo, i)} />
{/if}
{/each}
{/if}
{#if appSettings.value.debug.showVertices}
<T.Points visible={true}>
<T is={geo} />
<T.PointsMaterial size={0.25} />
</T.Points>
{/if}
{/each}
{/if}
<T.Group bind:ref={scene}></T.Group>
</T.Group>
{#if appSettings.value.debug.showStemLines && lines}
{#each lines as line (line[0].x + '-' + line[0].y + '-' + '' + line[0].z)}
<T.Mesh>
<MeshLineGeometry points={line} />
<MeshLineMaterial width={0.1} color="red" depthTest={false} />
</T.Mesh>
{/each}
{/if}
<T.Group bind:ref={scene}></T.Group>

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import SmallPerformanceViewer from '$lib/performance/SmallPerformanceViewer.svelte';
import { appSettings } from '$lib/settings/app-settings.svelte';
import { decodeFloat, splitNestedArray } from '@nodarium/utils';
import { splitNestedArray } from '@nodarium/utils';
import type { PerformanceStore } from '@nodarium/utils';
import { Canvas } from '@threlte/core';
import { DoubleSide, Vector3 } from 'three';
import { DoubleSide } from 'three';
import { type Group, MeshMatcapMaterial, TextureLoader } from 'three';
import { createGeometryPool, createInstancedGeometryPool } from './geometryPool';
import Scene from './Scene.svelte';
@@ -23,6 +23,7 @@
let geometryPool: ReturnType<typeof createGeometryPool>;
let instancePool: ReturnType<typeof createInstancedGeometryPool>;
export function updateGeometries(inputs: Int32Array[], group: Group) {
geometryPool = geometryPool || createGeometryPool(group, material);
instancePool = instancePool || createInstancedGeometryPool(group, material);
@@ -40,44 +41,16 @@
scene: Group;
centerCamera: boolean;
perf: PerformanceStore;
debugData?: Record<number, { type: string; data: Int32Array }>;
};
let { scene = $bindable(), centerCamera, perf }: Props = $props();
let lines = $state<Vector3[][]>([]);
function createLineGeometryFromEncodedData(encodedData: Int32Array) {
const positions: Vector3[] = [];
const amount = (encodedData.length - 1) / 4;
for (let i = 0; i < amount; i++) {
const x = decodeFloat(encodedData[2 + i * 4 + 0]);
const y = decodeFloat(encodedData[2 + i * 4 + 1]);
const z = decodeFloat(encodedData[2 + i * 4 + 2]);
positions.push(new Vector3(x, y, z));
}
return positions;
}
let { scene = $bindable(), centerCamera, debugData, perf }: Props = $props();
export const update = function update(result: Int32Array) {
perf.addPoint('split-result');
const inputs = splitNestedArray(result);
perf.endPoint();
if (appSettings.value.debug.showStemLines) {
perf.addPoint('create-lines');
lines = inputs
.map((input) => {
if (input[0] === 0) {
return createLineGeometryFromEncodedData(input);
}
})
.filter(Boolean) as Vector3[][];
perf.endPoint();
}
perf.addPoint('update-geometries');
const { totalVertices, totalFaces } = updateGeometries(inputs, scene);
@@ -89,7 +62,7 @@
};
</script>
{#if appSettings.value.debug.showPerformancePanel}
{#if appSettings.value.debug.advancedMode}
<SmallPerformanceViewer {fps} store={perf} />
{/if}
@@ -97,8 +70,8 @@
<Canvas>
<Scene
bind:this={sceneComponent}
{lines}
{centerCamera}
{debugData}
bind:scene
bind:fps
/>

View File

@@ -0,0 +1,90 @@
import { splitNestedArray } from '@nodarium/utils';
import {
BufferGeometry,
type Group,
InstancedMesh,
Line,
LineBasicMaterial,
Matrix4,
MeshBasicMaterial,
SphereGeometry,
Vector3
} from 'three';
function writePath(scene: Group, data: Int32Array): Vector3[] {
const positions: Vector3[] = [];
const f32 = new Float32Array(data.buffer);
for (let i = 2; i + 2 < f32.length; i += 4) {
const vec = new Vector3(f32[i], f32[i + 1], f32[i + 2]);
positions.push(vec);
}
// Path line
if (positions.length >= 2) {
const geometry = new BufferGeometry().setFromPoints(positions);
const line = new Line(
geometry,
new LineBasicMaterial({ color: 0xff0000, depthTest: false })
);
scene.add(line);
}
// Instanced spheres at points
if (positions.length > 0) {
const sphereGeometry = new SphereGeometry(0.05, 8, 8); // keep low-poly
const sphereMaterial = new MeshBasicMaterial({
color: 0xff0000,
depthTest: false
});
const spheres = new InstancedMesh(
sphereGeometry,
sphereMaterial,
positions.length
);
const matrix = new Matrix4();
for (let i = 0; i < positions.length; i++) {
matrix.makeTranslation(
positions[i].x,
positions[i].y,
positions[i].z
);
spheres.setMatrixAt(i, matrix);
}
spheres.instanceMatrix.needsUpdate = true;
scene.add(spheres);
}
return positions;
}
function clearGroup(group: Group) {
for (let i = group.children.length - 1; i >= 0; i--) {
const child = group.children[i];
group.remove(child);
// optional but correct: free GPU memory
// @ts-expect-error three.js runtime fields
child.geometry?.dispose?.();
// @ts-expect-error three.js runtime fields
child.material?.dispose?.();
}
}
export function updateDebugScene(
group: Group,
data: Record<number, { type: string; data: Int32Array }>
) {
clearGroup(group);
return Object.entries(data || {}).map(([, d]) => {
switch (d.type) {
case 'path':
splitNestedArray(d.data)
.forEach(p => writePath(group, p));
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return (_g: Group) => {};
}).flat();
}

View File

@@ -59,6 +59,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
private definitionMap: Map<string, NodeDefinition> = new Map();
private seed = Math.floor(Math.random() * 100000000);
private debugData: Record<number, { type: string; data: Int32Array }> = {};
perf?: PerformanceStore;
@@ -124,10 +125,10 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
}
}
const nodes = [];
const nodes = new Map<number, RuntimeNode>();
// loop through all the nodes and assign each nodes its depth
const stack = [outputNode];
const stack = [outputNode, ...graphNodes.filter(n => n.type.endsWith('/debug'))];
while (stack.length) {
const node = stack.pop();
if (!node) continue;
@@ -136,16 +137,31 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
parent.state.depth = node.state.depth + 1;
stack.push(parent);
}
nodes.push(node);
nodes.set(node.id, node);
}
return [outputNode, nodes] as const;
for (const node of graphNodes) {
if (node.type.endsWith('/debug')) {
node.state = node.state || {};
const parent = node.state.parents[0];
if (parent) {
node.state.depth = parent.state.depth - 1;
parent.state.debugNode = true;
}
nodes.set(node.id, node);
}
}
const _nodes = [...nodes.values()];
return [outputNode, _nodes] as const;
}
async execute(graph: Graph, settings: Record<string, unknown>) {
this.perf?.addPoint('runtime');
let a = performance.now();
this.debugData = {};
// Then we add some metadata to the graph
const [outputNode, nodes] = await this.addMetaData(graph);
@@ -237,6 +253,12 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
log.log(`Using cached value for ${node_type.id || node.id}`);
this.perf?.addPoint('cache-hit', 1);
results[node.id] = cachedValue as Int32Array;
if (node.state.debugNode && node_type.outputs) {
this.debugData[node.id] = {
type: node_type.outputs[0],
data: cachedValue
};
}
continue;
}
this.perf?.addPoint('cache-hit', 0);
@@ -245,6 +267,12 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
log.log(`Inputs:`, inputs);
a = performance.now();
results[node.id] = node_type.execute(encoded_inputs);
if (node.state.debugNode && node_type.outputs) {
this.debugData[node.id] = {
type: node_type.outputs[0],
data: results[node.id]
};
}
log.log('Executed', node.type, node.id);
b = performance.now();
@@ -273,6 +301,10 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
return res as unknown as Int32Array;
}
getDebugData() {
return this.debugData;
}
getPerformanceData() {
return this.perf?.get();
}

View File

@@ -5,6 +5,7 @@ type RuntimeState = {
parents: RuntimeNode[];
children: RuntimeNode[];
inputNodes: Record<string, RuntimeNode>;
debugNode?: boolean;
};
export type RuntimeNode = SerializedNode & { state: RuntimeState };

View File

@@ -1,3 +1,4 @@
import { debugNode } from '$lib/node-registry/debugNode';
import { IndexDBCache, RemoteNodeRegistry } from '$lib/node-registry/index';
import type { Graph } from '@nodarium/types';
import { createPerformanceStore } from '@nodarium/utils';
@@ -5,7 +6,7 @@ import { MemoryRuntimeExecutor } from './runtime-executor';
import { MemoryRuntimeCache } from './runtime-executor-cache';
const indexDbCache = new IndexDBCache('node-registry');
const nodeRegistry = new RemoteNodeRegistry('', indexDbCache);
const nodeRegistry = new RemoteNodeRegistry('', indexDbCache, [debugNode]);
const cache = new MemoryRuntimeCache();
const executor = new MemoryRuntimeExecutor(nodeRegistry, cache);
@@ -43,3 +44,7 @@ export async function executeGraph(
export function getPerformanceData() {
return performanceStore.get();
}
export function getDebugData() {
return executor.getDebugData();
}

View File

@@ -6,12 +6,15 @@ export class WorkerRuntimeExecutor implements RuntimeExecutor {
new URL(`./worker-runtime-executor-backend.ts`, import.meta.url)
);
async execute(graph: Graph, settings: Record<string, unknown>) {
execute(graph: Graph, settings: Record<string, unknown>) {
return this.worker.executeGraph(graph, settings);
}
async getPerformanceData() {
getPerformanceData() {
return this.worker.getPerformanceData();
}
getDebugData() {
return this.worker.getDebugData();
}
set useRuntimeCache(useCache: boolean) {
this.worker.setUseRuntimeCache(useCache);
}

View File

@@ -59,34 +59,9 @@ export const AppSettingTypes = {
label: 'Execute in WebWorker',
value: true
},
showIndices: {
advancedMode: {
type: 'boolean',
label: 'Show Indices',
value: false
},
showPerformancePanel: {
type: 'boolean',
label: 'Show Performance Panel',
value: false
},
showBenchmarkPanel: {
type: 'boolean',
label: 'Show Benchmark Panel',
value: false
},
showVertices: {
type: 'boolean',
label: 'Show Vertices',
value: false
},
showStemLines: {
type: 'boolean',
label: 'Show Stem Lines',
value: false
},
showGraphJson: {
type: 'boolean',
label: 'Show Graph Source',
label: 'Advanced Mode',
value: false
},
cache: {

View File

@@ -42,11 +42,13 @@
const store: Store = {};
Object.keys(inputs).forEach((key) => {
if (props) {
const value = props[key] || inputs[key].value;
const value = props[key] !== undefined ? props[key] : inputs[key].value;
if (Array.isArray(value) || typeof value === 'number') {
store[key] = value;
} else if (typeof value === 'boolean') {
store[key] = value ? 1 : 0;
} else {
console.error('Wrong error');
console.error('Wrong error', { value });
}
}
});

View File

@@ -4,6 +4,7 @@
import Grid from '$lib/grid';
import { debounceAsyncFunction } from '$lib/helpers';
import { createKeyMap } from '$lib/helpers/createKeyMap';
import { debugNode } from '$lib/node-registry/debugNode.js';
import { IndexDBCache, RemoteNodeRegistry } from '$lib/node-registry/index';
import NodeStore from '$lib/node-store/NodeStore.svelte';
import PerformanceViewer from '$lib/performance/PerformanceViewer.svelte';
@@ -32,7 +33,8 @@
const { data } = $props();
const registryCache = new IndexDBCache('node-registry');
const nodeRegistry = new RemoteNodeRegistry('', registryCache);
const nodeRegistry = new RemoteNodeRegistry('', registryCache, [debugNode]);
const workerRuntime = new WorkerRuntimeExecutor();
const runtimeCache = new MemoryRuntimeCache();
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
@@ -66,6 +68,7 @@
let sidebarOpen = $state(false);
let graphInterface = $state<ReturnType<typeof GraphInterface>>(null!);
let viewerComponent = $state<ReturnType<typeof Viewer>>();
let debugData = $state<Record<number, { type: string; data: Int32Array }>>();
const manager = $derived(graphInterface?.manager);
async function randomGenerate() {
@@ -105,6 +108,7 @@
if (appSettings.value.debug.useWorker) {
let perfData = await runtime.getPerformanceData();
debugData = await runtime.getDebugData();
let lastRun = perfData?.at(-1);
if (lastRun?.total) {
lastRun.runtime = lastRun.total;
@@ -163,6 +167,7 @@
bind:scene
bind:this={viewerComponent}
perf={performanceStore}
debugData={debugData}
centerCamera={appSettings.value.centerCamera}
/>
</Grid.Cell>
@@ -216,7 +221,7 @@
<Panel
id="performance"
title="Performance"
hidden={!appSettings.value.debug.showPerformancePanel}
hidden={!appSettings.value.debug.advancedMode}
icon="i-[tabler--brand-speedtest] bg-red-400"
>
{#if $performanceStore}
@@ -229,7 +234,7 @@
<Panel
id="graph-source"
title="Graph Source"
hidden={!appSettings.value.debug.showGraphJson}
hidden={!appSettings.value.debug.advancedMode}
icon="i-[tabler--code]"
>
<GraphSource graph={pm.graph ?? manager?.serialize()} />
@@ -237,7 +242,7 @@
<Panel
id="benchmark"
title="Benchmark"
hidden={!appSettings.value.debug.showBenchmarkPanel}
hidden={!appSettings.value.debug.advancedMode}
icon="i-[tabler--graph] bg-red-400"
>
<BenchmarkPanel run={randomGenerate} />

View File

@@ -3,6 +3,7 @@
"scripts": {
"postinstall": "pnpm run -r --filter 'ui' build",
"lint": "pnpm run -r --parallel lint",
"qa": "pnpm lint && pnpm check && pnpm test",
"format": "pnpm dprint fmt",
"format:check": "pnpm dprint check",
"test": "pnpm run -r --parallel test",

View File

@@ -89,6 +89,12 @@ export const NodeInputPathSchema = z.object({
value: z.array(z.number()).optional()
});
export const NodeInputAnySchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal('*'),
value: z.any().optional()
});
export const NodeInputSchema = z.union([
NodeInputSeedSchema,
NodeInputBooleanSchema,
@@ -100,7 +106,8 @@ export const NodeInputSchema = z.union([
NodeInputSeedSchema,
NodeInputVec3Schema,
NodeInputGeometrySchema,
NodeInputPathSchema
NodeInputPathSchema,
NodeInputAnySchema
]);
export type NodeInput = z.infer<typeof NodeInputSchema>;

View File

@@ -3,10 +3,10 @@
prefix: "i";
}
@source inline("{hover:,}{bg-,outline-,text-,}layer-0");
@source inline("{hover:,}{bg-,outline-,text-,}layer-1");
@source inline("{hover:,}{bg-,outline-,text-,}layer-2");
@source inline("{hover:,}{bg-,outline-,text-,}layer-3");
@source inline("{hover:,}{bg-,outline-,text-,}layer-0{/30,/50,}");
@source inline("{hover:,}{bg-,outline-,text-,}layer-1{/30,/50,}");
@source inline("{hover:,}{bg-,outline-,text-,}layer-2{/30,/50,}");
@source inline("{hover:,}{bg-,outline-,text-,}layer-3{/30,/50,}");
@source inline("{hover:,}{bg-,outline-,text-,}active");
@source inline("{hover:,}{bg-,outline-,text-,}selected");
@source inline("{hover:,}{bg-,outline-,text-,}outline{!,}");
@@ -80,6 +80,7 @@ html {
--neutral-100: #e7e7e7;
--neutral-200: #cecece;
--neutral-300: #7c7c7c;
--neutral-350: #808080;
--neutral-400: #2d2d2d;
--neutral-500: #171717;
--neutral-800: #111111;
@@ -107,7 +108,7 @@ body {
html.theme-light {
--color-text: var(--neutral-800);
--color-outline: var(--neutral-300);
--color-outline: var(--neutral-350);
--color-layer-0: var(--neutral-050);
--color-layer-1: var(--neutral-100);
--color-layer-2: var(--neutral-200);

View File

@@ -126,7 +126,7 @@
<button
aria-label="step down"
onmousedown={stepDown}
class="cursor-pointer w-4 bg-layer-3 opacity-30 hover:opacity-50"
class="cursor-pointer w-4 bg-layer-3/30 hover:bg-layer-3/50"
>
<span class="i-[tabler--chevron-compact-left] block h-full w-full text-outline!"></span>
</button>
@@ -161,7 +161,7 @@
<button
aria-label="step up"
onmousedown={stepUp}
class="cursor-pointer w-4 bg-layer-3 opacity-30 hover:opacity-50"
class="cursor-pointer w-4 bg-layer-3/30 hover:bg-layer-3/50"
>
<span class="i-[tabler--chevron-compact-right] block h-full w-full text-outline!"></span>
</button>

View File

@@ -73,7 +73,7 @@
<InputCheckbox bind:value={mirrorShape} />
<p>mirror</p>
</label>
<p>{JSON.stringify(points)}</p>
<p class="max-w-full overflow-hidden">{JSON.stringify(points)}</p>
{/snippet}
<div style:width="300px">
<InputShape bind:value={points} mirror={mirrorShape} />

View File

@@ -12,7 +12,7 @@
<section class="border-outline border-1/2 bg-layer-1 rounded border mb-4 p-4 flex flex-col gap-4 {_class}">
<h3 class="flex gap-2 font-bold">
{title}
<div class="flex gap-4 w-full font-normal opacity-50 max-w-[75%] whitespace-pre overflow-hidden text-clip">
<div class="flex gap-4 w-full font-normal opacity-50 max-w-[75%]">
{#if header}
{@render header()}
{:else}

View File

@@ -1,5 +1,10 @@
import { expect, test } from 'vitest';
import { concatEncodedArrays, decodeNestedArray, encodeNestedArray } from './flatTree';
import {
concatEncodedArrays,
decodeNestedArray,
encodeNestedArray,
splitNestedArray
} from './flatTree';
test('it correctly concats nested arrays', () => {
const input_a = encodeNestedArray([1, 2, 3]);
@@ -82,3 +87,80 @@ test('it correctly handles arrays with mixed data types', () => {
const decoded = decodeNestedArray(encodeNestedArray(input));
expect(decoded).toEqual(input);
});
// Test splitNestedArray function
test('it splits nested array into segments based on structure', () => {
const input = [[1, 2], [3, 4]];
const encoded = new Int32Array(encodeNestedArray(input));
const split = splitNestedArray(encoded);
// Based on the actual behavior, splitNestedArray returns segments
// but the specific behavior needs to match the implementation
expect(Array.isArray(split)).toBe(true);
expect(split.length).toBe(2);
expect(split[0][0]).toBe(1);
expect(split[0][1]).toBe(2);
expect(split[1][0]).toBe(3);
expect(split[1][1]).toBe(4);
});
// Test splitNestedArray function
test('it splits nested array into segments based on structure 2', () => {
// dprint-ignore
const encoded = new Int32Array([
0, 1,
0, 19,
0, 1,
0, 0, 0, 1060487823,
1067592955, 1079491492, -1086248132, 1056069822,
-1078247113, 1086620820, 1073133800, 1047681214,
-1068353940, 1094067306, 1078792112, 0,
1, 1,
0, 19,
0, 1,
0, 0, 0, 1060487823,
-1089446963, 1080524584, 1041006274, 1056069822,
-1092176382, 1087031528, -1088851934, 1047681214,
1081482392, 1094426140, -1107842261, 0,
1, 1,
1, 1
]);
// Should be split into two seperate arrays
const split = splitNestedArray(encoded);
// Based on the actual behavior, splitNestedArray returns segments
// but the specific behavior needs to match the implementation
expect(Array.isArray(split)).toBe(true);
expect(split.length).toBe(2);
expect(split[0][0]).toBe(0);
expect(split[0][1]).toBe(1);
expect(split[1][0]).toBe(0);
expect(split[1][1]).toBe(1);
});
// Test splitNestedArray function
test('it splits nested array into segments based on structure 2', () => {
// dprint-ignore
const encoded = new Int32Array( [
0, 1,
0, 27,
0, 1,
0, 0, 0, 1065353216,
0, 1067757391, 0, 1061997773,
0, 1076145999, 0, 1058642330,
0, 1081542391, 0, 1053609164,
0, 1084534607, 0, 1045220556,
0, 1087232803, 0, 0,
1, 1,
1, 1
]);
// Should be split into two seperate arrays
const split = splitNestedArray(encoded);
// Based on the actual behavior, splitNestedArray returns segments
// but the specific behavior needs to match the implementation
expect(Array.isArray(split)).toBe(true);
expect(split.length).toBe(1);
});