import { describe, expect, it } from 'vitest'; import { layoutSymbols, ringRadius } from '../../src/viewport/symbolLayout'; import { haloSvg } from '../../src/ui/svg'; const SPACING = 28; function distance(a: { x: number; y: number }, b: { x: number; y: number }): number { return Math.hypot(a.x - b.x, a.y - b.y); } describe('layoutSymbols', () => { it('leaves a symbol with room around it on its own position', () => { const spots = layoutSymbols( [ { id: 'a', x: 0, y: 0 }, { id: 'b', x: 100, y: 0 }, ], SPACING, ); expect(spots.get('a')).toEqual({ x: 0, y: 0, angle: null }); expect(spots.get('b')).toEqual({ x: 100, y: 0, angle: null }); }); it('puts two symbols on one spot left and right of it, the middle free', () => { const spots = layoutSymbols( [ { id: 'body', x: 50, y: 20 }, { id: 'collider', x: 50, y: 20 }, ], SPACING, ); const body = spots.get('body')!; const collider = spots.get('collider')!; expect(body.x).toBeCloseTo(36); expect(body.y).toBeCloseTo(20); expect(collider.x).toBeCloseTo(64); expect(collider.y).toBeCloseTo(20); }); it('spaces any number evenly on a ring round the middle, neighbours a symbol apart', () => { for (const count of [3, 4, 5, 8]) { const anchors = Array.from({ length: count }, (_, i) => ({ id: `s${i}`, x: 10, y: -5 })); const spots = [...layoutSymbols(anchors, SPACING).values()]; const r = ringRadius(count, SPACING); for (const spot of spots) expect(distance(spot, { x: 10, y: -5 })).toBeCloseTo(r); for (let i = 0; i < count; i++) expect(distance(spots[i]!, spots[(i + 1) % count]!)).toBeCloseTo(SPACING); const mean = { x: spots.reduce((s, p) => s + p.x, 0) / count, y: spots.reduce((s, p) => s + p.y, 0) / count, }; expect(distance(mean, { x: 10, y: -5 })).toBeCloseTo(0); } }); it('shares a spot whoever owns the symbols, and rings round the middle of close positions', () => { const spots = layoutSymbols( [ { id: 'a', x: 0, y: 0 }, { id: 'b', x: 10, y: 0 }, ], SPACING, ); expect(spots.get('a')!.x).toBeCloseTo(-9); expect(spots.get('b')!.x).toBeCloseTo(19); }); it('takes in a symbol the ring would cover, until nothing overlaps', () => { // Two on the origin reach out to ±28 – the third at 40 would sit on the right one. const anchors = [ { id: 'a', x: 0, y: 0 }, { id: 'b', x: 0, y: 0 }, { id: 'c', x: 40, y: 0 }, ]; const spots = [...layoutSymbols(anchors, SPACING).values()]; for (let i = 0; i < spots.length; i++) for (let j = i + 1; j < spots.length; j++) expect(distance(spots[i]!, spots[j]!)).toBeGreaterThanOrEqual(SPACING - 1e-9); expect(spots.every((s) => s.angle !== null)).toBe(true); }); it('places the same input the same way every time', () => { const anchors = [ { id: 'a', x: 0, y: 0 }, { id: 'b', x: 3, y: 1 }, { id: 'c', x: -2, y: 4 }, ]; expect([...layoutSymbols(anchors, SPACING)]).toEqual([...layoutSymbols(anchors, SPACING)]); }); }); describe('haloSvg', () => { it('widens every shape by its outline in currentColor, keeping hollow shapes hollow', () => { const svg = [ '', ' ', ' ', '', ].join('\n'); const halo = haloSvg(svg, 1.25); expect(halo).toContain( '', ); expect(halo).toContain( '', ); expect(halo.startsWith('')).toBe(true); }); it('outlines the open ends of a stroke too, keeping a round cap round', () => { const svg = [ '', ' ', ' ', '', ].join('\n'); const halo = haloSvg(svg, 1); expect(halo).toContain( '', ); expect(halo).toContain( '', ); }); });