// canvas.mjs's maths: boxes as frame fractions, clamped to the frame, // snapped to the thirds and to the other blocks' edges within one per // cent, moved and resized by handle, nudged by key, and written with // four decimals. import test from 'node:test' import assert from 'node:assert/strict' import { clampBox, fixBox, guides, snapMove, snapResize, moveBox, resizeBox, nudge, rectBox, MIN_SIZE, NUDGE, NUDGE_BIG } from './canvas.mjs' const near = (a, b, eps = 1e-9) => Math.abs(a - b) < eps const boxNear = (got, want) => { for (const k of ['x', 'y', 'w', 'h']) assert.ok(near(got[k], want[k]), `${k}: ${got[k]} vs ${want[k]}`) } test('clampBox keeps the box inside the frame and no smaller than the minimum', () => { assert.deepEqual(clampBox({ x: -0.2, y: 0.5, w: 0.3, h: 0.2 }), { x: 0, y: 0.5, w: 0.3, h: 0.2 }) assert.deepEqual(clampBox({ x: 0.9, y: 0.95, w: 0.3, h: 0.2 }), { x: 0.7, y: 0.8, w: 0.3, h: 0.2 }) assert.deepEqual(clampBox({ x: 0.1, y: 0.1, w: 0, h: 5 }), { x: 0.1, y: 0, w: MIN_SIZE, h: 1 }) }) test('fixBox writes four decimals', () => { assert.deepEqual(fixBox({ x: 0.123456, y: 0.1, w: 0.33333333, h: 0.2 }), { x: 0.1235, y: 0.1, w: 0.3333, h: 0.2 }) assert.deepEqual(fixBox({ x: 1.2, y: 0, w: 0.5, h: 0.5 }), { x: 0.5, y: 0, w: 0.5, h: 0.5 }) }) test('guides are the thirds plus every other block edge', () => { const g = guides([{ x: 0.1, y: 0.2, w: 0.3, h: 0.4 }]) assert.deepEqual(g.x.sort(), [0, 1 / 3, 2 / 3, 1, 0.1, 0.4].sort()) assert.deepEqual(g.y.sort(), [0, 1 / 3, 2 / 3, 1, 0.2, 0.6000000000000001].sort()) assert.equal(guides([]).x.length, 4) }) test('snapMove pulls the nearest edge onto a guide within one per cent, and leaves a far one', () => { const g = guides([{ x: 0.5, y: 0.5, w: 0.2, h: 0.2 }]) // left edge 0.008 from the frame's left → 0 boxNear(snapMove({ x: 0.008, y: 0.4, w: 0.2, h: 0.1 }, g), { x: 0, y: 0.4, w: 0.2, h: 0.1 }) // right edge 0.295 → the other block's left at 0.5 is too far; the third at 1/3 is 0.038 away — no snap boxNear(snapMove({ x: 0.095, y: 0.4, w: 0.2, h: 0.1 }, g), { x: 0.095, y: 0.4, w: 0.2, h: 0.1 }) // right edge at 0.495 → snaps to the other block's left edge, moving the whole box boxNear(snapMove({ x: 0.295, y: 0.4, w: 0.2, h: 0.1 }, g), { x: 0.3, y: 0.4, w: 0.2, h: 0.1 }) // top edge 0.505 → the other block's top at 0.5 boxNear(snapMove({ x: 0.1, y: 0.505, w: 0.2, h: 0.1 }, g), { x: 0.1, y: 0.5, w: 0.2, h: 0.1 }) // the closer of two candidate edges wins: left at 0.009 from 0, right at 0.004 from 1/3 const both = snapMove({ x: 0.009, y: 0, w: 1 / 3 - 0.005, h: 0.1 }, g) boxNear(both, { x: 1 / 3 - (1 / 3 - 0.005), y: 0, w: 1 / 3 - 0.005, h: 0.1 }) }) test('snapResize moves only the edges the handle drags', () => { const g = guides([]) // east handle: right edge 0.66 → 2/3, left stays boxNear(snapResize({ x: 0.2, y: 0.2, w: 0.46, h: 0.2 }, 'e', g), { x: 0.2, y: 0.2, w: 2 / 3 - 0.2, h: 0.2 }) // west handle: left edge 0.338 → 1/3, right edge stays at 0.8 boxNear(snapResize({ x: 0.338, y: 0.2, w: 0.462, h: 0.2 }, 'w', g), { x: 1 / 3, y: 0.2, w: 0.8 - 1 / 3, h: 0.2 }) // north-west: both leading edges boxNear(snapResize({ x: 0.005, y: 0.338, w: 0.3, h: 0.5 }, 'nw', g), { x: 0, y: 1 / 3, w: 0.305, h: 0.5 + 0.338 - 1 / 3 }) // south handle never touches x boxNear(snapResize({ x: 0.005, y: 0.2, w: 0.3, h: 0.795 }, 's', g), { x: 0.005, y: 0.2, w: 0.3, h: 0.8 }) }) test('moveBox and resizeBox by handle', () => { assert.deepEqual(moveBox({ x: 0.1, y: 0.1, w: 0.2, h: 0.2 }, 0.05, -0.05), { x: 0.15000000000000002, y: 0.05, w: 0.2, h: 0.2 }) boxNear(resizeBox({ x: 0.1, y: 0.1, w: 0.2, h: 0.2 }, 'se', 0.1, 0.1), { x: 0.1, y: 0.1, w: 0.3, h: 0.3 }) boxNear(resizeBox({ x: 0.1, y: 0.1, w: 0.2, h: 0.2 }, 'nw', 0.05, 0.05), { x: 0.15, y: 0.15, w: 0.15, h: 0.15 }) boxNear(resizeBox({ x: 0.1, y: 0.1, w: 0.2, h: 0.2 }, 'n', 0, -0.1), { x: 0.1, y: 0, w: 0.2, h: 0.3 }) boxNear(resizeBox({ x: 0.1, y: 0.1, w: 0.2, h: 0.2 }, 'w', 0.1, 0), { x: 0.2, y: 0.1, w: 0.1, h: 0.2 }) // A handle dragged past the opposite edge stops at the minimum size. boxNear(resizeBox({ x: 0.1, y: 0.1, w: 0.2, h: 0.2 }, 'e', -0.5, 0), { x: 0.1, y: 0.1, w: MIN_SIZE, h: 0.2 }) boxNear(resizeBox({ x: 0.1, y: 0.1, w: 0.2, h: 0.2 }, 'w', 0.5, 0), { x: 0.1 + 0.2 - MIN_SIZE, y: 0.1, w: MIN_SIZE, h: 0.2 }) }) test('nudge: arrows move by half a per cent, Shift by two, Alt resizes', () => { const b = { x: 0.1, y: 0.1, w: 0.2, h: 0.2 } boxNear(nudge(b, 'ArrowRight'), { ...b, x: 0.1 + NUDGE }) boxNear(nudge(b, 'ArrowUp', { shift: true }), { ...b, y: 0.1 - NUDGE_BIG }) boxNear(nudge(b, 'ArrowDown', { alt: true }), { ...b, h: 0.2 + NUDGE }) boxNear(nudge(b, 'ArrowLeft', { alt: true, shift: true }), { ...b, w: 0.2 - NUDGE_BIG }) assert.equal(nudge(b, 'Enter'), null) }) test('rectBox reads a block rectangle against its frame', () => { const frame = { left: 100, top: 50, width: 800, height: 450 } boxNear(rectBox(frame, { left: 180, top: 95, width: 400, height: 90 }), { x: 0.1, y: 0.1, w: 0.5, h: 0.2 }) assert.deepEqual(rectBox({ left: 0, top: 0, width: 0, height: 0 }, { left: 0, top: 0, width: 1, height: 1 }), { x: 0, y: 0, w: 0, h: 0 }) })