398 lines
13 KiB
TypeScript
398 lines
13 KiB
TypeScript
import { animate, lerp } from '$lib/helpers';
|
|
import { type NodeInstance } from '@nodarium/types';
|
|
import type { GraphManager } from '../graph-manager.svelte';
|
|
import { type GraphState } from '../graph-state.svelte';
|
|
import { snapToGrid as snapPointToGrid } from '../helpers';
|
|
import { maxZoom, minZoom, zoomSpeed } from './constants';
|
|
import { EdgeInteractionManager } from './edge.events';
|
|
|
|
export class MouseEventManager {
|
|
edgeInteractionManager: EdgeInteractionManager;
|
|
|
|
constructor(
|
|
private graph: GraphManager,
|
|
private state: GraphState
|
|
) {
|
|
this.edgeInteractionManager = new EdgeInteractionManager(graph, state);
|
|
}
|
|
|
|
handleWindowMouseUp(event: MouseEvent) {
|
|
this.edgeInteractionManager.handleMouseUp();
|
|
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?.state?.isMoving && !event.ctrlKey && !event.shiftKey) {
|
|
this.state.activeNodeId = clickedNodeId;
|
|
this.state.clearSelection();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (activeNode?.state?.isMoving) {
|
|
activeNode.state = activeNode.state || {};
|
|
activeNode.state.isMoving = false;
|
|
if (this.state.snapToGrid) {
|
|
const snapLevel = this.state.getSnapLevel();
|
|
activeNode.position[0] = snapPointToGrid(
|
|
activeNode?.state?.x ?? activeNode.position[0],
|
|
5 / snapLevel
|
|
);
|
|
activeNode.position[1] = snapPointToGrid(
|
|
activeNode?.state?.y ?? activeNode.position[1],
|
|
5 / snapLevel
|
|
);
|
|
} else {
|
|
activeNode.position[0] = activeNode?.state?.x ?? activeNode.position[0];
|
|
activeNode.position[1] = activeNode?.state?.y ?? activeNode.position[1];
|
|
}
|
|
const nodes = [
|
|
...[...(this.state.selectedNodes?.values() || [])].map((id) => this.graph.getNode(id))
|
|
] as NodeInstance[];
|
|
|
|
const vec = [
|
|
activeNode.position[0] - (activeNode?.state.x || 0),
|
|
activeNode.position[1] - (activeNode?.state.y || 0)
|
|
];
|
|
|
|
for (const node of nodes) {
|
|
if (!node) continue;
|
|
node.state = node.state || {};
|
|
const { x, y } = node.state;
|
|
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?.state
|
|
&& node.state['x'] !== undefined
|
|
&& node.state['y'] !== undefined
|
|
) {
|
|
node.state.x = lerp(node.state.x, node.position[0], a);
|
|
node.state.y = lerp(node.state.y, node.position[1], a);
|
|
this.state.updateNodePosition(node);
|
|
if (node?.state?.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;
|
|
}
|
|
|
|
handleContextMenu(event: MouseEvent) {
|
|
if (!this.state.addMenuPosition) {
|
|
event.preventDefault();
|
|
this.state.openNodePalette();
|
|
}
|
|
}
|
|
|
|
handleMouseDown(event: MouseEvent) {
|
|
// Right click
|
|
if (event.button === 2) {
|
|
return;
|
|
}
|
|
|
|
if (this.state.mouseDown) return;
|
|
this.state.edgeEndPosition = null;
|
|
const target = event.target as HTMLElement;
|
|
|
|
if (
|
|
target.nodeName !== 'CANVAS'
|
|
&& !target.classList.contains('node')
|
|
&& !target.classList.contains('content')
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const mx = event.clientX - this.state.rect.x;
|
|
const 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();
|
|
}
|
|
this.edgeInteractionManager.handleMouseDown();
|
|
} else if (event.ctrlKey) {
|
|
this.state.boxSelection = true;
|
|
}
|
|
|
|
const node = this.graph.getNode(this.state.activeNodeId);
|
|
if (!node) return;
|
|
node.state = node.state || {};
|
|
node.state.downX = node.position[0];
|
|
node.state.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.state = n.state || {};
|
|
n.state.downX = n.position[0];
|
|
n.state.downY = n.position[1];
|
|
}
|
|
}
|
|
|
|
this.state.edgeEndPosition = null;
|
|
}
|
|
|
|
handleWindowMouseMove(event: MouseEvent) {
|
|
const mx = event.clientX - this.state.rect.x;
|
|
const 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 < 1.5) {
|
|
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?.state) 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) {
|
|
this.edgeInteractionManager.handleMouseMove();
|
|
const node = this.graph.getNode(this.state.activeNodeId);
|
|
if (!node || event.buttons !== 1) return;
|
|
|
|
node.state = node.state || {};
|
|
|
|
const oldX = node.state.downX || 0;
|
|
const oldY = node.state.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.state.isMoving) {
|
|
const dist = Math.sqrt((oldX - newX) ** 2 + (oldY - newY) ** 2);
|
|
if (dist > 0.2) {
|
|
node.state.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?.state) continue;
|
|
n.state.x = (n?.state?.downX || 0) - vecX;
|
|
n.state.y = (n?.state?.downY || 0) - vecY;
|
|
this.state.updateNodePosition(n);
|
|
}
|
|
}
|
|
|
|
node.state.x = newX;
|
|
node.state.y = newY;
|
|
|
|
this.state.updateNodePosition(node);
|
|
|
|
return;
|
|
}
|
|
|
|
// here we are handling panning of camera
|
|
this.state.isPanning = true;
|
|
const newX = this.state.cameraDown[0]
|
|
- (mx - this.state.mouseDown[0]) / this.state.cameraPosition[2];
|
|
const newY = this.state.cameraDown[1]
|
|
- (my - this.state.mouseDown[1]) / this.state.cameraPosition[2];
|
|
|
|
this.state.cameraPosition[0] = newX;
|
|
this.state.cameraPosition[1] = 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.cameraPosition[0] = this.state.mousePosition[0]
|
|
- (this.state.mousePosition[0] - this.state.cameraPosition[0])
|
|
/ zoomRatio;
|
|
this.state.cameraPosition[1] = this.state.mousePosition[1]
|
|
- (this.state.mousePosition[1] - this.state.cameraPosition[1])
|
|
/ zoomRatio;
|
|
this.state.cameraPosition[2] = newZoom;
|
|
}
|
|
}
|