feat: allow reconnecting of edges

This commit is contained in:
max_richter 2024-03-11 22:00:16 +01:00
parent e473284797
commit af24b5cffe
11 changed files with 245 additions and 105 deletions

View File

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { T, extend } from "@threlte/core"; import { T, extend } from "@threlte/core";
import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras"; import { MeshLineGeometry, MeshLineMaterial } from "@threlte/extras";
import { onMount } from "svelte";
import { CubicBezierCurve, Mesh, Vector2, Vector3 } from "three"; import { CubicBezierCurve, Mesh, Vector2, Vector3 } from "three";
extend({ MeshLineGeometry, MeshLineMaterial }); extend({ MeshLineGeometry, MeshLineMaterial });

View File

@ -50,12 +50,6 @@
font-weight: 300; font-weight: 300;
font-size: 0.5em; font-size: 0.5em;
display: none; display: none;
transition: transform 0.2s ease;
}
.node.is-moving {
z-index: 100;
transition: none !important;
} }
.node.in-view { .node.in-view {

View File

@ -10,7 +10,7 @@
function createPath({ depth = 8, height = 20, y = 50 } = {}) { function createPath({ depth = 8, height = 20, y = 50 } = {}) {
let corner = 10; let corner = 10;
let right_bump = true; let right_bump = node.tmp.type.outputs.length > 0;
return `M0,100 return `M0,100
${ ${
@ -41,7 +41,8 @@
x: node.position.x + 5, x: node.position.x + 5,
y: node.position.y + 0.625, y: node.position.y + 0.625,
node, node,
socketIndex: 0, type: node.tmp?.type?.outputs?.[0] || "",
index: 0,
isInput: false, isInput: false,
}); });
} }
@ -56,6 +57,7 @@
role="button" role="button"
tabindex="0" tabindex="0"
on:mousedown={handleMouseDown} on:mousedown={handleMouseDown}
style={`background: var(--node-hovered-out-${node.tmp?.type?.outputs?.[0]}`}
/> />
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -64,18 +66,10 @@
height="100" height="100"
preserveAspectRatio="none" preserveAspectRatio="none"
style={` style={`
--path: path("${createPath({ depth: 5, height: 27, y: 46 })}"); --path: path("${createPath({ depth: 5, height: 27, y: 50 })}");
--hover-path: path("${createPath({ depth: 6, height: 33, y: 46 })}"); --hover-path: path("${createPath({ depth: 6, height: 33, y: 50 })}");
`} `}
> >
<!-- <ellipse -->
<!-- cx="100" -->
<!-- cy="48" -->
<!-- rx="5.4" -->
<!-- ry="20" -->
<!-- fill="rgba(255,0,0,0.3)" -->
<!-- id="one" -->
<!-- /> -->
<path <path
vector-effect="non-scaling-stroke" vector-effect="non-scaling-stroke"
fill="none" fill="none"
@ -91,18 +85,16 @@
width: 100%; width: 100%;
height: 12.5px; height: 12.5px;
} }
.wrapper > * {
/* pointer-events: none; */
}
.click-target { .click-target {
position: absolute; position: absolute;
right: -2.5px; right: -2.5px;
top: 4px; top: 3.8px;
height: 5px; height: 5px;
width: 5px; width: 5px;
z-index: 100; z-index: 100;
border-radius: 50%; border-radius: 50%;
opacity: 0.1;
} }
.click-target:hover + svg path { .click-target:hover + svg path {
@ -116,18 +108,14 @@
z-index: -1; z-index: -1;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
height: calc(100% + 1px); height: 100%;
overflow: visible; overflow: visible;
} }
ellipse {
z-index: 99;
}
svg path { svg path {
stroke-width: 0.2px; stroke-width: 0.2px;
transition: 0.2s; transition: 0.2s;
fill: #060606; fill: #131313;
stroke: #777; stroke: #777;
stroke-width: 0.1; stroke-width: 0.1;
d: var(--path); d: var(--path);

View File

@ -14,10 +14,10 @@
const state = getGraphState(); const state = getGraphState();
function createPath({ depth = 8, height = 20, y = 50 } = {}) { function createPath({ depth = 8, height = 20, y = 50 } = {}) {
let corner = isLast ? 2 : 0; let corner = isLast ? 5 : 0;
let right_bump = false; let right_bump = false;
let left_bump = true; let left_bump = node.tmp?.type?.inputs?.[id].internal !== true;
return `M0,0 return `M0,0
H100 H100
@ -53,7 +53,8 @@
x: node.position.x, x: node.position.x,
y: node.position.y + 2.5 + index * 2.5, y: node.position.y + 2.5 + index * 2.5,
node, node,
socketIndex: index, index: index,
type: node?.tmp?.type?.inputs?.[id].type || "",
isInput: true, isInput: true,
}); });
} }
@ -66,7 +67,15 @@
<div class="input">input</div> <div class="input">input</div>
</div> </div>
<div class="click-target" on:mousedown={handleMouseDown} /> {#if node.tmp?.type?.inputs?.[id].internal !== true}
<div
class="click-target"
on:mousedown={handleMouseDown}
role="button"
tabindex="0"
style={`background: var(--node-hovered-in-${node.tmp?.type?.inputs?.[id].type}`}
/>
{/if}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -75,8 +84,8 @@
height="100" height="100"
preserveAspectRatio="none" preserveAspectRatio="none"
style={` style={`
--path: path("${createPath({ depth: 5, height: 15, y: 48.2 })}"); --path: path("${createPath({ depth: 5, height: 15, y: 50 })}");
--hover-path: path("${createPath({ depth: 8, height: 24, y: 48.2 })}"); --hover-path: path("${createPath({ depth: 8, height: 24, y: 50 })}");
`} `}
> >
<path vector-effect="non-scaling-stroke"></path> <path vector-effect="non-scaling-stroke"></path>
@ -88,6 +97,7 @@
position: relative; position: relative;
width: 100%; width: 100%;
height: 25px; height: 25px;
transform: translateY(-0.5px);
} }
.click-target { .click-target {
@ -95,8 +105,9 @@
width: 6px; width: 6px;
height: 6px; height: 6px;
border-radius: 50%; border-radius: 50%;
top: 10px; top: 9.5px;
left: -3px; left: -3px;
opacity: 0.1;
} }
.content { .content {

View File

@ -2,19 +2,21 @@
import Edge from "../Edge.svelte"; import Edge from "../Edge.svelte";
import { HTML } from "@threlte/extras"; import { HTML } from "@threlte/extras";
import Node from "../Node.svelte"; import Node from "../Node.svelte";
import { 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 "./graph-state"; import { GraphState } from "./state";
import Camera from "../Camera.svelte"; import Camera from "../Camera.svelte";
import { event } from "@tauri-apps/api"; import type { Node as NodeType } from "$lib/types";
export let graph: GraphManager; export let graph: GraphManager;
setContext("graphManager", graph); setContext("graphManager", graph);
const status = graph.status; const status = graph.status;
const nodes = graph.nodes;
const edges = graph.edges;
const state = new GraphState(graph); const state = new GraphState(graph);
setContext("graphState", state); setContext("graphState", state);
@ -31,7 +33,15 @@
const minZoom = 4; const minZoom = 4;
const maxZoom = 150; const maxZoom = 150;
let edges = graph?.getEdges() || []; $: 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,
];
});
function handleMouseMove(event: MouseEvent) { function handleMouseMove(event: MouseEvent) {
state.setMouseFromEvent(event); state.setMouseFromEvent(event);
@ -56,6 +66,8 @@
if (_socket && smallestDist < 0.3) { if (_socket && smallestDist < 0.3) {
state.setMouse(_socket.position[0], _socket.position[1]); state.setMouse(_socket.position[0], _socket.position[1]);
state.hoveredSocket.set(_socket); state.hoveredSocket.set(_socket);
} else {
state.hoveredSocket.set(null);
} }
} }
@ -83,8 +95,9 @@
node.position.x = newX; node.position.x = newX;
node.position.y = newY; node.position.y = newY;
node.position = node.position; node.position = node.position;
edges = [...edges];
graph.nodes = [...graph.nodes]; nodes.set($nodes);
edges.set($edges);
} }
function handleMouseDown(ev: MouseEvent) { function handleMouseDown(ev: MouseEvent) {
@ -102,7 +115,7 @@
if ($activeNodeId < 0) return; if ($activeNodeId < 0) return;
$mouseDown = { x: ev.clientX, y: ev.clientY }; $mouseDown = { x: ev.clientX, y: ev.clientY };
const node = graph.nodes.find((node) => node.id === $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;
@ -139,23 +152,46 @@
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.isMoving = false; node.tmp.isMoving = false;
const snapLevel = getSnapLevel(); const snapLevel = getSnapLevel();
node.position.x = snapToGrid(node.position.x, 5 / snapLevel); const fx = snapToGrid(node.position.x, 5 / snapLevel);
node.position.y = snapToGrid(node.position.y, 5 / snapLevel); const fy = snapToGrid(node.position.y, 5 / snapLevel);
animate(500, (a: number) => {
node.position.x = lerp(node.position.x, fx, a);
node.position.y = lerp(node.position.y, fy, a);
if (node?.tmp?.isMoving) {
return false;
}
nodes.set($nodes);
edges.set($edges);
});
nodes.set($nodes);
edges.set($edges);
} else if ($hoveredSocket && $mouseDown && $mouseDown?.node) { } else if ($hoveredSocket && $mouseDown && $mouseDown?.node) {
const newEdge = [ if ($hoveredSocket.isInput) {
$mouseDown.node, const newEdge: [NodeType, number, NodeType, string] = [
$mouseDown.socketIndex,
$hoveredSocket.node, $hoveredSocket.node,
$hoveredSocket.index, $hoveredSocket.index || 0,
$mouseDown.node,
Object.keys($mouseDown?.node?.tmp?.type?.inputs || {})[
$mouseDown?.index || 0
],
]; ];
edges.push(newEdge); $edges = [...$edges, newEdge];
} 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];
}
} }
$mouseDown = false; $mouseDown = false;
$hoveredSocket = null; $hoveredSocket = null;
$activeNodeId = -1; $activeNodeId = -1;
graph.nodes = [...graph.nodes];
edges = [...edges];
} }
</script> </script>
@ -180,15 +216,15 @@
/> />
{#if $status === "idle"} {#if $status === "idle"}
{#each edges as edge} {#each edgePositions as [x1, y1, x2, y2]}
<Edge <Edge
from={{ from={{
x: edge[0].position.x + 5, x: x1,
y: edge[0].position.y + 0.625 + edge[1] * 2.5, y: y1,
}} }}
to={{ to={{
x: edge[2].position.x, x: x2,
y: edge[2].position.y + 2.5 + edge[3] * 2.5, y: y2,
}} }}
/> />
{/each} {/each}
@ -203,9 +239,9 @@
tabindex="0" tabindex="0"
class="wrapper" class="wrapper"
class:zoom-small={$cameraPosition[2] < 10} 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}
<Node {node} inView={$cameraPosition && isNodeInView(node)} /> <Node {node} inView={$cameraPosition && isNodeInView(node)} />
{/each} {/each}
</div> </div>

View File

@ -8,6 +8,7 @@ type Socket = {
node: Node; node: Node;
index: number; index: number;
isInput: boolean; isInput: boolean;
type: string;
position: [number, number]; position: [number, number];
} }
@ -16,7 +17,7 @@ export class GraphState {
activeNodeId: Writable<number> = writable(-1); activeNodeId: Writable<number> = writable(-1);
dimensions: Writable<[number, number]> = writable([100, 100]); dimensions: Writable<[number, number]> = writable([100, 100]);
mouse: Writable<[number, number]> = writable([0, 0]); mouse: Writable<[number, number]> = writable([0, 0]);
mouseDown: Writable<false | { x: number, y: number, node?: Node, socketIndex?: number, isInput?: boolean }> = writable(false); mouseDown: Writable<false | ({ x: number, y: number } & Omit<Socket, "position">)> = writable(false);
cameraPosition: Writable<[number, number, number]> = writable([0, 1, 0]); cameraPosition: Writable<[number, number, number]> = writable([0, 1, 0]);
cameraBounds = derived([this.cameraPosition, this.dimensions], ([_cameraPosition, [width, height]]) => { cameraBounds = derived([this.cameraPosition, this.dimensions], ([_cameraPosition, [width, height]]) => {
return [ 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<Socket, "position">) | false) {
if (!opts) { if (!opts) {
this.mouseDown.set(false); this.mouseDown.set(false);
return; 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(); debug.clear();
this.possibleSockets = this.graph.getPossibleSockets(node, socketIndex, isInput).map(([node, index]) => { // remove existing edge
if (isInput) { if (isInput) {
// debug.debugPosition(new Vector3(node.position.x + 5, 0, node.position.y + 0.625 + 2.5 * index)); 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 { return {
node, node,
index, index,
isInput,
type: node.tmp?.type?.inputs?.[key].type || "",
position: [node.position.x + 5, node.position.y + 0.625 + 2.5 * index] position: [node.position.x + 5, node.position.y + 0.625 + 2.5 * index]
} }
} else { } else {
// debug.debugPosition(new Vector3(node.position.x, 0, node.position.y + 2.5 + 2.5 * index));
return { return {
node, node,
index, index,
isInput,
type: node.tmp?.type?.outputs?.[index] || "",
position: [node.position.x, node.position.y + 2.5 + 2.5 * index] position: [node.position.x, node.position.y + 2.5 + 2.5 * index]
} }
} }
}); });
} }
console.log("possibleSockets", this.possibleSockets);
} }
} }

