All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 3m33s
717 lines
20 KiB
Svelte
717 lines
20 KiB
Svelte
<script lang="ts">
|
|
import type { Edge, Node, NodeType } from "@nodarium/types";
|
|
import { GraphSchema } from "@nodarium/types";
|
|
import { onMount } from "svelte";
|
|
import { createKeyMap } from "../../helpers/createKeyMap";
|
|
import AddMenu from "../components/AddMenu.svelte";
|
|
import Background from "../background/Background.svelte";
|
|
import BoxSelection from "../components/BoxSelection.svelte";
|
|
import EdgeEl from "../edges/Edge.svelte";
|
|
import NodeEl from "../node/Node.svelte";
|
|
import Camera from "../components/Camera.svelte";
|
|
import FloatingEdge from "../edges/FloatingEdge.svelte";
|
|
import {
|
|
animate,
|
|
lerp,
|
|
snapToGrid as snapPointToGrid,
|
|
} from "../helpers/index.js";
|
|
import { Canvas } from "@threlte/core";
|
|
import HelpView from "../components/HelpView.svelte";
|
|
import { getGraphManager, getGraphState } from "./state.svelte";
|
|
import { HTML } from "@threlte/extras";
|
|
|
|
const {
|
|
snapToGrid = $bindable(true),
|
|
showGrid = $bindable(true),
|
|
showHelp = $bindable(false),
|
|
keymap,
|
|
}: {
|
|
snapToGrid: boolean;
|
|
showGrid: boolean;
|
|
showHelp: boolean;
|
|
keymap: ReturnType<typeof createKeyMap>;
|
|
} = $props();
|
|
|
|
const minZoom = 1;
|
|
const maxZoom = 40;
|
|
let mouseDownNodeId = -1;
|
|
const cameraDown = [0, 0];
|
|
|
|
let isPanning = $state(false);
|
|
let isDragging = $state(false);
|
|
let hoveredNodeId = $state(-1);
|
|
|
|
const graph = getGraphManager();
|
|
const graphState = getGraphState();
|
|
|
|
function getEdgeId(edge: Edge) {
|
|
return `${edge[0].id}-${edge[1]}-${edge[2].id}-${edge[3]}`;
|
|
}
|
|
|
|
function getEdgePosition(edge: Edge) {
|
|
const fromNode = graph.nodes.get(edge[0].id);
|
|
const toNode = graph.nodes.get(edge[2].id);
|
|
|
|
// This check is important because nodes might not be there during some transitions.
|
|
if (!fromNode || !toNode) {
|
|
return [0, 0, 0, 0];
|
|
}
|
|
|
|
const pos1 = graphState.getSocketPosition(fromNode, edge[1]);
|
|
const pos2 = graphState.getSocketPosition(toNode, edge[3]);
|
|
return [pos1[0], pos1[1], pos2[0], pos2[1]];
|
|
}
|
|
|
|
function handleMouseMove(event: MouseEvent) {
|
|
let mx = event.clientX - graphState.rect.x;
|
|
let my = event.clientY - graphState.rect.y;
|
|
|
|
graphState.mousePosition = graphState.projectScreenToWorld(mx, my);
|
|
hoveredNodeId = graphState.getNodeIdFromEvent(event);
|
|
|
|
if (!graphState.mouseDown) return;
|
|
|
|
// we are creating a new edge here
|
|
if (graphState.activeSocket || graphState.possibleSockets?.length) {
|
|
let smallestDist = 1000;
|
|
let _socket;
|
|
for (const socket of graphState.possibleSockets) {
|
|
const dist = Math.sqrt(
|
|
(socket.position[0] - graphState.mousePosition[0]) ** 2 +
|
|
(socket.position[1] - graphState.mousePosition[1]) ** 2,
|
|
);
|
|
if (dist < smallestDist) {
|
|
smallestDist = dist;
|
|
_socket = socket;
|
|
}
|
|
}
|
|
|
|
if (_socket && smallestDist < 0.9) {
|
|
graphState.mousePosition = _socket.position;
|
|
graphState.hoveredSocket = _socket;
|
|
} else {
|
|
graphState.hoveredSocket = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// handle box selection
|
|
if (graphState.boxSelection) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const mouseD = graphState.projectScreenToWorld(
|
|
graphState.mouseDown[0],
|
|
graphState.mouseDown[1],
|
|
);
|
|
const x1 = Math.min(mouseD[0], graphState.mousePosition[0]);
|
|
const x2 = Math.max(mouseD[0], graphState.mousePosition[0]);
|
|
const y1 = Math.min(mouseD[1], graphState.mousePosition[1]);
|
|
const y2 = Math.max(mouseD[1], graphState.mousePosition[1]);
|
|
for (const node of graph.nodes.values()) {
|
|
if (!node?.tmp) continue;
|
|
const x = node.position[0];
|
|
const y = node.position[1];
|
|
const height = graphState.getNodeHeight(node.type);
|
|
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
|
|
graphState.selectedNodes?.add(node.id);
|
|
} else {
|
|
graphState.selectedNodes?.delete(node.id);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// here we are handling dragging of nodes
|
|
if (graphState.activeNodeId !== -1 && mouseDownNodeId !== -1) {
|
|
const node = graph.getNode(graphState.activeNodeId);
|
|
if (!node || event.buttons !== 1) return;
|
|
|
|
node.tmp = node.tmp || {};
|
|
|
|
const oldX = node.tmp.downX || 0;
|
|
const oldY = node.tmp.downY || 0;
|
|
|
|
let newX =
|
|
oldX + (mx - graphState.mouseDown[0]) / graphState.cameraPosition[2];
|
|
let newY =
|
|
oldY + (my - graphState.mouseDown[1]) / graphState.cameraPosition[2];
|
|
|
|
if (event.ctrlKey) {
|
|
const snapLevel = graphState.getSnapLevel();
|
|
if (snapToGrid) {
|
|
newX = snapPointToGrid(newX, 5 / snapLevel);
|
|
newY = snapPointToGrid(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 (graphState.selectedNodes?.size) {
|
|
for (const nodeId of graphState.selectedNodes) {
|
|
const n = graph.getNode(nodeId);
|
|
if (!n?.tmp) continue;
|
|
n.tmp.x = (n?.tmp?.downX || 0) - vecX;
|
|
n.tmp.y = (n?.tmp?.downY || 0) - vecY;
|
|
graphState.updateNodePosition(n);
|
|
}
|
|
}
|
|
|
|
node.tmp.x = newX;
|
|
node.tmp.y = newY;
|
|
|
|
graphState.updateNodePosition(node);
|
|
|
|
return;
|
|
}
|
|
|
|
// here we are handling panning of camera
|
|
isPanning = true;
|
|
let newX =
|
|
cameraDown[0] -
|
|
(mx - graphState.mouseDown[0]) / graphState.cameraPosition[2];
|
|
let newY =
|
|
cameraDown[1] -
|
|
(my - graphState.mouseDown[1]) / graphState.cameraPosition[2];
|
|
|
|
graphState.setCameraTransform(newX, newY);
|
|
}
|
|
|
|
const zoomSpeed = 2;
|
|
function handleMouseScroll(event: WheelEvent) {
|
|
const bodyIsFocused =
|
|
document.activeElement === document.body ||
|
|
document.activeElement === graphState.wrapper ||
|
|
document?.activeElement?.id === "graph";
|
|
if (!bodyIsFocused) return;
|
|
|
|
// Define zoom speed and clamp it between -1 and 1
|
|
const isNegative = event.deltaY < 0;
|
|
const normalizedDelta = Math.abs(event.deltaY * 0.01);
|
|
const delta = Math.pow(0.95, zoomSpeed * normalizedDelta);
|
|
|
|
// Calculate new zoom level and clamp it between minZoom and maxZoom
|
|
const newZoom = Math.max(
|
|
minZoom,
|
|
Math.min(
|
|
maxZoom,
|
|
isNegative
|
|
? graphState.cameraPosition[2] / delta
|
|
: graphState.cameraPosition[2] * delta,
|
|
),
|
|
);
|
|
|
|
// Calculate the ratio of the new zoom to the original zoom
|
|
const zoomRatio = newZoom / graphState.cameraPosition[2];
|
|
|
|
// Update camera position and zoom level
|
|
graphState.setCameraTransform(
|
|
graphState.mousePosition[0] -
|
|
(graphState.mousePosition[0] - graphState.cameraPosition[0]) /
|
|
zoomRatio,
|
|
graphState.mousePosition[1] -
|
|
(graphState.mousePosition[1] - graphState.cameraPosition[1]) /
|
|
zoomRatio,
|
|
newZoom,
|
|
);
|
|
}
|
|
|
|
function handleMouseDown(event: MouseEvent) {
|
|
if (graphState.mouseDown) return;
|
|
graphState.edgeEndPosition = null;
|
|
|
|
if (event.target instanceof HTMLElement) {
|
|
if (
|
|
event.target.nodeName !== "CANVAS" &&
|
|
!event.target.classList.contains("node") &&
|
|
!event.target.classList.contains("content")
|
|
) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
let mx = event.clientX - graphState.rect.x;
|
|
let my = event.clientY - graphState.rect.y;
|
|
|
|
graphState.mouseDown = [mx, my];
|
|
cameraDown[0] = graphState.cameraPosition[0];
|
|
cameraDown[1] = graphState.cameraPosition[1];
|
|
|
|
const clickedNodeId = graphState.getNodeIdFromEvent(event);
|
|
mouseDownNodeId = clickedNodeId;
|
|
|
|
// if we clicked on a node
|
|
if (clickedNodeId !== -1) {
|
|
if (graphState.activeNodeId === -1) {
|
|
graphState.activeNodeId = clickedNodeId;
|
|
// if the selected node is the same as the clicked node
|
|
} else if (graphState.activeNodeId === clickedNodeId) {
|
|
//$activeNodeId = -1;
|
|
// if the clicked node is different from the selected node and secondary
|
|
} else if (event.ctrlKey) {
|
|
graphState.selectedNodes.add(graphState.activeNodeId);
|
|
graphState.selectedNodes.delete(clickedNodeId);
|
|
graphState.activeNodeId = clickedNodeId;
|
|
// select the node
|
|
} else if (event.shiftKey) {
|
|
const activeNode = graph.getNode(graphState.activeNodeId);
|
|
const newNode = graph.getNode(clickedNodeId);
|
|
if (activeNode && newNode) {
|
|
const edge = graph.getNodesBetween(activeNode, newNode);
|
|
if (edge) {
|
|
graphState.selectedNodes.clear();
|
|
for (const node of edge) {
|
|
graphState.selectedNodes.add(node.id);
|
|
}
|
|
graphState.selectedNodes.add(clickedNodeId);
|
|
}
|
|
}
|
|
} else if (!graphState.selectedNodes.has(clickedNodeId)) {
|
|
graphState.activeNodeId = clickedNodeId;
|
|
graphState.clearSelection();
|
|
}
|
|
} else if (event.ctrlKey) {
|
|
graphState.boxSelection = true;
|
|
}
|
|
|
|
const node = graph.getNode(graphState.activeNodeId);
|
|
if (!node) return;
|
|
node.tmp = node.tmp || {};
|
|
node.tmp.downX = node.position[0];
|
|
node.tmp.downY = node.position[1];
|
|
|
|
if (graphState.selectedNodes) {
|
|
for (const nodeId of graphState.selectedNodes) {
|
|
const n = graph.getNode(nodeId);
|
|
if (!n) continue;
|
|
n.tmp = n.tmp || {};
|
|
n.tmp.downX = n.position[0];
|
|
n.tmp.downY = n.position[1];
|
|
}
|
|
}
|
|
|
|
graphState.edgeEndPosition = null;
|
|
}
|
|
|
|
function handleMouseUp(event: MouseEvent) {
|
|
isPanning = false;
|
|
if (!graphState.mouseDown) return;
|
|
|
|
const activeNode = graph.getNode(graphState.activeNodeId);
|
|
|
|
const clickedNodeId = graphState.getNodeIdFromEvent(event);
|
|
|
|
if (clickedNodeId !== -1) {
|
|
if (activeNode) {
|
|
if (!activeNode?.tmp?.isMoving && !event.ctrlKey && !event.shiftKey) {
|
|
graphState.activeNodeId = clickedNodeId;
|
|
graphState.clearSelection();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (activeNode?.tmp?.isMoving) {
|
|
activeNode.tmp = activeNode.tmp || {};
|
|
activeNode.tmp.isMoving = false;
|
|
if (snapToGrid) {
|
|
const snapLevel = graphState.getSnapLevel();
|
|
activeNode.position[0] = snapPointToGrid(
|
|
activeNode?.tmp?.x ?? activeNode.position[0],
|
|
5 / snapLevel,
|
|
);
|
|
activeNode.position[1] = snapPointToGrid(
|
|
activeNode?.tmp?.y ?? activeNode.position[1],
|
|
5 / snapLevel,
|
|
);
|
|
} else {
|
|
activeNode.position[0] = activeNode?.tmp?.x ?? activeNode.position[0];
|
|
activeNode.position[1] = activeNode?.tmp?.y ?? activeNode.position[1];
|
|
}
|
|
const nodes = [
|
|
...[...(graphState.selectedNodes?.values() || [])].map((id) =>
|
|
graph.getNode(id),
|
|
),
|
|
] as Node[];
|
|
|
|
const vec = [
|
|
activeNode.position[0] - (activeNode?.tmp.x || 0),
|
|
activeNode.position[1] - (activeNode?.tmp.y || 0),
|
|
];
|
|
|
|
for (const node of nodes) {
|
|
if (!node) continue;
|
|
node.tmp = node.tmp || {};
|
|
const { x, y } = node.tmp;
|
|
if (x !== undefined && y !== undefined) {
|
|
node.position[0] = x + vec[0];
|
|
node.position[1] = y + vec[1];
|
|
}
|
|
}
|
|
nodes.push(activeNode);
|
|
animate(500, (a: number) => {
|
|
for (const node of nodes) {
|
|
if (
|
|
node?.tmp &&
|
|
node.tmp["x"] !== undefined &&
|
|
node.tmp["y"] !== undefined
|
|
) {
|
|
node.tmp.x = lerp(node.tmp.x, node.position[0], a);
|
|
node.tmp.y = lerp(node.tmp.y, node.position[1], a);
|
|
graphState.updateNodePosition(node);
|
|
if (node?.tmp?.isMoving) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
graph.save();
|
|
} else if (graphState.hoveredSocket && graphState.activeSocket) {
|
|
if (
|
|
typeof graphState.hoveredSocket.index === "number" &&
|
|
typeof graphState.activeSocket.index === "string"
|
|
) {
|
|
graph.createEdge(
|
|
graphState.hoveredSocket.node,
|
|
graphState.hoveredSocket.index || 0,
|
|
graphState.activeSocket.node,
|
|
graphState.activeSocket.index,
|
|
);
|
|
} else if (
|
|
typeof graphState.activeSocket.index == "number" &&
|
|
typeof graphState.hoveredSocket.index === "string"
|
|
) {
|
|
graph.createEdge(
|
|
graphState.activeSocket.node,
|
|
graphState.activeSocket.index || 0,
|
|
graphState.hoveredSocket.node,
|
|
graphState.hoveredSocket.index,
|
|
);
|
|
}
|
|
graph.save();
|
|
} else if (graphState.activeSocket && event.ctrlKey) {
|
|
// Handle automatic adding of nodes on ctrl+mouseUp
|
|
graphState.edgeEndPosition = [
|
|
graphState.mousePosition[0],
|
|
graphState.mousePosition[1],
|
|
];
|
|
|
|
if (typeof graphState.activeSocket.index === "number") {
|
|
graphState.addMenuPosition = [
|
|
graphState.mousePosition[0],
|
|
graphState.mousePosition[1] - 3,
|
|
];
|
|
} else {
|
|
graphState.addMenuPosition = [
|
|
graphState.mousePosition[0] - 20,
|
|
graphState.mousePosition[1] - 3,
|
|
];
|
|
}
|
|
return;
|
|
}
|
|
|
|
// check if camera moved
|
|
if (
|
|
clickedNodeId === -1 &&
|
|
!graphState.boxSelection &&
|
|
cameraDown[0] === graphState.cameraPosition[0] &&
|
|
cameraDown[1] === graphState.cameraPosition[1] &&
|
|
graphState.isBodyFocused()
|
|
) {
|
|
graphState.activeNodeId = -1;
|
|
graphState.clearSelection();
|
|
}
|
|
|
|
graphState.mouseDown = null;
|
|
graphState.boxSelection = false;
|
|
graphState.activeSocket = null;
|
|
graphState.possibleSockets = [];
|
|
graphState.hoveredSocket = null;
|
|
graphState.addMenuPosition = null;
|
|
}
|
|
|
|
function handleMouseLeave() {
|
|
isDragging = false;
|
|
isPanning = false;
|
|
}
|
|
|
|
function handleDrop(event: DragEvent) {
|
|
event.preventDefault();
|
|
isDragging = false;
|
|
if (!event.dataTransfer) return;
|
|
const nodeId = event.dataTransfer.getData("data/node-id") as NodeType;
|
|
let mx = event.clientX - graphState.rect.x;
|
|
let my = event.clientY - graphState.rect.y;
|
|
|
|
if (nodeId) {
|
|
let nodeOffsetX = event.dataTransfer.getData("data/node-offset-x");
|
|
let nodeOffsetY = event.dataTransfer.getData("data/node-offset-y");
|
|
if (nodeOffsetX && nodeOffsetY) {
|
|
mx += parseInt(nodeOffsetX);
|
|
my += parseInt(nodeOffsetY);
|
|
}
|
|
|
|
let props = {};
|
|
let rawNodeProps = event.dataTransfer.getData("data/node-props");
|
|
if (rawNodeProps) {
|
|
try {
|
|
props = JSON.parse(rawNodeProps);
|
|
} catch (e) {}
|
|
}
|
|
|
|
const pos = graphState.projectScreenToWorld(mx, my);
|
|
graph.registry.load([nodeId]).then(() => {
|
|
graph.createNode({
|
|
type: nodeId,
|
|
props,
|
|
position: pos,
|
|
});
|
|
});
|
|
} else if (event.dataTransfer.files.length) {
|
|
const file = event.dataTransfer.files[0];
|
|
|
|
if (file.type === "application/wasm") {
|
|
const reader = new FileReader();
|
|
reader.onload = async (e) => {
|
|
const buffer = e.target?.result;
|
|
if (buffer?.constructor === ArrayBuffer) {
|
|
const nodeType = await graph.registry.register(buffer);
|
|
|
|
graph.createNode({
|
|
type: nodeType.id,
|
|
props: {},
|
|
position: graphState.projectScreenToWorld(mx, my),
|
|
});
|
|
}
|
|
};
|
|
reader.readAsArrayBuffer(file);
|
|
} else if (file.type === "application/json") {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
const buffer = e.target?.result as ArrayBuffer;
|
|
if (buffer) {
|
|
const state = GraphSchema.parse(JSON.parse(buffer.toString()));
|
|
graph.load(state);
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleDragEnter(e: DragEvent) {
|
|
e.preventDefault();
|
|
isDragging = true;
|
|
isPanning = false;
|
|
}
|
|
|
|
function handlerDragOver(e: DragEvent) {
|
|
e.preventDefault();
|
|
isDragging = true;
|
|
isPanning = false;
|
|
}
|
|
|
|
function handleDragEnd(e: DragEvent) {
|
|
e.preventDefault();
|
|
isDragging = true;
|
|
isPanning = false;
|
|
}
|
|
|
|
onMount(() => {
|
|
if (localStorage.getItem("cameraPosition")) {
|
|
const cPosition = JSON.parse(localStorage.getItem("cameraPosition")!);
|
|
if (Array.isArray(cPosition)) {
|
|
graphState.setCameraTransform(cPosition[0], cPosition[1], cPosition[2]);
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<svelte:window onmousemove={handleMouseMove} onmouseup={handleMouseUp} />
|
|
|
|
<div
|
|
onwheel={handleMouseScroll}
|
|
bind:this={graphState.wrapper}
|
|
class="graph-wrapper"
|
|
class:is-panning={isPanning}
|
|
class:is-hovering={hoveredNodeId !== -1}
|
|
aria-label="Graph"
|
|
role="button"
|
|
tabindex="0"
|
|
bind:clientWidth={graphState.width}
|
|
bind:clientHeight={graphState.height}
|
|
ondragenter={handleDragEnter}
|
|
ondragover={handlerDragOver}
|
|
ondragexit={handleDragEnd}
|
|
ondrop={handleDrop}
|
|
onmouseleave={handleMouseLeave}
|
|
onkeydown={keymap.handleKeyboardEvent}
|
|
onmousedown={handleMouseDown}
|
|
>
|
|
<input
|
|
type="file"
|
|
accept="application/wasm,application/json"
|
|
id="drop-zone"
|
|
disabled={!isDragging}
|
|
ondragend={handleDragEnd}
|
|
ondragleave={handleDragEnd}
|
|
/>
|
|
<label for="drop-zone"></label>
|
|
|
|
<Canvas shadows={false} renderMode="on-demand" colorManagementEnabled={false}>
|
|
<Camera
|
|
bind:camera={graphState.camera}
|
|
position={graphState.cameraPosition}
|
|
/>
|
|
|
|
{#if showGrid !== false}
|
|
<Background
|
|
cameraPosition={graphState.cameraPosition}
|
|
{maxZoom}
|
|
{minZoom}
|
|
width={graphState.width}
|
|
height={graphState.height}
|
|
/>
|
|
{/if}
|
|
|
|
{#if graphState.boxSelection && graphState.mouseDown}
|
|
<BoxSelection
|
|
cameraPosition={graphState.cameraPosition}
|
|
p1={{
|
|
x:
|
|
graphState.cameraPosition[0] +
|
|
(graphState.mouseDown[0] - graphState.width / 2) /
|
|
graphState.cameraPosition[2],
|
|
y:
|
|
graphState.cameraPosition[1] +
|
|
(graphState.mouseDown[1] - graphState.height / 2) /
|
|
graphState.cameraPosition[2],
|
|
}}
|
|
p2={{ x: graphState.mousePosition[0], y: graphState.mousePosition[1] }}
|
|
/>
|
|
{/if}
|
|
|
|
{#if graph.status === "idle"}
|
|
{#if graphState.addMenuPosition}
|
|
<AddMenu />
|
|
{/if}
|
|
|
|
{#if graphState.activeSocket}
|
|
<FloatingEdge
|
|
z={graphState.cameraPosition[2]}
|
|
from={{
|
|
x: graphState.activeSocket.position[0],
|
|
y: graphState.activeSocket.position[1],
|
|
}}
|
|
to={{
|
|
x: graphState.edgeEndPosition?.[0] ?? graphState.mousePosition[0],
|
|
y: graphState.edgeEndPosition?.[1] ?? graphState.mousePosition[1],
|
|
}}
|
|
/>
|
|
{/if}
|
|
|
|
{#each graph.edges as edge (getEdgeId(edge))}
|
|
{@const [x1, y1, x2, y2] = getEdgePosition(edge)}
|
|
<EdgeEl
|
|
z={graphState.cameraPosition[2]}
|
|
from={{
|
|
x: x1,
|
|
y: y1,
|
|
}}
|
|
to={{
|
|
x: x2,
|
|
y: y2,
|
|
}}
|
|
/>
|
|
{/each}
|
|
|
|
<HTML transform={false}>
|
|
<div
|
|
role="tree"
|
|
id="graph"
|
|
tabindex="0"
|
|
class="wrapper"
|
|
style:transform={`scale(${graphState.cameraPosition[2] * 0.1})`}
|
|
class:hovering-sockets={graphState.activeSocket}
|
|
>
|
|
{#each graph.nodes.values() as node (node.id)}
|
|
<NodeEl
|
|
{node}
|
|
inView={graphState.isNodeInView(node)}
|
|
z={graphState.cameraPosition[2]}
|
|
/>
|
|
{/each}
|
|
</div>
|
|
</HTML>
|
|
{:else if graph.status === "loading"}
|
|
<span>Loading</span>
|
|
{:else if graph.status === "error"}
|
|
<span>Error</span>
|
|
{/if}
|
|
</Canvas>
|
|
</div>
|
|
|
|
{#if showHelp}
|
|
<HelpView registry={graph.registry} />
|
|
{/if}
|
|
|
|
<style>
|
|
.graph-wrapper {
|
|
position: relative;
|
|
z-index: 0;
|
|
transition: opacity 0.3s ease;
|
|
height: 100%;
|
|
}
|
|
|
|
.wrapper {
|
|
position: absolute;
|
|
z-index: 100;
|
|
width: 0px;
|
|
height: 0px;
|
|
}
|
|
|
|
.is-hovering {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.is-panning {
|
|
cursor: grab;
|
|
}
|
|
|
|
input {
|
|
position: absolute;
|
|
z-index: 1;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: var(--layer-2);
|
|
opacity: 0;
|
|
}
|
|
input:disabled {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
input:disabled + label {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
label {
|
|
position: absolute;
|
|
z-index: 1;
|
|
top: 10px;
|
|
left: 10px;
|
|
border-radius: 5px;
|
|
width: calc(100% - 20px);
|
|
height: calc(100% - 25px);
|
|
border: dashed 4px var(--layer-2);
|
|
background: var(--layer-1);
|
|
opacity: 0.5;
|
|
}
|
|
</style>
|