Create node by dragging link into empty space (#1438)

* Create node by dragging into empty space

* Prevent add menu when disconnecting a link

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube 2023-10-24 21:22:41 +01:00 committed by GitHub
parent b8906f344e
commit 6ff958d6ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 100 additions and 57 deletions

View File

@ -271,9 +271,22 @@
if (e.key.toLowerCase() === "escape") { if (e.key.toLowerCase() === "escape") {
nodeListLocation = undefined; nodeListLocation = undefined;
document.removeEventListener("keydown", keydown); document.removeEventListener("keydown", keydown);
linkInProgressFromConnector = undefined;
} }
} }
function loadNodeList(e: PointerEvent, graphBounds: DOMRect) {
nodeListLocation = {
x: Math.round(((e.clientX - graphBounds.x) / transform.scale - transform.x) / GRID_SIZE),
y: Math.round(((e.clientY - graphBounds.y) / transform.scale - transform.y) / GRID_SIZE),
};
// Find actual relevant child and focus it (setTimeout is required to actually focus the input element)
setTimeout(() => nodeSearchInput?.focus(), 0);
document.addEventListener("keydown", keydown);
}
// TODO: Move the event listener from the graph to the window so dragging outside the graph area (or even the whole browser window) works // TODO: Move the event listener from the graph to the window so dragging outside the graph area (or even the whole browser window) works
function pointerDown(e: PointerEvent) { function pointerDown(e: PointerEvent) {
const [lmb, rmb] = [e.button === 0, e.button === 2]; const [lmb, rmb] = [e.button === 0, e.button === 2];
@ -287,15 +300,7 @@
if (rmb) { if (rmb) {
const graphBounds = graph?.getBoundingClientRect(); const graphBounds = graph?.getBoundingClientRect();
if (!graphBounds) return; if (!graphBounds) return;
nodeListLocation = { loadNodeList(e, graphBounds);
x: Math.round(((e.clientX - graphBounds.x) / transform.scale - transform.x) / GRID_SIZE),
y: Math.round(((e.clientY - graphBounds.y) / transform.scale - transform.y) / GRID_SIZE),
};
// Find actual relevant child and focus it (setTimeout is required to actually focus the input element)
setTimeout(() => nodeSearchInput?.focus(), 0);
document.addEventListener("keydown", keydown);
return; return;
} }
@ -303,7 +308,10 @@
if (lmb && nodeList) return; if (lmb && nodeList) return;
// Since the user is clicking elsewhere in the graph, ensure the add nodes list is closed // Since the user is clicking elsewhere in the graph, ensure the add nodes list is closed
if (lmb) nodeListLocation = undefined; if (lmb) {
nodeListLocation = undefined;
linkInProgressFromConnector = undefined;
}
// Alt-click sets the clicked node as previewed // Alt-click sets the clicked node as previewed
if (lmb && e.altKey && nodeId) { if (lmb && e.altKey && nodeId) {
@ -389,7 +397,7 @@
if (panning) { if (panning) {
transform.x += e.movementX / transform.scale; transform.x += e.movementX / transform.scale;
transform.y += e.movementY / transform.scale; transform.y += e.movementY / transform.scale;
} else if (linkInProgressFromConnector) { } else if (linkInProgressFromConnector && !nodeListLocation) {
const target = e.target as Element | undefined; const target = e.target as Element | undefined;
const dot = (target?.closest(`[data-port="input"]`) || undefined) as SVGSVGElement | undefined; const dot = (target?.closest(`[data-port="input"]`) || undefined) as SVGSVGElement | undefined;
if (dot) { if (dot) {
@ -436,9 +444,54 @@
else return undefined; else return undefined;
} }
// Check if this node should be inserted between two other nodes
function checkInsertBetween() {
if (selected.length !== 1) return;
const selectedNodeId = selected[0];
const selectedNode = nodesContainer?.querySelector(`[data-node="${String(selectedNodeId)}"]`) || undefined;
// Check that neither the input or output of the selected node are already connected.
const notConnected = $nodeGraph.links.findIndex((link) => link.linkStart === selectedNodeId || (link.linkEnd === selectedNodeId && link.linkEndInputIndex === BigInt(0))) === -1;
const input = selectedNode?.querySelector(`[data-port="input"]`) || undefined;
const output = selectedNode?.querySelector(`[data-port="output"]`) || undefined;
// TODO: Make sure inputs are correctly typed
if (!selectedNode || !notConnected || !input || !output || !nodesContainer) return;
// Fixes typing for some reason?
const theNodesContainer = nodesContainer;
// Find the link that the node has been dragged on top of
const link = $nodeGraph.links.find((link): boolean => {
const { nodeInput, nodeOutput } = resolveLink(link, theNodesContainer);
if (!nodeInput || !nodeOutput) return false;
const wireCurveLocations = buildWirePathLocations(nodeOutput.getBoundingClientRect(), nodeInput.getBoundingClientRect(), false, false);
const selectedNodeBounds = selectedNode.getBoundingClientRect();
const containerBoundsBounds = theNodesContainer.getBoundingClientRect();
return editor.instance.rectangleIntersects(
new Float64Array(wireCurveLocations.map((loc) => loc.x)),
new Float64Array(wireCurveLocations.map((loc) => loc.y)),
selectedNodeBounds.top - containerBoundsBounds.y,
selectedNodeBounds.left - containerBoundsBounds.x,
selectedNodeBounds.bottom - containerBoundsBounds.y,
selectedNodeBounds.right - containerBoundsBounds.x
);
});
// If the node has been dragged on top of the link then connect it into the middle.
if (link) {
editor.instance.connectNodesByLink(link.linkStart, 0, selectedNodeId, 0);
editor.instance.connectNodesByLink(selectedNodeId, 0, link.linkEnd, Number(link.linkEndInputIndex));
editor.instance.shiftNode(selectedNodeId);
}
}
function pointerUp(e: PointerEvent) { function pointerUp(e: PointerEvent) {
panning = false; panning = false;
const initialDisconnecting = disconnecting;
if (disconnecting) { if (disconnecting) {
editor.instance.disconnectNodes(BigInt(disconnecting.nodeId), disconnecting.inputIndex); editor.instance.disconnectNodes(BigInt(disconnecting.nodeId), disconnecting.inputIndex);
} }
@ -453,6 +506,24 @@
const { nodeId: inputConnectedNodeID, index: inputNodeConnectionIndex } = to; const { nodeId: inputConnectedNodeID, index: inputNodeConnectionIndex } = to;
editor.instance.connectNodesByLink(outputConnectedNodeID, outputNodeConnectionIndex, inputConnectedNodeID, inputNodeConnectionIndex); editor.instance.connectNodesByLink(outputConnectedNodeID, outputNodeConnectionIndex, inputConnectedNodeID, inputNodeConnectionIndex);
} }
} else if (linkInProgressFromConnector && !initialDisconnecting) {
// If the add node menu is already open, we don't want to open it again
if (nodeListLocation) return;
const graphBounds = graph?.getBoundingClientRect();
if (!graphBounds) return;
// Create the node list, which should set nodeListLocation to a valid value
loadNodeList(e, graphBounds);
if (!nodeListLocation) return;
let nodeListLocation2: { x: number; y: number } = nodeListLocation;
linkInProgressToConnector = new DOMRect(
(nodeListLocation2.x * GRID_SIZE + transform.x) * transform.scale + graphBounds.x,
(nodeListLocation2.y * GRID_SIZE + transform.y) * transform.scale + graphBounds.y
);
return;
} else if (draggingNodes) { } else if (draggingNodes) {
if (draggingNodes.startX === e.x || draggingNodes.startY === e.y) { if (draggingNodes.startX === e.x || draggingNodes.startY === e.y) {
if (selectIfNotDragged !== undefined && (selected.length !== 1 || selected[0] !== selectIfNotDragged)) { if (selectIfNotDragged !== undefined && (selected.length !== 1 || selected[0] !== selectIfNotDragged)) {
@ -463,48 +534,7 @@
if (selected.length > 0 && (draggingNodes.roundX !== 0 || draggingNodes.roundY !== 0)) editor.instance.moveSelectedNodes(draggingNodes.roundX, draggingNodes.roundY); if (selected.length > 0 && (draggingNodes.roundX !== 0 || draggingNodes.roundY !== 0)) editor.instance.moveSelectedNodes(draggingNodes.roundX, draggingNodes.roundY);
// Check if this node should be inserted between two other nodes checkInsertBetween();
if (selected.length === 1) {
const selectedNodeId = selected[0];
const selectedNode = nodesContainer?.querySelector(`[data-node="${String(selectedNodeId)}"]`) || undefined;
// Check that neither the input or output of the selected node are already connected.
const notConnected = $nodeGraph.links.findIndex((link) => link.linkStart === selectedNodeId || (link.linkEnd === selectedNodeId && link.linkEndInputIndex === BigInt(0))) === -1;
const input = selectedNode?.querySelector(`[data-port="input"]`) || undefined;
const output = selectedNode?.querySelector(`[data-port="output"]`) || undefined;
// TODO: Make sure inputs are correctly typed
if (selectedNode && notConnected && input && output && nodesContainer) {
const theNodesContainer = nodesContainer;
// Find the link that the node has been dragged on top of
const link = $nodeGraph.links.find((link): boolean => {
const { nodeInput, nodeOutput } = resolveLink(link, theNodesContainer);
if (!nodeInput || !nodeOutput) return false;
const wireCurveLocations = buildWirePathLocations(nodeOutput.getBoundingClientRect(), nodeInput.getBoundingClientRect(), false, false);
const selectedNodeBounds = selectedNode.getBoundingClientRect();
const containerBoundsBounds = theNodesContainer.getBoundingClientRect();
return editor.instance.rectangleIntersects(
new Float64Array(wireCurveLocations.map((loc) => loc.x)),
new Float64Array(wireCurveLocations.map((loc) => loc.y)),
selectedNodeBounds.top - containerBoundsBounds.y,
selectedNodeBounds.left - containerBoundsBounds.x,
selectedNodeBounds.bottom - containerBoundsBounds.y,
selectedNodeBounds.right - containerBoundsBounds.x
);
});
// If the node has been dragged on top of the link then connect it into the middle.
if (link) {
editor.instance.connectNodesByLink(link.linkStart, 0, selectedNodeId, 0);
editor.instance.connectNodesByLink(selectedNodeId, 0, link.linkEnd, Number(link.linkEndInputIndex));
editor.instance.shiftNode(selectedNodeId);
}
}
}
draggingNodes = undefined; draggingNodes = undefined;
selectIfNotDragged = undefined; selectIfNotDragged = undefined;
@ -517,8 +547,19 @@
function createNode(nodeType: string): void { function createNode(nodeType: string): void {
if (!nodeListLocation) return; if (!nodeListLocation) return;
editor.instance.createNode(nodeType, nodeListLocation.x, nodeListLocation.y); const inputNodeConnectionIndex = 0;
const inputConnectedNodeID = editor.instance.createNode(nodeType, nodeListLocation.x, nodeListLocation.y - 1);
nodeListLocation = undefined; nodeListLocation = undefined;
if (!linkInProgressFromConnector) return;
const from = connectorToNodeIndex(linkInProgressFromConnector);
if (from !== undefined) {
const { nodeId: outputConnectedNodeID, index: outputNodeConnectionIndex } = from;
editor.instance.connectNodesByLink(outputConnectedNodeID, outputNodeConnectionIndex, inputConnectedNodeID, inputNodeConnectionIndex);
}
linkInProgressFromConnector = undefined;
} }
function nodeBorderMask(nodeWidth: number, primaryInputExists: boolean, parameters: number, primaryOutputExists: boolean, exposedOutputs: number): string { function nodeBorderMask(nodeWidth: number, primaryInputExists: boolean, parameters: number, primaryOutputExists: boolean, exposedOutputs: number): string {
@ -654,7 +695,7 @@
</div> </div>
<div class="thumbnail"> <div class="thumbnail">
{#if $nodeGraph.thumbnails.has(node.id)} {#if $nodeGraph.thumbnails.has(node.id)}
{@html $nodeGraph.thumbnails.get(node.id) } {@html $nodeGraph.thumbnails.get(node.id)}
{/if} {/if}
{#if node.primaryOutput} {#if node.primaryOutput}
<svg <svg

View File

@ -643,9 +643,11 @@ impl JsEditorHandle {
/// Creates a new document node in the node graph /// Creates a new document node in the node graph
#[wasm_bindgen(js_name = createNode)] #[wasm_bindgen(js_name = createNode)]
pub fn create_node(&self, node_type: String, x: i32, y: i32) { pub fn create_node(&self, node_type: String, x: i32, y: i32) -> u64 {
let message = NodeGraphMessage::CreateNode { node_id: None, node_type, x, y }; let id = generate_uuid();
let message = NodeGraphMessage::CreateNode { node_id: Some(id), node_type, x, y };
self.dispatch(message); self.dispatch(message);
id
} }
/// Notifies the backend that the user selected a node in the node graph /// Notifies the backend that the user selected a node in the node graph