From 2a2fe7493380b79c2e5a48915c899a1d6ed24766 Mon Sep 17 00:00:00 2001 From: Macbook Date: Mon, 15 Jun 2026 00:43:19 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EB=9E=B5=20=ED=8E=B8=EC=A7=91?= =?UTF-8?q?=EA=B8=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../strategyEditor/StrategyEditorCanvas.tsx | 16 ++-- frontend/src/utils/paletteDragSession.ts | 88 ++++++++++++++++--- 2 files changed, 83 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx b/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx index ab3da77..d60721c 100644 --- a/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx +++ b/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx @@ -1295,15 +1295,17 @@ function StrategyEditorCanvasInner({ return; } const { flowPos, anchorId } = resolveDropFromClientRef.current(ev.x, ev.y); - const previewKey = anchorId - ? `${anchorId}:${Math.round(flowPos.x)}:${Math.round(flowPos.y)}` - : ''; - if (previewKey === lastPreviewKeyRef.current) return; - lastPreviewAnchorRef.current = anchorId; - lastPreviewKeyRef.current = previewKey; if (anchorId) { + const sideKey = `${anchorId}:${Math.round(flowPos.x / 8)}:${Math.round(flowPos.y / 8)}`; + if (sideKey === lastPreviewKeyRef.current) return; + lastPreviewKeyRef.current = sideKey; + lastPreviewAnchorRef.current = anchorId; updatePreviewRef.current(anchorId, flowPos); - } else { + return; + } + if (lastPreviewAnchorRef.current != null) { + lastPreviewAnchorRef.current = null; + lastPreviewKeyRef.current = null; clearPreviewRef.current(); } return; diff --git a/frontend/src/utils/paletteDragSession.ts b/frontend/src/utils/paletteDragSession.ts index 0e656ff..5aea13d 100644 --- a/frontend/src/utils/paletteDragSession.ts +++ b/frontend/src/utils/paletteDragSession.ts @@ -4,6 +4,7 @@ import { elementsUnderDragPoint } from './flowScreenProjection'; import { bindWindowDrag, + clientXYFromNative, isPrimaryPointerButton, } from './pointerDrag'; import { isDesktop } from './platform'; @@ -47,13 +48,16 @@ let activePointerId: number | null = null; let activeTouchId: number | null = null; let overlayController: OverlayController | null = null; let overlayElement: HTMLElement | null = null; +let gestureCaptureEl: HTMLElement | null = null; let listeners = new Set(); let dropHandler: PaletteDropHandler | null = null; let moveRafId: number | null = null; let pendingMove: { x: number; y: number } | null = null; -let unbindActiveDrag: (() => void) | null = null; +let unbindActiveMove: (() => void) | null = null; +let unbindActiveEnd: (() => void) | null = null; let pendingPointerListeners: (() => void) | null = null; let lastDragClientXY: { x: number; y: number } | null = null; +let endListenersArmed = false; function hasCoarsePointer(): boolean { if (typeof window === 'undefined') return false; @@ -108,6 +112,12 @@ function dispatchEnd(xy: { x: number; y: number }, payload: PaletteDragPayload) function releasePointerCapture(pointerId: number | null) { if (pointerId == null) return; + try { + gestureCaptureEl?.releasePointerCapture(pointerId); + } catch { + /* ignore */ + } + gestureCaptureEl = null; try { overlayElement?.releasePointerCapture(pointerId); } catch { @@ -127,10 +137,17 @@ function clearPendingPointerListeners() { pendingPointerListeners = null; } +function unbindActiveDragListeners() { + unbindActiveMove?.(); + unbindActiveMove = null; + unbindActiveEnd?.(); + unbindActiveEnd = null; + endListenersArmed = false; +} + function cleanupDrag() { const pointerId = activePointerId; - unbindActiveDrag?.(); - unbindActiveDrag = null; + unbindActiveDragListeners(); clearPendingPointerListeners(); releasePointerCapture(pointerId); activePayload = null; @@ -177,10 +194,12 @@ export function notifyPaletteDragMove(clientX: number, clientY: number) { } export function notifyPaletteDragEnd(clientX: number, clientY: number) { + if (!endListenersArmed) return; finishDrag('end', { x: clientX, y: clientY }); } export function notifyPaletteDragCancel(clientX: number, clientY: number) { + if (!endListenersArmed) return; finishDrag('cancel', { x: clientX, y: clientY }); } @@ -203,24 +222,43 @@ function eventMatchesActiveInput(ev: Event): boolean { return true; } -function bindActiveDragWindowListeners(pointerId: number) { - unbindActiveDrag?.(); +function bindActiveDragMoveListeners(pointerId: number) { + unbindActiveMove?.(); - unbindActiveDrag = bindWindowDrag( + unbindActiveMove = bindWindowDrag( (xy, ev) => { if (!activePayload || activePointerId !== pointerId) return; if (!eventMatchesActiveInput(ev)) return; notifyPaletteDragMove(xy.x, xy.y); }, - (xy, ev) => { - if (!activePayload || activePointerId !== pointerId) return; - if (!eventMatchesActiveInput(ev)) return; - finishDrag('end', xy); - }, + () => { /* end 는 별도 등록 */ }, { preventTouchScroll: true, useCapture: true }, ); } +function bindActiveDragEndListeners(pointerId: number) { + unbindActiveEnd?.(); + + const onEnd = (ev: Event) => { + if (!endListenersArmed) return; + if (!activePayload || activePointerId !== pointerId) return; + if (!eventMatchesActiveInput(ev)) return; + const xy = clientXYFromNative(ev) ?? lastDragClientXY; + if (!xy) return; + finishDrag('end', xy); + }; + + window.addEventListener('pointerup', onEnd, true); + window.addEventListener('mouseup', onEnd, true); + window.addEventListener('touchend', onEnd, true); + + unbindActiveEnd = () => { + window.removeEventListener('pointerup', onEnd, true); + window.removeEventListener('mouseup', onEnd, true); + window.removeEventListener('touchend', onEnd, true); + }; +} + function captureOnOverlay(pointerId: number) { try { overlayElement?.setPointerCapture(pointerId); @@ -236,6 +274,11 @@ function captureOnOverlay(pointerId: number) { } } +function armDragEndListeners(pointerId: number) { + endListenersArmed = true; + bindActiveDragEndListeners(pointerId); +} + function beginDrag( payload: PaletteDragPayload, xy: { x: number; y: number }, @@ -250,14 +293,23 @@ function beginDrag( activePointerId = pointerId; activeTouchId = touchId; lastDragClientXY = { x: xy.x, y: xy.y }; + endListenersArmed = false; document.body.classList.add('se-palette-drag-active'); - bindActiveDragWindowListeners(pointerId); + bindActiveDragMoveListeners(pointerId); overlayController?.mount({ payload, pointerId, x: xy.x, y: xy.y }); - requestAnimationFrame(() => captureOnOverlay(pointerId)); - emit({ phase: 'start', x: xy.x, y: xy.y, payload }); + notifyPaletteDragMove(xy.x, xy.y); + + requestAnimationFrame(() => { + if (activePointerId !== pointerId) return; + captureOnOverlay(pointerId); + requestAnimationFrame(() => { + if (activePointerId !== pointerId) return; + armDragEndListeners(pointerId); + }); + }); } export function bindPaletteDragOverlayElement(el: HTMLElement | null) { @@ -315,6 +367,14 @@ export function handlePalettePointerDown( e.preventDefault(); + const sourceEl = e.currentTarget as HTMLElement; + try { + sourceEl.setPointerCapture(e.pointerId); + gestureCaptureEl = sourceEl; + } catch { + /* overlay/body capture later */ + } + startPaletteDragGesture( e.clientX, e.clientY,