refactor: move view logic inside graph.svelte
This commit is contained in:
parent
af24b5cffe
commit
9241700ada
@ -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}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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() {
|
||||
|
8
frontend/src/lib/components/edges/FloatingEdge.svelte
Normal file
8
frontend/src/lib/components/edges/FloatingEdge.svelte
Normal 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} />
|
@ -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>
|
||||
|
72
frontend/src/lib/components/graph/GraphView.svelte
Normal file
72
frontend/src/lib/components/graph/GraphView.svelte
Normal 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>
|
@ -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");
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
})
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -23,3 +23,7 @@
|
||||
:root {
|
||||
font-family: 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user