From e5f6d87f392bfdb1ac044f636917a6a4a899c804 Mon Sep 17 00:00:00 2001 From: Tanish Manku Date: Fri, 13 Mar 2026 14:29:34 -0700 Subject: [PATCH] 3D model overlay UX improvements: button centering, WYSIWYG rotation, drop sizing - Edit 3D button now renders at node center (AABB center) with translate-x/y-1/2, eliminating rotation misalignment - Model3DOverlay card applies CSS rotate matching node.rotation (transform-origin 0 0) for WYSIWYG preview when the Konva node is rotated - Dropped 3D models now centered at drop point and capped at 25% of canvas dimensions to prevent oversized placements; nativeWidth/nativeHeight preserved in params for correct re-editing --- .../app/src/pages/PageDraw/PageDraw.tsx | 61 ++++++++++++------- .../PageDraw/components/Model3DOverlay.tsx | 2 + 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/frontend/apps/artcraft/app/src/pages/PageDraw/PageDraw.tsx b/frontend/apps/artcraft/app/src/pages/PageDraw/PageDraw.tsx index 7b654f3b53..e9a897ce3c 100644 --- a/frontend/apps/artcraft/app/src/pages/PageDraw/PageDraw.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageDraw/PageDraw.tsx @@ -71,10 +71,12 @@ const Edit3DButton = memo(function Edit3DButton({ }: Edit3DButtonProps) { const [, forceUpdate] = useState(0); const bump = useCallback(() => forceUpdate((t) => t + 1), []); + const [interacting, setInteracting] = useState(false); - // Attach Konva event listeners so the button repositions on every stage - // pan/zoom and on every transformer handle move (before onTransformEnd fires). - // Re-runs when nodeId changes so fresh transformers are picked up. + // Attach Konva event listeners: + // - Stage pan/zoom → bump() to reposition + // - Transformer start/end → hide/show button to avoid jitter + // - Node drag start/end → hide/show button useEffect(() => { const stage = stageRef.current; if (!stage?.on) return; @@ -84,35 +86,45 @@ const Edit3DButton = memo(function Edit3DButton({ `dragmove${ns} xChange${ns} yChange${ns} scaleXChange${ns} scaleYChange${ns}`, bump, ); + const transformers = stage.find("Transformer"); - transformers.forEach((tr) => tr.on(`transform${ns}`, bump)); + transformers.forEach((tr) => { + tr.on(`transformstart${ns}`, () => setInteracting(true)); + tr.on(`transformend${ns}`, () => setInteracting(false)); + }); + + const konvaNode = stage.findOne("#" + nodeId); + if (konvaNode) { + konvaNode.on(`dragstart${ns}`, () => setInteracting(true)); + konvaNode.on(`dragend${ns}`, () => setInteracting(false)); + } return () => { - stage.off(".edit3dbtn"); - transformers.forEach((tr) => tr.off(".edit3dbtn")); + stage.off(ns); + transformers.forEach((tr) => tr.off(ns)); + konvaNode?.off(ns); }; }, [stageRef, bump, nodeId]); + if (interacting) return null; + const stage = stageRef.current; if (!stage?.container) return null; - const stageRect = stage.container().getBoundingClientRect(); - const sx = stage.scaleX(); - const sy = stage.scaleY(); - - // Read live Konva node geometry so the button tracks transformer handles - // in real-time (the store value is stale until onTransformEnd commits it). const konvaNode = stage.findOne("#" + nodeId) as Konva.Shape | undefined; if (!konvaNode) return null; - const screenLeft = stageRect.left + stage.x() + konvaNode.x() * sx; - const screenTop = stageRect.top + stage.y() + konvaNode.y() * sy; - const screenW = konvaNode.width() * konvaNode.scaleX() * sx; + // getClientRect() returns the AABB in stage-container coords after all + // transforms (rotation, scale, stage pan/zoom) — no manual math needed. + const clientRect = konvaNode.getClientRect(); + const stageContainerRect = stage.container().getBoundingClientRect(); + const btnLeft = stageContainerRect.left + clientRect.x + clientRect.width / 2; + const btnTop = stageContainerRect.top + clientRect.y + clientRect.height / 2; return (