- Keep stale scene backings available during zoom for smooth interaction - Draw scaled backings with linear filtering to avoid blocky text artifacts
175 lines
4.9 KiB
TypeScript
175 lines
4.9 KiB
TypeScript
import { expect, mock, test } from 'bun:test'
|
|
import type { Canvas, Image as CKImage, Surface } from 'canvaskit-wasm'
|
|
|
|
import type { SkiaRenderer } from '#core/canvas/renderer'
|
|
import { renderSceneBacking } from '#core/canvas/renderer/retained-backing'
|
|
import type { SceneGraph } from '#core/scene-graph'
|
|
|
|
function createRenderer(surfaceFactory: () => Surface | null) {
|
|
const renderer: Partial<SkiaRenderer> = {
|
|
ck: {
|
|
AlphaType: { Premul: 'Premul' },
|
|
ColorSpace: { SRGB: 'SRGB' },
|
|
ColorType: { RGBA_8888: 'RGBA_8888' },
|
|
Color4f: mock((r: number, g: number, b: number, a: number) => [r, g, b, a]),
|
|
LTRBRect: mock((left: number, top: number, right: number, bottom: number) => [
|
|
left,
|
|
top,
|
|
right,
|
|
bottom
|
|
]),
|
|
FilterMode: { Linear: 'Linear' },
|
|
MipmapMode: { None: 'None' }
|
|
} as SkiaRenderer['ck'],
|
|
surface: {
|
|
makeSurface: mock(surfaceFactory)
|
|
} as SkiaRenderer['surface'],
|
|
opacityPaint: {
|
|
setAlphaf: mock()
|
|
} as SkiaRenderer['opacityPaint'],
|
|
panX: 0,
|
|
panY: 0,
|
|
zoom: 1,
|
|
dpr: 1,
|
|
viewportWidth: 100,
|
|
viewportHeight: 100,
|
|
pageColor: { r: 1, g: 1, b: 1 },
|
|
pageId: 'page',
|
|
sceneBacking: null,
|
|
sceneBackingBuild: null,
|
|
sceneBackingNeedsCrispRender: false,
|
|
sceneBackingPreviewUntil: 0,
|
|
sceneBackingAverageRecordMs: 40,
|
|
sceneBackingAverageViewportIntervalMs: 80,
|
|
scenePictureVersion: 0,
|
|
scenePicturePositionPreviewVersion: 0,
|
|
scenePicturePageId: null,
|
|
subtreePictureCache: new Map(),
|
|
subtreePictureCachePageId: null,
|
|
subtreePictureCacheSceneVersion: 0,
|
|
subtreePictureCachePositionPreviewVersion: 0,
|
|
worldViewport: { x: 0, y: 0, w: 0, h: 0 },
|
|
renderNode: mock()
|
|
}
|
|
return renderer as SkiaRenderer
|
|
}
|
|
|
|
function createCanvas() {
|
|
const canvas: Partial<Canvas> = {
|
|
drawImageRect: mock(),
|
|
drawImageRectOptions: mock()
|
|
}
|
|
return canvas as Canvas
|
|
}
|
|
|
|
function createGraph(positionPreviewVersion = 0) {
|
|
const graph: Partial<SceneGraph> = {
|
|
rootId: 'root',
|
|
positionPreviewVersion,
|
|
getNode: mock((id: string) => {
|
|
if (id === 'page') return { id: 'page', type: 'CANVAS', childIds: [] }
|
|
return null
|
|
}),
|
|
getAbsolutePosition: mock(() => ({ x: 0, y: 0 }))
|
|
}
|
|
return graph as SceneGraph
|
|
}
|
|
|
|
test('retained scene backing falls back when CanvasKit cannot create an offscreen surface', () => {
|
|
const r = createRenderer(() => null)
|
|
const canvas = createCanvas()
|
|
const graph = createGraph()
|
|
|
|
expect(renderSceneBacking(r, canvas, graph, 1)).toBe(false)
|
|
expect(r.surface.makeSurface).toHaveBeenCalled()
|
|
expect(canvas.drawImageRectOptions).not.toHaveBeenCalled()
|
|
expect(r.sceneBacking).toBeNull()
|
|
})
|
|
|
|
test('retained scene backing filters cross-zoom previews instead of falling back to live rendering', () => {
|
|
const r = createRenderer(() => null)
|
|
r.zoom = 1
|
|
r.sceneBackingPreviewUntil = Number.POSITIVE_INFINITY
|
|
r.sceneBacking = {
|
|
image: { delete: mock() } as CKImage,
|
|
pageId: 'page',
|
|
sceneVersion: 1,
|
|
positionPreviewVersion: 0,
|
|
panX: 0,
|
|
panY: 0,
|
|
zoom: 0.5,
|
|
width: 300,
|
|
height: 300,
|
|
dpr: 1,
|
|
worldX: 0,
|
|
worldY: 0,
|
|
worldWidth: 600,
|
|
worldHeight: 600
|
|
} as NonNullable<SkiaRenderer['sceneBacking']>
|
|
const canvas = createCanvas()
|
|
const graph = createGraph()
|
|
|
|
expect(renderSceneBacking(r, canvas, graph, 1)).toBe(true)
|
|
expect(canvas.drawImageRectOptions).toHaveBeenCalledWith(
|
|
r.sceneBacking.image,
|
|
expect.anything(),
|
|
expect.anything(),
|
|
r.ck.FilterMode.Linear,
|
|
r.ck.MipmapMode.None,
|
|
r.opacityPaint
|
|
)
|
|
})
|
|
|
|
test('retained scene backing allows same-zoom previews while panning', () => {
|
|
const r = createRenderer(() => null)
|
|
r.zoom = 1
|
|
r.sceneBackingPreviewUntil = Number.POSITIVE_INFINITY
|
|
r.sceneBacking = {
|
|
image: { delete: mock() } as CKImage,
|
|
pageId: 'page',
|
|
sceneVersion: 1,
|
|
positionPreviewVersion: 0,
|
|
panX: 0,
|
|
panY: 0,
|
|
zoom: 1,
|
|
width: 300,
|
|
height: 300,
|
|
dpr: 1,
|
|
worldX: 0,
|
|
worldY: 0,
|
|
worldWidth: 300,
|
|
worldHeight: 300
|
|
} as NonNullable<SkiaRenderer['sceneBacking']>
|
|
const canvas = createCanvas()
|
|
const graph = createGraph()
|
|
|
|
expect(renderSceneBacking(r, canvas, graph, 1)).toBe(true)
|
|
expect(canvas.drawImageRectOptions).toHaveBeenCalled()
|
|
})
|
|
|
|
test('retained scene backing invalidates stale position-preview metadata', () => {
|
|
const r = createRenderer(() => null)
|
|
r.sceneBacking = {
|
|
image: { delete: mock() } as CKImage,
|
|
pageId: 'page',
|
|
sceneVersion: 1,
|
|
positionPreviewVersion: 1,
|
|
panX: 0,
|
|
panY: 0,
|
|
zoom: 1,
|
|
width: 100,
|
|
height: 100,
|
|
dpr: 1,
|
|
worldX: 0,
|
|
worldY: 0,
|
|
worldWidth: 100,
|
|
worldHeight: 100
|
|
} as NonNullable<SkiaRenderer['sceneBacking']>
|
|
r.scenePicturePositionPreviewVersion = 1
|
|
const canvas = createCanvas()
|
|
const graph = createGraph(2)
|
|
|
|
expect(renderSceneBacking(r, canvas, graph, 1)).toBe(false)
|
|
expect(canvas.drawImageRectOptions).not.toHaveBeenCalled()
|
|
})
|