fix
This commit is contained in:
@@ -1930,7 +1930,15 @@ const DesktopWorkspaceView = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
if (typeof onDocumentPointerSelect === 'function') {
|
const alreadySelected = selectedDocumentIds.includes(doc.id);
|
||||||
|
if (
|
||||||
|
typeof onDocumentPointerSelect === 'function'
|
||||||
|
&& (!alreadySelected
|
||||||
|
|| event.metaKey
|
||||||
|
|| event.ctrlKey
|
||||||
|
|| event.shiftKey
|
||||||
|
|| event.altKey)
|
||||||
|
) {
|
||||||
onDocumentPointerSelect(doc.id, event);
|
onDocumentPointerSelect(doc.id, event);
|
||||||
}
|
}
|
||||||
handlePointerDown(event, doc.id);
|
handlePointerDown(event, doc.id);
|
||||||
|
|||||||
@@ -14,13 +14,6 @@ const ANGULAR_DAMPING = 11;
|
|||||||
const TORQUE_TO_ACCELERATION = 0.006;
|
const TORQUE_TO_ACCELERATION = 0.006;
|
||||||
const SETTLE_ANGULAR_VELOCITY = 1.2;
|
const SETTLE_ANGULAR_VELOCITY = 1.2;
|
||||||
const EDGE_COLLISION_THRESHOLD = 0.5;
|
const EDGE_COLLISION_THRESHOLD = 0.5;
|
||||||
const EDGE_ALIGNMENT_STIFFNESS = 30;
|
|
||||||
const BASE_TORQUE_FACTOR = 0.4;
|
|
||||||
const EDGE_COLLISION_TORQUE_FACTOR = 0.08;
|
|
||||||
const EDGE_ALIGNMENT_TORQUE_MULTIPLIER = 35;
|
|
||||||
const EDGE_COLLISION_EXTRA_DAMPING = 4;
|
|
||||||
const EDGE_REST_REALIGN_RATE = 10;
|
|
||||||
const EDGE_ALIGNMENT_EPSILON = 0.15;
|
|
||||||
|
|
||||||
const useDocumentDrag = () => {
|
const useDocumentDrag = () => {
|
||||||
const {
|
const {
|
||||||
@@ -39,8 +32,62 @@ const useDocumentDrag = () => {
|
|||||||
containerRef,
|
containerRef,
|
||||||
onDocumentOpen,
|
onDocumentOpen,
|
||||||
onInspectDocument,
|
onInspectDocument,
|
||||||
|
selectedDocumentIds,
|
||||||
} = useDesktopContext();
|
} = useDesktopContext();
|
||||||
|
|
||||||
|
const applyTransform = useCallback(
|
||||||
|
(docId, centerX, centerY, width, height, rotation, scale = 1) => {
|
||||||
|
const node = itemRefs.current.get(docId);
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.style.transform = formatTransform(
|
||||||
|
centerX - width / 2,
|
||||||
|
centerY - height / 2,
|
||||||
|
rotation,
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[itemRefs],
|
||||||
|
);
|
||||||
|
|
||||||
|
const finalizeGroupDrag = useCallback(
|
||||||
|
(dragState) => {
|
||||||
|
if (!dragState?.groupItems) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragState.groupItems.forEach((item) => {
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryItem = layoutRef.current.get(item.docId) || {};
|
||||||
|
const centerX = item.currentCenterX ?? entryItem.centerX ?? dragState.originCenterX;
|
||||||
|
const centerY = item.currentCenterY ?? entryItem.centerY ?? dragState.originCenterY;
|
||||||
|
const rotation = item.displayRotation ?? entryItem.rotation ?? 0;
|
||||||
|
|
||||||
|
layoutRef.current.set(item.docId, {
|
||||||
|
...entryItem,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
rotation,
|
||||||
|
});
|
||||||
|
|
||||||
|
applyTransform(
|
||||||
|
item.docId,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
item.width,
|
||||||
|
item.height,
|
||||||
|
rotation,
|
||||||
|
item.docId === dragState.docKey ? dragState.dragScale || 1 : 1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[applyTransform, layoutRef],
|
||||||
|
);
|
||||||
|
|
||||||
const tapHandler = usePointerTap({
|
const tapHandler = usePointerTap({
|
||||||
delay: 220,
|
delay: 220,
|
||||||
onSingle: ({ data, event }) => {
|
onSingle: ({ data, event }) => {
|
||||||
@@ -200,11 +247,11 @@ const useDocumentDrag = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handlePointerDown = useCallback(
|
const handlePointerDown = useCallback(
|
||||||
(event, docId) => {
|
(event, docIdInput) => {
|
||||||
if (debugDrag) {
|
if (debugDrag) {
|
||||||
console.log(
|
console.log(
|
||||||
'[desk] handlePointerDown fired for doc',
|
'[desk] handlePointerDown fired for doc',
|
||||||
docId,
|
docIdInput,
|
||||||
'button',
|
'button',
|
||||||
event.button,
|
event.button,
|
||||||
'pointerType',
|
'pointerType',
|
||||||
@@ -214,25 +261,74 @@ const useDocumentDrag = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
preventAll(event);
|
preventAll(event);
|
||||||
cancelInertiaAnimation(docId);
|
|
||||||
|
const docId = docIdInput != null ? docIdInput : null;
|
||||||
const docKey = docId != null ? String(docId) : null;
|
const docKey = docId != null ? String(docId) : null;
|
||||||
const doc = docKey ? documentLookup.get(docKey) : null;
|
if (!docKey) {
|
||||||
const { width: docWidth, height: docHeight } = ensureDocumentSize(doc);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelInertiaAnimation(docId);
|
||||||
|
|
||||||
|
const doc = documentLookup.get(docKey);
|
||||||
|
if (!doc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectionIds = Array.isArray(selectedDocumentIds)
|
||||||
|
? selectedDocumentIds.map((id) => String(id))
|
||||||
|
: [];
|
||||||
|
const metaOrCtrl = event.metaKey || event.ctrlKey;
|
||||||
|
if (metaOrCtrl && !selectionIds.includes(docKey)) {
|
||||||
|
selectionIds = [...selectionIds, docKey];
|
||||||
|
}
|
||||||
|
let groupDocIds = [];
|
||||||
|
if (selectionIds.includes(docKey) && selectionIds.length > 1) {
|
||||||
|
groupDocIds = selectionIds
|
||||||
|
.map((id) => String(id))
|
||||||
|
.filter((id, index, array) => array.indexOf(id) === index && documentLookup.has(id));
|
||||||
|
}
|
||||||
|
if (!groupDocIds.includes(docKey)) {
|
||||||
|
groupDocIds.unshift(docKey);
|
||||||
|
}
|
||||||
|
groupDocIds = groupDocIds.filter((id, index, array) => array.indexOf(id) === index);
|
||||||
|
if (!groupDocIds.length) {
|
||||||
|
groupDocIds = [docKey];
|
||||||
|
}
|
||||||
|
const isGroupDrag = groupDocIds.length > 1;
|
||||||
|
|
||||||
|
if (isGroupDrag) {
|
||||||
|
groupDocIds.forEach((id) => {
|
||||||
|
if (id !== docKey) {
|
||||||
|
cancelInertiaAnimation(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeInfo = ensureDocumentSize(doc) || { width: 0, height: 0 };
|
||||||
|
const docWidth = sizeInfo.width || 320;
|
||||||
|
const docHeight = sizeInfo.height || 240;
|
||||||
const { baseScale } = resolveBaseMetrics(doc, docWidth, docHeight);
|
const { baseScale } = resolveBaseMetrics(doc, docWidth, docHeight);
|
||||||
const normalizedBaseScale =
|
const normalizedBaseScale =
|
||||||
Number.isFinite(baseScale) && baseScale > 0 ? baseScale : 1;
|
Number.isFinite(baseScale) && baseScale > 0 ? baseScale : 1;
|
||||||
|
|
||||||
|
const entry = layoutRef.current.get(docKey) || null;
|
||||||
const defaultCenterX = canvasPadding + docWidth / 2;
|
const defaultCenterX = canvasPadding + docWidth / 2;
|
||||||
const defaultCenterY = canvasPadding + docHeight / 2;
|
const defaultCenterY = canvasPadding + docHeight / 2;
|
||||||
const metaPressed = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
|
||||||
if (!metaPressed) {
|
|
||||||
bringToFront(docId);
|
|
||||||
}
|
|
||||||
const entry = layoutRef.current.get(docId) || null;
|
|
||||||
const centerX = typeof entry?.centerX === 'number' ? entry.centerX : defaultCenterX;
|
const centerX = typeof entry?.centerX === 'number' ? entry.centerX : defaultCenterX;
|
||||||
const centerY = typeof entry?.centerY === 'number' ? entry.centerY : defaultCenterY;
|
const centerY = typeof entry?.centerY === 'number' ? entry.centerY : defaultCenterY;
|
||||||
|
|
||||||
|
const modifierPressed = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
||||||
|
if (!modifierPressed) {
|
||||||
|
if (isGroupDrag) {
|
||||||
|
groupDocIds.forEach((id) => bringToFront(id));
|
||||||
|
} else {
|
||||||
|
bringToFront(docId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (entry && (entry.centerX !== centerX || entry.centerY !== centerY)) {
|
if (entry && (entry.centerX !== centerX || entry.centerY !== centerY)) {
|
||||||
layoutRef.current.set(docId, { ...entry, centerX, centerY });
|
layoutRef.current.set(docKey, { ...entry, centerX, centerY });
|
||||||
}
|
}
|
||||||
|
|
||||||
const capturedTarget = event.currentTarget instanceof HTMLElement ? event.currentTarget : null;
|
const capturedTarget = event.currentTarget instanceof HTMLElement ? event.currentTarget : null;
|
||||||
@@ -245,6 +341,7 @@ const useDocumentDrag = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const containerRect = containerRef?.current?.getBoundingClientRect?.() || null;
|
const containerRect = containerRef?.current?.getBoundingClientRect?.() || null;
|
||||||
const containerLeft = containerRect?.left || 0;
|
const containerLeft = containerRect?.left || 0;
|
||||||
const containerTop = containerRect?.top || 0;
|
const containerTop = containerRect?.top || 0;
|
||||||
@@ -258,14 +355,48 @@ const useDocumentDrag = () => {
|
|||||||
const sinInitial = Math.sin(-initialRotationRad);
|
const sinInitial = Math.sin(-initialRotationRad);
|
||||||
const localPointerOffsetX = pointerOffsetX * cosInitial - pointerOffsetY * sinInitial;
|
const localPointerOffsetX = pointerOffsetX * cosInitial - pointerOffsetY * sinInitial;
|
||||||
const localPointerOffsetY = pointerOffsetX * sinInitial + pointerOffsetY * cosInitial;
|
const localPointerOffsetY = pointerOffsetX * sinInitial + pointerOffsetY * cosInitial;
|
||||||
|
|
||||||
|
const stackRandom = () => Math.random();
|
||||||
|
const groupItems = groupDocIds.map((id, index) => {
|
||||||
|
const itemDoc = documentLookup.get(id);
|
||||||
|
const itemSize = ensureDocumentSize(itemDoc) || sizeInfo;
|
||||||
|
const itemWidth = itemSize.width || docWidth;
|
||||||
|
const itemHeight = itemSize.height || docHeight;
|
||||||
|
const itemEntry = layoutRef.current.get(id) || null;
|
||||||
|
const itemCenterX =
|
||||||
|
typeof itemEntry?.centerX === 'number' ? itemEntry.centerX : canvasPadding + itemWidth / 2;
|
||||||
|
const itemCenterY =
|
||||||
|
typeof itemEntry?.centerY === 'number' ? itemEntry.centerY : canvasPadding + itemHeight / 2;
|
||||||
|
const radius = index === 0 ? 0 : 24 + index * 8;
|
||||||
|
const offsetAngle = (index * 1.618 + stackRandom() * 0.5) * Math.PI;
|
||||||
|
const offsetX = Math.cos(offsetAngle) * radius;
|
||||||
|
const offsetY = Math.sin(offsetAngle) * radius;
|
||||||
|
const initialRotation = itemEntry?.rotation ?? 0;
|
||||||
|
const targetRotation = initialRotation;
|
||||||
|
return {
|
||||||
|
docId: id,
|
||||||
|
width: itemWidth,
|
||||||
|
height: itemHeight,
|
||||||
|
currentCenterX: itemCenterX,
|
||||||
|
currentCenterY: itemCenterY,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
initialRotation,
|
||||||
|
displayRotation: initialRotation,
|
||||||
|
targetRotation,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const eventTimestamp =
|
const eventTimestamp =
|
||||||
typeof event.timeStamp === 'number' && Number.isFinite(event.timeStamp)
|
typeof event.timeStamp === 'number' && Number.isFinite(event.timeStamp)
|
||||||
? event.timeStamp
|
? event.timeStamp
|
||||||
: typeof performance !== 'undefined' && typeof performance.now === 'function'
|
: typeof performance !== 'undefined' && typeof performance.now === 'function'
|
||||||
? performance.now()
|
? performance.now()
|
||||||
: Date.now();
|
: Date.now();
|
||||||
|
|
||||||
dragStateRef.current = {
|
dragStateRef.current = {
|
||||||
docId,
|
docId,
|
||||||
|
docKey,
|
||||||
pointerId: event.pointerId,
|
pointerId: event.pointerId,
|
||||||
originCenterX: centerX,
|
originCenterX: centerX,
|
||||||
originCenterY: centerY,
|
originCenterY: centerY,
|
||||||
@@ -289,22 +420,47 @@ const useDocumentDrag = () => {
|
|||||||
localPointerOffsetY,
|
localPointerOffsetY,
|
||||||
containerRectLeft: containerLeft,
|
containerRectLeft: containerLeft,
|
||||||
containerRectTop: containerTop,
|
containerRectTop: containerTop,
|
||||||
|
isGroup: isGroupDrag,
|
||||||
|
groupDocIds,
|
||||||
|
groupItems,
|
||||||
|
groupElevated: !isGroupDrag,
|
||||||
};
|
};
|
||||||
|
|
||||||
setDraggingId(docId);
|
setDraggingId(docId);
|
||||||
|
|
||||||
|
if (isGroupDrag) {
|
||||||
|
groupItems.forEach((item) => {
|
||||||
|
if (item.docId === docKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
bringToFront,
|
bringToFront,
|
||||||
canvasPadding,
|
canvasPadding,
|
||||||
cancelInertiaAnimation,
|
cancelInertiaAnimation,
|
||||||
|
containerRef,
|
||||||
documentLookup,
|
documentLookup,
|
||||||
ensureDocumentSize,
|
ensureDocumentSize,
|
||||||
layoutRef,
|
layoutRef,
|
||||||
resolveBaseMetrics,
|
resolveBaseMetrics,
|
||||||
|
selectedDocumentIds,
|
||||||
setDraggingId,
|
setDraggingId,
|
||||||
debugDrag,
|
debugDrag,
|
||||||
containerRef,
|
itemRefs,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePointerMove = useCallback(
|
const handlePointerMove = useCallback(
|
||||||
(event) => {
|
(event) => {
|
||||||
@@ -327,6 +483,135 @@ const useDocumentDrag = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
preventAll(event);
|
preventAll(event);
|
||||||
|
|
||||||
|
if (state.isGroup) {
|
||||||
|
const containerRect = containerRef?.current?.getBoundingClientRect?.();
|
||||||
|
if (containerRect) {
|
||||||
|
state.containerRectLeft = containerRect.left;
|
||||||
|
state.containerRectTop = containerRect.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pointerCanvasX = event.clientX - state.containerRectLeft;
|
||||||
|
const pointerCanvasY = event.clientY - state.containerRectTop;
|
||||||
|
const deltaX = event.clientX - state.startX;
|
||||||
|
const deltaY = event.clientY - state.startY;
|
||||||
|
|
||||||
|
if (!state.moved) {
|
||||||
|
const distanceSquared = deltaX * deltaX + deltaY * deltaY;
|
||||||
|
if (distanceSquared < DRAG_HYSTERESIS_SQUARED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.moved = true;
|
||||||
|
if (!state.groupElevated) {
|
||||||
|
state.groupDocIds.forEach((id) => {
|
||||||
|
if (id === state.docKey) {
|
||||||
|
bringToFront(state.docId);
|
||||||
|
} else {
|
||||||
|
bringToFront(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
state.groupElevated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 desiredCenterX = pointerCanvasX - state.localPointerOffsetX;
|
||||||
|
const desiredCenterY = pointerCanvasY - state.localPointerOffsetY;
|
||||||
|
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.groupItems.forEach((item) => {
|
||||||
|
const isPrimary = item.docId === state.docKey;
|
||||||
|
|
||||||
|
if (isPrimary) {
|
||||||
|
item.currentCenterX = centerX;
|
||||||
|
item.currentCenterY = centerY;
|
||||||
|
item.offsetX *= 0.92;
|
||||||
|
item.offsetY *= 0.92;
|
||||||
|
item.displayRotation = state.rotation ?? item.displayRotation ?? 0;
|
||||||
|
} else {
|
||||||
|
item.offsetX *= 0.92;
|
||||||
|
item.offsetY *= 0.92;
|
||||||
|
if (Math.abs(item.offsetX) < 1) item.offsetX = 0;
|
||||||
|
if (Math.abs(item.offsetY) < 1) item.offsetY = 0;
|
||||||
|
|
||||||
|
const targetX = centerX + item.offsetX;
|
||||||
|
const targetY = centerY + item.offsetY;
|
||||||
|
const smoothing = 0.18;
|
||||||
|
item.currentCenterX += (targetX - item.currentCenterX) * smoothing;
|
||||||
|
item.currentCenterY += (targetY - item.currentCenterY) * smoothing;
|
||||||
|
|
||||||
|
const halfW = item.width / 2;
|
||||||
|
const halfH = item.height / 2;
|
||||||
|
const minX = canvasPadding + halfW;
|
||||||
|
const maxX = Math.max(minX, canvasWidth - canvasPadding - halfW);
|
||||||
|
const minY = canvasPadding + halfH;
|
||||||
|
const maxY = Math.max(minY, canvasHeight - canvasPadding - halfH);
|
||||||
|
item.currentCenterX = clamp(item.currentCenterX, minX, maxX);
|
||||||
|
item.currentCenterY = clamp(item.currentCenterY, minY, maxY);
|
||||||
|
|
||||||
|
const rotationBlend = 0.16;
|
||||||
|
item.displayRotation += (item.targetRotation - item.displayRotation) * rotationBlend;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryItem = layoutRef.current.get(item.docId) || {};
|
||||||
|
layoutRef.current.set(item.docId, {
|
||||||
|
...entryItem,
|
||||||
|
centerX: item.currentCenterX,
|
||||||
|
centerY: item.currentCenterY,
|
||||||
|
rotation: item.displayRotation ?? entryItem.rotation ?? 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
applyTransform(
|
||||||
|
item.docId,
|
||||||
|
item.currentCenterX,
|
||||||
|
item.currentCenterY,
|
||||||
|
item.width,
|
||||||
|
item.height,
|
||||||
|
item.displayRotation ?? entryItem.rotation ?? 0,
|
||||||
|
isPrimary ? state.dragScale || 1 : 1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
state.lastClientX = event.clientX;
|
||||||
|
state.lastClientY = event.clientY;
|
||||||
|
state.lastTimestamp =
|
||||||
|
typeof event.timeStamp === 'number' && Number.isFinite(event.timeStamp)
|
||||||
|
? event.timeStamp
|
||||||
|
: typeof performance !== 'undefined' && typeof performance.now === 'function'
|
||||||
|
? performance.now()
|
||||||
|
: Date.now();
|
||||||
|
|
||||||
|
recalcVisibleDocIds();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (state.locked) {
|
if (state.locked) {
|
||||||
if (debugDrag) {
|
if (debugDrag) {
|
||||||
console.log('[desk] handlePointerMove: locked drag for doc', state.docId);
|
console.log('[desk] handlePointerMove: locked drag for doc', state.docId);
|
||||||
@@ -358,7 +643,7 @@ const useDocumentDrag = () => {
|
|||||||
const pointerCanvasX = event.clientX - containerLeft;
|
const pointerCanvasX = event.clientX - containerLeft;
|
||||||
const pointerCanvasY = event.clientY - containerTop;
|
const pointerCanvasY = event.clientY - containerTop;
|
||||||
|
|
||||||
const rotationDeg = state.rotation || 0;
|
const rotationDeg = entry?.rotation ?? 0;
|
||||||
const rotationRad = (rotationDeg * Math.PI) / 180;
|
const rotationRad = (rotationDeg * Math.PI) / 180;
|
||||||
const cosRot = Math.cos(rotationRad);
|
const cosRot = Math.cos(rotationRad);
|
||||||
const sinRot = Math.sin(rotationRad);
|
const sinRot = Math.sin(rotationRad);
|
||||||
@@ -420,6 +705,26 @@ const useDocumentDrag = () => {
|
|||||||
const updated = { ...entry, centerX: currentCenterX, centerY: currentCenterY };
|
const updated = { ...entry, centerX: currentCenterX, centerY: currentCenterY };
|
||||||
layoutRef.current.set(state.docId, updated);
|
layoutRef.current.set(state.docId, updated);
|
||||||
|
|
||||||
|
applyTransform(
|
||||||
|
state.docId,
|
||||||
|
currentCenterX,
|
||||||
|
currentCenterY,
|
||||||
|
state.width,
|
||||||
|
state.height,
|
||||||
|
rotationDeg,
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const offsetX = pointerCanvasX - currentCenterX;
|
const offsetX = pointerCanvasX - currentCenterX;
|
||||||
const offsetY = pointerCanvasY - currentCenterY;
|
const offsetY = pointerCanvasY - currentCenterY;
|
||||||
const cosInverseCurrent = Math.cos(-rotationRad);
|
const cosInverseCurrent = Math.cos(-rotationRad);
|
||||||
@@ -442,147 +747,10 @@ const useDocumentDrag = () => {
|
|||||||
}
|
}
|
||||||
dt = clamp(dt, MIN_TIMESTEP, MAX_TIMESTEP);
|
dt = clamp(dt, MIN_TIMESTEP, MAX_TIMESTEP);
|
||||||
|
|
||||||
const previousClientX = state.lastClientX;
|
|
||||||
const previousClientY = state.lastClientY;
|
|
||||||
const velocityX = (event.clientX - previousClientX) / dt;
|
|
||||||
const velocityY = (event.clientY - previousClientY) / dt;
|
|
||||||
const rawTorque = offsetX * velocityY - offsetY * velocityX;
|
|
||||||
|
|
||||||
const rotationBeforeIntegration = state.rotation;
|
|
||||||
|
|
||||||
let torque = rawTorque * BASE_TORQUE_FACTOR;
|
|
||||||
let dampingOverride = null;
|
|
||||||
if (collidedWithEdge) {
|
|
||||||
const currentRotation =
|
|
||||||
typeof state.rotation === 'number'
|
|
||||||
? state.rotation
|
|
||||||
: state.restRotation + state.dynamicRotation;
|
|
||||||
torque =
|
|
||||||
(rawTorque * EDGE_COLLISION_TORQUE_FACTOR + currentRotation * EDGE_ALIGNMENT_STIFFNESS) *
|
|
||||||
EDGE_ALIGNMENT_TORQUE_MULTIPLIER;
|
|
||||||
const restBlend = 1 - Math.exp(-EDGE_REST_REALIGN_RATE * dt);
|
|
||||||
if (restBlend > 0) {
|
|
||||||
const previousRest = state.restRotation;
|
|
||||||
const nextRest = previousRest + (0 - previousRest) * restBlend;
|
|
||||||
state.restRotation = nextRest;
|
|
||||||
}
|
|
||||||
dampingOverride = ANGULAR_DAMPING + EDGE_COLLISION_EXTRA_DAMPING;
|
|
||||||
if (!pointerInsideCard) {
|
|
||||||
torque = 0;
|
|
||||||
currentCenterX = entry?.centerX ?? currentCenterX;
|
|
||||||
currentCenterY = entry?.centerY ?? currentCenterY;
|
|
||||||
if (updated.centerX !== currentCenterX || updated.centerY !== currentCenterY) {
|
|
||||||
layoutRef.current.set(state.docId, { ...updated, centerX: currentCenterX, centerY: currentCenterY });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.lastClientX = event.clientX;
|
state.lastClientX = event.clientX;
|
||||||
state.lastClientY = event.clientY;
|
state.lastClientY = event.clientY;
|
||||||
state.lastTimestamp = currentTimestamp;
|
state.lastTimestamp = currentTimestamp;
|
||||||
|
|
||||||
integrateRotation(state, dt, torque, dampingOverride);
|
|
||||||
|
|
||||||
if (!collidedWithEdge) {
|
|
||||||
const rotationAfter = state.rotation || 0;
|
|
||||||
if (rotationAfter !== rotationBeforeIntegration) {
|
|
||||||
const rotationAfterRad = (rotationAfter * Math.PI) / 180;
|
|
||||||
const cosAfter = Math.cos(rotationAfterRad);
|
|
||||||
const sinAfter = Math.sin(rotationAfterRad);
|
|
||||||
const rotatedOffsetXAfter =
|
|
||||||
state.localPointerOffsetX * cosAfter - state.localPointerOffsetY * sinAfter;
|
|
||||||
const rotatedOffsetYAfter =
|
|
||||||
state.localPointerOffsetX * sinAfter + state.localPointerOffsetY * cosAfter;
|
|
||||||
|
|
||||||
const desiredCenterXAfter = pointerCanvasX - rotatedOffsetXAfter;
|
|
||||||
const desiredCenterYAfter = pointerCanvasY - rotatedOffsetYAfter;
|
|
||||||
|
|
||||||
const absCosAfter = Math.abs(cosAfter);
|
|
||||||
const absSinAfter = Math.abs(sinAfter);
|
|
||||||
const rotatedHalfWidthAfter = absCosAfter * halfWidth + absSinAfter * halfHeight;
|
|
||||||
const rotatedHalfHeightAfter = absSinAfter * halfWidth + absCosAfter * halfHeight;
|
|
||||||
|
|
||||||
const minCenterXAfter = canvasPadding + rotatedHalfWidthAfter;
|
|
||||||
const maxCenterXAfter = Math.max(
|
|
||||||
minCenterXAfter,
|
|
||||||
canvasWidth - canvasPadding - rotatedHalfWidthAfter,
|
|
||||||
);
|
|
||||||
const minCenterYAfter = canvasPadding + rotatedHalfHeightAfter;
|
|
||||||
const maxCenterYAfter = Math.max(
|
|
||||||
minCenterYAfter,
|
|
||||||
canvasHeight - canvasPadding - rotatedHalfHeightAfter,
|
|
||||||
);
|
|
||||||
|
|
||||||
const correctedCenterX = clamp(
|
|
||||||
desiredCenterXAfter,
|
|
||||||
minCenterXAfter,
|
|
||||||
maxCenterXAfter,
|
|
||||||
);
|
|
||||||
const correctedCenterY = clamp(
|
|
||||||
desiredCenterYAfter,
|
|
||||||
minCenterYAfter,
|
|
||||||
maxCenterYAfter,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
Math.abs(correctedCenterX - currentCenterX) > 0.01 ||
|
|
||||||
Math.abs(correctedCenterY - currentCenterY) > 0.01
|
|
||||||
) {
|
|
||||||
const entryAfter = layoutRef.current.get(state.docId);
|
|
||||||
if (entryAfter) {
|
|
||||||
const adjustedEntry = {
|
|
||||||
...entryAfter,
|
|
||||||
centerX: correctedCenterX,
|
|
||||||
centerY: correctedCenterY,
|
|
||||||
};
|
|
||||||
layoutRef.current.set(state.docId, adjustedEntry);
|
|
||||||
const nodeAfter = itemRefs.current.get(state.docId);
|
|
||||||
if (nodeAfter) {
|
|
||||||
nodeAfter.style.transform = formatTransform(
|
|
||||||
correctedCenterX - state.width / 2,
|
|
||||||
correctedCenterY - state.height / 2,
|
|
||||||
state.rotation,
|
|
||||||
state.dragScale || 1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
currentCenterX = correctedCenterX;
|
|
||||||
currentCenterY = correctedCenterY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collidedWithEdge) {
|
|
||||||
const rotationAfter = state.rotation;
|
|
||||||
const crossedAlignment =
|
|
||||||
rotationBeforeIntegration > EDGE_ALIGNMENT_EPSILON && rotationAfter < 0
|
|
||||||
? rotationBeforeIntegration - rotationAfter > EDGE_ALIGNMENT_EPSILON
|
|
||||||
: rotationBeforeIntegration < -EDGE_ALIGNMENT_EPSILON && rotationAfter > 0
|
|
||||||
? rotationAfter - rotationBeforeIntegration > EDGE_ALIGNMENT_EPSILON
|
|
||||||
: false;
|
|
||||||
const nearAlignment = Math.abs(rotationAfter) <= EDGE_ALIGNMENT_EPSILON;
|
|
||||||
if (crossedAlignment || nearAlignment) {
|
|
||||||
state.restRotation = 0;
|
|
||||||
state.dynamicRotation = 0;
|
|
||||||
state.angularVelocity = 0;
|
|
||||||
state.rotation = 0;
|
|
||||||
const updatedEntry = layoutRef.current.get(state.docId);
|
|
||||||
if (updatedEntry) {
|
|
||||||
const alignedEntry = { ...updatedEntry, rotation: 0 };
|
|
||||||
layoutRef.current.set(state.docId, alignedEntry);
|
|
||||||
const node = itemRefs.current.get(state.docId);
|
|
||||||
if (node) {
|
|
||||||
node.style.transform = formatTransform(
|
|
||||||
alignedEntry.centerX - state.width / 2,
|
|
||||||
alignedEntry.centerY - state.height / 2,
|
|
||||||
0,
|
|
||||||
state.dragScale || 1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rotationForOffsetDeg = state.rotation || 0;
|
const rotationForOffsetDeg = state.rotation || 0;
|
||||||
const rotationForOffsetRad = (rotationForOffsetDeg * Math.PI) / 180;
|
const rotationForOffsetRad = (rotationForOffsetDeg * Math.PI) / 180;
|
||||||
const cosInverse = Math.cos(-rotationForOffsetRad);
|
const cosInverse = Math.cos(-rotationForOffsetRad);
|
||||||
@@ -609,8 +777,9 @@ const useDocumentDrag = () => {
|
|||||||
defaultCanvasHeight,
|
defaultCanvasHeight,
|
||||||
defaultCanvasWidth,
|
defaultCanvasWidth,
|
||||||
containerRef,
|
containerRef,
|
||||||
integrateRotation,
|
|
||||||
layoutRef,
|
layoutRef,
|
||||||
|
itemRefs,
|
||||||
|
applyTransform,
|
||||||
recalcVisibleDocIds,
|
recalcVisibleDocIds,
|
||||||
debugDrag,
|
debugDrag,
|
||||||
],
|
],
|
||||||
@@ -619,7 +788,18 @@ const useDocumentDrag = () => {
|
|||||||
const handlePointerUp = useCallback(
|
const handlePointerUp = useCallback(
|
||||||
(event) => {
|
(event) => {
|
||||||
const state = dragStateRef.current;
|
const state = dragStateRef.current;
|
||||||
if (state && state.pointerId === event.pointerId) {
|
if (!state || state.pointerId !== event.pointerId) {
|
||||||
|
finishDrag(event.pointerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isGroup) {
|
||||||
|
finalizeGroupDrag(state);
|
||||||
|
finishDrag(event.pointerId);
|
||||||
|
recalcVisibleDocIds();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (state.moved) {
|
if (state.moved) {
|
||||||
const inertiaState = {
|
const inertiaState = {
|
||||||
restRotation: state.restRotation,
|
restRotation: state.restRotation,
|
||||||
@@ -655,15 +835,14 @@ const useDocumentDrag = () => {
|
|||||||
docTitle: doc?.title || 'document',
|
docTitle: doc?.title || 'document',
|
||||||
});
|
});
|
||||||
finishDrag(event.pointerId);
|
finishDrag(event.pointerId);
|
||||||
return;
|
|
||||||
}
|
|
||||||
finishDrag(event.pointerId);
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
bringToFront,
|
bringToFront,
|
||||||
documentLookup,
|
documentLookup,
|
||||||
finishDrag,
|
finishDrag,
|
||||||
|
finalizeGroupDrag,
|
||||||
startInertiaAnimation,
|
startInertiaAnimation,
|
||||||
|
recalcVisibleDocIds,
|
||||||
tapHandler,
|
tapHandler,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -672,6 +851,13 @@ const useDocumentDrag = () => {
|
|||||||
(event) => {
|
(event) => {
|
||||||
const state = dragStateRef.current;
|
const state = dragStateRef.current;
|
||||||
if (state && state.pointerId === event.pointerId && state.moved) {
|
if (state && state.pointerId === event.pointerId && state.moved) {
|
||||||
|
if (state.isGroup) {
|
||||||
|
finalizeGroupDrag(state);
|
||||||
|
finishDrag(event.pointerId);
|
||||||
|
recalcVisibleDocIds();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const inertiaState = {
|
const inertiaState = {
|
||||||
restRotation: state.restRotation,
|
restRotation: state.restRotation,
|
||||||
dynamicRotation: state.dynamicRotation,
|
dynamicRotation: state.dynamicRotation,
|
||||||
@@ -688,7 +874,7 @@ const useDocumentDrag = () => {
|
|||||||
}
|
}
|
||||||
finishDrag(event.pointerId);
|
finishDrag(event.pointerId);
|
||||||
},
|
},
|
||||||
[finishDrag, startInertiaAnimation],
|
[finalizeGroupDrag, finishDrag, recalcVisibleDocIds, startInertiaAnimation],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user