fix(canvas): cull nested nodes in world space
This commit is contained in:
parent
66f562b52f
commit
2f39ecaa4d
|
|
@ -187,7 +187,9 @@ export class SkiaRenderer {
|
|||
canvas: Canvas,
|
||||
graph: SceneGraph,
|
||||
nodeId: string,
|
||||
overlays: RenderOverlays
|
||||
overlays: RenderOverlays,
|
||||
parentAbsX?: number,
|
||||
parentAbsY?: number
|
||||
) => void
|
||||
declare renderSection: (canvas: Canvas, node: SceneNode, graph: SceneGraph) => void
|
||||
declare renderComponentSet: (canvas: Canvas, node: SceneNode, graph: SceneGraph) => void
|
||||
|
|
|
|||
|
|
@ -136,8 +136,15 @@ const rendererMethods: ThisType<SkiaRenderer> = {
|
|||
Labels.drawComponentLabels(this, canvas, graph)
|
||||
},
|
||||
|
||||
renderNode(canvas: Canvas, graph: SceneGraph, nodeId: string, overlays: RenderOverlays): void {
|
||||
SceneRender.renderNode(this, canvas, graph, nodeId, overlays)
|
||||
renderNode(
|
||||
canvas: Canvas,
|
||||
graph: SceneGraph,
|
||||
nodeId: string,
|
||||
overlays: RenderOverlays,
|
||||
parentAbsX?: number,
|
||||
parentAbsY?: number
|
||||
): void {
|
||||
SceneRender.renderNode(this, canvas, graph, nodeId, overlays, parentAbsX, parentAbsY)
|
||||
},
|
||||
|
||||
renderSection(canvas: Canvas, node: SceneNode, graph: SceneGraph): void {
|
||||
|
|
|
|||
|
|
@ -99,7 +99,9 @@ function renderChildren(
|
|||
canvas: Canvas,
|
||||
graph: SceneGraph,
|
||||
node: SceneNode,
|
||||
overlays: RenderOverlays
|
||||
overlays: RenderOverlays,
|
||||
absX: number,
|
||||
absY: number
|
||||
): void {
|
||||
const isClippableContainer =
|
||||
node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE'
|
||||
|
|
@ -111,12 +113,12 @@ function renderChildren(
|
|||
canvas.clipRect(r.ck.LTRBRect(0, 0, node.width, node.height), r.ck.ClipOp.Intersect, true)
|
||||
}
|
||||
for (const childId of node.childIds) {
|
||||
r.renderNode(canvas, graph, childId, overlays)
|
||||
r.renderNode(canvas, graph, childId, overlays, absX, absY)
|
||||
}
|
||||
canvas.restore()
|
||||
} else {
|
||||
for (const childId of node.childIds) {
|
||||
r.renderNode(canvas, graph, childId, overlays)
|
||||
r.renderNode(canvas, graph, childId, overlays, absX, absY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -169,7 +171,7 @@ export function renderNode(
|
|||
|
||||
applyNodeTransforms(r, canvas, node, nodeId, overlays)
|
||||
renderNodeContent(r, canvas, graph, node, nodeId, overlays)
|
||||
renderChildren(r, canvas, graph, node, overlays)
|
||||
renderChildren(r, canvas, graph, node, overlays, absX, absY)
|
||||
|
||||
if (layerBlur) {
|
||||
canvas.restore()
|
||||
|
|
|
|||
85
tests/engine/render/canvas/culling.test.ts
Normal file
85
tests/engine/render/canvas/culling.test.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { describe, expect, mock, test } from 'bun:test'
|
||||
|
||||
import { renderNode } from '#core/canvas/scene'
|
||||
import type { SkiaRenderer } from '#core/canvas/renderer'
|
||||
import { SceneGraph } from '#core/scene-graph'
|
||||
|
||||
function pageId(graph: SceneGraph) {
|
||||
return graph.getPages()[0].id
|
||||
}
|
||||
|
||||
function createCanvas() {
|
||||
return {
|
||||
save: mock(() => undefined),
|
||||
restore: mock(() => undefined),
|
||||
translate: mock(() => undefined),
|
||||
saveLayer: mock(() => undefined),
|
||||
clipRect: mock(() => undefined),
|
||||
clipRRect: mock(() => undefined)
|
||||
}
|
||||
}
|
||||
|
||||
function createRenderer() {
|
||||
const rendered: string[] = []
|
||||
const renderer = {
|
||||
_nodeCount: 0,
|
||||
_culledCount: 0,
|
||||
worldViewport: { x: 900, y: 900, w: 300, h: 300 },
|
||||
opacityPaint: { setAlphaf: mock(() => undefined) },
|
||||
effectLayerPaint: {
|
||||
setImageFilter: mock(() => undefined),
|
||||
setColorFilter: mock(() => undefined),
|
||||
setBlendMode: mock(() => undefined)
|
||||
},
|
||||
ck: {
|
||||
BlendMode: { SrcOver: 'SrcOver' },
|
||||
LTRBRect: mock((left: number, top: number, right: number, bottom: number) => [
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom
|
||||
]),
|
||||
ClipOp: { Intersect: 'Intersect' }
|
||||
},
|
||||
getCachedBlur: mock(() => null),
|
||||
renderShape: mock((_canvas, node) => {
|
||||
rendered.push(node.id)
|
||||
}),
|
||||
renderSection: mock((_canvas, node) => {
|
||||
rendered.push(node.id)
|
||||
}),
|
||||
renderComponentSet: mock((_canvas, node) => {
|
||||
rendered.push(node.id)
|
||||
}),
|
||||
renderNode(canvas, graph, nodeId, overlays, parentAbsX, parentAbsY) {
|
||||
renderNode(this as SkiaRenderer, canvas, graph, nodeId, overlays, parentAbsX, parentAbsY)
|
||||
}
|
||||
}
|
||||
return { renderer: renderer as SkiaRenderer, rendered }
|
||||
}
|
||||
|
||||
describe('canvas culling', () => {
|
||||
test('uses accumulated absolute position for nested children', () => {
|
||||
const graph = new SceneGraph()
|
||||
const frame = graph.createNode('FRAME', pageId(graph), {
|
||||
x: 1000,
|
||||
y: 1000,
|
||||
width: 200,
|
||||
height: 200
|
||||
})
|
||||
const text = graph.createNode('TEXT', frame.id, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 24,
|
||||
text: 'Visible nested text'
|
||||
})
|
||||
const { renderer, rendered } = createRenderer()
|
||||
|
||||
renderNode(renderer, createCanvas(), graph, frame.id, {})
|
||||
|
||||
expect(rendered).toContain(frame.id)
|
||||
expect(rendered).toContain(text.id)
|
||||
expect(renderer._culledCount).toBe(0)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue