fix(kiwi): resolve variable radius overrides

- Resolve numeric variable aliases in symbol override radius fields
- Preserve imported 999 corner sentinels through G0 to G1 roundtrips
- Remove the resolved corner-radius roundtrip TODO
This commit is contained in:
Danila Poyarkov 2026-05-24 23:46:29 +03:00
parent ae2ba1455b
commit b4dfd90da0
4 changed files with 91 additions and 12 deletions

View file

@ -1,10 +1,92 @@
import type { GUID } from '#core/kiwi/fig/codec'
import type { OverridePatch } from '#core/kiwi/fig/instance-overrides/patches'
import type { OverrideContext, SymbolOverride } from '#core/kiwi/fig/instance-overrides/types'
import { guidToString } from '#core/kiwi/fig/node-change/convert'
import { guidToString, VARIABLE_BINDING_FIELDS_INVERSE } from '#core/kiwi/fig/node-change/convert'
import { applyStyleRefsToFields } from '#core/kiwi/fig/node-change/style-refs'
import { convertOverrideToProps } from './props'
interface AliasRef {
guid?: GUID
assetRef?: { key: string; version?: string }
}
const VARIABLE_RADIUS_FIELDS = new Set([
'RECTANGLE_TOP_LEFT_CORNER_RADIUS',
'RECTANGLE_TOP_RIGHT_CORNER_RADIUS',
'RECTANGLE_BOTTOM_LEFT_CORNER_RADIUS',
'RECTANGLE_BOTTOM_RIGHT_CORNER_RADIUS'
])
function assetRefKey(assetRef: { key: string; version?: string }): string {
return assetRef.version ? `${assetRef.key}@${assetRef.version}` : assetRef.key
}
function buildAssetRefMap(ctx: OverrideContext): Map<string, string> {
const refs = new Map<string, string>()
for (const [id, nc] of ctx.changeMap) {
const key = typeof nc.key === 'string' ? nc.key : undefined
if (!key) continue
refs.set(key, id)
const version = typeof nc.version === 'string' ? nc.version : undefined
if (version) refs.set(assetRefKey({ key, version }), id)
}
return refs
}
function resolveAliasId(alias: AliasRef, assetRefs: Map<string, string>): string | undefined {
if (alias.guid) return guidToString(alias.guid)
const assetRef = alias.assetRef
if (!assetRef?.key) return undefined
return assetRefs.get(assetRefKey(assetRef)) ?? assetRefs.get(assetRef.key)
}
function resolveFloatVariable(
ctx: OverrideContext,
id: string,
assetRefs: Map<string, string>,
depth = 0
): number | undefined {
if (depth > 10) return undefined
const nc = ctx.changeMap.get(id)
const entry = nc?.variableDataValues?.entries?.[0]
if (!entry) return undefined
const value = entry.variableData.value
if (!value) return undefined
if (typeof value.floatValue === 'number') return value.floatValue
const alias = value.alias as AliasRef | undefined
const aliasId = alias ? resolveAliasId(alias, assetRefs) : undefined
return aliasId ? resolveFloatVariable(ctx, aliasId, assetRefs, depth + 1) : undefined
}
function applyVariableRadiusOverrides(
ctx: OverrideContext,
fields: Record<string, unknown>,
props: ReturnType<typeof convertOverrideToProps>
): void {
const entries = (fields.variableConsumptionMap as { entries?: unknown[] } | undefined)?.entries
if (!entries?.length) return
const assetRefs = buildAssetRefMap(ctx)
for (const entry of entries) {
if (!entry || typeof entry !== 'object') continue
const variableEntry = entry as {
variableField?: string
variableData?: { value?: { alias?: AliasRef } }
}
const variableField = variableEntry.variableField
if (!variableField || !VARIABLE_RADIUS_FIELDS.has(variableField)) continue
const alias = variableEntry.variableData?.value?.alias
const id = alias ? resolveAliasId(alias, assetRefs) : undefined
const value = id ? resolveFloatVariable(ctx, id, assetRefs) : undefined
if (typeof value !== 'number') continue
const field = VARIABLE_BINDING_FIELDS_INVERSE[variableField]
if (field === 'topLeftRadius') props.topLeftRadius = value
else if (field === 'topRightRadius') props.topRightRadius = value
else if (field === 'bottomRightRadius') props.bottomRightRadius = value
else if (field === 'bottomLeftRadius') props.bottomLeftRadius = value
}
}
export function patchFromSymbolOverride(
ctx: OverrideContext,
targetId: string,
@ -20,6 +102,7 @@ export function patchFromSymbolOverride(
if (Object.keys(fields).length > 0) {
applyStyleRefsToFields(ctx.changeMap, fields)
const props = convertOverrideToProps(fields as Record<string, unknown>)
applyVariableRadiusOverrides(ctx, fields as Record<string, unknown>, props)
if (Object.keys(props).length > 0) patch.props = props
}

View file

@ -80,6 +80,10 @@ export interface InstanceNodeChange {
strokeGeometry?: Array<{ windingRule?: string; commandsBlob?: number }>
strokeWeight?: number
derivedSymbolData?: DerivedSymbolOverride[]
key?: string
version?: string
userFacingVersion?: string
variableDataValues?: NodeChange['variableDataValues']
}
/**

View file

@ -61,8 +61,8 @@ const SPECS: FixtureSpec[] = [
thumbnailHeight: 239,
imageCount: 3,
figKiwiVersion: 101,
g1ExportSize: 594522,
g2ExportSize: 594522
g1ExportSize: 594517,
g2ExportSize: 594517
}
]
@ -584,10 +584,6 @@ function verifyFixture(spec: FixtureSpec): void {
await ensureG2()
compareRawNodeFields(spec, g1Graph, g2Graph, g1, g2, 'G1->G2')
})
test.todo(
'BUG: corner radius 999 sentinel lost for non-pill nodes on scene import (40 nodes, raw data preserved)'
)
})
}

View file

@ -92,11 +92,7 @@ export const SCENE_VERIFIERS = new Map<string, Verifier>([
if (!ctx.key.includes('componentPropertyDefinitions')) return false
return ctx.a === 'VARIANT' && ctx.b === 'TEXT'
}
],
['topLeftRadius', (ctx) => !isIdempotent(ctx) && ctx.a === 999 && ctx.b === 0],
['topRightRadius', (ctx) => !isIdempotent(ctx) && ctx.a === 999 && ctx.b === 0],
['bottomRightRadius', (ctx) => !isIdempotent(ctx) && ctx.a === 999 && ctx.b === 0],
['bottomLeftRadius', (ctx) => !isIdempotent(ctx) && ctx.a === 999 && ctx.b === 0]
]
])
function verifyAEntries(