fix: preserve coordinates and convert geo shape text during JSON import

- 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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-11-19 16:14:23 -07:00
parent ed5628029d
commit e69fcad457
4 changed files with 144 additions and 14 deletions

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
20

View File

@ -80,6 +80,6 @@
"wrangler": "^4.33.2" "wrangler": "^4.33.2"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=20.0.0"
} }
} }

View File

@ -210,8 +210,15 @@ export function useAutomergeStoreV2({
// The Automerge Repo doesn't auto-broadcast because our WebSocket setup doesn't use peer discovery // The Automerge Repo doesn't auto-broadcast because our WebSocket setup doesn't use peer discovery
const triggerSync = () => { const triggerSync = () => {
try { try {
console.log('🔄 triggerSync() called')
const repo = (handle as any).repo const repo = (handle as any).repo
console.log('🔍 repo:', !!repo, 'handle:', !!handle, 'documentId:', handle?.documentId)
if (repo) { 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 // Try multiple approaches to trigger sync
// Approach 1: Use networkSubsystem.syncDoc if available // Approach 1: Use networkSubsystem.syncDoc if available
@ -223,10 +230,13 @@ export function useAutomergeStoreV2({
else if (repo.networkSubsystem && repo.networkSubsystem.adapters) { else if (repo.networkSubsystem && repo.networkSubsystem.adapters) {
console.log('🔄 Broadcasting sync to all network adapters') console.log('🔄 Broadcasting sync to all network adapters')
const adapters = Array.from(repo.networkSubsystem.adapters.values()) const adapters = Array.from(repo.networkSubsystem.adapters.values())
console.log('🔍 Found adapters:', adapters.length)
adapters.forEach((adapter: any) => { adapters.forEach((adapter: any) => {
console.log('🔍 Adapter has send:', typeof adapter?.send)
if (adapter && typeof adapter.send === 'function') { if (adapter && typeof adapter.send === 'function') {
// Send a sync message via the adapter // Send a sync message via the adapter
// The adapter should handle converting this to the right format // The adapter should handle converting this to the right format
console.log('📤 Sending sync via adapter')
adapter.send({ adapter.send({
type: 'sync', type: 'sync',
documentId: handle.documentId, documentId: handle.documentId,
@ -243,6 +253,8 @@ export function useAutomergeStoreV2({
else { else {
console.warn('⚠️ No known method to trigger sync broadcast found') console.warn('⚠️ No known method to trigger sync broadcast found')
} }
} else {
console.warn('⚠️ No repo found on handle')
} }
} catch (error) { } catch (error) {
console.error('❌ Error triggering manual sync:', error) console.error('❌ Error triggering manual sync:', error)

View File

@ -64,9 +64,23 @@ export function CustomMainMenu() {
const validateShapeGeometry = (shape: any): boolean => { const validateShapeGeometry = (shape: any): boolean => {
if (!shape || !shape.id) return false if (!shape || !shape.id) return false
// Validate basic numeric properties // CRITICAL: Only validate that x/y are valid numbers if they exist
shape.x = validateNumericValue(shape.x, 0, 'x') // DO NOT set default values here - let fixIncompleteShape handle that
shape.y = validateNumericValue(shape.y, 0, 'y') // 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.rotation = validateNumericValue(shape.rotation, 0, 'rotation')
shape.opacity = validateNumericValue(shape.opacity, 1, 'opacity') shape.opacity = validateNumericValue(shape.opacity, 1, 'opacity')
@ -165,12 +179,21 @@ export function CustomMainMenu() {
const fixIncompleteShape = (shape: any, pageId: string): any => { const fixIncompleteShape = (shape: any, pageId: string): any => {
const fixedShape = { ...shape } const fixedShape = { ...shape }
// DEBUG: Log coordinates before validation
const originalX = fixedShape.x
const originalY = fixedShape.y
// CRITICAL: Validate geometry first (fixes NaN/Infinity values) // CRITICAL: Validate geometry first (fixes NaN/Infinity values)
if (!validateShapeGeometry(fixedShape)) { if (!validateShapeGeometry(fixedShape)) {
console.warn(`⚠️ Shape failed geometry validation, skipping:`, fixedShape.id) console.warn(`⚠️ Shape failed geometry validation, skipping:`, fixedShape.id)
return null // Return null to indicate shape should be skipped 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 // CRITICAL: Validate and normalize shape type
const normalizedType = validateAndNormalizeShapeType(fixedShape) const normalizedType = validateAndNormalizeShapeType(fixedShape)
if (normalizedType !== fixedShape.type) { if (normalizedType !== fixedShape.type) {
@ -257,6 +280,33 @@ export function CustomMainMenu() {
if (!fixedShape.props.dash) fixedShape.props.dash = 'draw' if (!fixedShape.props.dash) fixedShape.props.dash = 'draw'
if (!fixedShape.props.size) fixedShape.props.size = 'm' if (!fixedShape.props.size) fixedShape.props.size = 'm'
if (!fixedShape.props.font) fixedShape.props.font = 'draw' 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') { } else if (fixedShape.type === 'VideoChat') {
// VideoChat shapes also need w/h in props, not top level // VideoChat shapes also need w/h in props, not top level
const wValue = fixedShape.w !== undefined ? fixedShape.w : 200 const wValue = fixedShape.w !== undefined ? fixedShape.w : 200
@ -503,6 +553,33 @@ export function CustomMainMenu() {
if (wValue !== undefined && !shape.props.w) shape.props.w = wValue if (wValue !== undefined && !shape.props.w) shape.props.w = wValue
if (hValue !== undefined && !shape.props.h) shape.props.h = hValue if (hValue !== undefined && !shape.props.h) shape.props.h = hValue
if (geoValue !== undefined && !shape.props.geo) shape.props.geo = geoValue 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) // CRITICAL: Remove invalid 'text' property from text shapes (TLDraw schema doesn't allow props.text)
@ -517,8 +594,21 @@ export function CustomMainMenu() {
console.log('About to call putContentOntoCurrentPage with:', contentToImport) 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 { try {
editor.putContentOntoCurrentPage(contentToImport, { select: true }) 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) { } catch (putContentError) {
console.error('putContentOntoCurrentPage failed, trying alternative approach:', putContentError) console.error('putContentOntoCurrentPage failed, trying alternative approach:', putContentError)
@ -593,6 +683,33 @@ export function CustomMainMenu() {
if (wValue !== undefined && !shape.props.w) shape.props.w = wValue if (wValue !== undefined && !shape.props.w) shape.props.w = wValue
if (hValue !== undefined && !shape.props.h) shape.props.h = hValue if (hValue !== undefined && !shape.props.h) shape.props.h = hValue
if (geoValue !== undefined && !shape.props.geo) shape.props.geo = geoValue 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) // CRITICAL: Remove invalid 'text' property from text shapes (TLDraw schema doesn't allow props.text)