editor: group-scale resize for multi-selection

This commit is contained in:
clawdbot 2026-01-29 05:03:44 +08:00
parent 5636de4154
commit d70c2b62ef

View File

@ -878,7 +878,10 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
}; };
for (const node of next.doc.screen.nodes) { for (const node of next.doc.screen.nodes) {
if (next.selection.ids.includes(node.id)) drag.snapshot.set(node.id, { ...node.rect }); if (!next.selection.ids.includes(node.id)) continue;
// Locked/hidden nodes can remain selected, but should not be transform targets.
if (node.locked || node.hidden) continue;
drag.snapshot.set(node.id, { ...node.rect });
} }
return { return {
@ -897,14 +900,80 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
const dy = Math.round((action.current.screenY - drag.startScreenY) / scale); const dy = Math.round((action.current.screenY - drag.startScreenY) / scale);
const handle = drag.handle; const handle = drag.handle;
const target = drag.snapshot.get(drag.targetId);
if (!target) return state;
const isTop = /t/.test(handle); const isTop = /t/.test(handle);
const isBottom = /b/.test(handle); const isBottom = /b/.test(handle);
const isLeft = /l/.test(handle); const isLeft = /l/.test(handle);
const isRight = /r/.test(handle); const isRight = /r/.test(handle);
const bounds = action.bounds;
// M2: multi-select resize scales the whole selection (unlocked + visible nodes only).
const isGroup = drag.snapshot.size > 1;
if (isGroup) {
const rects = Array.from(drag.snapshot.values());
const bbox0: Rect = {
x: Math.min(...rects.map((r) => r.x)),
y: Math.min(...rects.map((r) => r.y)),
w: Math.max(...rects.map((r) => r.x + r.w)) - Math.min(...rects.map((r) => r.x)),
h: Math.max(...rects.map((r) => r.y + r.h)) - Math.min(...rects.map((r) => r.y)),
};
// Resize the selection bbox using the same handle semantics as single-node resize.
let bbox1: Rect = {
x: bbox0.x + (isLeft ? dx : 0),
y: bbox0.y + (isTop ? dy : 0),
w: bbox0.w + (isLeft ? -dx : isRight ? dx : 0),
h: bbox0.h + (isTop ? -dy : isBottom ? dy : 0),
};
// No-op axes (e.g. 't' handle should not change width/x).
if (!isLeft && !isRight) {
bbox1.x = bbox0.x;
bbox1.w = bbox0.w;
}
if (!isTop && !isBottom) {
bbox1.y = bbox0.y;
bbox1.h = bbox0.h;
}
// Prevent degenerate scaling.
bbox1 = {
...bbox1,
w: Math.max(1, bbox1.w),
h: Math.max(1, bbox1.h),
};
bbox1 = clampRectToBounds(bbox1, bounds, 50);
const sx = bbox0.w ? bbox1.w / bbox0.w : 1;
const sy = bbox0.h ? bbox1.h / bbox0.h : 1;
const nodes = state.doc.screen.nodes.map((n) => {
const r0 = drag.snapshot.get(n.id);
if (!r0) return n;
const nextRect: Rect = {
x: Math.round(bbox1.x + (r0.x - bbox0.x) * sx),
y: Math.round(bbox1.y + (r0.y - bbox0.y) * sy),
w: Math.max(0, Math.round(r0.w * sx)),
h: Math.max(0, Math.round(r0.h * sy)),
};
return { ...n, rect: nextRect };
});
return {
...state,
doc: { screen: { ...state.doc.screen, nodes } },
canvas: { ...state.canvas, guides: { xs: [], ys: [] } },
};
}
const target = drag.snapshot.get(drag.targetId);
if (!target) return state;
const newH = target.h + (isTop ? -dy : isBottom ? dy : 0); const newH = target.h + (isTop ? -dy : isBottom ? dy : 0);
const newW = target.w + (isLeft ? -dx : isRight ? dx : 0); const newW = target.w + (isLeft ? -dx : isRight ? dx : 0);
@ -915,8 +984,6 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
h: Math.max(0, newH), h: Math.max(0, newH),
}; };
const bounds = action.bounds;
// goView-like: only snap when resizing a single selected node. // goView-like: only snap when resizing a single selected node.
const canSnap = state.selection.ids.length === 1; const canSnap = state.selection.ids.length === 1;
const movingId = drag.targetId; const movingId = drag.targetId;