From 12c556a95e57372898fa3124fc6e97053a597628 Mon Sep 17 00:00:00 2001 From: Macbook Date: Mon, 15 Jun 2026 01:38:52 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EB=9E=B5=ED=8E=B8=EC=A7=91=EA=B8=B0?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/strategyEditor/PaletteChip.tsx | 8 +- .../SidewaysFilterPaletteTab.tsx | 6 +- .../strategyEditor/StrategyEditorCanvas.tsx | 16 ++-- .../strategyEditor/StrategyListEditor.tsx | 1 - frontend/src/utils/paletteDragSession.ts | 80 ++++++++++++++++++- 5 files changed, 92 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/strategyEditor/PaletteChip.tsx b/frontend/src/components/strategyEditor/PaletteChip.tsx index 6641f72..aba1de6 100644 --- a/frontend/src/components/strategyEditor/PaletteChip.tsx +++ b/frontend/src/components/strategyEditor/PaletteChip.tsx @@ -3,9 +3,11 @@ import { handlePaletteMouseDown, handlePalettePointerDown, handlePaletteTouchStart, + endHtmlDragMouseTracking, markPaletteHtmlDragEnd, needsPointerPaletteDrag, shouldSuppressPaletteClick, + startHtmlDragMouseTracking, writePaletteHtmlDragData, type PaletteDragPayload, } from '../../utils/paletteDragSession'; @@ -55,11 +57,13 @@ export default function PaletteChip({ e.preventDefault(); return; } - writePaletteHtmlDragData(e, buildPayload()); + const payload = buildPayload(); + writePaletteHtmlDragData(e, payload); + startHtmlDragMouseTracking(e.clientX, e.clientY, payload); }; const onDragEnd = () => { - markPaletteHtmlDragEnd(); + endHtmlDragMouseTracking(); }; const onPointerDown = (e: React.PointerEvent) => { diff --git a/frontend/src/components/strategyEditor/SidewaysFilterPaletteTab.tsx b/frontend/src/components/strategyEditor/SidewaysFilterPaletteTab.tsx index 8549a30..6165f94 100644 --- a/frontend/src/components/strategyEditor/SidewaysFilterPaletteTab.tsx +++ b/frontend/src/components/strategyEditor/SidewaysFilterPaletteTab.tsx @@ -4,8 +4,9 @@ import type { DefType } from '../../utils/strategyEditorShared'; import { handlePalettePointerDown, handlePaletteTouchStart, - markPaletteHtmlDragEnd, + endHtmlDragMouseTracking, needsPointerPaletteDrag, + startHtmlDragMouseTracking, writePaletteHtmlDragData, type PaletteDragPayload, } from '../../utils/paletteDragSession'; @@ -88,11 +89,12 @@ function SidewaysFilterChip({ label: item.label, }; writePaletteHtmlDragData(e, payload as PaletteDragPayload); + startHtmlDragMouseTracking(e.clientX, e.clientY, payload as PaletteDragPayload); }; const onDragEnd = () => { hideHint(); - markPaletteHtmlDragEnd(); + endHtmlDragMouseTracking(); }; const onPointerDown = (e: React.PointerEvent) => { diff --git a/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx b/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx index 9e2ca7f..1373f55 100644 --- a/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx +++ b/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx @@ -1308,32 +1308,26 @@ function StrategyEditorCanvasInner({ if (anchorId) { updatePreviewRef.current(anchorId, flowPos); lastPreviewAnchorRef.current = anchorId; - return; - } - if (lastPreviewAnchorRef.current != null) { - lastPreviewAnchorRef.current = null; - lastPreviewKeyRef.current = null; - clearPreviewRef.current(); } }; + document.addEventListener('dragover', onDocDragOver, { passive: false }); + return () => document.removeEventListener('dragover', onDocDragOver); + }, []); + + useEffect(() => { const onDocDragEnd = () => { lastPreviewAnchorRef.current = null; lastPreviewKeyRef.current = null; clearPreviewRef.current(); markPaletteHtmlDragEnd(); }; - document.addEventListener('dragover', onDocDragOver, { passive: false }); - document.addEventListener('drag', onDocDragOver); document.addEventListener('dragend', onDocDragEnd); return () => { - document.removeEventListener('dragover', onDocDragOver); - document.removeEventListener('drag', onDocDragOver); document.removeEventListener('dragend', onDocDragEnd); }; }, []); useEffect(() => { - if (!needsPointerPaletteDrag()) return; return subscribePaletteDrag((ev: PaletteDragEvent) => { if (ev.phase === 'move') { const { flowPos, anchorId } = resolveDropFromClientRef.current(ev.x, ev.y); diff --git a/frontend/src/components/strategyEditor/StrategyListEditor.tsx b/frontend/src/components/strategyEditor/StrategyListEditor.tsx index b2ca2cb..fda05d0 100644 --- a/frontend/src/components/strategyEditor/StrategyListEditor.tsx +++ b/frontend/src/components/strategyEditor/StrategyListEditor.tsx @@ -493,7 +493,6 @@ export default function StrategyListEditor({ }, []); useEffect(() => { - if (!needsPointerPaletteDrag()) return; return subscribePaletteDrag(ev => { if (ev.phase === 'move') { if (!isOverListEditor(ev.x, ev.y)) { diff --git a/frontend/src/utils/paletteDragSession.ts b/frontend/src/utils/paletteDragSession.ts index eb8b630..0975c93 100644 --- a/frontend/src/utils/paletteDragSession.ts +++ b/frontend/src/utils/paletteDragSession.ts @@ -7,7 +7,7 @@ import { bindWindowDrag, isPrimaryPointerButton, } from './pointerDrag'; -import { isDesktop } from './platform'; +import { isDesktop, isMacDesktop } from './platform'; export type PaletteDragPayload = Record & { type: string; @@ -54,6 +54,7 @@ let moveRafId: number | null = null; let pendingMove: { x: number; y: number } | null = null; let unbindGesture: (() => void) | null = null; let lastDragClientXY: { x: number; y: number } | null = null; +let htmlDragMouseTrackUnbind: (() => void) | null = null; let htmlPaletteDragActive = false; let lastHtmlDragClientXY: { x: number; y: number } | null = null; @@ -80,6 +81,7 @@ function installHtmlPaletteDragGlobals() { }); window.addEventListener('dragend', () => { + stopHtmlDragMouseTracking(); htmlPaletteDragActive = false; lastHtmlDragClientXY = null; }); @@ -96,8 +98,9 @@ function hasCoarsePointer(): boolean { } } -/** HTML5 DnD 대신 pointer/touch overlay 드래그 (Tauri Desktop 전체 — macOS WKWebView HTML5 DnD 불안정) */ +/** HTML5 DnD 대신 pointer overlay (Windows Desktop 등). macOS WKWebView는 HTML5 + mousemove preview */ export function needsPointerPaletteDrag(): boolean { + if (isMacDesktop()) return false; return isDesktop() || hasCoarsePointer(); } @@ -277,8 +280,46 @@ function startPaletteDragGesture( finishDrag('end', xy); } }, - { preventTouchScroll: true, useCapture: isDesktop() }, + { preventTouchScroll: true }, ); + + if (gestureCaptureEl) { + const el = gestureCaptureEl; + const pid = pointerId; + const onElMove = (ev: PointerEvent) => { + if (ev.pointerId !== pid) return; + const xy = { x: ev.clientX, y: ev.clientY }; + if (!dragging) { + const dx = xy.x - startX; + const dy = xy.y - startY; + if (Math.hypot(dx, dy) < DRAG_THRESHOLD_PX) return; + dragging = true; + mountDragOverlay(payload, xy, pointerId); + return; + } + notifyPaletteDragMove(xy.x, xy.y); + }; + const onElEnd = (ev: PointerEvent) => { + if (ev.pointerId !== pid) return; + el.removeEventListener('pointermove', onElMove); + el.removeEventListener('pointerup', onElEnd); + el.removeEventListener('pointercancel', onElEnd); + unbindGestureListeners(); + if (dragging && activePayload) { + finishDrag('end', { x: ev.clientX, y: ev.clientY }); + } + }; + el.addEventListener('pointermove', onElMove); + el.addEventListener('pointerup', onElEnd); + el.addEventListener('pointercancel', onElEnd); + const windowUnbind = unbindGesture; + unbindGesture = () => { + el.removeEventListener('pointermove', onElMove); + el.removeEventListener('pointerup', onElEnd); + el.removeEventListener('pointercancel', onElEnd); + windowUnbind?.(); + }; + } } export function handlePalettePointerDown( @@ -336,12 +377,45 @@ export function handlePaletteTouchStart( startPaletteDragGesture(touch.clientX, touch.clientY, touch.identifier, payload); } +function stopHtmlDragMouseTracking(): void { + htmlDragMouseTrackUnbind?.(); + htmlDragMouseTrackUnbind = null; +} + +/** HTML5 drag 중 mousemove 로 preview 좌표 추적 (macOS WKWebView — dragover 좌표/hit-test 불안정) */ +export function startHtmlDragMouseTracking( + clientX: number, + clientY: number, + payload: PaletteDragPayload, +): void { + stopHtmlDragMouseTracking(); + if (clientX !== 0 || clientY !== 0) { + lastHtmlDragClientXY = { x: clientX, y: clientY }; + } + htmlDragMouseTrackUnbind = bindWindowDrag( + (xy) => { + lastHtmlDragClientXY = xy; + emit({ phase: 'move', x: xy.x, y: xy.y, payload }); + }, + () => { + /* native HTML5 drag 중 mouseup 은 dragend 전에 올 수 있음 — dragend 에서 정리 */ + }, + { preventTouchScroll: false }, + ); +} + +export function endHtmlDragMouseTracking(): void { + stopHtmlDragMouseTracking(); + markPaletteHtmlDragEnd(); +} + export function markPaletteHtmlDragStart(): void { htmlPaletteDragActive = true; lastHtmlDragClientXY = null; } export function markPaletteHtmlDragEnd(): void { + stopHtmlDragMouseTracking(); htmlPaletteDragActive = false; lastHtmlDragClientXY = null; }