feat: implement selection

This commit is contained in:
max_richter 2024-03-13 14:30:30 +01:00
parent 9241700ada
commit 305341fdf0
16 changed files with 521 additions and 202 deletions

View File

@ -28,6 +28,7 @@
"@sveltejs/vite-plugin-svelte": "^3.0.1", "@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tauri-apps/cli": "2.0.0-beta.3", "@tauri-apps/cli": "2.0.0-beta.3",
"@tsconfig/svelte": "^5.0.2", "@tsconfig/svelte": "^5.0.2",
"@zerodevx/svelte-json-view": "^1.0.9",
"histoire": "^0.17.9", "histoire": "^0.17.9",
"internal-ip": "^7.0.0", "internal-ip": "^7.0.0",
"svelte": "^4.2.8", "svelte": "^4.2.8",

View File

@ -2,12 +2,11 @@
import type { Node } from "$lib/types"; import type { Node } from "$lib/types";
import NodeHeader from "./NodeHeader.svelte"; import NodeHeader from "./NodeHeader.svelte";
import NodeParameter from "./NodeParameter.svelte"; import NodeParameter from "./NodeParameter.svelte";
import { activeNodeId, selectedNodes } from "./graph/stores";
export let node: Node; export let node: Node;
export let inView = true; export let inView = true;
export let possibleSocketIds: null | Set<string> = null;
const type = node?.tmp?.type; const type = node?.tmp?.type;
const parameters = Object.entries(type?.inputs || {}); const parameters = Object.entries(type?.inputs || {});
@ -15,17 +14,17 @@
<div <div
class="node" class="node"
class:active={$activeNodeId === node.id}
class:selected={!!$selectedNodes?.has(node.id)}
class:in-view={inView} class:in-view={inView}
data-node-id={node.id} data-node-id={node.id}
style={`--nx:${node.position.x * 10}px; bind:this={node.tmp.ref}
--ny: ${node.position.y * 10}px`}
> >
<NodeHeader {node} /> <NodeHeader {node} />
{#each parameters as [key, value], i} {#each parameters as [key, value], i}
<NodeParameter <NodeParameter
{node} {node}
{possibleSocketIds}
id={key} id={key}
index={i} index={i}
input={value} input={value}
@ -47,6 +46,18 @@
font-weight: 300; font-weight: 300;
font-size: 0.5em; font-size: 0.5em;
display: none; display: none;
--stroke: #777;
--stroke-width: 0.1px;
}
.node.active {
--stroke: white;
--stroke-width: 0.3px;
}
.node.selected {
--stroke: #f2be90;
--stroke-width: 0.2px;
} }
.node.in-view { .node.in-view {

View File

@ -1,41 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { Node } from "$lib/types"; import { createNodePath } from "$lib/helpers";
import type { Node, Socket } from "$lib/types";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { getGraphManager, getGraphState } from "./graph/context";
export let node: Node; export let node: Node;
const graph = getGraphManager(); const setDownSocket = getContext<(socket: Socket) => void>("setDownSocket");
const state = getGraphState();
function createPath({ depth = 8, height = 20, y = 50 } = {}) {
let corner = 10;
let right_bump = node.tmp.type.outputs.length > 0;
return `M0,100
${
corner
? ` V${corner}
Q0,0 ${corner / 4},0
H${100 - corner / 4}
Q100,0 100,${corner}
`
: ` V0
H100
`
}
V${y - height / 2}
${
right_bump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100`
}
V100
Z`.replace(/\s+/g, " ");
}
const setDownSocket = getContext("setDownSocket");
function handleMouseDown(event: MouseEvent) { function handleMouseDown(event: MouseEvent) {
event.stopPropagation(); event.stopPropagation();
@ -46,6 +16,35 @@
position: [node.position.x + 5, node.position.y + 0.625], position: [node.position.x + 5, node.position.y + 0.625],
}); });
} }
const cornerTop = 10;
const rightBump = !!node?.tmp?.type?.outputs?.length;
const aspectRatio = 0.25;
const path = createNodePath({
depth: 4.5,
height: 24,
y: 50,
cornerTop,
rightBump,
aspectRatio,
});
const pathDisabled = createNodePath({
depth: 0,
height: 15,
y: 50,
cornerTop,
rightBump,
aspectRatio,
});
const pathHover = createNodePath({
depth: 6,
height: 30,
y: 50,
cornerTop,
rightBump,
aspectRatio,
});
</script> </script>
<div class="wrapper" data-node-id={node.id}> <div class="wrapper" data-node-id={node.id}>
@ -66,8 +65,8 @@
height="100" height="100"
preserveAspectRatio="none" preserveAspectRatio="none"
style={` style={`
--path: path("${createPath({ depth: 5, height: 27, y: 50 })}"); --path: path("${path}");
--hover-path: path("${createPath({ depth: 6, height: 33, y: 50 })}"); --hover-path: path("${pathHover}");
`} `}
> >
<path <path
@ -116,8 +115,8 @@
stroke-width: 0.2px; stroke-width: 0.2px;
transition: 0.2s; transition: 0.2s;
fill: #131313; fill: #131313;
stroke: #777; stroke: var(--stroke);
stroke-width: 0.1; stroke-width: var(--stroke-width);
d: var(--path); d: var(--path);
} }

View File

@ -1,51 +1,17 @@
<script lang="ts"> <script lang="ts">
import type { NodeInput } from "$lib/types"; import type { NodeInput, Socket } from "$lib/types";
import type { Node } from "$lib/types"; import type { Node } from "$lib/types";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { createNodePath } from "$lib/helpers";
import { possibleSocketIds } from "./graph/stores";
export let node: Node; export let node: Node;
export let input: NodeInput; export let input: NodeInput;
export let id: string; export let id: string;
export let index: number; export let index: number;
export let possibleSocketIds: null | Set<string> = null;
export let isLast = false; export let isLast = false;
function createPath({ depth = 8, height = 20, y = 50 } = {}) { const setDownSocket = getContext<(socket: Socket) => void>("setDownSocket");
let corner = isLast ? 5 : 0;
let right_bump = false;
let left_bump = node.tmp?.type?.inputs?.[id].internal !== true;
return `M0,0
H100
V${y - height / 2}
${
right_bump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100`
}
${
corner
? ` V${100 - corner}
Q100,100 ${100 - corner / 2},100
H${corner / 2}
Q0,100 0,${100 - corner}
`
: ` V100
H0
`
}
${
left_bump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
: ` H0`
}
Z`.replace(/\s+/g, " ");
}
const setDownSocket = getContext("setDownSocket");
function handleMouseDown(ev: MouseEvent) { function handleMouseDown(ev: MouseEvent) {
ev.preventDefault(); ev.preventDefault();
@ -56,12 +22,41 @@
position: [node.position.x, node.position.y + 2.5 + index * 2.5], position: [node.position.x, node.position.y + 2.5 + index * 2.5],
}); });
} }
const leftBump = node.tmp?.type?.inputs?.[id].internal !== true;
const cornerBottom = isLast ? 5 : 0;
const aspectRatio = 0.5;
const path = createNodePath({
depth: 4,
height: 12,
y: 50,
cornerBottom,
leftBump,
aspectRatio,
});
const pathDisabled = createNodePath({
depth: 0,
height: 15,
y: 50,
cornerBottom,
leftBump,
aspectRatio,
});
const pathHover = createNodePath({
depth: 8,
height: 24,
y: 50,
cornerBottom,
leftBump,
aspectRatio,
});
</script> </script>
<div <div
class="wrapper" class="wrapper"
class:disabled={possibleSocketIds && class:disabled={$possibleSocketIds &&
!possibleSocketIds.has(`${node.id}-${id}`)} !$possibleSocketIds.has(`${node.id}-${id}`)}
> >
<div class="content"> <div class="content">
<label>{id}</label> <label>{id}</label>
@ -91,9 +86,9 @@
height="100" height="100"
preserveAspectRatio="none" preserveAspectRatio="none"
style={` style={`
--path: path("${createPath({ depth: 5, height: 15, y: 50 })}"); --path: path("${path}");
--hover-path-disabled: path("${createPath({ depth: 0, height: 15, y: 50 })}"); --hover-path: path("${pathHover}");
--hover-path: path("${createPath({ depth: 8, height: 24, y: 50 })}"); --hover-path-disabled: path("${pathDisabled}");
`} `}
> >
<path vector-effect="non-scaling-stroke"></path> <path vector-effect="non-scaling-stroke"></path>
@ -111,6 +106,8 @@
.target { .target {
position: absolute; position: absolute;
border-radius: 50%; border-radius: 50%;
/* background: red; */
/* opacity: 0.1; */
} }
.small.target { .small.target {
@ -126,6 +123,11 @@
top: 5px; top: 5px;
left: -7.5px; left: -7.5px;
cursor: unset; cursor: unset;
pointer-events: none;
}
:global(.hovering-sockets) .large.target {
pointer-events: all;
} }
.content { .content {
@ -169,19 +171,24 @@
} }
svg path { svg path {
stroke-width: 0.2px;
transition: 0.2s; transition: 0.2s;
fill: #060606; fill: #060606;
stroke: #777; stroke: var(--stroke);
stroke-width: 0.1; stroke-width: var(--stroke-width);
d: var(--path); d: var(--path);
} }
:global(.hovering-sockets) .large:hover ~ svg path { :global(.hovering-sockets) .large:hover ~ svg path {
d: var(--hover-path); d: var(--hover-path);
fill: #131313;
}
:global(.hovering-sockets) .small:hover ~ svg path {
fill: #161616;
} }
.disabled svg path { .disabled svg path {
d: var(--hover-path-disabled) !important; d: var(--hover-path-disabled) !important;
fill: #060606 !important;
} }
</style> </style>

