// Slice 4d — renderer core. // // These are the automated traces of 17.16: coordinate spaces and fit, the // primitive set, the normative transform order, group composition, depth // sorting, perspective, depth fog, style inheritance, the authoring limits, the // conic fallback, path legality, masking, and once-at-instantiation resolution. // Each asserts a numeric oracle against the frame plan rather than a pixel, // which is what makes them executable without a display (17.16). import assert from 'node:assert/strict'; import test from 'node:test'; import { ResolutionEngine } from '../src/runtime/resolution.js'; import { SeededRNG } from '../src/runtime/rng.js'; import { RuntimeFault } from '../src/runtime/types.js'; import { validateExhibit } from '../src/runtime/validator.js'; import { VisualEngine, resolveBackingStore, resolveFit, sampleTree } from '../src/runtime/visual-engine.js'; import { primitiveSubpaths } from '../src/runtime/visual-geometry.js'; import { parseColor } from '../src/runtime/visual-math.js'; import { renderFrame } from '../src/runtime/visual-canvas2d.js'; const close = (actual, expected, epsilon = 1e-9) => assert.ok(Math.abs(actual - expected) <= epsilon, `${actual} != ${expected}`); const closePoint = (actual, expected, epsilon = 1e-9) => { close(actual[0], expected[0], epsilon); close(actual[1], expected[1], epsilon); }; function exhibit(visuals, overrides = {}) { return { xzbt: '0.1', meta: { id: 'render-study', name: 'Render Study' }, runtime: { seed: 42 }, visuals, ...overrides }; } function build(visuals, overrides = {}) { const document = exhibit(visuals, overrides); const resolution = new ResolutionEngine(document, new SeededRNG(42)); return new VisualEngine(document, { resolution, rng: new SeededRNG(42), ...overrides.engine }); } const scene = { coordinateSpace: 'virtual', width: 1600, height: 900 }; const graphic = (content, extra = {}) => ({ type: 'graphic', content, ...extra }); const codes = (document) => validateExhibit(document).errors.map((error) => error.code); // --------------------------------------------------------------------------- // Trace 1 — coordinate spaces and fit (17.4, 19.3) // --------------------------------------------------------------------------- test('17.16.1 each coordinate space maps a known point under each fit mode', () => { // virtual, contain, matched aspect: a pure uniform scale with no letterbox. const exact = resolveFit({ coordinateSpace: 'virtual', width: 1600, height: 900, fit: 'contain' }, 1920, 1080); close(exact.sx, 1.2); close(exact.ox, 0); close(exact.oy, 0); // contain into a square display letterboxes on the short axis. const contain = resolveFit({ coordinateSpace: 'virtual', width: 1600, height: 900, fit: 'contain' }, 1000, 1000); close(contain.sx, 0.625); close(contain.ox, 0); close(contain.oy, 218.75); // cover crops instead: the offsets go negative. const cover = resolveFit({ coordinateSpace: 'virtual', width: 1600, height: 900, fit: 'cover' }, 1000, 1000); close(cover.sx, 1000 / 900); close(cover.oy, 0); close(cover.ox, (1000 - (1000 / 900) * 1600) / 2); assert.ok(cover.ox < 0, 'cover crops the long axis'); // stretch scales each axis independently and changes the aspect ratio. const stretch = resolveFit({ coordinateSpace: 'virtual', width: 1600, height: 900, fit: 'stretch' }, 1000, 1000); close(stretch.sx, 0.625); close(stretch.sy, 1000 / 900); close(stretch.ox, 0); close(stretch.oy, 0); // normalized is the unit square under the same rules. const normalized = resolveFit({ coordinateSpace: 'normalized' }, 1600, 900); close(normalized.sx, 900); close(normalized.ox, 350); // viewport has no intrinsic size, so it never scales and takes no fit default. const viewport = resolveFit({ coordinateSpace: 'viewport' }, 1280, 720); close(viewport.sx, 1); close(viewport.sy, 1); close(viewport.ox, 0); close(viewport.oy, 0); }); test('17.16.1 a scene point lands at the documented display point', () => { const engine = build({ scene: { ...scene, fit: 'contain' }, systems: { s: graphic({ mark: { type: 'point', position: { x: 800, y: 450 } } }) } }); const plan = engine.planFrame({ width: 1000, height: 1000 }); // Scene center, letterboxed: (800, 450) -> 0.625 * (800, 450) + (0, 218.75). closePoint(plan.layers[0].nodes[0].center, [500, 500]); }); test('19.5 the device-pixel multiplier is clamped, and drops below 1 on a large display (A3)', () => { assert.equal(resolveBackingStore(1920, 1080, 3).multiplier, 2, 'clamped to the ceiling'); assert.equal(resolveBackingStore(1920, 1080, 1).multiplier, 1); const large = resolveBackingStore(5000, 1000, 2); assert.ok(large.below1 && large.width === 4096, 'the backing-store cap wins over the multiplier floor'); assert.equal(resolveBackingStore(800, 600, 0.75).multiplier, 0.75, 'a reported ratio below 1 is kept'); }); // --------------------------------------------------------------------------- // Trace 2 — the primitive set (17.9) // --------------------------------------------------------------------------- test('17.16.2 every primitive instantiates, and the two rejections differ', () => { const minimal = { point: {}, line: { to: { x: 4, y: 0 } }, polyline: { points: [{ x: 0, y: 0 }, { x: 4, y: 4 }] }, polygon: { points: [{ x: 0, y: 0 }, { x: 4, y: 0 }, { x: 2, y: 4 }] }, rectangle: { size: { width: 4, height: 2 } }, 'rounded-rectangle': { size: { width: 8, height: 6 }, radius: 2 }, ellipse: { radius: 3 }, arc: { radius: 3, startAngle: 0, endAngle: 90 }, ring: { radius: 6, innerRadius: 3 }, path: { commands: [{ op: 'move', to: { x: 0, y: 0 } }, { op: 'line', to: { x: 3, y: 3 } }] }, bezier: { c1: { x: 1, y: 0 }, c2: { x: 2, y: 3 }, to: { x: 3, y: 3 } }, spline: { points: [{ x: 0, y: 0 }, { x: 3, y: 3 }, { x: 6, y: 0 }] }, text: { text: 'label' }, group: { children: { inner: { type: 'point' } } } }; const content = {}; for (const [type, fields] of Object.entries(minimal)) content[`k${type.replace(/-/g, '')}`] = { type, ...fields }; const document = exhibit({ scene, systems: { s: graphic(content) } }); assert.deepEqual(validateExhibit(document).errors, [], 'every minimal primitive validates'); const engine = build({ scene, systems: { s: graphic(content) } }); const plan = engine.planFrame({ width: 1600, height: 900 }); assert.equal(plan.layers[0].nodes.length, Object.keys(minimal).length); // An unknown type and an unknown property on a known type are different codes. assert.ok(codes(exhibit({ scene, systems: { s: graphic({ a: { type: 'planet', radius: 2 } }) } })).includes('ERR_INVALID_PRIMITIVE_TYPE')); assert.ok(codes(exhibit({ scene, systems: { s: graphic({ a: { type: 'ellipse', radius: 2, size: { width: 1, height: 1 } } }) } })).includes('ERR_UNKNOWN_FIELD')); // 17.8: an object carries no id, and 17.10: layer is a system property. assert.ok(codes(exhibit({ scene, systems: { s: graphic({ a: { type: 'point', id: 'a' } }) } })).includes('ERR_UNKNOWN_FIELD')); assert.ok(codes(exhibit({ scene, systems: { s: graphic({ a: { type: 'point', layer: 'near' } }) } })).includes('ERR_UNKNOWN_FIELD')); }); test('17.9 local extents and anchors follow the documented table (V4)', () => { // A rectangle is anchored by its top-left corner; circular primitives are centered. assert.deepEqual(primitiveSubpaths({ type: 'rectangle', size: { width: 10, height: 4 } })[0].start, [0, 0]); const ellipse = primitiveSubpaths({ type: 'ellipse', radius: 5 })[0]; closePoint(ellipse.start, [5, 0]); // A ring's angles default to a full turn, which is two subpaths with a hole. assert.equal(primitiveSubpaths({ type: 'ring', radius: 10, innerRadius: 4 }).length, 2); // A closed bezier spline closes with a straight line, not an invented curve. const closedBezier = primitiveSubpaths({ type: 'spline', mode: 'bezier', closed: true, points: [{ x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 0 }] })[0]; assert.equal(closedBezier.segments.at(-1).type, 'line'); }); test('17.9 the arc sweep bound is checked before normalization (V4)', () => { // 350 -> 10 clockwise is a 20-degree sweep through zero, not 340 the long way. const short = primitiveSubpaths({ type: 'arc', radius: 10, startAngle: 350, endAngle: 10 })[0]; assert.equal(short.segments.length, 1, 'a 20-degree sweep is one Bezier piece'); assert.throws(() => primitiveSubpaths({ type: 'arc', radius: 10, startAngle: 0, endAngle: 720 }), (error) => error instanceof RuntimeFault && error.code === 'ERR_OUT_OF_BOUNDS'); assert.ok(codes(exhibit({ scene, systems: { s: graphic({ a: { type: 'arc', radius: 10, startAngle: 0, endAngle: 720 } }) } })).includes('ERR_OUT_OF_BOUNDS')); // A zero sweep draws nothing and raises no diagnostic. assert.equal(primitiveSubpaths({ type: 'arc', radius: 10, startAngle: 45, endAngle: 45 })[0].segments.length, 0); }); // --------------------------------------------------------------------------- // Traces 3 and 4 — transforms and groups (17.11) // --------------------------------------------------------------------------- test('17.16.3 the transform composition order is normative where it matters', () => { const engine = build({ scene: { coordinateSpace: 'viewport' }, systems: { s: graphic({ mark: { type: 'point', position: { x: 10, y: 5 }, transform: { rotation: 90, scale: { x: 2, y: 1 } } } }) } }); const plan = engine.planFrame({ width: 100, height: 100 }); // Scale, then skew, then rotate about origin, then translate: (1,0) is not a // point of a `point`, so the origin itself lands at position exactly. closePoint(plan.layers[0].nodes[0].center, [10, 5]); // A rotated rectangle's corner is the case where the order is observable. const rotated = build({ scene: { coordinateSpace: 'viewport' }, systems: { s: graphic({ box: { type: 'rectangle', size: { width: 4, height: 2 }, transform: { rotation: 90, scale: { x: 2, y: 1 } } } }) } }).planFrame({ width: 100, height: 100 }); // Local (4, 0) -> scale (8, 0) -> rotate 90 (positive toward +y) -> (0, 8). closePoint(rotated.layers[0].nodes[0].subpaths[0].segments[0].to, [0, 8]); // Reversing the two stages would give (0, 4) scaled to (0, 4): a different point. assert.notDeepEqual(rotated.layers[0].nodes[0].subpaths[0].segments[0].to, [0, 4]); }); test('17.16.4 a nested group composes matrices and depth, and 9 levels is a limit', () => { const engine = build({ scene: { coordinateSpace: 'viewport' }, systems: { s: graphic({ outer: { type: 'group', position: { x: 100, y: 0 }, z: 5, children: { inner: { type: 'point', position: { x: 10, y: 3 }, z: 3 } } } }) } }); const plan = engine.planFrame({ width: 400, height: 400 }); const group = plan.layers[0].nodes[0]; closePoint(group.children[0].center, [110, 3], 1e-9); assert.equal(group.children[0].z, 8, 'z_parent + z_local'); const nest = (depth) => { let node = { type: 'point' }; for (let level = 0; level < depth; level += 1) node = { type: 'group', children: { g: node } }; return node; }; assert.ok(codes(exhibit({ scene, systems: { s: graphic({ root: nest(9) }) } })).includes('ERR_VISUAL_LIMIT_EXCEEDED')); assert.deepEqual(codes(exhibit({ scene, systems: { s: graphic({ root: nest(7) }) } })), []); }); // --------------------------------------------------------------------------- // Traces 5 and 6 — depth (17.6) // --------------------------------------------------------------------------- test('17.16.5 depth sorting draws greater z first and is stable in document order', () => { const engine = build({ scene, systems: { a: graphic({ near: { type: 'point', z: 0 }, mid: { type: 'point', z: 10 } }), b: graphic({ far: { type: 'point', z: 100 }, alsoMid: { type: 'point', z: 10 } }) } }); const order = () => engine.planFrame({ width: 1600, height: 900 }).layers[0].nodes.map((node) => node.path.split('.').pop()); const first = order(); assert.deepEqual(first, ['far', 'mid', 'alsoMid', 'near']); // Equal z keeps system key order then object key order, on every frame. assert.deepEqual(order(), first); assert.deepEqual(order(), first); }); test('17.16.6 perspective scales by focalLength / (focalLength + z), and culls at the eye', () => { const make = (projection) => build({ scene: { coordinateSpace: 'viewport' }, camera: { projection, focalLength: 1000, x: 200, y: 200 }, systems: { s: graphic({ front: { type: 'point', z: 0 }, back: { type: 'point', z: 1000 }, close: { type: 'point', z: -500 } }) } }).planFrame({ width: 400, height: 400 }); const perspective = make('perspective'); const factors = Object.fromEntries(perspective.layers[0].nodes.map((node) => [node.path.split('.').pop(), node.perspective])); close(factors.front, 1); close(factors.back, 0.5); close(factors.close, 2); const orthographic = make('orthographic'); for (const node of orthographic.layers[0].nodes) close(node.perspective, 1, 0); // Sorting, parallax, and fog still see z under orthographic. assert.deepEqual(orthographic.layers[0].nodes.map((node) => node.z), [1000, 0, -500]); // An object at or behind the eye is culled for the frame, with no diagnostic. const culled = build({ scene: { coordinateSpace: 'viewport' }, camera: { projection: 'perspective', focalLength: 1000 }, systems: { s: graphic({ eye: { type: 'point', z: -1000 }, behind: { type: 'point', z: -1200 }, ok: { type: 'point', z: -999 } }) } }).planFrame({ width: 400, height: 400 }); assert.equal(culled.culled, 2); assert.equal(culled.layers[0].nodes.length, 1); assert.deepEqual(culled.diagnostics, []); }); test('17.16.7 depth fog blends by its documented fraction, and far <= near is a range error', () => { const engine = build({ scene: { ...scene, depthFog: { color: '#000000', near: 0, far: 100, density: 1 } }, systems: { s: graphic({ a: { type: 'rectangle', size: { width: 1, height: 1 }, z: 0, style: { fill: '#ffffff' } }, b: { type: 'rectangle', size: { width: 1, height: 1 }, z: 50, style: { fill: '#ffffff' } }, c: { type: 'rectangle', size: { width: 1, height: 1 }, z: 200, style: { fill: '#ffffff' } } }) } }); const nodes = Object.fromEntries(engine.planFrame({ width: 1600, height: 900 }).layers[0].nodes.map((node) => [node.path.split('.').pop(), node])); close(nodes.a.fogFraction, 0); close(nodes.b.fogFraction, 0.5); close(nodes.c.fogFraction, 1); assert.equal(nodes.a.fill.color, '#ffffff'); assert.equal(nodes.b.fill.color, '#808080', '255 + (0 - 255) * 0.5'); assert.equal(nodes.c.fill.color, '#000000'); assert.ok(codes(exhibit({ scene: { ...scene, depthFog: { color: '#000', near: 100, far: 100 } } })).includes('ERR_INVALID_RANGE_ORDER')); }); test('17.6 fog keeps alpha and fogs gradient stops before the paint is built (V18)', () => { const engine = build({ scene: { ...scene, depthFog: { color: '#000000', near: 0, far: 100, density: 1 } }, systems: { s: graphic({ a: { type: 'rectangle', size: { width: 10, height: 10 }, z: 50, style: { fill: { type: 'linear-gradient', stops: [{ offset: 0, color: '#ffffff80' }, { offset: 1, color: '#ff0000' }], from: { x: 0, y: 0 }, to: { x: 10, y: 0 } } } } }) } }); const paint = engine.planFrame({ width: 1600, height: 900 }).layers[0].nodes[0].fill; assert.equal(paint.type, 'linear-gradient'); assert.deepEqual(paint.stops.map((stop) => stop.offset), [0, 1], 'offsets are preserved exactly'); // Alpha survives the fog; the components are halved toward the fog color. assert.equal(parseColor(paint.stops[0].color).a, parseColor('#ffffff80').a); assert.equal(paint.stops[1].color, '#800000'); }); // --------------------------------------------------------------------------- // Traces 8 to 12 — appearance, limits, fallback, paths, masking (17.12, 17.13) // --------------------------------------------------------------------------- test('17.16.8 style inheritance is per field, and a stroke-only fill is a declaration error', () => { const engine = build({ scene: { coordinateSpace: 'viewport' }, systems: { s: graphic({ g: { type: 'group', style: { fill: '#ff0000', stroke: '#00ff00', strokeWidth: 4 }, children: { box: { type: 'rectangle', size: { width: 2, height: 2 }, style: { fill: '#0000ff' } }, edge: { type: 'line', to: { x: 5, y: 0 } } } } }) } }); const [box, edge] = engine.planFrame({ width: 100, height: 100 }).layers[0].nodes[0].children; assert.equal(box.fill.color, '#0000ff', 'the child redeclares fill'); assert.equal(box.stroke.color, '#00ff00', 'and keeps the ancestor stroke'); close(box.strokeWidth, 4); // 17.12: an inherited fill is ignored for a stroke-only primitive rather than // becoming a validation error. assert.equal(edge.fill, null); assert.equal(edge.stroke.color, '#00ff00'); // A declared one is an error. assert.ok(codes(exhibit({ scene, systems: { s: graphic({ e: { type: 'line', to: { x: 1, y: 1 }, style: { fill: '#fff' } } }) } })).includes('ERR_UNKNOWN_FIELD')); }); test('17.12 opacity multiplies down the tree and layer opacity is not applied twice (V10)', () => { const engine = build({ scene: { coordinateSpace: 'viewport' }, layers: { only: { opacity: 0.5 } }, systems: { s: graphic({ g: { type: 'group', style: { opacity: 0.5 }, children: { a: { type: 'point', style: { opacity: 0.4 } }, b: { type: 'point' } } } }, { layer: 'only' }) } }); const plan = engine.planFrame({ width: 100, height: 100 }); const [a, b] = plan.layers[0].nodes[0].children; close(a.alpha, 0.2, 1e-12); close(b.alpha, 0.5, 1e-12); // The layer's own opacity is carried on the layer, once. close(plan.layers[0].opacity, 0.5); assert.ok(plan.layers[0].buffered, 'a non-default layer opacity needs a buffer'); }); test('17.16.9 each authoring limit rejects one past its maximum and accepts the maximum', () => { const points = (count) => Array.from({ length: count }, (_, index) => ({ x: index, y: 0 })); const stops = (count) => Array.from({ length: count }, (_, index) => ({ offset: index / (count - 1), color: '#ffffff' })); const commands = (count) => [{ op: 'move', to: { x: 0, y: 0 } }, ...Array.from({ length: count - 1 }, () => ({ op: 'line', to: { x: 1, y: 1 } }))]; const filters = (count) => Array.from({ length: count }, () => ({ type: 'invert', amount: 1 })); const cases = [ [{ type: 'polygon', points: points(513) }, { type: 'polygon', points: points(512) }], [{ type: 'spline', points: points(257) }, { type: 'spline', points: points(256) }], [{ type: 'path', commands: commands(513) }, { type: 'path', commands: commands(512) }], [{ type: 'rectangle', size: { width: 1, height: 1 }, style: { fill: { type: 'linear-gradient', from: { x: 0, y: 0 }, to: { x: 1, y: 0 }, stops: stops(17) } } }, { type: 'rectangle', size: { width: 1, height: 1 }, style: { fill: { type: 'linear-gradient', from: { x: 0, y: 0 }, to: { x: 1, y: 0 }, stops: stops(16) } } }], [{ type: 'point', style: { filters: filters(5) } }, { type: 'point', style: { filters: filters(4) } }] ]; for (const [over, at] of cases) { assert.ok(codes(exhibit({ scene, systems: { s: graphic({ a: over }) } })).includes('ERR_VISUAL_LIMIT_EXCEEDED'), JSON.stringify(over.type)); assert.deepEqual(codes(exhibit({ scene, systems: { s: graphic({ a: at }) } })), [], JSON.stringify(at.type)); } }); test('17.16.10 the conic fallback keeps its stops and warns once per paint instance', () => { const document = exhibit({ scene: { coordinateSpace: 'viewport' }, systems: { s: graphic({ disc: { type: 'ellipse', radius: 20, style: { fill: { type: 'conic-gradient', center: { x: 0, y: 0 }, angle: 0, stops: [{ offset: 0, color: '#ff0000' }, { offset: 1, color: '#0000ff' }] } } } }) } }); const engine = new VisualEngine(document, { resolution: new ResolutionEngine(document, new SeededRNG(42)), rng: new SeededRNG(42), capabilities: { conicGradient: false } }); let plan = engine.planFrame({ width: 100, height: 100 }); const paint = plan.layers[0].nodes[0].fill; assert.equal(paint.type, 'linear-gradient'); assert.ok(paint.approximated); assert.deepEqual(paint.stops.map((stop) => stop.color), ['#ff0000', '#0000ff'], 'the same stops, at the same offsets'); assert.equal(engine.cadence.raised.length, 1); for (let frame = 0; frame < 120; frame += 1) plan = engine.planFrame({ width: 100, height: 100 }); assert.equal(engine.cadence.raised.length, 1, 'once per paint instance, never once per frame'); assert.equal(engine.cadence.raised[0].code, 'WARN_VISUAL_APPROXIMATION'); // A renderer that does have conic gradients emits the real paint and no warning. const capable = build({ scene: { coordinateSpace: 'viewport' }, systems: document.visuals.systems }); assert.equal(capable.planFrame({ width: 100, height: 100 }).layers[0].nodes[0].fill.type, 'conic-gradient'); assert.equal(capable.cadence.raised.length, 0); }); test('17.16.11 path legality, arc radii, and bezier point counts', () => { const path = (commands) => exhibit({ scene, systems: { s: graphic({ a: { type: 'path', commands } }) } }); assert.ok(codes(path([{ op: 'line', to: { x: 1, y: 1 } }])).includes('ERR_INVALID_PATH')); assert.ok(codes(path([{ op: 'move', to: { x: 0, y: 0 } }, { op: 'close' }, { op: 'close' }])).includes('ERR_INVALID_PATH')); // A zero-radius arc fails validation rather than becoming a line. assert.ok(codes(path([{ op: 'move', to: { x: 0, y: 0 } }, { op: 'arc', radius: 0, to: { x: 1, y: 1 } }])).includes('ERR_OUT_OF_BOUNDS')); assert.ok(codes(exhibit({ scene, systems: { s: graphic({ a: { type: 'spline', mode: 'bezier', points: [{ x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 2 }] } }) } })).includes('ERR_SCHEMA_VALIDATION')); assert.deepEqual(codes(exhibit({ scene, systems: { s: graphic({ a: { type: 'spline', mode: 'bezier', points: [{ x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 2 }, { x: 3, y: 3 }] } }) } })), []); }); test('17.16.12 a mask names a child of its own group and that child is not drawn', () => { const engine = build({ scene: { coordinateSpace: 'viewport' }, systems: { s: graphic({ g: { type: 'group', style: { mask: 'cut' }, children: { shown: { type: 'point' }, cut: { type: 'ellipse', radius: 4 } } } }) } }); const group = engine.planFrame({ width: 100, height: 100 }).layers[0].nodes[0]; assert.equal(group.children.length, 1); assert.equal(group.children[0].path.endsWith('shown'), true); assert.ok(group.mask && group.mask.path.endsWith('cut')); assert.ok(group.needsBuffer, 'a mask requires an offscreen buffer'); assert.ok(codes(exhibit({ scene, systems: { s: graphic({ g: { type: 'group', style: { mask: 'missing' }, children: { a: { type: 'point' } } } }) } })).includes('ERR_INVALID_REFERENCE')); assert.ok(codes(exhibit({ scene, systems: { s: graphic({ a: { type: 'point', style: { mask: 'x' } } }) } })).includes('ERR_UNKNOWN_FIELD')); }); // --------------------------------------------------------------------------- // Trace 13 — once-at-instantiation resolution (17.14, 9.3) // --------------------------------------------------------------------------- test('17.16.13 visual fields resolve once and rendering consumes no procedural stream', () => { const visuals = { scene: { coordinateSpace: 'viewport' }, systems: { s: graphic({ mark: { type: 'point', position: { x: { random: { min: 0, max: 100 } }, y: 50 } } }) } }; const document = exhibit(visuals); let streamRequests = 0; const rng = new SeededRNG(42); const counting = { stream: (domain, key) => { streamRequests += 1; return rng.stream(domain, key); } }; const engine = new VisualEngine(document, { resolution: new ResolutionEngine(document, new SeededRNG(42)), rng: counting }); const afterInstantiation = streamRequests; assert.ok(afterInstantiation > 0, 'instantiation samples once'); const first = engine.planFrame({ width: 200, height: 200 }).layers[0].nodes[0].center[0]; for (let frame = 0; frame < 600; frame += 1) { const value = engine.planFrame({ width: 200, height: 200 }).layers[0].nodes[0].center[0]; assert.equal(value, first, 'a resolved position holds across every frame'); } assert.equal(streamRequests, afterInstantiation, 'rendering 600 frames requests no stream'); // Two runs of one seed resolve identically. const second = new VisualEngine(document, { resolution: new ResolutionEngine(document, new SeededRNG(42)), rng: new SeededRNG(42) }); assert.equal(second.planFrame({ width: 200, height: 200 }).layers[0].nodes[0].center[0], first); // A different seed does not. const other = new VisualEngine(document, { resolution: new ResolutionEngine(document, new SeededRNG(7)), rng: new SeededRNG(7) }); assert.notEqual(other.planFrame({ width: 200, height: 200 }).layers[0].nodes[0].center[0], first); }); test('17.14 sampling is depth-first in property-document order', () => { const resolver = { evaluate: (spec, stream, path) => path }; assert.deepEqual(sampleTree({ b: { random: {} }, a: { nested: { random: {} } } }, resolver, null, '$'), { b: '$.b', a: { nested: '$.a.nested' } }); // A path command's `op` is a command token, not a ValueSpec operator. assert.deepEqual(sampleTree({ commands: [{ op: 'move', to: { x: 1, y: 2 } }] }, resolver, null, '$'), { commands: [{ op: 'move', to: { x: 1, y: 2 } }] }); }); // --------------------------------------------------------------------------- // Compositing, buffers, and the Canvas 2D backend (17.12, 19.5) // --------------------------------------------------------------------------- test('19.5 buffer refusals are taken farthest first and are diagnosed, not silent (V10)', () => { const content = {}; for (let index = 0; index < 20; index += 1) { content[`o${index}`] = { type: 'rectangle', size: { width: 1, height: 1 }, z: index, style: { blur: 2 } }; } const engine = build({ scene: { coordinateSpace: 'viewport' }, systems: { s: graphic(content) } }); const plan = engine.planFrame({ width: 200, height: 200 }); assert.equal(plan.buffers.limit, 16); assert.equal(plan.buffers.granted, 16); assert.equal(plan.buffers.shed, 4); const shed = plan.layers[0].nodes.filter((node) => node.shed).map((node) => node.z); assert.deepEqual(shed.sort((a, b) => a - b), [16, 17, 18, 19], 'the four farthest lost their buffers'); for (const node of plan.layers[0].nodes.filter((entry) => entry.shed)) { assert.equal(node.blur, 0, 'a refused object draws without its buffered features'); } assert.equal(plan.diagnostics[0].code, 'WARN_VISUAL_APPROXIMATION'); assert.equal(plan.diagnostics.length, 1, 'one diagnostic per key per second, not one per shed item'); }); test('the Canvas 2D backend issues the compositing order of 17.12', () => { const calls = []; const record = (name) => (...args) => calls.push([name, ...args]); const context = { canvas: { width: 200, height: 200 }, save: record('save'), restore: record('restore'), beginPath: record('beginPath'), moveTo: record('moveTo'), lineTo: record('lineTo'), bezierCurveTo: record('bezierCurveTo'), closePath: record('closePath'), fill: record('fill'), stroke: record('stroke'), arc: record('arc'), ellipse: record('ellipse'), clip: record('clip'), fillRect: record('fillRect'), setTransform: record('setTransform'), setLineDash: record('setLineDash'), fillText: record('fillText'), strokeText: record('strokeText'), drawImage: record('drawImage'), createLinearGradient: () => ({ addColorStop: record('addColorStop') }) }; const engine = build({ scene: { coordinateSpace: 'viewport' }, systems: { s: graphic({ box: { type: 'rectangle', size: { width: 10, height: 10 }, style: { fill: '#ffffff', stroke: '#ff0000', strokeWidth: 2 } }, label: { type: 'text', text: 'HI', size: 12, style: { fill: '#ffffff' } } }) } }); const plan = engine.planFrame({ width: 200, height: 200, devicePixelRatio: 2 }); renderFrame(context, plan); const names = calls.map((call) => call[0]); assert.ok(names.includes('fillRect'), 'the background is cleared to scene.background'); assert.ok(names.includes('moveTo') && names.includes('lineTo') && names.includes('fill') && names.includes('stroke')); assert.ok(names.includes('fillText'), 'text draws through the backend text engine'); // The stroke width reaches the backend already in device pixels: 2 * dpr, // with no canvas transform to distort it. close(plan.layers[0].nodes[0].strokeWidth, 4, 1e-12); close(context.lineWidth, 4, 1e-12); });