Zoom the tile tab in the same steps as the scene view
The tile tab zoomed freely, so the pixels of a tile could end up
unevenly wide at odd factors. Scene view, container tab and animation
preview already snap to FREE_ZOOM_STEPS. The tile tab now does the
same, so every pixel editor zooms alike.
- The wheel steps through FREE_ZOOM_STEPS, counted in device pixels.
The board position is rounded to whole device pixels.
- New wheelSteps() in src/viewport/view.ts turns wheel events into
steps: one notch is one step, a trackpad gathers 50 px per step.
FreeViewport uses it too, so the rule exists only once.
- Fit takes the largest step at which the whole board fits. Refitting
on a new tab or board size stays as before.
- panAndZoom takes an optional zoom rule instead of limits. Graph and
node board keep zooming freely.
- A saved view from before the steps snaps to the nearest step.
- The cap parameter of frameBoxes from 4027afc has no user anymore.
It is removed together with its unit test.
Fit rounds down, and below 100 percent the steps are far apart. In the
small e2e window the 48 px board lands at 25 instead of 41 percent,
the 16 px board at 100 instead of 124 percent.
Tests:
- check.sh: 161 files, 2370 tests, exit 0 (-1 cap test,
+1 wheelSteps test)
- e2e 1-5 and 39: all passed, 169 checks. Three checks in 39 now
expect steps: Fit on a step with the next one too large, one notch
goes 0.25 to 0.5, 16 px tiles refit to at least 100 percent.
- Counter check: a free Fit factor fails exactly those three checks
(zoom 0.413 and 1.2396).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4027afc228
commit
021a6dcb56
8 files changed
+110
-47
No files matched your search
@@ -19,10 +19,11 @@ import {
|
||||
tilePlayback,
|
||||
} from '../../core/model/tile';
|
||||
import { findAssetOf } from '../../core/model/Project';
|
||||
import { type Size, Theme, textStyle, Widget } from '../../ui';
|
||||
import { type Size, Theme, textStyle, ui, Widget } from '../../ui';
|
||||
import { textureLibrary } from '../../viewport/TextureLibrary';
|
||||
import { type Box, frameBoxes } from '../framing';
|
||||
import { type GraphView, panAndZoom } from '../graph/graphView';
|
||||
import { nearestZoomStep, stepFreeZoom, wheelSteps, zoomStepBelow } from '../../viewport/view';
|
||||
import type { Point } from '../framing';
|
||||
import { type GraphView, panAndZoom, zoomedAbout } from '../graph/graphView';
|
||||
import type { EditorState } from '../state';
|
||||
|
||||
/** Tiles per row in the combination grid; 47 of them then read as six rows of eight. */
|
||||
@@ -38,8 +39,6 @@ const MIN_CAPTION_SIZE = 6;
|
||||
const CAPTION_MARGIN = 4;
|
||||
const SECTION_GAP = 1.2;
|
||||
const FIT_MARGIN = 16;
|
||||
/** Wider than the graph's: a 48 px tile is small, and its quarters are what one wants to look at. */
|
||||
const ZOOM_LIMITS = { min: 0.1, max: 6 };
|
||||
|
||||
/** One tile of the board: where it sits and which quarters it is drawn from. */
|
||||
interface BoardTile {
|
||||
@@ -64,7 +63,8 @@ interface Caption {
|
||||
* The Tile tab's picture (#118): every tile the block can produce, captioned with the
|
||||
* neighbours that ask for it, and below or beside them a sample map from the same quarters – the
|
||||
* one place that shows whether the edges really continue across tile borders. A drag pans, the
|
||||
* wheel zooms, nothing is edited here.
|
||||
* wheel zooms in the scene view's steps (`FREE_ZOOM_STEPS`, device pixels), so every texel of a
|
||||
* tile gets as many screen pixels as its neighbour; nothing is edited here.
|
||||
*/
|
||||
export class TileCanvas extends Widget {
|
||||
private readonly bg = new Graphics();
|
||||
@@ -107,6 +107,7 @@ export class TileCanvas extends Widget {
|
||||
/** 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;
|
||||
private readonly wheel = wheelSteps();
|
||||
|
||||
constructor(private readonly state: EditorState) {
|
||||
super();
|
||||
@@ -126,7 +127,7 @@ export class TileCanvas extends Widget {
|
||||
this.followsFit = false;
|
||||
this.applyView();
|
||||
},
|
||||
ZOOM_LIMITS,
|
||||
(view, at, deltaY) => this.zoomedStep(view, at, deltaY),
|
||||
);
|
||||
|
||||
state.on('project', () => this.redraw());
|
||||
@@ -174,7 +175,9 @@ export class TileCanvas extends Widget {
|
||||
restoreView(saved: unknown): void {
|
||||
const { view, needsFit } = saved as { view: GraphView; needsFit: boolean };
|
||||
if (needsFit) return;
|
||||
this.view = { pan: { ...view.pan }, zoom: view.zoom };
|
||||
// A view saved before the steps, or on a screen with another scaling, lands on the nearest one.
|
||||
const zoom = nearestZoomStep(view.zoom * this.ratio()) / this.ratio();
|
||||
this.view = { pan: { ...view.pan }, zoom };
|
||||
this.needsFit = false;
|
||||
this.followsFit = false;
|
||||
this.redraw();
|
||||
@@ -204,7 +207,9 @@ export class TileCanvas extends Widget {
|
||||
tile: number;
|
||||
problem: string;
|
||||
beside: boolean;
|
||||
/** CSS pixels per image pixel; times `ratio` it is one of the scene view's steps. */
|
||||
zoom: number;
|
||||
ratio: 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. */
|
||||
@@ -223,6 +228,7 @@ export class TileCanvas extends Widget {
|
||||
problem: info?.problem ?? '',
|
||||
beside: this.beside,
|
||||
zoom: this.view.zoom,
|
||||
ratio: this.ratio(),
|
||||
pan: { ...this.view.pan },
|
||||
rect: { x: origin.x, y: origin.y, w: this.w, h: this.h },
|
||||
size: { ...this.board },
|
||||
@@ -310,9 +316,11 @@ export class TileCanvas extends Widget {
|
||||
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, ZOOM_LIMITS.max);
|
||||
this.view = { pan: { x: framing.panX, y: framing.panY }, zoom: framing.zoom };
|
||||
// The largest step the board fits at, as in the scene view – no longer a free factor.
|
||||
const { w, h } = this.board;
|
||||
const fitted = Math.min((this.w - FIT_MARGIN * 2) / w, (this.h - FIT_MARGIN * 2) / h);
|
||||
const zoom = zoomStepBelow(fitted * this.ratio()) / this.ratio();
|
||||
this.view = { pan: { x: (this.w - w * zoom) / 2, y: (this.h - h * zoom) / 2 }, zoom };
|
||||
this.needsFit = false;
|
||||
this.fittedFor = { w: this.w, h: this.h, board: { ...this.board } };
|
||||
}
|
||||
@@ -498,8 +506,25 @@ export class TileCanvas extends Widget {
|
||||
this.hint.position.set(Theme.padding * 2, Theme.padding * 2);
|
||||
}
|
||||
|
||||
/** One step about the pointer once the wheel has travelled far enough; null until then. */
|
||||
private zoomedStep(view: GraphView, at: Point, deltaY: number): GraphView | null {
|
||||
const direction = this.wheel(deltaY);
|
||||
if (direction === 0) return null;
|
||||
const zoom = stepFreeZoom(view.zoom * this.ratio(), direction) / this.ratio();
|
||||
if (zoom === view.zoom) return null;
|
||||
return zoomedAbout(view, at, zoom / view.zoom, { min: zoom, max: zoom });
|
||||
}
|
||||
|
||||
/** Device pixels per CSS pixel: the steps count device pixels, as in the scene view. */
|
||||
private ratio(): number {
|
||||
return ui().app.renderer.resolution;
|
||||
}
|
||||
|
||||
/** The board starts on a whole device pixel, so a step draws every texel alike. */
|
||||
private applyView(): void {
|
||||
this.content.position.set(Math.round(this.view.pan.x), Math.round(this.view.pan.y));
|
||||
const ratio = this.ratio();
|
||||
const snap = (v: number) => Math.round(v * ratio) / ratio;
|
||||
this.content.position.set(snap(this.view.pan.x), snap(this.view.pan.y));
|
||||
this.content.scale.set(this.view.zoom);
|
||||
this.placeCaptions();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user