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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,11 @@
import type { Vector3 } from "three"; import { Vector3 } from "three";
import { lines, points } from "./store"; import { lines, points } from "./store";
export function debugPosition(pos: Vector3) { export function debugPosition(x: number, y: number) {
points.update((p) => { points.update((p) => {
p.push(pos); p.push(new Vector3(x, 1, y));
return p; return p;
}); });
} }
export function clear() { 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"> <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 { animate, lerp, snapToGrid } from "$lib/helpers";
import Debug from "../debug/Debug.svelte"; import Debug from "../debug/Debug.svelte";
import { OrthographicCamera } from "three"; import { OrthographicCamera } from "three";
import Background from "../background/Background.svelte"; import Background from "../background/Background.svelte";
import type { GraphManager } from "$lib/graph-manager"; import type { GraphManager } from "$lib/graph-manager";
import { setContext } from "svelte"; import { setContext } from "svelte";
import { GraphState } from "./state";
import Camera from "../Camera.svelte"; import Camera from "../Camera.svelte";
import GraphView from "./GraphView.svelte";
import type { Node as NodeType } from "$lib/types"; 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; export let graph: GraphManager;
setContext("graphManager", graph); setContext("graphManager", graph);
@ -18,44 +18,131 @@
const nodes = graph.nodes; const nodes = graph.nodes;
const edges = graph.edges; 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; let camera: OrthographicCamera;
const minZoom = 4; const minZoom = 4;
const maxZoom = 150; 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) => { let activeNodeId = -1;
const index = Object.keys(edge[2].tmp?.type?.inputs || {}).indexOf(edge[3]); let downSocket: null | Socket = null;
return [ let possibleSockets: Socket[] = [];
edge[0].position.x + 5, $: possibleSocketIds = possibleSockets?.length
edge[0].position.y + 0.625 + edge[1] * 2.5, ? new Set(possibleSockets.map((s) => `${s.node.id}-${s.index}`))
edge[2].position.x, : null;
edge[2].position.y + 2.5 + index * 2.5, 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) { setContext("setDownSocket", (socket: Socket) => {
state.setMouseFromEvent(event); downSocket = socket;
if (!$mouseDown) return; let { node, index, position } = socket;
if (state?.possibleSockets?.length) {
// 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 smallestDist = 1000;
let _socket; let _socket;
for (const socket of state.possibleSockets) { for (const socket of possibleSockets) {
const posX = socket.position[0];
const posY = socket.position[1];
const dist = Math.sqrt( 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) { if (dist < smallestDist) {
smallestDist = dist; smallestDist = dist;
@ -64,28 +151,27 @@
} }
if (_socket && smallestDist < 0.3) { if (_socket && smallestDist < 0.3) {
state.setMouse(_socket.position[0], _socket.position[1]); mousePosition = _socket.position;
state.hoveredSocket.set(_socket); hoveredSocket = _socket;
} else { } else {
state.hoveredSocket.set(null); hoveredSocket = null;
} }
} }
if ($activeNodeId === -1) return; if (activeNodeId === -1) return;
const node = graph.getNode($activeNodeId);
const node = graph.getNode(activeNodeId);
if (!node) return; if (!node) return;
if (!node.tmp) node.tmp = {}; node.tmp = node.tmp || {};
node.tmp.isMoving = true; node.tmp.isMoving = true;
let newX = let newX =
(node?.tmp?.downX || 0) + (node?.tmp?.downX || 0) +
(event.clientX - $mouseDown.x) / $cameraPosition[2]; (event.clientX - mouseDown[0]) / cameraPosition[2];
let newY = let newY =
(node?.tmp?.downY || 0) + (node?.tmp?.downY || 0) +
(event.clientY - $mouseDown.y) / $cameraPosition[2]; (event.clientY - mouseDown[1]) / cameraPosition[2];
if (event.ctrlKey) { if (event.ctrlKey) {
const snapLevel = getSnapLevel(); const snapLevel = getSnapLevel();
@ -100,54 +186,32 @@
edges.set($edges); edges.set($edges);
} }
function handleMouseDown(ev: MouseEvent) { function handleMouseDown(event: MouseEvent) {
if ($mouseDown) return; if (mouseDown) return;
for (const node of ev.composedPath()) { for (const node of event.composedPath()) {
let _activeNodeId = (node as unknown as HTMLElement)?.getAttribute?.( let _activeNodeId = (node as unknown as HTMLElement)?.getAttribute?.(
"data-node-id", "data-node-id",
)!; )!;
if (_activeNodeId) { if (_activeNodeId) {
$activeNodeId = parseInt(_activeNodeId, 10); activeNodeId = parseInt(_activeNodeId, 10);
break; break;
} }
} }
if ($activeNodeId < 0) return; if (activeNodeId < 0) return;
$mouseDown = { x: ev.clientX, y: ev.clientY }; mouseDown = [event.clientX, event.clientY];
const node = graph.getNode($activeNodeId); const node = graph.getNode(activeNodeId);
if (!node) return; if (!node) return;
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.downX = node.position.x; node.tmp.downX = node.position.x;
node.tmp.downY = node.position.y; node.tmp.downY = node.position.y;
} }
function getSnapLevel() { function handleMouseUp(event: MouseEvent) {
const z = $cameraPosition[2]; if (event.button !== 0) return;
if (z > 66) {
return 8;
} else if (z > 55) {
return 4;
} else if (z > 11) {
return 2;
} else {
}
return 1;
}
function isNodeInView(node: any) { const node = graph.getNode(activeNodeId);
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);
if (node) { if (node) {
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.isMoving = false; node.tmp.isMoving = false;
@ -157,41 +221,39 @@
animate(500, (a: number) => { animate(500, (a: number) => {
node.position.x = lerp(node.position.x, fx, a); node.position.x = lerp(node.position.x, fx, a);
node.position.y = lerp(node.position.y, fy, a); node.position.y = lerp(node.position.y, fy, a);
nodes.set($nodes);
edges.set($edges);
if (node?.tmp?.isMoving) { if (node?.tmp?.isMoving) {
return false; return false;
} }
nodes.set($nodes);
edges.set($edges);
}); });
nodes.set($nodes); } else if (hoveredSocket && downSocket) {
edges.set($edges); console.log({ hoveredSocket, downSocket });
} else if ($hoveredSocket && $mouseDown && $mouseDown?.node) { if (
if ($hoveredSocket.isInput) { typeof hoveredSocket.index === "number" &&
const newEdge: [NodeType, number, NodeType, string] = [ typeof downSocket.index === "string"
$hoveredSocket.node, ) {
$hoveredSocket.index || 0, graph.createEdge(
$mouseDown.node, hoveredSocket.node,
Object.keys($mouseDown?.node?.tmp?.type?.inputs || {})[ hoveredSocket.index || 0,
$mouseDown?.index || 0 downSocket.node,
], downSocket.index,
]; );
$edges = [...$edges, newEdge];
} else { } else {
const newEdge: [NodeType, number, NodeType, string] = [ graph.createEdge(
$mouseDown.node, downSocket.node,
$mouseDown?.index || 0, downSocket.index || 0,
$hoveredSocket.node, hoveredSocket.node,
Object.keys($hoveredSocket.node?.tmp?.type?.inputs || {})[ hoveredSocket.index,
$hoveredSocket.index );
],
];
$edges = [...$edges, newEdge];
} }
} }
$mouseDown = false; mouseDown = null;
$hoveredSocket = null; downSocket = null;
$activeNodeId = -1; possibleSockets = [];
hoveredSocket = null;
activeNodeId = -1;
} }
</script> </script>
@ -201,67 +263,30 @@
on:mousedown={handleMouseDown} on:mousedown={handleMouseDown}
/> />
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />
<Debug /> <Debug />
<Camera bind:camera {maxZoom} {minZoom} bind:position={$cameraPosition} /> <Camera bind:camera {maxZoom} {minZoom} bind:position={cameraPosition} />
<Background <Background {cameraPosition} {maxZoom} {minZoom} {width} {height} />
cx={$cameraPosition[0]}
cy={$cameraPosition[1]}
cz={$cameraPosition[2]}
{maxZoom}
{minZoom}
width={$dimensions[0]}
height={$dimensions[1]}
/>
{#if $status === "idle"} {#if $status === "idle"}
{#each edgePositions as [x1, y1, x2, y2]} {#if downSocket}
<Edge <FloatingEdge
from={{ from={{ x: downSocket.position[0], y: downSocket.position[1] }}
x: x1, to={{ x: mousePosition[0], y: mousePosition[1] }}
y: y1,
}}
to={{
x: x2,
y: y2,
}}
/> />
{/each}
{#if $mouseDown && $mouseDown?.node}
<Edge from={$mouseDown} to={{ x: $mouse[0], y: $mouse[1] }} />
{/if} {/if}
<GraphView
<HTML transform={false}> {nodes}
<div {edges}
role="tree" {cameraPosition}
tabindex="0" {possibleSocketIds}
class="wrapper" {downSocket}
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>
{:else if $status === "loading"} {:else if $status === "loading"}
<span>Loading</span> <span>Loading</span>
{:else if $status === "error"} {:else if $status === "error"}
<span>Error</span> <span>Error</span>
{/if} {/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 type { GraphManager } from "$lib/graph-manager";
import { getContext } from "svelte"; import { getContext } from "svelte";
import type { GraphState } from "./graph-state"; import type { GraphView } from "./view";
export function getGraphManager(): GraphManager { export function getGraphManager(): GraphManager {
return getContext("graphManager"); return getContext("graphManager");
} }
export function getGraphState(): GraphState { export function getGraphState(): GraphView {
return getContext("graphState"); 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 { writable, type Writable } from "svelte/store";
import type { Graph, NodeRegistry as INodeRegistry, NodeType, Node, Edge } from "./types"; import type { Graph, NodeRegistry as INodeRegistry, NodeType, Node, Edge, Socket } from "./types";
const nodeTypes: NodeType[] = [ const nodeTypes: NodeType[] = [
{ {
@ -38,8 +38,8 @@ export class GraphManager {
status: Writable<"loading" | "idle" | "error"> = writable("loading"); status: Writable<"loading" | "idle" | "error"> = writable("loading");
private _nodes: Node[] = []; private _nodes: Map<number, Node> = new Map();
nodes: Writable<Node[]> = writable([]); nodes: Writable<Map<number, Node>> = writable(new Map());
private _edges: Edge[] = []; private _edges: Edge[] = [];
edges: Writable<Edge[]> = writable([]); edges: Writable<Edge[]> = writable([]);
@ -54,10 +54,10 @@ export class GraphManager {
async load() { async load() {
const nodes = this.graph.nodes; const nodes = new Map(this.graph.nodes.map(node => [node.id, node]));
for (const node of nodes) { for (const node of nodes.values()) {
const nodeType = this.getNodeType(node.type); const nodeType = this.nodeRegistry.getNode(node.type);
if (!nodeType) { if (!nodeType) {
console.error(`Node type not found: ${node.type}`); console.error(`Node type not found: ${node.type}`);
this.status.set("error"); this.status.set("error");
@ -67,39 +67,110 @@ export class GraphManager {
node.tmp.type = nodeType; node.tmp.type = nodeType;
} }
this.nodes.set(nodes);
this.edges.set(this.graph.edges.map((edge) => { this.edges.set(this.graph.edges.map((edge) => {
const from = this._nodes.find((node) => node.id === edge[0]); const from = nodes.get(edge[0]);
const to = this._nodes.find((node) => node.id === edge[2]); const to = nodes.get(edge[2]);
if (!from || !to) return; 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; return [from, edge[1], to, edge[3]] as const;
}) })
.filter(Boolean) as unknown as [Node, number, Node, string][] .filter(Boolean) as unknown as [Node, number, Node, string][]
); );
this.nodes.set(nodes);
console.log(this._nodes);
this.status.set("idle"); this.status.set("idle");
} }
getNode(id: number) { getAllNodes() {
return this._nodes.find((node) => node.id === id); 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 []; if (!nodeType) return [];
const nodes = this._nodes.filter(n => n.id !== node.id);
const sockets: [Node, number][] = [] const sockets: [Node, string | number][] = []
if (isInput) { // 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) { for (const node of nodes) {
const nodeType = this.getNodeType(node.type); const nodeType = node?.tmp?.type;
const inputs = nodeType?.outputs; const inputs = nodeType?.outputs;
if (!inputs) continue; if (!inputs) continue;
for (let index = 0; index < inputs.length; index++) { for (let index = 0; index < inputs.length; index++) {
@ -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) { for (const node of nodes) {
const nodeType = this.getNodeType(node.type); const inputs = node?.tmp?.type?.inputs;
const inputs = nodeType?.inputs; if (!inputs) continue;
const entries = Object.values(inputs || {}); for (const key in inputs) {
entries.map((input, index) => { if (inputs[key].type === ownType && edges.get(node.id) !== key) {
if (input.type === ownType) { sockets.push([node, key]);
sockets.push([node, index]); }
} }
});
} }
} }
return sockets; return sockets;
} }
getNodeType(id: string): NodeType {
return this.nodeRegistry.getNode(id)!;
}
removeEdge(edge: Edge) { removeEdge(edge: Edge) {
const id0 = edge[0].id; const id0 = edge[0].id;
const sid0 = edge[1]; const sid0 = edge[1];
@ -148,8 +221,8 @@ export class GraphManager {
return this._edges return this._edges
.filter((edge) => edge[2].id === node.id) .filter((edge) => edge[2].id === node.id)
.map((edge) => { .map((edge) => {
const from = this._nodes.find((node) => node.id === edge[0].id); const from = this.getNode(edge[0].id);
const to = this._nodes.find((node) => node.id === edge[2].id); const to = this.getNode(edge[2].id);
if (!from || !to) return; if (!from || !to) return;
return [from, edge[1], to, edge[3]] as const; return [from, edge[1], to, edge[3]] as const;
}) })
@ -158,10 +231,10 @@ export class GraphManager {
getEdgesFromNode(node: Node) { getEdgesFromNode(node: Node) {
return this._edges return this._edges
.filter((edge) => edge[0] === node.id) .filter((edge) => edge[0].id === node.id)
.map((edge) => { .map((edge) => {
const from = this._nodes.find((node) => node.id === edge[0]); const from = this.getNode(edge[0].id);
const to = this._nodes.find((node) => node.id === edge[2]); const to = this.getNode(edge[2].id);
if (!from || !to) return; if (!from || !to) return;
return [from, edge[1], to, edge[3]] as const; return [from, edge[1], to, edge[3]] as const;
}) })

View File

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

View File

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

View File

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