diff --git a/frontend/src/DesktopWorkspace.jsx b/frontend/src/DesktopWorkspace.jsx index 0ad4b2f..abaa12a 100644 --- a/frontend/src/DesktopWorkspace.jsx +++ b/frontend/src/DesktopWorkspace.jsx @@ -35,9 +35,6 @@ const CARD_MIN = 240; const CARD_MAX = 340; const TAG_REMOVE_DISTANCE = 160; const STACK_HIT_EPSILON = 4; -const STACK_CENTER_TOLERANCE = 0.35; -const STACK_CENTER_MIN = 32; -const STACK_ROTATION_TOLERANCE = 15; const DEBUG_DRAG = false; const DEBUG_FOCUS = true; @@ -1953,7 +1950,6 @@ const DesktopWorkspaceView = () => { onClearSelection, detailPanelOpen, onCloseDetailPanel, - documentLookup, } = useDesktopContext(); const { handlePointerDown, handlePointerMove, handlePointerUp, handlePointerCancel } = @@ -1974,7 +1970,8 @@ const DesktopWorkspaceView = () => { return []; } - const hits = []; + const candidates = []; + items.forEach((doc) => { if (!doc?.id) { return; @@ -1983,6 +1980,7 @@ const DesktopWorkspaceView = () => { if (!layout) { return; } + const sizeInfo = ensureDocumentSize(doc); if (!sizeInfo) { return; @@ -2018,122 +2016,66 @@ const DesktopWorkspaceView = () => { const halfWidth = width / 2; const halfHeight = height / 2; - if ( + const containsPointer = Math.abs(localX) <= halfWidth + STACK_HIT_EPSILON - && Math.abs(localY) <= halfHeight + STACK_HIT_EPSILON - ) { - const docKey = String(doc.id); - if (!hits.some((entry) => entry.id === docKey)) { - hits.push({ - id: docKey, - z: Number.isFinite(layout.z) ? layout.z : 0, - }); - } - } + && Math.abs(localY) <= halfHeight + STACK_HIT_EPSILON; + + candidates.push({ + id: String(doc.id), + z: Number.isFinite(layout.z) ? layout.z : 0, + centerX, + centerY, + rotationDeg, + width, + height, + halfWidth, + halfHeight, + localX, + localY, + marginX: halfWidth - Math.abs(localX), + marginY: halfHeight - Math.abs(localY), + containsPointer, + }); }); - if (!hits.length) { + const pointerCandidates = candidates.filter((candidate) => candidate.containsPointer); + if (!pointerCandidates.length) { return []; } - hits.sort((a, b) => (b.z ?? 0) - (a.z ?? 0)); + const sortedByZ = [...pointerCandidates].sort((a, b) => (b.z ?? 0) - (a.z ?? 0)); + const targetKey = targetDocId != null ? String(targetDocId) : sortedByZ[0].id; - const targetKey = targetDocId != null ? String(targetDocId) : hits[0].id; - const orderedIds = hits.map((entry) => entry.id); + const primary = sortedByZ.find((candidate) => candidate.id === targetKey) || sortedByZ[0]; + if (!primary) { + return []; + } + + const radius = Math.max(Math.min(primary.halfWidth, primary.halfHeight) * 1.2, 6); + const radiusSquared = radius * radius; + + const selected = candidates + .filter((candidate) => { + if (!candidate?.id) { + return false; + } + const dx = candidate.centerX - primary.centerX; + const dy = candidate.centerY - primary.centerY; + return dx * dx + dy * dy <= radiusSquared + 1e-4; + }) + .sort((a, b) => (b.z ?? 0) - (a.z ?? 0)); if (targetKey) { - const targetIndex = orderedIds.indexOf(targetKey); + const targetIndex = selected.findIndex((entry) => entry.id === targetKey); if (targetIndex > 0) { - const [targetEntry] = orderedIds.splice(targetIndex, 1); - orderedIds.unshift(targetEntry); + const [targetEntry] = selected.splice(targetIndex, 1); + selected.unshift(targetEntry); } } - const primaryKey = orderedIds[0]; - if (!primaryKey) { - return orderedIds; - } - - const primaryDoc = documentLookup.get(primaryKey) || null; - const primaryLayout = primaryDoc - ? layoutSnapshot.get(primaryDoc.id) ?? layoutRef.current.get(primaryKey) - : null; - const primarySize = primaryDoc ? ensureDocumentSize(primaryDoc) : null; - - if (!primaryLayout || !primarySize) { - return orderedIds; - } - - const primaryCenterX = Number(primaryLayout.centerX); - const primaryCenterY = Number(primaryLayout.centerY); - const primaryRotation = Number(primaryLayout.rotation) || 0; - if (!Number.isFinite(primaryCenterX) || !Number.isFinite(primaryCenterY)) { - return orderedIds; - } - - const centerTolX = Math.max(primarySize.width * STACK_CENTER_TOLERANCE, STACK_CENTER_MIN); - const centerTolY = Math.max(primarySize.height * STACK_CENTER_TOLERANCE, STACK_CENTER_MIN); - - const filteredIds = []; - - orderedIds.forEach((docKey, index) => { - if (!docKey) { - return; - } - if (index === 0 || docKey === targetKey) { - filteredIds.push(docKey); - return; - } - - const candidateDoc = documentLookup.get(docKey) || null; - if (!candidateDoc) { - return; - } - - const candidateLayout = layoutSnapshot.get(candidateDoc.id) ?? layoutRef.current.get(docKey); - if (!candidateLayout) { - return; - } - - const candidateSize = ensureDocumentSize(candidateDoc); - if (!candidateSize) { - return; - } - - const candidateCenterX = Number(candidateLayout.centerX); - const candidateCenterY = Number(candidateLayout.centerY); - if (!Number.isFinite(candidateCenterX) || !Number.isFinite(candidateCenterY)) { - return; - } - - const dx = Math.abs(candidateCenterX - primaryCenterX); - const dy = Math.abs(candidateCenterY - primaryCenterY); - if (dx > centerTolX || dy > centerTolY) { - return; - } - - const candidateRotation = Number(candidateLayout.rotation) || 0; - const rotationDiffRaw = Math.abs(candidateRotation - primaryRotation) % 360; - const rotationDiff = rotationDiffRaw > 180 ? 360 - rotationDiffRaw : rotationDiffRaw; - if (rotationDiff > STACK_ROTATION_TOLERANCE) { - return; - } - - const sizeRatio = candidateSize.width && primarySize.width - ? Math.min(candidateSize.width, primarySize.width) / Math.max(candidateSize.width, primarySize.width) - : 1; - const heightRatio = candidateSize.height && primarySize.height - ? Math.min(candidateSize.height, primarySize.height) / Math.max(candidateSize.height, primarySize.height) - : 1; - - if (sizeRatio < 0.55 || heightRatio < 0.55) { - return; - } - - filteredIds.push(docKey); - }); - - return filteredIds; + return selected + .map((candidate) => candidate.id) + .filter((id, index, array) => array.indexOf(id) === index); }, [ activeTagSet, @@ -2142,7 +2084,6 @@ const DesktopWorkspaceView = () => { layoutRef, layoutSnapshot, containerRef, - documentLookup, ], ); diff --git a/frontend/src/desktop/useDocumentDrag.js b/frontend/src/desktop/useDocumentDrag.js index 2c2b18f..9aaa656 100644 --- a/frontend/src/desktop/useDocumentDrag.js +++ b/frontend/src/desktop/useDocumentDrag.js @@ -380,8 +380,7 @@ const useDocumentDrag = () => { const localPointerOffsetX = pointerOffsetX * cosInitial - pointerOffsetY * sinInitial; const localPointerOffsetY = pointerOffsetX * sinInitial + pointerOffsetY * cosInitial; - const stackRandom = () => Math.random(); - const groupItems = groupDocIds.map((id, index) => { + const groupItems = groupDocIds.map((id) => { const itemDoc = documentLookup.get(id); const itemSize = ensureDocumentSize(itemDoc) || sizeInfo; const itemWidth = itemSize.width || docWidth; @@ -391,10 +390,8 @@ const useDocumentDrag = () => { 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 baseOffsetX = itemCenterX - centerX; + const baseOffsetY = itemCenterY - centerY; const initialRotation = itemEntry?.rotation ?? 0; const targetRotation = initialRotation; return { @@ -403,8 +400,10 @@ const useDocumentDrag = () => { height: itemHeight, currentCenterX: itemCenterX, currentCenterY: itemCenterY, - offsetX, - offsetY, + baseOffsetX, + baseOffsetY, + offsetX: baseOffsetX, + offsetY: baseOffsetY, initialRotation, displayRotation: initialRotation, targetRotation, @@ -599,14 +598,15 @@ const useDocumentDrag = () => { if (isPrimary) { item.currentCenterX = centerX; item.currentCenterY = centerY; - item.offsetX *= 0.92; - item.offsetY *= 0.92; + item.offsetX = item.baseOffsetX ?? 0; + item.offsetY = item.baseOffsetY ?? 0; 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 decay = 0.82; + const currentOffsetX = (item.offsetX ?? item.baseOffsetX ?? 0) * decay; + const currentOffsetY = (item.offsetY ?? item.baseOffsetY ?? 0) * decay; + item.offsetX = Math.abs(currentOffsetX) < 0.5 ? 0 : currentOffsetX; + item.offsetY = Math.abs(currentOffsetY) < 0.5 ? 0 : currentOffsetY; const targetX = centerX + item.offsetX; const targetY = centerY + item.offsetY;