editor: group resize handles for multi-select

This commit is contained in:
clawdbot 2026-01-29 10:38:52 +08:00
parent cdcc9d049b
commit 510d478be3

View File

@ -48,6 +48,25 @@ export function Canvas(props: CanvasProps) {
const bounds = useMemo(() => ({ w: props.screen.width, h: props.screen.height }), [props.screen.width, props.screen.height]); const bounds = useMemo(() => ({ w: props.screen.width, h: props.screen.height }), [props.screen.width, props.screen.height]);
const resizableSelected = useMemo(() => {
return props.screen.nodes.filter(
(n) => props.selectionIds.includes(n.id) && !n.locked && !n.hidden,
);
}, [props.screen.nodes, props.selectionIds]);
const groupResize = useMemo(() => {
if (resizableSelected.length < 2) return null;
const rects = resizableSelected.map((n) => n.rect);
const x1 = Math.min(...rects.map((r) => r.x));
const y1 = Math.min(...rects.map((r) => r.y));
const x2 = Math.max(...rects.map((r) => r.x + r.w));
const y2 = Math.max(...rects.map((r) => r.y + r.h));
return {
targetId: resizableSelected[0]!.id,
rect: { x: x1, y: y1, w: x2 - x1, h: y2 - y1 },
};
}, [resizableSelected]);
const clientToCanvas = useCallback((clientX: number, clientY: number) => { const clientToCanvas = useCallback((clientX: number, clientY: number) => {
const el = ref.current; const el = ref.current;
if (!el) return null; if (!el) return null;
@ -325,6 +344,7 @@ export function Canvas(props: CanvasProps) {
key={node.id} key={node.id}
node={node} node={node}
selected={props.selectionIds.includes(node.id)} selected={props.selectionIds.includes(node.id)}
selectionCount={props.selectionIds.length}
onPointerDown={(e) => { onPointerDown={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -368,6 +388,17 @@ export function Canvas(props: CanvasProps) {
/> />
))} ))}
{groupResize ? (
<GroupSelectionResizeBox
rect={groupResize.rect}
onPointerDown={(e, handle) => {
e.preventDefault();
e.stopPropagation();
props.onBeginResize(e, groupResize.targetId, handle);
}}
/>
) : null}
{box && ( {box && (
<div <div
style={{ style={{
@ -391,6 +422,7 @@ export function Canvas(props: CanvasProps) {
function NodeView(props: { function NodeView(props: {
node: WidgetNode; node: WidgetNode;
selected: boolean; selected: boolean;
selectionCount: number;
onPointerDown: (e: React.PointerEvent) => void; onPointerDown: (e: React.PointerEvent) => void;
onContextMenu: (e: React.MouseEvent) => void; onContextMenu: (e: React.MouseEvent) => void;
onResizePointerDown: (e: React.PointerEvent, handle: ResizeHandle) => void; onResizePointerDown: (e: React.PointerEvent, handle: ResizeHandle) => void;
@ -565,7 +597,35 @@ function NodeView(props: {
} }
})()} })()}
{props.selected && !node.locked && !node.hidden && <ResizeHandles onPointerDown={props.onResizePointerDown} />} {props.selected &&
props.selectionCount === 1 &&
!node.locked &&
!node.hidden && <ResizeHandles onPointerDown={props.onResizePointerDown} />}
</div>
);
}
function GroupSelectionResizeBox(props: {
rect: { x: number; y: number; w: number; h: number };
onPointerDown: (e: React.PointerEvent, handle: ResizeHandle) => void;
}) {
const r = props.rect;
return (
<div
style={{
position: 'absolute',
left: r.x,
top: r.y,
width: r.w,
height: r.h,
border: '1px solid rgba(24,144,255,0.9)',
boxShadow: '0 0 0 2px rgba(24,144,255,0.18)',
pointerEvents: 'none',
}}
>
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'auto' }}>
<ResizeHandles onPointerDown={props.onPointerDown} />
</div>
</div> </div>
); );
} }