Skip to content

Commit 6fdd3c8

Browse files
authored
Implement smarter multi-directional node reflow (#815)
Replace the downward-only collision resolution with a BFS-based algorithm that moves nodes in the direction (up, down, left, right) requiring the least displacement, minimizing cascading collisions. Replace the "moved node" tracking with a "sacred nodes" concept for nodes that should not move.
1 parent 6a5e54f commit 6fdd3c8

File tree

3 files changed

+400
-484
lines changed

3 files changed

+400
-484
lines changed

src/flow/Editor.ts

Lines changed: 13 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,10 @@ import {
5353
getNodeBounds,
5454
calculateReflowPositions,
5555
NodeBounds,
56-
nodesOverlap
56+
snapToGrid
5757
} from './utils';
5858
import { FloatingWindow } from '../layout/FloatingWindow';
5959

60-
export function snapToGrid(value: number): number {
61-
const snapped = Math.round(value / 20) * 20;
62-
return Math.max(snapped, 0);
63-
}
64-
6560
export function findNodeForExit(
6661
definition: FlowDefinition,
6762
exitUuid: string
@@ -1636,22 +1631,13 @@ export class Editor extends RapidElement {
16361631

16371632
/**
16381633
* Checks for node collisions and reflows nodes as needed.
1639-
* Nodes are only moved downward to resolve collisions.
1640-
*
1641-
* @param movedNodeUuids - UUIDs of nodes that were just moved/dropped
1642-
* @param droppedNodeUuid - UUID of the specific node that was dropped (if applicable)
1643-
* @param dropTargetBounds - Bounds of the node that was dropped onto (if applicable)
1634+
* Sacred nodes (just moved/dropped) keep their positions while
1635+
* other nodes are moved in the least-disruptive direction.
16441636
*/
1645-
private checkCollisionsAndReflow(
1646-
movedNodeUuids: string[],
1647-
droppedNodeUuid: string | null = null,
1648-
dropTargetBounds: NodeBounds | null = null
1649-
): void {
1637+
private checkCollisionsAndReflow(sacredNodeUuids: string[]): void {
16501638
if (!this.definition) return;
16511639

1652-
// Get all node bounds (only for actual nodes, not stickies)
16531640
const allBounds: NodeBounds[] = [];
1654-
16551641
for (const node of this.definition.nodes) {
16561642
const nodeUI = this.definition._ui?.nodes[node.uuid];
16571643
if (!nodeUI?.position) continue;
@@ -1662,45 +1648,17 @@ export class Editor extends RapidElement {
16621648
}
16631649
}
16641650

1665-
// Check if we need to determine midpoint priority for a dropped node
1666-
let targetHasPriority = false;
1667-
if (droppedNodeUuid && dropTargetBounds) {
1668-
const droppedBounds = allBounds.find((b) => b.uuid === droppedNodeUuid);
1669-
if (droppedBounds) {
1670-
// Check if the bottom of the dropped node is below the midpoint of the target
1671-
// If bottom is above midpoint, dropped node gets preference (targetHasPriority = false)
1672-
// If bottom is below midpoint, target gets preference (targetHasPriority = true)
1673-
const droppedBottom = droppedBounds.bottom;
1674-
const targetMidpoint =
1675-
dropTargetBounds.top + dropTargetBounds.height / 2;
1676-
targetHasPriority = droppedBottom > targetMidpoint;
1677-
}
1678-
}
1679-
1680-
// Calculate reflow positions for each moved node
1681-
const allReflowPositions: { [uuid: string]: FlowPosition } = {};
1682-
1683-
for (const movedUuid of movedNodeUuids) {
1684-
const movedBounds = allBounds.find((b) => b.uuid === movedUuid);
1685-
if (!movedBounds) continue;
1686-
1687-
// Calculate reflow for this moved node
1688-
const reflowPositions = calculateReflowPositions(
1689-
movedUuid,
1690-
movedBounds,
1691-
allBounds,
1692-
droppedNodeUuid === movedUuid ? targetHasPriority : false
1693-
);
1651+
const reflowPositions = calculateReflowPositions(
1652+
sacredNodeUuids,
1653+
allBounds
1654+
);
16941655

1695-
// Merge into all reflow positions
1656+
if (reflowPositions.size > 0) {
1657+
const positions: { [uuid: string]: FlowPosition } = {};
16961658
for (const [uuid, position] of reflowPositions.entries()) {
1697-
allReflowPositions[uuid] = position;
1659+
positions[uuid] = position;
16981660
}
1699-
}
1700-
1701-
// If there are positions to update, apply them
1702-
if (Object.keys(allReflowPositions).length > 0) {
1703-
getStore().getState().updateCanvasPositions(allReflowPositions);
1661+
getStore().getState().updateCanvasPositions(positions);
17041662
}
17051663
}
17061664

@@ -1905,49 +1863,7 @@ export class Editor extends RapidElement {
19051863
if (nodeUuids.length > 0) {
19061864
// Allow DOM to update before checking collisions
19071865
setTimeout(() => {
1908-
// If only one node was moved, detect which node it might have been dropped onto
1909-
let droppedNodeUuid: string | null = null;
1910-
let dropTargetBounds: NodeBounds | null = null;
1911-
1912-
if (nodeUuids.length === 1) {
1913-
droppedNodeUuid = nodeUuids[0];
1914-
const droppedNodeUI = this.definition._ui?.nodes[droppedNodeUuid];
1915-
1916-
if (droppedNodeUI?.position) {
1917-
const droppedBounds = getNodeBounds(
1918-
droppedNodeUuid,
1919-
droppedNodeUI.position
1920-
);
1921-
1922-
if (droppedBounds) {
1923-
// Find which node (if any) the dropped node overlaps with
1924-
for (const node of this.definition.nodes) {
1925-
if (node.uuid === droppedNodeUuid) continue;
1926-
1927-
const nodeUI = this.definition._ui?.nodes[node.uuid];
1928-
if (!nodeUI?.position) continue;
1929-
1930-
const targetBounds = getNodeBounds(
1931-
node.uuid,
1932-
nodeUI.position
1933-
);
1934-
if (
1935-
targetBounds &&
1936-
nodesOverlap(droppedBounds, targetBounds)
1937-
) {
1938-
dropTargetBounds = targetBounds;
1939-
break; // Use the first overlapping node
1940-
}
1941-
}
1942-
}
1943-
}
1944-
}
1945-
1946-
this.checkCollisionsAndReflow(
1947-
nodeUuids,
1948-
droppedNodeUuid,
1949-
dropTargetBounds
1950-
);
1866+
this.checkCollisionsAndReflow(nodeUuids);
19511867
}, 0);
19521868
} else {
19531869
// No nodes moved, just repaint connections

0 commit comments

Comments
 (0)