From 590c2fbf96fb4785367cf9d033752ec2d9a70726 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 11 Jul 2025 22:32:09 +0700 Subject: [PATCH] multiselect and line node resizing/moving fix --- .../app/src/pages/PageDraw/PaintSurface.tsx | 124 +++++++++++++++++- .../src/pages/PageDraw/stores/SceneState.ts | 24 +++- 2 files changed, 135 insertions(+), 13 deletions(-) diff --git a/frontend/apps/editor3d/app/src/pages/PageDraw/PaintSurface.tsx b/frontend/apps/editor3d/app/src/pages/PageDraw/PaintSurface.tsx index 92fe5ae691..bb2a1d7a7f 100644 --- a/frontend/apps/editor3d/app/src/pages/PageDraw/PaintSurface.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageDraw/PaintSurface.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useLayoutEffect } from "react"; +import React, { useState, useEffect, useLayoutEffect, useMemo } from "react"; import { Stage, Layer, @@ -94,6 +94,30 @@ export const PaintSurface = ({ } | null>(null); const [isMiddleMousePressed, setIsMiddleMousePressed] = useState(false); + const boundingBox = useMemo(() => { + if (selectedNodeIds.length < 2 || !stageRef.current) return null; + + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + + selectedNodeIds.forEach((id) => { + const konvaNode = stageRef.current!.findOne(`#${id}`); + if (!konvaNode) return; + + const rect = konvaNode.getClientRect({ relativeTo: stageRef.current }); + minX = Math.min(minX, rect.x); + minY = Math.min(minY, rect.y); + maxX = Math.max(maxX, rect.x + rect.width); + maxY = Math.max(maxY, rect.y + rect.height); + }); + + if (minX === Infinity) return null; + return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedNodeIds, store.nodes, store.lineNodes, stageRef]); + useLayoutEffect(() => { const updateDimensions = () => { if (containerRef.current) { @@ -240,6 +264,8 @@ export const PaintSurface = ({ opacity: opacity, locked: false, zIndex: store.lineNodes.length, + x: 0, + y: 0, }; store.selectNode(null); store.addLineNode(newLineNode, false); @@ -1300,9 +1326,21 @@ export const PaintSurface = ({ const isLineNode = store.lineNodes.find((ln) => ln.id === nodeId); if (isLineNode) { - store.updateLineNode(nodeId, commonNodeUpdates, true); + store.updateLineNode( + nodeId, + { + x: finalX, + y: finalY, + rotation: finalRotation, + scaleX: finalScaleX, + scaleY: finalScaleY, + offsetX: finalOffsetX, + offsetY: finalOffsetY, + }, + false, + ); } else { - store.updateNode(nodeId, commonNodeUpdates, true); + store.updateNode(nodeId, { ...commonNodeUpdates }, false); } }} /> @@ -1526,10 +1564,25 @@ export const PaintSurface = ({ ); if (isLineNode) { - // Lines keep their points; we don't change their width/height semantics - store.updateLineNode(nodeId, commonNodeUpdates, true); + store.updateLineNode( + nodeId, + { + x: finalX, + y: finalY, + rotation: finalRotation, + scaleX: finalScaleX, + scaleY: finalScaleY, + offsetX: finalOffsetX, + offsetY: finalOffsetY, + }, + false, + ); } else { - store.updateNode(nodeId, commonNodeUpdates, true); + store.updateNode( + nodeId, + { ...commonNodeUpdates }, + false, + ); } } }); @@ -1537,6 +1590,65 @@ export const PaintSurface = ({ }} /> )} + {/* Draggable transparent rect to move the whole selection together */} + {selectedNodeIds.length > 1 && boundingBox && ( + { + const tgt = e.target as Konva.Rect & { + lastX?: number; + lastY?: number; + }; + tgt.lastX = tgt.x(); + tgt.lastY = tgt.y(); + }} + onDragMove={(e) => { + const tgt = e.target as Konva.Rect & { + lastX?: number; + lastY?: number; + }; + const dx = tgt.x() - (tgt.lastX ?? tgt.x()); + const dy = tgt.y() - (tgt.lastY ?? tgt.y()); + + tgt.lastX = tgt.x(); + tgt.lastY = tgt.y(); + + selectedNodeIds.forEach((id) => { + const lineNode = store.lineNodes.find( + (ln) => ln.id === id, + ); + if (lineNode) { + store.moveLineNode(id, dx, dy); + } else { + const currentNode = store.nodes.find( + (n) => n.id === id, + ); + if (currentNode) { + // Move each shape individually without triggering extra multi-select propagation + store.moveNode( + id, + currentNode.x + dx, + currentNode.y + dy, + undefined, + undefined, + false, + ); + } + } + }); + }} + onDragEnd={() => { + store.saveState(); + }} + listening={true} + strokeWidth={0} + /> + )} diff --git a/frontend/apps/editor3d/app/src/pages/PageDraw/stores/SceneState.ts b/frontend/apps/editor3d/app/src/pages/PageDraw/stores/SceneState.ts index a608aed255..6149500016 100644 --- a/frontend/apps/editor3d/app/src/pages/PageDraw/stores/SceneState.ts +++ b/frontend/apps/editor3d/app/src/pages/PageDraw/stores/SceneState.ts @@ -165,7 +165,12 @@ interface SceneState { updates: Partial, shouldSaveState: boolean, ) => void; - moveLineNode: (id: string, dx: number, dy: number) => void; + moveLineNode: ( + id: string, + dx: number, + dy: number, + shouldSaveState?: boolean, + ) => void; // Add a specific method for file uploads createImageFromFile: ( @@ -683,23 +688,28 @@ export const useSceneStore = create((set, get) => ({ } }, - moveLineNode: (id: string, dx: number, dy: number) => { + moveLineNode: ( + id: string, + dx: number, + dy: number, + shouldSaveState: boolean = false, + ) => { set((state) => { const newLineNodes = state.lineNodes.map((node) => { if (node.id === id) { return { ...node, - points: node.points.map((point, index) => { - // Even indices are x coordinates, odd are y - return index % 2 === 0 ? point + dx : point + dy; - }), + x: (node.x ?? 0) + dx, + y: (node.y ?? 0) + dy, }; } return node; }); return { lineNodes: newLineNodes }; }); - get().saveState(); + if (shouldSaveState) { + get().saveState(); + } }, // Add a specific method for file uploads