diff --git a/scripts/analyze-pattern-oracle.ts b/scripts/analyze-pattern-oracle.ts index 7e406a7ca..63c83dadb 100644 --- a/scripts/analyze-pattern-oracle.ts +++ b/scripts/analyze-pattern-oracle.ts @@ -66,16 +66,13 @@ const analysis = regions.map((region) => { const figmaRows = summarizeRows(findPatternComponents(figma, minComponentPixels, region)) const oursRows = summarizeRows(findPatternComponents(ours, minComponentPixels, region)) - return [ - region.name, - { figma: figmaRows, ours: oursRows, rowDeltas: rowDeltas(figmaRows, oursRows) } - ] + return [region.name, analyzeRows(figmaRows, oursRows)] }) ) : (() => { const figmaRows = summarizeRows(findPatternComponents(figma, minComponentPixels)) const oursRows = summarizeRows(findPatternComponents(ours, minComponentPixels)) - return { figma: figmaRows, ours: oursRows, rowDeltas: rowDeltas(figmaRows, oursRows) } + return analyzeRows(figmaRows, oursRows) })() console.log(JSON.stringify(analysis, null, 2)) @@ -236,7 +233,36 @@ function summarizeRows(components: Component[]): RowSummary[] { .toSorted((a, b) => a.y - b.y) } -function rowDeltas(figmaRows: RowSummary[], oursRows: RowSummary[]) { +function analyzeRows(figmaRows: RowSummary[], oursRows: RowSummary[]) { + const pairedRows = pairedRowDeltas(figmaRows, oursRows) + return { + figma: figmaRows, + ours: oursRows, + pairedRows, + nearestRows: nearestRowDeltas(figmaRows, oursRows), + summary: rowDeltaSummary(pairedRows, figmaRows.length, oursRows.length) + } +} + +function pairedRowDeltas(figmaRows: RowSummary[], oursRows: RowSummary[]) { + const count = Math.min(figmaRows.length, oursRows.length) + return Array.from({ length: count }, (_, index) => { + const figmaRow = figmaRows[index] + const oursRow = oursRows[index] + return { + index, + figmaY: figmaRow?.y ?? null, + oursY: oursRow?.y ?? null, + deltaY: figmaRow && oursRow ? Number((oursRow.y - figmaRow.y).toFixed(2)) : null, + figmaFirstX: figmaRow?.firstX ?? null, + oursFirstX: oursRow?.firstX ?? null, + deltaFirstX: + figmaRow && oursRow ? Number((oursRow.firstX - figmaRow.firstX).toFixed(2)) : null + } + }) +} + +function nearestRowDeltas(figmaRows: RowSummary[], oursRows: RowSummary[]) { return figmaRows.map((figmaRow) => { const nearest = oursRows.toSorted( (a, b) => Math.abs(a.y - figmaRow.y) - Math.abs(b.y - figmaRow.y) @@ -251,3 +277,21 @@ function rowDeltas(figmaRows: RowSummary[], oursRows: RowSummary[]) { } }) } + +function rowDeltaSummary( + rows: ReturnType, + figmaRowCount: number, + openPencilRowCount: number +) { + const valid = rows.filter((row) => row.deltaY !== null && row.deltaFirstX !== null) + if (valid.length === 0) return null + const avgDeltaY = valid.reduce((sum, row) => sum + (row.deltaY ?? 0), 0) / valid.length + const avgDeltaFirstX = valid.reduce((sum, row) => sum + (row.deltaFirstX ?? 0), 0) / valid.length + return { + pairedRowCount: valid.length, + avgDeltaY: Number(avgDeltaY.toFixed(2)), + avgDeltaFirstX: Number(avgDeltaFirstX.toFixed(2)), + missingOpenPencilRows: Math.max(0, figmaRowCount - openPencilRowCount), + extraOpenPencilRows: Math.max(0, openPencilRowCount - figmaRowCount) + } +} diff --git a/tests/engine/io/fig/import/pattern-paint-oracle.test.ts b/tests/engine/io/fig/import/pattern-paint-oracle.test.ts index 1bef8532c..26863ecb5 100644 --- a/tests/engine/io/fig/import/pattern-paint-oracle.test.ts +++ b/tests/engine/io/fig/import/pattern-paint-oracle.test.ts @@ -36,6 +36,16 @@ interface PaintOracle { source: { id: string; visible: boolean } targets: Array<{ alignment: string; fills: PatternOracleFill[] }> metrics: { rmseNormalized: number; fuzzDifferentPixels: number } + analysis: Record< + string, + { + pairedRowCount: number + avgDeltaY: number + avgDeltaFirstX: number + missingOpenPencilRows: number + extraOpenPencilRows: number + } + > } effects: { noise: { results: Record } @@ -83,6 +93,9 @@ describe('Figma pattern/noise/custom paint oracle availability', () => { } expect(alignment.metrics.rmseNormalized).toBeCloseTo(0.246422) expect(alignment.metrics.fuzzDifferentPixels).toBe(22209) + expect(alignment.analysis.START).toMatchObject({ avgDeltaY: 0, avgDeltaFirstX: 0 }) + expect(alignment.analysis.CENTER).toMatchObject({ avgDeltaY: 3, avgDeltaFirstX: -0.5 }) + expect(alignment.analysis.END).toMatchObject({ avgDeltaY: -6.67, avgDeltaFirstX: -0.5 }) }) test('records that noise and custom paint payloads are still blocked on Figma-authored samples', () => { diff --git a/tests/fixtures/figma-oracles/pattern-noise-custom-paints.json b/tests/fixtures/figma-oracles/pattern-noise-custom-paints.json index 617a474de..f3aa35a15 100644 --- a/tests/fixtures/figma-oracles/pattern-noise-custom-paints.json +++ b/tests/fixtures/figma-oracles/pattern-noise-custom-paints.json @@ -287,6 +287,29 @@ "fuzzDifferentPercent": 30.51, "rmseNormalized": 0.246422 }, - "note": "Captures START/CENTER/END pattern alignment in one frame with a visible source node. START matches current OpenPencil row origin closely; CENTER and END show remaining Figma origin differences." + "note": "Captures START/CENTER/END pattern alignment in one frame with a visible source node. START matches current OpenPencil row origin closely; CENTER and END show remaining Figma origin differences.", + "analysis": { + "START": { + "pairedRowCount": 6, + "avgDeltaY": 0, + "avgDeltaFirstX": 0, + "missingOpenPencilRows": 0, + "extraOpenPencilRows": 0 + }, + "CENTER": { + "pairedRowCount": 6, + "avgDeltaY": 3, + "avgDeltaFirstX": -0.5, + "missingOpenPencilRows": 1, + "extraOpenPencilRows": 0 + }, + "END": { + "pairedRowCount": 6, + "avgDeltaY": -6.67, + "avgDeltaFirstX": -0.5, + "missingOpenPencilRows": 0, + "extraOpenPencilRows": 0 + } + } } }