From 65495a07b21ec10c722616f5527835b306995c3f Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Sun, 2 Nov 2025 14:08:08 +0100 Subject: [PATCH] fix --- frontend/src/DesktopWorkspace.jsx | 10 +- frontend/src/desktop/useDocumentDrag.js | 628 +++++++++++++++--------- 2 files changed, 416 insertions(+), 222 deletions(-) diff --git a/frontend/src/DesktopWorkspace.jsx b/frontend/src/DesktopWorkspace.jsx index 0f0a67d..f105146 100644 --- a/frontend/src/DesktopWorkspace.jsx +++ b/frontend/src/DesktopWorkspace.jsx @@ -1930,7 +1930,15 @@ const DesktopWorkspaceView = () => { } }} 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); } handlePointerDown(event, doc.id); diff --git a/frontend/src/desktop/useDocumentDrag.js b/frontend/src/desktop/useDocumentDrag.js index 5eb5704..587a133 100644 --- a/frontend/src/desktop/useDocumentDrag.js +++ b/frontend/src/desktop/useDocumentDrag.js @@ -14,13 +14,6 @@ const ANGULAR_DAMPING = 11; const TORQUE_TO_ACCELERATION = 0.006; const SETTLE_ANGULAR_VELOCITY = 1.2; 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 { @@ -39,8 +32,62 @@ const useDocumentDrag = () => { containerRef, onDocumentOpen, onInspectDocument, + selectedDocumentIds, } = 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({ delay: 220, onSingle: ({ data, event }) => { @@ -200,11 +247,11 @@ const useDocumentDrag = () => { ); const handlePointerDown = useCallback( - (event, docId) => { + (event, docIdInput) => { if (debugDrag) { console.log( '[desk] handlePointerDown fired for doc', - docId, + docIdInput, 'button', event.button, 'pointerType', @@ -214,25 +261,74 @@ const useDocumentDrag = () => { ); } preventAll(event); - cancelInertiaAnimation(docId); + + const docId = docIdInput != null ? docIdInput : null; const docKey = docId != null ? String(docId) : null; - const doc = docKey ? documentLookup.get(docKey) : null; - const { width: docWidth, height: docHeight } = ensureDocumentSize(doc); + if (!docKey) { + 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 normalizedBaseScale = Number.isFinite(baseScale) && baseScale > 0 ? baseScale : 1; + + const entry = layoutRef.current.get(docKey) || null; const defaultCenterX = canvasPadding + docWidth / 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 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)) { - layoutRef.current.set(docId, { ...entry, centerX, centerY }); + layoutRef.current.set(docKey, { ...entry, centerX, centerY }); } const capturedTarget = event.currentTarget instanceof HTMLElement ? event.currentTarget : null; @@ -245,6 +341,7 @@ const useDocumentDrag = () => { } } } + const containerRect = containerRef?.current?.getBoundingClientRect?.() || null; const containerLeft = containerRect?.left || 0; const containerTop = containerRect?.top || 0; @@ -258,14 +355,48 @@ const useDocumentDrag = () => { const sinInitial = Math.sin(-initialRotationRad); const localPointerOffsetX = pointerOffsetX * cosInitial - pointerOffsetY * sinInitial; 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 = typeof event.timeStamp === 'number' && Number.isFinite(event.timeStamp) ? event.timeStamp : typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now(); + dragStateRef.current = { docId, + docKey, pointerId: event.pointerId, originCenterX: centerX, originCenterY: centerY, @@ -289,44 +420,198 @@ const useDocumentDrag = () => { localPointerOffsetY, containerRectLeft: containerLeft, containerRectTop: containerTop, - }; - setDraggingId(docId); - }, - [ - bringToFront, + isGroup: isGroupDrag, + groupDocIds, + groupItems, + groupElevated: !isGroupDrag, + }; + + 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, canvasPadding, cancelInertiaAnimation, - documentLookup, - ensureDocumentSize, - layoutRef, - resolveBaseMetrics, - setDraggingId, - debugDrag, containerRef, - ], - ); + documentLookup, + ensureDocumentSize, + layoutRef, + resolveBaseMetrics, + selectedDocumentIds, + setDraggingId, + debugDrag, + itemRefs, + ], +); const handlePointerMove = useCallback( (event) => { const state = dragStateRef.current; if (!state) { - if (debugDrag) { - console.log('[desk] handlePointerMove: no drag state for pointer', event.pointerId); - } - return; + if (debugDrag) { + console.log('[desk] handlePointerMove: no drag state for pointer', event.pointerId); } - if (state.pointerId !== event.pointerId) { - if (debugDrag) { - console.log( - '[desk] handlePointerMove: pointer mismatch expected', - state.pointerId, - 'got', - event.pointerId, - ); - } - return; + return; + } + if (state.pointerId !== event.pointerId) { + if (debugDrag) { + console.log( + '[desk] handlePointerMove: pointer mismatch expected', + state.pointerId, + 'got', + event.pointerId, + ); } - preventAll(event); + return; + } + 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 (debugDrag) { console.log('[desk] handlePointerMove: locked drag for doc', state.docId); @@ -358,7 +643,7 @@ const useDocumentDrag = () => { const pointerCanvasX = event.clientX - containerLeft; const pointerCanvasY = event.clientY - containerTop; - const rotationDeg = state.rotation || 0; + const rotationDeg = entry?.rotation ?? 0; const rotationRad = (rotationDeg * Math.PI) / 180; const cosRot = Math.cos(rotationRad); const sinRot = Math.sin(rotationRad); @@ -420,6 +705,26 @@ const useDocumentDrag = () => { const updated = { ...entry, centerX: currentCenterX, centerY: currentCenterY }; 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 offsetY = pointerCanvasY - currentCenterY; const cosInverseCurrent = Math.cos(-rotationRad); @@ -442,147 +747,10 @@ const useDocumentDrag = () => { } 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.lastClientY = event.clientY; 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 rotationForOffsetRad = (rotationForOffsetDeg * Math.PI) / 180; const cosInverse = Math.cos(-rotationForOffsetRad); @@ -609,8 +777,9 @@ const useDocumentDrag = () => { defaultCanvasHeight, defaultCanvasWidth, containerRef, - integrateRotation, layoutRef, + itemRefs, + applyTransform, recalcVisibleDocIds, debugDrag, ], @@ -619,51 +788,61 @@ const useDocumentDrag = () => { const handlePointerUp = useCallback( (event) => { const state = dragStateRef.current; - if (state && state.pointerId === event.pointerId) { - if (state.moved) { - const inertiaState = { - restRotation: state.restRotation, - dynamicRotation: state.dynamicRotation, - angularVelocity: state.angularVelocity, - rotation: state.rotation, - width: state.width, - height: state.height, - dragScale: state.dragScale || 1, - }; - const docId = state.docId; - finishDrag(event.pointerId); - startInertiaAnimation(docId, inertiaState); - return; - } - - const docId = state.docId; - const metaPressed = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey; - if (!metaPressed) { - bringToFront(docId); - } - const originInfo = { - rotation: state.rotation || 0, - scale: state.baseScale || 1, - width: state.width, - height: state.height, - }; - const docKey = docId != null ? String(docId) : null; - const doc = docKey ? documentLookup.get(docKey) : null; - tapHandler(event, { - docId, - originInfo, - docTitle: doc?.title || 'document', - }); + if (!state || state.pointerId !== event.pointerId) { finishDrag(event.pointerId); return; } + + if (state.isGroup) { + finalizeGroupDrag(state); + finishDrag(event.pointerId); + recalcVisibleDocIds(); + return; + } + + if (state.moved) { + const inertiaState = { + restRotation: state.restRotation, + dynamicRotation: state.dynamicRotation, + angularVelocity: state.angularVelocity, + rotation: state.rotation, + width: state.width, + height: state.height, + dragScale: state.dragScale || 1, + }; + const docId = state.docId; + finishDrag(event.pointerId); + startInertiaAnimation(docId, inertiaState); + return; + } + + const docId = state.docId; + const metaPressed = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey; + if (!metaPressed) { + bringToFront(docId); + } + const originInfo = { + rotation: state.rotation || 0, + scale: state.baseScale || 1, + width: state.width, + height: state.height, + }; + const docKey = docId != null ? String(docId) : null; + const doc = docKey ? documentLookup.get(docKey) : null; + tapHandler(event, { + docId, + originInfo, + docTitle: doc?.title || 'document', + }); finishDrag(event.pointerId); }, [ bringToFront, documentLookup, finishDrag, + finalizeGroupDrag, startInertiaAnimation, + recalcVisibleDocIds, tapHandler, ], ); @@ -672,6 +851,13 @@ const useDocumentDrag = () => { (event) => { const state = dragStateRef.current; if (state && state.pointerId === event.pointerId && state.moved) { + if (state.isGroup) { + finalizeGroupDrag(state); + finishDrag(event.pointerId); + recalcVisibleDocIds(); + return; + } + const inertiaState = { restRotation: state.restRotation, dynamicRotation: state.dynamicRotation, @@ -688,7 +874,7 @@ const useDocumentDrag = () => { } finishDrag(event.pointerId); }, - [finishDrag, startInertiaAnimation], + [finalizeGroupDrag, finishDrag, recalcVisibleDocIds, startInertiaAnimation], ); return {