Compare commits
8 Commits
6229becfd8
...
ca8b1e15ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
ca8b1e15ac
|
|||
|
4878d02705
|
|||
|
2b4c81f557
|
|||
|
d178f812fb
|
|||
|
669a2c7991
|
|||
|
becd7a1eb3
|
|||
|
d140f42468
|
|||
|
be835e5cff
|
@@ -1,7 +1 @@
|
|||||||
# Tauri + Svelte + Typescript
|
# Nodarium App
|
||||||
|
|
||||||
This template should help get you started developing with Tauri, Svelte and TypeScript in Vite.
|
|
||||||
|
|
||||||
## Recommended IDE Setup
|
|
||||||
|
|
||||||
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
|
|
||||||
|
|||||||
@@ -6,10 +6,13 @@
|
|||||||
toneMapped: false,
|
toneMapped: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let lineColor = $state(colors.edge.clone().convertSRGBToLinear());
|
||||||
|
|
||||||
$effect.root(() => {
|
$effect.root(() => {
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
appSettings.value.theme;
|
appSettings.value.theme;
|
||||||
circleMaterial.color = colors.edge.clone().convertSRGBToLinear();
|
circleMaterial.color = colors.edge.clone().convertSRGBToLinear();
|
||||||
|
lineColor = colors.edge.clone().convertSRGBToLinear();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,24 +41,21 @@
|
|||||||
|
|
||||||
const { from, to, z }: Props = $props();
|
const { from, to, z }: Props = $props();
|
||||||
|
|
||||||
|
const thickness = $derived(Math.max(0.001, 0.00082 * Math.exp(0.055 * z)));
|
||||||
|
|
||||||
let mesh = $state<Mesh>();
|
let mesh = $state<Mesh>();
|
||||||
|
|
||||||
const lineColor = $derived(
|
let lastId: string | null = null;
|
||||||
appSettings.value.theme && colors.edge.clone().convertSRGBToLinear(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let lastId: number | null = null;
|
|
||||||
|
|
||||||
const primeA = 31;
|
|
||||||
const primeB = 37;
|
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
const new_x = to.x - from.x;
|
const new_x = to.x - from.x;
|
||||||
const new_y = to.y - from.y;
|
const new_y = to.y - from.y;
|
||||||
const curveId = new_x * primeA + new_y * primeB;
|
const curveId = `${from.x}-${from.y}-${to.x}-${to.y}`;
|
||||||
|
|
||||||
if (lastId === curveId) {
|
if (lastId === curveId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
lastId = curveId;
|
||||||
|
|
||||||
const length = Math.floor(
|
const length = Math.floor(
|
||||||
Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4,
|
Math.sqrt(Math.pow(new_x, 2) + Math.pow(new_y, 2)) / 4,
|
||||||
@@ -111,5 +111,5 @@
|
|||||||
position.z={from.y}
|
position.z={from.y}
|
||||||
position.y={0.1}
|
position.y={0.1}
|
||||||
>
|
>
|
||||||
<MeshLineMaterial width={Math.max(z * 0.00012, 0.00003)} color={lineColor} />
|
<MeshLineMaterial width={thickness} color={lineColor} />
|
||||||
</T.Mesh>
|
</T.Mesh>
|
||||||
|
|||||||
@@ -178,7 +178,6 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const nodeType = this.registry.getNode(node.type);
|
const nodeType = this.registry.getNode(node.type);
|
||||||
if (nodeType) {
|
if (nodeType) {
|
||||||
node.tmp = {
|
node.tmp = {
|
||||||
random: (Math.random() - 0.5) * 2,
|
|
||||||
type: nodeType,
|
type: nodeType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -234,7 +233,6 @@ export class GraphManager extends EventEmitter<{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
node.tmp = node.tmp || {};
|
node.tmp = node.tmp || {};
|
||||||
node.tmp.random = (Math.random() - 0.5) * 2;
|
|
||||||
node.tmp.type = nodeType;
|
node.tmp.type = nodeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,13 +458,13 @@ export class GraphManager extends EventEmitter<{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const node: Node = {
|
const node: Node = $state({
|
||||||
id: this.createNodeId(),
|
id: this.createNodeId(),
|
||||||
type,
|
type,
|
||||||
position,
|
position,
|
||||||
tmp: { type: nodeType },
|
tmp: { type: nodeType },
|
||||||
props,
|
props,
|
||||||
};
|
});
|
||||||
|
|
||||||
this.nodes.set(node.id, node);
|
this.nodes.set(node.id, node);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Edge, Node, NodeType } from "@nodarium/types";
|
import type { Edge } from "@nodarium/types";
|
||||||
import { GraphSchema } from "@nodarium/types";
|
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { createKeyMap } from "../../helpers/createKeyMap";
|
import { createKeyMap } from "../../helpers/createKeyMap";
|
||||||
import AddMenu from "../components/AddMenu.svelte";
|
import AddMenu from "../components/AddMenu.svelte";
|
||||||
@@ -10,39 +9,23 @@
|
|||||||
import NodeEl from "../node/Node.svelte";
|
import NodeEl from "../node/Node.svelte";
|
||||||
import Camera from "../components/Camera.svelte";
|
import Camera from "../components/Camera.svelte";
|
||||||
import FloatingEdge from "../edges/FloatingEdge.svelte";
|
import FloatingEdge from "../edges/FloatingEdge.svelte";
|
||||||
import {
|
|
||||||
animate,
|
|
||||||
lerp,
|
|
||||||
snapToGrid as snapPointToGrid,
|
|
||||||
} from "../helpers/index.js";
|
|
||||||
import { Canvas } from "@threlte/core";
|
import { Canvas } from "@threlte/core";
|
||||||
import HelpView from "../components/HelpView.svelte";
|
import HelpView from "../components/HelpView.svelte";
|
||||||
import { getGraphManager, getGraphState } from "./state.svelte";
|
import { getGraphManager, getGraphState } from "./state.svelte";
|
||||||
import { HTML } from "@threlte/extras";
|
import { HTML } from "@threlte/extras";
|
||||||
|
import { FileDropEventManager, MouseEventManager } from "./events";
|
||||||
|
import { maxZoom, minZoom } from "./constants";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
snapToGrid = $bindable(true),
|
|
||||||
showGrid = $bindable(true),
|
|
||||||
showHelp = $bindable(false),
|
|
||||||
keymap,
|
keymap,
|
||||||
}: {
|
}: {
|
||||||
snapToGrid: boolean;
|
|
||||||
showGrid: boolean;
|
|
||||||
showHelp: boolean;
|
|
||||||
keymap: ReturnType<typeof createKeyMap>;
|
keymap: ReturnType<typeof createKeyMap>;
|
||||||
} = $props();
|
} = $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 graph = getGraphManager();
|
||||||
const graphState = getGraphState();
|
const graphState = getGraphState();
|
||||||
|
const fileDropEvents = new FileDropEventManager(graph, graphState);
|
||||||
|
const mouseEvents = new MouseEventManager(graph, graphState);
|
||||||
|
|
||||||
function getEdgeId(edge: Edge) {
|
function getEdgeId(edge: Edge) {
|
||||||
return `${edge[0].id}-${edge[1]}-${edge[2].id}-${edge[3]}`;
|
return `${edge[0].id}-${edge[1]}-${edge[2].id}-${edge[3]}`;
|
||||||
@@ -62,467 +45,6 @@
|
|||||||
return [pos1[0], pos1[1], pos2[0], pos2[1]];
|
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] - 25 / graphState.cameraPosition[2],
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
graphState.addMenuPosition = [
|
|
||||||
graphState.mousePosition[0] - 155 / graphState.cameraPosition[2],
|
|
||||||
graphState.mousePosition[1] - 25 / graphState.cameraPosition[2],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
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(() => {
|
onMount(() => {
|
||||||
if (localStorage.getItem("cameraPosition")) {
|
if (localStorage.getItem("cameraPosition")) {
|
||||||
const cPosition = JSON.parse(localStorage.getItem("cameraPosition")!);
|
const cPosition = JSON.parse(localStorage.getItem("cameraPosition")!);
|
||||||
@@ -533,34 +55,33 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window onmousemove={handleMouseMove} onmouseup={handleMouseUp} />
|
<svelte:window
|
||||||
|
onmousemove={(ev) => mouseEvents.handleMouseMove(ev)}
|
||||||
|
onmouseup={(ev) => mouseEvents.handleMouseUp(ev)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onwheel={handleMouseScroll}
|
onwheel={(ev) => mouseEvents.handleMouseScroll(ev)}
|
||||||
bind:this={graphState.wrapper}
|
bind:this={graphState.wrapper}
|
||||||
class="graph-wrapper"
|
class="graph-wrapper"
|
||||||
class:is-panning={isPanning}
|
class:is-panning={graphState.isPanning}
|
||||||
class:is-hovering={hoveredNodeId !== -1}
|
class:is-hovering={graphState.hoveredNodeId !== -1}
|
||||||
aria-label="Graph"
|
aria-label="Graph"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
bind:clientWidth={graphState.width}
|
bind:clientWidth={graphState.width}
|
||||||
bind:clientHeight={graphState.height}
|
bind:clientHeight={graphState.height}
|
||||||
ondragenter={handleDragEnter}
|
onkeydown={(ev) => keymap.handleKeyboardEvent(ev)}
|
||||||
ondragover={handlerDragOver}
|
onmousedown={(ev) => mouseEvents.handleMouseDown(ev)}
|
||||||
ondragexit={handleDragEnd}
|
{...fileDropEvents.getEventListenerProps()}
|
||||||
ondrop={handleDrop}
|
|
||||||
onmouseleave={handleMouseLeave}
|
|
||||||
onkeydown={keymap.handleKeyboardEvent}
|
|
||||||
onmousedown={handleMouseDown}
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="application/wasm,application/json"
|
accept="application/wasm,application/json"
|
||||||
id="drop-zone"
|
id="drop-zone"
|
||||||
disabled={!isDragging}
|
disabled={!graphState.isDragging}
|
||||||
ondragend={handleDragEnd}
|
ondragend={(ev) => fileDropEvents.handleDragEnd(ev)}
|
||||||
ondragleave={handleDragEnd}
|
ondragleave={(ev) => fileDropEvents.handleDragEnd(ev)}
|
||||||
/>
|
/>
|
||||||
<label for="drop-zone"></label>
|
<label for="drop-zone"></label>
|
||||||
|
|
||||||
@@ -570,7 +91,7 @@
|
|||||||
position={graphState.cameraPosition}
|
position={graphState.cameraPosition}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if showGrid !== false}
|
{#if graphState.showGrid !== false}
|
||||||
<Background
|
<Background
|
||||||
cameraPosition={graphState.cameraPosition}
|
cameraPosition={graphState.cameraPosition}
|
||||||
{maxZoom}
|
{maxZoom}
|
||||||
@@ -657,7 +178,7 @@
|
|||||||
</Canvas>
|
</Canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showHelp}
|
{#if graphState.showHelp}
|
||||||
<HelpView registry={graph.registry} />
|
<HelpView registry={graph.registry} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,12 @@
|
|||||||
setGraphManager(manager);
|
setGraphManager(manager);
|
||||||
|
|
||||||
const graphState = new GraphState(manager);
|
const graphState = new GraphState(manager);
|
||||||
|
$effect(() => {
|
||||||
|
graphState.showGrid = showGrid;
|
||||||
|
graphState.snapToGrid = snapToGrid;
|
||||||
|
graphState.showHelp = showHelp;
|
||||||
|
});
|
||||||
|
|
||||||
setGraphState(graphState);
|
setGraphState(graphState);
|
||||||
|
|
||||||
setupKeymaps(keymap, manager, graphState);
|
setupKeymaps(keymap, manager, graphState);
|
||||||
@@ -78,4 +84,4 @@
|
|||||||
manager.load(graph);
|
manager.load(graph);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<GraphEl {keymap} bind:showGrid bind:snapToGrid bind:showHelp />
|
<GraphEl {keymap} />
|
||||||
|
|||||||
3
app/src/lib/graph-interface/graph/constants.ts
Normal file
3
app/src/lib/graph-interface/graph/constants.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const minZoom = 1;
|
||||||
|
export const maxZoom = 40;
|
||||||
|
export const zoomSpeed = 2;
|
||||||
500
app/src/lib/graph-interface/graph/events.ts
Normal file
500
app/src/lib/graph-interface/graph/events.ts
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
import { GraphSchema, type NodeType, type Node } from "@nodarium/types";
|
||||||
|
import type { GraphManager } from "../graph-manager.svelte";
|
||||||
|
import type { GraphState } from "./state.svelte";
|
||||||
|
import { animate, lerp } from "$lib/helpers";
|
||||||
|
import { snapToGrid as snapPointToGrid } from "../helpers";
|
||||||
|
import { maxZoom, minZoom, zoomSpeed } from "./constants";
|
||||||
|
|
||||||
|
|
||||||
|
export class FileDropEventManager {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private graph: GraphManager,
|
||||||
|
private state: GraphState
|
||||||
|
) { }
|
||||||
|
|
||||||
|
handleFileDrop(event: DragEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.state.isDragging = false;
|
||||||
|
if (!event.dataTransfer) return;
|
||||||
|
const nodeId = event.dataTransfer.getData("data/node-id") as NodeType;
|
||||||
|
let mx = event.clientX - this.state.rect.x;
|
||||||
|
let my = event.clientY - this.state.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 = this.state.projectScreenToWorld(mx, my);
|
||||||
|
this.graph.registry.load([nodeId]).then(() => {
|
||||||
|
this.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 this.graph.registry.register(buffer);
|
||||||
|
|
||||||
|
this.graph.createNode({
|
||||||
|
type: nodeType.id,
|
||||||
|
props: {},
|
||||||
|
position: this.state.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()));
|
||||||
|
this.graph.load(state);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseLeave() {
|
||||||
|
this.state.isDragging = false;
|
||||||
|
this.state.isPanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragEnter(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.state.isDragging = true;
|
||||||
|
this.state.isPanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragOver(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.state.isDragging = true;
|
||||||
|
this.state.isPanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragEnd(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.state.isDragging = true;
|
||||||
|
this.state.isPanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEventListenerProps() {
|
||||||
|
return {
|
||||||
|
ondragenter: (ev: DragEvent) => this.handleDragEnter(ev),
|
||||||
|
ondragover: (ev: DragEvent) => this.handleDragOver(ev),
|
||||||
|
ondragexit: (ev: DragEvent) => this.handleDragEnd(ev),
|
||||||
|
ondrop: (ev: DragEvent) => this.handleFileDrop(ev),
|
||||||
|
onmouseleave: () => this.handleMouseLeave(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class MouseEventManager {
|
||||||
|
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private graph: GraphManager,
|
||||||
|
private state: GraphState
|
||||||
|
) { }
|
||||||
|
|
||||||
|
|
||||||
|
handleMouseUp(event: MouseEvent) {
|
||||||
|
this.state.isPanning = false;
|
||||||
|
if (!this.state.mouseDown) return;
|
||||||
|
|
||||||
|
const activeNode = this.graph.getNode(this.state.activeNodeId);
|
||||||
|
|
||||||
|
const clickedNodeId = this.state.getNodeIdFromEvent(event);
|
||||||
|
|
||||||
|
if (clickedNodeId !== -1) {
|
||||||
|
if (activeNode) {
|
||||||
|
if (!activeNode?.tmp?.isMoving && !event.ctrlKey && !event.shiftKey) {
|
||||||
|
this.state.activeNodeId = clickedNodeId;
|
||||||
|
this.state.clearSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeNode?.tmp?.isMoving) {
|
||||||
|
activeNode.tmp = activeNode.tmp || {};
|
||||||
|
activeNode.tmp.isMoving = false;
|
||||||
|
if (this.state.snapToGrid) {
|
||||||
|
const snapLevel = this.state.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 = [
|
||||||
|
...[...(this.state.selectedNodes?.values() || [])].map((id) =>
|
||||||
|
this.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);
|
||||||
|
this.state.updateNodePosition(node);
|
||||||
|
if (node?.tmp?.isMoving) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.graph.save();
|
||||||
|
} else if (this.state.hoveredSocket && this.state.activeSocket) {
|
||||||
|
if (
|
||||||
|
typeof this.state.hoveredSocket.index === "number" &&
|
||||||
|
typeof this.state.activeSocket.index === "string"
|
||||||
|
) {
|
||||||
|
this.graph.createEdge(
|
||||||
|
this.state.hoveredSocket.node,
|
||||||
|
this.state.hoveredSocket.index || 0,
|
||||||
|
this.state.activeSocket.node,
|
||||||
|
this.state.activeSocket.index,
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
typeof this.state.activeSocket.index == "number" &&
|
||||||
|
typeof this.state.hoveredSocket.index === "string"
|
||||||
|
) {
|
||||||
|
this.graph.createEdge(
|
||||||
|
this.state.activeSocket.node,
|
||||||
|
this.state.activeSocket.index || 0,
|
||||||
|
this.state.hoveredSocket.node,
|
||||||
|
this.state.hoveredSocket.index,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.graph.save();
|
||||||
|
} else if (this.state.activeSocket && event.ctrlKey) {
|
||||||
|
// Handle automatic adding of nodes on ctrl+mouseUp
|
||||||
|
this.state.edgeEndPosition = [
|
||||||
|
this.state.mousePosition[0],
|
||||||
|
this.state.mousePosition[1],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (typeof this.state.activeSocket.index === "number") {
|
||||||
|
this.state.addMenuPosition = [
|
||||||
|
this.state.mousePosition[0],
|
||||||
|
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2],
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
this.state.addMenuPosition = [
|
||||||
|
this.state.mousePosition[0] - 155 / this.state.cameraPosition[2],
|
||||||
|
this.state.mousePosition[1] - 25 / this.state.cameraPosition[2],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if camera moved
|
||||||
|
if (
|
||||||
|
clickedNodeId === -1 &&
|
||||||
|
!this.state.boxSelection &&
|
||||||
|
this.state.cameraDown[0] === this.state.cameraPosition[0] &&
|
||||||
|
this.state.cameraDown[1] === this.state.cameraPosition[1] &&
|
||||||
|
this.state.isBodyFocused()
|
||||||
|
) {
|
||||||
|
this.state.activeNodeId = -1;
|
||||||
|
this.state.clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.mouseDown = null;
|
||||||
|
this.state.boxSelection = false;
|
||||||
|
this.state.activeSocket = null;
|
||||||
|
this.state.possibleSockets = [];
|
||||||
|
this.state.hoveredSocket = null;
|
||||||
|
this.state.addMenuPosition = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleMouseDown(event: MouseEvent) {
|
||||||
|
if (this.state.mouseDown) return;
|
||||||
|
this.state.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 - this.state.rect.x;
|
||||||
|
let my = event.clientY - this.state.rect.y;
|
||||||
|
|
||||||
|
this.state.mouseDown = [mx, my];
|
||||||
|
this.state.cameraDown[0] = this.state.cameraPosition[0];
|
||||||
|
this.state.cameraDown[1] = this.state.cameraPosition[1];
|
||||||
|
|
||||||
|
const clickedNodeId = this.state.getNodeIdFromEvent(event);
|
||||||
|
this.state.mouseDownNodeId = clickedNodeId;
|
||||||
|
|
||||||
|
// if we clicked on a node
|
||||||
|
if (clickedNodeId !== -1) {
|
||||||
|
if (this.state.activeNodeId === -1) {
|
||||||
|
this.state.activeNodeId = clickedNodeId;
|
||||||
|
// if the selected node is the same as the clicked node
|
||||||
|
} else if (this.state.activeNodeId === clickedNodeId) {
|
||||||
|
//$activeNodeId = -1;
|
||||||
|
// if the clicked node is different from the selected node and secondary
|
||||||
|
} else if (event.ctrlKey) {
|
||||||
|
this.state.selectedNodes.add(this.state.activeNodeId);
|
||||||
|
this.state.selectedNodes.delete(clickedNodeId);
|
||||||
|
this.state.activeNodeId = clickedNodeId;
|
||||||
|
// select the node
|
||||||
|
} else if (event.shiftKey) {
|
||||||
|
const activeNode = this.graph.getNode(this.state.activeNodeId);
|
||||||
|
const newNode = this.graph.getNode(clickedNodeId);
|
||||||
|
if (activeNode && newNode) {
|
||||||
|
const edge = this.graph.getNodesBetween(activeNode, newNode);
|
||||||
|
if (edge) {
|
||||||
|
this.state.selectedNodes.clear();
|
||||||
|
for (const node of edge) {
|
||||||
|
this.state.selectedNodes.add(node.id);
|
||||||
|
}
|
||||||
|
this.state.selectedNodes.add(clickedNodeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!this.state.selectedNodes.has(clickedNodeId)) {
|
||||||
|
this.state.activeNodeId = clickedNodeId;
|
||||||
|
this.state.clearSelection();
|
||||||
|
}
|
||||||
|
} else if (event.ctrlKey) {
|
||||||
|
this.state.boxSelection = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = this.graph.getNode(this.state.activeNodeId);
|
||||||
|
if (!node) return;
|
||||||
|
node.tmp = node.tmp || {};
|
||||||
|
node.tmp.downX = node.position[0];
|
||||||
|
node.tmp.downY = node.position[1];
|
||||||
|
|
||||||
|
if (this.state.selectedNodes) {
|
||||||
|
for (const nodeId of this.state.selectedNodes) {
|
||||||
|
const n = this.graph.getNode(nodeId);
|
||||||
|
if (!n) continue;
|
||||||
|
n.tmp = n.tmp || {};
|
||||||
|
n.tmp.downX = n.position[0];
|
||||||
|
n.tmp.downY = n.position[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.edgeEndPosition = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleMouseMove(event: MouseEvent) {
|
||||||
|
let mx = event.clientX - this.state.rect.x;
|
||||||
|
let my = event.clientY - this.state.rect.y;
|
||||||
|
|
||||||
|
this.state.mousePosition = this.state.projectScreenToWorld(mx, my);
|
||||||
|
this.state.hoveredNodeId = this.state.getNodeIdFromEvent(event);
|
||||||
|
|
||||||
|
if (!this.state.mouseDown) return;
|
||||||
|
|
||||||
|
// we are creating a new edge here
|
||||||
|
if (this.state.activeSocket || this.state.possibleSockets?.length) {
|
||||||
|
let smallestDist = 1000;
|
||||||
|
let _socket;
|
||||||
|
for (const socket of this.state.possibleSockets) {
|
||||||
|
const dist = Math.sqrt(
|
||||||
|
(socket.position[0] - this.state.mousePosition[0]) ** 2 +
|
||||||
|
(socket.position[1] - this.state.mousePosition[1]) ** 2,
|
||||||
|
);
|
||||||
|
if (dist < smallestDist) {
|
||||||
|
smallestDist = dist;
|
||||||
|
_socket = socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_socket && smallestDist < 0.9) {
|
||||||
|
this.state.mousePosition = _socket.position;
|
||||||
|
this.state.hoveredSocket = _socket;
|
||||||
|
} else {
|
||||||
|
this.state.hoveredSocket = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle box selection
|
||||||
|
if (this.state.boxSelection) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const mouseD = this.state.projectScreenToWorld(
|
||||||
|
this.state.mouseDown[0],
|
||||||
|
this.state.mouseDown[1],
|
||||||
|
);
|
||||||
|
const x1 = Math.min(mouseD[0], this.state.mousePosition[0]);
|
||||||
|
const x2 = Math.max(mouseD[0], this.state.mousePosition[0]);
|
||||||
|
const y1 = Math.min(mouseD[1], this.state.mousePosition[1]);
|
||||||
|
const y2 = Math.max(mouseD[1], this.state.mousePosition[1]);
|
||||||
|
for (const node of this.graph.nodes.values()) {
|
||||||
|
if (!node?.tmp) continue;
|
||||||
|
const x = node.position[0];
|
||||||
|
const y = node.position[1];
|
||||||
|
const height = this.state.getNodeHeight(node.type);
|
||||||
|
if (x > x1 - 20 && x < x2 && y > y1 - height && y < y2) {
|
||||||
|
this.state.selectedNodes?.add(node.id);
|
||||||
|
} else {
|
||||||
|
this.state.selectedNodes?.delete(node.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// here we are handling dragging of nodes
|
||||||
|
if (this.state.activeNodeId !== -1 && this.state.mouseDownNodeId !== -1) {
|
||||||
|
const node = this.graph.getNode(this.state.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 - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
||||||
|
let newY =
|
||||||
|
oldY + (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
||||||
|
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
const snapLevel = this.state.getSnapLevel();
|
||||||
|
if (this.state.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 (this.state.selectedNodes?.size) {
|
||||||
|
for (const nodeId of this.state.selectedNodes) {
|
||||||
|
const n = this.graph.getNode(nodeId);
|
||||||
|
if (!n?.tmp) continue;
|
||||||
|
n.tmp.x = (n?.tmp?.downX || 0) - vecX;
|
||||||
|
n.tmp.y = (n?.tmp?.downY || 0) - vecY;
|
||||||
|
this.state.updateNodePosition(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.tmp.x = newX;
|
||||||
|
node.tmp.y = newY;
|
||||||
|
|
||||||
|
this.state.updateNodePosition(node);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// here we are handling panning of camera
|
||||||
|
this.state.isPanning = true;
|
||||||
|
let newX =
|
||||||
|
this.state.cameraDown[0] -
|
||||||
|
(mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
||||||
|
let newY =
|
||||||
|
this.state.cameraDown[1] -
|
||||||
|
(my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
||||||
|
|
||||||
|
this.state.setCameraTransform(newX, newY);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleMouseScroll(event: WheelEvent) {
|
||||||
|
const bodyIsFocused =
|
||||||
|
document.activeElement === document.body ||
|
||||||
|
document.activeElement === this.state.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
|
||||||
|
? this.state.cameraPosition[2] / delta
|
||||||
|
: this.state.cameraPosition[2] * delta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate the ratio of the new zoom to the original zoom
|
||||||
|
const zoomRatio = newZoom / this.state.cameraPosition[2];
|
||||||
|
|
||||||
|
// Update camera position and zoom level
|
||||||
|
this.state.setCameraTransform(
|
||||||
|
this.state.mousePosition[0] -
|
||||||
|
(this.state.mousePosition[0] - this.state.cameraPosition[0]) /
|
||||||
|
zoomRatio,
|
||||||
|
this.state.mousePosition[1] -
|
||||||
|
(this.state.mousePosition[1] - this.state.cameraPosition[1]) /
|
||||||
|
zoomRatio,
|
||||||
|
newZoom,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -53,6 +53,16 @@ export class GraphState {
|
|||||||
edgeEndPosition = $state<[number, number] | null>();
|
edgeEndPosition = $state<[number, number] | null>();
|
||||||
addMenuPosition = $state<[number, number] | null>(null);
|
addMenuPosition = $state<[number, number] | null>(null);
|
||||||
|
|
||||||
|
snapToGrid = $state(false);
|
||||||
|
showGrid = $state(true)
|
||||||
|
showHelp = $state(false)
|
||||||
|
|
||||||
|
cameraDown = [0, 0];
|
||||||
|
mouseDownNodeId = -1;
|
||||||
|
|
||||||
|
isPanning = $state(false);
|
||||||
|
isDragging = $state(false);
|
||||||
|
hoveredNodeId = $state(-1);
|
||||||
mousePosition = $state([0, 0]);
|
mousePosition = $state([0, 0]);
|
||||||
mouseDown = $state<[number, number] | null>(null);
|
mouseDown = $state<[number, number] | null>(null);
|
||||||
activeNodeId = $state(-1);
|
activeNodeId = $state(-1);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
import { colors } from "../graph/colors.svelte";
|
import { colors } from "../graph/colors.svelte";
|
||||||
import { appSettings } from "$lib/settings/app-settings.svelte";
|
import { appSettings } from "$lib/settings/app-settings.svelte";
|
||||||
|
|
||||||
const graph = getGraphManager();
|
|
||||||
const graphState = getGraphState();
|
const graphState = getGraphState();
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -37,14 +36,11 @@
|
|||||||
const height = graphState.getNodeHeight(node.type);
|
const height = graphState.getNodeHeight(node.type);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!node?.tmp) node.tmp = {};
|
|
||||||
node.tmp.mesh = meshRef;
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (!node.tmp) node.tmp = {};
|
if (!node.tmp) node.tmp = {};
|
||||||
|
if (meshRef && !node.tmp?.mesh) {
|
||||||
node.tmp.mesh = meshRef;
|
node.tmp.mesh = meshRef;
|
||||||
graphState.updateNodePosition(node);
|
graphState.updateNodePosition(node);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,8 @@
|
|||||||
z = 2,
|
z = 2,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const zOffset = (node.tmp?.random || 0) * 0.5;
|
// If we dont have a random offset, all nodes becom visible at the same zoom level -> stuttering
|
||||||
|
const zOffset = Math.random() - 0.5;
|
||||||
const zLimit = 2 - zOffset;
|
const zLimit = 2 - zOffset;
|
||||||
|
|
||||||
const parameters = Object.entries(node?.tmp?.type?.inputs || {}).filter(
|
const parameters = Object.entries(node?.tmp?.type?.inputs || {}).filter(
|
||||||
|
|||||||
@@ -84,13 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if node?.tmp?.type?.inputs?.[id]?.internal !== true}
|
{#if node?.tmp?.type?.inputs?.[id]?.internal !== true}
|
||||||
<div
|
<div data-node-socket class="large target"></div>
|
||||||
data-node-socket
|
|
||||||
class="large target"
|
|
||||||
onmousedown={handleMouseDown}
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
></div>
|
|
||||||
<div
|
<div
|
||||||
data-node-socket
|
data-node-socket
|
||||||
class="small target"
|
class="small target"
|
||||||
|
|||||||
@@ -90,7 +90,10 @@
|
|||||||
|
|
||||||
let runIndex = 0;
|
let runIndex = 0;
|
||||||
|
|
||||||
async function update(g: Graph, s: Record<string, any> = graphSettings) {
|
async function update(
|
||||||
|
g: Graph,
|
||||||
|
s: Record<string, any> = $state.snapshot(graphSettings),
|
||||||
|
) {
|
||||||
runIndex++;
|
runIndex++;
|
||||||
performanceStore.startRun();
|
performanceStore.startRun();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export type Node = {
|
|||||||
tmp?: {
|
tmp?: {
|
||||||
depth?: number;
|
depth?: number;
|
||||||
mesh?: any;
|
mesh?: any;
|
||||||
random?: number;
|
|
||||||
parents?: Node[];
|
parents?: Node[];
|
||||||
children?: Node[];
|
children?: Node[];
|
||||||
inputNodes?: Record<string, Node>;
|
inputNodes?: Record<string, Node>;
|
||||||
|
|||||||
Reference in New Issue
Block a user