From e69fcad45708ece8884ccb3775bcbd87729c6031 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 19 Nov 2025 16:14:23 -0700 Subject: [PATCH] fix: preserve coordinates and convert geo shape text during JSON import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix coordinate collapse bug where shapes were resetting to (0,0) - Convert geo shape props.text to props.richText (tldraw schema change) - Preserve text in meta.text for backward compatibility - Add .nvmrc to enforce Node 20 - Update package.json to require Node >=20.0.0 - Add debug logging for sync and import operations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .nvmrc | 1 + package.json | 2 +- src/automerge/useAutomergeStoreV2.ts | 12 +++ src/ui/CustomMainMenu.tsx | 143 ++++++++++++++++++++++++--- 4 files changed, 144 insertions(+), 14 deletions(-) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/package.json b/package.json index 7957dcd..5944c12 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,6 @@ "wrangler": "^4.33.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/src/automerge/useAutomergeStoreV2.ts b/src/automerge/useAutomergeStoreV2.ts index 679e104..d9c9c3f 100644 --- a/src/automerge/useAutomergeStoreV2.ts +++ b/src/automerge/useAutomergeStoreV2.ts @@ -210,8 +210,15 @@ export function useAutomergeStoreV2({ // The Automerge Repo doesn't auto-broadcast because our WebSocket setup doesn't use peer discovery const triggerSync = () => { try { + console.log('🔄 triggerSync() called') const repo = (handle as any).repo + console.log('🔍 repo:', !!repo, 'handle:', !!handle, 'documentId:', handle?.documentId) + if (repo) { + console.log('🔍 repo.networkSubsystem:', !!repo.networkSubsystem) + console.log('🔍 repo.networkSubsystem.syncDoc:', typeof repo.networkSubsystem?.syncDoc) + console.log('🔍 repo.networkSubsystem.adapters:', !!repo.networkSubsystem?.adapters) + // Try multiple approaches to trigger sync // Approach 1: Use networkSubsystem.syncDoc if available @@ -223,10 +230,13 @@ export function useAutomergeStoreV2({ else if (repo.networkSubsystem && repo.networkSubsystem.adapters) { console.log('🔄 Broadcasting sync to all network adapters') const adapters = Array.from(repo.networkSubsystem.adapters.values()) + console.log('🔍 Found adapters:', adapters.length) adapters.forEach((adapter: any) => { + console.log('🔍 Adapter has send:', typeof adapter?.send) if (adapter && typeof adapter.send === 'function') { // Send a sync message via the adapter // The adapter should handle converting this to the right format + console.log('📤 Sending sync via adapter') adapter.send({ type: 'sync', documentId: handle.documentId, @@ -243,6 +253,8 @@ export function useAutomergeStoreV2({ else { console.warn('⚠️ No known method to trigger sync broadcast found') } + } else { + console.warn('⚠️ No repo found on handle') } } catch (error) { console.error('❌ Error triggering manual sync:', error) diff --git a/src/ui/CustomMainMenu.tsx b/src/ui/CustomMainMenu.tsx index c450c4f..d4d4b25 100644 --- a/src/ui/CustomMainMenu.tsx +++ b/src/ui/CustomMainMenu.tsx @@ -63,10 +63,24 @@ export function CustomMainMenu() { // Helper function to validate shape geometry data const validateShapeGeometry = (shape: any): boolean => { if (!shape || !shape.id) return false - - // Validate basic numeric properties - shape.x = validateNumericValue(shape.x, 0, 'x') - shape.y = validateNumericValue(shape.y, 0, 'y') + + // CRITICAL: Only validate that x/y are valid numbers if they exist + // DO NOT set default values here - let fixIncompleteShape handle that + // This preserves original coordinates and prevents coordinate collapse + if (shape.x !== undefined && shape.x !== null) { + if (typeof shape.x !== 'number' || isNaN(shape.x) || !isFinite(shape.x)) { + console.warn(`⚠️ Invalid x coordinate for shape ${shape.id}:`, shape.x) + shape.x = undefined // Mark as invalid so fixIncompleteShape can handle it + } + } + if (shape.y !== undefined && shape.y !== null) { + if (typeof shape.y !== 'number' || isNaN(shape.y) || !isFinite(shape.y)) { + console.warn(`⚠️ Invalid y coordinate for shape ${shape.id}:`, shape.y) + shape.y = undefined // Mark as invalid so fixIncompleteShape can handle it + } + } + + // Validate rotation and opacity with defaults (these are safe to default) shape.rotation = validateNumericValue(shape.rotation, 0, 'rotation') shape.opacity = validateNumericValue(shape.opacity, 1, 'opacity') @@ -164,12 +178,21 @@ export function CustomMainMenu() { // Function to fix incomplete shape data for proper rendering const fixIncompleteShape = (shape: any, pageId: string): any => { const fixedShape = { ...shape } - + + // DEBUG: Log coordinates before validation + const originalX = fixedShape.x + const originalY = fixedShape.y + // CRITICAL: Validate geometry first (fixes NaN/Infinity values) if (!validateShapeGeometry(fixedShape)) { console.warn(`⚠️ Shape failed geometry validation, skipping:`, fixedShape.id) return null // Return null to indicate shape should be skipped } + + // DEBUG: Log if coordinates changed during validation + if (originalX !== fixedShape.x || originalY !== fixedShape.y) { + console.log(`🔍 Coordinates changed during validation for ${fixedShape.id}: (${originalX},${originalY}) → (${fixedShape.x},${fixedShape.y})`) + } // CRITICAL: Validate and normalize shape type const normalizedType = validateAndNormalizeShapeType(fixedShape) @@ -239,24 +262,51 @@ export function CustomMainMenu() { const wValue = fixedShape.w !== undefined ? fixedShape.w : 100 const hValue = fixedShape.h !== undefined ? fixedShape.h : 100 const geoValue = fixedShape.geo !== undefined ? fixedShape.geo : 'rectangle' - + // Remove w/h/geo from top level (TLDraw validation requires they be in props only) delete fixedShape.w delete fixedShape.h delete fixedShape.geo - + // Ensure props exists and has the correct values if (!fixedShape.props) fixedShape.props = {} if (fixedShape.props.w === undefined) fixedShape.props.w = wValue if (fixedShape.props.h === undefined) fixedShape.props.h = hValue if (fixedShape.props.geo === undefined) fixedShape.props.geo = geoValue - + // Set default props if missing if (!fixedShape.props.color) fixedShape.props.color = 'black' if (!fixedShape.props.fill) fixedShape.props.fill = 'none' if (!fixedShape.props.dash) fixedShape.props.dash = 'draw' if (!fixedShape.props.size) fixedShape.props.size = 'm' if (!fixedShape.props.font) fixedShape.props.font = 'draw' + + // CRITICAL: Convert props.text to props.richText for geo shapes (tldraw schema change) + // tldraw no longer accepts props.text on geo shapes - must use richText + // Also preserve in meta.text for backward compatibility (used by search and runLLMprompt) + if ('text' in fixedShape.props && typeof fixedShape.props.text === 'string') { + const textContent = fixedShape.props.text + + // Convert text string to richText format for tldraw + fixedShape.props.richText = { + type: 'doc', + content: textContent ? [{ + type: 'paragraph', + content: [{ + type: 'text', + text: textContent + }] + }] : [] + } + + // CRITICAL: Preserve original text in meta.text for backward compatibility + // This is used by search (src/utils/searchUtils.ts) and other legacy code + if (!fixedShape.meta) fixedShape.meta = {} + fixedShape.meta.text = textContent + + // Remove invalid props.text + delete fixedShape.props.text + } } else if (fixedShape.type === 'VideoChat') { // VideoChat shapes also need w/h in props, not top level const wValue = fixedShape.w !== undefined ? fixedShape.w : 200 @@ -492,17 +542,44 @@ export function CustomMainMenu() { const wValue = 'w' in shape ? shape.w : undefined const hValue = 'h' in shape ? shape.h : undefined const geoValue = 'geo' in shape ? shape.geo : undefined - + // Remove from top level delete shape.w delete shape.h delete shape.geo - + // Ensure props exists and move values there if (!shape.props) shape.props = {} if (wValue !== undefined && !shape.props.w) shape.props.w = wValue if (hValue !== undefined && !shape.props.h) shape.props.h = hValue if (geoValue !== undefined && !shape.props.geo) shape.props.geo = geoValue + + // CRITICAL: Convert props.text to props.richText for geo shapes (tldraw schema change) + // tldraw no longer accepts props.text on geo shapes - must use richText + // Also preserve in meta.text for backward compatibility (used by search and runLLMprompt) + if ('text' in shape.props && typeof shape.props.text === 'string') { + const textContent = shape.props.text + + // Convert text string to richText format for tldraw + shape.props.richText = { + type: 'doc', + content: textContent ? [{ + type: 'paragraph', + content: [{ + type: 'text', + text: textContent + }] + }] : [] + } + + // CRITICAL: Preserve original text in meta.text for backward compatibility + // This is used by search (src/utils/searchUtils.ts) and other legacy code + if (!shape.meta) shape.meta = {} + shape.meta.text = textContent + + // Remove invalid props.text + delete shape.props.text + } } // CRITICAL: Remove invalid 'text' property from text shapes (TLDraw schema doesn't allow props.text) @@ -516,9 +593,22 @@ export function CustomMainMenu() { } console.log('About to call putContentOntoCurrentPage with:', contentToImport) - + + // DEBUG: Log first 5 shapes' coordinates before import + console.log('🔍 Coordinates before putContentOntoCurrentPage:') + contentToImport.shapes.slice(0, 5).forEach((shape: any) => { + console.log(` Shape ${shape.id} (${shape.type}): x=${shape.x}, y=${shape.y}`) + }) + try { editor.putContentOntoCurrentPage(contentToImport, { select: true }) + + // DEBUG: Log first 5 shapes' coordinates after import + console.log('🔍 Coordinates after putContentOntoCurrentPage:') + const importedShapes = editor.getCurrentPageShapes() + importedShapes.slice(0, 5).forEach((shape: any) => { + console.log(` Shape ${shape.id} (${shape.type}): x=${shape.x}, y=${shape.y}`) + }) } catch (putContentError) { console.error('putContentOntoCurrentPage failed, trying alternative approach:', putContentError) @@ -582,17 +672,44 @@ export function CustomMainMenu() { const wValue = 'w' in shape ? shape.w : undefined const hValue = 'h' in shape ? shape.h : undefined const geoValue = 'geo' in shape ? shape.geo : undefined - + // Remove from top level delete shape.w delete shape.h delete shape.geo - + // Ensure props exists and move values there if (!shape.props) shape.props = {} if (wValue !== undefined && !shape.props.w) shape.props.w = wValue if (hValue !== undefined && !shape.props.h) shape.props.h = hValue if (geoValue !== undefined && !shape.props.geo) shape.props.geo = geoValue + + // CRITICAL: Convert props.text to props.richText for geo shapes (tldraw schema change) + // tldraw no longer accepts props.text on geo shapes - must use richText + // Also preserve in meta.text for backward compatibility (used by search and runLLMprompt) + if ('text' in shape.props && typeof shape.props.text === 'string') { + const textContent = shape.props.text + + // Convert text string to richText format for tldraw + shape.props.richText = { + type: 'doc', + content: textContent ? [{ + type: 'paragraph', + content: [{ + type: 'text', + text: textContent + }] + }] : [] + } + + // CRITICAL: Preserve original text in meta.text for backward compatibility + // This is used by search (src/utils/searchUtils.ts) and other legacy code + if (!shape.meta) shape.meta = {} + shape.meta.text = textContent + + // Remove invalid props.text + delete shape.props.text + } } // CRITICAL: Remove invalid 'text' property from text shapes (TLDraw schema doesn't allow props.text)