View File

@ -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"; import type { Graph, NodeRegistry as INodeRegistry, NodeType, Node, Edge } from "./types";
const nodeTypes: NodeType[] = [ const nodeTypes: NodeType[] = [
@ -12,11 +12,19 @@ const nodeTypes: NodeType[] = [
{ {
id: "math", id: "math",
inputs: { inputs: {
"type": { type: "select", options: ["add", "subtract", "multiply", "divide"], internal: true },
"a": { type: "float" }, "a": { type: "float" },
"b": { type: "float" }, "b": { type: "float" },
}, },
outputs: ["float"], outputs: ["float"],
}, },
{
id: "output",
inputs: {
"input": { type: "float" },
},
outputs: [],
}
] ]
export class NodeRegistry implements INodeRegistry { export class NodeRegistry implements INodeRegistry {
@ -30,10 +38,18 @@ export class GraphManager {
status: Writable<"loading" | "idle" | "error"> = writable("loading"); status: Writable<"loading" | "idle" | "error"> = writable("loading");
nodes: Node[] = []; private _nodes: Node[] = [];
edges: Edge[] = []; nodes: Writable<Node[]> = writable([]);
private _edges: Edge[] = [];
edges: Writable<Edge[]> = writable([]);
private constructor(private graph: Graph, private nodeRegistry: NodeRegistry = new NodeRegistry()) { 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() { async load() {
@ -47,15 +63,27 @@ export class GraphManager {
this.status.set("error"); this.status.set("error");
return; return;
} }
node.tmp = node.tmp || {};
node.tmp.type = nodeType;
} }
this.nodes = this.graph.nodes; this.nodes.set(nodes);
this.edges = this.graph.edges; 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"); this.status.set("idle");
} }
getNode(id: number) { 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][] { getPossibleSockets(node: Node, socketIndex: number, isInput: boolean): [Node, number][] {
@ -63,13 +91,11 @@ export class GraphManager {
const nodeType = this.getNodeType(node.type); const nodeType = this.getNodeType(node.type);
if (!nodeType) return []; 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][] = [] const sockets: [Node, number][] = []
if (isInput) { if (isInput) {
const ownType = Object.values(nodeType?.inputs || {})[socketIndex].type; const ownType = Object.values(nodeType?.inputs || {})[socketIndex].type;
for (const node of nodes) { for (const node of nodes) {
@ -108,17 +134,39 @@ export class GraphManager {
return this.nodeRegistry.getNode(id)!; return this.nodeRegistry.getNode(id)!;
} }
getEdges() { removeEdge(edge: Edge) {
return this.edges const id0 = edge[0].id;
.map((edge) => { const sid0 = edge[1];
const from = this.nodes.find((node) => node.id === edge.from); const id2 = edge[2].id;
const to = this.nodes.find((node) => node.id === edge.to); const sid2 = edge[3];
if (!from || !to) return; this.edges.update((edges) => {
return [from, edge.fromSocket, to, edge.toSocket] as const; return edges.filter((e) => e[0].id !== id0 || e[1] !== sid0 || e[2].id !== id2 || e[3] !== sid2);
}) });
.filter(Boolean) as unknown as [Node, number, Node, number][];
} }
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 { static createEmptyGraph({ width = 20, height = 20 } = {}): GraphManager {
@ -146,14 +194,22 @@ export class GraphManager {
type: i == 0 ? "input/float" : "math", type: i == 0 ? "input/float" : "math",
}); });
graph.edges.push({ graph.edges.push([i, 0, i + 1, i === amount - 1 ? "input" : "a",]);
from: i,
fromSocket: 0,
to: (i + 1),
toSocket: 0,
});
} }
graph.nodes.push({
id: amount,
tmp: {
visible: false,
},
position: {
x: width * 7.5,
y: (height - 1) * 10,
},
type: "output",
props: {},
});
return new GraphManager(graph); return new GraphManager(graph);
} }

View File

@ -1,3 +1,23 @@
export function snapToGrid(value: number, gridSize: number = 10) { export function snapToGrid(value: number, gridSize: number = 10) {
return Math.round(value / gridSize) * gridSize; 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);
}

View File

@ -1,3 +1,4 @@
import type { NodeInput } from "./inputs";
export type { NodeInput } from "./inputs"; export type { NodeInput } from "./inputs";
export type Node = { export type Node = {
@ -5,6 +6,7 @@ export type Node = {
type: string; type: string;
props?: Record<string, any>, props?: Record<string, any>,
tmp?: { tmp?: {
type?: NodeType;
downX?: number; downX?: number;
downY?: number; downY?: number;
visible?: boolean; visible?: boolean;
@ -34,12 +36,7 @@ export interface NodeRegistry {
} }
export type Edge = { export type Edge = [Node, number, Node, string];
from: number;
fromSocket: number;
to: number;
toSocket: number;
}
export type Graph = { export type Graph = {
meta?: { meta?: {
@ -47,5 +44,5 @@ export type Graph = {
lastModified?: string; lastModified?: string;
}, },
nodes: Node[]; nodes: Node[];
edges: Edge[]; edges: [number, number, number, string][];
} }

View File

@ -18,4 +18,8 @@ type NodeInputSelect = {
options: string[]; options: string[];
} }
export type NodeInput = NodeInputFloat | NodeInputInteger | NodeInputSelect; type DefaultOptions = {
internal?: boolean;
}
export type NodeInput = (NodeInputFloat | NodeInputInteger | NodeInputSelect) & DefaultOptions;

View File

@ -7,7 +7,7 @@
import { GraphManager } from "$lib/graph-manager"; import { GraphManager } from "$lib/graph-manager";
import Graph from "$lib/components/graph/Graph.svelte"; 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(); graph.load();
onMount(async () => { onMount(async () => {