feat: migrate most of graph-manager to svelte-5
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 2m44s

This commit is contained in:
max_richter 2024-11-02 19:37:22 +01:00
parent fa659ab74e
commit 4f03f2af5a
21 changed files with 321 additions and 264 deletions

View File

@ -1,16 +1,23 @@
<script lang="ts"> <script lang="ts">
import { HTML } from "@threlte/extras"; import { HTML } from "@threlte/extras";
export let p1 = { x: 0, y: 0 }; type Props = {
export let p2 = { x: 0, y: 0 }; p1: { x: number; y: number };
p2: { x: number; y: number };
cameraPosition: [number, number, number];
};
export let cameraPosition = [0, 1, 0]; const {
p1 = { x: 0, y: 0 },
p2 = { x: 0, y: 0 },
cameraPosition = [0, 1, 0],
}: Props = $props();
$: width = Math.abs(p1.x - p2.x) * cameraPosition[2]; const width = $derived(Math.abs(p1.x - p2.x) * cameraPosition[2]);
$: height = Math.abs(p1.y - p2.y) * cameraPosition[2]; const height = $derived(Math.abs(p1.y - p2.y) * cameraPosition[2]);
$: x = Math.max(p1.x, p2.x) - width / cameraPosition[2]; const x = $derived(Math.max(p1.x, p2.x) - width / cameraPosition[2]);
$: y = Math.max(p1.y, p2.y) - height / cameraPosition[2]; const y = $derived(Math.max(p1.y, p2.y) - height / cameraPosition[2]);
</script> </script>
<HTML position.x={x} position.z={y} transform={false}> <HTML position.x={x} position.z={y} transform={false}>

View File

@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
import { T } from '@threlte/core'; import { T } from "@threlte/core";
import { type OrthographicCamera } from 'three'; import { type OrthographicCamera } from "three";
type Props = {
camera: OrthographicCamera;
position: [number, number, number];
};
export let camera: OrthographicCamera | undefined = undefined; let { camera = $bindable(), position }: Props = $props();
export let position: [number, number, number];
</script> </script>
<T.OrthographicCamera <T.OrthographicCamera

View File

@ -2,16 +2,17 @@
import type { NodeDefinition, NodeRegistry } from "@nodes/types"; import type { NodeDefinition, NodeRegistry } from "@nodes/types";
import { onMount } from "svelte"; import { onMount } from "svelte";
let mx = 0; let mx = $state(0);
let my = 0; let my = $state(0);
let node: NodeDefinition | undefined = undefined; let node: NodeDefinition | undefined = $state(undefined);
let input: string | undefined = undefined; let input: string | undefined = $state(undefined);
let wrapper: HTMLDivElement; let wrapper: HTMLDivElement;
type Props = { registry: NodeRegistry };
const { registry }: Props = $props();
export let registry: NodeRegistry; let width = $state(0);
let width = 0;
function handleMouseOver(ev: MouseEvent) { function handleMouseOver(ev: MouseEvent) {
let target = ev.target as HTMLElement | null; let target = ev.target as HTMLElement | null;
@ -45,7 +46,7 @@
}); });
</script> </script>
<svelte:window on:mousemove={handleMouseOver} /> <svelte:window onmousemove={handleMouseOver} />
<div <div
class="help-wrapper p-4" class="help-wrapper p-4"

View File

@ -3,24 +3,27 @@
import BackgroundVert from "./Background.vert"; import BackgroundVert from "./Background.vert";
import BackgroundFrag from "./Background.frag"; import BackgroundFrag from "./Background.frag";
import { colors } from "../graph/stores.js"; import { colors } from "../graph/state.svelte";
import { Color } from "three"; import { Color } from "three";
export let minZoom = 4; type Props = {
export let maxZoom = 150; minZoom: number;
maxZoom: number;
cameraPosition: [number, number, number];
width: number;
height: number;
};
export let cameraPosition: [number, number, number] = [0, 1, 0]; let {
minZoom = 4,
maxZoom = 150,
cameraPosition = [0, 1, 0],
width = globalThis?.innerWidth || 100,
height = globalThis?.innerHeight || 100,
}: Props = $props();
export let width = globalThis?.innerWidth || 100; let bw = $derived(width / cameraPosition[2]);
export let height = globalThis?.innerHeight || 100; let bh = $derived(height / cameraPosition[2]);
let bw = 2;
let bh = 2;
$: if (width && height) {
bw = width / cameraPosition[2];
bh = height / cameraPosition[2];
}
</script> </script>
<T.Group <T.Group

View File

