feat: merge svelte-5
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 38s

This commit is contained in:
max_richter 2024-12-19 18:31:19 +01:00
commit a740da1099
45 changed files with 2270 additions and 2837 deletions

View File

@ -13,34 +13,34 @@
"@nodes/registry": "link:../packages/registry", "@nodes/registry": "link:../packages/registry",
"@nodes/ui": "link:../packages/ui", "@nodes/ui": "link:../packages/ui",
"@nodes/utils": "link:../packages/utils", "@nodes/utils": "link:../packages/utils",
"@sveltejs/kit": "^2.7.4", "@sveltejs/kit": "^2.12.2",
"@threlte/core": "8.0.0-next.23", "@threlte/core": "8.0.0-next.23",
"@threlte/extras": "9.0.0-next.33", "@threlte/extras": "9.0.0-next.33",
"@types/three": "^0.169.0", "@types/three": "^0.171.0",
"@unocss/reset": "^0.63.6", "@unocss/reset": "^0.65.2",
"comlink": "^4.4.1", "comlink": "^4.4.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"idb": "^8.0.0", "idb": "^8.0.1",
"jsondiffpatch": "^0.6.0", "jsondiffpatch": "^0.6.0",
"three": "^0.170.0" "three": "^0.171.0"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/tabler": "^1.2.7", "@iconify-json/tabler": "^1.2.13",
"@nodes/types": "link:../packages/types", "@nodes/types": "link:../packages/types",
"@sveltejs/adapter-static": "^3.0.6", "@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tsconfig/svelte": "^5.0.4", "@tsconfig/svelte": "^5.0.4",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@unocss/preset-icons": "^0.63.6", "@unocss/preset-icons": "^0.65.2",
"svelte": "^5.1.9", "svelte": "^5.14.4",
"svelte-check": "^4.0.5", "svelte-check": "^4.1.1",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.6.3", "typescript": "^5.7.2",
"unocss": "^0.63.6", "unocss": "^0.65.2",
"vite": "^5.4.10", "vite": "^6.0.4",
"vite-plugin-comlink": "^5.1.0", "vite-plugin-comlink": "^5.1.0",
"vite-plugin-glsl": "^1.3.0", "vite-plugin-glsl": "^1.3.1",
"vite-plugin-wasm": "^3.3.0", "vite-plugin-wasm": "^3.3.0",
"vitest": "^2.1.4" "vitest": "^2.1.8"
} }
} }

View File

@ -15,7 +15,7 @@
var value = JSON.parse(store); var value = JSON.parse(store);
var themes = ["dark", "light", "catppuccin"]; var themes = ["dark", "light", "catppuccin"];
if (themes[value.theme]) { if (themes[value.theme]) {
document.body.classList.add("theme-" + themes[value.theme]); document.documentElement.classList.add("theme-" + themes[value.theme]);
} }
} catch (e) { } } catch (e) { }
} }

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { NodeDefinition, NodeRegistry } from "@nodes/types"; import type { NodeDefinition, NodeRegistry } from "@nodes/types";
import { onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
let mx = $state(0); let mx = $state(0);
let my = $state(0); let my = $state(0);
@ -39,9 +39,10 @@
} }
onMount(() => { onMount(() => {
wrapper?.parentElement?.setAttribute("style", "cursor:help !important"); const style = wrapper.parentElement?.style;
style?.setProperty("cursor", "help");
return () => { return () => {
wrapper?.parentElement?.style.removeProperty("cursor"); style?.removeProperty("cursor");
}; };
}); });
</script> </script>
@ -91,8 +92,9 @@
border-radius: 5px; border-radius: 5px;
top: 10px; top: 10px;
left: 10px; left: 10px;
max-width: 250px;
border: 1px solid var(--outline); border: 1px solid var(--outline);
z-index: 1000; z-index: 10000;
display: none; display: none;
} }

View File

@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import { T } from "@threlte/core"; import { T } from "@threlte/core";
import BackgroundVert from "./Background.vert"; import BackgroundVert from "./Background.vert";
import BackgroundFrag from "./Background.frag"; import BackgroundFrag from "./Background.frag";
import { colors } from "../graph/state.svelte"; import { colors } from "../graph/colors.svelte";
import { Color } from "three"; import { Color } from "three";
import { appSettings } from "$lib/settings/app-settings.svelte";
type Props = { type Props = {
minZoom: number; minZoom: number;
@ -42,10 +42,10 @@
value: [0, 1, 0], value: [0, 1, 0],
}, },
backgroundColor: { backgroundColor: {
value: new Color(0x171717), value: colors["layer-0"].clone(),
}, },
lineColor: { lineColor: {
value: new Color(0x111111), value: colors["outline"].clone(),
}, },
zoomLimits: { zoomLimits: {
value: [2, 50], value: [2, 50],
@ -55,8 +55,9 @@
}, },
}} }}
uniforms.camPos.value={cameraPosition} uniforms.camPos.value={cameraPosition}
uniforms.backgroundColor.value={$colors["layer-0"]} uniforms.backgroundColor.value={appSettings.theme &&
uniforms.lineColor.value={$colors["outline"]} colors["layer-0"].clone()}
uniforms.lineColor.value={appSettings.theme && colors["outline"].clone()}
uniforms.zoomLimits.value={[minZoom, maxZoom]} uniforms.zoomLimits.value={[minZoom, maxZoom]}
uniforms.dimensions.value={[width, height]} uniforms.dimensions.value={[width, height]}
/> />

View File

