diff --git a/packages/animations/src/catalog.ts b/packages/animations/src/catalog.ts index 354afbd..930d096 100644 --- a/packages/animations/src/catalog.ts +++ b/packages/animations/src/catalog.ts @@ -158,6 +158,7 @@ export function layoutFilters(): LayoutFilter[] { { id: 'all', label: 'All', layout: null }, { id: 'grid-7x7', label: '7×7', layout: presets['grid-7x7']() }, { id: 'grace-cathedral', label: 'Grace', layout: presets['grace-cathedral']() }, + { id: 'grace-28', label: 'Grace 28', layout: presets['grace-28']() }, { id: 'nova', label: 'Nova', layout: presets.nova() } ]; } diff --git a/packages/cli/README.md b/packages/cli/README.md index 89900ee..4820880 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -157,7 +157,7 @@ via walk-up search (`wavegrid.json`, `.wavegridrc`, `package.json` keys, } ``` -Built-in presets: `grid-7x7`, `grid-7x2`, `ring-6`, `nova`, `grace-cathedral`, `ring-25-filled`, `ring-25-hollow`, `disc-25`. +Built-in presets: `grid-7x7`, `grid-7x2`, `ring-6`, `nova`, `grace-cathedral`, `grace-28`, `ring-25-filled`, `ring-25-hollow`, `disc-25`. `layout` also takes shorthand for a custom shape, so a project can map its own rig without editing JSON: diff --git a/packages/layout/__tests__/config.test.ts b/packages/layout/__tests__/config.test.ts index 8a27654..29e1c40 100644 --- a/packages/layout/__tests__/config.test.ts +++ b/packages/layout/__tests__/config.test.ts @@ -35,6 +35,24 @@ describe('resolveLayout', () => { expect(grace.fixtures[0].ring).toBe(2); }); + it('resolves grace-28 as grace-cathedral with a ring of four in place of the centre', () => { + const grace = resolveLayout({ preset: 'grace-cathedral' }); + const g28 = resolveLayout({ preset: 'grace-28' }); + expect(g28.count).toBe(28); + expect(g28.topology).toBe('rings'); + + // 1–24 keep the venue's positions and numbering. + for (let i = 0; i < 24; i++) { + expect(g28.fixtures[i].x).toBeCloseTo(grace.fixtures[i].x, 6); + expect(g28.fixtures[i].y).toBeCloseTo(grace.fixtures[i].y, 6); + } + + const centre = g28.fixtures.slice(24); + expect(centre).toHaveLength(4); + expect(centre.every(f => +f.radius.toFixed(3) === 0.25)).toBe(true); + expect(centre.every(f => f.ring === 0)).toBe(true); + }); + it('staggers grace-cathedral’s inner ring half a step off the outer one', () => { const grace = resolveLayout({ preset: 'grace-cathedral' }); const posOf = (f: { angle: number }) => diff --git a/packages/layout/src/presets.ts b/packages/layout/src/presets.ts index 1e9b362..d560948 100644 --- a/packages/layout/src/presets.ts +++ b/packages/layout/src/presets.ts @@ -32,6 +32,20 @@ export const presets: Record Layout> = { id: 'grace-cathedral', name: 'Grace Cathedral (12 + 12 + centre)' }), + /** + * Grace with three extra lasers: the centre dot becomes a ring of four + * (indices 25–28, a diamond at 12/3/6/9 o'clock). Outer and inner rings are + * unchanged so 1–24 still match the venue numbering. + */ + 'grace-28': () => ringsLayout({ + rings: [ + { count: 12, radius: 1, phase: 15 }, + { count: 12, radius: 0.62 }, + { count: 4, radius: 0.25 } + ], + id: 'grace-28', + name: 'Grace Cathedral (12 + 12 + 4)' + }), 'ring-25-filled': () => filledRingLayout({ count: 25, id: 'ring-25-filled', name: '25-cannon filled ring' }), 'ring-25-hollow': () => annulusLayout({ count: 25, innerRadius: 0.5, id: 'ring-25-hollow', name: '25-cannon ring with a hollow centre' }), 'disc-25': () => annulusLayout({ count: 25, innerRadius: 0, id: 'disc-25', name: '25-cannon disc (concentric rings)' })