diff --git a/frontend/src/lib/components/Camera.svelte b/frontend/src/lib/components/Camera.svelte index d586ff8..1b8a475 100644 --- a/frontend/src/lib/components/Camera.svelte +++ b/frontend/src/lib/components/Camera.svelte @@ -17,7 +17,6 @@ position[0] = camera.position.x; position[1] = camera.position.z; position[2] = camera.zoom; - saveControls(); } @@ -57,6 +56,7 @@ = null; + + const type = node?.tmp?.type; const parameters = Object.entries(type?.inputs || {}); @@ -18,7 +16,6 @@
diff --git a/frontend/src/lib/components/NodeHeader.svelte b/frontend/src/lib/components/NodeHeader.svelte index 8c173e8..4e4f36e 100644 --- a/frontend/src/lib/components/NodeHeader.svelte +++ b/frontend/src/lib/components/NodeHeader.svelte @@ -1,5 +1,6 @@ diff --git a/frontend/src/lib/components/NodeParameter.svelte b/frontend/src/lib/components/NodeParameter.svelte index fb767ed..99d8ef8 100644 --- a/frontend/src/lib/components/NodeParameter.svelte +++ b/frontend/src/lib/components/NodeParameter.svelte @@ -1,17 +1,16 @@ -
+
@@ -69,11 +71,16 @@ {#if node.tmp?.type?.inputs?.[id].internal !== true}
+
{/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; } diff --git a/frontend/src/lib/components/background/Background.svelte b/frontend/src/lib/components/background/Background.svelte index 684ce88..d6ff2db 100644 --- a/frontend/src/lib/components/background/Background.svelte +++ b/frontend/src/lib/components/background/Background.svelte @@ -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]; } - + diff --git a/frontend/src/lib/components/debug/index.ts b/frontend/src/lib/components/debug/index.ts index b539c9b..6db9502 100644 --- a/frontend/src/lib/components/debug/index.ts +++ b/frontend/src/lib/components/debug/index.ts @@ -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() { diff --git a/frontend/src/lib/components/Edge.svelte b/frontend/src/lib/components/edges/Edge.svelte similarity index 100% rename from frontend/src/lib/components/Edge.svelte rename to frontend/src/lib/components/edges/Edge.svelte diff --git a/frontend/src/lib/components/edges/FloatingEdge.svelte b/frontend/src/lib/components/edges/FloatingEdge.svelte new file mode 100644 index 0000000..427c26d --- /dev/null +++ b/frontend/src/lib/components/edges/FloatingEdge.svelte @@ -0,0 +1,8 @@ + + + diff --git a/frontend/src/lib/components/graph/Graph.svelte b/frontend/src/lib/components/graph/Graph.svelte index e68c94f..b262182 100644 --- a/frontend/src/lib/components/graph/Graph.svelte +++ b/frontend/src/lib/components/graph/Graph.svelte @@ -1,16 +1,16 @@ @@ -201,67 +263,30 @@ on:mousedown={handleMouseDown} /> + + - + - + {#if $status === "idle"} - {#each edgePositions as [x1, y1, x2, y2]} - - {/each} - - {#if $mouseDown && $mouseDown?.node} - {/if} - - -
- {#each $nodes as node} - - {/each} -
- + {:else if $status === "loading"} Loading {:else if $status === "error"} Error {/if} - - diff --git a/frontend/src/lib/components/graph/GraphView.svelte b/frontend/src/lib/components/graph/GraphView.svelte new file mode 100644 index 0000000..cdaf228 --- /dev/null +++ b/frontend/src/lib/components/graph/GraphView.svelte @@ -0,0 +1,72 @@ + + +{#each $edges as edge} + {@const pos = getEdgePosition(edge)} + {@const [x1, y1, x2, y2] = pos} + +{/each} + + +
+ {#each $nodes.values() as node} + + {/each} +
+ + + diff --git a/frontend/src/lib/components/graph/context.ts b/frontend/src/lib/components/graph/context.ts index 7eeff01..20e5666 100644 --- a/frontend/src/lib/components/graph/context.ts +++ b/frontend/src/lib/components/graph/context.ts @@ -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"); } diff --git a/frontend/src/lib/components/graph/state.ts b/frontend/src/lib/components/graph/state.ts deleted file mode 100644 index f9ebd9b..0000000 --- a/frontend/src/lib/components/graph/state.ts +++ /dev/null @@ -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 = writable(-1); - dimensions: Writable<[number, number]> = writable([100, 100]); - mouse: Writable<[number, number]> = writable([0, 0]); - mouseDown: Writable)> = 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 = 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) | 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); - - } - -} diff --git a/frontend/src/lib/graph-manager.ts b/frontend/src/lib/graph-manager.ts index 01f1c45..b75fe7a 100644 --- a/frontend/src/lib/graph-manager.ts +++ b/frontend/src/lib/graph-manager.ts @@ -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 = writable([]); + private _nodes: Map = new Map(); + nodes: Writable> = writable(new Map()); private _edges: Edge[] = []; edges: Writable = 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; }) diff --git a/frontend/src/lib/types/index.ts b/frontend/src/lib/types/index.ts index 639ac5b..70d9f2f 100644 --- a/frontend/src/lib/types/index.ts +++ b/frontend/src/lib/types/index.ts @@ -6,6 +6,8 @@ export type Node = { type: string; props?: Record, 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; } diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index f3a1c92..bf10d1d 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -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); + // } + // });
- - + +
diff --git a/frontend/src/routes/app.css b/frontend/src/routes/app.css index 6e439ab..0cb0dc3 100644 --- a/frontend/src/routes/app.css +++ b/frontend/src/routes/app.css @@ -23,3 +23,7 @@ :root { font-family: 'Fira Code', monospace; } + +body { + overflow: hidden; +}