fix(scene-graph): validate text invalidation patches

- Ignore undefined values before cache invalidation and preview mutation\n- Treat text direction as a glyph-shaping input\n- Cover the corrected invalidation contract
This commit is contained in:
Danila Poyarkov 2026-08-07 08:43:01 +03:00
parent f9701ef855
commit c446524ce2
5 changed files with 16 additions and 7 deletions

View file

@ -381,7 +381,15 @@ export class SceneGraph {
const node = this.nodes.get(id)
if (!node) return
let entries = Object.entries(changes) as Array<[string, unknown]>
changes = Object.fromEntries(
entries.filter(([, value]) => value !== undefined)
) as Partial<SceneNode>
changes = styleDetachmentChanges(node, changes)
entries = Object.entries(changes) as Array<[string, unknown]>
changes = Object.fromEntries(
entries.filter(([, value]) => value !== undefined)
) as Partial<SceneNode>
// Only clear absPosCache when layout-affecting properties change.
// Fills, strokes, effects, plugin data changes do NOT affect absolute position.
@ -408,10 +416,6 @@ export class SceneGraph {
const glyphChanged = Object.keys(changes).some((k) => GLYPH_AFFECTING_KEYS.has(k))
if (node.figmaDerivedTextGlyphs && glyphChanged) node.figmaDerivedTextGlyphs = null
}
const entries = Object.entries(changes) as Array<[string, unknown]>
changes = Object.fromEntries(
entries.filter(([, value]) => value !== undefined)
) as Partial<SceneNode>
if (this.sourceMetadataPreservationDepth === 0) {
markSourceFieldsEdited(node, Object.keys(changes))
}

View file

@ -50,6 +50,11 @@ export function updateNodePreview(
): Partial<SceneNode> | null {
const node = graph.nodes.get(id)
if (!node) return null
changes = Object.fromEntries(
(Object.entries(changes) as Array<[string, unknown]>).filter(
([, value]) => value !== undefined
)
) as Partial<SceneNode>
if ((Object.keys(changes) as (keyof SceneNode)[]).every((key) => node[key] === changes[key])) {
return null
}

View file

@ -24,6 +24,7 @@ export const GLYPH_AFFECTING_KEYS: ReadonlySet<string> = new Set([
'fontFamily',
'fontWeight',
'italic',
'textDirection',
'lineHeight',
'letterSpacing',
'textCase',

View file

@ -35,6 +35,7 @@ describe('text rendering invalidation keys', () => {
'fontFamily',
'fontWeight',
'italic',
'textDirection',
'lineHeight',
'letterSpacing',
'textCase',
@ -49,7 +50,6 @@ describe('text rendering invalidation keys', () => {
'height',
'fills',
'textAlignHorizontal',
'textDirection',
'textAlignVertical',
'textDecoration'
]) {

View file

@ -200,7 +200,7 @@ describe('updateNode', () => {
const textNode = expectDefined(graph.getNode(textId), 'text node')
textNode.figmaDerivedTextGlyphs = glyphs
graph.updateNode(textId, { fontFamily: 'Noto Sans SC' })
graph.updateNode(textId, { textDirection: 'RTL' })
expect(expectDefined(graph.getNode(textId), 'updated node').figmaDerivedTextGlyphs).toBeNull()
})
@ -211,7 +211,6 @@ describe('updateNode', () => {
{ height: 24 },
{ fills: [] },
{ textAlignHorizontal: 'CENTER' as const },
{ textDirection: 'RTL' as const },
{ textAlignVertical: 'CENTER' as const },
{ textDecoration: 'UNDERLINE' as const }
]