Files
feuerwerk/tests/editor/sheet-view.test.ts
T
rootandClaude Opus 5 4d84231b9f Trim needless comments and fix the picker width
Remove comments that only restate the code, keeping the section banners
and the notes that carry a reason, a measured fact or an external
contract.

ButtonOptions.width claimed to be a fixed width but only set minWidth,
so the Graph and Texture pickers grew with the name they showed. It is
now what it promises, with a separate minWidth for the buttons that have
two forms. Narrow buttons shrink their padding, otherwise a 24px icon
button had 8px left for its glyph and truncate dropped it entirely.

__gm.labelsDropped() guards that class: the overflow guard only saw text
that was too wide, never a label truncated to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:52:05 +02:00

141 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import type { FrameRect } from '../../src/core/model/texture';
import {
dragFrame,
fitView,
type FrameHandle,
handleAt,
handleCursor,
handlePoint,
imageToLocal,
localToImage,
MAX_SHEET_ZOOM,
MIN_SHEET_ZOOM,
rectBetween,
zoomAbout,
} from '../../src/editor/texture/sheetView';
const RECT: FrameRect = { x: 10, y: 20, w: 40, h: 30 };
describe('sheet view mapping', () => {
it('maps image pixels to the widget and back', () => {
const view = { panX: 100, panY: 50, zoom: 2 };
expect(imageToLocal(view, 0, 0)).toEqual({ x: 100, y: 50 });
expect(imageToLocal(view, 8, 4)).toEqual({ x: 116, y: 58 });
expect(localToImage(view, 116, 58)).toEqual({ x: 8, y: 4 });
});
it('fits a picture centred, with the same margin on both axes', () => {
// A 100x50 picture in a 400x400 widget is limited by its width.
const view = fitView(100, 50, 400, 400);
expect(view.zoom).toBeCloseTo(3.6);
expect(view.panX).toBeCloseTo((400 - 100 * 3.6) / 2);
expect(view.panY).toBeCloseTo((400 - 50 * 3.6) / 2);
// Fitting only has to guarantee that the whole picture is inside the widget.
const end = imageToLocal(view, 100, 50);
expect(view.panX).toBeGreaterThanOrEqual(0);
expect(end.x).toBeLessThanOrEqual(400);
expect(end.y).toBeLessThanOrEqual(400);
});
it('has a defined answer for a picture that has not been measured', () => {
expect(fitView(0, 0, 400, 400)).toEqual({ panX: 0, panY: 0, zoom: 1 });
expect(fitView(100, 50, 0, 0)).toEqual({ panX: 0, panY: 0, zoom: 1 });
});
it('keeps the pixel under the cursor under the cursor while zooming', () => {
const view = { panX: 100, panY: 50, zoom: 2 };
const before = localToImage(view, 300, 260);
const zoomed = zoomAbout(view, 1.5, 300, 260);
expect(zoomed.zoom).toBeCloseTo(3);
expect(localToImage(zoomed, 300, 260).x).toBeCloseTo(before.x);
expect(localToImage(zoomed, 300, 260).y).toBeCloseTo(before.y);
});
it('clamps the zoom at both ends', () => {
const view = { panX: 0, panY: 0, zoom: 1 };
expect(zoomAbout(view, 1e6, 0, 0).zoom).toBe(MAX_SHEET_ZOOM);
expect(zoomAbout(view, 1e-6, 0, 0).zoom).toBe(MIN_SHEET_ZOOM);
});
});
describe('grabbing a frame', () => {
it('finds each of the eight handles at its own corner or edge', () => {
expect(handlePoint(RECT, { hx: -1, hy: -1 })).toEqual({ x: 10, y: 20 });
expect(handlePoint(RECT, { hx: 1, hy: 1 })).toEqual({ x: 50, y: 50 });
expect(handlePoint(RECT, { hx: 0, hy: -1 })).toEqual({ x: 30, y: 20 });
expect(handleAt(RECT, { x: 10, y: 20 }, 2)).toEqual({ hx: -1, hy: -1 });
expect(handleAt(RECT, { x: 50, y: 20 }, 2)).toEqual({ hx: 1, hy: -1 });
expect(handleAt(RECT, { x: 30, y: 50 }, 2)).toEqual({ hx: 0, hy: 1 });
});
it('grabs the inside as a move and misses outside entirely', () => {
expect(handleAt(RECT, { x: 30, y: 35 }, 2)).toBe('move');
expect(handleAt(RECT, { x: 5, y: 35 }, 2)).toBeNull();
expect(handleAt(RECT, { x: 30, y: 60 }, 2)).toBeNull();
});
it('prefers a corner where handles overlap, so a tiny frame stays resizable', () => {
// A 4x4 frame with a 6 px tolerance: every handle matches its centre, and a grab there
// must resize, or the frame could never be adjusted.
const tiny: FrameRect = { x: 0, y: 0, w: 4, h: 4 };
expect(handleAt(tiny, { x: 2, y: 2 }, 6)).toEqual({ hx: -1, hy: -1 });
});
it('names a cursor for every handle', () => {
expect(handleCursor('move')).toBe('move');
expect(handleCursor({ hx: -1, hy: -1 })).toBe('nwse-resize');
expect(handleCursor({ hx: 1, hy: -1 })).toBe('nesw-resize');
expect(handleCursor({ hx: 0, hy: 1 })).toBe('ns-resize');
expect(handleCursor({ hx: -1, hy: 0 })).toBe('ew-resize');
});
});
describe('dragging a frame', () => {
it('moves without changing the size', () => {
expect(dragFrame(RECT, 'move', 5, -7)).toEqual({ x: 15, y: 13, w: 40, h: 30 });
});
it('pins the opposite edge when a corner is dragged', () => {
const dragged = dragFrame(RECT, { hx: -1, hy: -1 }, 4, 6);
expect(dragged).toEqual({ x: 14, y: 26, w: 36, h: 24 });
expect(dragged.x + dragged.w).toBe(RECT.x + RECT.w);
expect(dragged.y + dragged.h).toBe(RECT.y + RECT.h);
});
it('moves only its own axis when an edge is dragged', () => {
expect(dragFrame(RECT, { hx: 0, hy: 1 }, 9, 5)).toEqual({ x: 10, y: 20, w: 40, h: 35 });
expect(dragFrame(RECT, { hx: 1, hy: 0 }, 9, 5)).toEqual({ x: 10, y: 20, w: 49, h: 30 });
});
it('is allowed to invert – squaring that up belongs to one place only', () => {
// The negative width is on purpose: `normalizeFrameRect` corrects a drag and a typed
// value identically.
const dragged = dragFrame(RECT, { hx: -1, hy: 0 }, 60, 0);
expect(dragged.w).toBeLessThan(0);
});
it('spans a rectangle between two points in either order', () => {
const forward = rectBetween({ x: 10, y: 20 }, { x: 30, y: 50 });
expect(forward).toEqual({ x: 10, y: 20, w: 20, h: 30 });
expect(rectBetween({ x: 30, y: 50 }, { x: 10, y: 20 })).toEqual(forward);
});
it('walks a frame through the eight handles back to where it started', () => {
// A sign error in any of the eight branches breaks the drag-and-back identity.
const handles: FrameHandle[] = [
'move',
{ hx: -1, hy: -1 },
{ hx: 1, hy: -1 },
{ hx: -1, hy: 1 },
{ hx: 1, hy: 1 },
{ hx: 0, hy: -1 },
{ hx: 0, hy: 1 },
{ hx: -1, hy: 0 },
{ hx: 1, hy: 0 },
];
for (const handle of handles)
expect(dragFrame(dragFrame(RECT, handle, 3, 4), handle, -3, -4)).toEqual(RECT);
});
});