The released game only ever zoomed in whole steps up to 400%, so a
small camera on a large screen stayed far smaller than it could be.
The "Click to play" caption was drawn in the 11 px panel title size
and was easy to miss.
- GAME_SCALE_STEPS gains 300%, 500% and 600%. The game tab picker,
Auto in the editor and the game link all read the same list.
- The start caption takes its size from the new Theme.captionSize
(16) instead of a constant in GameScreen.
- The player page keeps the default arrow cursor at all times. It no
longer switches to the hand over the caption.
Tests:
- view.test: Auto for 320x180 in 2000x2000 is now 6, with cases
for 3 and 5. play-settings.test takes 2.5 and 8 as invalid steps.
- e2e 60 checks the caption against the panel title size and the
arrow cursor before and after the start. Module 03 follows the new
step list.
- ./check.sh 2808 tests, CHECK EXIT 0 (before the last Theme tweak,
which changes no test). Full e2e run ALL PASSED, 1574 checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scene view had no header bar of its own, and its eye sat in the
top bar. Switching between Scene and Game also showed two different
pictures, since nothing tied the free view to the camera.
- Theme.headerPadding (4) replaces Theme.padding in the top bar and
in TabHeader, for the padding and for minHeight. Both bars shrink
from 40 to 32 px. Nothing else uses the new value.
- The new ScenePane wraps the scene view in a header bar with Fit
and the visibility eye. The eye is gone from the top bar, which
keeps the menu, undo and redo.
- Fit with an active camera shows the world exactly as the Game tab
does at its chosen step: the same zoom, and the camera centre on
the same screen pixel. The rule is the pure freeViewOfCamera in
viewport/view.ts, which also mirrors the letterbox snapping.
- Fit without a camera frames all content on a whole step, as
before.
- New hook __gm.sceneHeaderAt(name). e2e 46g opens the eye there.
- The doc comment above Theme.treeIndent went away in the working
tree along with this. It is committed as found.
Not checked: the two views only match while they have the same size.
With the tilemap panel open the scene view is narrower, and Auto may
pick a smaller step there.
Tests:
- ./check.sh: 178 files, 2575 tests, CHECK EXIT 0
- 2 new unit tests in view.test.ts: three cameras times four steps
land on the same screen point as in the Game tab, Auto stays whole
- e2e 3h (2 checks): a real click on Fit at Auto and at Fit, crate,
camera frame and view box match in both tabs
- GM_STEPS=1-3,46: ALL PASSED, 94 checks
- Counter-probes: without the centre offset the unit test fails,
with Fit ignoring the camera both 3h checks fail
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scene view stopped at 24 device pixels per world pixel, which is
too little to work on single texels of small pixel art.
- FREE_ZOOM_STEPS gains the steps 32, 48 and 64. All of them are whole
numbers, so every texel keeps the same width on screen.
- The tile editor takes the same steps, so its fit can now land above
24 on a small board.
- The unit test and the step list in e2e 39 follow the new top step.
Tests:
- ./check.sh: 178 files, 2573 tests, CHECK EXIT 0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The wheel zoomed smoothly and fit computed any factor, so the scene
view almost never showed pixel art with even texels. Under Windows
scaling even zoom 1 was off. The zoom now only takes steps that give
every texel the same number of screen pixels.
- The zoom counts device pixels per world pixel. freeView divides by
the renderer resolution, so zoom 1 is one screen pixel per texel.
- FREE_ZOOM_STEPS: 1/8, 1/4, 1/2, then 1 to 6, 8, 12, 16 and 24.
- The wheel moves one step per notch. A trackpad collects its travel
and steps every 50 px.
- Fit rounds down to the next step. A saved zoom from before snaps to
the nearest step on a log scale.
- Pan, drag, grid and resize use CSS pixels per world unit, so they
still follow the pointer under Windows scaling.
- Always on, no toggle, as decided in the session.
Tests:
- 4 unit tests: zoom 1 at 125%, whole steps, wheel steps and ends, fit
rounding and snapping of a saved zoom.
- Counter-check: without the ratio in freeView the 125% test fails.
- check.sh: 156 files, 2320 tests, CHECK EXIT 0.
- e2e 1-17,28,30,38-40: 726 checks passed on the steps before the
final values (up to 8). Not rerun on the final steps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 125% Windows scaling the game view drew 100% as 1.25 screen pixels
per camera pixel, so pixel art showed texels of uneven width even at
the round steps. The steps now count device pixels instead of CSS
pixels.
- gameScaleFactor, cameraFactor and cameraView take the device pixel
ratio. 100% is one screen pixel per camera pixel, 200% two.
- Auto picks the largest step that fits in device pixels. Fit reports
its factor in device pixels too, e.g. 156% at 125% scaling.
- Fixed steps start the frame on a whole device pixel.
- The game view reads the ratio from the renderer resolution.
Limits: main.ts caps the resolution at 2, so scaling above 200% is
stretched by the browser again. A changed ratio at runtime only counts
after a reload, as before.
Tests:
- 2 unit tests at 125%: 100% is one device pixel on a whole pixel,
auto and fit count device pixels.
- Counter-check: without the conversion the 100% test fails.
- check.sh: 156 files, CHECK EXIT 0 on the combined working tree.
- No e2e: the harness runs at devicePixelRatio 1, same math as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The game view always stretched the camera picture to fit the pane.
Pixel art came out blurred at odd factors, and there was no way to
see the game at its real size. The view now has a header with a
scale drop-down, and 100 % is the default.
- The header of the new GamePane offers Auto, Fit, 25 %, 50 %,
100 %, 200 % and 400 %. Auto and Fit show the factor they land
on, e.g. "Auto · 100%", and follow the pane when it resizes.
- 100 % means one screen pixel per pixel of the camera resolution
(width x height). The camera zoom scales the world on top of
that, so a zoomed camera still fills the same frame.
- Auto takes the largest fixed step that fits the whole picture
and falls back to 25 % when nothing fits. Fit keeps the old
behaviour.
- A picture larger than the pane is cropped around the camera
centre. Fixed steps start on a whole pixel so pixel art stays
sharp at 100 %.
- The choice is kept in the browser like "Play starts". Nothing
is written into the project.
Found on the way: the first draft applied the step to the world
instead of the camera resolution. The camera zoom then had no
effect at 100 %, which the existing check 12c caught (zoom 1.37
measured as scale 1).
The CI smokes were not run locally, because that would reseed
projects the user works in. All three smoke cameras are 640x360
and fit the 670x383 harness view at 100 %.
Tests:
- ./check.sh: 156 files, 2298 tests, CHECK EXIT 0 (before 2290).
- 8 unit tests in tests/viewport/view.test.ts for whole-pixel
steps, cropping, Auto and Fit with fallback, and zoomed cameras.
- 7 e2e checks in 03g. e2e 1-17,28,30,38-40: ALL PASSED, 727
checks.
- Counter-checks: no cropping, no rounding, no factor reported to
the header, and no zoom share each fail their own tests.
- Check total: 1367 (CI at c1a702b) + 7 = 1374, computed only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Tooling:
- Prettier as the single formatting authority (110 columns, single quotes,
measured against the existing style); Markdown excluded because table-cell
padding turns the README glossary into 1000-character lines.
- Type-aware ESLint ending in eslint-config-prettier, so a rule always means
"this is a bug", never "this looks different".
- .editorconfig and an `npm run check` that runs typecheck, lint, format and
tests in one go.
Findings fixed on the first run: three more duplicate imports, four
String(unknown) calls (parameter types narrowed instead of casts), two dead
initialisers, two redundant casts, a console.log for a real error. Two
warnings were false here and are disabled with a reason in place.
Tests 132 -> 269:
- node-catalog.test.ts covers every node with a run(), with a guard that fails
when a new node has no row.
- behavior-events.test.ts covers the ten event nodes through a running World,
checking timing rather than existence.
- Input's state machine (press/release/releaseAll) is reachable without DOM
events now; the DOM handlers are thin adapters.
Compare fell back to == without a configured operator although the schema
default is <; aligned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>