This commit is contained in:
2025-11-06 23:11:22 +01:00
parent 07169e853b
commit 5b88ecf2dd
3 changed files with 102 additions and 52 deletions
+3 -11
View File
@@ -242,10 +242,6 @@ const DesktopWorkspace = ({
engine.recalcVisibleDocIds(); engine.recalcVisibleDocIds();
}, [engine]); }, [engine]);
const syncLayoutSnapshot = useCallback((force = false) => {
engine.syncLayoutSnapshot(force);
}, [engine]);
const setDraggingId = useCallback((value) => { const setDraggingId = useCallback((value) => {
engine.setDraggingId(value); engine.setDraggingId(value);
}, [engine]); }, [engine]);
@@ -666,7 +662,6 @@ const DesktopWorkspace = ({
resolveBaseMetrics, resolveBaseMetrics,
bringToFront, bringToFront,
setDraggingId, setDraggingId,
syncLayoutSnapshot,
canvasSize, canvasSize,
openOverlayForDoc, openOverlayForDoc,
recalcVisibleDocIds, recalcVisibleDocIds,
@@ -725,7 +720,6 @@ const DesktopWorkspace = ({
markLayoutDirty, markLayoutDirty,
tagDropTargetId, tagDropTargetId,
visibleDocIds, visibleDocIds,
syncLayoutSnapshot,
], ],
); );
return ( return (
@@ -778,10 +772,9 @@ const DesktopWorkspaceView = ({
onCloseDetailPanel, onCloseDetailPanel,
documentLookup, documentLookup,
resolveBaseMetrics, resolveBaseMetrics,
bringToFront, bringToFront,
setDraggingId, setDraggingId,
syncLayoutSnapshot, canvasSize,
canvasSize,
openOverlayForDoc, openOverlayForDoc,
recalcVisibleDocIds, recalcVisibleDocIds,
dragSettings, dragSettings,
@@ -800,7 +793,6 @@ const DesktopWorkspaceView = ({
resolveBaseMetrics, resolveBaseMetrics,
bringToFront, bringToFront,
setDraggingId, setDraggingId,
syncLayoutSnapshot,
canvasSize, canvasSize,
openOverlayForDoc, openOverlayForDoc,
recalcVisibleDocIds, recalcVisibleDocIds,
+31 -26
View File
@@ -19,7 +19,6 @@ const useDocumentDrag = (options = {}) => {
resolveBaseMetrics, resolveBaseMetrics,
bringToFront, bringToFront,
setDraggingId, setDraggingId,
syncLayoutSnapshot,
canvasSize, canvasSize,
openOverlayForDoc, openOverlayForDoc,
recalcVisibleDocIds, recalcVisibleDocIds,
@@ -114,7 +113,7 @@ const useDocumentDrag = (options = {}) => {
}, [dragTransformsRef, layoutRef, markLayoutDirty]); }, [dragTransformsRef, layoutRef, markLayoutDirty]);
const finishDrag = useCallback( const finishDrag = useCallback(
(pointerId, { shouldSync = false, clearTransforms = true } = {}) => { (pointerId, { clearTransforms = true } = {}) => {
const state = dragStateRef.current; const state = dragStateRef.current;
if (state && state.pointerId === pointerId) { if (state && state.pointerId === pointerId) {
const capturedTarget = state.capturedTarget; const capturedTarget = state.capturedTarget;
@@ -134,11 +133,8 @@ const useDocumentDrag = (options = {}) => {
if (clearTransforms) { if (clearTransforms) {
clearDragTransforms(); clearDragTransforms();
} }
if (shouldSync) {
syncLayoutSnapshot(true);
}
}, },
[clearDragTransforms, debugDrag, engine, setDraggingId, syncLayoutSnapshot], [clearDragTransforms, debugDrag, engine, setDraggingId],
); );
const handlePointerDown = useCallback( const handlePointerDown = useCallback(
@@ -176,7 +172,7 @@ const useDocumentDrag = (options = {}) => {
: []; : [];
if (!stackDocIdsOption && !pointerModifierActive && !wasSelectedAtPointerDown) { if (!stackDocIdsOption && !pointerModifierActive && !wasSelectedAtPointerDown) {
selectionIds = []; selectionIds = [docKey];
} }
if (stackDocIdsOption && stackDocIdsOption.length) { if (stackDocIdsOption && stackDocIdsOption.length) {
@@ -193,20 +189,23 @@ const useDocumentDrag = (options = {}) => {
if (!stackDocIdsOption && metaOrCtrl && !selectionIds.includes(docKey)) { if (!stackDocIdsOption && metaOrCtrl && !selectionIds.includes(docKey)) {
selectionIds = [...selectionIds, docKey]; selectionIds = [...selectionIds, docKey];
} }
let groupDocIds = selectionIds
selectionIds = selectionIds
.map((id) => String(id)) .map((id) => String(id))
.filter((id, index, array) => array.indexOf(id) === index && documentLookup.has(id)); .filter((id, index, array) => array.indexOf(id) === index && documentLookup.has(id));
if (!groupDocIds.includes(docKey)) {
groupDocIds.unshift(docKey); if (!selectionIds.includes(docKey)) {
selectionIds.unshift(docKey);
} }
groupDocIds = groupDocIds.filter((id, index, array) => array.indexOf(id) === index);
if (!groupDocIds.length) { if (!selectionIds.length) {
groupDocIds = [docKey]; selectionIds = [docKey];
} }
const isGroupDrag = groupDocIds.length > 1;
const isGroupDrag = selectionIds.length > 1;
if (isGroupDrag) { if (isGroupDrag) {
groupDocIds.forEach((id) => { selectionIds.forEach((id) => {
if (id !== docKey) { if (id !== docKey) {
engine?.cancelInertiaAnimation?.(id); engine?.cancelInertiaAnimation?.(id);
} }
@@ -230,7 +229,7 @@ const useDocumentDrag = (options = {}) => {
if (!modifierPressed) { if (!modifierPressed) {
if (isGroupDrag) { if (isGroupDrag) {
const layout = layoutRef.current; const layout = layoutRef.current;
const ordered = [...groupDocIds] const ordered = [...selectionIds]
.filter((id, index, array) => array.indexOf(id) === index) .filter((id, index, array) => array.indexOf(id) === index)
.sort((a, b) => { .sort((a, b) => {
const aZ = layout.get(a)?.z ?? 0; const aZ = layout.get(a)?.z ?? 0;
@@ -275,7 +274,7 @@ const useDocumentDrag = (options = {}) => {
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 groupItems = groupDocIds.map((id) => { const groupItems = selectionIds.map((id) => {
const itemDoc = documentLookup.get(id); const itemDoc = documentLookup.get(id);
const itemSize = ensureDocumentSize(itemDoc) || sizeInfo; const itemSize = ensureDocumentSize(itemDoc) || sizeInfo;
const itemWidth = itemSize.width || docWidth; const itemWidth = itemSize.width || docWidth;
@@ -343,7 +342,7 @@ const useDocumentDrag = (options = {}) => {
containerRectLeft: containerLeft, containerRectLeft: containerLeft,
containerRectTop: containerTop, containerRectTop: containerTop,
isGroup: isGroupDrag, isGroup: isGroupDrag,
groupDocIds, activeDocIds: selectionIds,
groupItems, groupItems,
groupElevated: !isGroupDrag, groupElevated: !isGroupDrag,
stackDocIds: hasStackSource ? stackDocIdsOption : null, stackDocIds: hasStackSource ? stackDocIdsOption : null,
@@ -368,7 +367,7 @@ const useDocumentDrag = (options = {}) => {
}); });
}); });
engine?.beginDrag?.(state.groupDocIds); engine?.beginDrag?.(state.activeDocIds);
setDraggingId(docKey); setDraggingId(docKey);
@@ -380,6 +379,7 @@ const useDocumentDrag = (options = {}) => {
const node = itemRefs.current.get(item.docId); const node = itemRefs.current.get(item.docId);
if (node) { if (node) {
item.displayRotation = item.initialRotation; item.displayRotation = item.initialRotation;
const itemEntry = layoutRef.current.get(item.docId) || null;
applyDomTransform(node, { applyDomTransform(node, {
centerX: item.currentCenterX, centerX: item.currentCenterX,
centerY: item.currentCenterY, centerY: item.currentCenterY,
@@ -387,6 +387,7 @@ const useDocumentDrag = (options = {}) => {
height: item.height, height: item.height,
rotation: item.displayRotation ?? 0, rotation: item.displayRotation ?? 0,
scale: 1, scale: 1,
zIndex: itemEntry?.z,
}); });
} }
}); });
@@ -447,7 +448,7 @@ const useDocumentDrag = (options = {}) => {
} }
if (!state.groupElevated) { if (!state.groupElevated) {
const layout = layoutRef.current; const layout = layoutRef.current;
const sortedGroup = state.groupDocIds const sortedGroup = state.activeDocIds
.filter((id) => id !== state.docKey) .filter((id) => id !== state.docKey)
.sort((a, b) => { .sort((a, b) => {
const aZ = layout.get(a)?.z ?? 0; const aZ = layout.get(a)?.z ?? 0;
@@ -515,6 +516,7 @@ const useDocumentDrag = (options = {}) => {
item.displayRotation += (item.targetRotation - item.displayRotation) * rotationBlend; item.displayRotation += (item.targetRotation - item.displayRotation) * rotationBlend;
} }
const entry = layoutRef.current.get(item.docId) || null;
const payload = { const payload = {
centerX: item.currentCenterX, centerX: item.currentCenterX,
centerY: item.currentCenterY, centerY: item.currentCenterY,
@@ -522,6 +524,7 @@ const useDocumentDrag = (options = {}) => {
width: item.width, width: item.width,
height: item.height, height: item.height,
scale: isPrimary ? state.dragScale || 1 : 1, scale: isPrimary ? state.dragScale || 1 : 1,
zIndex: entry?.z,
}; };
setDragTransform(item.docId, payload); setDragTransform(item.docId, payload);
@@ -630,6 +633,7 @@ const useDocumentDrag = (options = {}) => {
state.currentCenterX = currentCenterX; state.currentCenterX = currentCenterX;
state.currentCenterY = currentCenterY; state.currentCenterY = currentCenterY;
const layoutEntry = layoutRef.current.get(state.docKey) || null;
const transformPayload = { const transformPayload = {
centerX: currentCenterX, centerX: currentCenterX,
centerY: currentCenterY, centerY: currentCenterY,
@@ -637,6 +641,7 @@ const useDocumentDrag = (options = {}) => {
width: state.width, width: state.width,
height: state.height, height: state.height,
scale: state.dragScale || 1, scale: state.dragScale || 1,
zIndex: layoutEntry?.z,
}; };
setDragTransform(state.docKey, transformPayload); setDragTransform(state.docKey, transformPayload);
@@ -710,8 +715,8 @@ const useDocumentDrag = (options = {}) => {
if (state.isGroup) { if (state.isGroup) {
engine?.finalizeGroupDrag?.(state); engine?.finalizeGroupDrag?.(state);
commitActiveDragTransforms(state.groupDocIds); commitActiveDragTransforms(state.activeDocIds);
finishDrag(event.pointerId, { shouldSync: true }); finishDrag(event.pointerId);
recalcVisibleDocIds(); recalcVisibleDocIds();
return; return;
} }
@@ -728,7 +733,7 @@ const useDocumentDrag = (options = {}) => {
dragScale: state.dragScale || 1, dragScale: state.dragScale || 1,
}; };
const docId = state.docKey; const docId = state.docKey;
finishDrag(event.pointerId, { shouldSync: true }); finishDrag(event.pointerId);
engine?.startInertiaAnimation?.(docId, inertiaState); engine?.startInertiaAnimation?.(docId, inertiaState);
return; return;
} }
@@ -770,8 +775,8 @@ const useDocumentDrag = (options = {}) => {
if (state && state.pointerId === event.pointerId && state.moved) { if (state && state.pointerId === event.pointerId && state.moved) {
if (state.isGroup) { if (state.isGroup) {
engine?.finalizeGroupDrag?.(state); engine?.finalizeGroupDrag?.(state);
commitActiveDragTransforms(state.groupDocIds); commitActiveDragTransforms(state.activeDocIds);
finishDrag(event.pointerId, { shouldSync: true }); finishDrag(event.pointerId);
recalcVisibleDocIds(); recalcVisibleDocIds();
return; return;
} }
@@ -787,7 +792,7 @@ const useDocumentDrag = (options = {}) => {
dragScale: state.dragScale || 1, dragScale: state.dragScale || 1,
}; };
const docId = state.docKey; const docId = state.docKey;
finishDrag(event.pointerId, { shouldSync: true }); finishDrag(event.pointerId);
engine?.startInertiaAnimation?.(docId, inertiaState); engine?.startInertiaAnimation?.(docId, inertiaState);
return; return;
} }
+68 -15
View File
@@ -26,6 +26,7 @@ export const applyDomTransform = (
height, height,
rotation = 0, rotation = 0,
scale = 1, scale = 1,
zIndex,
} = {}, } = {},
) => { ) => {
if (!node) { if (!node) {
@@ -38,6 +39,9 @@ export const applyDomTransform = (
const originX = cx - w / 2; const originX = cx - w / 2;
const originY = cy - h / 2; const originY = cy - h / 2;
node.style.transform = formatTransform(originX, originY, rotation || 0, scale || 1); node.style.transform = formatTransform(originX, originY, rotation || 0, scale || 1);
if (zIndex != null && node.style.zIndex !== String(zIndex)) {
node.style.zIndex = String(zIndex);
}
}; };
export const clampCardDimensions = (width, height) => { export const clampCardDimensions = (width, height) => {
@@ -399,6 +403,11 @@ export class WorkspaceEngine {
this.pendingRemovalTag = null; this.pendingRemovalTag = null;
this.dragInProgress = false; this.dragInProgress = false;
this.activeDragDocIds = new Set(); this.activeDragDocIds = new Set();
this.pendingSnapshotSync = false;
this.pendingPersistSync = false;
this.persistDebounceId = null;
this.pendingSnapshotSync = false;
this.pendingPersistSync = false;
this.items = []; this.items = [];
this.documentLookup = new Map(); this.documentLookup = new Map();
@@ -529,6 +538,16 @@ export class WorkspaceEngine {
endDrag() { endDrag() {
this.dragInProgress = false; this.dragInProgress = false;
this.activeDragDocIds.clear(); this.activeDragDocIds.clear();
this.flushPendingLayoutOps();
}
flushPendingLayoutOps() {
if (this.pendingSnapshotSync) {
this.syncLayoutSnapshot();
}
if (this.pendingPersistSync) {
this.persistLayoutSnapshot();
}
} }
setTagDropTargetId(docId) { setTagDropTargetId(docId) {
@@ -583,6 +602,7 @@ export class WorkspaceEngine {
} }
this.markLayoutDirty(); this.markLayoutDirty();
this.syncLayoutSnapshot(); this.syncLayoutSnapshot();
this.persistLayoutSnapshot();
} }
bringToFront(docId) { bringToFront(docId) {
@@ -598,10 +618,11 @@ export class WorkspaceEngine {
this.layout.set(key, { ...entry, z: this.zCounter }); this.layout.set(key, { ...entry, z: this.zCounter });
this.markLayoutDirty(); this.markLayoutDirty();
this.syncLayoutSnapshot(); this.syncLayoutSnapshot();
this.persistLayoutSnapshot();
this.recalcVisibleDocIds(); this.recalcVisibleDocIds();
} }
applyTransform(docId, centerX, centerY, width, height, rotation, scale = 1) { applyTransform(docId, centerX, centerY, width, height, rotation, scale = 1, zIndex = null) {
const key = docId != null ? String(docId) : null; const key = docId != null ? String(docId) : null;
if (!key) { if (!key) {
return; return;
@@ -614,6 +635,7 @@ export class WorkspaceEngine {
height, height,
rotation, rotation,
scale, scale,
zIndex,
}); });
} }
@@ -635,12 +657,14 @@ export class WorkspaceEngine {
const centerY = item.currentCenterY ?? entry.centerY ?? dragState.originCenterY; const centerY = item.currentCenterY ?? entry.centerY ?? dragState.originCenterY;
const rotation = item.displayRotation ?? entry.rotation ?? 0; const rotation = item.displayRotation ?? entry.rotation ?? 0;
this.layout.set(key, { const nextEntry = {
...entry, ...entry,
centerX, centerX,
centerY, centerY,
rotation, rotation,
}); };
this.layout.set(key, nextEntry);
this.applyTransform( this.applyTransform(
key, key,
@@ -650,10 +674,13 @@ export class WorkspaceEngine {
item.height, item.height,
rotation, rotation,
key === dragState.docKey ? dragState.dragScale || 1 : 1, key === dragState.docKey ? dragState.dragScale || 1 : 1,
nextEntry.z,
); );
}); });
this.markLayoutDirty(); this.markLayoutDirty();
this.syncLayoutSnapshot();
this.persistLayoutSnapshot();
} }
cancelInertiaAnimation(docId) { cancelInertiaAnimation(docId) {
@@ -721,7 +748,8 @@ export class WorkspaceEngine {
simulationState.rotation = simulationState.restRotation + dynamicRotation; simulationState.rotation = simulationState.restRotation + dynamicRotation;
const rotation = simulationState.rotation; const rotation = simulationState.rotation;
this.layout.set(key, { ...entry, rotation }); const nextEntry = { ...entry, rotation };
this.layout.set(key, nextEntry);
this.markLayoutDirty(); this.markLayoutDirty();
this.applyTransform( this.applyTransform(
@@ -732,6 +760,7 @@ export class WorkspaceEngine {
simulationState.height, simulationState.height,
rotation, rotation,
simulationState.dragScale || 1, simulationState.dragScale || 1,
nextEntry.z,
); );
const isSettled = Math.abs(angularVelocity) < SETTLE_ANGULAR_VELOCITY; const isSettled = Math.abs(angularVelocity) < SETTLE_ANGULAR_VELOCITY;
@@ -775,6 +804,7 @@ export class WorkspaceEngine {
if (settled) { if (settled) {
this.inertiaAnimations.delete(key); this.inertiaAnimations.delete(key);
this.syncLayoutSnapshot(); this.syncLayoutSnapshot();
this.persistLayoutSnapshot();
return; return;
} }
simulationState.frameId = window.requestAnimationFrame(step); simulationState.frameId = window.requestAnimationFrame(step);
@@ -784,27 +814,38 @@ export class WorkspaceEngine {
this.inertiaAnimations.set(key, simulationState); this.inertiaAnimations.set(key, simulationState);
} }
syncLayoutSnapshot(force = false) { syncLayoutSnapshot() {
if (this.dragInProgress && !force) { if (this.dragInProgress) {
this.pendingSnapshotSync = true;
return; return;
} }
this.pendingSnapshotSync = false;
this.layoutSnapshot = new Map(this.layout); this.layoutSnapshot = new Map(this.layout);
this.emit(); this.emit();
this.persistLayoutSnapshot(force);
} }
async persistLayoutSnapshot(force = false) { async persistLayoutSnapshot() {
if (this.dragInProgress && !force) { if (this.dragInProgress) {
this.pendingPersistSync = true;
return; return;
} }
if (!this.allowLayoutPersistence || !this.tenantId || !this.viewId) { if (!this.allowLayoutPersistence || !this.tenantId || !this.viewId) {
this.pendingPersistSync = false;
return; return;
} }
if (!force && !this.layoutDirty) { if (!this.layoutDirty && !this.pendingPersistSync) {
return; return;
} }
this.pendingPersistSync = false;
this.layoutDirty = false; this.layoutDirty = false;
const snapshot = new Map(this.layoutSnapshot); if (this.persistDebounceId) {
clearTimeout(this.persistDebounceId);
this.persistDebounceId = null;
}
const snapshotSource = this.layoutSnapshot && this.layoutSnapshot.size
? this.layoutSnapshot
: this.layout;
const snapshot = new Map(snapshotSource);
const merged = new Map(this.persistedLayout); const merged = new Map(this.persistedLayout);
snapshot.forEach((entry, docId) => { snapshot.forEach((entry, docId) => {
if (!docId || !entry) { if (!docId || !entry) {
@@ -836,10 +877,21 @@ export class WorkspaceEngine {
}); });
}); });
try { const persistTask = async () => {
await upsertLayoutRecords({ tenantId: this.tenantId, viewId: this.viewId, entries: records }); try {
} catch (error) { await upsertLayoutRecords({ tenantId: this.tenantId, viewId: this.viewId, entries: records });
console.warn('[desk] Failed to persist layout snapshot', error); } catch (error) {
console.warn('[desk] Failed to persist layout snapshot', error);
}
};
if (typeof window !== 'undefined' && typeof window.setTimeout === 'function') {
this.persistDebounceId = window.setTimeout(() => {
this.persistDebounceId = null;
void persistTask();
}, 100);
} else {
await persistTask();
} }
} }
@@ -940,6 +992,7 @@ export class WorkspaceEngine {
this.layout = next; this.layout = next;
this.zCounter = Math.max(this.zCounter, maxZ); this.zCounter = Math.max(this.zCounter, maxZ);
this.syncLayoutSnapshot(); this.syncLayoutSnapshot();
this.persistLayoutSnapshot();
this.recalcVisibleDocIds(); this.recalcVisibleDocIds();
} }