Files
tts-workshop/packages/tabletop/src/stacking.test.ts
T
hypercross 43c6334413 fix(tabletop): tilt cards uniformly about the y axis
Apply the same tilt to every stacked part and rotate it about its local
Y axis instead of Z. Default tilt to 1 degree when a stacking strategy
is present, and arc the poker deck along the surface's bottom edge.
2026-08-10 00:23:08 +08:00

120 lines
4.4 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);
// A stacking strategy defaults to a 1° tilt, so it's not the identity.
expect(stackingOffset({ limit: 5 }, 0, 3)).toEqual({ x: 0, y: 0, rotation: 0, z: 0, tilt: 1 });
});
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);
});
it('tilts every part the same amount without a curve', () => {
const offset = stackingOffset({ tilt: 0.1 }, 2, 3);
expect(offset).toEqual({ x: 0, y: 0, rotation: 0, z: 0, tilt: 0.1 });
});
it('tilts every part the same amount along the curve', () => {
const offset = stackingOffset({ curve: 'M 0 0 L 100 0', tilt: 0.1 }, 1, 3);
expect(offset.x).toBeCloseTo(50);
expect(offset.tilt).toBeCloseTo(0.1);
});
it('tilts only the shown parts', () => {
// limit 2 shows indices 0,1; index 2 is dropped.
expect(stackingOffset({ tilt: 0.1, limit: 2 }, 2, 4)).toBe(NO_OFFSET);
expect(stackingOffset({ tilt: 0.1, limit: 2 }, 1, 4).tilt).toBeCloseTo(0.1);
});
it('ramps z from zStart to zEnd across the curve', () => {
// 3 parts on a 100-long curve: u = 0, 0.5, 1. z ramps 0 -> 40.
const first = stackingOffset({ curve: 'M 0 0 L 100 0', zStart: 0, zEnd: 40 }, 0, 3);
const mid = stackingOffset({ curve: 'M 0 0 L 100 0', zStart: 0, zEnd: 40 }, 1, 3);
const last = stackingOffset({ curve: 'M 0 0 L 100 0', zStart: 0, zEnd: 40 }, 2, 3);
expect(first.z).toBeCloseTo(0);
expect(mid.z).toBeCloseTo(20);
expect(last.z).toBeCloseTo(40);
});
it('returns no offset without a stacking strategy', () => {
expect(stackingOffset(undefined, 0, 3)).toBe(NO_OFFSET);
});
});