test(canvas): tighten pattern visual oracle
This commit is contained in:
parent
e02782d1b5
commit
16e523380b
|
|
@ -31,7 +31,8 @@ const { values: opts } = parseArgs({
|
|||
options: {
|
||||
scale: { type: 'string', default: '2' },
|
||||
output: { type: 'string', short: 'o', default: '/tmp/visual-compare' },
|
||||
node: { type: 'string', short: 'n' }
|
||||
node: { type: 'string', short: 'n' },
|
||||
resize: { type: 'boolean', default: false }
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -39,6 +40,7 @@ const scale = Number(opts.scale)
|
|||
const outputDir = opts.output ?? '/tmp/visual-compare'
|
||||
const figmaPath = `${outputDir}/figma.png`
|
||||
const oursPath = `${outputDir}/ours.png`
|
||||
const normalizedOursPath = `${outputDir}/ours-normalized.png`
|
||||
const diffPath = `${outputDir}/diff.png`
|
||||
|
||||
if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true })
|
||||
|
|
@ -177,16 +179,26 @@ async function diff() {
|
|||
const figmaSize = (await $`identify -format '%wx%h' ${figmaPath}`.quiet()).text().trim()
|
||||
const oursSize = (await $`identify -format '%wx%h' ${oursPath}`.quiet()).text().trim()
|
||||
|
||||
const compareOursPath = figmaSize === oursSize ? oursPath : normalizedOursPath
|
||||
if (figmaSize !== oursSize) {
|
||||
console.log(` ⚠ Size mismatch: Figma ${figmaSize}, Ours ${oursSize} → resizing`)
|
||||
await $`magick ${oursPath} -resize ${figmaSize}! ${oursPath}`.quiet()
|
||||
const mode = opts.resize ? 'resizing' : 'padding/cropping without scaling'
|
||||
console.log(` ⚠ Size mismatch: Figma ${figmaSize}, Ours ${oursSize} → ${mode}`)
|
||||
if (opts.resize) {
|
||||
await $`magick ${oursPath} -resize ${figmaSize}! ${normalizedOursPath}`.quiet()
|
||||
} else {
|
||||
await $`magick ${oursPath} -background none -gravity northwest -extent ${figmaSize} ${normalizedOursPath}`.quiet()
|
||||
}
|
||||
}
|
||||
|
||||
const result =
|
||||
await $`magick compare -metric AE -highlight-color red -lowlight-color '#FFFFFF33' -compose src ${figmaPath} ${oursPath} ${diffPath}`
|
||||
await $`magick compare -metric AE -highlight-color red -lowlight-color '#FFFFFF33' -compose src ${figmaPath} ${compareOursPath} ${diffPath}`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
|
||||
const rmseResult = await $`magick compare -metric RMSE ${figmaPath} ${compareOursPath} null:`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
const rmse = rmseResult.stderr.toString().trim()
|
||||
const diffPixels = Number.parseInt(result.stderr.toString().trim(), 10) || 0
|
||||
const [w, h] = figmaSize.split('x').map(Number)
|
||||
const total = w * h
|
||||
|
|
@ -196,6 +208,7 @@ async function diff() {
|
|||
console.log(
|
||||
` ${diffPixels.toLocaleString()} different pixels (${pct}% of ${total.toLocaleString()})`
|
||||
)
|
||||
console.log(` RMSE ${rmse}`)
|
||||
console.log(`\n✅ Done! Images in ${outputDir}/`)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ interface PatternOracleFill {
|
|||
|
||||
interface PaintOracle {
|
||||
pattern: {
|
||||
source: { id: string }
|
||||
frame: { id: string }
|
||||
source: { id: string; visible: boolean }
|
||||
target: { fills: PatternOracleFill[] }
|
||||
}
|
||||
pluginRuntimeCreation: Record<string, { ok: boolean; message: string }>
|
||||
|
|
@ -33,6 +34,7 @@ describe('Figma pattern/noise/custom paint oracle availability', () => {
|
|||
test('records the live Figma pattern paint payload', () => {
|
||||
const patternFill = readOracle().pattern.target.fills[0]
|
||||
|
||||
expect(readOracle().pattern.source.visible).toBe(true)
|
||||
expect(patternFill?.type).toBe('PATTERN')
|
||||
expect(patternFill?.sourceNodeId).toBe(readOracle().pattern.source.id)
|
||||
expect(patternFill?.tileType).toBe('RECTANGULAR')
|
||||
|
|
@ -47,7 +49,7 @@ describe('Figma pattern/noise/custom paint oracle availability', () => {
|
|||
expect(oracle.pluginRuntimeCreation.PATTERN_DIRECT_FILLS_ASSIGNMENT?.ok).toBe(false)
|
||||
|
||||
for (const type of ['NOISE', 'CUSTOM']) {
|
||||
expect(oracle.pluginRuntimeCreation[type]?.ok).toBe(false)
|
||||
expect(oracle.pluginRuntimeCreation[`${type}_ASYNC_FILLS`]?.ok).toBe(false)
|
||||
expect(oracle.currentFileFillTypes[type]).toBeUndefined()
|
||||
for (const counts of Object.values(oracle.localFigFixtureFillTypes)) {
|
||||
expect(counts[type]).toBeUndefined()
|
||||
|
|
|
|||
|
|
@ -5,21 +5,26 @@
|
|||
"captured": "2026-05-22"
|
||||
},
|
||||
"pattern": {
|
||||
"page": "OpenPencil pattern oracle 2026-05-22",
|
||||
"page": "OpenPencil pattern visible source oracle 2026-05-22",
|
||||
"frame": {
|
||||
"id": "296784:2258",
|
||||
"name": "Pattern visible source frame"
|
||||
},
|
||||
"source": {
|
||||
"id": "296519:2244",
|
||||
"name": "Pattern source dot"
|
||||
"id": "296784:2259",
|
||||
"name": "Pattern visible source dot",
|
||||
"visible": true
|
||||
},
|
||||
"target": {
|
||||
"id": "296519:2245",
|
||||
"name": "Pattern fill target",
|
||||
"id": "296784:2260",
|
||||
"name": "Pattern fill target with source",
|
||||
"fills": [
|
||||
{
|
||||
"type": "PATTERN",
|
||||
"visible": true,
|
||||
"opacity": 1,
|
||||
"blendMode": "NORMAL",
|
||||
"sourceNodeId": "296519:2244",
|
||||
"sourceNodeId": "296784:2259",
|
||||
"tileType": "RECTANGULAR",
|
||||
"scalingFactor": 1,
|
||||
"spacing": { "x": 0.25, "y": 0.4000000059604645 },
|
||||
|
|
@ -28,20 +33,20 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"note": "Captured with setFillsAsync; direct node.fills assignment rejects PATTERN."
|
||||
"note": "Captured with setFillsAsync; the frame includes a visible source node so clipboard import has the referenced pattern source. Direct node.fills assignment rejects PATTERN."
|
||||
},
|
||||
"pluginRuntimeCreation": {
|
||||
"PATTERN_DIRECT_FILLS_ASSIGNMENT": {
|
||||
"ok": false,
|
||||
"message": "Directly assigning PATTERN to node.fills fails validation; Figma requires setFillsAsync after loading/referencing the pattern source node."
|
||||
},
|
||||
"NOISE": {
|
||||
"NOISE_ASYNC_FILLS": {
|
||||
"ok": false,
|
||||
"message": "in set_fills: Property \"fills\" failed validation: Invalid discriminator value. Expected 'SOLID' | 'GRADIENT_LINEAR' | 'GRADIENT_RADIAL' | 'GRADIENT_ANGULAR' | 'GRADIENT_DIAMOND' | 'IMAGE' | 'VIDEO' at [0].type"
|
||||
"message": "setFillsAsync rejects NOISE; supported async non-standard paint discriminator is PATTERN with required tileType and sourceNodeId."
|
||||
},
|
||||
"CUSTOM": {
|
||||
"CUSTOM_ASYNC_FILLS": {
|
||||
"ok": false,
|
||||
"message": "in set_fills: Property \"fills\" failed validation: Invalid discriminator value. Expected 'SOLID' | 'GRADIENT_LINEAR' | 'GRADIENT_RADIAL' | 'GRADIENT_ANGULAR' | 'GRADIENT_DIAMOND' | 'IMAGE' | 'VIDEO' at [0].type"
|
||||
"message": "setFillsAsync rejects CUSTOM; supported async non-standard paint discriminator is PATTERN with required tileType and sourceNodeId."
|
||||
}
|
||||
},
|
||||
"currentFileFillTypes": {
|
||||
|
|
@ -69,5 +74,5 @@
|
|||
"GRADIENT_LINEAR": 9
|
||||
}
|
||||
},
|
||||
"status": "A real PATTERN payload is now captured through Figma's async paint API. Real NOISE and CUSTOM paint payloads are still unavailable in the connected Figma file and committed .fig fixtures, so first-class rendering for those remains blocked on Figma-authored samples."
|
||||
"status": "A real PATTERN payload is captured through Figma's async paint API with source and target in the same copied subtree. Real NOISE and CUSTOM paint payloads are still unavailable in the connected Figma file and committed .fig fixtures, so first-class rendering for those remains blocked on Figma-authored samples."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,41 +6,45 @@
|
|||
},
|
||||
"comparisons": [
|
||||
{
|
||||
"name": "rich-text-decorations",
|
||||
"nodeId": "296488:2238",
|
||||
"output": "/tmp/open-pencil-oracles/rich-text/decorations-scale1",
|
||||
"figmaSize": "493x41",
|
||||
"openPencilSizeBeforeResize": "493x23",
|
||||
"differentPixels": 10611,
|
||||
"differentPercent": 52.5,
|
||||
"finding": "OpenPencil imports style-run decoration metadata, but exact Figma line metrics/leading trim still differ substantially."
|
||||
"name": "pattern-visible-source-frame",
|
||||
"nodeId": "296784:2258",
|
||||
"output": "/tmp/open-pencil-oracles/pattern-visible-source-norm",
|
||||
"figmaSize": "190x110",
|
||||
"openPencilSize": "190x110",
|
||||
"differentPixels": 13026,
|
||||
"differentPercent": 62.33,
|
||||
"rmseNormalized": 0.361297,
|
||||
"finding": "This is the reliable pattern oracle: source and target are copied together and Figma renders the pattern. OpenPencil now resolves the source, but tile origin/spacing still differs from Figma."
|
||||
},
|
||||
{
|
||||
"name": "rich-text-offset-skip-ink",
|
||||
"nodeId": "296619:2252",
|
||||
"output": "/tmp/open-pencil-oracles/rich-text/offset-skipink-scale1",
|
||||
"figmaSize": "212x32",
|
||||
"openPencilSizeBeforeResize": "212x39",
|
||||
"differentPixels": 4154,
|
||||
"differentPercent": 61.23,
|
||||
"finding": "Underline offset and skip-ink now round-trip, but CanvasKit rendering does not expose matching underline offset/skip-ink controls."
|
||||
"name": "rich-text-decorations",
|
||||
"nodeId": "296488:2238",
|
||||
"output": "/tmp/open-pencil-oracles/rich-text/decorations-norm",
|
||||
"figmaSize": "493x41",
|
||||
"openPencilSize": "493x23",
|
||||
"normalization": "Padded/cropped without scaling before diff",
|
||||
"differentPixels": 6881,
|
||||
"differentPercent": 34.04,
|
||||
"rmseNormalized": 0.243701,
|
||||
"finding": "OpenPencil imports style-run decoration metadata, but exact Figma text height/line metrics still differ substantially."
|
||||
},
|
||||
{
|
||||
"name": "nested-luminance-mask",
|
||||
"nodeId": "296616:2249",
|
||||
"output": "/tmp/open-pencil-oracles/masks/nested-scale1",
|
||||
"output": "/tmp/open-pencil-oracles/masks/nested-norm",
|
||||
"figmaSize": "240x120",
|
||||
"openPencilSizeBeforeResize": "240x120",
|
||||
"openPencilSize": "240x120",
|
||||
"differentPixels": 12944,
|
||||
"differentPercent": 44.94,
|
||||
"finding": "Visual output is structurally close, but colors/antialiasing differ enough for a high raw AE score."
|
||||
"rmseNormalized": 0.00385526,
|
||||
"finding": "Raw AE is high because antialiasing touches many edge pixels, but RMSE is very low and the output is structurally close. Future mask comparison should use tolerant/perceptual metrics."
|
||||
},
|
||||
{
|
||||
"name": "pattern-target-without-source",
|
||||
"nodeId": "296519:2245",
|
||||
"output": "/tmp/open-pencil-oracles/pattern-scale1",
|
||||
"figmaSize": "160x100",
|
||||
"openPencilSizeBeforeResize": "160x100",
|
||||
"openPencilSize": "160x100",
|
||||
"differentPixels": 16000,
|
||||
"differentPercent": 100,
|
||||
"finding": "Copying/exporting only a pattern-filled target does not include its source node in clipboard data; OpenPencil must fall back without whole-document context."
|
||||
|
|
@ -50,14 +54,14 @@
|
|||
"nodeId": "296731:2254",
|
||||
"output": "/tmp/open-pencil-oracles/pattern-frame-scale1-after-sourceid",
|
||||
"figmaSize": "180x100",
|
||||
"openPencilSizeBeforeResize": "180x100",
|
||||
"openPencilSize": "180x100",
|
||||
"differentPixels": 9102,
|
||||
"differentPercent": 50.57,
|
||||
"finding": "Resolving pattern sources by imported Figma source id works in OpenPencil, but this hidden-source frame oracle renders blank in Figma and is not a parity baseline."
|
||||
}
|
||||
],
|
||||
"nextActions": [
|
||||
"Create a Figma pattern oracle where the source is included in the copied subtree and Figma still renders the pattern target.",
|
||||
"Tune pattern tile origin and spacing against pattern-visible-source-frame.",
|
||||
"Use perceptual/color-tolerant image diff for mask comparisons instead of raw absolute-error counts.",
|
||||
"Tune rich text line metrics before treating underline placement diffs as actionable."
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue