work
This commit is contained in:
@@ -1,17 +1,18 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useDesktopContext } from './context';
|
||||
import { preventAll } from './events';
|
||||
import { clamp, formatTransform } from './math';
|
||||
import { preventAll, safeInvoke } from './events';
|
||||
import { clamp } from './math';
|
||||
import usePointerTap from '../ui/usePointerTap';
|
||||
import createDragPhysics, { MIN_TIMESTEP, MAX_TIMESTEP } from './dragPhysics';
|
||||
import { MIN_TIMESTEP, MAX_TIMESTEP, applyDomTransform } from './workspaceEngine';
|
||||
|
||||
const DRAG_HYSTERESIS_PX = 4;
|
||||
const DRAG_HYSTERESIS_SQUARED = DRAG_HYSTERESIS_PX * DRAG_HYSTERESIS_PX;
|
||||
const EDGE_COLLISION_THRESHOLD = 0.5;
|
||||
|
||||
const useDocumentDrag = () => {
|
||||
const useDocumentDrag = (options = {}) => {
|
||||
const {
|
||||
engine,
|
||||
layoutRef,
|
||||
dragTransformsRef,
|
||||
itemRefs,
|
||||
documentLookup,
|
||||
ensureDocumentSize,
|
||||
@@ -28,42 +29,22 @@ const useDocumentDrag = () => {
|
||||
onDocumentStackSelect,
|
||||
selectedDocumentIds,
|
||||
markLayoutDirty,
|
||||
} = useDesktopContext();
|
||||
} = options;
|
||||
|
||||
const markLayoutDirtyRef = useRef(markLayoutDirty);
|
||||
useEffect(() => {
|
||||
markLayoutDirtyRef.current = markLayoutDirty;
|
||||
}, [markLayoutDirty]);
|
||||
|
||||
const syncLayoutSnapshotRef = useRef(syncLayoutSnapshot);
|
||||
useEffect(() => {
|
||||
syncLayoutSnapshotRef.current = syncLayoutSnapshot;
|
||||
}, [syncLayoutSnapshot]);
|
||||
|
||||
const physicsRef = useRef(null);
|
||||
if (!physicsRef.current) {
|
||||
physicsRef.current = createDragPhysics({
|
||||
layoutRef,
|
||||
itemRefs,
|
||||
markLayoutDirtyRef,
|
||||
syncLayoutSnapshotRef,
|
||||
});
|
||||
}
|
||||
const {
|
||||
canvasPadding = 24,
|
||||
defaultCanvasWidth = 1024,
|
||||
defaultCanvasHeight = 680,
|
||||
debugDrag = false,
|
||||
} = settings || {};
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
physicsRef.current?.dispose?.();
|
||||
engine?.disposeInertiaAnimations?.();
|
||||
},
|
||||
[],
|
||||
[engine],
|
||||
);
|
||||
|
||||
const {
|
||||
applyTransform,
|
||||
finalizeGroupDrag,
|
||||
cancelInertiaAnimation,
|
||||
startInertiaAnimation,
|
||||
} = physicsRef.current;
|
||||
|
||||
const tapHandler = usePointerTap({
|
||||
delay: 220,
|
||||
onSingle: ({ data, event }) => {
|
||||
@@ -87,31 +68,77 @@ const useDocumentDrag = () => {
|
||||
openOverlayForDoc(data.docId, data.originInfo);
|
||||
},
|
||||
});
|
||||
|
||||
const dragStateRef = useRef(null);
|
||||
const { canvasPadding, defaultCanvasWidth, defaultCanvasHeight, debugDrag } = settings;
|
||||
|
||||
const finishDrag = useCallback(
|
||||
(pointerId) => {
|
||||
const state = dragStateRef.current;
|
||||
if (!state || state.pointerId !== pointerId) {
|
||||
const setDragTransform = useCallback((docKey, transform) => {
|
||||
if (!docKey) {
|
||||
return;
|
||||
}
|
||||
const map = dragTransformsRef?.current;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
map.set(String(docKey), transform);
|
||||
}, [dragTransformsRef]);
|
||||
|
||||
const clearDragTransforms = useCallback(() => {
|
||||
const map = dragTransformsRef?.current;
|
||||
if (!map || typeof map.clear !== 'function') {
|
||||
return;
|
||||
}
|
||||
map.clear();
|
||||
}, [dragTransformsRef]);
|
||||
|
||||
const commitActiveDragTransforms = useCallback((docIds = null) => {
|
||||
const map = dragTransformsRef?.current;
|
||||
if (!map || !map.size) {
|
||||
return;
|
||||
}
|
||||
const keys = Array.isArray(docIds) && docIds.length
|
||||
? docIds.map((id) => (id != null ? String(id) : null)).filter(Boolean)
|
||||
: Array.from(map.keys());
|
||||
keys.forEach((key) => {
|
||||
const transform = map.get(key);
|
||||
if (!transform) {
|
||||
return;
|
||||
}
|
||||
const capturedTarget = state.capturedTarget;
|
||||
if (capturedTarget && typeof capturedTarget.releasePointerCapture === 'function') {
|
||||
try {
|
||||
capturedTarget.releasePointerCapture(pointerId);
|
||||
} catch (error) {
|
||||
if (debugDrag) {
|
||||
console.warn('[desk] releasePointerCapture failed', error);
|
||||
const previous = layoutRef.current.get(key) || {};
|
||||
layoutRef.current.set(key, {
|
||||
...previous,
|
||||
centerX: transform.centerX,
|
||||
centerY: transform.centerY,
|
||||
rotation: transform.rotation ?? previous.rotation ?? 0,
|
||||
});
|
||||
});
|
||||
markLayoutDirty?.();
|
||||
}, [dragTransformsRef, layoutRef, markLayoutDirty]);
|
||||
|
||||
const finishDrag = useCallback(
|
||||
(pointerId, { shouldSync = false, clearTransforms = true } = {}) => {
|
||||
const state = dragStateRef.current;
|
||||
if (state && state.pointerId === pointerId) {
|
||||
const capturedTarget = state.capturedTarget;
|
||||
if (capturedTarget && typeof capturedTarget.releasePointerCapture === 'function') {
|
||||
try {
|
||||
capturedTarget.releasePointerCapture(pointerId);
|
||||
} catch (error) {
|
||||
if (debugDrag) {
|
||||
console.warn('[desk] releasePointerCapture failed', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dragStateRef.current = null;
|
||||
setDraggingId((current) => (current === state.docId ? null : current));
|
||||
syncLayoutSnapshot();
|
||||
setDraggingId(null);
|
||||
engine?.endDrag?.();
|
||||
if (clearTransforms) {
|
||||
clearDragTransforms();
|
||||
}
|
||||
if (shouldSync) {
|
||||
syncLayoutSnapshot(true);
|
||||
}
|
||||
},
|
||||
[debugDrag, setDraggingId, syncLayoutSnapshot],
|
||||
[clearDragTransforms, debugDrag, engine, setDraggingId, syncLayoutSnapshot],
|
||||
);
|
||||
|
||||
const handlePointerDown = useCallback(
|
||||
@@ -136,7 +163,7 @@ const useDocumentDrag = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelInertiaAnimation(docId);
|
||||
engine?.cancelInertiaAnimation?.(docKey);
|
||||
|
||||
const doc = documentLookup.get(docKey);
|
||||
if (!doc) {
|
||||
@@ -193,7 +220,7 @@ const useDocumentDrag = () => {
|
||||
if (isGroupDrag) {
|
||||
groupDocIds.forEach((id) => {
|
||||
if (id !== docKey) {
|
||||
cancelInertiaAnimation(id);
|
||||
engine?.cancelInertiaAnimation?.(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -300,11 +327,13 @@ const useDocumentDrag = () => {
|
||||
const hasStackSource = Array.isArray(stackDocIdsOption) && stackDocIdsOption.length > 1;
|
||||
|
||||
dragStateRef.current = {
|
||||
docId,
|
||||
docId: docKey,
|
||||
docKey,
|
||||
pointerId: event.pointerId,
|
||||
originCenterX: centerX,
|
||||
originCenterY: centerY,
|
||||
currentCenterX: centerX,
|
||||
currentCenterY: centerY,
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
rotation: entry?.rotation ?? 0,
|
||||
@@ -334,7 +363,26 @@ const useDocumentDrag = () => {
|
||||
stackReplace,
|
||||
};
|
||||
|
||||
setDraggingId(docId);
|
||||
const state = dragStateRef.current;
|
||||
|
||||
clearDragTransforms();
|
||||
state.groupItems.forEach((item) => {
|
||||
if (!item?.docId) {
|
||||
return;
|
||||
}
|
||||
setDragTransform(item.docId, {
|
||||
centerX: item.currentCenterX,
|
||||
centerY: item.currentCenterY,
|
||||
rotation: item.displayRotation ?? item.initialRotation ?? 0,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
scale: item.docId === state.docKey ? state.dragScale || 1 : 1,
|
||||
});
|
||||
});
|
||||
|
||||
engine?.beginDrag?.(state.groupDocIds);
|
||||
|
||||
setDraggingId(docKey);
|
||||
|
||||
if (isGroupDrag) {
|
||||
groupItems.forEach((item) => {
|
||||
@@ -344,22 +392,23 @@ const useDocumentDrag = () => {
|
||||
const node = itemRefs.current.get(item.docId);
|
||||
if (node) {
|
||||
item.displayRotation = item.initialRotation;
|
||||
node.style.transform = formatTransform(
|
||||
item.currentCenterX - item.width / 2,
|
||||
item.currentCenterY - item.height / 2,
|
||||
item.displayRotation,
|
||||
1,
|
||||
);
|
||||
applyDomTransform(node, {
|
||||
centerX: item.currentCenterX,
|
||||
centerY: item.currentCenterY,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
rotation: item.displayRotation ?? 0,
|
||||
scale: 1,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
}, [
|
||||
bringToFront,
|
||||
canvasPadding,
|
||||
cancelInertiaAnimation,
|
||||
containerRef,
|
||||
documentLookup,
|
||||
canvasPadding,
|
||||
containerRef,
|
||||
documentLookup,
|
||||
engine,
|
||||
ensureDocumentSize,
|
||||
layoutRef,
|
||||
resolveBaseMetrics,
|
||||
@@ -367,8 +416,9 @@ const useDocumentDrag = () => {
|
||||
setDraggingId,
|
||||
debugDrag,
|
||||
itemRefs,
|
||||
],
|
||||
);
|
||||
clearDragTransforms,
|
||||
setDragTransform,
|
||||
]);
|
||||
|
||||
const handlePointerMove = useCallback(
|
||||
(event) => {
|
||||
@@ -411,14 +461,11 @@ const useDocumentDrag = () => {
|
||||
}
|
||||
state.moved = true;
|
||||
if (
|
||||
state.isGroup
|
||||
&& !state.stackSelectionApplied
|
||||
!state.stackSelectionApplied
|
||||
&& Array.isArray(state.stackDocIds)
|
||||
&& state.stackDocIds.length > 0
|
||||
) {
|
||||
if (typeof onDocumentStackSelect === 'function') {
|
||||
onDocumentStackSelect(state.stackDocIds, event, { replace: state.stackReplace });
|
||||
}
|
||||
safeInvoke(onDocumentStackSelect, state.stackDocIds, event, { replace: state.stackReplace });
|
||||
state.stackSelectionApplied = true;
|
||||
}
|
||||
if (!state.groupElevated) {
|
||||
@@ -431,11 +478,8 @@ const useDocumentDrag = () => {
|
||||
return aZ - bZ;
|
||||
});
|
||||
|
||||
sortedGroup.forEach((id) => {
|
||||
bringToFront(id);
|
||||
});
|
||||
|
||||
bringToFront(state.docId);
|
||||
sortedGroup.forEach((id) => bringToFront(id));
|
||||
bringToFront(state.docKey);
|
||||
state.groupElevated = true;
|
||||
}
|
||||
}
|
||||
@@ -456,22 +500,8 @@ const useDocumentDrag = () => {
|
||||
const centerX = clamp(desiredCenterX, minCenterX, maxCenterX);
|
||||
const centerY = clamp(desiredCenterY, minCenterY, maxCenterY);
|
||||
|
||||
const primaryEntry = layoutRef.current.get(state.docKey) || {};
|
||||
const primaryRotation = state.rotation ?? primaryEntry.rotation ?? 0;
|
||||
layoutRef.current.set(state.docKey, {
|
||||
...primaryEntry,
|
||||
centerX,
|
||||
centerY,
|
||||
});
|
||||
const primaryNode = itemRefs.current.get(state.docId);
|
||||
if (primaryNode) {
|
||||
primaryNode.style.transform = formatTransform(
|
||||
centerX - docWidth / 2,
|
||||
centerY - docHeight / 2,
|
||||
primaryRotation,
|
||||
state.dragScale || 1,
|
||||
);
|
||||
}
|
||||
state.currentCenterX = centerX;
|
||||
state.currentCenterY = centerY;
|
||||
|
||||
state.groupItems.forEach((item) => {
|
||||
const isPrimary = item.docId === state.docKey;
|
||||
@@ -508,25 +538,18 @@ const useDocumentDrag = () => {
|
||||
item.displayRotation += (item.targetRotation - item.displayRotation) * rotationBlend;
|
||||
}
|
||||
|
||||
markLayoutDirty?.();
|
||||
|
||||
const entryItem = layoutRef.current.get(item.docId) || {};
|
||||
layoutRef.current.set(item.docId, {
|
||||
...entryItem,
|
||||
const payload = {
|
||||
centerX: item.currentCenterX,
|
||||
centerY: item.currentCenterY,
|
||||
rotation: item.displayRotation ?? entryItem.rotation ?? 0,
|
||||
});
|
||||
rotation: item.displayRotation ?? 0,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
scale: isPrimary ? state.dragScale || 1 : 1,
|
||||
};
|
||||
|
||||
applyTransform(
|
||||
item.docId,
|
||||
item.currentCenterX,
|
||||
item.currentCenterY,
|
||||
item.width,
|
||||
item.height,
|
||||
item.displayRotation ?? entryItem.rotation ?? 0,
|
||||
isPrimary ? state.dragScale || 1 : 1,
|
||||
);
|
||||
setDragTransform(item.docId, payload);
|
||||
const node = itemRefs.current.get(item.docId);
|
||||
applyDomTransform(node, payload);
|
||||
});
|
||||
|
||||
state.lastClientX = event.clientX;
|
||||
@@ -538,21 +561,15 @@ const useDocumentDrag = () => {
|
||||
? performance.now()
|
||||
: Date.now();
|
||||
|
||||
recalcVisibleDocIds();
|
||||
return;
|
||||
}
|
||||
if (state.locked) {
|
||||
if (debugDrag) {
|
||||
console.log('[desk] handlePointerMove: locked drag for doc', state.docId);
|
||||
console.log('[desk] handlePointerMove: locked drag for doc', state.docKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = layoutRef.current.get(state.docId);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaX = event.clientX - state.startX;
|
||||
const deltaY = event.clientY - state.startY;
|
||||
|
||||
@@ -572,7 +589,8 @@ const useDocumentDrag = () => {
|
||||
const pointerCanvasX = event.clientX - containerLeft;
|
||||
const pointerCanvasY = event.clientY - containerTop;
|
||||
|
||||
const rotationDeg = entry?.rotation ?? 0;
|
||||
const entry = layoutRef.current.get(state.docKey) || {};
|
||||
const rotationDeg = state.rotation ?? entry.rotation ?? 0;
|
||||
const rotationRad = (rotationDeg * Math.PI) / 180;
|
||||
const cosRot = Math.cos(rotationRad);
|
||||
const sinRot = Math.sin(rotationRad);
|
||||
@@ -581,8 +599,12 @@ const useDocumentDrag = () => {
|
||||
const rotatedOffsetY =
|
||||
state.localPointerOffsetX * sinRot + state.localPointerOffsetY * cosRot;
|
||||
|
||||
const previousCenterX = Number.isFinite(entry.centerX) ? entry.centerX : state.originCenterX;
|
||||
const previousCenterY = Number.isFinite(entry.centerY) ? entry.centerY : state.originCenterY;
|
||||
const previousCenterX = Number.isFinite(state.currentCenterX)
|
||||
? state.currentCenterX
|
||||
: state.originCenterX;
|
||||
const previousCenterY = Number.isFinite(state.currentCenterY)
|
||||
? state.currentCenterY
|
||||
: state.originCenterY;
|
||||
|
||||
const absCos = Math.abs(cosRot);
|
||||
const absSin = Math.abs(sinRot);
|
||||
@@ -614,7 +636,7 @@ const useDocumentDrag = () => {
|
||||
if (distanceSquared < DRAG_HYSTERESIS_SQUARED) {
|
||||
return;
|
||||
}
|
||||
bringToFront(state.docId);
|
||||
bringToFront(state.docKey);
|
||||
state.moved = true;
|
||||
}
|
||||
|
||||
@@ -631,28 +653,21 @@ const useDocumentDrag = () => {
|
||||
currentCenterY = previousCenterY;
|
||||
}
|
||||
|
||||
const updated = { ...entry, centerX: currentCenterX, centerY: currentCenterY };
|
||||
layoutRef.current.set(state.docId, updated);
|
||||
state.currentCenterX = currentCenterX;
|
||||
state.currentCenterY = currentCenterY;
|
||||
|
||||
applyTransform(
|
||||
state.docId,
|
||||
currentCenterX,
|
||||
currentCenterY,
|
||||
state.width,
|
||||
state.height,
|
||||
rotationDeg,
|
||||
state.dragScale || 1,
|
||||
);
|
||||
const transformPayload = {
|
||||
centerX: currentCenterX,
|
||||
centerY: currentCenterY,
|
||||
rotation: rotationDeg,
|
||||
width: state.width,
|
||||
height: state.height,
|
||||
scale: state.dragScale || 1,
|
||||
};
|
||||
|
||||
const primaryNode = itemRefs.current.get(state.docId);
|
||||
if (primaryNode) {
|
||||
primaryNode.style.transform = formatTransform(
|
||||
currentCenterX - state.width / 2,
|
||||
currentCenterY - state.height / 2,
|
||||
rotationDeg,
|
||||
state.dragScale || 1,
|
||||
);
|
||||
}
|
||||
setDragTransform(state.docKey, transformPayload);
|
||||
const primaryNode = itemRefs.current.get(state.docKey);
|
||||
applyDomTransform(primaryNode, transformPayload);
|
||||
|
||||
const offsetX = pointerCanvasX - currentCenterX;
|
||||
const offsetY = pointerCanvasY - currentCenterY;
|
||||
@@ -663,8 +678,6 @@ const useDocumentDrag = () => {
|
||||
const pointerInsideCard =
|
||||
Math.abs(pointerLocalX) <= halfWidth && Math.abs(pointerLocalY) <= halfHeight;
|
||||
|
||||
markLayoutDirty?.();
|
||||
|
||||
const currentTimestamp =
|
||||
typeof event.timeStamp === 'number' && Number.isFinite(event.timeStamp)
|
||||
? event.timeStamp
|
||||
@@ -696,9 +709,8 @@ const useDocumentDrag = () => {
|
||||
}
|
||||
|
||||
if (debugDrag) {
|
||||
console.log('[desk] handlePointerMove: moved doc', state.docId, 'to', currentCenterX, currentCenterY);
|
||||
console.log('[desk] handlePointerMove: moved doc', state.docKey, 'to', currentCenterX, currentCenterY);
|
||||
}
|
||||
recalcVisibleDocIds();
|
||||
},
|
||||
[
|
||||
bringToFront,
|
||||
@@ -710,11 +722,9 @@ const useDocumentDrag = () => {
|
||||
containerRef,
|
||||
layoutRef,
|
||||
itemRefs,
|
||||
applyTransform,
|
||||
recalcVisibleDocIds,
|
||||
debugDrag,
|
||||
onDocumentStackSelect,
|
||||
markLayoutDirty,
|
||||
setDragTransform,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -727,13 +737,15 @@ const useDocumentDrag = () => {
|
||||
}
|
||||
|
||||
if (state.isGroup) {
|
||||
finalizeGroupDrag(state);
|
||||
finishDrag(event.pointerId);
|
||||
engine?.finalizeGroupDrag?.(state);
|
||||
commitActiveDragTransforms(state.groupDocIds);
|
||||
finishDrag(event.pointerId, { shouldSync: true });
|
||||
recalcVisibleDocIds();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.moved) {
|
||||
commitActiveDragTransforms([state.docKey]);
|
||||
const inertiaState = {
|
||||
restRotation: state.restRotation,
|
||||
dynamicRotation: state.dynamicRotation,
|
||||
@@ -743,13 +755,13 @@ const useDocumentDrag = () => {
|
||||
height: state.height,
|
||||
dragScale: state.dragScale || 1,
|
||||
};
|
||||
const docId = state.docId;
|
||||
finishDrag(event.pointerId);
|
||||
startInertiaAnimation(docId, inertiaState);
|
||||
const docId = state.docKey;
|
||||
finishDrag(event.pointerId, { shouldSync: true });
|
||||
engine?.startInertiaAnimation?.(docId, inertiaState);
|
||||
return;
|
||||
}
|
||||
|
||||
const docId = state.docId;
|
||||
const docId = state.docKey;
|
||||
const metaPressed = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
||||
if (!metaPressed) {
|
||||
bringToFront(docId);
|
||||
@@ -771,10 +783,10 @@ const useDocumentDrag = () => {
|
||||
},
|
||||
[
|
||||
bringToFront,
|
||||
commitActiveDragTransforms,
|
||||
documentLookup,
|
||||
engine,
|
||||
finishDrag,
|
||||
finalizeGroupDrag,
|
||||
startInertiaAnimation,
|
||||
recalcVisibleDocIds,
|
||||
tapHandler,
|
||||
],
|
||||
@@ -785,12 +797,14 @@ const useDocumentDrag = () => {
|
||||
const state = dragStateRef.current;
|
||||
if (state && state.pointerId === event.pointerId && state.moved) {
|
||||
if (state.isGroup) {
|
||||
finalizeGroupDrag(state);
|
||||
finishDrag(event.pointerId);
|
||||
engine?.finalizeGroupDrag?.(state);
|
||||
commitActiveDragTransforms(state.groupDocIds);
|
||||
finishDrag(event.pointerId, { shouldSync: true });
|
||||
recalcVisibleDocIds();
|
||||
return;
|
||||
}
|
||||
|
||||
commitActiveDragTransforms([state.docKey]);
|
||||
const inertiaState = {
|
||||
restRotation: state.restRotation,
|
||||
dynamicRotation: state.dynamicRotation,
|
||||
@@ -800,14 +814,14 @@ const useDocumentDrag = () => {
|
||||
height: state.height,
|
||||
dragScale: state.dragScale || 1,
|
||||
};
|
||||
const docId = state.docId;
|
||||
finishDrag(event.pointerId);
|
||||
startInertiaAnimation(docId, inertiaState);
|
||||
const docId = state.docKey;
|
||||
finishDrag(event.pointerId, { shouldSync: true });
|
||||
engine?.startInertiaAnimation?.(docId, inertiaState);
|
||||
return;
|
||||
}
|
||||
finishDrag(event.pointerId);
|
||||
},
|
||||
[finalizeGroupDrag, finishDrag, recalcVisibleDocIds, startInertiaAnimation],
|
||||
[commitActiveDragTransforms, engine, finishDrag, recalcVisibleDocIds],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user