import { test } from 'node:test'; import assert from 'node:assert/strict'; import { STAGE_H, STAGE_W, clampTickPair, containFit, defaultTape, defaultTicks, lineX, stageToImage, tapeEndpoints, } from './tapeGeometry.ts'; import { cropMarginFraction, DEFAULT_CROP_FILL } from '../png/crop.ts'; test('defaultTicks sit at crop insets for 90% fill', () => { const t = defaultTicks(0.9); const inset = STAGE_H * cropMarginFraction(0.9); assert.ok(Math.abs(t.tickA - inset) < 1e-9); assert.ok(Math.abs(t.tickB - (STAGE_H - inset)) < 1e-9); }); test('lineX is the middle of the 3:4 stage', () => { assert.equal(lineX(), STAGE_W / 2); }); test('identity tape: stage centre maps near image centre', () => { const imgW = 480, imgH = 640; const tape = defaultTape(DEFAULT_CROP_FILL, 0); const [ix, iy] = stageToImage(STAGE_W / 2, STAGE_H / 2, tape, imgW, imgH); assert.ok(Math.abs(ix - imgW / 2) < 1e-6, `ix=${ix}`); assert.ok(Math.abs(iy - imgH / 2) < 1e-6, `iy=${iy}`); }); test('tapeEndpoints span equals tick distance in image px at identity (matching aspect)', () => { const imgW = 480, imgH = 640; const tape = defaultTape(0.9, 0); const ep = tapeEndpoints(tape, imgW, imgH); const { scale0 } = containFit(imgW, imgH); const expected = Math.abs(tape.tickB - tape.tickA) / scale0; assert.ok(Math.abs(ep.spanPx - expected) < 1e-6, `span ${ep.spanPx} vs ${expected}`); // Direction should be +y (down the stage → down the image) assert.ok(Math.abs(ep.ux) < 1e-6, `ux=${ep.ux}`); assert.ok(ep.uy > 0.99, `uy=${ep.uy}`); }); test('zoom scales inverse: 2× zoom halves image-space tick span', () => { const imgW = 480, imgH = 640; const base = defaultTape(0.9, 0); const zoomed = { ...base, zoom: 2 }; const a = tapeEndpoints(base, imgW, imgH).spanPx; const b = tapeEndpoints(zoomed, imgW, imgH).spanPx; assert.ok(Math.abs(b * 2 - a) < 1e-6, `base=${a} zoomed=${b}`); }); test('clampTickPair enforces gap and order', () => { const p = clampTickPair(400, 100, 40); assert.ok(p.tickB - p.tickA >= 40); assert.ok(p.tickA <= p.tickB); });