@ -1,5 +1,5 @@
<script context="module" lang="ts"> <script module lang="ts">
import { colors } from "../graph/stores"; import { colors } from "../graph/state.svelte";
const circleMaterial = new MeshBasicMaterial({ const circleMaterial = new MeshBasicMaterial({
color: get(colors).edge, color: get(colors).edge,
@ -29,19 +29,23 @@
import { createEdgeGeometry } from "./createEdgeGeometry.js"; import { createEdgeGeometry } from "./createEdgeGeometry.js";
import { get } from "svelte/store"; import { get } from "svelte/store";
export let from: { x: number; y: number }; type Props = {
export let to: { x: number; y: number }; from: { x: number; y: number };
to: { x: number; y: number };
};
const { from, to }: Props = $props();
let samples = 5; let samples = 5;
let geometry: BufferGeometry; let geometry: BufferGeometry|null = $state(null);
let lastId: number | null = null; let lastId: number | null = null;
const primeA = 31; const primeA = 31;
const primeB = 37; const primeB = 37;
export const update = function () { function update() {
const new_x = to.x - from.x; const new_x = to.x - from.x;
const new_y = to.y - from.y; const new_y = to.y - from.y;
const curveId = new_x * primeA + new_y * primeB; const curveId = new_x * primeA + new_y * primeB;
@ -75,11 +79,13 @@
lineCache.set(curveId, geometry); lineCache.set(curveId, geometry);
}; };
$: if (from || to) { $effect(() => {
if (from || to) {
update(); update();
} }
});
$: lineColor = $colors["edge"].clone().convertSRGBToLinear(); const lineColor = $derived($colors.edge.clone().convertSRGBToLinear());
</script> </script>
<T.Mesh <T.Mesh

View File

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

View File

@ -1,11 +1,10 @@
import { writable, type Writable } from "svelte/store"; import type { Edge, Graph, Node, NodeInput, NodeRegistry, Socket, } from "@nodes/types";
import type { Graph, Node, Edge, Socket, NodeRegistry, } from "@nodes/types";
import { HistoryManager } from "./history-manager.js"
import EventEmitter from "./helpers/EventEmitter.js";
import throttle from "./helpers/throttle.js";
import { createLogger } from "./helpers/index.js";
import type { NodeInput } from "@nodes/types";
import { fastHashString } from "@nodes/utils"; import { fastHashString } from "@nodes/utils";
import { writable, type Writable } from "svelte/store";
import EventEmitter from "./helpers/EventEmitter.js";
import { createLogger } from "./helpers/index.js";
import throttle from "./helpers/throttle.js";
import { HistoryManager } from "./history-manager.js";
const logger = createLogger("graph-manager"); const logger = createLogger("graph-manager");
@ -68,6 +67,7 @@ export class GraphManager extends EventEmitter<{ "save": Graph, "result": any, "
const edges = this._edges.map(edge => [edge[0].id, edge[1], edge[2].id, edge[3]]) as Graph["edges"]; const edges = this._edges.map(edge => [edge[0].id, edge[1], edge[2].id, edge[3]]) as Graph["edges"];
const serialized = { id: this.graph.id, settings: this.settings, nodes, edges }; const serialized = { id: this.graph.id, settings: this.settings, nodes, edges };
logger.groupEnd(); logger.groupEnd();
console.log({ serialized });
return clone(serialized); return clone(serialized);
} }

View File

@ -4,7 +4,6 @@
lerp, lerp,
snapToGrid as snapPointToGrid, snapToGrid as snapPointToGrid,
} from "../helpers/index.js"; } from "../helpers/index.js";
import { Canvas } from "@threlte/core";
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 type { GraphManager } from "../graph-manager.js";
@ -14,20 +13,16 @@
import type { Node, NodeId, Node as NodeType, Socket } from "@nodes/types"; import type { Node, NodeId, Node as NodeType, Socket } from "@nodes/types";
import { GraphSchema } from "@nodes/types"; import { GraphSchema } from "@nodes/types";
import FloatingEdge from "../edges/FloatingEdge.svelte"; import FloatingEdge from "../edges/FloatingEdge.svelte";
import { import { getGraphState } from "./state.svelte";
activeNodeId,
activeSocket,
hoveredSocket,
possibleSockets,
possibleSocketIds,
selectedNodes,
} from "./stores.js";
import { createKeyMap } from "../../helpers/createKeyMap"; import { createKeyMap } from "../../helpers/createKeyMap";
import BoxSelection from "../BoxSelection.svelte"; import BoxSelection from "../BoxSelection.svelte";
import AddMenu from "../AddMenu.svelte"; import AddMenu from "../AddMenu.svelte";
import HelpView from "../HelpView.svelte"; import HelpView from "../HelpView.svelte";
import FileSaver from "file-saver"; import FileSaver from "file-saver";
import { Canvas } from "@threlte/core";
const state = getGraphState();
export let manager: GraphManager; export let manager: GraphManager;
@ -184,7 +179,7 @@
} }
setContext("setDownSocket", (socket: Socket) => { setContext("setDownSocket", (socket: Socket) => {
$activeSocket = socket; state.activeSocket = socket;
let { node, index, position } = socket; let { node, index, position } = socket;
@ -203,14 +198,14 @@
} }
mouseDown = position; mouseDown = position;
$activeSocket = { state.activeSocket = {
node, node,
index, index,
position, position,
}; };
$possibleSockets = manager state.possibleSockets = manager
.getPossibleSockets($activeSocket) .getPossibleSockets(state.activeSocket)
.map(([node, index]) => { .map(([node, index]) => {
return { return {
node, node,
@ -218,9 +213,6 @@
position: getSocketPosition(node, index), position: getSocketPosition(node, index),
}; };
}); });
$possibleSocketIds = new Set(
$possibleSockets.map((s) => `${s.node.id}-${s.index}`),
);
}); });
function getSnapLevel() { function getSnapLevel() {
@ -271,10 +263,10 @@
if (!mouseDown) return; if (!mouseDown) return;
// we are creating a new edge here // we are creating a new edge here
if ($activeSocket || $possibleSockets?.length) { if (state.activeSocket || state.possibleSockets?.length) {
let smallestDist = 1000; let smallestDist = 1000;
let _socket; let _socket;
for (const socket of $possibleSockets) { for (const socket of state.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,
@ -287,9 +279,9 @@
if (_socket && smallestDist < 0.9) { if (_socket && smallestDist < 0.9) {
mousePosition = _socket.position; mousePosition = _socket.position;
$hoveredSocket = _socket; state.hoveredSocket = _socket;
} else { } else {
$hoveredSocket = null; state.hoveredSocket = null;
} }
return; return;
} }
@ -309,18 +301,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) {
$selectedNodes?.add(node.id); state.selectedNodes?.add(node.id);
} else { } else {
$selectedNodes?.delete(node.id); state.selectedNodes?.delete(node.id);
} }
} }
$selectedNodes = $selectedNodes;
return; return;
} }
// here we are handling dragging of nodes // here we are handling dragging of nodes
if ($activeNodeId !== -1 && mouseDownId !== -1) { if (state.activeNodeId !== -1 && mouseDownId !== -1) {
const node = manager.getNode($activeNodeId); const node = manager.getNode(state.activeNodeId);
if (!node || event.buttons !== 1) return; if (!node || event.buttons !== 1) return;
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
@ -349,8 +340,8 @@
const vecX = oldX - newX; const vecX = oldX - newX;
const vecY = oldY - newY; const vecY = oldY - newY;
if ($selectedNodes?.size) { if (state.selectedNodes?.size) {
for (const nodeId of $selectedNodes) { for (const nodeId of state.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;
@ -433,44 +424,43 @@
// if we clicked on a node // if we clicked on a node
if (clickedNodeId !== -1) { if (clickedNodeId !== -1) {
if ($activeNodeId === -1) { if (state.activeNodeId === -1) {
$activeNodeId = clickedNodeId; state.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 ($activeNodeId === clickedNodeId) { } else if (state.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) {
$selectedNodes = $selectedNodes || new Set(); state.selectedNodes = state.selectedNodes || new Set();
$selectedNodes.add($activeNodeId); state.selectedNodes.add(state.activeNodeId);
$selectedNodes.delete(clickedNodeId); state.selectedNodes.delete(clickedNodeId);
$activeNodeId = clickedNodeId; state.activeNodeId = clickedNodeId;
// select the node // select the node
} else if (event.shiftKey) { } else if (event.shiftKey) {
const activeNode = manager.getNode($activeNodeId); const activeNode = manager.getNode(state.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)); const selected = new Set(edge.map((n) => n.id));
selected.add(clickedNodeId); selected.add(clickedNodeId);
$selectedNodes = selected; state.selectedNodes = selected;
} }
} }
} else if (!$selectedNodes?.has(clickedNodeId)) { } else if (!state.selectedNodes?.has(clickedNodeId)) {
$activeNodeId = clickedNodeId; state.activeNodeId = clickedNodeId;
$selectedNodes?.clear(); state.clearSelection();
$selectedNodes = $selectedNodes;
} }
} else if (event.ctrlKey) { } else if (event.ctrlKey) {
boxSelection = true; boxSelection = true;
} }
const node = manager.getNode($activeNodeId); const node = manager.getNode(state.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 ($selectedNodes) { if (state.selectedNodes) {
for (const nodeId of $selectedNodes) { for (const nodeId of state.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 || {};
@ -481,8 +471,8 @@
} }
function copyNodes() { function copyNodes() {
if ($activeNodeId === -1 && !$selectedNodes?.size) return; if (state.activeNodeId === -1 && !state.selectedNodes?.size) return;
let _nodes = [$activeNodeId, ...($selectedNodes?.values() || [])] let _nodes = [state.activeNodeId, ...(state.selectedNodes?.values() || [])]
.map((id) => manager.getNode(id)) .map((id) => manager.getNode(id))
.filter(Boolean) as Node[]; .filter(Boolean) as Node[];
@ -518,7 +508,7 @@
.filter(Boolean) as Node[]; .filter(Boolean) as Node[];
const newNodes = manager.createGraph(_nodes, clipboard.edges); const newNodes = manager.createGraph(_nodes, clipboard.edges);
$selectedNodes = new Set(newNodes.map((n) => n.id)); state.selectedNodes = new Set(newNodes.map((n) => n.id));
} }
const isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT"; const isBodyFocused = () => document?.activeElement?.nodeName !== "INPUT";
@ -527,10 +517,10 @@
key: "l", key: "l",
description: "Select linked nodes", description: "Select linked nodes",
callback: () => { callback: () => {
const activeNode = manager.getNode($activeNodeId); const activeNode = manager.getNode(state.activeNodeId);
if (activeNode) { if (activeNode) {
const nodes = manager.getLinkedNodes(activeNode); const nodes = manager.getLinkedNodes(activeNode);
$selectedNodes = new Set(nodes.map((n) => n.id)); state.selectedNodes = new Set(nodes.map((n) => n.id));
} }
console.log(activeNode); console.log(activeNode);
}, },
@ -562,9 +552,8 @@
key: "Escape", key: "Escape",
description: "Deselect nodes", description: "Deselect nodes",
callback: () => { callback: () => {
$activeNodeId = -1; state.activeNodeId = -1;
$selectedNodes?.clear(); state.clearSelection();
$selectedNodes = $selectedNodes;
(document.activeElement as HTMLElement)?.blur(); (document.activeElement as HTMLElement)?.blur();
}, },
}); });
@ -616,7 +605,7 @@
description: "Select all nodes", description: "Select all nodes",
callback: () => { callback: () => {
if (!isBodyFocused()) return; if (!isBodyFocused()) return;
$selectedNodes = new Set($nodes.keys()); state.selectedNodes = new Set($nodes.keys());
}, },
}); });
@ -665,22 +654,21 @@
callback: (event) => { callback: (event) => {
if (!isBodyFocused()) return; if (!isBodyFocused()) return;
manager.startUndoGroup(); manager.startUndoGroup();
if ($activeNodeId !== -1) { if (state.activeNodeId !== -1) {
const node = manager.getNode($activeNodeId); const node = manager.getNode(state.activeNodeId);
if (node) { if (node) {
manager.removeNode(node, { restoreEdges: event.ctrlKey }); manager.removeNode(node, { restoreEdges: event.ctrlKey });
$activeNodeId = -1; state.activeNodeId = -1;
} }
} }
if ($selectedNodes) { if (state.selectedNodes) {
for (const nodeId of $selectedNodes) { for (const nodeId of state.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 });
} }
} }
$selectedNodes.clear(); state.clearSelection();
$selectedNodes = $selectedNodes;
} }
manager.saveUndoGroup(); manager.saveUndoGroup();
}, },
@ -689,16 +677,15 @@
function handleMouseUp(event: MouseEvent) { function handleMouseUp(event: MouseEvent) {
if (!mouseDown) return; if (!mouseDown) return;
const activeNode = manager.getNode($activeNodeId); const activeNode = manager.getNode(state.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) {
$selectedNodes?.clear(); state.clearSelection();
$selectedNodes = $selectedNodes; state.activeNodeId = clickedNodeId;
$activeNodeId = clickedNodeId;
} }
} }
} }
@ -721,7 +708,7 @@
activeNode.position[1] = activeNode?.tmp?.y ?? activeNode.position[1]; activeNode.position[1] = activeNode?.tmp?.y ?? activeNode.position[1];
} }
const nodes = [ const nodes = [
...[...($selectedNodes?.values() || [])].map((id) => ...[...(state.selectedNodes?.values() || [])].map((id) =>
manager.getNode(id), manager.getNode(id),
), ),
] as NodeType[]; ] as NodeType[];
@ -760,26 +747,26 @@
$edges = $edges; $edges = $edges;
}); });
manager.save(); manager.save();
} else if ($hoveredSocket && $activeSocket) { } else if (state.hoveredSocket && state.activeSocket) {
if ( if (
typeof $hoveredSocket.index === "number" && typeof state.hoveredSocket.index === "number" &&
typeof $activeSocket.index === "string" typeof state.activeSocket.index === "string"
) { ) {
manager.createEdge( manager.createEdge(
$hoveredSocket.node, state.hoveredSocket.node,
$hoveredSocket.index || 0, state.hoveredSocket.index || 0,
$activeSocket.node, state.activeSocket.node,
$activeSocket.index, state.activeSocket.index,
); );
} else if ( } else if (
typeof $activeSocket.index == "number" && typeof state.activeSocket.index == "number" &&
typeof $hoveredSocket.index === "string" typeof state.hoveredSocket.index === "string"
) { ) {
manager.createEdge( manager.createEdge(
$activeSocket.node, state.activeSocket.node,
$activeSocket.index || 0, state.activeSocket.index || 0,
$hoveredSocket.node, state.hoveredSocket.node,
$hoveredSocket.index, state.hoveredSocket.index,
); );
} }
manager.save(); manager.save();
@ -793,17 +780,15 @@
cameraDown[1] === cameraPosition[1] && cameraDown[1] === cameraPosition[1] &&
isBodyFocused() isBodyFocused()
) { ) {
$activeNodeId = -1; state.activeNodeId = -1;
$selectedNodes?.clear(); state.clearSelection();
$selectedNodes = $selectedNodes;
} }
mouseDown = null; mouseDown = null;
boxSelection = false; boxSelection = false;
$activeSocket = null; state.activeSocket = null;
$possibleSockets = []; state.possibleSockets = [];
$possibleSocketIds = null; state.hoveredSocket = null;
$hoveredSocket = null;
addMenuPosition = null; addMenuPosition = null;
} }
@ -958,9 +943,12 @@
<AddMenu bind:position={addMenuPosition} graph={manager} /> <AddMenu bind:position={addMenuPosition} graph={manager} />
{/if} {/if}
{#if $activeSocket} {#if state.activeSocket}
<FloatingEdge <FloatingEdge
from={{ x: $activeSocket.position[0], y: $activeSocket.position[1] }} from={{
x: state.activeSocket.position[0],
y: state.activeSocket.position[1],
}}
to={{ x: mousePosition[0], y: mousePosition[1] }} to={{ x: mousePosition[0], y: mousePosition[1] }}
/> />
{/if} {/if}

View File

@ -5,13 +5,14 @@
import Node from "../node/Node.svelte"; import Node from "../node/Node.svelte";
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { activeSocket } from "./stores.js"; import { getGraphState } from "./state.svelte";
export let nodes: Writable<Map<number, NodeType>>; export let nodes: Writable<Map<number, NodeType>>;
export let edges: Writable<EdgeType[]>; export let edges: Writable<EdgeType[]>;
export let cameraPosition = [0, 0, 4]; export let cameraPosition = [0, 0, 4];
const graphState = getGraphState();
const isNodeInView = getContext<(n: NodeType) => boolean>("isNodeInView"); const isNodeInView = getContext<(n: NodeType) => boolean>("isNodeInView");
const getSocketPosition = const getSocketPosition =
@ -58,7 +59,7 @@
tabindex="0" tabindex="0"
class="wrapper" class="wrapper"
style:transform={`scale(${cameraPosition[2] * 0.1})`} style:transform={`scale(${cameraPosition[2] * 0.1})`}
class:hovering-sockets={activeSocket} class:hovering-sockets={graphState.activeSocket}
> >
{#each $nodes.values() as node (node.id)} {#each $nodes.values() as node (node.id)}
<Node <Node

View File

@ -2,58 +2,77 @@
import type { Graph, Node, NodeRegistry } from "@nodes/types"; import type { Graph, Node, NodeRegistry } from "@nodes/types";
import GraphEl from "./Graph.svelte"; import GraphEl from "./Graph.svelte";
import { GraphManager } from "../graph-manager.js"; import { GraphManager } from "../graph-manager.js";
import { createEventDispatcher, setContext } from "svelte"; import { setContext } from "svelte";
import { type Writable } from "svelte/store"; 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 { activeNodeId } from "./stores"; import { GraphState } from "./state.svelte";
export let registry: NodeRegistry; type Props = {
export let graph: Graph; graph: Graph;
export let settings: Writable<Record<string, any>> | undefined; registry: NodeRegistry;
settings?: Writable<Record<string, any>>;
activeNode?: Node;
showGrid?: boolean;
snapToGrid?: boolean;
showHelp?: boolean;
settingTypes?: Record<string, any>;
onsave?: (save: Graph) => void;
onresult?: (result: any) => void;
};
let {
graph,
registry,
settings = $bindable(),
activeNode = $bindable(),
showGrid,
snapToGrid,
showHelp = $bindable(false),
settingTypes = $bindable(),
onsave,
onresult,
}: Props = $props();
export const keymap = createKeyMap([]);
export const manager = new GraphManager(registry); export const manager = new GraphManager(registry);
export let activeNode: Node | undefined;
$: if ($activeNodeId !== -1) { const state = new GraphState();
activeNode = manager.getNode($activeNodeId); setContext("graphState", state);
$effect(() => {
if (state.activeNodeId !== -1) {
activeNode = manager.getNode(state.activeNodeId);
} else { } else {
activeNode = undefined; activeNode = undefined;
} }
});
export const status = manager.status;
export const keymap = createKeyMap([]);
setContext("keymap", keymap); setContext("keymap", keymap);
export let showGrid = false;
export let snapToGrid = false;
export let showHelp = false;
export let settingTypes = {};
const updateSettings = debounce((s) => { const updateSettings = debounce((s) => {
manager.setSettings(s); manager.setSettings(s);
}, 200); }, 200);
$: if (settings && $settings) { $effect(() => {
if (settingTypes && settings) {
updateSettings($settings); updateSettings($settings);
} }
});
manager.on("settings", (_settings) => { manager.on("settings", (_settings) => {
settingTypes = _settings.types; settingTypes = _settings.types;
settings.set(_settings.values); settings?.set(_settings.values);
}); });
manager.on("result", (result) => { manager.on("result", (result) => onresult?.(result));
dispatch("result", result);
});
manager.on("save", (save) => { manager.on("save", (save) => onsave?.(save));
dispatch("save", save);
});
manager.load(graph); manager.load(graph);
const dispatch = createEventDispatcher();
</script> </script>
<GraphEl {manager} bind:showGrid bind:snapToGrid bind:showHelp /> <GraphEl {manager} bind:showGrid bind:snapToGrid bind:showHelp />

View File

@ -18,7 +18,8 @@ let lastStyle = "";
function updateColors() { function updateColors() {
if (!("getComputedStyle" in globalThis)) return; if (!("getComputedStyle" in globalThis)) return;
const style = getComputedStyle(document.body); console.log("updateColors")
const style = getComputedStyle(document.body.parentElement!);
let hash = ""; let hash = "";
for (const v of variables) { for (const v of variables) {
let color = style.getPropertyValue(`--${v}`); let color = style.getPropertyValue(`--${v}`);

View File

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

View File

@ -1,13 +0,0 @@
import type { Socket } from "@nodes/types";
import { writable, type Writable } from "svelte/store";
import { Color } from "three/src/math/Color.js";
export const activeNodeId: Writable<number> = writable(-1);
export const selectedNodes: Writable<Set<number> | null> = writable(new Set());
export const activeSocket: Writable<Socket | null> = writable(null);
export const hoveredSocket: Writable<Socket | null> = writable(null);
export const possibleSockets: Writable<Socket[]> = writable([]);
export const possibleSocketIds: Writable<Set<string> | null> = writable(null);
export { colors } from "./colors";

View File

@ -1,38 +1,44 @@
<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 { activeNodeId, selectedNodes } from "../graph/stores.js"; import { colors, getGraphState } from "../graph/state.svelte";
import { colors } from "../graph/stores";
import { T } from "@threlte/core"; import { T } from "@threlte/core";
import { Color, type Mesh } from "three"; import { Color, 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";
export let node: Node; const graphState = getGraphState();
export let inView = true;
export let z = 2;
$: isActive = $activeNodeId === node.id; type Props = {
$: isSelected = !!$selectedNodes?.has(node.id); node: Node;
inView: boolean;
z: number;
};
const { node, inView, z }: Props = $props();
const isActive = $derived(graphState.activeNodeId === node.id);
const isSelected = $derived(!!graphState.selectedNodes?.has(node.id));
const updateNodePosition = const updateNodePosition =
getContext<(n: Node) => void>("updateNodePosition"); getContext<(n: Node) => void>("updateNodePosition");
const getNodeHeight = getContext<(n: string) => number>("getNodeHeight"); const getNodeHeight = getContext<(n: string) => number>("getNodeHeight");
let meshRef: Mesh; let meshRef: Mesh | undefined = $state();
const height = getNodeHeight?.(node.type); const height = getNodeHeight?.(node.type);
$: if (node && meshRef) { $effect(() => {
if (node && meshRef) {
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.mesh = meshRef; node.tmp.mesh = meshRef;
updateNodePosition?.(node); updateNodePosition?.(node);
} }
});
$: colorBright = $colors["layer-2"]; const colorBright = $colors["layer-2"];
$: colorDark = $colors["layer-1"]; const colorDark = $colors["layer-1"];
onMount(() => { onMount(() => {
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};

View File

@ -60,7 +60,7 @@
role="button" role="button"
tabindex="0" tabindex="0"
on:mousedown={handleMouseDown} on:mousedown={handleMouseDown}
/> ></div>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100" viewBox="0 0 100 100"

View File

@ -3,23 +3,33 @@
import { getGraphManager } from "../graph/context.js"; import { getGraphManager } from "../graph/context.js";
import { Input } from "@nodes/ui"; import { Input } from "@nodes/ui";
export let node: Node; type Props = {
export let input: NodeInput; node: Node;
export let id: string; input: NodeInput;
id: string;
elementId?: string;
};
const {
node,
input,
id,
elementId = `input-${Math.random().toString(36).substring(7)}`,
}: Props = $props();
const graph = getGraphManager(); const graph = getGraphManager();
let value = node?.props?.[id] ?? input.value; let value = $state(node?.props?.[id] ?? input.value);
export let elementId: string = `input-${Math.random().toString(36).substring(7)}`; $effect(() => {
if (value !== undefined && node?.props?.[id] !== value) {
$: if (node?.props?.[id] !== value) {
node.props = { ...node.props, [id]: value }; node.props = { ...node.props, [id]: value };
if (graph) { if (graph) {
graph.save(); graph.save();
graph.execute(); graph.execute();
} }
} }
});
</script> </script>
<Input id="input-{elementId}" {input} bind:value /> <Input id="input-{elementId}" {input} bind:value />

View File

@ -6,20 +6,25 @@
} from "@nodes/types"; } from "@nodes/types";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { createNodePath } from "../helpers/index.js"; import { createNodePath } from "../helpers/index.js";
import { possibleSocketIds } from "../graph/stores.js";
import { getGraphManager } from "../graph/context.js"; import { getGraphManager } from "../graph/context.js";
import NodeInput from "./NodeInput.svelte"; import NodeInput from "./NodeInput.svelte";
import { getGraphState } from "../graph/state.svelte.js";
export let node: NodeType; type Props = {
export let input: NodeInputType; node: NodeType;
export let id: string; input: NodeInputType;
export let isLast = false; id: string;
isLast?: boolean;
};
const { node = $bindable(), input, id, isLast }: Props = $props();
const inputType = node?.tmp?.type?.inputs?.[id]!; const inputType = node?.tmp?.type?.inputs?.[id]!;
const socketId = `${node.id}-${id}`; const socketId = `${node.id}-${id}`;
const graph = getGraphManager(); const graph = getGraphManager();
const graphState = getGraphState();
const graphId = graph?.id; const graphId = graph?.id;
const inputSockets = graph?.inputSockets; const inputSockets = graph?.inputSockets;
@ -75,7 +80,7 @@
class="wrapper" class="wrapper"
data-node-type={node.type} data-node-type={node.type}
data-node-input={id} data-node-input={id}
class:disabled={$possibleSocketIds && !$possibleSocketIds.has(socketId)} class:disabled={!graphState.possibleSocketIds.has(socketId)}
> >
{#key id && graphId} {#key id && graphId}
<div class="content" class:disabled={$inputSockets?.has(socketId)}> <div class="content" class:disabled={$inputSockets?.has(socketId)}>
@ -91,17 +96,17 @@
<div <div
data-node-socket data-node-socket
class="large target" class="large target"
on:mousedown={handleMouseDown} onmousedown={handleMouseDown}
role="button" role="button"
tabindex="0" tabindex="0"
/> ></div>
<div <div
data-node-socket data-node-socket
class="small target" class="small target"
on:mousedown={handleMouseDown} onmousedown={handleMouseDown}
role="button" role="button"
tabindex="0" tabindex="0"
/> ></div>
{/if} {/if}
{/key} {/key}
@ -185,9 +190,11 @@
d: var(--path); d: var(--path);
} }
:global(.hovering-sockets) .large:hover ~ svg path { :global {
.hovering-sockets .large:hover ~ svg path {
d: var(--hover-path); d: var(--hover-path);
} }
}
.content.disabled { .content.disabled {
opacity: 0.2; opacity: 0.2;

View File

@ -50,6 +50,7 @@
<div class="wrapper" class:visible={$activePanel}> <div class="wrapper" class:visible={$activePanel}>
<div class="tabs"> <div class="tabs">
<button <button
aria-label="Close"
on:click={() => { on:click={() => {
setActivePanel($activePanel ? false : keys[0]); setActivePanel($activePanel ? false : keys[0]);
}} }}
@ -59,11 +60,12 @@
{#each keys as panel (panels[panel].id)} {#each keys as panel (panels[panel].id)}
{#if panels[panel].visible !== false} {#if panels[panel].visible !== false}
<button <button
aria-label={panel}
class="tab {panels[panel].classes}" class="tab {panels[panel].classes}"
class:active={panel === $activePanel} class:active={panel === $activePanel}
on:click={() => setActivePanel(panel)} on:click={() => setActivePanel(panel)}
> >
<span class={`block w-6 h-6 ${panels[panel].icon}`} /> <span class={`block w-6 h-6 ${panels[panel].icon}`}></span>
</button> </button>
{/if} {/if}
{/each} {/each}

View File

@ -20,14 +20,16 @@ export const AppSettings = localStore("node.settings", {
const themes = ["dark", "light", "catppuccin", "solarized", "high-contrast", "nord", "dracula"]; const themes = ["dark", "light", "catppuccin", "solarized", "high-contrast", "nord", "dracula"];
AppSettings.subscribe((value) => { AppSettings.subscribe((value) => {
const classes = document.body.classList; const classes = document.body.parentElement?.classList;
const newClassName = `theme-${themes[value.theme]}`; const newClassName = `theme-${themes[value.theme]}`;
if (classes) {
for (const className of classes) { for (const className of classes) {
if (className.startsWith("theme-") && className !== newClassName) { if (className.startsWith("theme-") && className !== newClassName) {
classes.remove(className); classes.remove(className);
} }
} }
document.body.classList.add(newClassName); }
document.body?.parentElement?.classList.add(newClassName);
}); });
export const AppSettingTypes = { export const AppSettingTypes = {

View File

@ -24,11 +24,12 @@
MemoryRuntimeExecutor, MemoryRuntimeExecutor,
} from "@nodes/runtime"; } from "@nodes/runtime";
import { IndexDBCache, RemoteNodeRegistry } from "@nodes/registry"; import { IndexDBCache, RemoteNodeRegistry } from "@nodes/registry";
import { decodeNestedArray, createPerformanceStore } from "@nodes/utils"; import { createPerformanceStore } from "@nodes/utils";
import BenchmarkPanel from "$lib/settings/panels/BenchmarkPanel.svelte"; import BenchmarkPanel from "$lib/settings/panels/BenchmarkPanel.svelte";
import { debounceAsyncFunction } from "$lib/helpers"; import { debounceAsyncFunction } from "$lib/helpers";
import type { Component } from "svelte";
let performanceStore = createPerformanceStore("page"); let performanceStore = createPerformanceStore();
const registryCache = new IndexDBCache("node-registry"); const registryCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry(""); const nodeRegistry = new RemoteNodeRegistry("");
@ -38,17 +39,6 @@
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache); const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
memoryRuntime.perf = performanceStore; memoryRuntime.perf = performanceStore;
globalThis.decode = decodeNestedArray;
globalThis.clearCache = () => {
registryCache.clear();
runtimeCache.clear();
localStorage.clear();
setTimeout(() => {
window.location.reload();
}, 500);
};
$: runtime = $AppSettings.useWorker ? workerRuntime : memoryRuntime; $: runtime = $AppSettings.useWorker ? workerRuntime : memoryRuntime;
let activeNode: Node | undefined; let activeNode: Node | undefined;
@ -59,11 +49,10 @@
? JSON.parse(localStorage.getItem("graph")!) ? JSON.parse(localStorage.getItem("graph")!)
: templates.defaultPlant; : templates.defaultPlant;
let manager: GraphManager; let graphInterface: ReturnType<typeof GraphInterface>;
let managerStatus: Writable<"loading" | "error" | "idle">; $: manager = graphInterface?.manager;
$: if (manager) { $: managerStatus = manager?.status;
managerStatus = manager.status; $: keymap = graphInterface?.keymap;
}
async function randomGenerate() { async function randomGenerate() {
const g = manager.serialize(); const g = manager.serialize();
@ -71,7 +60,6 @@
await handleUpdate(g, s); await handleUpdate(g, s);
} }
let keymap: ReturnType<typeof createKeyMap>;
let applicationKeymap = createKeyMap([ let applicationKeymap = createKeyMap([
{ {
key: "r", key: "r",
@ -136,8 +124,8 @@
}; };
} }
function handleSave(event: CustomEvent<Graph>) { function handleSave(graph: Graph) {
localStorage.setItem("graph", JSON.stringify(event.detail)); localStorage.setItem("graph", JSON.stringify(graph));
} }
</script> </script>
@ -156,18 +144,17 @@
<Grid.Cell> <Grid.Cell>
{#key graph} {#key graph}
<GraphInterface <GraphInterface
bind:this={graphInterface}
{graph} {graph}
registry={nodeRegistry} registry={nodeRegistry}
bind:manager
bind:activeNode bind:activeNode
bind:keymap
showGrid={$AppSettings.showNodeGrid} showGrid={$AppSettings.showNodeGrid}
snapToGrid={$AppSettings.snapToGrid} snapToGrid={$AppSettings.snapToGrid}
bind:showHelp={$AppSettings.showHelp} bind:showHelp={$AppSettings.showHelp}
bind:settings={graphSettings} bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes} bind:settingTypes={graphSettingTypes}
on:result={(ev) => handleUpdate(ev.detail, $graphSettings)} onresult={(result) => handleUpdate(result, $graphSettings)}
on:save={handleSave} onsave={(graph) => handleSave(graph)}
/> />
<Settings> <Settings>
<Panel id="general" title="General" icon="i-tabler-settings"> <Panel id="general" title="General" icon="i-tabler-settings">

View File

@ -5,6 +5,7 @@ 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() { 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);