From af24b5cffe31d4cba2af81cc5f81f5a96810207c Mon Sep 17 00:00:00 2001 From: Max Richter Date: Mon, 11 Mar 2024 22:00:16 +0100 Subject: [PATCH] feat: allow reconnecting of edges --- frontend/src/lib/components/Edge.svelte | 1 - frontend/src/lib/components/Node.svelte | 6 - frontend/src/lib/components/NodeHeader.svelte | 32 ++---- .../src/lib/components/NodeParameter.svelte | 25 +++-- .../src/lib/components/graph/Graph.svelte | 86 ++++++++++----- .../graph/{graph-state.ts => state.ts} | 57 ++++++++-- frontend/src/lib/graph-manager.ts | 104 ++++++++++++++---- frontend/src/lib/helpers.ts | 20 ++++ frontend/src/lib/types/index.ts | 11 +- frontend/src/lib/types/inputs.ts | 6 +- frontend/src/routes/+page.svelte | 2 +- 11 files changed, 245 insertions(+), 105 deletions(-) rename frontend/src/lib/components/graph/{graph-state.ts => state.ts} (58%) diff --git a/frontend/src/lib/components/Edge.svelte b/frontend/src/lib/components/Edge.svelte index 6d016bc..f6a713e 100644 --- a/frontend/src/lib/components/Edge.svelte +++ b/frontend/src/lib/components/Edge.svelte @@ -1,7 +1,6 @@ @@ -180,15 +216,15 @@ /> {#if $status === "idle"} - {#each edges as edge} + {#each edgePositions as [x1, y1, x2, y2]} {/each} @@ -203,9 +239,9 @@ tabindex="0" class="wrapper" class:zoom-small={$cameraPosition[2] < 10} - style={`--cz: ${$cameraPosition[2]}`} + style={`--cz: ${$cameraPosition[2]}; ${$mouseDown ? `--node-hovered-${$mouseDown.isInput ? "out" : "in"}-${$mouseDown.type}: red;` : ""}`} > - {#each graph.nodes as node} + {#each $nodes as node} {/each} diff --git a/frontend/src/lib/components/graph/graph-state.ts b/frontend/src/lib/components/graph/state.ts similarity index 58% rename from frontend/src/lib/components/graph/graph-state.ts rename to frontend/src/lib/components/graph/state.ts index 9fc6d9d..f9ebd9b 100644 --- a/frontend/src/lib/components/graph/graph-state.ts +++ b/frontend/src/lib/components/graph/state.ts @@ -8,6 +8,7 @@ type Socket = { node: Node; index: number; isInput: boolean; + type: string; position: [number, number]; } @@ -16,7 +17,7 @@ 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); + mouseDown: Writable)> = writable(false); cameraPosition: Writable<[number, number, number]> = writable([0, 1, 0]); cameraBounds = derived([this.cameraPosition, this.dimensions], ([_cameraPosition, [width, height]]) => { return [ @@ -56,39 +57,73 @@ export class GraphState { ]); } - setMouseDown(opts: { x: number, y: number, node?: Node, socketIndex?: number, isInput?: boolean } | false) { + 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; } - const { x, y, node, socketIndex, isInput } = opts; - this.mouseDown.set({ x, y, node, socketIndex, isInput }); - if (node && socketIndex !== undefined) { + let { x, y, node, index, isInput, type } = opts; + + if (node && index !== undefined && isInput !== undefined) { debug.clear(); - this.possibleSockets = this.graph.getPossibleSockets(node, socketIndex, isInput).map(([node, index]) => { + // 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) { - // debug.debugPosition(new Vector3(node.position.x + 5, 0, node.position.y + 0.625 + 2.5 * index)); + 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 { - // debug.debugPosition(new Vector3(node.position.x, 0, node.position.y + 2.5 + 2.5 * index)); 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 e5fe03f..01f1c45 100644 --- a/frontend/src/lib/graph-manager.ts +++ b/frontend/src/lib/graph-manager.ts @@ -1,4 +1,4 @@ -import { writable, type Writable } from "svelte/store"; +import { get, writable, type Writable } from "svelte/store"; import type { Graph, NodeRegistry as INodeRegistry, NodeType, Node, Edge } from "./types"; const nodeTypes: NodeType[] = [ @@ -12,11 +12,19 @@ const nodeTypes: NodeType[] = [ { id: "math", inputs: { + "type": { type: "select", options: ["add", "subtract", "multiply", "divide"], internal: true }, "a": { type: "float" }, "b": { type: "float" }, }, outputs: ["float"], }, + { + id: "output", + inputs: { + "input": { type: "float" }, + }, + outputs: [], + } ] export class NodeRegistry implements INodeRegistry { @@ -30,10 +38,18 @@ export class GraphManager { status: Writable<"loading" | "idle" | "error"> = writable("loading"); - nodes: Node[] = []; - edges: Edge[] = []; + private _nodes: Node[] = []; + nodes: Writable = writable([]); + private _edges: Edge[] = []; + edges: Writable = writable([]); private constructor(private graph: Graph, private nodeRegistry: NodeRegistry = new NodeRegistry()) { + this.nodes.subscribe((nodes) => { + this._nodes = nodes; + }); + this.edges.subscribe((edges) => { + this._edges = edges; + }); } async load() { @@ -47,15 +63,27 @@ export class GraphManager { this.status.set("error"); return; } + node.tmp = node.tmp || {}; + node.tmp.type = nodeType; } - this.nodes = this.graph.nodes; - this.edges = this.graph.edges; + 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; + return [from, edge[1], to, edge[3]] as const; + }) + .filter(Boolean) as unknown as [Node, number, Node, string][] + ); + + this.status.set("idle"); } + getNode(id: number) { - return this.nodes.find((node) => node.id === id); + return this._nodes.find((node) => node.id === id); } getPossibleSockets(node: Node, socketIndex: number, isInput: boolean): [Node, number][] { @@ -63,13 +91,11 @@ export class GraphManager { const nodeType = this.getNodeType(node.type); if (!nodeType) return []; - const nodes = this.nodes.filter(n => n.id !== node.id); - + const nodes = this._nodes.filter(n => n.id !== node.id); const sockets: [Node, number][] = [] if (isInput) { - const ownType = Object.values(nodeType?.inputs || {})[socketIndex].type; for (const node of nodes) { @@ -108,17 +134,39 @@ export class GraphManager { return this.nodeRegistry.getNode(id)!; } - getEdges() { - return this.edges - .map((edge) => { - const from = this.nodes.find((node) => node.id === edge.from); - const to = this.nodes.find((node) => node.id === edge.to); - if (!from || !to) return; - return [from, edge.fromSocket, to, edge.toSocket] as const; - }) - .filter(Boolean) as unknown as [Node, number, Node, number][]; + removeEdge(edge: Edge) { + const id0 = edge[0].id; + const sid0 = edge[1]; + const id2 = edge[2].id; + const sid2 = edge[3]; + this.edges.update((edges) => { + return edges.filter((e) => e[0].id !== id0 || e[1] !== sid0 || e[2].id !== id2 || e[3] !== sid2); + }); } + getEdgesToNode(node: Node) { + 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); + if (!from || !to) return; + return [from, edge[1], to, edge[3]] as const; + }) + .filter(Boolean) as unknown as [Node, number, Node, string][]; + } + + getEdgesFromNode(node: Node) { + return this._edges + .filter((edge) => edge[0] === node.id) + .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; + return [from, edge[1], to, edge[3]] as const; + }) + .filter(Boolean) as unknown as [Node, number, Node, string][]; + } static createEmptyGraph({ width = 20, height = 20 } = {}): GraphManager { @@ -146,14 +194,22 @@ export class GraphManager { type: i == 0 ? "input/float" : "math", }); - graph.edges.push({ - from: i, - fromSocket: 0, - to: (i + 1), - toSocket: 0, - }); + graph.edges.push([i, 0, i + 1, i === amount - 1 ? "input" : "a",]); } + graph.nodes.push({ + id: amount, + tmp: { + visible: false, + }, + position: { + x: width * 7.5, + y: (height - 1) * 10, + }, + type: "output", + props: {}, + }); + return new GraphManager(graph); } diff --git a/frontend/src/lib/helpers.ts b/frontend/src/lib/helpers.ts index 87c43f1..d3d34b0 100644 --- a/frontend/src/lib/helpers.ts +++ b/frontend/src/lib/helpers.ts @@ -1,3 +1,23 @@ export function snapToGrid(value: number, gridSize: number = 10) { return Math.round(value / gridSize) * gridSize; +} + +export function lerp(a: number, b: number, t: number) { + return a + (b - a) * t; +} + +export function animate(duration: number, callback: (progress: number) => void | false) { + const start = performance.now(); + const loop = (time: number) => { + const progress = (time - start) / duration; + if (progress < 1) { + const res = callback(progress); + if (res !== false) { + requestAnimationFrame(loop); + } + } else { + callback(1); + } } + requestAnimationFrame(loop); +} diff --git a/frontend/src/lib/types/index.ts b/frontend/src/lib/types/index.ts index 5cee08e..639ac5b 100644 --- a/frontend/src/lib/types/index.ts +++ b/frontend/src/lib/types/index.ts @@ -1,3 +1,4 @@ +import type { NodeInput } from "./inputs"; export type { NodeInput } from "./inputs"; export type Node = { @@ -5,6 +6,7 @@ export type Node = { type: string; props?: Record, tmp?: { + type?: NodeType; downX?: number; downY?: number; visible?: boolean; @@ -34,12 +36,7 @@ export interface NodeRegistry { } -export type Edge = { - from: number; - fromSocket: number; - to: number; - toSocket: number; -} +export type Edge = [Node, number, Node, string]; export type Graph = { meta?: { @@ -47,5 +44,5 @@ export type Graph = { lastModified?: string; }, nodes: Node[]; - edges: Edge[]; + edges: [number, number, number, string][]; } diff --git a/frontend/src/lib/types/inputs.ts b/frontend/src/lib/types/inputs.ts index 8c9c4bc..31465ab 100644 --- a/frontend/src/lib/types/inputs.ts +++ b/frontend/src/lib/types/inputs.ts @@ -18,4 +18,8 @@ type NodeInputSelect = { options: string[]; } -export type NodeInput = NodeInputFloat | NodeInputInteger | NodeInputSelect; +type DefaultOptions = { + internal?: boolean; +} + +export type NodeInput = (NodeInputFloat | NodeInputInteger | NodeInputSelect) & DefaultOptions; diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index c68bd1c..f3a1c92 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -7,7 +7,7 @@ import { GraphManager } from "$lib/graph-manager"; import Graph from "$lib/components/graph/Graph.svelte"; - const graph = GraphManager.createEmptyGraph({ width: 3, height: 3 }); + const graph = GraphManager.createEmptyGraph({ width: 12, height: 12 }); graph.load(); onMount(async () => {