@ -1,13 +1,15 @@
<script module lang="ts"> <script module lang="ts">
import { colors } from "../graph/state.svelte"; import { colors } from "../graph/colors.svelte";
const circleMaterial = new MeshBasicMaterial({ const circleMaterial = new MeshBasicMaterial({
color: get(colors).edge, color: colors.edge.clone(),
toneMapped: false, toneMapped: false,
}); });
$effect.root(() => {
colors.subscribe((c) => { $effect(() => {
circleMaterial.color.copy(c.edge.clone().convertSRGBToLinear()); appSettings.theme;
circleMaterial.color = colors.edge.clone().convertSRGBToLinear();
});
}); });
const lineCache = new Map<number, BufferGeometry>(); const lineCache = new Map<number, BufferGeometry>();
@ -27,19 +29,22 @@
import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js"; import { CubicBezierCurve } from "three/src/extras/curves/CubicBezierCurve.js";
import { Vector2 } from "three/src/math/Vector2.js"; import { Vector2 } from "three/src/math/Vector2.js";
import { createEdgeGeometry } from "./createEdgeGeometry.js"; import { createEdgeGeometry } from "./createEdgeGeometry.js";
import { get } from "svelte/store"; import { appSettings } from "$lib/settings/app-settings.svelte";
type Props = { type Props = {
from: { x: number; y: number }; from: { x: number; y: number };
to: { x: number; y: number }; to: { x: number; y: number };
z: number;
}; };
const { from, to }: Props = $props(); const { from, to, z }: Props = $props();
let samples = 5;
let geometry: BufferGeometry | null = $state(null); let geometry: BufferGeometry | null = $state(null);
const lineColor = $derived(
appSettings.theme && colors.edge.clone().convertSRGBToLinear(),
);
let lastId: number | null = null; let lastId: number | null = null;
const primeA = 31; const primeA = 31;
@ -63,7 +68,8 @@
const length = Math.floor( const length = Math.floor(
Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4, Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4,
); );
samples = Math.min(Math.max(10, length), 60) * 2;
const samples = Math.max(length * 16, 10);
curve.v0.set(0, 0); curve.v0.set(0, 0);
curve.v1.set(mid.x, 0); curve.v1.set(mid.x, 0);
@ -77,15 +83,13 @@
geometry = createEdgeGeometry(points); geometry = createEdgeGeometry(points);
lineCache.set(curveId, geometry); lineCache.set(curveId, geometry);
}; }
$effect(() => { $effect(() => {
if (from || to) { if (from || to) {
update(); update();
} }
}); });
const lineColor = $derived($colors.edge.clone().convertSRGBToLinear());
</script> </script>
<T.Mesh <T.Mesh
@ -110,6 +114,6 @@
{#if geometry} {#if geometry}
<T.Mesh position.x={from.x} position.z={from.y} position.y={0.1} {geometry}> <T.Mesh position.x={from.x} position.z={from.y} position.y={0.1} {geometry}>
<MeshLineMaterial width={3} attenuate={false} color={lineColor} /> <MeshLineMaterial width={Math.max(z * 0.0001, 0.00001)} color={lineColor} />
</T.Mesh> </T.Mesh>
{/if} {/if}

View File

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

View File

@ -1,11 +1,29 @@
import { BufferGeometry, Vector3, BufferAttribute } from 'three' import { BufferAttribute, BufferGeometry, Vector3 } from 'three';
import { setXY, setXYZ, setXYZW, setXYZXYZ } from './utils.js' import { setXY, setXYZ, setXYZW, setXYZXYZ } from './utils.js';
export function createEdgeGeometry(points: Vector3[]) { export function createEdgeGeometry(points: Vector3[]) {
let shape = 'none' const length = points[0].distanceTo(points[points.length - 1]);
let shapeFunction = (p: number) => 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 // When the component first runs we create the buffer geometry and allocate the buffer attributes
let pointCount = points.length let pointCount = points.length
@ -19,9 +37,7 @@ export function createEdgeGeometry(points: Vector3[]) {
let indices: number[] = [] let indices: number[] = []
let indicesIndex = 0 let indicesIndex = 0
if (shape === 'taper') {
shapeFunction = (p: number) => 1 * Math.pow(4 * p * (1 - p), 1)
}
for (let j = 0; j < pointCount; j++) { for (let j = 0; j < pointCount; j++) {
const c = j / points.length const c = j / points.length
@ -30,7 +46,7 @@ export function createEdgeGeometry(points: Vector3[]) {
counterIndex += 2 counterIndex += 2
setXY(side, doubleIndex, 1, -1) setXY(side, doubleIndex, 1, -1)
let width = shape === 'none' ? 1 : shapeFunction(j / (pointCount - 1)) let width = shapeFunction((j / (pointCount - 1)))
setXY(widthArray, doubleIndex, width, width) setXY(widthArray, doubleIndex, width, width)
doubleIndex += 2 doubleIndex += 2

View File

@ -6,7 +6,6 @@
} from "../helpers/index.js"; } from "../helpers/index.js";
import type { OrthographicCamera } from "three"; import type { OrthographicCamera } from "three";
import Background from "../background/Background.svelte"; import Background from "../background/Background.svelte";
import type { GraphManager } from "../graph-manager.js";
import { getContext, onMount, setContext } from "svelte"; import { getContext, onMount, setContext } from "svelte";
import Camera from "../Camera.svelte"; import Camera from "../Camera.svelte";
import GraphView from "./GraphView.svelte"; import GraphView from "./GraphView.svelte";
@ -23,14 +22,13 @@
import { Canvas } from "@threlte/core"; import { Canvas } from "@threlte/core";
import { getGraphManager } from "./context.js"; import { getGraphManager } from "./context.js";
const state = getGraphState(); const graphState = getGraphState();
export let snapToGrid = true; export let snapToGrid = true;
export let showGrid = true; export let showGrid = true;
export let showHelp = false; export let showHelp = false;
let keymap = const keymap = getContext<ReturnType<typeof createKeyMap>>("keymap");
getContext<ReturnType<typeof createKeyMap>>("keymap") || createKeyMap([]);
const manager = getGraphManager(); const manager = getGraphManager();
@ -179,7 +177,7 @@
} }
setContext("setDownSocket", (socket: Socket) => { setContext("setDownSocket", (socket: Socket) => {
state.activeSocket = socket; graphState.activeSocket = socket;
let { node, index, position } = socket; let { node, index, position } = socket;
@ -198,14 +196,14 @@
} }
mouseDown = position; mouseDown = position;
state.activeSocket = { graphState.activeSocket = {
node, node,
index, index,
position, position,
}; };
state.possibleSockets = manager graphState.possibleSockets = manager
.getPossibleSockets(state.activeSocket) .getPossibleSockets(graphState.activeSocket)
.map(([node, index]) => { .map(([node, index]) => {
return { return {
node, node,
@ -259,14 +257,15 @@
let my = event.clientY - rect.y; let my = event.clientY - rect.y;
mousePosition = projectScreenToWorld(mx, my); mousePosition = projectScreenToWorld(mx, my);
hoveredNodeId = getNodeIdFromEvent(event);
if (!mouseDown) return; if (!mouseDown) return;
// we are creating a new edge here // we are creating a new edge here
if (state.activeSocket || state.possibleSockets?.length) { if (graphState.activeSocket || graphState.possibleSockets?.length) {
let smallestDist = 1000; let smallestDist = 1000;
let _socket; let _socket;
for (const socket of state.possibleSockets) { for (const socket of graphState.possibleSockets) {
const dist = Math.sqrt( const dist = Math.sqrt(
(socket.position[0] - mousePosition[0]) ** 2 + (socket.position[0] - mousePosition[0]) ** 2 +
(socket.position[1] - mousePosition[1]) ** 2, (socket.position[1] - mousePosition[1]) ** 2,
@ -279,9 +278,9 @@
if (_socket && smallestDist < 0.9) { if (_socket && smallestDist < 0.9) {
mousePosition = _socket.position; mousePosition = _socket.position;
state.hoveredSocket = _socket; graphState.hoveredSocket = _socket;
} else { } else {
state.hoveredSocket = null; graphState.hoveredSocket = null;
} }
return; return;
} }
@ -301,17 +300,17 @@
const y = node.position[1]; const y = node.position[1];
const height = getNodeHeight(node.type); const height = getNodeHeight(node.type);
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) { if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
state.selectedNodes?.add(node.id); graphState.selectedNodes?.add(node.id);
} else { } else {
state.selectedNodes?.delete(node.id); graphState.selectedNodes?.delete(node.id);
} }
} }
return; return;
} }
// here we are handling dragging of nodes // here we are handling dragging of nodes
if (state.activeNodeId !== -1 && mouseDownId !== -1) { if (graphState.activeNodeId !== -1 && mouseDownId !== -1) {
const node = manager.getNode(state.activeNodeId); const node = manager.getNode(graphState.activeNodeId);
if (!node || event.buttons !== 1) return; if (!node || event.buttons !== 1) return;
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
@ -340,8 +339,8 @@
const vecX = oldX - newX; const vecX = oldX - newX;
const vecY = oldY - newY; const vecY = oldY - newY;
if (state.selectedNodes?.size) { if (graphState.selectedNodes?.size) {
for (const nodeId of state.selectedNodes) { for (const nodeId of graphState.selectedNodes) {
const n = manager.getNode(nodeId); const n = manager.getNode(nodeId);
if (!n?.tmp) continue; if (!n?.tmp) continue;
n.tmp.x = (n?.tmp?.downX || 0) - vecX; n.tmp.x = (n?.tmp?.downX || 0) - vecX;
@ -360,6 +359,7 @@
} }
// here we are handling panning of camera // here we are handling panning of camera
isPanning = true;
let newX = cameraDown[0] - (mx - mouseDown[0]) / cameraPosition[2]; let newX = cameraDown[0] - (mx - mouseDown[0]) / cameraPosition[2];
let newY = cameraDown[1] - (my - mouseDown[1]) / cameraPosition[2]; let newY = cameraDown[1] - (my - mouseDown[1]) / cameraPosition[2];
@ -424,43 +424,46 @@
// if we clicked on a node // if we clicked on a node
if (clickedNodeId !== -1) { if (clickedNodeId !== -1) {
if (state.activeNodeId === -1) { if (graphState.activeNodeId === -1) {
state.activeNodeId = clickedNodeId; graphState.activeNodeId = clickedNodeId;
// if the selected node is the same as the clicked node // if the selected node is the same as the clicked node
} else if (state.activeNodeId === clickedNodeId) { } else if (graphState.activeNodeId === clickedNodeId) {
//$activeNodeId = -1; //$activeNodeId = -1;
// if the clicked node is different from the selected node and secondary // if the clicked node is different from the selected node and secondary
} else if (event.ctrlKey) { } else if (event.ctrlKey) {
state.selectedNodes = state.selectedNodes || new Set(); graphState.selectedNodes.add(graphState.activeNodeId);
state.selectedNodes.add(state.activeNodeId); graphState.selectedNodes.delete(clickedNodeId);
state.selectedNodes.delete(clickedNodeId); graphState.activeNodeId = clickedNodeId;
state.activeNodeId = clickedNodeId;
// select the node // select the node
} else if (event.shiftKey) { } else if (event.shiftKey) {
const activeNode = manager.getNode(state.activeNodeId); const activeNode = manager.getNode(graphState.activeNodeId);
const newNode = manager.getNode(clickedNodeId); const newNode = manager.getNode(clickedNodeId);
if (activeNode && newNode) { if (activeNode && newNode) {
const edge = manager.getNodesBetween(activeNode, newNode); const edge = manager.getNodesBetween(activeNode, newNode);
if (edge) { if (edge) {
const selected = new Set(edge.map((n) => n.id)); graphState.selectedNodes.clear();
selected.add(clickedNodeId); for (const node of edge) {
state.selectedNodes = selected; graphState.selectedNodes.add(node.id);
}
graphState.selectedNodes.add(clickedNodeId);
} }
} }
} else if (!state.selectedNodes?.has(clickedNodeId)) { } else if (!graphState.selectedNodes.has(clickedNodeId)) {
state.activeNodeId = clickedNodeId; graphState.activeNodeId = clickedNodeId;
state.clearSelection(); graphState.clearSelection();
} }
} else if (event.ctrlKey) { } else if (event.ctrlKey) {
boxSelection = true; boxSelection = true;
} }
const node = manager.getNode(state.activeNodeId);
const node = manager.getNode(graphState.activeNodeId);
if (!node) return; if (!node) return;
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.downX = node.position[0]; node.tmp.downX = node.position[0];
node.tmp.downY = node.position[1]; node.tmp.downY = node.position[1];
if (state.selectedNodes) {
for (const nodeId of state.selectedNodes) { if (graphState.selectedNodes) {
for (const nodeId of graphState.selectedNodes) {
const n = manager.getNode(nodeId); const n = manager.getNode(nodeId);
if (!n) continue; if (!n) continue;
n.tmp = n.tmp || {}; n.tmp = n.tmp || {};
@ -471,8 +474,12 @@
} }
function copyNodes() { function copyNodes() {
if (state.activeNodeId === -1 && !state.selectedNodes?.size) return; if (graphState.activeNodeId === -1 && !graphState.selectedNodes?.size)
let _nodes = [state.activeNodeId, ...(state.selectedNodes?.values() || [])] return;
let _nodes = [
graphState.activeNodeId,
...(graphState.selectedNodes?.values() || []),
]
.map((id) => manager.getNode(id)) .map((id) => manager.getNode(id))
.filter(Boolean) as Node[]; .filter(Boolean) as Node[];
@ -508,7 +515,10 @@
.filter(Boolean) as Node[]; .filter(Boolean) as Node[];
const newNodes = manager.createGraph(_nodes, clipboard.edges); const newNodes = manager.createGraph(_nodes, clipboard.edges);
state.selectedNodes = new Set(newNodes.map((n) => n.id)); graphState.selectedNodes.clear();
for (const node of newNodes) {
graphState.selectedNodes.add(node.id);
}
} }
const isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT"; const isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT";
@ -517,12 +527,14 @@
key: "l", key: "l",
description: "Select linked nodes", description: "Select linked nodes",
callback: () => { callback: () => {
const activeNode = manager.getNode(state.activeNodeId); const activeNode = manager.getNode(graphState.activeNodeId);
if (activeNode) { if (activeNode) {
const nodes = manager.getLinkedNodes(activeNode); const nodes = manager.getLinkedNodes(activeNode);
state.selectedNodes = new Set(nodes.map((n) => n.id)); graphState.selectedNodes.clear();
for (const node of nodes) {
graphState.selectedNodes.add(node.id);
}
} }
console.log(activeNode);
}, },
}); });
@ -552,8 +564,8 @@
key: "Escape", key: "Escape",
description: "Deselect nodes", description: "Deselect nodes",
callback: () => { callback: () => {
state.activeNodeId = -1; graphState.activeNodeId = -1;
state.clearSelection(); graphState.clearSelection();
(document.activeElement as HTMLElement)?.blur(); (document.activeElement as HTMLElement)?.blur();
}, },
}); });
@ -605,7 +617,9 @@
description: "Select all nodes", description: "Select all nodes",
callback: () => { callback: () => {
if (!isBodyFocused()) return; if (!isBodyFocused()) return;
state.selectedNodes = new Set($nodes.keys()); for (const node of $nodes.keys()) {
graphState.selectedNodes.add(node);
}
}, },
}); });
@ -654,38 +668,39 @@
callback: (event) => { callback: (event) => {
if (!isBodyFocused()) return; if (!isBodyFocused()) return;
manager.startUndoGroup(); manager.startUndoGroup();
if (state.activeNodeId !== -1) { if (graphState.activeNodeId !== -1) {
const node = manager.getNode(state.activeNodeId); const node = manager.getNode(graphState.activeNodeId);
if (node) { if (node) {
manager.removeNode(node, { restoreEdges: event.ctrlKey }); manager.removeNode(node, { restoreEdges: event.ctrlKey });
state.activeNodeId = -1; graphState.activeNodeId = -1;
} }
} }
if (state.selectedNodes) { if (graphState.selectedNodes) {
for (const nodeId of state.selectedNodes) { for (const nodeId of graphState.selectedNodes) {
const node = manager.getNode(nodeId); const node = manager.getNode(nodeId);
if (node) { if (node) {
manager.removeNode(node, { restoreEdges: event.ctrlKey }); manager.removeNode(node, { restoreEdges: event.ctrlKey });
} }
} }
state.clearSelection(); graphState.clearSelection();
} }
manager.saveUndoGroup(); manager.saveUndoGroup();
}, },
}); });
function handleMouseUp(event: MouseEvent) { function handleMouseUp(event: MouseEvent) {
isPanning = false;
if (!mouseDown) return; if (!mouseDown) return;
const activeNode = manager.getNode(state.activeNodeId); const activeNode = manager.getNode(graphState.activeNodeId);
const clickedNodeId = getNodeIdFromEvent(event); const clickedNodeId = getNodeIdFromEvent(event);
if (clickedNodeId !== -1) { if (clickedNodeId !== -1) {
if (activeNode) { if (activeNode) {
if (!activeNode?.tmp?.isMoving && !event.ctrlKey && !event.shiftKey) { if (!activeNode?.tmp?.isMoving && !event.ctrlKey && !event.shiftKey) {
state.clearSelection(); graphState.activeNodeId = clickedNodeId;
state.activeNodeId = clickedNodeId; graphState.clearSelection();
} }
} }
} }
@ -708,7 +723,7 @@
activeNode.position[1] = activeNode?.tmp?.y ?? activeNode.position[1]; activeNode.position[1] = activeNode?.tmp?.y ?? activeNode.position[1];
} }
const nodes = [ const nodes = [
...[...(state.selectedNodes?.values() || [])].map((id) => ...[...(graphState.selectedNodes?.values() || [])].map((id) =>
manager.getNode(id), manager.getNode(id),
), ),
] as NodeType[]; ] as NodeType[];
@ -747,26 +762,26 @@
$edges = $edges; $edges = $edges;
}); });
manager.save(); manager.save();
} else if (state.hoveredSocket && state.activeSocket) { } else if (graphState.hoveredSocket && graphState.activeSocket) {
if ( if (
typeof state.hoveredSocket.index === "number" && typeof graphState.hoveredSocket.index === "number" &&
typeof state.activeSocket.index === "string" typeof graphState.activeSocket.index === "string"
) { ) {
manager.createEdge( manager.createEdge(
state.hoveredSocket.node, graphState.hoveredSocket.node,
state.hoveredSocket.index || 0, graphState.hoveredSocket.index || 0,
state.activeSocket.node, graphState.activeSocket.node,
state.activeSocket.index, graphState.activeSocket.index,
); );
} else if ( } else if (
typeof state.activeSocket.index == "number" && typeof graphState.activeSocket.index == "number" &&
typeof state.hoveredSocket.index === "string" typeof graphState.hoveredSocket.index === "string"
) { ) {
manager.createEdge( manager.createEdge(
state.activeSocket.node, graphState.activeSocket.node,
state.activeSocket.index || 0, graphState.activeSocket.index || 0,
state.hoveredSocket.node, graphState.hoveredSocket.node,
state.hoveredSocket.index, graphState.hoveredSocket.index,
); );
} }
manager.save(); manager.save();
@ -780,22 +795,25 @@
cameraDown[1] === cameraPosition[1] && cameraDown[1] === cameraPosition[1] &&
isBodyFocused() isBodyFocused()
) { ) {
state.activeNodeId = -1; graphState.activeNodeId = -1;
state.clearSelection(); graphState.clearSelection();
} }
mouseDown = null; mouseDown = null;
boxSelection = false; boxSelection = false;
state.activeSocket = null; graphState.activeSocket = null;
state.possibleSockets = []; graphState.possibleSockets = [];
state.hoveredSocket = null; graphState.hoveredSocket = null;
addMenuPosition = null; addMenuPosition = null;
} }
let isPanning = false;
let isDragging = false; let isDragging = false;
let hoveredNodeId = -1;
function handleMouseLeave() { function handleMouseLeave() {
isDragging = false; isDragging = false;
isPanning = false;
} }
function handleDrop(event: DragEvent) { function handleDrop(event: DragEvent) {
@ -865,16 +883,19 @@
function handleDragEnter(e: DragEvent) { function handleDragEnter(e: DragEvent) {
e.preventDefault(); e.preventDefault();
isDragging = true; isDragging = true;
isPanning = false;
} }
function handlerDragOver(e: DragEvent) { function handlerDragOver(e: DragEvent) {
isDragging = true;
e.preventDefault(); e.preventDefault();
isDragging = true;
isPanning = false;
} }
function handleDragEnd(e: DragEvent) { function handleDragEnd(e: DragEvent) {
isDragging = false;
e.preventDefault(); e.preventDefault();
isDragging = true;
isPanning = false;
} }
onMount(() => { onMount(() => {
@ -893,6 +914,8 @@
on:wheel={handleMouseScroll} on:wheel={handleMouseScroll}
bind:this={wrapper} bind:this={wrapper}
class="graph-wrapper" class="graph-wrapper"
class:is-panning={isPanning}
class:is-hovering={hoveredNodeId !== -1}
aria-label="Graph" aria-label="Graph"
role="button" role="button"
tabindex="0" tabindex="0"
@ -916,9 +939,6 @@
/> />
<label for="drop-zone"></label> <label for="drop-zone"></label>
{#if showHelp}
<HelpView registry={manager.registry} />
{/if}
<Canvas shadows={false} renderMode="on-demand" colorManagementEnabled={false}> <Canvas shadows={false} renderMode="on-demand" colorManagementEnabled={false}>
<Camera bind:camera position={cameraPosition} /> <Camera bind:camera position={cameraPosition} />
@ -943,11 +963,12 @@
<AddMenu bind:position={addMenuPosition} graph={manager} /> <AddMenu bind:position={addMenuPosition} graph={manager} />
{/if} {/if}
{#if state.activeSocket} {#if graphState.activeSocket}
<FloatingEdge <FloatingEdge
z={cameraPosition[2]}
from={{ from={{
x: state.activeSocket.position[0], x: graphState.activeSocket.position[0],
y: state.activeSocket.position[1], y: graphState.activeSocket.position[1],
}} }}
to={{ x: mousePosition[0], y: mousePosition[1] }} to={{ x: mousePosition[0], y: mousePosition[1] }}
/> />
@ -962,6 +983,10 @@
</Canvas> </Canvas>
</div> </div>
{#if showHelp}
<HelpView registry={manager.registry} />
{/if}
<style> <style>
.graph-wrapper { .graph-wrapper {
position: relative; position: relative;
@ -969,6 +994,15 @@
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
height: 100%; height: 100%;
} }
.is-hovering {
cursor: pointer;
}
.is-panning {
cursor: grab;
}
input { input {
position: absolute; position: absolute;
z-index: 1; z-index: 1;

View File

@ -6,10 +6,23 @@
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { getGraphState } from "./state.svelte"; import { getGraphState } from "./state.svelte";
import { useThrelte } from "@threlte/core";
import { appSettings } from "$lib/settings/app-settings.svelte";
export let nodes: Writable<Map<number, NodeType>>; type Props = {
export let edges: Writable<EdgeType[]>; nodes: Writable<Map<number, NodeType>>;
export let cameraPosition = [0, 0, 4]; edges: Writable<EdgeType[]>;
cameraPosition: [number, number, number];
};
const { nodes, edges, cameraPosition = [0, 0, 4] }: Props = $props();
const { invalidate } = useThrelte();
$effect(() => {
appSettings.theme;
invalidate();
});
const graphState = getGraphState(); const graphState = getGraphState();
@ -23,7 +36,6 @@
function getEdgePosition(edge: EdgeType) { function getEdgePosition(edge: EdgeType) {
const pos1 = getSocketPosition(edge[0], edge[1]); const pos1 = getSocketPosition(edge[0], edge[1]);
const pos2 = getSocketPosition(edge[2], edge[3]); const pos2 = getSocketPosition(edge[2], edge[3]);
return [pos1[0], pos1[1], pos2[0], pos2[1]]; return [pos1[0], pos1[1], pos2[0], pos2[1]];
} }
@ -41,6 +53,7 @@
{@const pos = getEdgePosition(edge)} {@const pos = getEdgePosition(edge)}
{@const [x1, y1, x2, y2] = pos} {@const [x1, y1, x2, y2] = pos}
<Edge <Edge
z={cameraPosition[2]}
from={{ from={{
x: x1, x: x1,
y: y1, y: y1,

View File

@ -3,19 +3,18 @@
import GraphEl from "./Graph.svelte"; import GraphEl from "./Graph.svelte";
import { GraphManager } from "../graph-manager.js"; import { GraphManager } from "../graph-manager.js";
import { setContext } from "svelte"; import { setContext } from "svelte";
import { type Writable } from "svelte/store";
import { debounce } from "$lib/helpers"; import { debounce } from "$lib/helpers";
import { createKeyMap } from "$lib/helpers/createKeyMap"; import { createKeyMap } from "$lib/helpers/createKeyMap";
import { GraphState } from "./state.svelte"; import { GraphState } from "./state.svelte";
const state = new GraphState(); const graphState = new GraphState();
setContext("graphState", state); setContext("graphState", graphState);
type Props = { type Props = {
graph: Graph; graph: Graph;
registry: NodeRegistry; registry: NodeRegistry;
settings?: Writable<Record<string, any>>; settings?: Record<string, any>;
activeNode?: Node; activeNode?: Node;
showGrid?: boolean; showGrid?: boolean;
@ -41,33 +40,32 @@
}: Props = $props(); }: Props = $props();
export const keymap = createKeyMap([]); export const keymap = createKeyMap([]);
setContext("keymap", keymap);
export const manager = new GraphManager(registry); export const manager = new GraphManager(registry);
setContext("graphManager", manager); setContext("graphManager", manager);
$effect(() => { $effect(() => {
if (state.activeNodeId !== -1) { if (graphState.activeNodeId !== -1) {
activeNode = manager.getNode(state.activeNodeId); activeNode = manager.getNode(graphState.activeNodeId);
} else { } else if (activeNode) {
activeNode = undefined; activeNode = undefined;
} }
}); });
setContext("keymap", keymap);
const updateSettings = debounce((s) => { const updateSettings = debounce((s) => {
manager.setSettings(s); manager.setSettings(s);
}, 200); }, 200);
$effect(() => { $effect(() => {
if (settingTypes && settings) { if (settingTypes && settings) {
updateSettings($settings); updateSettings($state.snapshot(settings));
} }
}); });
manager.on("settings", (_settings) => { manager.on("settings", (_settings) => {
settingTypes = _settings.types; settingTypes = { ...settingTypes, ..._settings.types };
settings?.set(_settings.values); settings = _settings.values;
}); });
manager.on("result", (result) => onresult?.(result)); manager.on("result", (result) => onresult?.(result));

View File

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

View File

@ -1,48 +0,0 @@
import { readable } from "svelte/store";
import { Color } from "three";
const variables = [
"layer-0",
"layer-1",
"layer-2",
"layer-3",
"outline",
"active",
"selected",
"edge",
] as const;
const store = Object.fromEntries(variables.map(v => [v, new Color()])) as Record<typeof variables[number], Color>;
let lastStyle = "";
function updateColors() {
if (!("getComputedStyle" in globalThis)) return;
const style = getComputedStyle(document.body.parentElement!);
let hash = "";
for (const v of variables) {
let color = style.getPropertyValue(`--${v}`);
hash += color;
store[v].setStyle(color);
}
if (hash === lastStyle) return;
lastStyle = hash;
}
export const colors = readable(store, set => {
updateColors();
set(store);
setTimeout(() => {
updateColors();
set(store);
}, 1000);
window.onload = function () { updateColors(); set(store) };
document.body.addEventListener("transitionstart", () => {
updateColors();
set(store);
})
});

View File

@ -1,26 +1,22 @@
import type { Socket } from "@nodes/types"; import type { Socket } from "@nodes/types";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { SvelteSet } from 'svelte/reactivity';
export function getGraphState() { export function getGraphState() {
return getContext<GraphState>("graphState"); return getContext<GraphState>("graphState");
} }
export class GraphState { export class GraphState {
activeNodeId = $state(-1); activeNodeId = $state(-1);
selectedNodes = $state(new Set<number>()); selectedNodes = new SvelteSet<number>();
activeSocket = $state<Socket | null>(null); activeSocket = $state<Socket | null>(null);
hoveredSocket = $state<Socket | null>(null); hoveredSocket = $state<Socket | null>(null);
possibleSockets = $state<Socket[]>([]); possibleSockets = $state<Socket[]>([]);
possibleSocketIds = $derived(new Set( possibleSocketIds = $derived(new Set(
this.possibleSockets.map((s) => `${s.node.id}-${s.index}`), this.possibleSockets.map((s) => `${s.node.id}-${s.index}`),
)); ));
clearSelection() { clearSelection() {
this.selectedNodes = new Set(); this.selectedNodes.clear();
}
} }
}
export { colors } from "./colors";

View File

@ -1,12 +1,14 @@
<script lang="ts"> <script lang="ts">
import type { Node } from "@nodes/types"; import type { Node } from "@nodes/types";
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
import { colors, getGraphState } from "../graph/state.svelte"; import { getGraphState } from "../graph/state.svelte";
import { T } from "@threlte/core"; import { T } from "@threlte/core";
import { Color, type Mesh } from "three"; import { type Mesh } from "three";
import NodeFrag from "./Node.frag"; import NodeFrag from "./Node.frag";
import NodeVert from "./Node.vert"; import NodeVert from "./Node.vert";
import NodeHtml from "./NodeHTML.svelte"; import NodeHtml from "./NodeHTML.svelte";
import { colors } from "../graph/colors.svelte";
import { appSettings } from "$lib/settings/app-settings.svelte";
const graphState = getGraphState(); const graphState = getGraphState();
@ -18,7 +20,16 @@
const { node, inView, z }: Props = $props(); const { node, inView, z }: Props = $props();
const isActive = $derived(graphState.activeNodeId === node.id); const isActive = $derived(graphState.activeNodeId === node.id);
const isSelected = $derived(!!graphState.selectedNodes?.has(node.id)); const isSelected = $derived(graphState.selectedNodes.has(node.id));
let strokeColor = $state(colors.selected);
$effect(() => {
appSettings.theme;
strokeColor = isSelected
? colors.selected
: isActive
? colors.active
: colors.outline;
});
const updateNodePosition = const updateNodePosition =
getContext<(n: Node) => void>("updateNodePosition"); getContext<(n: Node) => void>("updateNodePosition");
@ -30,16 +41,11 @@
const height = getNodeHeight?.(node.type); const height = getNodeHeight?.(node.type);
$effect(() => { $effect(() => {
if (node && meshRef) {
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.mesh = meshRef; node.tmp.mesh = meshRef;
updateNodePosition?.(node); updateNodePosition?.(node);
}
}); });
const colorBright = $colors["layer-2"];
const colorDark = $colors["layer-1"];
onMount(() => { onMount(() => {
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.mesh = meshRef; node.tmp.mesh = meshRef;
@ -61,20 +67,14 @@
fragmentShader={NodeFrag} fragmentShader={NodeFrag}
transparent transparent
uniforms={{ uniforms={{
uColorBright: { value: new Color("#171717") }, uColorBright: { value: colors["layer-2"] },
uColorDark: { value: new Color("#151515") }, uColorDark: { value: colors["layer-1"] },
uStrokeColor: { value: new Color("#9d5f28") }, uStrokeColor: { value: colors.outline.clone() },
uStrokeWidth: { value: 1.0 }, uStrokeWidth: { value: 1.0 },
uWidth: { value: 20 }, uWidth: { value: 20 },
uHeight: { value: height }, uHeight: { value: height },
}} }}
uniforms.uColorBright.value={colorBright} uniforms.uStrokeColor.value={strokeColor.clone()}
uniforms.uColorDark.value={colorDark}
uniforms.uStrokeColor.value={isSelected
? $colors.selected
: isActive
? $colors.active
: $colors.outline}
uniforms.uStrokeWidth.value={(7 - z) / 3} uniforms.uStrokeWidth.value={(7 - z) / 3}
/> />
</T.Mesh> </T.Mesh>

View File

@ -3,14 +3,26 @@
import NodeHeader from "./NodeHeader.svelte"; import NodeHeader from "./NodeHeader.svelte";
import NodeParameter from "./NodeParameter.svelte"; import NodeParameter from "./NodeParameter.svelte";
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
export let isActive = false;
export let isSelected = false;
export let inView = true;
export let z = 2;
let ref: HTMLDivElement; let ref: HTMLDivElement;
export let node: Node;
export let position = "absolute"; type Props = {
node: Node;
position?: "absolute" | "fixed" | "relative";
isActive?: boolean;
isSelected?: boolean;
inView?: boolean;
z?: number;
};
let {
node = $bindable(),
position = "absolute",
isActive = false,
isSelected = false,
inView = true,
z = 2,
}: Props = $props();
const zOffset = (node.tmp?.random || 0) * 0.5; const zOffset = (node.tmp?.random || 0) * 0.5;
const zLimit = 2 - zOffset; const zLimit = 2 - zOffset;
@ -25,12 +37,6 @@
const updateNodePosition = const updateNodePosition =
getContext<(n: Node) => void>("updateNodePosition"); getContext<(n: Node) => void>("updateNodePosition");
$: if (node && ref) {
node.tmp = node.tmp || {};
node.tmp.ref = ref;
updateNodePosition?.(node);
}
onMount(() => { onMount(() => {
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.ref = ref; node.tmp.ref = ref;

View File

@ -26,9 +26,9 @@
const aspectRatio = 0.25; const aspectRatio = 0.25;
const path = createNodePath({ const path = createNodePath({
depth: 5, depth: 5.5,
height: 29, height: 34,
y: 50, y: 49,
cornerTop, cornerTop,
rightBump, rightBump,
aspectRatio, aspectRatio,
@ -42,9 +42,9 @@
aspectRatio, aspectRatio,
}); });
const pathHover = createNodePath({ const pathHover = createNodePath({
depth: 9, depth: 8.5,
height: 50, height: 50,
y: 50, y: 49,
cornerTop, cornerTop,
rightBump, rightBump,
aspectRatio, aspectRatio,
@ -103,12 +103,12 @@
svg { svg {
position: absolute; position: absolute;
top: 0; top: 1px;
left: 0; left: 1px;
z-index: -1; z-index: -1;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: calc(100% - 2px);
height: 100%; height: calc(100% - 1px);
overflow: visible; overflow: visible;
} }

View File

@ -19,7 +19,18 @@
const graph = getGraphManager(); const graph = getGraphManager();
let value = $state(node?.props?.[id] ?? input.value); function getDefaultValue() {
if (node?.props?.[id] !== undefined) return node?.props?.[id] as number;
if ("value" in input && input?.value !== undefined)
return input?.value as number;
if (input.type === "boolean") return 0;
if (input.type === "float") return 0.5;
if (input.type === "integer") return 0;
if (input.type === "select") return 0;
return 0;
}
let value = $state(getDefaultValue());
$effect(() => { $effect(() => {
if (value !== undefined && node?.props?.[id] !== value) { if (value !== undefined && node?.props?.[id] !== value) {

View File

@ -53,14 +53,14 @@
const path = createNodePath({ const path = createNodePath({
depth: 7, depth: 7,
height: 20, height: 20,
y: 51, y: 50.5,
cornerBottom, cornerBottom,
leftBump, leftBump,
aspectRatio, aspectRatio,
}); });
const pathDisabled = createNodePath({ const pathDisabled = createNodePath({
depth: 4.5, depth: 6,
height: 14, height: 18,
y: 50.5, y: 50.5,
cornerBottom, cornerBottom,
leftBump, leftBump,
@ -172,11 +172,11 @@
svg { svg {
position: absolute; position: absolute;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: calc(100% - 2px);
height: 100%; height: 100%;
overflow: visible; overflow: visible;
top: 0; top: 0;
left: 0; left: 1px;
z-index: -1; z-index: -1;
} }

View File

@ -18,7 +18,6 @@ export function createKeyMap(keys: Shortcut[]) {
const store = writable(new Map(keys.map(k => [getShortcutId(k), k]))); const store = writable(new Map(keys.map(k => [getShortcutId(k), k])));
return { return {
handleKeyboardEvent: (event: KeyboardEvent) => { handleKeyboardEvent: (event: KeyboardEvent) => {
const activeElement = document.activeElement as HTMLElement; const activeElement = document.activeElement as HTMLElement;

View File

@ -11,9 +11,9 @@
fps: false, fps: false,
}); });
$: vertices = $store?.at(-1)?.["total-vertices"][0] || 0; $: vertices = $store?.at(-1)?.["total-vertices"]?.[0] || 0;
$: faces = $store?.at(-1)?.["total-faces"][0] || 0; $: faces = $store?.at(-1)?.["total-faces"]?.[0] || 0;
$: runtime = $store?.at(-1)?.["runtime"][0] || 0; $: runtime = $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) || [];

View File

@ -9,21 +9,28 @@
Box3, Box3,
Mesh, Mesh,
MeshBasicMaterial, MeshBasicMaterial,
Color,
} from "three"; } from "three";
import { AppSettings } from "../settings/app-settings"; import { appSettings } from "../settings/app-settings.svelte";
import Camera from "./Camera.svelte"; import Camera from "./Camera.svelte";
import { colors } from "$lib/graph-interface/graph/colors.svelte";
const { renderStage, invalidate: _invalidate } = useThrelte(); const { renderStage, invalidate: _invalidate } = useThrelte();
export let fps: number[] = []; type Props = {
// let renderer = threlte.renderer; fps: number[];
// let rendererRender = renderer.render; lines: Vector3[][];
// renderer.render = function (scene, camera) { scene: Group;
// const a = performance.now(); centerCamera: boolean;
// rendererRender.call(renderer, scene, camera); };
// fps.push(performance.now() - a);
// fps = fps.slice(-100); let {
// }; lines,
centerCamera,
fps = $bindable(),
scene = $bindable(),
}: Props = $props();
useTask( useTask(
(delta) => { (delta) => {
fps.push(1 / delta); fps.push(1 / delta);
@ -53,12 +60,8 @@
_invalidate(); _invalidate();
}; };
let geometries: BufferGeometry[] = []; let geometries = $state<BufferGeometry[]>();
export let lines: Vector3[][]; let center = $state(new Vector3(0, 4, 0));
export let scene: Group;
export let centerCamera: boolean = true;
let center = new Vector3(0, 4, 0);
function isMesh(child: Mesh | any): child is Mesh { function isMesh(child: Mesh | any): child is Mesh {
return child.isObject3D && "material" in child; return child.isObject3D && "material" in child;
@ -68,14 +71,15 @@
return material.isMaterial && "matcap" in material; return material.isMaterial && "matcap" in material;
} }
$: if ($AppSettings && scene) { $effect(() => {
const wireframe = appSettings.debug.wireframe;
scene.traverse(function (child) { scene.traverse(function (child) {
if (isMesh(child) && isMatCapMaterial(child.material)) { if (isMesh(child) && isMatCapMaterial(child.material)) {
child.material.wireframe = $AppSettings.wireframe; child.material.wireframe = wireframe;
} }
}); });
invalidate(); _invalidate();
} });
function getPosition(geo: BufferGeometry, i: number) { function getPosition(geo: BufferGeometry, i: number) {
return [ return [
@ -88,13 +92,18 @@
<Camera {center} {centerCamera} /> <Camera {center} {centerCamera} />
{#if $AppSettings.showGrid} {#if appSettings.showGrid}
<T.GridHelper args={[20, 20]} /> <T.GridHelper
args={[20, 20]}
colorGrid={colors["outline"]}
colorCenterLine={new Color("red")}
/>
{/if} {/if}
<T.Group> <T.Group>
{#if geometries}
{#each geometries as geo} {#each geometries as geo}
{#if $AppSettings.showIndices} {#if appSettings.debug.showIndices}
{#each geo.attributes.position.array as _, i} {#each geo.attributes.position.array as _, i}
{#if i % 3 === 0} {#if i % 3 === 0}
<Text fontSize={0.25} position={getPosition(geo, i)} /> <Text fontSize={0.25} position={getPosition(geo, i)} />
@ -102,18 +111,19 @@
{/each} {/each}
{/if} {/if}
{#if $AppSettings.showVertices} {#if appSettings.debug.showVertices}
<T.Points visible={true}> <T.Points visible={true}>
<T is={geo} /> <T is={geo} />
<T.PointsMaterial size={0.25} /> <T.PointsMaterial size={0.25} />
</T.Points> </T.Points>
{/if} {/if}
{/each} {/each}
{/if}
<T.Group bind:ref={scene}></T.Group> <T.Group bind:ref={scene}></T.Group>
</T.Group> </T.Group>
{#if $AppSettings.showStemLines && lines} {#if appSettings.debug.showStemLines && lines}
{#each lines as line} {#each lines as line}
<T.Mesh> <T.Mesh>
<MeshLineGeometry points={line} /> <MeshLineGeometry points={line} />

View File

@ -2,12 +2,10 @@
import { Canvas } from "@threlte/core"; import { Canvas } from "@threlte/core";
import Scene from "./Scene.svelte"; import Scene from "./Scene.svelte";
import { Vector3 } from "three"; import { Vector3 } from "three";
import { decodeFloat, splitNestedArray } from "@nodes/utils"; import { decodeFloat, splitNestedArray } from "@nodes/utils";
import type { PerformanceStore } from "@nodes/utils"; import type { PerformanceStore } from "@nodes/utils";
import { AppSettings } from "$lib/settings/app-settings"; import { appSettings } from "$lib/settings/app-settings.svelte";
import SmallPerformanceViewer from "$lib/performance/SmallPerformanceViewer.svelte"; import SmallPerformanceViewer from "$lib/performance/SmallPerformanceViewer.svelte";
import { MeshMatcapMaterial, TextureLoader, type Group } from "three"; import { MeshMatcapMaterial, TextureLoader, type Group } from "three";
import { import {
createGeometryPool, createGeometryPool,
@ -22,9 +20,11 @@
matcap, matcap,
}); });
let sceneComponent = $state<ReturnType<typeof Scene>>();
let fps = $state<number[]>([]);
let geometryPool: ReturnType<typeof createGeometryPool>; let geometryPool: ReturnType<typeof createGeometryPool>;
let instancePool: ReturnType<typeof createInstancedGeometryPool>; let instancePool: ReturnType<typeof createInstancedGeometryPool>;
export function updateGeometries(inputs: Int32Array[], group: Group) { export function updateGeometries(inputs: Int32Array[], group: Group) {
geometryPool = geometryPool || createGeometryPool(group, material); geometryPool = geometryPool || createGeometryPool(group, material);
instancePool = instancePool || createInstancedGeometryPool(group, material); instancePool = instancePool || createInstancedGeometryPool(group, material);
@ -38,14 +38,15 @@
}; };
} }
export let centerCamera: boolean = true; type Props = {
export let perf: PerformanceStore; scene: Group;
export let scene: Group; centerCamera: boolean;
let fps: number[] = []; perf: PerformanceStore;
};
let lines: Vector3[][] = []; let { scene = $bindable(), centerCamera, perf }: Props = $props();
let invalidate: () => void; let lines = $state<Vector3[][]>([]);
function createLineGeometryFromEncodedData(encodedData: Int32Array) { function createLineGeometryFromEncodedData(encodedData: Int32Array) {
const positions: Vector3[] = []; const positions: Vector3[] = [];
@ -63,12 +64,12 @@
} }
export const update = function update(result: Int32Array) { export const update = function update(result: Int32Array) {
perf?.addPoint("split-result"); perf.addPoint("split-result");
const inputs = splitNestedArray(result); const inputs = splitNestedArray(result);
perf?.endPoint(); perf.endPoint();
if ($AppSettings.showStemLines) { if (appSettings.debug.showStemLines) {
perf?.addPoint("create-lines"); perf.addPoint("create-lines");
lines = inputs lines = inputs
.map((input) => { .map((input) => {
if (input[0] === 0) { if (input[0] === 0) {
@ -79,21 +80,27 @@
perf.endPoint(); perf.endPoint();
} }
perf?.addPoint("update-geometries"); perf.addPoint("update-geometries");
const { totalVertices, totalFaces } = updateGeometries(inputs, scene); const { totalVertices, totalFaces } = updateGeometries(inputs, scene);
perf?.endPoint(); perf.endPoint();
perf?.addPoint("total-vertices", totalVertices); perf.addPoint("total-vertices", totalVertices);
perf?.addPoint("total-faces", totalFaces); perf.addPoint("total-faces", totalFaces);
invalidate(); sceneComponent?.invalidate();
}; };
</script> </script>
{#if $AppSettings.showPerformancePanel} {#if appSettings.debug.showPerformancePanel}
<SmallPerformanceViewer {fps} store={perf} /> <SmallPerformanceViewer {fps} store={perf} />
{/if} {/if}
<Canvas> <Canvas>
<Scene bind:scene bind:invalidate {lines} {centerCamera} bind:fps /> <Scene
bind:this={sceneComponent}
{lines}
{centerCamera}
bind:scene
bind:fps
/>
</Canvas> </Canvas>

View File

@ -1,6 +1,5 @@
import { fastHashArrayBuffer } from "@nodes/utils"; import { fastHashArrayBuffer } from "@nodes/utils";
import { BufferAttribute, BufferGeometry, Float32BufferAttribute, Group, InstancedMesh, Material, Matrix4, Mesh } from "three" import { BufferAttribute, BufferGeometry, Float32BufferAttribute, Group, InstancedMesh, Material, Matrix4, Mesh } from "three";
function fastArrayHash(arr: ArrayBuffer) { function fastArrayHash(arr: ArrayBuffer) {
let ints = new Uint8Array(arr); let ints = new Uint8Array(arr);
@ -108,7 +107,6 @@ export function createGeometryPool(parentScene: Group, material: Material) {
scene.add(mesh); scene.add(mesh);
meshes.push(mesh); meshes.push(mesh);
} }
} }

View File

@ -1,6 +1,5 @@
import type { Graph, NodeRegistry, NodeDefinition, RuntimeExecutor, NodeInput } from "@nodes/types"; import type { Graph, NodeDefinition, NodeInput, NodeRegistry, RuntimeExecutor, SyncCache } from "@nodes/types";
import { concatEncodedArrays, encodeFloat, fastHashArrayBuffer, createLogger, type PerformanceStore } from "@nodes/utils" import { concatEncodedArrays, createLogger, encodeFloat, fastHashArrayBuffer, type PerformanceStore } from "@nodes/utils";
import type { SyncCache } from "@nodes/types";
const log = createLogger("runtime-executor"); const log = createLogger("runtime-executor");
log.mute() log.mute()
@ -9,6 +8,7 @@ function getValue(input: NodeInput, value?: unknown) {
if (value === undefined && "value" in input) { if (value === undefined && "value" in input) {
value = input.value value = input.value
} }
if (input.type === "float") { if (input.type === "float") {
return encodeFloat(value as number); return encodeFloat(value as number);
} }

View File

@ -5,9 +5,6 @@ import type { Graph, RuntimeExecutor } from "@nodes/types";
export class WorkerRuntimeExecutor implements RuntimeExecutor { export class WorkerRuntimeExecutor implements RuntimeExecutor {
private worker = new ComlinkWorker<typeof import('./worker-runtime-executor-backend.ts')>(new URL(`./worker-runtime-executor-backend.ts`, import.meta.url)); private worker = new ComlinkWorker<typeof import('./worker-runtime-executor-backend.ts')>(new URL(`./worker-runtime-executor-backend.ts`, import.meta.url));
constructor() {
console.log(import.meta.url)
}
async execute(graph: Graph, settings: Record<string, unknown>) { async execute(graph: Graph, settings: Record<string, unknown>) {
return this.worker.executeGraph(graph, settings); return this.worker.executeGraph(graph, settings);
} }

View File

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

View File

@ -39,6 +39,7 @@ export const AppSettingTypes = {
} }
}, },
debug: { debug: {
title: "Debug",
wireframe: { wireframe: {
type: "boolean", type: "boolean",
label: "Wireframe", label: "Wireframe",
@ -79,7 +80,8 @@ export const AppSettingTypes = {
amount: { amount: {
type: "integer", type: "integer",
min: 2, min: 2,
max: 15 max: 15,
value: 4
}, },
loadGrid: { loadGrid: {
type: "button", type: "button",
@ -103,16 +105,26 @@ export const AppSettingTypes = {
} }
}, },
} }
} as const } as const;
type IsInputDefinition<T> = T extends NodeInput ? T : never; type IsInputDefinition<T> = T extends NodeInput ? T : never;
type HasTitle = { title: string }; type HasTitle = { title: string };
type Widen<T> = T extends boolean
? boolean
: T extends number
? number
: T extends string
? string
: T;
type ExtractSettingsValues<T> = { type ExtractSettingsValues<T> = {
[K in keyof T]: T[K] extends HasTitle -readonly [K in keyof T]: T[K] extends HasTitle
? ExtractSettingsValues<Omit<T[K], 'title'>> ? ExtractSettingsValues<Omit<T[K], 'title'>>
: T[K] extends IsInputDefinition<T[K]> : T[K] extends IsInputDefinition<T[K]>
? T[K] extends { value: any } ? T[K] extends { value: infer V }
? T[K]['value'] ? Widen<V>
: never : never
: T[K] extends Record<string, any> : T[K] extends Record<string, any>
? ExtractSettingsValues<T[K]> ? ExtractSettingsValues<T[K]>
@ -138,8 +150,8 @@ export const appSettings = localState("app-settings", settingsToStore(AppSetting
$effect.root(() => { $effect.root(() => {
$effect(() => { $effect(() => {
const { theme } = $state.snapshot(appSettings); const theme = appSettings.theme;
const classes = document.body.parentElement?.classList; const classes = document.documentElement.classList;
const newClassName = `theme-${theme}`; const newClassName = `theme-${theme}`;
if (classes) { if (classes) {
for (const className of classes) { for (const className of classes) {
@ -148,6 +160,6 @@ $effect.root(() => {
} }
} }
} }
document.body?.parentElement?.classList.add(newClassName); document.documentElement.classList.add(newClassName);
}); });
}); });

View File

@ -1,136 +0,0 @@
import localStore from "$lib/helpers/localStore";
export const AppSettings = localStore("node.settings", {
theme: 0,
showGrid: true,
showNodeGrid: true,
snapToGrid: true,
showHelp: false,
wireframe: false,
showIndices: false,
showVertices: false,
showPerformancePanel: false,
showBenchmarkPanel: false,
centerCamera: true,
showStemLines: false,
useWorker: true,
amount: 5
});
const themes = ["dark", "light", "catppuccin", "solarized", "high-contrast", "nord", "dracula"];
AppSettings.subscribe((value) => {
const classes = document.body.parentElement?.classList;
const newClassName = `theme-${themes[value.theme]}`;
if (classes) {
for (const className of classes) {
if (className.startsWith("theme-") && className !== newClassName) {
classes.remove(className);
}
}
}
document.body?.parentElement?.classList.add(newClassName);
});
export const AppSettingTypes = {
theme: {
type: "select",
options: themes,
label: "Theme",
value: themes[0],
},
showGrid: {
type: "boolean",
label: "Show Grid",
value: true,
},
centerCamera: {
type: "boolean",
label: "Center Camera",
value: true
},
nodeInterface: {
__title: "Node Interface",
showNodeGrid: {
type: "boolean",
label: "Show Grid",
value: true
},
snapToGrid: {
type: "boolean",
label: "Snap to Grid",
value: true
},
showHelp: {
type: "boolean",
label: "Show Help",
value: false
}
},
debug: {
wireframe: {
type: "boolean",
label: "Wireframe",
value: false,
},
useWorker: {
type: "boolean",
label: "Execute runtime in worker",
value: true,
},
showIndices: {
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,
},
stressTest: {
__title: "Stress Test",
amount: {
type: "integer",
min: 2,
max: 15
},
loadGrid: {
type: "button",
label: "Load Grid"
},
loadTree: {
type: "button",
label: "Load Tree"
},
lottaFaces: {
type: "button",
label: "Load 'lots of faces'"
},
lottaNodes: {
type: "button",
label: "Load 'lots of nodes'"
},
lottaNodesAndFaces: {
type: "button",
label: "Load 'lots of nodes and faces'"
}
},
}
}

View File

@ -0,0 +1,13 @@
import type { NodeInput } from "@nodes/types";
type Button = { type: "button"; label?: string };
type InputType = NodeInput | Button;
export interface SettingsType {
[key: string]: (SettingsType & { title?: string }) | InputType;
}
export type SettingsStore = {
[key: string]: SettingsStore | string | number | boolean
};

View File

@ -1,89 +0,0 @@
<script lang="ts">
import type { Node, NodeInput } from "@nodes/types";
import NestedSettings from "./NestedSettings.svelte";
import { writable } from "svelte/store";
import type { GraphManager } from "$lib/graph-interface/graph-manager";
export let manager: GraphManager;
export let node: Node | undefined;
function filterInputs(inputs: Record<string, NodeInput>) {
return Object.fromEntries(
Object.entries(inputs)
.filter(([_key, value]) => {
return value.hidden === true;
})
.map(([key, value]) => {
//@ts-ignore
value.__node_type = node?.tmp?.type.id;
//@ts-ignore
value.__node_input = key;
return [key, value];
}),
);
}
function createStore(
props: Node["props"],
inputs: Record<string, NodeInput>,
) {
const store: Record<string, unknown> = {};
Object.keys(inputs).forEach((key) => {
if (props) {
//@ts-ignore
store[key] = props[key] || inputs[key].value;
}
});
return writable(store);
}
let nodeDefinition: Record<string, NodeInput> | undefined;
$: nodeDefinition = node?.tmp?.type
? filterInputs(node.tmp.type.inputs)
: undefined;
$: store = node ? createStore(node.props, nodeDefinition) : undefined;
let lastPropsHash = "";
function updateNode() {
if (!node || !$store) return;
let needsUpdate = false;
Object.keys($store).forEach((_key: string) => {
node.props = node.props || {};
const key = _key as keyof typeof $store;
if (node && $store) {
needsUpdate = true;
node.props[key] = $store[key];
}
});
let propsHash = JSON.stringify(node.props);
if (propsHash === lastPropsHash) {
return;
}
lastPropsHash = propsHash;
// console.log(needsUpdate, node.props, $store);
if (needsUpdate) {
manager.execute();
}
}
$: if (store && $store) {
updateNode();
}
</script>
{#if node}
{#key node.id}
{#if nodeDefinition && store && Object.keys(nodeDefinition).length > 0}
<NestedSettings
id="activeNodeSettings"
settings={nodeDefinition}
{store}
/>
{:else}
<p class="mx-4">Active Node has no Settings</p>
{/if}
{/key}
{:else}
<p class="mx-4">No active node</p>
{/if}

View File

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

View File

@ -1,60 +0,0 @@
<script lang="ts">
import type { createKeyMap } from "$lib/helpers/createKeyMap";
import { ShortCut } from "@nodes/ui";
export let keymap: ReturnType<typeof createKeyMap>;
const keys = keymap?.keys;
export let title = "Keymap";
</script>
<div class="wrapper">
<h3>{title}</h3>
<section>
{#each $keys as key}
{#if key.description}
<div class="command-wrapper">
<ShortCut
alt={key.alt}
ctrl={key.ctrl}
shift={key.shift}
key={key.key}
/>
</div>
<p>{key.description}</p>
{/if}
{/each}
</section>
</div>
<style>
.wrapper {
padding: 1em;
display: flex;
flex-direction: column;
gap: 1em;
}
section {
display: grid;
grid-template-columns: min-content 1fr;
gap: 1em;
}
h3 {
margin: 0;
}
.command-wrapper {
display: flex;
justify-content: right;
align-items: center;
}
p {
font-size: 0.9em;
margin: 0;
display: flex;
align-items: center;
}
</style>

View File

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

View File

@ -0,0 +1,82 @@
<script lang="ts">
import type { Node, NodeInput } from "@nodes/types";
import NestedSettings from "$lib/settings/NestedSettings.svelte";
import type { GraphManager } from "$lib/graph-interface/graph-manager";
type Props = {
manager: GraphManager;
node: Node;
};
const { manager, node }: Props = $props();
const nodeDefinition = filterInputs(node.tmp?.type?.inputs);
function filterInputs(inputs?: Record<string, NodeInput>) {
const _inputs = $state.snapshot(inputs);
return Object.fromEntries(
Object.entries(structuredClone(_inputs ?? {}))
.filter(([_key, value]) => {
return value.hidden === true;
})
.map(([key, value]) => {
//@ts-ignore
value.__node_type = node?.tmp?.type.id;
//@ts-ignore
value.__node_input = key;
return [key, value];
}),
);
}
type Store = Record<string, number | number[]>;
let store = $state<Store>(createStore(node?.props, nodeDefinition));
function createStore(
props: Node["props"],
inputs: Record<string, NodeInput>,
): Store {
const store: Store = {};
Object.keys(inputs).forEach((key) => {
if (props) {
//@ts-ignore
store[key] = props[key] || inputs[key].value;
}
});
return store;
}
let lastPropsHash = "";
function updateNode() {
if (!node || !store) return;
let needsUpdate = false;
Object.keys(store).forEach((_key: string) => {
node.props = node.props || {};
const key = _key as keyof typeof store;
if (node && store) {
needsUpdate = true;
node.props[key] = store[key];
}
});
let propsHash = JSON.stringify(node.props);
if (propsHash === lastPropsHash) {
return;
}
lastPropsHash = propsHash;
if (needsUpdate) {
manager.execute();
}
}
$effect(() => {
if (store) {
updateNode();
}
});
</script>
<NestedSettings
id="activeNodeSettings"
bind:value={store}
type={nodeDefinition}
/>

View File

@ -0,0 +1,24 @@
<script lang="ts">
import type { Node } from "@nodes/types";
import type { GraphManager } from "$lib/graph-interface/graph-manager";
import ActiveNodeSelected from "./ActiveNodeSelected.svelte";
type Props = {
manager: GraphManager;
node: Node | undefined;
};
const { manager, node }: Props = $props();
</script>
{#if node}
{#key node.id}
{#if node}
<ActiveNodeSelected {manager} {node} />
{:else}
<p class="mx-4">Active Node has no Settings</p>
{/if}
{/key}
{:else}
<p class="mx-4">No active node</p>
{/if}

View File

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

View File

@ -0,0 +1,66 @@
<script lang="ts">
import type { createKeyMap } from "$lib/helpers/createKeyMap";
import { ShortCut } from "@nodes/ui";
import { get } from "svelte/store";
type Props = {
keymaps: {
keymap: ReturnType<typeof createKeyMap>;
title: string;
}[];
};
let { keymaps }: Props = $props();
console.log({ keymaps });
</script>
<table class="wrapper">
<tbody>
{#each keymaps as keymap}
<tr>
<td colspan="2">
<h3>{keymap.title}</h3>
</td>
</tr>
{#each get(keymap.keymap?.keys) as key}
<tr>
{#if key.description}
<td class="command-wrapper">
<ShortCut
alt={key.alt}
ctrl={key.ctrl}
shift={key.shift}
key={key.key}
/>
</td>
<td>{key.description}</td>
{/if}
</tr>
{/each}
{/each}
</tbody>
</table>
<style>
.wrapper {
padding: 1em;
}
h3 {
margin: 0;
}
.command-wrapper {
display: flex;
justify-content: right;
align-items: center;
}
td {
font-size: 0.9em;
margin: 0;
padding: 7px;
padding-left: 0;
align-items: center;
}
</style>

View File

@ -4,23 +4,20 @@
import * as templates from "$lib/graph-templates"; import * as templates from "$lib/graph-templates";
import type { Graph, Node } from "@nodes/types"; import type { Graph, Node } from "@nodes/types";
import Viewer from "$lib/result-viewer/Viewer.svelte"; import Viewer from "$lib/result-viewer/Viewer.svelte";
import Settings from "$lib/settings/Settings.svelte";
import { AppSettingTypes, AppSettings } from "$lib/settings/app-settings";
import { import {
appSettings as _appSettings, appSettings,
AppSettingTypes as _AppSettingTypes, AppSettingTypes,
} from "$lib/settings/app-settings.svelte"; } from "$lib/settings/app-settings.svelte";
import { writable } from "svelte/store"; import Keymap from "$lib/sidebar/panels/Keymap.svelte";
import Keymap from "$lib/settings/panels/Keymap.svelte"; import Sidebar from "$lib/sidebar/Sidebar.svelte";
import { createKeyMap } from "$lib/helpers/createKeyMap"; import { createKeyMap } from "$lib/helpers/createKeyMap";
import NodeStore from "$lib/node-store/NodeStore.svelte"; import NodeStore from "$lib/node-store/NodeStore.svelte";
import ActiveNodeSettings from "$lib/settings/panels/ActiveNodeSettings.svelte"; import ActiveNodeSettings from "$lib/sidebar/panels/ActiveNodeSettings.svelte";
import PerformanceViewer from "$lib/performance/PerformanceViewer.svelte"; import PerformanceViewer from "$lib/performance/PerformanceViewer.svelte";
import Panel from "$lib/settings/Panel.svelte"; import Panel from "$lib/sidebar/Panel.svelte";
import GraphSettings from "$lib/settings/panels/GraphSettings.svelte"; import NestedSettings from "$lib/settings/NestedSettings.svelte";
import NestedSettings from "$lib/settings/panels/NestedSettings.svelte";
import type { Group } from "three"; import type { Group } from "three";
import ExportSettings from "$lib/settings/panels/ExportSettings.svelte"; import ExportSettings from "$lib/sidebar/panels/ExportSettings.svelte";
import { import {
MemoryRuntimeCache, MemoryRuntimeCache,
WorkerRuntimeExecutor, WorkerRuntimeExecutor,
@ -28,8 +25,9 @@
} from "$lib/runtime"; } from "$lib/runtime";
import { IndexDBCache, RemoteNodeRegistry } from "@nodes/registry"; import { IndexDBCache, RemoteNodeRegistry } from "@nodes/registry";
import { createPerformanceStore } from "@nodes/utils"; import { createPerformanceStore } from "@nodes/utils";
import BenchmarkPanel from "$lib/settings/panels/BenchmarkPanel.svelte"; import BenchmarkPanel from "$lib/sidebar/panels/BenchmarkPanel.svelte";
import { debounceAsyncFunction } from "$lib/helpers"; import { debounceAsyncFunction } from "$lib/helpers";
import { onMount } from "svelte";
let performanceStore = createPerformanceStore(); let performanceStore = createPerformanceStore();
@ -41,24 +39,26 @@
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache); const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
memoryRuntime.perf = performanceStore; memoryRuntime.perf = performanceStore;
$: runtime = $AppSettings.useWorker ? workerRuntime : memoryRuntime; const runtime = $derived(
appSettings.debug.useWorker ? workerRuntime : memoryRuntime,
);
let activeNode: Node | undefined; let activeNode = $state<Node | undefined>(undefined);
let scene: Group; let scene = $state<Group>(null!);
let updateViewerResult: (result: Int32Array) => void;
let graph = localStorage.getItem("graph") let graph = localStorage.getItem("graph")
? JSON.parse(localStorage.getItem("graph")!) ? JSON.parse(localStorage.getItem("graph")!)
: templates.defaultPlant; : templates.defaultPlant;
let graphInterface: ReturnType<typeof GraphInterface>; let graphInterface = $state<ReturnType<typeof GraphInterface>>(null!);
$: manager = graphInterface?.manager; let viewerComponent = $state<ReturnType<typeof Viewer>>();
$: managerStatus = manager?.status; const manager = $derived(graphInterface?.manager);
$: keymap = graphInterface?.keymap; const managerStatus = $derived(manager?.status);
async function randomGenerate() { async function randomGenerate() {
if (!manager) return;
const g = manager.serialize(); const g = manager.serialize();
const s = { ...$graphSettings, randomSeed: true }; const s = { ...graphSettings, randomSeed: true };
await handleUpdate(g, s); await handleUpdate(g, s);
} }
@ -69,18 +69,20 @@
callback: randomGenerate, callback: randomGenerate,
}, },
]); ]);
let graphSettings = writable<Record<string, any>>({}); let graphSettings = $state<Record<string, any>>({});
let graphSettingTypes = {}; let graphSettingTypes = $state({
randomSeed: { type: "boolean", value: false },
});
const handleUpdate = debounceAsyncFunction( const handleUpdate = debounceAsyncFunction(
async (g: Graph, s: Record<string, any>) => { async (g: Graph, s: Record<string, any> = graphSettings) => {
performanceStore.startRun(); performanceStore.startRun();
try { try {
let a = performance.now(); let a = performance.now();
const graphResult = await runtime.execute(g, s); const graphResult = await runtime.execute(g, $state.snapshot(s));
let b = performance.now(); let b = performance.now();
if ($AppSettings.useWorker) { if (appSettings.debug.useWorker) {
let perfData = await runtime.getPerformanceData(); let perfData = await runtime.getPerformanceData();
let lastRun = perfData?.at(-1); let lastRun = perfData?.at(-1);
if (lastRun?.total) { if (lastRun?.total) {
@ -94,7 +96,7 @@
} }
} }
updateViewerResult(graphResult); viewerComponent?.update(graphResult);
} catch (error) { } catch (error) {
console.log("errors", error); console.log("errors", error);
} finally { } finally {
@ -103,32 +105,35 @@
}, },
); );
$: if (AppSettings) { // $ if (AppSettings) {
//@ts-ignore // //@ts-ignore
AppSettingTypes.debug.stressTest.loadGrid.callback = () => { // AppSettingTypes.debug.stressTest.loadGrid.callback = () => {
graph = templates.grid($AppSettings.amount, $AppSettings.amount); // graph = templates.grid($AppSettings.amount, $AppSettings.amount);
}; // };
//@ts-ignore // //@ts-ignore
AppSettingTypes.debug.stressTest.loadTree.callback = () => { // AppSettingTypes.debug.stressTest.loadTree.callback = () => {
graph = templates.tree($AppSettings.amount); // graph = templates.tree($AppSettings.amount);
}; // };
//@ts-ignore // //@ts-ignore
AppSettingTypes.debug.stressTest.lottaFaces.callback = () => { // AppSettingTypes.debug.stressTest.lottaFaces.callback = () => {
graph = templates.lottaFaces; // graph = templates.lottaFaces;
}; // };
//@ts-ignore // //@ts-ignore
AppSettingTypes.debug.stressTest.lottaNodes.callback = () => { // AppSettingTypes.debug.stressTest.lottaNodes.callback = () => {
graph = templates.lottaNodes; // graph = templates.lottaNodes;
}; // };
//@ts-ignore // //@ts-ignore
AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => { // AppSettingTypes.debug.stressTest.lottaNodesAndFaces.callback = () => {
graph = templates.lottaNodesAndFaces; // graph = templates.lottaNodesAndFaces;
}; // };
} // }
function handleSave(graph: Graph) { function handleSave(graph: Graph) {
localStorage.setItem("graph", JSON.stringify(graph)); localStorage.setItem("graph", JSON.stringify(graph));
} }
onMount(() => {
handleUpdate(graph);
});
</script> </script>
<svelte:document on:keydown={applicationKeymap.handleKeyboardEvent} /> <svelte:document on:keydown={applicationKeymap.handleKeyboardEvent} />
@ -137,10 +142,10 @@
<Grid.Row> <Grid.Row>
<Grid.Cell> <Grid.Cell>
<Viewer <Viewer
perf={performanceStore}
bind:scene bind:scene
bind:update={updateViewerResult} bind:this={viewerComponent}
centerCamera={$AppSettings.centerCamera} perf={performanceStore}
centerCamera={appSettings.centerCamera}
/> />
</Grid.Cell> </Grid.Cell>
<Grid.Cell> <Grid.Cell>
@ -149,21 +154,21 @@
bind:this={graphInterface} bind:this={graphInterface}
{graph} {graph}
registry={nodeRegistry} registry={nodeRegistry}
showGrid={appSettings.nodeInterface.showNodeGrid}
snapToGrid={appSettings.nodeInterface.snapToGrid}
bind:activeNode bind:activeNode
showGrid={$AppSettings.showNodeGrid} bind:showHelp={appSettings.nodeInterface.showHelp}
snapToGrid={$AppSettings.snapToGrid}
bind:showHelp={$AppSettings.showHelp}
bind:settings={graphSettings} bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes} bind:settingTypes={graphSettingTypes}
onresult={(result) => handleUpdate(result, $graphSettings)} onresult={(result) => handleUpdate(result)}
onsave={(graph) => handleSave(graph)} onsave={(graph) => handleSave(graph)}
/> />
<Settings> <Sidebar>
<Panel id="general" title="General" icon="i-tabler-settings"> <Panel id="general" title="General" icon="i-tabler-settings">
<NestedSettings <NestedSettings
id="general" id="general"
value={_appSettings} value={appSettings}
type={_AppSettingTypes} type={AppSettingTypes}
/> />
</Panel> </Panel>
<Panel <Panel
@ -171,10 +176,12 @@
title="Keyboard Shortcuts" title="Keyboard Shortcuts"
icon="i-tabler-keyboard" icon="i-tabler-keyboard"
> >
<Keymap title="Application" keymap={applicationKeymap} /> <Keymap
{#if keymap} keymaps={[
<Keymap title="Node-Editor" {keymap} /> { keymap: applicationKeymap, title: "Application" },
{/if} { keymap: graphInterface.keymap, title: "Node-Editor" },
]}
/>
</Panel> </Panel>
<Panel id="exports" title="Exporter" icon="i-tabler-package-export"> <Panel id="exports" title="Exporter" icon="i-tabler-package-export">
<ExportSettings {scene} /> <ExportSettings {scene} />
@ -191,7 +198,7 @@
id="performance" id="performance"
title="Performance" title="Performance"
classes="text-red-400" classes="text-red-400"
hidden={!$AppSettings.showPerformancePanel} hidden={!appSettings.debug.showPerformancePanel}
icon="i-tabler-brand-speedtest" icon="i-tabler-brand-speedtest"
> >
{#if $performanceStore} {#if $performanceStore}
@ -202,7 +209,7 @@
id="benchmark" id="benchmark"
title="Benchmark" title="Benchmark"
classes="text-red-400" classes="text-red-400"
hidden={!$AppSettings.showBenchmarkPanel} hidden={!appSettings.debug.showBenchmarkPanel}
icon="i-tabler-graph" icon="i-tabler-graph"
> >
<BenchmarkPanel run={randomGenerate} /> <BenchmarkPanel run={randomGenerate} />
@ -213,9 +220,11 @@
classes="text-blue-400" classes="text-blue-400"
icon="i-custom-graph" icon="i-custom-graph"
> >
{#if Object.keys(graphSettingTypes).length > 0} <NestedSettings
<GraphSettings type={graphSettingTypes} store={graphSettings} /> id="graph-settings"
{/if} type={graphSettingTypes}
bind:value={graphSettings}
/>
</Panel> </Panel>
<Panel <Panel
id="active-node" id="active-node"
@ -225,7 +234,7 @@
> >
<ActiveNodeSettings {manager} node={activeNode} /> <ActiveNodeSettings {manager} node={activeNode} />
</Panel> </Panel>
</Settings> </Sidebar>
{/key} {/key}
</Grid.Cell> </Grid.Cell>
</Grid.Row> </Grid.Row>

View File

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

View File

@ -8,11 +8,5 @@
"build:deploy": "pnpm build", "build:deploy": "pnpm build",
"dev": "pnpm -r --filter 'app' --filter './packages/node-registry' dev" "dev": "pnpm -r --filter 'app' --filter './packages/node-registry' dev"
}, },
"packageManager": "pnpm@9.11.0+sha512.0a203ffaed5a3f63242cd064c8fb5892366c103e328079318f78062f24ea8c9d50bc6a47aa3567cabefd824d170e78fa2745ed1f16b132e16436146b7688f19b", "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c"
"dependencies": {
"@types/pg": "^8.11.10",
"drizzle-kit": "^0.30.1",
"drizzle-orm": "^0.38.2",
"pg": "^8.13.1"
}
} }

View File

@ -34,6 +34,6 @@
} }
.content { .content {
padding-left: 12px; /* padding-left: 12px; */
} }
</style> </style>

3355
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff