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:
parent
b8906f344e
commit
6ff958d6ae
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue