Merge pull request #13 from Jeff-Emmett/add-runpod-AI-API
prevent coordinate collapse on reload
This commit is contained in:
commit
cb5045984b
|
|
@ -3,7 +3,8 @@ import * as Automerge from "@automerge/automerge"
|
|||
|
||||
export function applyAutomergePatchesToTLStore(
|
||||
patches: Automerge.Patch[],
|
||||
store: TLStore
|
||||
store: TLStore,
|
||||
automergeDoc?: any // Optional Automerge document to read full records from
|
||||
) {
|
||||
const toRemove: TLRecord["id"][] = []
|
||||
const updatedObjects: { [id: string]: TLRecord } = {}
|
||||
|
|
@ -42,6 +43,29 @@ export function applyAutomergePatchesToTLStore(
|
|||
}
|
||||
}
|
||||
|
||||
// CRITICAL: If record doesn't exist in store yet, try to get it from Automerge document
|
||||
// This prevents coordinates from defaulting to 0,0 when patches create new records
|
||||
let automergeRecord: any = null
|
||||
if (!existingRecord && automergeDoc && automergeDoc.store && automergeDoc.store[id]) {
|
||||
try {
|
||||
automergeRecord = automergeDoc.store[id]
|
||||
// Extract coordinates from Automerge record if it's a shape
|
||||
if (automergeRecord && automergeRecord.typeName === 'shape') {
|
||||
const docX = automergeRecord.x
|
||||
const docY = automergeRecord.y
|
||||
if (typeof docX === 'number' && !isNaN(docX) && docX !== null && docX !== undefined) {
|
||||
storeCoordinates.x = docX
|
||||
}
|
||||
if (typeof docY === 'number' && !isNaN(docY) && docY !== null && docY !== undefined) {
|
||||
storeCoordinates.y = docY
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// If we can't read from Automerge doc, continue without it
|
||||
console.warn(`Could not read record ${id} from Automerge document:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
// Infer typeName from ID pattern if record doesn't exist
|
||||
let defaultTypeName = 'shape'
|
||||
let defaultRecord: any = {
|
||||
|
|
@ -112,7 +136,20 @@ export function applyAutomergePatchesToTLStore(
|
|||
}
|
||||
}
|
||||
|
||||
let record = updatedObjects[id] || (existingRecord ? JSON.parse(JSON.stringify(existingRecord)) : defaultRecord)
|
||||
// CRITICAL: When creating a new record, prefer using the full record from Automerge document
|
||||
// This ensures we get all properties including coordinates, not just defaults
|
||||
let record: any
|
||||
if (updatedObjects[id]) {
|
||||
record = updatedObjects[id]
|
||||
} else if (existingRecord) {
|
||||
record = JSON.parse(JSON.stringify(existingRecord))
|
||||
} else if (automergeRecord) {
|
||||
// Use the full record from Automerge document - this has all properties including coordinates
|
||||
record = JSON.parse(JSON.stringify(automergeRecord))
|
||||
} else {
|
||||
// Fallback to default record only if we can't get it from anywhere else
|
||||
record = defaultRecord
|
||||
}
|
||||
|
||||
// CRITICAL: For shapes, ensure x and y are always present (even if record came from updatedObjects)
|
||||
// This prevents coordinates from being lost when records are created from patches
|
||||
|
|
@ -157,6 +194,28 @@ export function applyAutomergePatchesToTLStore(
|
|||
const originalX = storeCoordinates.x !== undefined ? storeCoordinates.x : recordX
|
||||
const originalY = storeCoordinates.y !== undefined ? storeCoordinates.y : recordY
|
||||
const hadOriginalCoordinates = originalX !== undefined && originalY !== undefined
|
||||
|
||||
// CRITICAL: Store original richText and arrow text before patch application to preserve them
|
||||
// This ensures richText and arrow text aren't lost when patches only update other properties
|
||||
let originalRichText: any = undefined
|
||||
let originalArrowText: any = undefined
|
||||
if (record.typeName === 'shape') {
|
||||
// Get richText from store's current state (most reliable)
|
||||
if (existingRecord && (existingRecord as any).props && (existingRecord as any).props.richText) {
|
||||
originalRichText = (existingRecord as any).props.richText
|
||||
} else if ((record as any).props && (record as any).props.richText) {
|
||||
originalRichText = (record as any).props.richText
|
||||
}
|
||||
|
||||
// Get arrow text from store's current state (most reliable)
|
||||
if ((record as any).type === 'arrow') {
|
||||
if (existingRecord && (existingRecord as any).props && (existingRecord as any).props.text !== undefined) {
|
||||
originalArrowText = (existingRecord as any).props.text
|
||||
} else if ((record as any).props && (record as any).props.text !== undefined) {
|
||||
originalArrowText = (record as any).props.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (patch.action) {
|
||||
case "insert": {
|
||||
|
|
@ -229,6 +288,42 @@ export function applyAutomergePatchesToTLStore(
|
|||
updatedObjects[id] = { ...updatedObjects[id], y: defaultRecord.y || 0 } as TLRecord
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: Preserve richText and arrow text after patch application
|
||||
// This prevents richText and arrow text from being lost when patches only update other properties
|
||||
const currentRecord = updatedObjects[id]
|
||||
|
||||
// Preserve richText for geo/note/text shapes
|
||||
if (originalRichText !== undefined && (currentRecord as any).type !== 'arrow') {
|
||||
const patchedProps = (currentRecord as any).props || {}
|
||||
const patchedRichText = patchedProps.richText
|
||||
// If patch didn't include richText, preserve the original
|
||||
if (patchedRichText === undefined || patchedRichText === null) {
|
||||
updatedObjects[id] = {
|
||||
...currentRecord,
|
||||
props: {
|
||||
...patchedProps,
|
||||
richText: originalRichText
|
||||
}
|
||||
} as TLRecord
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve arrow text for arrow shapes
|
||||
if (originalArrowText !== undefined && (currentRecord as any).type === 'arrow') {
|
||||
const patchedProps = (currentRecord as any).props || {}
|
||||
const patchedText = patchedProps.text
|
||||
// If patch didn't include text, preserve the original
|
||||
if (patchedText === undefined || patchedText === null) {
|
||||
updatedObjects[id] = {
|
||||
...currentRecord,
|
||||
props: {
|
||||
...patchedProps,
|
||||
text: originalArrowText
|
||||
}
|
||||
} as TLRecord
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: Re-check typeName after patch application to ensure it's still correct
|
||||
|
|
@ -660,6 +755,46 @@ export function sanitizeRecord(record: any): TLRecord {
|
|||
sanitized.props.richText = cleanRichTextNaN(sanitized.props.richText)
|
||||
}
|
||||
|
||||
// CRITICAL: Preserve arrow text property (ensure it's a string)
|
||||
if (sanitized.type === 'arrow') {
|
||||
// Ensure text property exists and is a string
|
||||
if (sanitized.props.text === undefined || sanitized.props.text === null) {
|
||||
sanitized.props.text = ''
|
||||
} else if (typeof sanitized.props.text !== 'string') {
|
||||
// If text is not a string (e.g., RichText object), convert it to string
|
||||
try {
|
||||
if (typeof sanitized.props.text === 'object' && sanitized.props.text !== null) {
|
||||
// Try to extract text from RichText object
|
||||
const textObj = sanitized.props.text as any
|
||||
if (Array.isArray(textObj.content)) {
|
||||
// Extract text from RichText content
|
||||
const extractText = (content: any[]): string => {
|
||||
return content.map((item: any) => {
|
||||
if (item.type === 'text' && item.text) {
|
||||
return item.text
|
||||
} else if (item.content && Array.isArray(item.content)) {
|
||||
return extractText(item.content)
|
||||
}
|
||||
return ''
|
||||
}).join('')
|
||||
}
|
||||
sanitized.props.text = extractText(textObj.content)
|
||||
} else if (textObj.text && typeof textObj.text === 'string') {
|
||||
sanitized.props.text = textObj.text
|
||||
} else {
|
||||
sanitized.props.text = String(sanitized.props.text)
|
||||
}
|
||||
} else {
|
||||
sanitized.props.text = String(sanitized.props.text)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`⚠️ AutomergeToTLStore: Error converting arrow text to string for ${sanitized.id}:`, e)
|
||||
sanitized.props.text = String(sanitized.props.text)
|
||||
}
|
||||
}
|
||||
// Note: We preserve text even if it's an empty string - that's a valid value
|
||||
}
|
||||
|
||||
// CRITICAL: Fix richText structure for text shapes - REQUIRED field
|
||||
if (sanitized.type === 'text') {
|
||||
// Text shapes MUST have props.richText as an object - initialize if missing
|
||||
|
|
|
|||
|
|
@ -144,19 +144,97 @@ function sanitizeRecord(record: TLRecord): TLRecord {
|
|||
console.warn(`🔧 TLStoreToAutomerge: Error checking richText for shape ${sanitized.id}:`, e)
|
||||
}
|
||||
|
||||
// CRITICAL: Extract arrow text BEFORE deep copy to handle RichText instances properly
|
||||
// Arrow text should be a string, but might be a RichText object in edge cases
|
||||
let arrowTextValue: any = undefined
|
||||
if (sanitized.type === 'arrow') {
|
||||
try {
|
||||
const props = sanitized.props || {}
|
||||
if ('text' in props) {
|
||||
try {
|
||||
// Use Object.getOwnPropertyDescriptor to safely check if it's a getter
|
||||
const descriptor = Object.getOwnPropertyDescriptor(props, 'text')
|
||||
let textValue: any = undefined
|
||||
|
||||
if (descriptor && descriptor.get) {
|
||||
// It's a getter - try to call it safely
|
||||
try {
|
||||
textValue = descriptor.get.call(props)
|
||||
} catch (getterError) {
|
||||
console.warn(`🔧 TLStoreToAutomerge: Error calling text getter for arrow ${sanitized.id}:`, getterError)
|
||||
textValue = undefined
|
||||
}
|
||||
} else {
|
||||
// It's a regular property - access it directly
|
||||
textValue = (props as any).text
|
||||
}
|
||||
|
||||
// Now process the value
|
||||
if (textValue !== undefined && textValue !== null) {
|
||||
// If it's a string, use it directly
|
||||
if (typeof textValue === 'string') {
|
||||
arrowTextValue = textValue
|
||||
}
|
||||
// If it's a RichText object, extract the text content
|
||||
else if (typeof textValue === 'object' && textValue !== null) {
|
||||
// Try to extract text from RichText object
|
||||
try {
|
||||
const serialized = JSON.parse(JSON.stringify(textValue))
|
||||
// If it has content array, extract text from it
|
||||
if (Array.isArray(serialized.content)) {
|
||||
// Extract text from RichText content
|
||||
const extractText = (content: any[]): string => {
|
||||
return content.map((item: any) => {
|
||||
if (item.type === 'text' && item.text) {
|
||||
return item.text
|
||||
} else if (item.content && Array.isArray(item.content)) {
|
||||
return extractText(item.content)
|
||||
}
|
||||
return ''
|
||||
}).join('')
|
||||
}
|
||||
arrowTextValue = extractText(serialized.content)
|
||||
} else {
|
||||
// Fallback: try to get text property
|
||||
arrowTextValue = serialized.text || ''
|
||||
}
|
||||
} catch (serializeError) {
|
||||
// If serialization fails, try to extract manually
|
||||
if ((textValue as any).text && typeof (textValue as any).text === 'string') {
|
||||
arrowTextValue = (textValue as any).text
|
||||
} else {
|
||||
arrowTextValue = String(textValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
// For other types, convert to string
|
||||
else {
|
||||
arrowTextValue = String(textValue)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`🔧 TLStoreToAutomerge: Error extracting text for arrow ${sanitized.id}:`, e)
|
||||
arrowTextValue = undefined
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`🔧 TLStoreToAutomerge: Error checking text for arrow ${sanitized.id}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: For all shapes, ensure props is a deep mutable copy to preserve all properties
|
||||
// This is essential for custom shapes like ObsNote and for preserving richText in geo shapes
|
||||
// Use JSON parse/stringify to create a deep copy of nested objects (like richText.content)
|
||||
// Remove richText temporarily to avoid serialization issues
|
||||
// Remove richText and arrow text temporarily to avoid serialization issues
|
||||
try {
|
||||
const propsWithoutRichText: any = {}
|
||||
// Copy all props except richText
|
||||
const propsWithoutSpecial: any = {}
|
||||
// Copy all props except richText and arrow text (if extracted)
|
||||
for (const key in sanitized.props) {
|
||||
if (key !== 'richText') {
|
||||
propsWithoutRichText[key] = (sanitized.props as any)[key]
|
||||
if (key !== 'richText' && !(sanitized.type === 'arrow' && key === 'text' && arrowTextValue !== undefined)) {
|
||||
propsWithoutSpecial[key] = (sanitized.props as any)[key]
|
||||
}
|
||||
}
|
||||
sanitized.props = JSON.parse(JSON.stringify(propsWithoutRichText))
|
||||
sanitized.props = JSON.parse(JSON.stringify(propsWithoutSpecial))
|
||||
} catch (e) {
|
||||
console.warn(`🔧 TLStoreToAutomerge: Error deep copying props for shape ${sanitized.id}:`, e)
|
||||
// Fallback: just copy props without deep copy
|
||||
|
|
@ -164,6 +242,9 @@ function sanitizeRecord(record: TLRecord): TLRecord {
|
|||
if (richTextValue !== undefined) {
|
||||
delete (sanitized.props as any).richText
|
||||
}
|
||||
if (arrowTextValue !== undefined) {
|
||||
delete (sanitized.props as any).text
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: For geo shapes, move w/h/geo from top-level to props (required by TLDraw schema)
|
||||
|
|
@ -210,11 +291,17 @@ function sanitizeRecord(record: TLRecord): TLRecord {
|
|||
|
||||
// CRITICAL: For arrow shapes, preserve text property
|
||||
if (sanitized.type === 'arrow') {
|
||||
// CRITICAL: Preserve text property - only set default if truly missing (preserve empty strings and all other values)
|
||||
if ((sanitized.props as any).text === undefined || (sanitized.props as any).text === null) {
|
||||
(sanitized.props as any).text = ''
|
||||
// CRITICAL: Restore extracted text value if available, otherwise preserve existing text
|
||||
if (arrowTextValue !== undefined) {
|
||||
// Use the extracted text value (handles RichText objects by extracting text content)
|
||||
(sanitized.props as any).text = arrowTextValue
|
||||
} else {
|
||||
// CRITICAL: Preserve text property - only set default if truly missing (preserve empty strings and all other values)
|
||||
if ((sanitized.props as any).text === undefined || (sanitized.props as any).text === null) {
|
||||
(sanitized.props as any).text = ''
|
||||
}
|
||||
// Note: We preserve text even if it's an empty string - that's a valid value
|
||||
}
|
||||
// Note: We preserve text even if it's an empty string - that's a valid value
|
||||
}
|
||||
|
||||
// CRITICAL: For note shapes, preserve richText property (required for note shapes)
|
||||
|
|
|
|||
Loading…
Reference in New Issue