Let Fit fill the tile tab instead of stopping at 100 percent
Fit in the tile tab left the board small in the middle of the tab. It used frameBoxes, which never zooms past 1 so that two graph nodes do not blow up to fill the screen. The tile board is measured in image pixels, though, and with 16 px tiles it is only about 345 x 186 px. - frameBoxes takes a new cap parameter, default 1. The graph and the node board keep their behaviour. - The tile tab passes its zoom limit of 6, so Fit fills the tab up to 600 percent. - TileCanvas remembers whether the view is still the one Fit set. While it is, a change in the size of the tab or of the board fits again. After a pan or zoom by hand the view stays until Fit. Found on the way: switching the image to another tile size (48 to 16 px) kept the old zoom, and the board shrank to a third. The refit above covers that too. Tests: - ./check.sh: 161 files, 2370 tests, CHECK EXIT 0 (was 2369). - New unit test in graph-framing: with a raised cap Fit fills the view up to the limit. The graph test for "not past 100 %" holds. - e2e 39: ALL PASSED, 31 checks (was 29). Fit fills the tab at 48 px (zoom 0.41) and fits again after the switch to 16 px (zoom 1.24). - Counter probes: a cap of 1 and no refit on a new board size each fail exactly the 16 px check (zoom 1 and zoom 0.41). - e2e sum in the README is 1384, computed, not yet seen in CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
50bddf68d4
commit
4027afc228
5 files changed
+55
-5
No files matched your search
@@ -43,7 +43,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
|
||||
The browser suite and the three example games need a machine with a GPU:
|
||||
|
||||
```bash
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1382 checks (GM_STEPS=20 for one step)
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1384 checks (GM_STEPS=20 for one step)
|
||||
# every tool signs in first – see the Accounts page
|
||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||
|
||||
@@ -148,21 +148,24 @@ export function boundsOf(boxes: readonly Box[], seed?: Bounds): Bounds | null {
|
||||
|
||||
/**
|
||||
* The view that shows every box at once, centred, with `margin` screen pixels to spare.
|
||||
* Never zooms past 1: a two-node graph blown up to fill the screen helps nobody. The scene view
|
||||
* deliberately does not share that cap, which is why only the bounds sum is common.
|
||||
* Never zooms past `cap`, 1 by default: a two-node graph blown up to fill the screen helps nobody.
|
||||
* The Tile tab lifts it, since its board is measured in image pixels of a tile – 16 px of them
|
||||
* at 100 % is a stamp in the corner. The scene view does not share the cap at all, which is why
|
||||
* only the bounds sum is common.
|
||||
*/
|
||||
export function frameBoxes(
|
||||
boxes: readonly Box[],
|
||||
view: { w: number; h: number },
|
||||
margin: number,
|
||||
limits: { min: number; max: number },
|
||||
cap = 1,
|
||||
): Framing {
|
||||
const bounds = boundsOf(boxes);
|
||||
if (!bounds) return { zoom: 1, panX: view.w / 2, panY: view.h / 2 };
|
||||
const { minX, minY, maxX, maxY } = bounds;
|
||||
const width = Math.max(maxX - minX, 1);
|
||||
const height = Math.max(maxY - minY, 1);
|
||||
const fitted = Math.min((view.w - margin * 2) / width, (view.h - margin * 2) / height, 1);
|
||||
const fitted = Math.min((view.w - margin * 2) / width, (view.h - margin * 2) / height, cap);
|
||||
const zoom = Math.min(Math.max(fitted, limits.min), limits.max);
|
||||
return {
|
||||
zoom,
|
||||
|
||||
@@ -97,6 +97,15 @@ export class TileCanvas extends Widget {
|
||||
private beside = false;
|
||||
private info: TileInfo | null = null;
|
||||
private needsFit = true;
|
||||
/**
|
||||
* Whether the view is still the one Fit chose. A panel that settles on its size after the tab
|
||||
* opened – or is resized later – then fits again, instead of keeping a board framed for the
|
||||
* first, smaller size; the same when the board itself changes size. A pan or a zoom by hand
|
||||
* ends it until the next Fit.
|
||||
*/
|
||||
private followsFit = true;
|
||||
/** The view size and the board a fit was made for. */
|
||||
private fittedFor = { w: 0, h: 0, board: { w: 0, h: 0 } };
|
||||
private readonly stopPictures: () => void;
|
||||
|
||||
constructor(private readonly state: EditorState) {
|
||||
@@ -114,6 +123,7 @@ export class TileCanvas extends Widget {
|
||||
(view, gesture) => {
|
||||
this.view = view;
|
||||
if (gesture === 'zoom') this.needsFit = false;
|
||||
this.followsFit = false;
|
||||
this.applyView();
|
||||
},
|
||||
ZOOM_LIMITS,
|
||||
@@ -147,11 +157,13 @@ export class TileCanvas extends Widget {
|
||||
this.assetId = assetId;
|
||||
this.time = 0;
|
||||
this.needsFit = true;
|
||||
this.followsFit = true;
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
fit(): void {
|
||||
this.needsFit = true;
|
||||
this.followsFit = true;
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
@@ -164,6 +176,7 @@ export class TileCanvas extends Widget {
|
||||
if (needsFit) return;
|
||||
this.view = { pan: { ...view.pan }, zoom: view.zoom };
|
||||
this.needsFit = false;
|
||||
this.followsFit = false;
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
@@ -194,6 +207,8 @@ export class TileCanvas extends Widget {
|
||||
zoom: number;
|
||||
pan: { x: number; y: number };
|
||||
rect: { x: number; y: number; w: number; h: number };
|
||||
/** Grid and sample map together, in image pixels of the tile. */
|
||||
size: { w: number; h: number };
|
||||
} {
|
||||
const info = this.info;
|
||||
const origin = this.getGlobalPosition();
|
||||
@@ -210,6 +225,7 @@ export class TileCanvas extends Widget {
|
||||
zoom: this.view.zoom,
|
||||
pan: { ...this.view.pan },
|
||||
rect: { x: origin.x, y: origin.y, w: this.w, h: this.h },
|
||||
size: { ...this.board },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -273,6 +289,8 @@ export class TileCanvas extends Widget {
|
||||
}
|
||||
|
||||
override layout(): void {
|
||||
const resized = this.w !== this.fittedFor.w || this.h !== this.fittedFor.h;
|
||||
if (this.followsFit && resized) this.needsFit = true;
|
||||
if (this.needsFit) this.redraw();
|
||||
}
|
||||
|
||||
@@ -288,11 +306,15 @@ export class TileCanvas extends Widget {
|
||||
this.build(asset);
|
||||
this.paint(asset);
|
||||
this.syncHint(asset);
|
||||
// Another picture with smaller tiles would otherwise leave the board a third of its size.
|
||||
const regrown = this.board.w !== this.fittedFor.board.w || this.board.h !== this.fittedFor.board.h;
|
||||
if (this.followsFit && regrown) this.needsFit = true;
|
||||
if (this.needsFit && this.w > 0 && this.h > 0 && this.tiles.length > 0) {
|
||||
const box: Box = { id: 'board', x: 0, y: 0, w: this.board.w, h: this.board.h };
|
||||
const framing = frameBoxes([box], { w: this.w, h: this.h }, FIT_MARGIN, ZOOM_LIMITS);
|
||||
const framing = frameBoxes([box], { w: this.w, h: this.h }, FIT_MARGIN, ZOOM_LIMITS, ZOOM_LIMITS.max);
|
||||
this.view = { pan: { x: framing.panX, y: framing.panY }, zoom: framing.zoom };
|
||||
this.needsFit = false;
|
||||
this.fittedFor = { w: this.w, h: this.h, board: { ...this.board } };
|
||||
}
|
||||
this.applyView();
|
||||
}
|
||||
|
||||
@@ -141,6 +141,13 @@ describe('fit', () => {
|
||||
expect(f.zoom).toBe(1);
|
||||
});
|
||||
|
||||
it('fills the view up to a raised cap – the Tile tab, whose board is a few tiles of 16 px', () => {
|
||||
const board: Box[] = [{ id: 'board', x: 0, y: 0, w: 345, h: 186 }];
|
||||
const f = frameBoxes(board, { w: 1000, h: 600 }, 16, { min: 0.1, max: 6 }, 6);
|
||||
expect(f.zoom).toBeCloseTo((1000 - 32) / 345);
|
||||
expect(frameBoxes(board, { w: 10000, h: 6000 }, 16, { min: 0.1, max: 6 }, 6).zoom).toBe(6);
|
||||
});
|
||||
|
||||
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 },
|
||||
|
||||
@@ -208,6 +208,17 @@ export async function tile(t) {
|
||||
await settle();
|
||||
board = await json('__gm.tile.board()');
|
||||
const fitted = { zoom: board.zoom, pan: board.pan };
|
||||
// Fit used to stop at 100 %, which left a board measured in 16 px tiles a stamp in the corner:
|
||||
// framed, it reaches the margin on one side (or the zoom limit).
|
||||
const fills = (b) =>
|
||||
b.zoom === 6 ||
|
||||
Math.abs(Math.max((b.size.w * b.zoom) / (b.rect.w - 32), (b.size.h * b.zoom) / (b.rect.h - 32)) - 1) <
|
||||
0.01;
|
||||
check(
|
||||
'Fit fills the view with the board',
|
||||
fills(board),
|
||||
JSON.stringify({ zoom: board.zoom, size: board.size, rect: board.rect }),
|
||||
);
|
||||
const centre = { x: board.rect.x + board.rect.w / 2, y: board.rect.y + board.rect.h - 40 };
|
||||
await drag(centre.x, centre.y, centre.x + 60, centre.y - 40);
|
||||
board = await json('__gm.tile.board()');
|
||||
@@ -396,6 +407,13 @@ export async function tile(t) {
|
||||
board.kind === 'terrain' && board.combinations === 47 && board.tile === 16 && board.problem === '',
|
||||
JSON.stringify(board),
|
||||
);
|
||||
// A third of the tile size, and the view untouched since the last Fit: the board frames itself
|
||||
// again, past 100 %, instead of shrinking to a third where it stood.
|
||||
check(
|
||||
'smaller tiles are framed again, past 100 %',
|
||||
board.zoom > 1 && fills(board),
|
||||
JSON.stringify({ zoom: board.zoom, size: board.size, rect: board.rect }),
|
||||
);
|
||||
const wholeTile = (tx, ty) => [
|
||||
[2 * tx, 2 * ty],
|
||||
[2 * tx + 1, 2 * ty],
|
||||
|
||||
Reference in new issue
Block a user