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('