fix: lasso wasnt moving nodes if there was also a text item in the selection, now it works

This commit is contained in:
Stan
2026-02-02 06:56:40 +00:00
parent 19873adcd6
commit f5ce1689c9
2 changed files with 95 additions and 74 deletions
+18 -8
View File
@@ -362,16 +362,21 @@ export const useScene = () => {
); );
const updateTextBox = useCallback( const updateTextBox = useCallback(
(id: string, updates: Partial<TextBox>) => { (id: string, updates: Partial<TextBox>, currentState?: State) => {
if (!model?.actions || !scene?.actions || !currentViewId) return; if (!model?.actions || !scene?.actions || !currentViewId) return currentState || getState();
saveToHistoryBeforeChange(); if (!transactionInProgress.current) {
saveToHistoryBeforeChange();
}
const stateToUse = currentState || getState();
const newState = reducers.view({ const newState = reducers.view({
action: 'UPDATE_TEXTBOX', action: 'UPDATE_TEXTBOX',
payload: { id, ...updates }, payload: { id, ...updates },
ctx: { viewId: currentViewId, state: getState() } ctx: { viewId: currentViewId, state: stateToUse }
}); });
setState(newState); setState(newState);
return newState;
}, },
[ [
getState, getState,
@@ -428,16 +433,21 @@ export const useScene = () => {
); );
const updateRectangle = useCallback( const updateRectangle = useCallback(
(id: string, updates: Partial<Rectangle>) => { (id: string, updates: Partial<Rectangle>, currentState?: State) => {
if (!model?.actions || !scene?.actions || !currentViewId) return; if (!model?.actions || !scene?.actions || !currentViewId) return currentState || getState();
saveToHistoryBeforeChange(); if (!transactionInProgress.current) {
saveToHistoryBeforeChange();
}
const stateToUse = currentState || getState();
const newState = reducers.view({ const newState = reducers.view({
action: 'UPDATE_RECTANGLE', action: 'UPDATE_RECTANGLE',
payload: { id, ...updates }, payload: { id, ...updates },
ctx: { viewId: currentViewId, state: getState() } ctx: { viewId: currentViewId, state: stateToUse }
}); });
setState(newState); setState(newState);
return newState;
}, },
[ [
getState, getState,
@@ -17,11 +17,14 @@ const dragItems = (
delta: Coords, delta: Coords,
scene: ReturnType<typeof useScene> scene: ReturnType<typeof useScene>
) => { ) => {
// Separate items from other draggable elements // Separate all item types upfront
const itemRefs = items.filter(item => item.type === 'ITEM'); const itemRefs = items.filter(item => item.type === 'ITEM');
const otherRefs = items.filter(item => item.type !== 'ITEM'); const textBoxRefs = items.filter(item => item.type === 'TEXTBOX');
const rectangleRefs = items.filter(item => item.type === 'RECTANGLE');
const anchorRefs = items.filter(item => item.type === 'CONNECTOR_ANCHOR');
// If there are items being dragged, find nearest unoccupied tiles for them // Calculate node targets if any nodes are selected
let newTiles: Coords[] | null = null;
if (itemRefs.length > 0) { if (itemRefs.length > 0) {
const itemsWithTargets = itemRefs.map(item => { const itemsWithTargets = itemRefs.map(item => {
const node = getItemByIdOrThrow(scene.items, item.id).value; const node = getItemByIdOrThrow(scene.items, item.id).value;
@@ -31,85 +34,93 @@ const dragItems = (
}; };
}); });
// Find nearest unoccupied tiles for all items newTiles = findNearestUnoccupiedTilesForGroup(
const newTiles = findNearestUnoccupiedTilesForGroup(
itemsWithTargets, itemsWithTargets,
scene, scene,
itemRefs.map(item => item.id) // Exclude the items being dragged itemRefs.map(item => item.id)
); );
// If we found valid positions for all items, move them // If nodes can't find valid positions, abort the entire drag operation
if (newTiles) { if (!newTiles) {
// Wrap all updates in a transaction to prevent history issues return;
scene.transaction(() => { }
// Chain state updates to avoid race conditions }
let currentState: State | undefined;
// Check if there's anything to update
const hasUpdates = newTiles || textBoxRefs.length > 0 || rectangleRefs.length > 0;
if (hasUpdates) {
// Wrap ALL updates in a single transaction with state chaining
// This ensures each update builds on the previous one's state
scene.transaction(() => {
let currentState: State | undefined;
// 1. Update nodes
if (newTiles) {
itemRefs.forEach((item, index) => { itemRefs.forEach((item, index) => {
currentState = scene.updateViewItem(item.id, { currentState = scene.updateViewItem(item.id, {
tile: newTiles[index] tile: newTiles[index]
}, currentState); }, currentState);
}); });
}
// 2. Update textboxes (chained from node state)
textBoxRefs.forEach((item) => {
const textBox = getItemByIdOrThrow(scene.textBoxes, item.id).value;
currentState = scene.updateTextBox(item.id, {
tile: CoordsUtils.add(textBox.tile, delta)
}, currentState);
}); });
}
// 3. Update rectangles (chained from textbox state)
rectangleRefs.forEach((item) => {
const rectangle = getItemByIdOrThrow(scene.rectangles, item.id).value;
currentState = scene.updateRectangle(item.id, {
from: CoordsUtils.add(rectangle.from, delta),
to: CoordsUtils.add(rectangle.to, delta)
}, currentState);
});
});
} }
// Handle non-item references (rectangles, textboxes, connector anchors) // Handle connector anchors separately (they have different update logic)
otherRefs.forEach((item) => { anchorRefs.forEach((item) => {
if (item.type === 'RECTANGLE') { const connector = getAnchorParent(item.id, scene.connectors);
// Skip rectangles if regular items are also being dragged
// This is because items use snap-to-grid logic, while rectangles move freely
// Moving them together would cause desynchronization
if (itemRefs.length > 0) return;
const rectangle = getItemByIdOrThrow(scene.rectangles, item.id).value; const newConnector = produce(connector, (draft) => {
const newFrom = CoordsUtils.add(rectangle.from, delta); const anchor = getItemByIdOrThrow(connector.anchors, item.id);
const newTo = CoordsUtils.add(rectangle.to, delta);
scene.updateRectangle(item.id, { from: newFrom, to: newTo }); const itemAtTile = getItemAtTile({ tile, scene });
} else if (item.type === 'TEXTBOX') {
const textBox = getItemByIdOrThrow(scene.textBoxes, item.id).value;
scene.updateTextBox(item.id, { switch (itemAtTile?.type) {
tile: CoordsUtils.add(textBox.tile, delta) case 'ITEM':
}); draft.anchors[anchor.index] = {
} else if (item.type === 'CONNECTOR_ANCHOR') { ...anchor.value,
const connector = getAnchorParent(item.id, scene.connectors); ref: {
item: itemAtTile.id
}
};
break;
case 'CONNECTOR_ANCHOR':
draft.anchors[anchor.index] = {
...anchor.value,
ref: {
anchor: itemAtTile.id
}
};
break;
default:
draft.anchors[anchor.index] = {
...anchor.value,
ref: {
tile
}
};
break;
}
});
const newConnector = produce(connector, (draft) => { scene.updateConnector(connector.id, newConnector);
const anchor = getItemByIdOrThrow(connector.anchors, item.id);
const itemAtTile = getItemAtTile({ tile, scene });
switch (itemAtTile?.type) {
case 'ITEM':
draft.anchors[anchor.index] = {
...anchor.value,
ref: {
item: itemAtTile.id
}
};
break;
case 'CONNECTOR_ANCHOR':
draft.anchors[anchor.index] = {
...anchor.value,
ref: {
anchor: itemAtTile.id
}
};
break;
default:
draft.anchors[anchor.index] = {
...anchor.value,
ref: {
tile
}
};
break;
}
});
scene.updateConnector(connector.id, newConnector);
}
}); });
}; };