Files
feuerwerk/tests/editor/graph-framing.test.ts
T
rootandClaude Opus 5 11981b40d0 Give the three asset tabs one base, and move the viewports into the editor
src/viewport/ held three classes only src/editor/ ever used, and framing.ts sat
under graph/ although the scene view wants the same bounds sum. The graph, the
texture and the container tab repeated the same header, subscription and
disposal code with two silent divergences: the graph tab said "nothing shown"
with null instead of '', and the texture tab refell back to the first asset on
a selection change but not on a project event.

These arrive together because GraphCanvas ties them: it reads framing from its
new home, hands its fold marks to folding.ts, and its '' change is what
GraphEditor must agree with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 07:14:38 +02:00

112 lines
4.2 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 { boundsOf, type Box, boxesInBand, frameBoxes } from '../../src/editor/framing';
const LIMITS = { min: 0.25, max: 2.5 };
/** Two nodes side by side, the right one twice as tall. */
const boxes: Box[] = [
{ id: 'a', x: 0, y: 0, w: 100, h: 60 },
{ id: 'b', x: 200, y: 0, w: 100, h: 120 },
];
describe('rubber band', () => {
it('catches a node the band merely touches, not only one it encloses', () => {
// The band clips a's right edge and never reaches b.
expect(boxesInBand(boxes, { x1: 90, y1: 10, x2: 150, y2: 20 })).toEqual(['a']);
});
it('catches both when the band spans them', () => {
expect(boxesInBand(boxes, { x1: -10, y1: -10, x2: 310, y2: 130 })).toEqual(['a', 'b']);
});
it('reads a band dragged up and to the left the same way', () => {
expect(boxesInBand(boxes, { x1: 310, y1: 130, x2: -10, y2: -10 })).toEqual(['a', 'b']);
});
it('catches nothing in empty space', () => {
expect(boxesInBand(boxes, { x1: 120, y1: 200, x2: 180, y2: 260 })).toEqual([]);
});
});
describe('fit', () => {
/** Where a graph point lands on screen under a framing. */
const toScreen = (f: { zoom: number; panX: number; panY: number }, x: number, y: number) => ({
x: x * f.zoom + f.panX,
y: y * f.zoom + f.panY,
});
it('leaves no node outside the view – the reported bug', () => {
// Wider than the view at zoom 1, so framing has to zoom out rather than just centre.
const view = { w: 200, h: 200 };
const f = frameBoxes(boxes, view, 24, LIMITS);
expect(f.zoom).toBeLessThan(1);
for (const b of boxes) {
const topLeft = toScreen(f, b.x, b.y);
const bottomRight = toScreen(f, b.x + b.w, b.y + b.h);
expect(topLeft.x).toBeGreaterThanOrEqual(0);
expect(topLeft.y).toBeGreaterThanOrEqual(0);
expect(bottomRight.x).toBeLessThanOrEqual(view.w);
expect(bottomRight.y).toBeLessThanOrEqual(view.h);
}
});
it('measures each node by its own height', () => {
// b is twice as tall as a: pretending both were 60 high would frame too little.
const tall = frameBoxes([{ id: 'b', x: 0, y: 0, w: 100, h: 400 }], { w: 200, h: 200 }, 24, LIMITS);
const short = frameBoxes([{ id: 'a', x: 0, y: 0, w: 100, h: 60 }], { w: 200, h: 200 }, 24, LIMITS);
expect(tall.zoom).toBeLessThan(short.zoom);
});
it('centres what it frames', () => {
const view = { w: 200, h: 200 };
const f = frameBoxes(boxes, view, 24, LIMITS);
const centre = toScreen(f, 150, 60);
expect(centre.x).toBeCloseTo(100);
expect(centre.y).toBeCloseTo(100);
});
it('does not blow a small graph up past its natural size', () => {
const f = frameBoxes([{ id: 'a', x: 0, y: 0, w: 10, h: 10 }], { w: 800, h: 600 }, 24, LIMITS);
expect(f.zoom).toBe(1);
});
it('never zooms out past the limit, however far the nodes are apart', () => {
const far: Box[] = [
{ id: 'a', x: 0, y: 0, w: 100, h: 60 },
{ id: 'b', x: 100000, y: 0, w: 100, h: 60 },
];
expect(frameBoxes(far, { w: 200, h: 200 }, 24, LIMITS).zoom).toBe(LIMITS.min);
});
it('centres the view on an empty graph instead of returning nonsense', () => {
const f = frameBoxes([], { w: 200, h: 100 }, 24, LIMITS);
expect(f).toEqual({ zoom: 1, panX: 100, panY: 50 });
});
});
describe('boundsOf', () => {
it('encloses every box', () => {
const boxes: Box[] = [
{ id: 'a', x: 10, y: 20, w: 100, h: 60 },
{ id: 'b', x: -40, y: 5, w: 20, h: 10 },
];
expect(boundsOf(boxes)).toEqual({ minX: -40, minY: 5, maxX: 110, maxY: 80 });
});
it('reports nothing for no boxes at all', () => {
expect(boundsOf([])).toBeNull();
});
/** The scene view seeds the origin so a template drawn far to one side cannot hide it. */
it('keeps a seed inside the result even when every box is far away', () => {
const far: Box[] = [{ id: 'a', x: 500, y: 500, w: 10, h: 10 }];
const origin = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
expect(boundsOf(far, origin)).toEqual({ minX: 0, minY: 0, maxX: 510, maxY: 510 });
});
it('still answers for no boxes when a seed is given', () => {
const origin = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
expect(boundsOf([], origin)).toEqual(origin);
});
});