256 lines
7.5 KiB
JavaScript
256 lines
7.5 KiB
JavaScript
import { useCallback, useRef } from 'react';
|
|
import { useDesktopContext } from './context';
|
|
import { preventAll } from './events';
|
|
import { clamp, formatTransform } from './math';
|
|
|
|
const DRAG_HYSTERESIS_PX = 4;
|
|
const DRAG_HYSTERESIS_SQUARED = DRAG_HYSTERESIS_PX * DRAG_HYSTERESIS_PX;
|
|
|
|
const useDocumentDrag = () => {
|
|
const {
|
|
layoutRef,
|
|
itemRefs,
|
|
documentLookup,
|
|
ensureDocumentSize,
|
|
resolveBaseMetrics,
|
|
bringToFront,
|
|
setDraggingId,
|
|
syncLayoutSnapshot,
|
|
canvasSize,
|
|
openOverlayForDoc,
|
|
recalcVisibleDocIds,
|
|
settings,
|
|
} = useDesktopContext();
|
|
|
|
const dragStateRef = useRef(null);
|
|
const { canvasPadding, defaultCanvasWidth, defaultCanvasHeight, debugDrag } = settings;
|
|
|
|
const finishDrag = useCallback(
|
|
(pointerId) => {
|
|
const state = dragStateRef.current;
|
|
if (!state || state.pointerId !== pointerId) {
|
|
return;
|
|
}
|
|
const capturedTarget = state.capturedTarget;
|
|
if (capturedTarget && typeof capturedTarget.releasePointerCapture === 'function') {
|
|
try {
|
|
capturedTarget.releasePointerCapture(pointerId);
|
|
} catch (error) {
|
|
if (debugDrag) {
|
|
console.warn('[skeuo] releasePointerCapture failed', error);
|
|
}
|
|
}
|
|
}
|
|
dragStateRef.current = null;
|
|
setDraggingId((current) => (current === state.docId ? null : current));
|
|
syncLayoutSnapshot();
|
|
},
|
|
[debugDrag, setDraggingId, syncLayoutSnapshot],
|
|
);
|
|
|
|
const handlePointerDown = useCallback(
|
|
(event, docId) => {
|
|
if (debugDrag) {
|
|
console.log(
|
|
'[skeuo] handlePointerDown fired for doc',
|
|
docId,
|
|
'button',
|
|
event.button,
|
|
'pointerType',
|
|
event.pointerType,
|
|
'pointerId',
|
|
event.pointerId,
|
|
);
|
|
}
|
|
preventAll(event);
|
|
const docKey = docId != null ? String(docId) : null;
|
|
const doc = docKey ? documentLookup.get(docKey) : null;
|
|
const { width: docWidth, height: docHeight } = ensureDocumentSize(doc);
|
|
const { baseScale } = resolveBaseMetrics(doc, docWidth, docHeight);
|
|
const normalizedBaseScale =
|
|
Number.isFinite(baseScale) && baseScale > 0 ? baseScale : 1;
|
|
const defaultCenterX = canvasPadding + docWidth / 2;
|
|
const defaultCenterY = canvasPadding + docHeight / 2;
|
|
bringToFront(docId);
|
|
const entry = layoutRef.current.get(docId) || null;
|
|
const centerX = typeof entry?.centerX === 'number' ? entry.centerX : defaultCenterX;
|
|
const centerY = typeof entry?.centerY === 'number' ? entry.centerY : defaultCenterY;
|
|
|
|
if (entry && (entry.centerX !== centerX || entry.centerY !== centerY)) {
|
|
layoutRef.current.set(docId, { ...entry, centerX, centerY });
|
|
}
|
|
|
|
const capturedTarget = event.currentTarget instanceof HTMLElement ? event.currentTarget : null;
|
|
if (capturedTarget && typeof capturedTarget.setPointerCapture === 'function') {
|
|
try {
|
|
capturedTarget.setPointerCapture(event.pointerId);
|
|
} catch (error) {
|
|
if (debugDrag) {
|
|
console.warn('[skeuo] setPointerCapture failed', error);
|
|
}
|
|
}
|
|
}
|
|
dragStateRef.current = {
|
|
docId,
|
|
pointerId: event.pointerId,
|
|
originCenterX: centerX,
|
|
originCenterY: centerY,
|
|
startX: event.clientX,
|
|
startY: event.clientY,
|
|
rotation: entry?.rotation ?? 0,
|
|
moved: false,
|
|
locked: false,
|
|
width: docWidth,
|
|
height: docHeight,
|
|
dragScale: 1,
|
|
baseScale: normalizedBaseScale,
|
|
capturedTarget,
|
|
};
|
|
setDraggingId(docId);
|
|
},
|
|
[
|
|
bringToFront,
|
|
canvasPadding,
|
|
documentLookup,
|
|
ensureDocumentSize,
|
|
layoutRef,
|
|
resolveBaseMetrics,
|
|
setDraggingId,
|
|
debugDrag,
|
|
],
|
|
);
|
|
|
|
const handlePointerMove = useCallback(
|
|
(event) => {
|
|
const state = dragStateRef.current;
|
|
if (!state) {
|
|
if (debugDrag) {
|
|
console.log('[skeuo] handlePointerMove: no drag state for pointer', event.pointerId);
|
|
}
|
|
return;
|
|
}
|
|
if (state.pointerId !== event.pointerId) {
|
|
if (debugDrag) {
|
|
console.log(
|
|
'[skeuo] handlePointerMove: pointer mismatch expected',
|
|
state.pointerId,
|
|
'got',
|
|
event.pointerId,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
preventAll(event);
|
|
if (state.locked) {
|
|
if (debugDrag) {
|
|
console.log('[skeuo] handlePointerMove: locked drag for doc', state.docId);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const entry = layoutRef.current.get(state.docId);
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
|
|
const deltaX = event.clientX - state.startX;
|
|
const deltaY = event.clientY - state.startY;
|
|
const nextCenterX = state.originCenterX + deltaX;
|
|
const nextCenterY = state.originCenterY + deltaY;
|
|
|
|
const docWidth = state.width;
|
|
const docHeight = state.height;
|
|
const halfWidth = docWidth / 2;
|
|
const halfHeight = docHeight / 2;
|
|
const canvasWidth = canvasSize.width || defaultCanvasWidth;
|
|
const canvasHeight = canvasSize.height || defaultCanvasHeight;
|
|
const minCenterX = canvasPadding + halfWidth;
|
|
const maxCenterX = Math.max(minCenterX, canvasWidth - canvasPadding - halfWidth);
|
|
const minCenterY = canvasPadding + halfHeight;
|
|
const maxCenterY = Math.max(minCenterY, canvasHeight - canvasPadding - halfHeight);
|
|
const clampedCenterX = clamp(nextCenterX, minCenterX, maxCenterX);
|
|
const clampedCenterY = clamp(nextCenterY, minCenterY, maxCenterY);
|
|
|
|
if (!state.moved) {
|
|
const distanceSquared = deltaX * deltaX + deltaY * deltaY;
|
|
if (distanceSquared < DRAG_HYSTERESIS_SQUARED) {
|
|
return;
|
|
}
|
|
bringToFront(state.docId);
|
|
state.moved = true;
|
|
}
|
|
|
|
const updated = { ...entry, centerX: clampedCenterX, centerY: clampedCenterY };
|
|
layoutRef.current.set(state.docId, updated);
|
|
|
|
const node = itemRefs.current.get(state.docId);
|
|
if (node) {
|
|
node.style.transform = formatTransform(
|
|
clampedCenterX - state.width / 2,
|
|
clampedCenterY - state.height / 2,
|
|
state.rotation,
|
|
state.dragScale || 1,
|
|
);
|
|
}
|
|
if (debugDrag) {
|
|
console.log('[skeuo] handlePointerMove: moved doc', state.docId, 'to', clampedCenterX, clampedCenterY);
|
|
}
|
|
recalcVisibleDocIds();
|
|
},
|
|
[
|
|
bringToFront,
|
|
canvasPadding,
|
|
canvasSize.height,
|
|
canvasSize.width,
|
|
defaultCanvasHeight,
|
|
defaultCanvasWidth,
|
|
itemRefs,
|
|
layoutRef,
|
|
recalcVisibleDocIds,
|
|
debugDrag,
|
|
],
|
|
);
|
|
|
|
const handlePointerUp = useCallback(
|
|
(event) => {
|
|
const state = dragStateRef.current;
|
|
if (state && state.pointerId === event.pointerId) {
|
|
if (state.moved) {
|
|
finishDrag(event.pointerId);
|
|
return;
|
|
}
|
|
|
|
const docId = state.docId;
|
|
bringToFront(docId);
|
|
const originInfo = {
|
|
rotation: state.rotation || 0,
|
|
scale: state.baseScale || 1,
|
|
width: state.width,
|
|
height: state.height,
|
|
};
|
|
finishDrag(event.pointerId);
|
|
openOverlayForDoc(docId, originInfo);
|
|
return;
|
|
}
|
|
finishDrag(event.pointerId);
|
|
},
|
|
[bringToFront, finishDrag, openOverlayForDoc],
|
|
);
|
|
|
|
const handlePointerCancel = useCallback(
|
|
(event) => {
|
|
finishDrag(event.pointerId);
|
|
},
|
|
[finishDrag],
|
|
);
|
|
|
|
return {
|
|
handlePointerDown,
|
|
handlePointerMove,
|
|
handlePointerUp,
|
|
handlePointerCancel,
|
|
};
|
|
};
|
|
|
|
export default useDocumentDrag;
|