openpencil/tests/engine/render/canvas/schema-fill-fallback.test.ts
Danila Poyarkov a7d7b71995 fix(fig): preserve multi-color vector fills
- Resolve Figma path style overrides into format-neutral geometry fills
- Render and export per-path paints across normal and pattern drawing
- Preserve path fills through copying, instance scaling, and derived geometry
- Cover import, serialization, scaling, and CanvasKit output

Co-authored-by: Rob Coenen <753704+rcoenen@users.noreply.github.com>
2026-07-28 11:45:58 +03:00

97 lines
2.9 KiB
TypeScript

import { describe, expect, mock, test } from 'bun:test'
import { SceneGraph } from '@open-pencil/scene-graph'
import type { Fill, SceneNode } from '@open-pencil/scene-graph'
import { applyFill } from '#core/canvas/fills'
import type { SkiaRenderer } from '#core/canvas/renderer'
function createRenderer() {
const picture = {
makeShader: mock(() => 'pattern-shader'),
delete: mock(() => undefined)
}
const recorder = {
beginRecording: mock(() => ({
drawRect: mock(() => undefined),
drawRRect: mock(() => undefined),
restore: mock(() => undefined),
save: mock(() => undefined),
scale: mock(() => undefined),
translate: mock(() => undefined)
})),
finishRecordingAsPicture: mock(() => picture),
delete: mock(() => undefined)
}
return {
fillPaint: {
setShader: mock(() => undefined),
setColor: mock(() => undefined),
setAlphaf: mock(() => undefined),
setBlendMode: mock(() => undefined)
},
ck: {
BlendMode: { SrcOver: 'source-over' },
Color4f: mock((r, g, b, a) => ['color', r, g, b, a]),
FilterMode: { Linear: 'linear' },
LTRBRect: mock((left, top, right, bottom) => [left, top, right, bottom]),
PictureRecorder: mock(() => recorder),
TileMode: { Repeat: 'repeat' }
},
resolveFillColor: mock((fill: Fill) => fill.color),
makeRRect: mock(() => 'rrect')
} as SkiaRenderer
}
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', () => {
const graph = new SceneGraph()
const page = graph.getPages()[0]
const source = graph.createNode('RECTANGLE', page.id, {
width: 10,
height: 10,
fills: [
{
type: 'SOLID',
color: { r: 1, g: 0, b: 0, a: 1 },
opacity: 1,
visible: true
}
]
})
source.source.id = '12:34'
const renderer = createRenderer()
const fill: Fill = {
type: 'PATTERN',
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,
visible: true
}
expect(applyFill(renderer, fill, node, graph)).toBe(true)
expect(renderer.fillPaint.setShader).toHaveBeenLastCalledWith('pattern-shader')
expect(renderer.fillPaint.setColor).not.toHaveBeenCalledWith(['color', 0.2, 0.3, 0.4, 0.8])
})
test.each(['PATTERN', 'NOISE', 'CUSTOM'] as const)(
'renders %s fills as solid fallback',
(type) => {
const renderer = createRenderer()
const fill: Fill = {
type,
color: { r: 0.2, g: 0.3, b: 0.4, a: 0.8 },
opacity: 1,
visible: true
}
expect(applyFill(renderer, fill, node, new SceneGraph())).toBe(true)
expect(renderer.fillPaint.setColor).toHaveBeenCalledWith(['color', 0.2, 0.3, 0.4, 0.8])
}
)
})