Implement the tabletop library's game state store, setup seeding with bare-type expansion, surface mount tree resolution, part placement, and the stacking positioning process with a dependency-free SVG path helper. Wire the public API and add unit plus vite integration tests.
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { stackingOffset, parsePath, pointAt, NO_OFFSET } from './stacking.js';
|
|
|
|
describe('parsePath', () => {
|
|
it('measures a straight line', () => {
|
|
const path = parsePath('M 0 0 L 10 0');
|
|
expect(path.length).toBeCloseTo(10);
|
|
});
|
|
|
|
it('measures a cubic curve', () => {
|
|
const path = parsePath('M 0 0 C 20 -20 40 -20 60 0');
|
|
// Longer than the chord (60) but finite.
|
|
expect(path.length).toBeGreaterThan(60);
|
|
expect(path.length).toBeLessThan(80);
|
|
});
|
|
|
|
it('handles relative commands', () => {
|
|
const path = parsePath('m 0 0 l 10 0 l 0 10');
|
|
expect(path.length).toBeCloseTo(20);
|
|
});
|
|
|
|
it('supports h/v/z', () => {
|
|
const path = parsePath('M 0 0 H 10 V 10 Z');
|
|
// 10 right + 10 down + the diagonal back to the start (closes the triangle).
|
|
expect(path.length).toBeCloseTo(10 + 10 + Math.sqrt(200));
|
|
});
|
|
});
|
|
|
|
describe('pointAt', () => {
|
|
it('returns the start at distance 0 and end at full length', () => {
|
|
const path = parsePath('M 0 0 L 10 0');
|
|
expect(pointAt(path, 0)).toMatchObject({ x: 0, y: 0 });
|
|
const end = pointAt(path, path.length);
|
|
expect(end.x).toBeCloseTo(10);
|
|
expect(end.y).toBeCloseTo(0);
|
|
});
|
|
|
|
it('interpolates along the path', () => {
|
|
const path = parsePath('M 0 0 L 10 0');
|
|
const mid = pointAt(path, 5);
|
|
expect(mid.x).toBeCloseTo(5);
|
|
expect(mid.angle).toBeCloseTo(0);
|
|
});
|
|
});
|
|
|
|
describe('stackingOffset', () => {
|
|
it('returns no offset without a curve', () => {
|
|
expect(stackingOffset(undefined, 0, 3)).toBe(NO_OFFSET);
|
|
expect(stackingOffset({ limit: 5 }, 0, 3)).toBe(NO_OFFSET);
|
|
});
|
|
|
|
it('spreads parts evenly along a straight curve', () => {
|
|
const offset = stackingOffset({ curve: 'M 0 0 L 100 0' }, 1, 3);
|
|
// step = length / max(steps=1, 2) = 50; part 1 at 50.
|
|
expect(offset.x).toBeCloseTo(50);
|
|
expect(offset.y).toBeCloseTo(0);
|
|
});
|
|
|
|
it('aligns to center', () => {
|
|
const offset = stackingOffset({ curve: 'M 0 0 L 100 0', align: 'center' }, 0, 3);
|
|
// span = 50 * 2 = 100; centered start = (100 - 100)/2 = 0.
|
|
expect(offset.x).toBeCloseTo(0);
|
|
});
|
|
|
|
it('aligns to end', () => {
|
|
const offset = stackingOffset({ curve: 'M 0 0 L 100 0', align: 'end' }, 2, 3);
|
|
// start = 100 - 100 = 0; part 2 at 100.
|
|
expect(offset.x).toBeCloseTo(100);
|
|
});
|
|
|
|
it('respects a positive limit (first n)', () => {
|
|
const offset = stackingOffset({ curve: 'M 0 0 L 100 0', limit: 2 }, 2, 4);
|
|
// Part 2 is beyond the first 2 shown -> not placed.
|
|
expect(offset).toBe(NO_OFFSET);
|
|
});
|
|
|
|
it('respects a negative limit (last n)', () => {
|
|
// Last 2 of 4 are indices 2,3. Part 2 is the first shown.
|
|
const offset = stackingOffset({ curve: 'M 0 0 L 100 0', limit: -2 }, 2, 4);
|
|
expect(offset.x).toBeCloseTo(0);
|
|
});
|
|
|
|
it('uses steps to densify the curve', () => {
|
|
// steps=4, 3 parts -> step = 100 / max(4, 2) = 25.
|
|
const offset = stackingOffset({ curve: 'M 0 0 L 100 0', steps: 4 }, 1, 3);
|
|
expect(offset.x).toBeCloseTo(25);
|
|
});
|
|
}); |