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>
141 lines
5.6 KiB
TypeScript
141 lines
5.6 KiB
TypeScript
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);
|
||
});
|
||
});
|