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 (