feat: allow reconnecting of edges
This commit is contained in:
parent
e473284797
commit
af24b5cffe
@ -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 });
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
|
@ -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 {
|
||||||
|
@ -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 || 0,
|
||||||
$hoveredSocket.index,
|
$mouseDown.node,
|
||||||
];
|
Object.keys($mouseDown?.node?.tmp?.type?.inputs || {})[
|
||||||
edges.push(newEdge);
|
$mouseDown?.index || 0
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$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>
|
||||||
|
@ -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) {
|
||||||
|
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) {
|
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 {
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
@ -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][];
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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 () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user