test(canvas): record Figma visual oracle diffs

This commit is contained in:
Danila Poyarkov 2026-05-26 20:25:00 +03:00
parent 3722924026
commit e02782d1b5
5 changed files with 82 additions and 8 deletions

View file

@ -162,6 +162,15 @@ function recordPatternSource(
return picture
}
function resolvePatternSource(graph: SceneGraph, sourceId: string): SceneNode | null {
const direct = graph.getNode(sourceId)
if (direct) return direct
for (const node of graph.getAllNodes()) {
if (node.source.id === sourceId) return node
}
return null
}
function applyPatternFill(
r: SkiaRenderer,
fill: Fill,
@ -169,8 +178,8 @@ function applyPatternFill(
graph: SceneGraph
): boolean {
const sourceId = fill.sourceNodeId
if (!sourceId || sourceId === node.id) return false
const source = graph.getNode(sourceId)
if (!sourceId || sourceId === node.id || sourceId === node.source.id) return false
const source = resolvePatternSource(graph, sourceId)
if (!source || source.width <= 0 || source.height <= 0) return false
const layout = patternTileLayout(source, fill)

View file

@ -19,7 +19,7 @@ OpenPencil is moving toward production-grade Figma compatibility while keeping d
### Figma fidelity
- Preserve and round-trip more Figma metadata safely.
- Add visual regression coverage for full multi-page `.fig` documents. `scripts/export-fixture-visuals.ts` exports current smoke fixture pages to `/tmp` for manual comparison without committing large images.
- Add visual regression coverage for full multi-page `.fig` documents. `scripts/export-fixture-visuals.ts` exports current smoke fixture pages to `/tmp` for manual comparison without committing large images; `tests/fixtures/figma-oracles/visual-comparison-report.json` records the current Figma-vs-OpenPencil oracle diff findings.
- Close high-impact renderer gaps: remaining mask edge cases, blend isolation, pattern fills, and broader variable-font fixtures.
- Improve boolean operation editing/export now that imported Figma `BOOLEAN_OPERATION` nodes remain boolean operations.

View file

@ -23,7 +23,7 @@ import { SkiaRenderer } from '@open-pencil/core/canvas'
import { renderNodesToImage, initCanvasKit } from '@open-pencil/core/io'
import { computeAllLayouts } from '@open-pencil/core/layout'
import { SceneGraph } from '@open-pencil/core/scene-graph'
import { loadFont } from '@open-pencil/core/text'
import { fontManager } from '@open-pencil/core/text'
import { parseFigmaClipboard, importClipboardNodes } from '#core/clipboard'
@ -81,7 +81,7 @@ async function runWithNodeId(nodeId: string) {
console.log('📋 Exporting clipboard data from Figma…')
// Select the node, copy, read clipboard, render with our engine
await $`figma-use eval ${`(() => { const n = figma.getNodeById('${nodeId}'); if (n) { figma.currentPage.selection = [n]; } })()`}`.quiet()
await $`figma-use eval ${`(() => { const n = figma.getNodeById('${nodeId}'); if (!n) return; let page = n.parent; while (page && page.type !== 'PAGE') page = page.parent; if (page) { figma.currentPage = page; page.selection = [n]; } })()`}`.quiet()
await Bun.sleep(200)
await $`osascript -e 'tell application "Figma" to activate'`.quiet()
await Bun.sleep(300)
@ -118,7 +118,7 @@ async function renderOurs(html: string) {
if (node.fontFamily) families.add(node.fontFamily)
}
for (const family of families) {
await loadFont(family)
await fontManager.loadFont(family)
}
const ck = await initCanvasKit()

View file

@ -40,7 +40,7 @@ function createRenderer() {
} as SkiaRenderer
}
const node = { id: '1:2', width: 100, height: 100 } as SceneNode
const node = { id: '1:2', source: { id: '' }, width: 100, height: 100 } as SceneNode
describe('schema fill fallback rendering', () => {
test('renders pattern fills from referenced source nodes', () => {
@ -58,10 +58,11 @@ describe('schema fill fallback rendering', () => {
}
]
})
source.source.id = '12:34'
const renderer = createRenderer()
const fill: Fill = {
type: 'PATTERN',
sourceNodeId: source.id,
sourceNodeId: '12:34',
patternSpacing: { x: 0.25, y: 0.4 },
color: { r: 0.2, g: 0.3, b: 0.4, a: 0.8 },
opacity: 1,

View file

@ -0,0 +1,64 @@
{
"source": {
"tool": "scripts/visual-compare.ts",
"captured": "2026-05-22",
"outputRoot": "/tmp/open-pencil-oracles"
},
"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": "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": "nested-luminance-mask",
"nodeId": "296616:2249",
"output": "/tmp/open-pencil-oracles/masks/nested-scale1",
"figmaSize": "240x120",
"openPencilSizeBeforeResize": "240x120",
"differentPixels": 12944,
"differentPercent": 44.94,
"finding": "Visual output is structurally close, but colors/antialiasing differ enough for a high raw AE score."
},
{
"name": "pattern-target-without-source",
"nodeId": "296519:2245",
"output": "/tmp/open-pencil-oracles/pattern-scale1",
"figmaSize": "160x100",
"openPencilSizeBeforeResize": "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."
},
{
"name": "pattern-frame-with-hidden-source",
"nodeId": "296731:2254",
"output": "/tmp/open-pencil-oracles/pattern-frame-scale1-after-sourceid",
"figmaSize": "180x100",
"openPencilSizeBeforeResize": "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.",
"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."
]
}