/** * A field shader (#170) is laid over a source's whole field at once: the particles are drawn into a * picture of a fixed frame around the source, and the shader draws that picture. These tests pin * the rows it brings and the arithmetic of the frame; what it looks like is e2e 31. */ import { describe, expect, it } from 'vitest'; import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands'; import { AddNodeCommand } from '../../src/core/commands/graphCommands'; import { apply, transformMatrix } from '../../src/core/model/matrix'; import { findAssetOf } from '../../src/core/model/Project'; import type { SceneObject } from '../../src/core/model/SceneObject'; import { FIELD_HEIGHT_KEY, FIELD_SHADER_KEY, FIELD_WIDTH_KEY, fieldSizeOf, isValidParticlePropertyName, } from '../../src/core/properties/particles'; import { isRowShown, schemaFor, schemaForAsset, schemaForObject, } from '../../src/core/properties/PropertySchema'; import { SHADER_KEY, SHADER_PROPERTY } from '../../src/core/properties/shaders'; import { FIELD_MAX_PIXELS, fieldFrame } from '../../src/viewport/fieldFrame'; import { makeProject, roundTrip } from '../helpers'; function source() { const f = makeProject(); const create = new CreateAssetCommand('particles', 'Sparks'); f.stack.execute(create); const id = create.asset.id; const asset = () => findAssetOf(f.project, id, 'particles')!; const shader = (name: string, uniform: string, initial = 3) => { const made = new CreateAssetCommand('shader', name); f.stack.execute(made); f.stack.execute( new AddNodeCommand(made.asset.id, SHADER_PROPERTY, 0, 0, { name: uniform, default: initial, min: 0, max: 10, }), ); return made.asset.id; }; const set = (key: string, value: string | number) => f.stack.execute(new SetAssetPropertyCommand(id, key, value)); const rows = () => schemaForAsset(f.project, asset()); const keys = () => rows().map((d) => d.key); return { ...f, id, asset, shader, set, rows, keys }; } describe('the field shader rows', () => { it('sit under the particles’ shader, and the frame shows only once one is picked', () => { const own = schemaFor('particles').map((d) => d.key); expect(own.slice(own.indexOf(SHADER_KEY))).toEqual([ SHADER_KEY, FIELD_SHADER_KEY, FIELD_WIDTH_KEY, FIELD_HEIGHT_KEY, ]); expect(schemaFor('particles').find((d) => d.key === FIELD_SHADER_KEY)).toMatchObject({ label: 'Field shader', kind: 'asset', assetTypes: ['shader'], default: '', }); const width = schemaFor('particles').find((d) => d.key === FIELD_WIDTH_KEY)!; expect(width).toMatchObject({ label: 'Field width', default: 256 }); expect(isRowShown(width, { [FIELD_SHADER_KEY]: '' })).toBe(false); expect(isRowShown(width, { [FIELD_SHADER_KEY]: 'glow' })).toBe(true); }); it('add the field shader’s properties, and a name both shaders declare is one row', () => { const f = source(); const each = f.shader('Each', 'level'); const field = f.shader('Field', 'glow'); f.set(FIELD_SHADER_KEY, field); expect(f.keys().at(-1)).toBe('glow'); f.set(SHADER_KEY, each); expect(f.keys().slice(-2)).toEqual(['level', 'glow']); const shared = f.shader('Also level', 'level', 7); f.set(FIELD_SHADER_KEY, shared); expect(f.keys().filter((k) => k === 'level')).toEqual(['level']); // The first shader's row it is: both uniforms read the one value the source holds. expect(f.rows().find((d) => d.key === 'level')).toMatchObject({ default: 3 }); }); it('follow an object that picks a field shader of its own', () => { const f = source(); const field = f.shader('Field', 'glow'); const object = { overrides: { [FIELD_SHADER_KEY]: field } } as unknown as SceneObject; expect(schemaForObject(f.project, f.asset(), object).map((d) => d.key)).toContain('glow'); expect(f.keys()).not.toContain('glow'); }); it('carry the shader, the frame and the values through a save', () => { const f = source(); const field = f.shader('Field', 'glow'); f.set(FIELD_SHADER_KEY, field); f.set(FIELD_WIDTH_KEY, 400); f.set('glow', 5); const loaded = findAssetOf(roundTrip(f.project), f.id, 'particles')!; expect(loaded.props[FIELD_SHADER_KEY]).toBe(field); expect(fieldSizeOf(loaded.props)).toEqual({ w: 400, h: 256 }); expect(loaded.props.glow).toBe(5); }); it('are names a particle graph cannot take', () => { for (const key of [FIELD_SHADER_KEY, FIELD_WIDTH_KEY, FIELD_HEIGHT_KEY]) expect(isValidParticlePropertyName(key)).toBe(false); }); }); describe('the frame', () => { it('has as many pixels as it covers on screen, its corner at the picture’s origin', () => { const m = transformMatrix(100, 50, 0, 1, false, false); const frame = fieldFrame(m, 256, 128, 2); expect(frame.pixels).toEqual({ w: 512, h: 256 }); expect(apply(frame.toPicture, { x: 100 - 128, y: 50 - 64 })).toEqual({ x: 0, y: 0 }); expect(apply(frame.toPicture, { x: 100 + 128, y: 50 + 64 })).toEqual({ x: 512, y: 256 }); expect(frame.extent).toEqual({ left: -28, top: -14, right: 228, bottom: 114 }); }); it('turns and grows with its source', () => { const m = transformMatrix(0, 0, 90, 2, false, false); const frame = fieldFrame(m, 100, 50, 1); expect(frame.pixels).toEqual({ w: 200, h: 100 }); // The source's own +x points down the world now: the frame's right edge lies at world y 100. const p = apply(frame.toPicture, { x: 0, y: 100 }); expect(p.x).toBeCloseTo(200); expect(p.y).toBeCloseTo(50); expect(frame.extent.bottom).toBeCloseTo(100); expect(frame.extent.right).toBeCloseTo(50); }); it('keeps between one pixel and the cap, whatever the zoom', () => { const m = transformMatrix(0, 0, 0, 1, false, false); expect(fieldFrame(m, 256, 256, 40).pixels).toEqual({ w: FIELD_MAX_PIXELS, h: FIELD_MAX_PIXELS }); expect(fieldFrame(m, 256, 256, 0.0001).pixels).toEqual({ w: 1, h: 1 }); // The picture is squeezed into the cap, so the frame still spans all of it. const capped = fieldFrame(m, 256, 256, 40); expect(apply(capped.toPicture, { x: 128, y: 128 })).toEqual({ x: FIELD_MAX_PIXELS, y: FIELD_MAX_PIXELS }); }); it('counts a pixel of its picture as a unit of the frame, at any zoom and under the cap', () => { const m = transformMatrix(0, 0, 0, 1, false, false); expect(fieldFrame(m, 256, 128, 2).unitsPerPixel).toEqual({ x: 0.5, y: 0.5 }); expect(fieldFrame(m, 256, 128, 0.5).unitsPerPixel).toEqual({ x: 2, y: 2 }); expect(fieldFrame(m, 256, 256, 40).unitsPerPixel).toEqual({ x: 0.125, y: 0.125 }); // A source grown twice has twice the pixels for the same frame: half a unit each. expect(fieldFrame(transformMatrix(0, 0, 0, 2, false, false), 100, 50, 1).unitsPerPixel).toEqual({ x: 0.5, y: 0.5, }); }); });