diff --git a/src/automerge/AutomergeToTLStore.ts b/src/automerge/AutomergeToTLStore.ts index ad6a3f4..e667f68 100644 --- a/src/automerge/AutomergeToTLStore.ts +++ b/src/automerge/AutomergeToTLStore.ts @@ -135,8 +135,10 @@ export function applyAutomergePatchesToTLStore( } // CRITICAL: Store original x and y before patch application to preserve them + // We need to preserve coordinates from existing records to prevent them from being reset 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 + const hadOriginalCoordinates = originalX !== undefined && originalY !== undefined switch (patch.action) { case "insert": { @@ -172,19 +174,42 @@ export function applyAutomergePatchesToTLStore( } // CRITICAL: After patch application, ensure x and y coordinates are preserved for shapes + // This prevents coordinates from being reset to 0,0 when patches don't include them 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 } - } + const patchedX = (patchedRecord as any).x + const patchedY = (patchedRecord as any).y + const patchedHasValidX = typeof patchedX === 'number' && !isNaN(patchedX) && patchedX !== null && patchedX !== undefined + const patchedHasValidY = typeof patchedY === 'number' && !isNaN(patchedY) && patchedY !== null && patchedY !== undefined - 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: If we had original coordinates, preserve them unless patch explicitly set different valid coordinates + // This prevents coordinates from collapsing to 0,0 after bulk upload + if (hadOriginalCoordinates) { + // Only use patched coordinates if they're explicitly set and different from original + // Otherwise, preserve the original coordinates + if (patchedHasValidX && patchedX !== originalX) { + // Patch explicitly set a different X coordinate - use it + updatedObjects[id] = { ...patchedRecord, x: patchedX } + } else { + // Preserve original X coordinate + updatedObjects[id] = { ...patchedRecord, x: originalX } + } + + if (patchedHasValidY && patchedY !== originalY) { + // Patch explicitly set a different Y coordinate - use it + updatedObjects[id] = { ...updatedObjects[id], y: patchedY } as TLRecord + } else { + // Preserve original Y coordinate + updatedObjects[id] = { ...updatedObjects[id], y: originalY } as TLRecord + } + } else { + // No original coordinates - use patched values or defaults + if (!patchedHasValidX) { + updatedObjects[id] = { ...patchedRecord, x: defaultRecord.x || 0 } + } + if (!patchedHasValidY) { + updatedObjects[id] = { ...updatedObjects[id], y: defaultRecord.y || 0 } as TLRecord + } } } diff --git a/src/automerge/TLStoreToAutomerge.ts b/src/automerge/TLStoreToAutomerge.ts index 1e369cf..311a4a4 100644 --- a/src/automerge/TLStoreToAutomerge.ts +++ b/src/automerge/TLStoreToAutomerge.ts @@ -55,8 +55,15 @@ function sanitizeRecord(record: TLRecord): TLRecord { // Ensure required top-level fields exist if (sanitized.typeName === 'shape') { - if (typeof sanitized.x !== 'number') sanitized.x = 0 - if (typeof sanitized.y !== 'number') sanitized.y = 0 + // CRITICAL: Only set defaults if coordinates are truly missing or invalid + // DO NOT overwrite valid coordinates (including 0, which is a valid position) + // Only set to 0 if the value is undefined, null, or NaN + if (sanitized.x === undefined || sanitized.x === null || (typeof sanitized.x === 'number' && isNaN(sanitized.x))) { + sanitized.x = 0 + } + if (sanitized.y === undefined || sanitized.y === null || (typeof sanitized.y === 'number' && isNaN(sanitized.y))) { + sanitized.y = 0 + } if (typeof sanitized.rotation !== 'number') sanitized.rotation = 0 if (typeof sanitized.isLocked !== 'boolean') sanitized.isLocked = false if (typeof sanitized.opacity !== 'number') sanitized.opacity = 1 @@ -311,8 +318,29 @@ export function applyTLStoreChangesToAutomerge( // Handle added records if (changes.added) { Object.values(changes.added).forEach((record) => { + // CRITICAL: For shapes, preserve x and y coordinates before sanitization + // This ensures coordinates aren't lost when saving to Automerge + let originalX: number | undefined = undefined + let originalY: number | undefined = undefined + if (record.typeName === 'shape') { + originalX = (record as any).x + originalY = (record as any).y + } + // Sanitize record before saving to ensure all required fields are present const sanitizedRecord = sanitizeRecord(record) + + // CRITICAL: Restore original coordinates if they were valid + // This prevents coordinates from being reset to 0,0 when saving to Automerge + if (record.typeName === 'shape' && originalX !== undefined && originalY !== undefined) { + if (typeof originalX === 'number' && !isNaN(originalX) && originalX !== null) { + (sanitizedRecord as any).x = originalX + } + if (typeof originalY === 'number' && !isNaN(originalY) && originalY !== null) { + (sanitizedRecord as any).y = originalY + } + } + // CRITICAL: Create a deep copy to ensure all properties (including richText and text) are preserved // This prevents Automerge from treating the object as read-only const recordToSave = JSON.parse(JSON.stringify(sanitizedRecord)) @@ -326,6 +354,14 @@ export function applyTLStoreChangesToAutomerge( // This is simpler than deep comparison and leverages Automerge's conflict resolution if (changes.updated) { Object.values(changes.updated).forEach(([_, record]) => { + // CRITICAL: For shapes, preserve x and y coordinates before sanitization + // This ensures coordinates aren't lost when updating records in Automerge + let originalX: number | undefined = undefined + let originalY: number | undefined = undefined + if (record.typeName === 'shape') { + originalX = (record as any).x + originalY = (record as any).y + } // DEBUG: Log richText, meta.text, and Obsidian note properties before sanitization if (record.typeName === 'shape') { if (record.type === 'geo' && (record.props as any)?.richText) { @@ -371,6 +407,17 @@ export function applyTLStoreChangesToAutomerge( const sanitizedRecord = sanitizeRecord(record) + // CRITICAL: Restore original coordinates if they were valid + // This prevents coordinates from being reset to 0,0 when updating records in Automerge + if (record.typeName === 'shape' && originalX !== undefined && originalY !== undefined) { + if (typeof originalX === 'number' && !isNaN(originalX) && originalX !== null) { + (sanitizedRecord as any).x = originalX + } + if (typeof originalY === 'number' && !isNaN(originalY) && originalY !== null) { + (sanitizedRecord as any).y = originalY + } + } + // DEBUG: Log richText, meta.text, and Obsidian note properties after sanitization if (sanitizedRecord.typeName === 'shape') { if (sanitizedRecord.type === 'geo' && (sanitizedRecord.props as any)?.richText) { diff --git a/src/automerge/useAutomergeStoreV2.ts b/src/automerge/useAutomergeStoreV2.ts index 45d55bb..5fe8cf3 100644 --- a/src/automerge/useAutomergeStoreV2.ts +++ b/src/automerge/useAutomergeStoreV2.ts @@ -436,23 +436,36 @@ export function useAutomergeStoreV2({ // Create a clean copy of the record const cleanRecord = JSON.parse(JSON.stringify(record)) - // CRITICAL: For shapes, preserve x and y coordinates BEFORE sanitization - // This ensures coordinates aren't lost during the sanitization process + // CRITICAL: For shapes, preserve x and y coordinates + // We MUST preserve coordinates - they should never be reset to 0,0 unless truly missing if (cleanRecord.typeName === 'shape') { + // Store original coordinates BEFORE any processing const originalX = cleanRecord.x const originalY = cleanRecord.y + const hadValidX = typeof originalX === 'number' && !isNaN(originalX) && originalX !== null && originalX !== undefined + const hadValidY = typeof originalY === 'number' && !isNaN(originalY) && originalY !== null && originalY !== undefined // Use the same sanitizeRecord function that patches use // This ensures consistency between dev and production const sanitized = sanitizeRecord(cleanRecord) - // CRITICAL: Restore original coordinates if they were valid - // sanitizeRecord only sets defaults if coordinates are missing/invalid - // But we want to preserve the original values if they exist - if (typeof originalX === 'number' && !isNaN(originalX) && originalX !== null && originalX !== undefined) { + // CRITICAL: ALWAYS restore original coordinates if they were valid + // Even if sanitizeRecord preserved them, we ensure they're correct + // This prevents any possibility of coordinates being reset + if (hadValidX) { (sanitized as any).x = originalX } - if (typeof originalY === 'number' && !isNaN(originalY) && originalY !== null && originalY !== undefined) { + if (hadValidY) { + (sanitized as any).y = originalY + } + + // Log if coordinates were changed (for debugging) + if (hadValidX && (sanitized as any).x !== originalX) { + console.warn(`⚠️ Coordinate X was changed during sanitization for shape ${cleanRecord.id}: ${originalX} -> ${(sanitized as any).x}. Restored to ${originalX}.`) + (sanitized as any).x = originalX + } + if (hadValidY && (sanitized as any).y !== originalY) { + console.warn(`⚠️ Coordinate Y was changed during sanitization for shape ${cleanRecord.id}: ${originalY} -> ${(sanitized as any).y}. Restored to ${originalY}.`) (sanitized as any).y = originalY }