refactor: move view logic inside graph.svelte

This commit is contained in:
max_richter 2024-03-12 18:47:50 +01:00
parent af24b5cffe
commit 9241700ada
16 changed files with 465 additions and 384 deletions

View File

@ -17,7 +17,6 @@
position[0] = camera.position.x;
position[1] = camera.position.z;
position[2] = camera.zoom;
saveControls();
}
@ -57,6 +56,7 @@
<T.OrthographicCamera bind:ref={camera} position.y={10} makeDefault>
<OrbitControls
args={[camera, window]}
bind:ref={controls}
enableZoom={true}
zoomSpeed={2}

View File

@ -2,15 +2,13 @@
import type { Node } from "$lib/types";
import NodeHeader from "./NodeHeader.svelte";
import NodeParameter from "./NodeParameter.svelte";
import { getGraphManager } from "./graph/context";
export let node: Node;
const graph = getGraphManager();
export let inView = true;
const type = graph.getNodeType(node.type);
export let possibleSocketIds: null | Set<string> = null;
const type = node?.tmp?.type;
const parameters = Object.entries(type?.inputs || {});
</script>
@ -18,7 +16,6 @@
<div
class="node"
class:in-view={inView}
class:is-moving={node?.tmp?.isMoving}
data-node-id={node.id}
style={`--nx:${node.position.x * 10}px;
--ny: ${node.position.y * 10}px`}
@ -28,9 +25,9 @@
{#each parameters as [key, value], i}
<NodeParameter
{node}
{possibleSocketIds}
id={key}
index={i}
value={node?.props?.[key]}
input={value}
isLast={i == parameters.length - 1}
/>

View File

@ -1,5 +1,6 @@
<script lang="ts">
import type { Node } from "$lib/types";
import { getContext } from "svelte";
import { getGraphManager, getGraphState } from "./graph/context";
export let node: Node;
@ -34,16 +35,15 @@
Z`.replace(/\s+/g, " ");
}
const setDownSocket = getContext("setDownSocket");
function handleMouseDown(event: MouseEvent) {
event.stopPropagation();
event.preventDefault();
state.setMouseDown({
x: node.position.x + 5,
y: node.position.y + 0.625,
setDownSocket({
node,
type: node.tmp?.type?.outputs?.[0] || "",
index: 0,
isInput: false,
position: [node.position.x + 5, node.position.y + 0.625],
});
}
</script>

View File

@ -1,17 +1,16 @@
<script lang="ts">
import type { NodeInput } from "$lib/types";
import type { Node } from "$lib/types";
import { getGraphState } from "./graph/context";
import { getContext } from "svelte";
export let node: Node;
export let value: unknown;
export let input: NodeInput;
export let id: string;
export let index: number;
export let isLast = false;
export let possibleSocketIds: null | Set<string> = null;
const state = getGraphState();
export let isLast = false;
function createPath({ depth = 8, height = 20, y = 50 } = {}) {
let corner = isLast ? 5 : 0;
@ -46,21 +45,24 @@
Z`.replace(/\s+/g, " ");
}
const setDownSocket = getContext("setDownSocket");
function handleMouseDown(ev: MouseEvent) {
ev.preventDefault();
ev.stopPropagation();
state.setMouseDown({
x: node.position.x,
y: node.position.y + 2.5 + index * 2.5,
setDownSocket({
node,
index: index,
type: node?.tmp?.type?.inputs?.[id].type || "",
isInput: true,
index: id,
position: [node.position.x, node.position.y + 2.5 + index * 2.5],
});
}
</script>
<div class="wrapper">
<div
class="wrapper"
class:disabled={possibleSocketIds &&
!possibleSocketIds.has(`${node.id}-${id}`)}
>
<div class="content">
<label>{id}</label>
@ -69,11 +71,16 @@
{#if node.tmp?.type?.inputs?.[id].internal !== true}
<div
class="click-target"
class="large target"
on:mousedown={handleMouseDown}
role="button"
tabindex="0"
/>
<div
class="small target"
on:mousedown={handleMouseDown}
role="button"
tabindex="0"
style={`background: var(--node-hovered-in-${node.tmp?.type?.inputs?.[id].type}`}
/>
{/if}
@ -85,6 +92,7 @@
preserveAspectRatio="none"
style={`
--path: path("${createPath({ depth: 5, height: 15, y: 50 })}");
--hover-path-disabled: path("${createPath({ depth: 0, height: 15, y: 50 })}");
--hover-path: path("${createPath({ depth: 8, height: 24, y: 50 })}");
`}
>
@ -100,14 +108,24 @@
transform: translateY(-0.5px);
}
.click-target {
.target {
position: absolute;
border-radius: 50%;
}
.small.target {
width: 6px;
height: 6px;
border-radius: 50%;
top: 9.5px;
left: -3px;
opacity: 0.1;
}
.large.target {
width: 15px;
height: 15px;
top: 5px;
left: -7.5px;
cursor: unset;
}
.content {
@ -142,7 +160,6 @@
svg {
position: absolute;
box-sizing: border-box;
/* pointer-events: none; */
width: 100%;
height: 100%;
overflow: visible;
@ -160,7 +177,11 @@
d: var(--path);
}
.click-target:hover + svg path {
d: var(--hover-path) !important;
:global(.hovering-sockets) .large:hover ~ svg path {
d: var(--hover-path);
}
.disabled svg path {
d: var(--hover-path-disabled) !important;
}
</style>

View File

@ -8,9 +8,7 @@
export let minZoom = 4;
export let maxZoom = 150;
export let cx = 0;
export let cy = 0;
export let cz = 30;
export let cameraPosition: [number, number, number] = [0, 1, 0];
export let width = globalThis?.innerWidth || 100;
export let height = globalThis?.innerHeight || 100;
@ -19,12 +17,16 @@
let bh = 2;
$: if (width && height) {
bw = width / cz;
bh = height / cz;
bw = width / cameraPosition[2];
bh = height / cameraPosition[2];
}
</script>
<T.Group position.x={cx} position.z={cy} position.y={-1.0}>
<T.Group
position.x={cameraPosition[0]}
position.z={cameraPosition[1]}
position.y={-1.0}
>
<T.Mesh rotation.x={-Math.PI / 2} position.y={0.2} scale.x={bw} scale.y={bh}>
<T.PlaneGeometry args={[1, 1]} />
<T.ShaderMaterial
@ -54,9 +56,9 @@
value: 100,
},
}}
uniforms.cx.value={cx}
uniforms.cy.value={cy}
uniforms.cz.value={cz}
uniforms.cx.value={cameraPosition[0]}
uniforms.cy.value={cameraPosition[1]}
uniforms.cz.value={cameraPosition[2]}
uniforms.width.value={width}
uniforms.height.value={height}
/>

View File

@ -1,12 +1,11 @@
import type { Vector3 } from "three";
import { Vector3 } from "three";
import { lines, points } from "./store";
export function debugPosition(pos: Vector3) {
export function debugPosition(x: number, y: number) {
points.update((p) => {
p.push(pos);
p.push(new Vector3(x, 1, y));
return p;
});
}
export function clear() {

View File

@ -0,0 +1,8 @@
<script lang="ts">
import Edge from "./Edge.svelte";
export let from: { x: number; y: number };
export let to: { x: number; y: number };
</script>
<Edge {from} {to} />

View File

@ -1,16 +1,16 @@
<script lang="ts">
import Edge from "../Edge.svelte";
import { HTML } from "@threlte/extras";
import Node from "../Node.svelte";
import { animate, lerp, snapToGrid } from "$lib/helpers";
import Debug from "../debug/Debug.svelte";
import { OrthographicCamera } from "three";
import Background from "../background/Background.svelte";
import type { GraphManager } from "$lib/graph-manager";
import { setContext } from "svelte";
import { GraphState } from "./state";
import Camera from "../Camera.svelte";
import GraphView from "./GraphView.svelte";
import type { Node as NodeType } from "$lib/types";
import FloatingEdge from "../edges/FloatingEdge.svelte";
import * as debug from "../debug";
import type { Socket } from "$lib/types";
export let graph: GraphManager;
setContext("graphManager", graph);
@ -18,44 +18,131 @@
const nodes = graph.nodes;
const edges = graph.edges;
const state = new GraphState(graph);
setContext("graphState", state);
const mouse = state.mouse;
const dimensions = state.dimensions;
const mouseDown = state.mouseDown;
const cameraPosition = state.cameraPosition;
const cameraBounds = state.cameraBounds;
const activeNodeId = state.activeNodeId;
const hoveredSocket = state.hoveredSocket;
let camera: OrthographicCamera;
const minZoom = 4;
const maxZoom = 150;
let mousePosition = [0, 0];
let mouseDown: null | [number, number] = null;
let cameraPosition: [number, number, number] = [0, 1, 0];
let width = 100;
let height = 100;
$: edgePositions = $edges.map((edge) => {
const index = Object.keys(edge[2].tmp?.type?.inputs || {}).indexOf(edge[3]);
return [
edge[0].position.x + 5,
edge[0].position.y + 0.625 + edge[1] * 2.5,
edge[2].position.x,
edge[2].position.y + 2.5 + index * 2.5,
];
let activeNodeId = -1;
let downSocket: null | Socket = null;
let possibleSockets: Socket[] = [];
$: possibleSocketIds = possibleSockets?.length
? new Set(possibleSockets.map((s) => `${s.node.id}-${s.index}`))
: null;
let hoveredSocket: Socket | null = null;
$: cameraBounds = [
cameraPosition[0] - width / cameraPosition[2],
cameraPosition[0] + width / cameraPosition[2],
cameraPosition[1] - height / cameraPosition[2],
cameraPosition[1] + height / cameraPosition[2],
];
setContext("isNodeInView", (node: NodeType) => {
return (
node.position.x > cameraBounds[0] &&
node.position.x < cameraBounds[1] &&
node.position.y > cameraBounds[2] &&
node.position.y < cameraBounds[3]
);
});
function handleMouseMove(event: MouseEvent) {
state.setMouseFromEvent(event);
setContext("setDownSocket", (socket: Socket) => {
downSocket = socket;
if (!$mouseDown) return;
if (state?.possibleSockets?.length) {
let { node, index, position } = socket;
// remove existing edge
if (typeof index === "string") {
const edges = graph.getEdgesToNode(node);
console.log({ edges });
for (const edge of edges) {
if (edge[3] === index) {
node = edge[0];
index = edge[1];
position = getSocketPosition({ node, index });
graph.removeEdge(edge);
break;
}
}
}
mouseDown = position;
downSocket = {
node,
index,
position,
};
possibleSockets = graph
.getPossibleSockets(downSocket)
.map(([node, index]) => {
return {
node,
index,
position: getSocketPosition({ node, index }),
};
});
});
function getSnapLevel() {
const z = cameraPosition[2];
if (z > 66) {
return 8;
} else if (z > 55) {
return 4;
} else if (z > 11) {
return 2;
} else {
}
return 1;
}
function getSocketPosition(
socket: Omit<Socket, "position">,
): [number, number] {
if (typeof socket.index === "number") {
return [
socket.node.position.x + 5,
socket.node.position.y + 0.625 + 2.5 * socket.index,
];
} else {
const _index = Object.keys(socket.node.tmp?.type?.inputs || {}).indexOf(
socket.index,
);
return [
socket.node.position.x,
socket.node.position.y + 2.5 + 2.5 * _index,
];
}
}
function setMouseFromEvent(event: MouseEvent) {
const x = event.clientX;
const y = event.clientY;
mousePosition = [
cameraPosition[0] + (x - width / 2) / cameraPosition[2],
cameraPosition[1] + (y - height / 2) / cameraPosition[2],
];
}
function handleMouseMove(event: MouseEvent) {
setMouseFromEvent(event);
if (!mouseDown) return;
if (possibleSockets?.length) {
let smallestDist = 1000;
let _socket;
for (const socket of state.possibleSockets) {
const posX = socket.position[0];
const posY = socket.position[1];
for (const socket of possibleSockets) {
const dist = Math.sqrt(
(posX - $mouse[0]) ** 2 + (posY - $mouse[1]) ** 2,
(socket.position[0] - mousePosition[0]) ** 2 +
(socket.position[1] - mousePosition[1]) ** 2,
);
if (dist < smallestDist) {
smallestDist = dist;
@ -64,28 +151,27 @@
}
if (_socket && smallestDist < 0.3) {
state.setMouse(_socket.position[0], _socket.position[1]);
state.hoveredSocket.set(_socket);
mousePosition = _socket.position;
hoveredSocket = _socket;
} else {
state.hoveredSocket.set(null);
hoveredSocket = null;
}
}
if ($activeNodeId === -1) return;
const node = graph.getNode($activeNodeId);
if (activeNodeId === -1) return;
const node = graph.getNode(activeNodeId);
if (!node) return;
if (!node.tmp) node.tmp = {};
node.tmp = node.tmp || {};
node.tmp.isMoving = true;
let newX =
(node?.tmp?.downX || 0) +
(event.clientX - $mouseDown.x) / $cameraPosition[2];
(event.clientX - mouseDown[0]) / cameraPosition[2];
let newY =
(node?.tmp?.downY || 0) +
(event.clientY - $mouseDown.y) / $cameraPosition[2];
(event.clientY - mouseDown[1]) / cameraPosition[2];
if (event.ctrlKey) {
const snapLevel = getSnapLevel();
@ -100,54 +186,32 @@
edges.set($edges);
}
function handleMouseDown(ev: MouseEvent) {
if ($mouseDown) return;
function handleMouseDown(event: MouseEvent) {
if (mouseDown) return;
for (const node of ev.composedPath()) {
for (const node of event.composedPath()) {
let _activeNodeId = (node as unknown as HTMLElement)?.getAttribute?.(
"data-node-id",
)!;
if (_activeNodeId) {
$activeNodeId = parseInt(_activeNodeId, 10);
activeNodeId = parseInt(_activeNodeId, 10);
break;
}
}
if ($activeNodeId < 0) return;
if (activeNodeId < 0) return;
$mouseDown = { x: ev.clientX, y: ev.clientY };
const node = graph.getNode($activeNodeId);
mouseDown = [event.clientX, event.clientY];
const node = graph.getNode(activeNodeId);
if (!node) return;
node.tmp = node.tmp || {};
node.tmp.downX = node.position.x;
node.tmp.downY = node.position.y;
}
function getSnapLevel() {
const z = $cameraPosition[2];
if (z > 66) {
return 8;
} else if (z > 55) {
return 4;
} else if (z > 11) {
return 2;
} else {
}
return 1;
}
function handleMouseUp(event: MouseEvent) {
if (event.button !== 0) return;
function isNodeInView(node: any) {
return (
node.position.x > $cameraBounds[0] &&
node.position.x < $cameraBounds[1] &&
node.position.y > $cameraBounds[2] &&
node.position.y < $cameraBounds[3]
);
}
function handleMouseUp(ev: MouseEvent) {
if (ev.button !== 0) return;
const node = graph.getNode($activeNodeId);
const node = graph.getNode(activeNodeId);
if (node) {
node.tmp = node.tmp || {};
node.tmp.isMoving = false;
@ -157,41 +221,39 @@
animate(500, (a: number) => {
node.position.x = lerp(node.position.x, fx, a);
node.position.y = lerp(node.position.y, fy, a);
nodes.set($nodes);
edges.set($edges);
if (node?.tmp?.isMoving) {
return false;
}
nodes.set($nodes);
edges.set($edges);
});
nodes.set($nodes);
edges.set($edges);
} else if ($hoveredSocket && $mouseDown && $mouseDown?.node) {
if ($hoveredSocket.isInput) {
const newEdge: [NodeType, number, NodeType, string] = [
$hoveredSocket.node,
$hoveredSocket.index || 0,
$mouseDown.node,
Object.keys($mouseDown?.node?.tmp?.type?.inputs || {})[
$mouseDown?.index || 0
],
];
$edges = [...$edges, newEdge];
} else if (hoveredSocket && downSocket) {
console.log({ hoveredSocket, downSocket });
if (
typeof hoveredSocket.index === "number" &&
typeof downSocket.index === "string"
) {
graph.createEdge(
hoveredSocket.node,
hoveredSocket.index || 0,
downSocket.node,
downSocket.index,
);
} else {
const newEdge: [NodeType, number, NodeType, string] = [
$mouseDown.node,
$mouseDown?.index || 0,
$hoveredSocket.node,
Object.keys($hoveredSocket.node?.tmp?.type?.inputs || {})[
$hoveredSocket.index
],
];
$edges = [...$edges, newEdge];
graph.createEdge(
downSocket.node,
downSocket.index || 0,
hoveredSocket.node,
hoveredSocket.index,
);
}
}
$mouseDown = false;
$hoveredSocket = null;
$activeNodeId = -1;
mouseDown = null;
downSocket = null;
possibleSockets = [];
hoveredSocket = null;
activeNodeId = -1;
}
</script>
@ -201,67 +263,30 @@
on:mousedown={handleMouseDown}
/>
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />
<Debug />
<Camera bind:camera {maxZoom} {minZoom} bind:position={$cameraPosition} />
<Camera bind:camera {maxZoom} {minZoom} bind:position={cameraPosition} />
<Background
cx={$cameraPosition[0]}
cy={$cameraPosition[1]}
cz={$cameraPosition[2]}
{maxZoom}
{minZoom}
width={$dimensions[0]}
height={$dimensions[1]}
/>
<Background {cameraPosition} {maxZoom} {minZoom} {width} {height} />
{#if $status === "idle"}
{#each edgePositions as [x1, y1, x2, y2]}
<Edge
from={{
x: x1,
y: y1,
}}
to={{
x: x2,
y: y2,
}}
{#if downSocket}
<FloatingEdge
from={{ x: downSocket.position[0], y: downSocket.position[1] }}
to={{ x: mousePosition[0], y: mousePosition[1] }}
/>
{/each}
{#if $mouseDown && $mouseDown?.node}
<Edge from={$mouseDown} to={{ x: $mouse[0], y: $mouse[1] }} />
{/if}
<HTML transform={false}>
<div
role="tree"
tabindex="0"
class="wrapper"
class:zoom-small={$cameraPosition[2] < 10}
style={`--cz: ${$cameraPosition[2]}; ${$mouseDown ? `--node-hovered-${$mouseDown.isInput ? "out" : "in"}-${$mouseDown.type}: red;` : ""}`}
>
{#each $nodes as node}
<Node {node} inView={$cameraPosition && isNodeInView(node)} />
{/each}
</div>
</HTML>
<GraphView
{nodes}
{edges}
{cameraPosition}
{possibleSocketIds}
{downSocket}
/>
{:else if $status === "loading"}
<span>Loading</span>
{:else if $status === "error"}
<span>Error</span>
{/if}
<style>
:global(body) {
overflow: hidden;
}
.wrapper {
position: absolute;
z-index: 100;
width: 0px;
height: 0px;
transform: scale(calc(var(--cz) * 0.1));
}
</style>

View File

@ -0,0 +1,72 @@
<script lang="ts">
import type { Edge as EdgeType, Node as NodeType } from "$lib/types";
import { HTML } from "@threlte/extras";
import Edge from "../edges/Edge.svelte";
import Node from "../Node.svelte";
import { getContext } from "svelte";
import type { Writable } from "svelte/store";
export let nodes: Writable<Map<number, NodeType>>;
export let edges: Writable<EdgeType[]>;
export let cameraPosition = [0, 1, 0];
export let downSocket: null | { node: NodeType; index: number | string } =
null;
export let possibleSocketIds: null | Set<string> = null;
const isNodeInView = getContext<(n: NodeType) => boolean>("isNodeInView");
function getEdgePosition(edge: EdgeType) {
const index = Object.keys(edge[2].tmp?.type?.inputs || {}).indexOf(edge[3]);
return [
edge[0].position.x + 5,
edge[0].position.y + 0.625 + edge[1] * 2.5,
edge[2].position.x,
edge[2].position.y + 2.5 + index * 2.5,
];
}
</script>
{#each $edges as edge}
{@const pos = getEdgePosition(edge)}
{@const [x1, y1, x2, y2] = pos}
<Edge
from={{
x: x1,
y: y1,
}}
to={{
x: x2,
y: y2,
}}
/>
{/each}
<HTML transform={false}>
<div
role="tree"
tabindex="0"
class="wrapper"
class:zoom-small={cameraPosition[2] < 10}
class:hovering-sockets={downSocket}
style={`--cz: ${cameraPosition[2]}`}
>
{#each $nodes.values() as node}
<Node
{node}
inView={cameraPosition && isNodeInView(node)}
{possibleSocketIds}
/>
{/each}
</div>
</HTML>
<style>
.wrapper {
position: absolute;
z-index: 100;
width: 0px;
height: 0px;
transform: scale(calc(var(--cz) * 0.1));
}
</style>

View File

@ -1,11 +1,11 @@
import type { GraphManager } from "$lib/graph-manager";
import { getContext } from "svelte";
import type { GraphState } from "./graph-state";
import type { GraphView } from "./view";
export function getGraphManager(): GraphManager {
return getContext("graphManager");
}
export function getGraphState(): GraphState {
export function getGraphState(): GraphView {
return getContext("graphState");
}

View File

@ -1,129 +0,0 @@
import type { GraphManager } from "$lib/graph-manager";
import type { Node } from "$lib/types";
import { derived, get, writable, type Writable } from "svelte/store";
import * as debug from "../debug";
type Socket = {
node: Node;
index: number;
isInput: boolean;
type: string;
position: [number, number];
}
export class GraphState {
activeNodeId: Writable<number> = writable(-1);
dimensions: Writable<[number, number]> = writable([100, 100]);
mouse: Writable<[number, number]> = writable([0, 0]);
mouseDown: Writable<false | ({ x: number, y: number } & Omit<Socket, "position">)> = writable(false);
cameraPosition: Writable<[number, number, number]> = writable([0, 1, 0]);
cameraBounds = derived([this.cameraPosition, this.dimensions], ([_cameraPosition, [width, height]]) => {
return [
_cameraPosition[0] - width / _cameraPosition[2],
_cameraPosition[0] + width / _cameraPosition[2],
_cameraPosition[1] - height / _cameraPosition[2],
_cameraPosition[1] + height / _cameraPosition[2],
] as const
});
possibleSockets: Socket[] = [];
hoveredSocket: Writable<Socket | null> = writable(null);
constructor(private graph: GraphManager) {
if (globalThis?.innerWidth && globalThis?.innerHeight) {
this.dimensions.set([window.innerWidth, window.innerHeight]);
globalThis.addEventListener("resize", () => {
this.dimensions.set([window.innerWidth, window.innerHeight]);
})
}
}
setMouse(x: number, y: number) {
this.mouse.set([x, y]);
}
setMouseFromEvent(event: MouseEvent) {
const x = event.clientX;
const y = event.clientY;
const cameraPosition = get(this.cameraPosition);
const dimensions = get(this.dimensions);
this.mouse.set([
cameraPosition[0] + (x - dimensions[0] / 2) / cameraPosition[2],
cameraPosition[1] + (y - dimensions[1] / 2) / cameraPosition[2],
]);
}
getSocketPosition(node: Node, index: number | string) {
const isOutput = typeof index === "number";
if (isOutput) {
return [node.position.x + 5, node.position.y + 0.625 + 2.5 * index] as const;
} else {
const _index = Object.keys(node.tmp?.type?.inputs || {}).indexOf(index);
return [node.position.x, node.position.y + 2.5 + 2.5 * _index] as const;
}
}
setMouseDown(opts: ({ x: number, y: number } & Omit<Socket, "position">) | false) {
if (!opts) {
this.mouseDown.set(false);
return;
}
let { x, y, node, index, isInput, type } = opts;
if (node && index !== undefined && isInput !== undefined) {
debug.clear();
// remove existing edge
if (isInput) {
const edges = this.graph.getEdgesToNode(node);
const key = Object.keys(node.tmp?.type?.inputs || {})[index];
for (const edge of edges) {
if (edge[3] === key) {
node = edge[2];
index = 0;
const pos = this.getSocketPosition(edge[0], index);
x = pos[0];
y = pos[1];
isInput = false;
this.graph.removeEdge(edge);
break;
}
}
}
this.mouseDown.set({ x, y, node, index, isInput, type });
this.possibleSockets = this.graph.getPossibleSockets(node, index, isInput).map(([node, index]) => {
if (isInput) {
const key = Object.keys(node.tmp?.type?.inputs || {})[index];
return {
node,
index,
isInput,
type: node.tmp?.type?.inputs?.[key].type || "",
position: [node.position.x + 5, node.position.y + 0.625 + 2.5 * index]
}
} else {
return {
node,
index,
isInput,
type: node.tmp?.type?.outputs?.[index] || "",
position: [node.position.x, node.position.y + 2.5 + 2.5 * index]
}
}
});
}
console.log("possibleSockets", this.possibleSockets);
}
}

View File

@ -1,5 +1,5 @@
import { get, writable, type Writable } from "svelte/store";
import type { Graph, NodeRegistry as INodeRegistry, NodeType, Node, Edge } from "./types";
import { writable, type Writable } from "svelte/store";
import type { Graph, NodeRegistry as INodeRegistry, NodeType, Node, Edge, Socket } from "./types";
const nodeTypes: NodeType[] = [
{
@ -38,8 +38,8 @@ export class GraphManager {
status: Writable<"loading" | "idle" | "error"> = writable("loading");
private _nodes: Node[] = [];
nodes: Writable<Node[]> = writable([]);
private _nodes: Map<number, Node> = new Map();
nodes: Writable<Map<number, Node>> = writable(new Map());
private _edges: Edge[] = [];
edges: Writable<Edge[]> = writable([]);
@ -54,10 +54,10 @@ export class GraphManager {
async load() {
const nodes = this.graph.nodes;
const nodes = new Map(this.graph.nodes.map(node => [node.id, node]));
for (const node of nodes) {
const nodeType = this.getNodeType(node.type);
for (const node of nodes.values()) {
const nodeType = this.nodeRegistry.getNode(node.type);
if (!nodeType) {
console.error(`Node type not found: ${node.type}`);
this.status.set("error");
@ -67,39 +67,110 @@ export class GraphManager {
node.tmp.type = nodeType;
}
this.nodes.set(nodes);
this.edges.set(this.graph.edges.map((edge) => {
const from = this._nodes.find((node) => node.id === edge[0]);
const to = this._nodes.find((node) => node.id === edge[2]);
if (!from || !to) return;
const from = nodes.get(edge[0]);
const to = nodes.get(edge[2]);
if (!from || !to) {
console.error("Edge references non-existing node");
return;
};
from.tmp = from.tmp || {};
from.tmp.children = from.tmp.children || [];
from.tmp.children.push(to);
to.tmp = to.tmp || {};
to.tmp.parents = to.tmp.parents || [];
to.tmp.parents.push(from);
return [from, edge[1], to, edge[3]] as const;
})
.filter(Boolean) as unknown as [Node, number, Node, string][]
);
this.nodes.set(nodes);
console.log(this._nodes);
this.status.set("idle");
}
getNode(id: number) {
return this._nodes.find((node) => node.id === id);
getAllNodes() {
return Array.from(this._nodes.values());
}
getPossibleSockets(node: Node, socketIndex: number, isInput: boolean): [Node, number][] {
getNode(id: number) {
return this._nodes.get(id);
}
const nodeType = this.getNodeType(node.type);
getChildrenOfNode(node: Node) {
const children = [];
const stack = node.tmp?.children?.slice(0);
while (stack?.length) {
const child = stack.pop();
if (!child) continue;
children.push(child);
stack.push(...child.tmp?.children || []);
}
return children;
}
createEdge(from: Node, fromSocket: number, to: Node, toSocket: string) {
const existingEdges = this.getEdgesToNode(to);
// check if this exact edge already exists
const existingEdge = existingEdges.find(e => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket);
if (existingEdge) {
console.log("Edge already exists");
console.log(existingEdge)
return;
};
// check if socket types match
const fromSocketType = from.tmp?.type?.outputs?.[fromSocket];
const toSocketType = to.tmp?.type?.inputs?.[toSocket]?.type;
if (fromSocketType !== toSocketType) {
console.error(`Socket types do not match: ${fromSocketType} !== ${toSocketType}`);
return;
}
this.edges.update((edges) => {
return [...edges.filter(e => e[2].id !== to.id || e[3] !== toSocket), [from, fromSocket, to, toSocket]];
});
}
getParentsOfNode(node: Node) {
const parents = [];
const stack = node.tmp?.parents?.slice(0);
while (stack?.length) {
const parent = stack.pop();
if (!parent) continue;
parents.push(parent);
stack.push(...parent.tmp?.parents || []);
}
return parents;
}
getPossibleSockets({ node, index }: Socket): [Node, string | number][] {
const nodeType = node?.tmp?.type;
if (!nodeType) return [];
const nodes = this._nodes.filter(n => n.id !== node.id);
const sockets: [Node, number][] = []
if (isInput) {
const sockets: [Node, string | number][] = []
// if index is a string, we are an input looking for outputs
if (typeof index === "string") {
const ownType = Object.values(nodeType?.inputs || {})[socketIndex].type;
// filter out self and child nodes
const children = new Set(this.getChildrenOfNode(node).map(n => n.id));
const nodes = this.getAllNodes().filter(n => n.id !== node.id && !children.has(n.id));
const ownType = nodeType?.inputs?.[index].type;
for (const node of nodes) {
const nodeType = this.getNodeType(node.type);
const nodeType = node?.tmp?.type;
const inputs = nodeType?.outputs;
if (!inputs) continue;
for (let index = 0; index < inputs.length; index++) {
@ -109,31 +180,33 @@ export class GraphManager {
}
}
} else {
} else if (typeof index === "number") {
// if index is a number, we are an output looking for inputs
const ownType = nodeType.outputs?.[socketIndex];
// filter out self and parent nodes
const parents = new Set(this.getParentsOfNode(node).map(n => n.id));
const nodes = this.getAllNodes().filter(n => n.id !== node.id && !parents.has(n.id));
// get edges from this socket
const edges = new Map(this.getEdgesFromNode(node).filter(e => e[1] === index).map(e => [e[2].id, e[3]]));
const ownType = nodeType.outputs?.[index];
for (const node of nodes) {
const nodeType = this.getNodeType(node.type);
const inputs = nodeType?.inputs;
const entries = Object.values(inputs || {});
entries.map((input, index) => {
if (input.type === ownType) {
sockets.push([node, index]);
const inputs = node?.tmp?.type?.inputs;
if (!inputs) continue;
for (const key in inputs) {
if (inputs[key].type === ownType && edges.get(node.id) !== key) {
sockets.push([node, key]);
}
});
}
}
}
return sockets;
}
getNodeType(id: string): NodeType {
return this.nodeRegistry.getNode(id)!;
}
removeEdge(edge: Edge) {
const id0 = edge[0].id;
const sid0 = edge[1];
@ -148,8 +221,8 @@ export class GraphManager {
return this._edges
.filter((edge) => edge[2].id === node.id)
.map((edge) => {
const from = this._nodes.find((node) => node.id === edge[0].id);
const to = this._nodes.find((node) => node.id === edge[2].id);
const from = this.getNode(edge[0].id);
const to = this.getNode(edge[2].id);
if (!from || !to) return;
return [from, edge[1], to, edge[3]] as const;
})
@ -158,10 +231,10 @@ export class GraphManager {
getEdgesFromNode(node: Node) {
return this._edges
.filter((edge) => edge[0] === node.id)
.filter((edge) => edge[0].id === node.id)
.map((edge) => {
const from = this._nodes.find((node) => node.id === edge[0]);
const to = this._nodes.find((node) => node.id === edge[2]);
const from = this.getNode(edge[0].id);
const to = this.getNode(edge[2].id);
if (!from || !to) return;
return [from, edge[1], to, edge[3]] as const;
})

View File

@ -6,6 +6,8 @@ export type Node = {
type: string;
props?: Record<string, any>,
tmp?: {
parents?: Node[],
children?: Node[],
type?: NodeType;
downX?: number;
downY?: number;
@ -31,6 +33,13 @@ export type NodeType = {
}
}
export type Socket = {
node: Node;
index: number | string;
position: [number, number];
};
export interface NodeRegistry {
getNode: (id: string) => NodeType | undefined;
}

View File

@ -10,26 +10,26 @@
const graph = GraphManager.createEmptyGraph({ width: 12, height: 12 });
graph.load();
onMount(async () => {
try {
const res = await invoke("greet", { name: "Dude" });
console.log({ res });
} catch (error) {
console.log(error);
}
try {
const res2 = await invoke("run_nodes", {});
console.log({ res2 });
} catch (error) {
console.log(error);
}
});
// onMount(async () => {
// try {
// const res = await invoke("greet", { name: "Dude" });
// console.log({ res });
// } catch (error) {
// console.log(error);
// }
//
// try {
// const res2 = await invoke("run_nodes", {});
// console.log({ res2 });
// } catch (error) {
// console.log(error);
// }
// });
</script>
<div>
<Canvas shadows={false} renderMode="on-demand">
<PerfMonitor />
<Canvas shadows={false} renderMode="on-demand" autoRender={true}>
<!-- <PerfMonitor /> -->
<Graph {graph} />
</Canvas>
</div>

View File

@ -23,3 +23,7 @@
:root {
font-family: 'Fira Code', monospace;
}
body {
overflow: hidden;
}