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(
|
export function applyAutomergePatchesToTLStore(
|
||||||
patches: Automerge.Patch[],
|
patches: Automerge.Patch[],
|
||||||
store: TLStore
|
store: TLStore,
|
||||||
|
automergeDoc?: any // Optional Automerge document to read full records from
|
||||||
) {
|
) {
|
||||||
const toRemove: TLRecord["id"][] = []
|
const toRemove: TLRecord["id"][] = []
|
||||||
const updatedObjects: { [id: string]: TLRecord } = {}
|
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
|
// Infer typeName from ID pattern if record doesn't exist
|
||||||
let defaultTypeName = 'shape'
|
let defaultTypeName = 'shape'
|
||||||
let defaultRecord: any = {
|
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)
|
// 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
|
// 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 originalX = storeCoordinates.x !== undefined ? storeCoordinates.x : recordX
|
||||||
const originalY = storeCoordinates.y !== undefined ? storeCoordinates.y : recordY
|
const originalY = storeCoordinates.y !== undefined ? storeCoordinates.y : recordY
|
||||||
const hadOriginalCoordinates = originalX !== undefined && originalY !== undefined
|
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) {
|
switch (patch.action) {
|
||||||
case "insert": {
|
case "insert": {
|
||||||
|
|
@ -229,6 +288,42 @@ export function applyAutomergePatchesToTLStore(
|
||||||
updatedObjects[id] = { ...updatedObjects[id], y: defaultRecord.y || 0 } as TLRecord
|
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
|
// 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)
|
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
|
// CRITICAL: Fix richText structure for text shapes - REQUIRED field
|
||||||
if (sanitized.type === 'text') {
|
if (sanitized.type === 'text') {
|
||||||
// Text shapes MUST have props.richText as an object - initialize if missing
|
// 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)
|
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
|
// 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
|
// 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)
|
// 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 {
|
try {
|
||||||
const propsWithoutRichText: any = {}
|
const propsWithoutSpecial: any = {}
|
||||||
// Copy all props except richText
|
// Copy all props except richText and arrow text (if extracted)
|
||||||
for (const key in sanitized.props) {
|
for (const key in sanitized.props) {
|
||||||
if (key !== 'richText') {
|
if (key !== 'richText' && !(sanitized.type === 'arrow' && key === 'text' && arrowTextValue !== undefined)) {
|
||||||
propsWithoutRichText[key] = (sanitized.props as any)[key]
|
propsWithoutSpecial[key] = (sanitized.props as any)[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sanitized.props = JSON.parse(JSON.stringify(propsWithoutRichText))
|
sanitized.props = JSON.parse(JSON.stringify(propsWithoutSpecial))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`🔧 TLStoreToAutomerge: Error deep copying props for shape ${sanitized.id}:`, e)
|
console.warn(`🔧 TLStoreToAutomerge: Error deep copying props for shape ${sanitized.id}:`, e)
|
||||||
// Fallback: just copy props without deep copy
|
// Fallback: just copy props without deep copy
|
||||||
|
|
@ -164,6 +242,9 @@ function sanitizeRecord(record: TLRecord): TLRecord {
|
||||||
if (richTextValue !== undefined) {
|
if (richTextValue !== undefined) {
|
||||||
delete (sanitized.props as any).richText
|
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)
|
// 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
|
// CRITICAL: For arrow shapes, preserve text property
|
||||||
if (sanitized.type === 'arrow') {
|
if (sanitized.type === 'arrow') {
|
||||||
// CRITICAL: Preserve text property - only set default if truly missing (preserve empty strings and all other values)
|
// CRITICAL: Restore extracted text value if available, otherwise preserve existing text
|
||||||
if ((sanitized.props as any).text === undefined || (sanitized.props as any).text === null) {
|
if (arrowTextValue !== undefined) {
|
||||||
(sanitized.props as any).text = ''
|
// 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)
|
// CRITICAL: For note shapes, preserve richText property (required for note shapes)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue