// Slice 4e — components, procedural systems, behaviors, fields, trails, links. // // The automated traces of 18.10. Where a trace names a closed-form result — the // integrator, the emission accumulator, a life ramp, a distribution's draw // count, a noise sample — the test asserts that number rather than a rendered // pixel, which is what makes these executable without a display. 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 } from '../src/runtime/visual-engine.js'; import { ProceduralSystem } from '../src/runtime/visual-systems.js'; import { FieldSet } from '../src/runtime/visual-fields.js'; import { placeItem, sampleCount } from '../src/runtime/visual-distributions.js'; import { PERMUTATION_DRAWS, noiseVector, octaveNoise, permutationTable } from '../src/runtime/visual-noise.js'; import { behaviorChannels, curveValue, waveform } from '../src/runtime/visual-behaviors.js'; const close = (actual, expected, epsilon = 1e-9) => assert.ok(Math.abs(actual - expected) <= epsilon, `${actual} != ${expected}`); const TICK = 1 / 60; function exhibit(visuals, extra = {}) { return { xzbt: '0.1', meta: { id: 'procedural-study', name: 'Procedural Study' }, runtime: { seed: 42 }, visuals, ...extra }; } const codes = (document) => validateExhibit(document).errors.map((error) => error.code); const scene = { coordinateSpace: 'viewport' }; function engineFor(document, seed = 42) { return new VisualEngine(document, { resolution: new ResolutionEngine(document, new SeededRNG(seed)), rng: new SeededRNG(seed) }); } /** A bare system, for the motion traces that want a closed-form oracle. */ function system(spec, { seed = 42, fields = null, scene: box = { x: 0, y: 0, width: 100, height: 100 } } = {}) { const document = exhibit({ scene, systems: {} }); const resolution = new ResolutionEngine(document, new SeededRNG(seed)); return new ProceduralSystem('s', spec, { systemPath: 'visuals.systems.s', resolver: resolution.valueResolver, rng: new SeededRNG(seed), noiseTable: permutationTable(new SeededRNG(seed).stream('visual', 'behavior-noise')), scene: box, fields, instantiateItemNode: () => null }); } // --------------------------------------------------------------------------- // Traces 1 to 3 — components (18.1) // --------------------------------------------------------------------------- const panel = { parameters: { tint: { type: 'color', default: '#3a6ea5' }, lit: { type: 'boolean', default: false }, scale: { type: 'number', default: 1, min: 0, max: 4 } }, content: { frame: { type: 'rectangle', size: { width: 24, height: 16 }, style: { fill: { ref: 'inputs.tint' } } } } }; test('18.10.1 a component instantiates with defaults, with inputs, and nested', () => { const document = exhibit({ scene, systems: { s: { type: 'graphic', content: { a: { type: 'component', component: 'panel' }, b: { type: 'component', component: 'panel', inputs: { tint: '#ff0000' } } } } } }, { components: { visual: { panel } } }); assert.deepEqual(validateExhibit(document).errors, []); const plan = engineFor(document).planFrame({ width: 200, height: 200 }); const [a, b] = plan.layers[0].nodes; assert.equal(a.children[0].fill.color, '#3a6ea5', 'the declared default'); assert.equal(b.children[0].fill.color, '#ff0000', 'the supplied input'); // An undeclared input key, and inputs.* used outside a component. const undeclared = exhibit({ scene, systems: { s: { type: 'graphic', content: { a: { type: 'component', component: 'panel', inputs: { hue: 1 } } } } } }, { components: { visual: { panel } } }); assert.throws(() => engineFor(undeclared).planFrame({ width: 10, height: 10 }), (error) => error.code === 'ERR_INVALID_REFERENCE'); const outside = exhibit({ scene, systems: { s: { type: 'graphic', content: { a: { type: 'rectangle', size: { width: { ref: 'inputs.tint' }, height: 1 } } } } } }); assert.throws(() => engineFor(outside), (error) => error.code === 'ERR_INVALID_REFERENCE'); // A supplied value of the wrong type, and one outside a declared range. const wrongType = exhibit({ scene, systems: { s: { type: 'graphic', content: { a: { type: 'component', component: 'panel', inputs: { scale: 'big' } } } } } }, { components: { visual: { panel } } }); assert.throws(() => engineFor(wrongType), (error) => error.code === 'ERR_TYPE_MISMATCH'); const outOfRange = exhibit({ scene, systems: { s: { type: 'graphic', content: { a: { type: 'component', component: 'panel', inputs: { scale: 9 } } } } } }, { components: { visual: { panel } } }); assert.throws(() => engineFor(outOfRange), (error) => error.code === 'ERR_OUT_OF_BOUNDS'); }); test('18.10.2 component recursion and nesting are bounded at eight levels', () => { const selfReferential = { visual: { loop: { content: { inner: { type: 'component', component: 'loop' } } } } }; const document = exhibit({ scene, systems: { s: { type: 'graphic', content: { a: { type: 'component', component: 'loop' } } } } }, { components: selfReferential }); assert.throws(() => engineFor(document), (error) => error.code === 'ERR_COMPONENT_RECURSION'); const chain = (depth) => { const components = {}; for (let level = 0; level < depth; level += 1) { components[`c${level}`] = level === depth - 1 ? { content: { leaf: { type: 'point' } } } : { content: { next: { type: 'component', component: `c${level + 1}` } } }; } return components; }; const deep = exhibit({ scene, systems: { s: { type: 'graphic', content: { a: { type: 'component', component: 'c0' } } } } }, { components: { visual: chain(10) } }); assert.throws(() => engineFor(deep), (error) => error.code === 'ERR_COMPONENT_RECURSION'); const legal = exhibit({ scene, systems: { s: { type: 'graphic', content: { a: { type: 'component', component: 'c0' } } } } }, { components: { visual: chain(6) } }); assert.doesNotThrow(() => engineFor(legal).planFrame({ width: 50, height: 50 })); }); test('18.10.3 two instances sample independently from their expansion paths', () => { const jittered = { parameters: {}, content: { dot: { type: 'point', position: { x: { random: { min: 0, max: 100 } }, y: 0 } } } }; const build = (keys) => { const content = {}; for (const key of keys) content[key] = { type: 'component', component: 'jitter' }; return engineFor(exhibit({ scene, systems: { s: { type: 'graphic', content } } }, { components: { visual: { jitter: jittered } } })) .planFrame({ width: 200, height: 200 }).layers[0].nodes; }; const [first, second] = build(['a', 'b']); assert.notEqual(first.children[0].center[0], second.children[0].center[0], 'two instances sample independently'); // Adding an unrelated sibling does not perturb either instance. const withExtra = build(['a', 'b', 'zzz']); close(withExtra[0].children[0].center[0], first.children[0].center[0]); close(withExtra[1].children[0].center[0], second.children[0].center[0]); }); // --------------------------------------------------------------------------- // Traces 4 to 7 — particles (18.2) // --------------------------------------------------------------------------- test('18.10.4 particle fields resolve once per particle and hold for its life', () => { const document = exhibit({ scene, systems: { motes: { type: 'particles', capacity: 8, count: 4, lifetime: '10s', velocity: { x: { random: { min: -5, max: 5 } }, y: 0 }, render: { type: 'point' } } } }); const engine = engineFor(document); const velocities = engine.systems[0].procedural.items.map((item) => item.vx); assert.equal(new Set(velocities).size, 4, 'each particle samples its own velocity'); for (let tick = 0; tick < 120; tick += 1) engine.advance(1000 / 60); assert.deepEqual(engine.systems[0].procedural.items.map((item) => item.vx), velocities, 'and holds it for its whole life'); // Two runs of one seed create identical particles in identical order. assert.deepEqual(engineFor(document).systems[0].procedural.items.map((item) => item.vx), velocities); assert.notDeepEqual(engineFor(document, 7).systems[0].procedural.items.map((item) => item.vx), velocities); }); test('18.10.5 the integrator matches its closed form and is order-sensitive', () => { // v <- v + a*dt, then p <- p + v*dt, so after n ticks p = a*dt^2*n(n+1)/2. const acceleration = 10; const ticks = 60; const particles = system({ type: 'particles', capacity: 4, count: 1, render: { type: 'point' }, acceleration: { x: acceleration, y: 0 } }); for (let tick = 0; tick < ticks; tick += 1) particles.advance(TICK); const item = particles.items[0]; close(item.vx, acceleration * ticks * TICK, 1e-9); close(item.base.x, acceleration * TICK * TICK * ((ticks * (ticks + 1)) / 2), 1e-9); // Integrating position before velocity would leave it one step behind. assert.notEqual(item.base.x, acceleration * TICK * TICK * (((ticks - 1) * ticks) / 2)); }); test('18.10.6 drag is dt-correct: one tick of 1s equals ten of 0.1s', () => { const make = () => system({ type: 'particles', capacity: 4, count: 1, render: { type: 'point' }, velocity: { x: 100, y: 0 }, drag: 0.5 }); const coarse = make(); coarse.advance(1); const fine = make(); for (let tick = 0; tick < 10; tick += 1) fine.advance(0.1); close(coarse.items[0].vx, fine.items[0].vx, 1e-9); close(coarse.items[0].vx, 100 * 0.5, 1e-9); }); test('18.10.7 a life ramp interpolates under each curve, and needs a lifetime', () => { const ramps = system({ type: 'particles', capacity: 4, count: 1, lifetime: '10s', render: { type: 'point' }, size: { from: 0, to: 1, curve: 'smooth' } }); const item = ramps.items[0]; close(ramps.rampValue(item.ramps.size, 0), 0); close(ramps.rampValue(item.ramps.size, 0.5), 0.5); close(ramps.rampValue(item.ramps.size, 1), 1); for (const [curve, midpoint] of [['step', 0], ['linear', 0.5], ['exponential', 0.25], ['smooth', 0.5]]) { close(curveValue(curve, 0.5), midpoint, 1e-12); close(curveValue(curve, 0), 0); close(curveValue(curve, 1), 1); } assert.throws(() => system({ type: 'particles', capacity: 4, count: 1, render: { type: 'point' }, size: { from: 0, to: 1 } }), (error) => error instanceof RuntimeFault && error.code === 'ERR_SCHEMA_VALIDATION'); }); // --------------------------------------------------------------------------- // Traces 8 and 9 — emission (18.2, 18.4) // --------------------------------------------------------------------------- test('18.10.8 rate emission is exactly floor(rate * t) under three tick lengths', () => { // Steps that are exact in binary, so the oracle is floor(rate * t) and not a // statement about floating-point summation. for (const [step, ticks] of [[1 / 64, 64], [1 / 32, 32], [1 / 16, 16]]) { const emitter = system({ type: 'emitter', capacity: 512, rate: 6, lifetime: '60s', emit: { type: 'point' } }); for (let tick = 0; tick < ticks; tick += 1) emitter.advance(step); assert.equal(emitter.created, 6, `at a step of ${step}`); // Half a second in, the count is exactly floor(rate * t) too. const half = system({ type: 'emitter', capacity: 512, rate: 6, lifetime: '60s', emit: { type: 'point' } }); for (let tick = 0; tick < ticks / 2; tick += 1) half.advance(step); assert.equal(half.created, 3); } // A burst emits its whole count on the first tick at or after its `at`. const bursting = system({ type: 'emitter', capacity: 512, burst: [{ at: '500ms', count: 24 }], lifetime: '60s', emit: { type: 'point' } }); bursting.advance(0.4); assert.equal(bursting.created, 0, 'not before its offset'); bursting.advance(0.2); assert.equal(bursting.created, 24, 'the whole count on the first tick at or after it'); bursting.advance(1); assert.equal(bursting.created, 24, 'and never again'); }); test('18.10.9 unbounded emission is rejected, count alone passes, capacity evicts oldest', () => { assert.throws(() => system({ type: 'emitter', capacity: 8, rate: 4, emit: { type: 'point' } }), (error) => error.code === 'ERR_UNBOUNDED_EMISSION'); assert.doesNotThrow(() => system({ type: 'particles', capacity: 8, count: 8, render: { type: 'point' } })); const full = system({ type: 'particles', capacity: 4, rate: 60, lifetime: '60s', render: { type: 'point' } }); for (let tick = 0; tick < 60; tick += 1) full.advance(TICK); assert.equal(full.items.length, 4, 'the live count stays at capacity'); assert.equal(full.items[0].ordinal, full.created - 4, 'and the oldest were evicted'); }); // --------------------------------------------------------------------------- // Trace 10 — distributions (18.3) // --------------------------------------------------------------------------- test('18.10.10 each distribution places at its documented position and draw count', () => { const grid = { type: 'grid', origin: { x: 0, y: 0 }, columns: 12, rows: 8, spacing: { x: 10, y: 10 } }; assert.deepEqual([0, 1, 12, 13].map((index) => placeItem(grid, { index, count: 96 }).x), [0, 10, 0, 10]); assert.deepEqual([0, 1, 12, 13].map((index) => placeItem(grid, { index, count: 96 }).y), [0, 0, 10, 10]); // A count larger than rows * columns wraps rather than failing. assert.equal(placeItem(grid, { index: 96, count: 200 }).x, 0); // n == 1 places at fraction 0, matching repeat.fraction. assert.equal(placeItem({ type: 'line', from: { x: 0, y: 0 }, to: { x: 10, y: 0 }, mode: 'even' }, { index: 0, count: 1 }).x, 0); // The three depth curves are monotonic and bias toward near. const depth = { type: 'depth', near: 0, far: 100 }; for (const curve of ['uniform', 'linear', 'exponential']) { const stream = new SeededRNG(42).stream('visual', `d-${curve}`); const value = placeItem({ ...depth, curve }, { stream }).z; assert.ok(value >= 0 && value <= 100, curve); } // The draw-count table of 18.3, asserted by counting the draws themselves. const drawn = (distribution) => { const source = new SeededRNG(42).stream('visual', 'draws'); let draws = 0; const counting = { nextFloat: () => { draws += 1; return source.nextFloat(); }, nextInteger: (a, b) => source.nextInteger(a, b) }; placeItem(distribution, { index: 0, count: 4, stream: counting }); return draws; }; assert.equal(sampleCount({ type: 'point' }), 0); assert.equal(sampleCount({ type: 'ellipse' }), 2); assert.equal(sampleCount({ type: 'ellipse', fill: 'perimeter' }), 1, 'one distance sample, not two'); assert.equal(sampleCount({ type: 'rectangle', fill: 'perimeter' }), 1); assert.equal(sampleCount({ type: 'grid' }), 0); assert.equal(sampleCount({ type: 'grid', jitter: { x: 1, y: 1 } }), 2); assert.equal(sampleCount({ type: 'ring', mode: 'even' }), 0); assert.equal(sampleCount({ type: 'ring', mode: 'even', depth: { near: 0, far: 1 } }), 1, 'the depth sub-block draws last'); for (const distribution of [ { type: 'point' }, { type: 'ellipse', center: { x: 0, y: 0 }, radius: 4 }, { type: 'ellipse', center: { x: 0, y: 0 }, radius: 4, fill: 'perimeter' }, { type: 'rectangle', center: { x: 0, y: 0 }, size: { width: 4, height: 2 }, fill: 'perimeter' }, { type: 'rectangle', center: { x: 0, y: 0 }, size: { width: 4, height: 2 } }, { type: 'uniform', min: { x: 0, y: 0 }, max: { x: 1, y: 1 } }, { type: 'uniform', min: { x: 0, y: 0, z: 0 }, max: { x: 1, y: 1, z: 1 } }, { type: 'ring', radius: 4, innerRadius: 1 }, { type: 'ring', radius: 4, innerRadius: 1, mode: 'even' }, { type: 'grid', columns: 2, rows: 2, spacing: { x: 1, y: 1 } }, { type: 'grid', columns: 2, rows: 2, spacing: { x: 1, y: 1 }, jitter: { x: 1, y: 1 } }, { type: 'depth', near: 0, far: 10 }, { type: 'ellipse', center: { x: 0, y: 0 }, radius: 4, depth: { near: 0, far: 10 } } ]) { assert.equal(drawn(distribution), sampleCount(distribution), JSON.stringify(distribution)); } // An index-driven placement on a continuous rate emission has no count. assert.throws(() => system({ type: 'particles', capacity: 8, rate: 4, lifetime: '2s', render: { type: 'point' }, distribution: { type: 'grid', columns: 2, rows: 2 } }), (error) => error.code === 'ERR_INVALID_DISTRIBUTION'); assert.throws(() => placeItem({ type: 'spiral' }, {}), (error) => error.code === 'ERR_INVALID_DISTRIBUTION_TYPE'); }); // --------------------------------------------------------------------------- // Trace 11 — repeaters (18.5) // --------------------------------------------------------------------------- test('18.10.11 a repeater resolves repeat.index, repeat.count, and repeat.fraction', () => { const document = exhibit({ scene, systems: { wall: { type: 'repeater', count: 4, repeat: { type: 'rectangle', size: { width: { ref: 'repeat.index' }, height: { ref: 'repeat.fraction' } }, position: { x: 0, y: 0 } }, distribution: { type: 'line', from: { x: 0, y: 0 }, to: { x: 30, y: 0 }, mode: 'even' } } } }); const engine = engineFor(document); const items = engine.systems[0].procedural.items; assert.equal(items.length, 4); assert.deepEqual(items.map((item) => item.node.raw.size.width), [0, 1, 2, 3]); assert.deepEqual(items.map((item) => item.node.raw.size.height), [0, 1 / 3, 2 / 3, 1]); assert.deepEqual(items.map((item) => Math.round(item.base.x)), [0, 10, 20, 30], 'even placement consumes no samples'); // repeat.* outside a repeater does not resolve. const outside = exhibit({ scene, systems: { s: { type: 'graphic', content: { a: { type: 'rectangle', size: { width: { ref: 'repeat.index' }, height: 1 } } } } } }); assert.throws(() => engineFor(outside), (error) => error.code === 'ERR_INVALID_REFERENCE'); }); // --------------------------------------------------------------------------- // Traces 12 to 14 — behaviors (18.6) // --------------------------------------------------------------------------- test('18.10.12 every behavior advances a known object, and the rejections differ', () => { const target = { type: 'polyline', points: [{ x: 0, y: 0 }, { x: 1, y: 1 }] }; const behaviors = [ { type: 'drift', velocity: { x: 10, y: 0 } }, { type: 'rotate', speed: 90 }, { type: 'oscillate', property: 'position.y', amplitude: 5, frequency: 1 }, { type: 'orbit', center: { x: 0, y: 0 }, radius: 10, speed: 90 }, { type: 'wander', strength: 5, rate: 1 }, { type: 'follow-path', path: [{ op: 'move', to: { x: 0, y: 0 } }, { op: 'line', to: { x: 10, y: 0 } }], speed: 5 }, { type: 'point-wander', amplitude: { x: 1, y: 1 }, rate: 1 }, { type: 'pulse', property: 'style.opacity', amplitude: 0.5, frequency: 2 }, { type: 'twinkle', min: 0.2, max: 1, rate: 3 }, { type: 'noise-displace', amplitude: { x: 2, y: 2 }, scale: 50, speed: 0.1 }, { type: 'face-motion' }, { type: 'wrap' }, { type: 'bounce', restitution: 0.5 }, { type: 'attract', target: { x: 50, y: 50 }, strength: 20 }, { type: 'repel', target: { x: 50, y: 50 }, strength: 20 }, { type: 'field-follow', field: 'wind', strength: 1 }, { type: 'morph', to: 'other', duration: '1s' } ]; const fields = new FieldSet({ wind: { type: 'directional', direction: 0, strength: 5 } }, { rng: new SeededRNG(42) }); for (const behavior of behaviors) { const owner = system({ type: 'particles', capacity: 4, count: 1, lifetime: '10s', render: target, behaviors: [behavior] }, { fields }); const item = owner.items[0]; item.points = [{ x: 0, y: 0, z: 0 }, { x: 1, y: 1, z: 0 }]; owner.advance(TICK, { resolveTarget: () => ({ x: 50, y: 50 }), resolveMorphTarget: () => [{ x: 5, y: 5, z: 0 }, { x: 6, y: 6, z: 0 }] }); const place = owner.itemPosition(item); assert.ok(Number.isFinite(place.x) && Number.isFinite(place.y) && Number.isFinite(place.rotation), behavior.type); } // Documented single-behavior oracles. const drifting = system({ type: 'particles', capacity: 4, count: 1, render: { type: 'point' }, behaviors: [{ type: 'drift', velocity: { x: 60, y: 0 } }] }); for (let tick = 0; tick < 60; tick += 1) drifting.advance(TICK); close(drifting.itemPosition(drifting.items[0]).x, 60, 1e-9); const spinning = system({ type: 'particles', capacity: 4, count: 1, render: { type: 'point' }, behaviors: [{ type: 'rotate', speed: 90 }] }); for (let tick = 0; tick < 60; tick += 1) spinning.advance(TICK); close(spinning.itemPosition(spinning.items[0]).rotation, 90, 1e-9); // An orbit is recomputed from t, so it traces its circle rather than spiralling. const orbiting = system({ type: 'particles', capacity: 4, count: 1, render: { type: 'point' }, behaviors: [{ type: 'orbit', center: { x: 0, y: 0 }, radius: 10, speed: 360 }] }); for (let tick = 0; tick < 60; tick += 1) orbiting.advance(TICK); const orbited = orbiting.itemPosition(orbiting.items[0]); close(Math.hypot(orbited.x, orbited.y), 10, 1e-9); // Rejections. const owner = (object, extra = {}) => exhibit({ scene, systems: { s: { type: 'graphic', content: { a: { ...object, ...extra } } } } }); assert.ok(codes(owner({ type: 'point' }, { behaviors: [{ type: 'levitate' }] })).includes('ERR_INVALID_BEHAVIOR_TYPE')); assert.ok(codes(owner({ type: 'point' }, { behaviors: [{ type: 'oscillate', property: 'style.hue', amplitude: 1 }] })).includes('ERR_INVALID_BEHAVIOR_TARGET')); assert.ok(codes(owner({ type: 'ellipse', radius: 2 }, { behaviors: [{ type: 'oscillate', property: 'size.width', amplitude: 1 }] })).includes('ERR_INVALID_BEHAVIOR_TARGET')); assert.ok(codes(owner({ type: 'rectangle', size: { width: 1, height: 1 } }, { behaviors: [{ type: 'point-wander', amplitude: { x: 1, y: 1 } }] })).includes('ERR_INVALID_BEHAVIOR_TARGET')); const nine = Array.from({ length: 9 }, () => ({ type: 'rotate', speed: 1 })); assert.ok(codes(owner({ type: 'point' }, { behaviors: nine })).includes('ERR_VISUAL_LIMIT_EXCEEDED')); assert.ok(codes(owner({ type: 'point' }, { behaviors: [{ type: 'field-follow', field: 'nope' }] })).includes('ERR_INVALID_REFERENCE')); assert.ok(codes(owner({ type: 'point' }, { behaviors: [{ type: 'follow-path', path: [], speed: 1, duration: '1s' }] })).includes('ERR_SCHEMA_VALIDATION')); }); test('18.10.13 behavior composition follows array order and the documented rule', () => { const two = system({ type: 'particles', capacity: 4, count: 1, render: { type: 'point' }, behaviors: [{ type: 'drift', velocity: { x: 60, y: 0 } }, { type: 'drift', velocity: { x: 30, y: 0 } }] }); for (let tick = 0; tick < 60; tick += 1) two.advance(TICK); // Position accumulates: 60 + 30 over one logical second. close(two.itemPosition(two.items[0]).x, 90, 1e-9); // Scale multiplies rather than accumulating. const scaled = system({ type: 'particles', capacity: 4, count: 1, render: { type: 'point' }, behaviors: [ { type: 'oscillate', property: 'transform.scale.x', amplitude: 0, center: 2, frequency: 0 }, { type: 'oscillate', property: 'transform.scale.x', amplitude: 0, center: 3, frequency: 0 } ] }); scaled.advance(TICK); close(scaled.items[0].scale.x, 6, 1e-12); assert.deepEqual(behaviorChannels({ type: 'attract' }), ['velocity.x', 'velocity.y', 'velocity.z'], 'a force behavior writes velocity, which no automation track can address'); }); test('18.10.14 morph is restricted to point-list geometry and rejects mismatches', () => { const pair = (a, b) => exhibit({ scene, systems: { s: { type: 'graphic', content: { source: { ...a, behaviors: [{ type: 'morph', to: 'target', duration: '1s' }] }, target: b } } } }); const line3 = { type: 'polyline', points: [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 2, y: 0 }] }; assert.deepEqual(codes(pair(line3, { type: 'polyline', points: [{ x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }] })), []); assert.ok(codes(pair(line3, { type: 'polyline', points: [{ x: 0, y: 1 }, { x: 1, y: 1 }] })).includes('ERR_MORPH_INCOMPATIBLE')); assert.ok(codes(pair(line3, { type: 'rectangle', size: { width: 1, height: 1 } })).includes('ERR_MORPH_INCOMPATIBLE')); assert.ok(codes(pair({ type: 'spline', mode: 'linear', points: [{ x: 0, y: 0 }, { x: 1, y: 0 }] }, { type: 'spline', mode: 'catmull-rom', points: [{ x: 0, y: 1 }, { x: 1, y: 1 }] })).includes('ERR_MORPH_INCOMPATIBLE')); const missing = exhibit({ scene, systems: { s: { type: 'graphic', content: { source: { ...line3, behaviors: [{ type: 'morph', to: 'nowhere', duration: '1s' }] } } } } }); assert.ok(codes(missing).includes('ERR_INVALID_REFERENCE')); }); // --------------------------------------------------------------------------- // Traces 15 and 16 — coherent noise (18.7) // --------------------------------------------------------------------------- test('18.10.15 the noise function reproduces its oracles and consumes 255 samples', () => { const before = new SeededRNG(42).stream('visual', 'current'); const counting = new SeededRNG(42).stream('visual', 'current'); const table = permutationTable(counting); // The shuffle draws one sample per swap, not one per entry. for (let draw = 0; draw < PERMUTATION_DRAWS; draw += 1) before.nextFloat(); close(before.nextFloat(), counting.nextFloat(), 0); assert.equal(PERMUTATION_DRAWS, 255); // A gradient noise value is exactly zero on the lattice. close(octaveNoise(table, 0, 0, 0, 1, 0.5), 0, 0); close(octaveNoise(table, 3, -2, 5, 1, 0.5), 0, 0); // Two runs of one seed agree exactly, at lattice and non-lattice positions. const again = permutationTable(new SeededRNG(42).stream('visual', 'current')); for (const [x, y, z] of [[0.5, 0.25, 0], [13.37, -7.5, 2.25], [0, 0, 0]]) { close(octaveNoise(table, x, y, z, 3, 0.5), octaveNoise(again, x, y, z, 3, 0.5), 0); } // A different seed gives a different table. const other = permutationTable(new SeededRNG(7).stream('visual', 'current')); assert.notEqual(octaveNoise(table, 0.5, 0.25, 0, 1, 0.5), octaveNoise(other, 0.5, 0.25, 0, 1, 0.5)); // persistence: 0 stays defined, contributing one octave. close(octaveNoise(table, 0.5, 0.25, 0, 4, 0), octaveNoise(table, 0.5, 0.25, 0, 1, 0), 0); const field = (extra) => exhibit({ scene, fields: { n: { type: 'noise', scale: 100, ...extra } } }); assert.ok(codes(field({ octaves: 5 })).includes('ERR_OUT_OF_BOUNDS')); assert.ok(codes(field({ persistence: 1.5 })).includes('ERR_OUT_OF_BOUNDS')); assert.ok(codes(exhibit({ scene, fields: { n: { type: 'noise', scale: 0 } } })).includes('ERR_OUT_OF_BOUNDS')); assert.ok(codes(exhibit({ scene, fields: { n: { type: 'swirl' } } })).includes('ERR_INVALID_FIELD_TYPE')); const nine = Object.fromEntries(Array.from({ length: 9 }, (_, index) => [`f${index}`, { type: 'directional', direction: 0, strength: 1 }])); assert.ok(codes(exhibit({ scene, fields: nine })).includes('ERR_VISUAL_LIMIT_EXCEEDED')); }); test('18.10.16 curl is divergence-free within tolerance at non-lattice positions', () => { const table = permutationTable(new SeededRNG(42).stream('visual', 'current')); const amplitude = 1; const step = 1e-3; let worst = 0; for (let i = 0; i < 12; i += 1) { for (let j = 0; j < 12; j += 1) { const x = i * 0.37 + 0.13; const y = j * 0.41 + 0.07; const dx = noiseVector(table, x + step, y, 0, { mode: 'curl', amplitude })[0] - noiseVector(table, x - step, y, 0, { mode: 'curl', amplitude })[0]; const dy = noiseVector(table, x, y + step, 0, { mode: 'curl', amplitude })[1] - noiseVector(table, x, y - step, 0, { mode: 'curl', amplitude })[1]; worst = Math.max(worst, Math.abs((dx + dy) / (2 * step))); } } assert.ok(worst < 1e-4, `divergence ${worst} exceeds the documented tolerance`); // `value` mode points along its declared direction. const [vx, vy] = noiseVector(table, 0.3, 0.7, 0, { mode: 'value', direction: 90, amplitude: 1 }); assert.ok(Math.abs(vx) < 1e-12 && Math.abs(vy) > 0, 'the scalar rides the declared direction'); }); // --------------------------------------------------------------------------- // Traces 17 to 19 — trails, links, and the target surface (18.8, 8.1) // --------------------------------------------------------------------------- test('18.10.17 trail history is sampled on the logical clock and is bounded', () => { const build = () => system({ type: 'particles', capacity: 4, count: 1, lifetime: '60s', render: { type: 'point' }, velocity: { x: 60, y: 0 }, trail: { length: 8 } }); const fast = build(); for (let tick = 0; tick < 30; tick += 1) fast.advance(TICK); const geometry = fast.items[0].trail.map((sample) => Math.round(sample.x * 1e6) / 1e6); assert.equal(geometry.length, 8, 'the history is capped at its declared length'); // The same logical span gives the same geometry however many frames drew it. const slow = build(); for (let tick = 0; tick < 30; tick += 1) slow.advance(TICK); assert.deepEqual(slow.items[0].trail.map((sample) => Math.round(sample.x * 1e6) / 1e6), geometry); assert.throws(() => { const over = system({ type: 'particles', capacity: 4, count: 1, lifetime: '60s', render: { type: 'point' }, trail: { length: 129 } }); over.advance(TICK); }, (error) => error.code === 'ERR_VISUAL_LIMIT_EXCEEDED'); // A removed item's history goes with it. const expiring = system({ type: 'particles', capacity: 4, count: 1, lifetime: '100ms', render: { type: 'point' }, trail: { length: 8 } }); for (let tick = 0; tick < 12; tick += 1) expiring.advance(TICK); assert.equal(expiring.items.length, 0); }); test('18.10.18 link enumeration is deterministic, de-duplicated, and bounded', () => { const linked = system({ type: 'repeater', count: 4, repeat: { type: 'point' }, distribution: { type: 'line', from: { x: 0, y: 0 }, to: { x: 30, y: 0 }, mode: 'even' }, links: { rule: 'distance', maxDistance: 15 } }); const pairs = linked.linkPairs(); // Items sit at 0, 10, 20, 30: adjacent pairs only, each once. assert.equal(pairs.length, 3); assert.deepEqual(pairs.map((pair) => Math.round(pair.distance)), [10, 10, 10]); assert.deepEqual(linked.linkPairs().map((pair) => Math.round(pair.from.x)), pairs.map((pair) => Math.round(pair.from.x))); const indexed = system({ type: 'repeater', count: 4, repeat: { type: 'point' }, links: { rule: 'index', stride: 1, closed: true } }); assert.equal(indexed.linkPairs().length, 4, 'three strides plus the closing link'); const capped = system({ type: 'repeater', count: 4, repeat: { type: 'point' }, distribution: { type: 'line', from: { x: 0, y: 0 }, to: { x: 3, y: 0 }, mode: 'even' }, links: { rule: 'distance', maxDistance: 100, maxLinks: 2 } }); assert.equal(capped.linkPairs().length, 2, 'the tail of the ascending order is dropped'); assert.ok(codes(exhibit({ scene, systems: { e: { type: 'emitter', capacity: 8, rate: 1, lifetime: '1s', emit: { type: 'point' }, links: { rule: 'index' } } } })).includes('ERR_UNKNOWN_FIELD')); assert.throws(() => system({ type: 'particles', capacity: 400, count: 1, render: { type: 'point' }, links: { rule: 'distance', maxDistance: 10 } }), (error) => error.code === 'ERR_VISUAL_LIMIT_EXCEEDED'); assert.throws(() => system({ type: 'particles', capacity: 8, count: 1, render: { type: 'point' }, links: { rule: 'distance', fadeWithDistance: true } }), (error) => error.code === 'ERR_SCHEMA_VALIDATION'); }); test('18.10.19 nothing introduced by this section becomes an external target', () => { const document = exhibit({ scene, fields: { wind: { type: 'directional', direction: 0, strength: 1 } }, systems: { motes: { type: 'particles', capacity: 8, count: 1, render: { type: 'point' }, behaviors: [{ type: 'rotate', speed: 1 }] } } }, { state: { energy: { type: 'number', initial: 0.5 } } }); for (const target of ['visuals.fields.wind.strength', 'visuals.systems.motes.rate', 'visuals.systems.motes.behaviors[0].speed', 'visuals.systems.motes.capacity']) { const bound = { ...document, bindings: [{ source: 'state.energy', target }] }; assert.ok(codes(bound).includes('ERR_UNSUPPORTED_TARGET'), target); } // The one system-level property that is a target stays one. const visible = { ...document, bindings: [{ source: 'state.energy', target: 'visuals.systems.motes.visible' }] }; assert.ok(!codes(visible).includes('ERR_UNSUPPORTED_TARGET')); });