View File

@ -22,3 +22,12 @@
<MeshLineMaterial color="red" linewidth={1} attenuate={false} /> <MeshLineMaterial color="red" linewidth={1} attenuate={false} />
</T.Mesh> </T.Mesh>
{/each} {/each}
<style>
.wrapper {
position: fixed;
top: 10px;
left: 10px;
background: white;
}
</style>

View File

@ -22,7 +22,7 @@
let mesh: Mesh; let mesh: Mesh;
function update(force = false) { export const update = function (force = false) {
if (!force) { if (!force) {
const new_x = from.x + to.x; const new_x = from.x + to.x;
const new_y = from.y + to.y; const new_y = from.y + to.y;
@ -50,7 +50,7 @@
points = curve.getPoints(samples).map((p) => new Vector3(p.x, 0, p.y)); points = curve.getPoints(samples).map((p) => new Vector3(p.x, 0, p.y));
// mesh.setGeometry(points); // mesh.setGeometry(points);
// mesh.needsUpdate = true; // mesh.needsUpdate = true;
} };
update(); update();
$: if (from || to) { $: if (from || to) {

View File

@ -9,8 +9,15 @@
import GraphView from "./GraphView.svelte"; import GraphView from "./GraphView.svelte";
import type { Node as NodeType } from "$lib/types"; import type { Node as NodeType } from "$lib/types";
import FloatingEdge from "../edges/FloatingEdge.svelte"; import FloatingEdge from "../edges/FloatingEdge.svelte";
import * as debug from "../debug";
import type { Socket } from "$lib/types"; import type { Socket } from "$lib/types";
import {
activeNodeId,
activeSocket,
hoveredSocket,
possibleSockets,
possibleSocketIds,
selectedNodes,
} from "./stores";
export let graph: GraphManager; export let graph: GraphManager;
setContext("graphManager", graph); setContext("graphManager", graph);
@ -27,39 +34,68 @@
let width = 100; let width = 100;
let height = 100; let height = 100;
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 = [ $: cameraBounds = [
cameraPosition[0] - width / cameraPosition[2], cameraPosition[0] - width / cameraPosition[2] / 2,
cameraPosition[0] + width / cameraPosition[2], cameraPosition[0] + width / cameraPosition[2] / 2,
cameraPosition[1] - height / cameraPosition[2], cameraPosition[1] - height / cameraPosition[2] / 2,
cameraPosition[1] + height / cameraPosition[2], cameraPosition[1] + height / cameraPosition[2] / 2,
]; ];
export let debug = {};
$: debug = {
activeNodeId: $activeNodeId,
activeSocket: $activeSocket
? `${$activeSocket?.node.id}-${$activeSocket?.index}`
: null,
hoveredSocket: $hoveredSocket
? `${$hoveredSocket?.node.id}-${$hoveredSocket?.index}`
: null,
selectedNodes: [...($selectedNodes?.values() || [])],
};
function updateNodePosition(node: NodeType) {
node.tmp = node.tmp || {};
if (node?.tmp?.ref) {
node.tmp.ref.style.setProperty("--nx", `${node.position.x * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.position.y * 10}px`);
}
}
const nodeHeightCache: Record<string, number> = {};
function getNodeHeight(nodeTypeId: string) {
if (nodeTypeId in nodeHeightCache) {
return nodeHeightCache[nodeTypeId];
}
const node = graph.getNodeType(nodeTypeId);
if (!node?.inputs) {
return 1.25;
}
const height = 1.25 + 2.5 * Object.keys(node.inputs).length;
nodeHeightCache[nodeTypeId] = height;
return height;
}
setContext("isNodeInView", (node: NodeType) => { setContext("isNodeInView", (node: NodeType) => {
const height = getNodeHeight(node.type);
const width = 5;
return ( return (
node.position.x > cameraBounds[0] && // check x-axis
node.position.x > cameraBounds[0] - width &&
node.position.x < cameraBounds[1] && node.position.x < cameraBounds[1] &&
node.position.y > cameraBounds[2] && // check y-axis
node.position.y > cameraBounds[2] - height &&
node.position.y < cameraBounds[3] node.position.y < cameraBounds[3]
); );
}); });
setContext("setDownSocket", (socket: Socket) => { setContext("setDownSocket", (socket: Socket) => {
downSocket = socket; $activeSocket = socket;
let { node, index, position } = socket; let { node, index, position } = socket;
// remove existing edge // remove existing edge
if (typeof index === "string") { if (typeof index === "string") {
const edges = graph.getEdgesToNode(node); const edges = graph.getEdgesToNode(node);
console.log({ edges });
for (const edge of edges) { for (const edge of edges) {
if (edge[3] === index) { if (edge[3] === index) {
node = edge[0]; node = edge[0];
@ -72,14 +108,14 @@
} }
mouseDown = position; mouseDown = position;
downSocket = { $activeSocket = {
node, node,
index, index,
position, position,
}; };
possibleSockets = graph $possibleSockets = graph
.getPossibleSockets(downSocket) .getPossibleSockets($activeSocket)
.map(([node, index]) => { .map(([node, index]) => {
return { return {
node, node,
@ -87,6 +123,9 @@
position: getSocketPosition({ node, index }), position: getSocketPosition({ node, index }),
}; };
}); });
$possibleSocketIds = new Set(
$possibleSockets.map((s) => `${s.node.id}-${s.index}`),
);
}); });
function getSnapLevel() { function getSnapLevel() {
@ -136,10 +175,11 @@
if (!mouseDown) return; if (!mouseDown) return;
if (possibleSockets?.length) { // we are creating a new edge here
if ($possibleSockets?.length) {
let smallestDist = 1000; let smallestDist = 1000;
let _socket; let _socket;
for (const socket of possibleSockets) { for (const socket of $possibleSockets) {
const dist = Math.sqrt( const dist = Math.sqrt(
(socket.position[0] - mousePosition[0]) ** 2 + (socket.position[0] - mousePosition[0]) ** 2 +
(socket.position[1] - mousePosition[1]) ** 2, (socket.position[1] - mousePosition[1]) ** 2,
@ -152,119 +192,236 @@
if (_socket && smallestDist < 0.3) { if (_socket && smallestDist < 0.3) {
mousePosition = _socket.position; mousePosition = _socket.position;
hoveredSocket = _socket; $hoveredSocket = _socket;
} else { } else {
hoveredSocket = null; $hoveredSocket = null;
} }
} }
if (activeNodeId === -1) return; if ($activeNodeId === -1) return;
const node = graph.getNode(activeNodeId); const node = graph.getNode($activeNodeId);
if (!node) return; if (!node || event.buttons !== 1) return;
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
node.tmp.isMoving = true;
let newX = const oldX = node.tmp.downX || 0;
(node?.tmp?.downX || 0) + const oldY = node.tmp.downY || 0;
(event.clientX - mouseDown[0]) / cameraPosition[2];
let newY = let newX = oldX + (event.clientX - mouseDown[0]) / cameraPosition[2];
(node?.tmp?.downY || 0) + let newY = oldY + (event.clientY - mouseDown[1]) / cameraPosition[2];
(event.clientY - mouseDown[1]) / cameraPosition[2];
if (event.ctrlKey) { if (event.ctrlKey) {
const snapLevel = getSnapLevel(); const snapLevel = getSnapLevel();
newX = snapToGrid(newX, 5 / snapLevel); newX = snapToGrid(newX, 5 / snapLevel);
newY = snapToGrid(newY, 5 / snapLevel); newY = snapToGrid(newY, 5 / snapLevel);
} }
if (!node.tmp.isMoving) {
const dist = Math.sqrt((oldX - newX) ** 2 + (oldY - newY) ** 2);
if (dist > 0.2) {
node.tmp.isMoving = true;
}
}
const vecX = oldX - newX;
const vecY = oldY - newY;
if ($selectedNodes?.size) {
for (const nodeId of $selectedNodes) {
const n = graph.getNode(nodeId);
if (!n) continue;
n.position.x = (n?.tmp?.downX || 0) - vecX;
n.position.y = (n?.tmp?.downY || 0) - vecY;
updateNodePosition(n);
}
}
node.position.x = newX; node.position.x = newX;
node.position.y = newY; node.position.y = newY;
node.position = node.position; node.position = node.position;
nodes.set($nodes); updateNodePosition(node);
edges.set($edges);
$edges = $edges;
} }
function handleMouseDown(event: MouseEvent) { function handleMouseDown(event: MouseEvent) {
if (mouseDown) return; if (mouseDown) return;
for (const node of event.composedPath()) {
let _activeNodeId = (node as unknown as HTMLElement)?.getAttribute?.(
"data-node-id",
)!;
if (_activeNodeId) {
activeNodeId = parseInt(_activeNodeId, 10);
break;
}
}
if (activeNodeId < 0) return;
mouseDown = [event.clientX, event.clientY]; mouseDown = [event.clientX, event.clientY];
const node = graph.getNode(activeNodeId);
if (event.target instanceof HTMLElement && event.buttons === 1) {
const nodeElement = event.target.closest(".node");
const _activeNodeId = nodeElement?.getAttribute?.("data-node-id");
if (_activeNodeId) {
const nodeId = parseInt(_activeNodeId, 10);
if ($activeNodeId !== -1) {
// if the selected node is the same as the clicked node
if ($activeNodeId === nodeId) {
//$activeNodeId = -1;
// if the clicked node is different from the selected node and secondary
} else if (event.ctrlKey) {
$selectedNodes = $selectedNodes || new Set();
$selectedNodes.add($activeNodeId);
$selectedNodes.delete(nodeId);
$activeNodeId = nodeId;
// select the node
} else if (event.shiftKey) {
const activeNode = graph.getNode($activeNodeId);
const newNode = graph.getNode(nodeId);
if (activeNode && newNode) {
const edge = graph.getNodesBetween(activeNode, newNode);
if (edge) {
$selectedNodes = new Set(edge.map((n) => n.id));
}
$activeNodeId = nodeId;
}
} else if (!$selectedNodes?.has(nodeId)) {
$activeNodeId = nodeId;
}
} else {
$activeNodeId = nodeId;
}
} else {
$activeNodeId = -1;
$selectedNodes?.clear();
$selectedNodes = $selectedNodes;
}
}
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;
node.tmp.downY = node.position.y; node.tmp.downY = node.position.y;
if ($selectedNodes) {
for (const nodeId of $selectedNodes) {
const n = graph.getNode(nodeId);
if (!n) continue;
n.tmp = n.tmp || {};
n.tmp.downX = n.position.x;
n.tmp.downY = n.position.y;
}
}
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Delete") {
if ($activeNodeId !== -1) {
const node = graph.getNode($activeNodeId);
if (node) {
graph.removeNode(node);
$activeNodeId = -1;
}
}
if ($selectedNodes) {
for (const nodeId of $selectedNodes) {
const node = graph.getNode(nodeId);
if (node) {
graph.removeNode(node);
}
}
$selectedNodes.clear();
$selectedNodes = $selectedNodes;
}
}
} }
function handleMouseUp(event: MouseEvent) { function handleMouseUp(event: MouseEvent) {
if (event.button !== 0) return; const activeNode = graph.getNode($activeNodeId);
const node = graph.getNode(activeNodeId); if (event.target instanceof HTMLElement && event.button === 0) {
if (node) { const nodeElement = event.target.closest(".node");
node.tmp = node.tmp || {}; const _activeNodeId = nodeElement?.getAttribute?.("data-node-id");
node.tmp.isMoving = false; if (_activeNodeId) {
const nodeId = parseInt(_activeNodeId, 10);
if (activeNode) {
if (!activeNode?.tmp?.isMoving && !event.ctrlKey && !event.shiftKey) {
$selectedNodes?.clear();
$selectedNodes = $selectedNodes;
$activeNodeId = nodeId;
}
}
}
}
if (activeNode?.tmp?.isMoving) {
activeNode.tmp = activeNode.tmp || {};
activeNode.tmp.isMoving = false;
const snapLevel = getSnapLevel(); const snapLevel = getSnapLevel();
const fx = snapToGrid(node.position.x, 5 / snapLevel); const fx = snapToGrid(activeNode.position.x, 5 / snapLevel);
const fy = snapToGrid(node.position.y, 5 / snapLevel); const fy = snapToGrid(activeNode.position.y, 5 / snapLevel);
if ($selectedNodes) {
for (const nodeId of $selectedNodes) {
const node = graph.getNode(nodeId);
if (!node) continue;
node.tmp = node.tmp || {};
node.tmp.snapX = node.position.x - (activeNode.position.x - fx);
node.tmp.snapY = node.position.y - (activeNode.position.y - fy);
}
}
animate(500, (a: number) => { animate(500, (a: number) => {
node.position.x = lerp(node.position.x, fx, a); activeNode.position.x = lerp(activeNode.position.x, fx, a);
node.position.y = lerp(node.position.y, fy, a); activeNode.position.y = lerp(activeNode.position.y, fy, a);
nodes.set($nodes); updateNodePosition(activeNode);
edges.set($edges);
if (node?.tmp?.isMoving) { if ($selectedNodes) {
for (const nodeId of $selectedNodes) {
const node = graph.getNode(nodeId);
if (!node) continue;
node.position.x = lerp(node.position.x, node?.tmp?.snapX || 0, a);
node.position.y = lerp(node.position.y, node?.tmp?.snapY || 0, a);
updateNodePosition(node);
}
}
if (activeNode?.tmp?.isMoving) {
return false; return false;
} }
$edges = $edges;
}); });
} else if (hoveredSocket && downSocket) { } else if ($hoveredSocket && $activeSocket) {
console.log({ hoveredSocket, downSocket });
if ( if (
typeof hoveredSocket.index === "number" && typeof $hoveredSocket.index === "number" &&
typeof downSocket.index === "string" typeof $activeSocket.index === "string"
) { ) {
graph.createEdge( graph.createEdge(
hoveredSocket.node, $hoveredSocket.node,
hoveredSocket.index || 0, $hoveredSocket.index || 0,
downSocket.node, $activeSocket.node,
downSocket.index, $activeSocket.index,
); );
} else { } else if (
typeof $activeSocket.index == "number" &&
typeof $hoveredSocket.index === "string"
) {
graph.createEdge( graph.createEdge(
downSocket.node, $activeSocket.node,
downSocket.index || 0, $activeSocket.index || 0,
hoveredSocket.node, $hoveredSocket.node,
hoveredSocket.index, $hoveredSocket.index,
); );
} }
} }
mouseDown = null; mouseDown = null;
downSocket = null; $activeSocket = null;
possibleSockets = []; $possibleSockets = [];
hoveredSocket = null; $possibleSocketIds = null;
activeNodeId = -1; $hoveredSocket = null;
} }
</script> </script>
<svelte:document <svelte:window
on:mousemove={handleMouseMove} on:mousemove={handleMouseMove}
on:mouseup={handleMouseUp} on:mouseup={handleMouseUp}
on:mousedown={handleMouseDown} on:mousedown={handleMouseDown}
on:keydown={handleKeyDown}
bind:innerWidth={width}
bind:innerHeight={height}
/> />
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />
<Debug /> <Debug />
<Camera bind:camera {maxZoom} {minZoom} bind:position={cameraPosition} /> <Camera bind:camera {maxZoom} {minZoom} bind:position={cameraPosition} />
@ -272,19 +429,13 @@
<Background {cameraPosition} {maxZoom} {minZoom} {width} {height} /> <Background {cameraPosition} {maxZoom} {minZoom} {width} {height} />
{#if $status === "idle"} {#if $status === "idle"}
{#if downSocket} {#if $activeSocket}
<FloatingEdge <FloatingEdge
from={{ x: downSocket.position[0], y: downSocket.position[1] }} from={{ x: $activeSocket.position[0], y: $activeSocket.position[1] }}
to={{ x: mousePosition[0], y: mousePosition[1] }} to={{ x: mousePosition[0], y: mousePosition[1] }}
/> />
{/if} {/if}
<GraphView <GraphView {nodes} {edges} {cameraPosition} />
{nodes}
{edges}
{cameraPosition}
{possibleSocketIds}
{downSocket}
/>
{:else if $status === "loading"} {:else if $status === "loading"}
<span>Loading</span> <span>Loading</span>
{:else if $status === "error"} {:else if $status === "error"}

View File

@ -1,18 +1,16 @@
<script lang="ts"> <script lang="ts">
import type { Edge as EdgeType, Node as NodeType } from "$lib/types"; import type { Edge as EdgeType, Node as NodeType, Socket } from "$lib/types";
import { HTML } from "@threlte/extras"; import { HTML } from "@threlte/extras";
import Edge from "../edges/Edge.svelte"; import Edge from "../edges/Edge.svelte";
import Node from "../Node.svelte"; import Node from "../Node.svelte";
import { getContext } from "svelte"; import { getContext, onMount } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { activeSocket } from "./stores";
export let nodes: Writable<Map<number, NodeType>>; export let nodes: Writable<Map<number, NodeType>>;
export let edges: Writable<EdgeType[]>; export let edges: Writable<EdgeType[]>;
export let cameraPosition = [0, 1, 0]; 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"); const isNodeInView = getContext<(n: NodeType) => boolean>("isNodeInView");
@ -25,9 +23,18 @@
edge[2].position.y + 2.5 + index * 2.5, edge[2].position.y + 2.5 + index * 2.5,
]; ];
} }
onMount(() => {
for (const node of $nodes.values()) {
if (node?.tmp?.ref) {
node.tmp.ref.style.setProperty("--nx", `${node.position.x * 10}px`);
node.tmp.ref.style.setProperty("--ny", `${node.position.y * 10}px`);
}
}
});
</script> </script>
{#each $edges as edge} {#each $edges as edge (edge[0].id + edge[2].id + edge[3])}
{@const pos = getEdgePosition(edge)} {@const pos = getEdgePosition(edge)}
{@const [x1, y1, x2, y2] = pos} {@const [x1, y1, x2, y2] = pos}
<Edge <Edge
@ -48,15 +55,11 @@
tabindex="0" tabindex="0"
class="wrapper" class="wrapper"
class:zoom-small={cameraPosition[2] < 10} class:zoom-small={cameraPosition[2] < 10}
class:hovering-sockets={downSocket} class:hovering-sockets={activeSocket}
style={`--cz: ${cameraPosition[2]}`} style={`--cz: ${cameraPosition[2]}`}
> >
{#each $nodes.values() as node} {#each $nodes.values() as node (node.id)}
<Node <Node {node} inView={cameraPosition && isNodeInView(node)} />
{node}
inView={cameraPosition && isNodeInView(node)}
{possibleSocketIds}
/>
{/each} {/each}
</div> </div>
</HTML> </HTML>

View File

@ -1,11 +1,6 @@
import type { GraphManager } from "$lib/graph-manager"; import type { GraphManager } from "$lib/graph-manager";
import { getContext } from "svelte"; import { getContext } from "svelte";
import type { GraphView } from "./view";
export function getGraphManager(): GraphManager { export function getGraphManager(): GraphManager {
return getContext("graphManager"); return getContext("graphManager");
} }
export function getGraphState(): GraphView {
return getContext("graphState");
}

View File

@ -0,0 +1,10 @@
import type { Node, Socket } from "$lib/types";
import { writable, type Writable } from "svelte/store";
export const activeNodeId: Writable<number> = writable(-1);
export const selectedNodes: Writable<Set<number> | null> = writable(null);
export const activeSocket: Writable<Socket | null> = writable(null);
export const hoveredSocket: Writable<Socket | null> = writable(null);
export const possibleSockets: Writable<Socket[]> = writable([]);
export const possibleSocketIds: Writable<Set<string> | null> = writable(null);

View File

@ -0,0 +1,20 @@
<script lang="ts">
export let title = "Details";
</script>
<details>
<summary>{title}</summary>
<slot />
</details>
<style>
details {
padding: 1em;
color: white;
outline: solid 0.1px white;
border-radius: 2px;
font-weight: 300;
font-size: 0.9em;
}
</style>

View File

@ -50,6 +50,19 @@ export class GraphManager {
this.edges.subscribe((edges) => { this.edges.subscribe((edges) => {
this._edges = edges; this._edges = edges;
}); });
globalThis["serialize"] = () => this.serialize();
}
serialize() {
const nodes = Array.from(this._nodes.values()).map(node => ({
id: node.id,
position: node.position,
type: node.type,
props: node.props,
}));
const edges = this._edges.map(edge => [edge[0].id, edge[1], edge[2].id, edge[3]]);
return { nodes, edges };
} }
async load() { async load() {
@ -87,8 +100,6 @@ export class GraphManager {
); );
this.nodes.set(nodes); this.nodes.set(nodes);
console.log(this._nodes);
this.status.set("idle"); this.status.set("idle");
} }
@ -102,6 +113,10 @@ export class GraphManager {
return this._nodes.get(id); return this._nodes.get(id);
} }
getNodeType(id: string) {
return this.nodeRegistry.getNode(id);
}
getChildrenOfNode(node: Node) { getChildrenOfNode(node: Node) {
const children = []; const children = [];
const stack = node.tmp?.children?.slice(0); const stack = node.tmp?.children?.slice(0);
@ -114,8 +129,35 @@ export class GraphManager {
return children; return children;
} }
createEdge(from: Node, fromSocket: number, to: Node, toSocket: string) { getNodesBetween(from: Node, to: Node): Node[] | undefined {
// < - - - - from
const toParents = this.getParentsOfNode(to);
// < - - - - from - - - - to
const fromParents = this.getParentsOfNode(from);
if (toParents.includes(from)) {
return toParents.splice(toParents.indexOf(from));
} else if (fromParents.includes(to)) {
return fromParents.splice(fromParents.indexOf(to));
} else {
// these two nodes are not connected
return;
}
}
private updateNodeParents(node: Node) {
}
removeNode(node: Node) {
const edges = this._edges.filter((edge) => edge[0].id !== node.id && edge[2].id !== node.id);
this.edges.set(edges);
this.nodes.update((nodes) => {
nodes.delete(node.id);
return nodes;
});
}
createEdge(from: Node, fromSocket: number, to: Node, toSocket: string) {
const existingEdges = this.getEdgesToNode(to); const existingEdges = this.getEdgesToNode(to);
@ -150,7 +192,7 @@ export class GraphManager {
parents.push(parent); parents.push(parent);
stack.push(...parent.tmp?.parents || []); stack.push(...parent.tmp?.parents || []);
} }
return parents; return parents.reverse();
} }
getPossibleSockets({ node, index }: Socket): [Node, string | number][] { getPossibleSockets({ node, index }: Socket): [Node, string | number][] {

View File

@ -21,3 +21,44 @@ export function animate(duration: number, callback: (progress: number) => void |
} }
requestAnimationFrame(loop); requestAnimationFrame(loop);
} }
export function createNodePath({
depth = 8,
height = 20,
y = 50,
cornerTop = 0,
cornerBottom = 0,
leftBump = false,
rightBump = false,
aspectRatio = 1,
} = {}) {
return `M0,${cornerTop}
${cornerTop
? ` V${cornerTop}
Q0,0 ${cornerTop * aspectRatio},0
H${100 - cornerTop * aspectRatio}
Q100,0 100,${cornerTop}
`
: ` V0
H100
`
}
V${y - height / 2}
${rightBump
? ` C${100 - depth},${y - height / 2} ${100 - depth},${y + height / 2} 100,${y + height / 2}`
: ` H100`
}
${cornerBottom
? ` V${100 - cornerBottom}
Q100,100 ${100 - cornerBottom * aspectRatio},100
H${cornerBottom * aspectRatio}
Q0,100 0,${100 - cornerBottom}
`
: `${leftBump ? `V100 H0` : `V100`}`
}
${leftBump
? ` V${y + height / 2} C${depth},${y + height / 2} ${depth},${y - height / 2} 0,${y - height / 2}`
: ` H0`
}
Z`.replace(/\s+/g, " ");
}

View File

@ -11,6 +11,9 @@ export type Node = {
type?: NodeType; type?: NodeType;
downX?: number; downX?: number;
downY?: number; downY?: number;
snapX?: number;
snapY?: number;
ref?: HTMLElement;
visible?: boolean; visible?: boolean;
isMoving?: boolean; isMoving?: boolean;
}, },

View File

@ -2,14 +2,17 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { PerfMonitor } from "@threlte/extras"; import { PerfMonitor } from "@threlte/extras";
import { Canvas } from "@threlte/core"; import { Canvas } from "@threlte/core";
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";
import Details from "$lib/elements/Details.svelte";
import { JsonView } from "@zerodevx/svelte-json-view";
const graph = GraphManager.createEmptyGraph({ width: 12, height: 12 }); const graph = GraphManager.createEmptyGraph({ width: 12, height: 12 });
graph.load(); graph.load();
let debug: undefined;
// onMount(async () => { // onMount(async () => {
// try { // try {
// const res = await invoke("greet", { name: "Dude" }); // const res = await invoke("greet", { name: "Dude" });
@ -27,15 +30,28 @@
// }); // });
</script> </script>
<div> <div class="wrapper">
<Details>
<JsonView json={debug} />
</Details>
</div>
<div class="canvas-wrapper">
<Canvas shadows={false} renderMode="on-demand" autoRender={true}> <Canvas shadows={false} renderMode="on-demand" autoRender={true}>
<!-- <PerfMonitor /> --> <!-- <PerfMonitor /> -->
<Graph {graph} /> <Graph {graph} bind:debug />
</Canvas> </Canvas>
</div> </div>
<style> <style>
div { .wrapper {
position: absolute;
z-index: 100;
top: 10px;
left: 10px;
}
.canvas-wrapper {
height: 100vh; height: 100vh;
} }

View File

@ -54,6 +54,9 @@ importers:
'@tsconfig/svelte': '@tsconfig/svelte':
specifier: ^5.0.2 specifier: ^5.0.2
version: 5.0.2 version: 5.0.2
'@zerodevx/svelte-json-view':
specifier: ^1.0.9
version: 1.0.9(svelte@4.2.12)
histoire: histoire:
specifier: ^0.17.9 specifier: ^0.17.9
version: 0.17.9(vite@5.1.4) version: 0.17.9(vite@5.1.4)
@ -1228,6 +1231,14 @@ packages:
- supports-color - supports-color
dev: false dev: false
/@zerodevx/svelte-json-view@1.0.9(svelte@4.2.12):
resolution: {integrity: sha512-2KKxBfDxEo7lM/kJSy+m1PdLAp5Q9c5nB6OYVBg7oWPdCLXB9JVH1Ytxn2hkqTn77m9MobqGI1fz9FFOTPONfA==}
peerDependencies:
svelte: ^3.57.0 || ^4.0.0
dependencies:
svelte: 4.2.12
dev: true
/abab@2.0.6: /abab@2.0.6:
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
deprecated: Use your platform's native atob() and btoa() methods instead deprecated: Use your platform's native atob() and btoa() methods instead