From 0fa1652f72fbf62cd35b57e6412df6a420670d76 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 10 Nov 2025 23:01:35 -0800 Subject: [PATCH] prevent coordinate reset --- src/automerge/AutomergeToTLStore.ts | 21 +++++++++++ src/automerge/useAutomergeStoreV2.ts | 54 ++++++++++++++++++++++++++-- src/routes/Board.tsx | 26 +++++++++++++- src/ui/CustomMainMenu.tsx | 13 +++---- 4 files changed, 104 insertions(+), 10 deletions(-) diff --git a/src/automerge/AutomergeToTLStore.ts b/src/automerge/AutomergeToTLStore.ts index 237072e..9653099 100644 --- a/src/automerge/AutomergeToTLStore.ts +++ b/src/automerge/AutomergeToTLStore.ts @@ -134,6 +134,10 @@ export function applyAutomergePatchesToTLStore( } } + // CRITICAL: Store original x and y before patch application to preserve them + const originalX = (record.typeName === 'shape' && typeof record.x === 'number' && !isNaN(record.x)) ? record.x : undefined + const originalY = (record.typeName === 'shape' && typeof record.y === 'number' && !isNaN(record.y)) ? record.y : undefined + switch (patch.action) { case "insert": { updatedObjects[id] = applyInsertToObject(patch, record) @@ -167,6 +171,23 @@ export function applyAutomergePatchesToTLStore( } } + // CRITICAL: After patch application, ensure x and y coordinates are preserved for shapes + if (updatedObjects[id] && updatedObjects[id].typeName === 'shape') { + const patchedRecord = updatedObjects[id] + // Preserve original x and y if they were valid, otherwise use defaults + if (originalX !== undefined && (typeof patchedRecord.x !== 'number' || patchedRecord.x === null || isNaN(patchedRecord.x))) { + updatedObjects[id] = { ...patchedRecord, x: originalX } + } else if (typeof patchedRecord.x !== 'number' || patchedRecord.x === null || isNaN(patchedRecord.x)) { + updatedObjects[id] = { ...patchedRecord, x: defaultRecord.x || 0 } + } + + if (originalY !== undefined && (typeof patchedRecord.y !== 'number' || patchedRecord.y === null || isNaN(patchedRecord.y))) { + updatedObjects[id] = { ...patchedRecord, y: originalY } + } else if (typeof patchedRecord.y !== 'number' || patchedRecord.y === null || isNaN(patchedRecord.y)) { + updatedObjects[id] = { ...patchedRecord, y: defaultRecord.y || 0 } + } + } + // CRITICAL: Re-check typeName after patch application to ensure it's still correct // Note: obsidian_vault records are skipped above, so we don't need to handle them here }) diff --git a/src/automerge/useAutomergeStoreV2.ts b/src/automerge/useAutomergeStoreV2.ts index 7662345..eebdc17 100644 --- a/src/automerge/useAutomergeStoreV2.ts +++ b/src/automerge/useAutomergeStoreV2.ts @@ -371,7 +371,23 @@ export function useAutomergeStoreV2({ // CRITICAL: Force editor to see shapes by refreshing them multiple times // Sometimes the editor needs multiple updates to detect shapes const refreshShapes = (attempt: number) => { - const shapesToRefresh = existingStoreShapes.map(s => store.get(s.id)).filter((s): s is TLRecord => s !== undefined && s.typeName === 'shape') + // CRITICAL: Preserve x and y coordinates when refreshing shapes + const shapesToRefresh = existingStoreShapes.map(s => { + const shapeFromStore = store.get(s.id) + if (shapeFromStore && shapeFromStore.typeName === 'shape') { + // Preserve original x and y from the store shape data + const originalX = s.x !== undefined && typeof s.x === 'number' && !isNaN(s.x) ? s.x : (shapeFromStore as any).x + const originalY = s.y !== undefined && typeof s.y === 'number' && !isNaN(s.y) ? s.y : (shapeFromStore as any).y + + // Ensure x and y are preserved + if (typeof originalX === 'number' && !isNaN(originalX) && typeof originalY === 'number' && !isNaN(originalY)) { + return { ...shapeFromStore, x: originalX, y: originalY } as TLRecord + } + return shapeFromStore + } + return null + }).filter((s): s is TLRecord => s !== null && s.typeName === 'shape') + if (shapesToRefresh.length > 0) { store.mergeRemoteChanges(() => { // Re-put shapes to trigger editor update @@ -419,7 +435,23 @@ export function useAutomergeStoreV2({ // CRITICAL: Force editor to see shapes by refreshing them multiple times // Sometimes the editor needs multiple updates to detect shapes const refreshShapes = (attempt: number) => { - const shapesToRefresh = currentShapes.map(s => store.get(s.id)).filter((s): s is TLRecord => s !== undefined && s.typeName === 'shape') + // CRITICAL: Preserve x and y coordinates when refreshing shapes + const shapesToRefresh = currentShapes.map(s => { + const shapeFromStore = store.get(s.id) + if (shapeFromStore && shapeFromStore.typeName === 'shape') { + // Preserve original x and y from the store shape data + const originalX = s.x !== undefined && typeof s.x === 'number' && !isNaN(s.x) ? s.x : (shapeFromStore as any).x + const originalY = s.y !== undefined && typeof s.y === 'number' && !isNaN(s.y) ? s.y : (shapeFromStore as any).y + + // Ensure x and y are preserved + if (typeof originalX === 'number' && !isNaN(originalX) && typeof originalY === 'number' && !isNaN(originalY)) { + return { ...shapeFromStore, x: originalX, y: originalY } as TLRecord + } + return shapeFromStore + } + return null + }).filter((s): s is TLRecord => s !== null && s.typeName === 'shape') + if (shapesToRefresh.length > 0) { store.mergeRemoteChanges(() => { // Re-put shapes to trigger editor update @@ -500,7 +532,23 @@ export function useAutomergeStoreV2({ // Sometimes the editor needs multiple updates to detect shapes const refreshShapes = (attempt: number) => { const shapes = store.allRecords().filter((r: any) => r.typeName === 'shape') - const shapesToRefresh = shapes.map(s => store.get(s.id)).filter((s): s is TLRecord => s !== undefined && s.typeName === 'shape') + // CRITICAL: Preserve x and y coordinates when refreshing shapes + const shapesToRefresh = shapes.map(s => { + const shapeFromStore = store.get(s.id) + if (shapeFromStore && shapeFromStore.typeName === 'shape') { + // Preserve original x and y from the store shape data + const originalX = s.x !== undefined && typeof s.x === 'number' && !isNaN(s.x) ? s.x : (shapeFromStore as any).x + const originalY = s.y !== undefined && typeof s.y === 'number' && !isNaN(s.y) ? s.y : (shapeFromStore as any).y + + // Ensure x and y are preserved + if (typeof originalX === 'number' && !isNaN(originalX) && typeof originalY === 'number' && !isNaN(originalY)) { + return { ...shapeFromStore, x: originalX, y: originalY } as TLRecord + } + return shapeFromStore + } + return null + }).filter((s): s is TLRecord => s !== null && s.typeName === 'shape') + if (shapesToRefresh.length > 0) { store.mergeRemoteChanges(() => { // Re-put shapes to trigger editor update diff --git a/src/routes/Board.tsx b/src/routes/Board.tsx index 35b04df..e139e98 100644 --- a/src/routes/Board.tsx +++ b/src/routes/Board.tsx @@ -288,7 +288,22 @@ export function Board() { console.warn(`⚠️ Board: ${storeShapes.length} shapes in store (${storeShapesOnCurrentPage.length} on current page) but editor sees 0. Forcing refresh...`) // Force refresh by re-putting shapes with mergeRemoteChanges - const shapesToRefresh = storeShapesOnCurrentPage.map((s: any) => store.store!.get(s.id)).filter((s): s is TLRecord => s !== undefined && s.typeName === 'shape') + // CRITICAL: Preserve x and y coordinates when refreshing shapes + const shapesToRefresh = storeShapesOnCurrentPage.map((s: any) => { + const shapeFromStore = store.store!.get(s.id) + if (shapeFromStore && shapeFromStore.typeName === 'shape') { + // Preserve original x and y from the store shape data + const originalX = s.x !== undefined && typeof s.x === 'number' && !isNaN(s.x) ? s.x : (shapeFromStore as any).x + const originalY = s.y !== undefined && typeof s.y === 'number' && !isNaN(s.y) ? s.y : (shapeFromStore as any).y + + // Ensure x and y are preserved + if (typeof originalX === 'number' && !isNaN(originalX) && typeof originalY === 'number' && !isNaN(originalY)) { + return { ...shapeFromStore, x: originalX, y: originalY } as TLRecord + } + return shapeFromStore + } + return null + }).filter((s): s is TLRecord => s !== null && s.typeName === 'shape') if (shapesToRefresh.length > 0) { store.store.mergeRemoteChanges(() => { store.store!.put(shapesToRefresh) @@ -337,9 +352,18 @@ export function Board() { const currentStore = store.store const shapesToRefresh = missingShapes.slice(0, 10) // Limit to first 10 to avoid performance issues + // CRITICAL: Preserve x and y coordinates when refreshing shapes const refreshedShapes = shapesToRefresh.map((s: any) => { const shapeFromStore = currentStore.get(s.id) if (shapeFromStore && shapeFromStore.typeName === 'shape') { + // Preserve original x and y from the store shape data + const originalX = s.x !== undefined && typeof s.x === 'number' && !isNaN(s.x) ? s.x : (shapeFromStore as any).x + const originalY = s.y !== undefined && typeof s.y === 'number' && !isNaN(s.y) ? s.y : (shapeFromStore as any).y + + // Ensure x and y are preserved + if (typeof originalX === 'number' && !isNaN(originalX) && typeof originalY === 'number' && !isNaN(originalY)) { + return { ...shapeFromStore, x: originalX, y: originalY } as TLRecord + } return shapeFromStore } return null diff --git a/src/ui/CustomMainMenu.tsx b/src/ui/CustomMainMenu.tsx index b67d57b..7516311 100644 --- a/src/ui/CustomMainMenu.tsx +++ b/src/ui/CustomMainMenu.tsx @@ -202,13 +202,14 @@ export function CustomMainMenu() { } // CRITICAL: Preserve existing coordinates - only set defaults if truly missing - // x/y can be 0, which is a valid coordinate, so check for undefined/null - // Note: validateShapeGeometry already ensures x/y are valid numbers - if (fixedShape.x === undefined || fixedShape.x === null) { - fixedShape.x = Math.random() * 400 + 50 // Random position only if missing + // x/y can be 0, which is a valid coordinate, so check for undefined/null/NaN + // Note: validateShapeGeometry already ensures x/y are valid numbers, but we need to + // handle the case where they might be NaN or Infinity after validation + if (fixedShape.x === undefined || fixedShape.x === null || isNaN(fixedShape.x) || !isFinite(fixedShape.x)) { + fixedShape.x = Math.random() * 400 + 50 // Random position only if missing or invalid } - if (fixedShape.y === undefined || fixedShape.y === null) { - fixedShape.y = Math.random() * 300 + 50 // Random position only if missing + if (fixedShape.y === undefined || fixedShape.y === null || isNaN(fixedShape.y) || !isFinite(fixedShape.y)) { + fixedShape.y = Math.random() * 300 + 50 // Random position only if missing or invalid } // Preserve rotation, isLocked, opacity - only set defaults if missing