전략 편집기 수정

This commit is contained in:
Macbook
2026-06-15 00:43:19 +09:00
parent 78d9dca870
commit 2a2fe74933
2 changed files with 83 additions and 21 deletions
@@ -1295,15 +1295,17 @@ function StrategyEditorCanvasInner({
return; return;
} }
const { flowPos, anchorId } = resolveDropFromClientRef.current(ev.x, ev.y); 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) { 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); updatePreviewRef.current(anchorId, flowPos);
} else { return;
}
if (lastPreviewAnchorRef.current != null) {
lastPreviewAnchorRef.current = null;
lastPreviewKeyRef.current = null;
clearPreviewRef.current(); clearPreviewRef.current();
} }
return; return;
+74 -14
View File
@@ -4,6 +4,7 @@
import { elementsUnderDragPoint } from './flowScreenProjection'; import { elementsUnderDragPoint } from './flowScreenProjection';
import { import {
bindWindowDrag, bindWindowDrag,
clientXYFromNative,
isPrimaryPointerButton, isPrimaryPointerButton,
} from './pointerDrag'; } from './pointerDrag';
import { isDesktop } from './platform'; import { isDesktop } from './platform';
@@ -47,13 +48,16 @@ let activePointerId: number | null = null;
let activeTouchId: number | null = null; let activeTouchId: number | null = null;
let overlayController: OverlayController | null = null; let overlayController: OverlayController | null = null;
let overlayElement: HTMLElement | null = null; let overlayElement: HTMLElement | null = null;
let gestureCaptureEl: HTMLElement | null = null;
let listeners = new Set<PaletteDragListener>(); let listeners = new Set<PaletteDragListener>();
let dropHandler: PaletteDropHandler | null = null; let dropHandler: PaletteDropHandler | null = null;
let moveRafId: number | null = null; let moveRafId: number | null = null;
let pendingMove: { x: number; y: 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 pendingPointerListeners: (() => void) | null = null;
let lastDragClientXY: { x: number; y: number } | null = null; let lastDragClientXY: { x: number; y: number } | null = null;
let endListenersArmed = false;
function hasCoarsePointer(): boolean { function hasCoarsePointer(): boolean {
if (typeof window === 'undefined') return false; if (typeof window === 'undefined') return false;
@@ -108,6 +112,12 @@ function dispatchEnd(xy: { x: number; y: number }, payload: PaletteDragPayload)
function releasePointerCapture(pointerId: number | null) { function releasePointerCapture(pointerId: number | null) {
if (pointerId == null) return; if (pointerId == null) return;
try {
gestureCaptureEl?.releasePointerCapture(pointerId);
} catch {
/* ignore */
}
gestureCaptureEl = null;
try { try {
overlayElement?.releasePointerCapture(pointerId); overlayElement?.releasePointerCapture(pointerId);
} catch { } catch {
@@ -127,10 +137,17 @@ function clearPendingPointerListeners() {
pendingPointerListeners = null; pendingPointerListeners = null;
} }
function unbindActiveDragListeners() {
unbindActiveMove?.();
unbindActiveMove = null;
unbindActiveEnd?.();
unbindActiveEnd = null;
endListenersArmed = false;
}
function cleanupDrag() { function cleanupDrag() {
const pointerId = activePointerId; const pointerId = activePointerId;
unbindActiveDrag?.(); unbindActiveDragListeners();
unbindActiveDrag = null;
clearPendingPointerListeners(); clearPendingPointerListeners();
releasePointerCapture(pointerId); releasePointerCapture(pointerId);
activePayload = null; activePayload = null;
@@ -177,10 +194,12 @@ export function notifyPaletteDragMove(clientX: number, clientY: number) {
} }
export function notifyPaletteDragEnd(clientX: number, clientY: number) { export function notifyPaletteDragEnd(clientX: number, clientY: number) {
if (!endListenersArmed) return;
finishDrag('end', { x: clientX, y: clientY }); finishDrag('end', { x: clientX, y: clientY });
} }
export function notifyPaletteDragCancel(clientX: number, clientY: number) { export function notifyPaletteDragCancel(clientX: number, clientY: number) {
if (!endListenersArmed) return;
finishDrag('cancel', { x: clientX, y: clientY }); finishDrag('cancel', { x: clientX, y: clientY });
} }
@@ -203,24 +222,43 @@ function eventMatchesActiveInput(ev: Event): boolean {
return true; return true;
} }
function bindActiveDragWindowListeners(pointerId: number) { function bindActiveDragMoveListeners(pointerId: number) {
unbindActiveDrag?.(); unbindActiveMove?.();
unbindActiveDrag = bindWindowDrag( unbindActiveMove = bindWindowDrag(
(xy, ev) => { (xy, ev) => {
if (!activePayload || activePointerId !== pointerId) return; if (!activePayload || activePointerId !== pointerId) return;
if (!eventMatchesActiveInput(ev)) return; if (!eventMatchesActiveInput(ev)) return;
notifyPaletteDragMove(xy.x, xy.y); notifyPaletteDragMove(xy.x, xy.y);
}, },
(xy, ev) => { () => { /* end 는 별도 등록 */ },
if (!activePayload || activePointerId !== pointerId) return;
if (!eventMatchesActiveInput(ev)) return;
finishDrag('end', xy);
},
{ preventTouchScroll: true, useCapture: true }, { 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) { function captureOnOverlay(pointerId: number) {
try { try {
overlayElement?.setPointerCapture(pointerId); overlayElement?.setPointerCapture(pointerId);
@@ -236,6 +274,11 @@ function captureOnOverlay(pointerId: number) {
} }
} }
function armDragEndListeners(pointerId: number) {
endListenersArmed = true;
bindActiveDragEndListeners(pointerId);
}
function beginDrag( function beginDrag(
payload: PaletteDragPayload, payload: PaletteDragPayload,
xy: { x: number; y: number }, xy: { x: number; y: number },
@@ -250,14 +293,23 @@ function beginDrag(
activePointerId = pointerId; activePointerId = pointerId;
activeTouchId = touchId; activeTouchId = touchId;
lastDragClientXY = { x: xy.x, y: xy.y }; lastDragClientXY = { x: xy.x, y: xy.y };
endListenersArmed = false;
document.body.classList.add('se-palette-drag-active'); document.body.classList.add('se-palette-drag-active');
bindActiveDragWindowListeners(pointerId); bindActiveDragMoveListeners(pointerId);
overlayController?.mount({ payload, pointerId, x: xy.x, y: xy.y }); overlayController?.mount({ payload, pointerId, x: xy.x, y: xy.y });
requestAnimationFrame(() => captureOnOverlay(pointerId));
emit({ phase: 'start', x: xy.x, y: xy.y, payload }); 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) { export function bindPaletteDragOverlayElement(el: HTMLElement | null) {
@@ -315,6 +367,14 @@ export function handlePalettePointerDown(
e.preventDefault(); e.preventDefault();
const sourceEl = e.currentTarget as HTMLElement;
try {
sourceEl.setPointerCapture(e.pointerId);
gestureCaptureEl = sourceEl;
} catch {
/* overlay/body capture later */
}
startPaletteDragGesture( startPaletteDragGesture(
e.clientX, e.clientX,
e.clientY, e.clientY,