ui/map-renderer: clamp torus zoom-out to minScaleNoWrap
The renderer's torus mode laid out the world in a 3×3 grid of wrap
copies (TORUS_OFFSETS) so the user could pan past an edge without
seeing a void. Below `minScale = max(viewport/world)` the world
shrinks below the viewport along at least one axis and the wrap
copies become visible side-by-side — the user reported a 9-tile
mosaic that pans and zooms as one rigid unit. The doc explicitly
deferred the fix ("if profiling ever reveals that users do this");
real usage is the trigger.
Apply `clampZoom({ minScale })` in both modes; torus still keeps
free pan (no `clamp({ direction: "all" })`) so the wrap copies
fill the cross-edge slack as designed. Resize re-evaluates the
clamp so a window resize does not strand the camera below the new
floor. Documentation in `ui/docs/renderer.md` updated to describe
the new shared invariant.
Regression test in `tests/e2e/playground-map.spec.ts` wheels out
aggressively in torus mode and asserts `camera.scale >= minScale`
across all four Playwright projects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+17
-17
@@ -147,28 +147,28 @@ each copy renders the full line at its offset, and PixiJS' culling
|
||||
naturally drops the parts outside its container's reachable area.
|
||||
|
||||
The nine-copy upper bound assumes the visible viewport never
|
||||
exceeds three tile-widths or three tile-heights of the world. In
|
||||
no-wrap mode we enforce `clampZoom({ minScale })` directly. In
|
||||
torus mode we do not enforce a minScale; the playground starts at
|
||||
`minScale * 1.2` so a user has to zoom out aggressively before
|
||||
seeing more than nine copies. If profiling ever reveals that
|
||||
users do this, the renderer should switch to a generalised tile
|
||||
loop.
|
||||
exceeds three tile-widths or three tile-heights of the world. To
|
||||
hold this assumption in both modes, the renderer enforces
|
||||
`clampZoom({ minScale })` with `minScale = max(viewport.W/world.W,
|
||||
viewport.H/world.H)` regardless of wrap mode. Without this, in
|
||||
torus mode the user could zoom out far enough to see the 3×3 grid
|
||||
of wrap copies at once — the copies are there to fill partial slack
|
||||
near a panned edge, not to be visible simultaneously. The clamp is
|
||||
re-evaluated on every viewport resize so a window resize does not
|
||||
strand the camera below the new minimum.
|
||||
|
||||
## No-wrap camera
|
||||
|
||||
`pixi-viewport`'s built-in `clamp({ direction: 'all' })` plugin
|
||||
keeps the camera inside the world rectangle by default. We layer
|
||||
two project-specific rules on top, both implemented via the
|
||||
`'moved'` event:
|
||||
|
||||
1. When the visible viewport is larger than the world along an
|
||||
axis, the camera is **centred** on that axis. `pixi-viewport`'s
|
||||
default would pin the world to the top-left of the screen,
|
||||
which is jarring at low zoom.
|
||||
2. `clampZoom({ minScale })` enforces `minScale = max(viewport.W/world.W,
|
||||
viewport.H/world.H)` so the user cannot zoom out below
|
||||
"viewport fits world".
|
||||
the project-specific centring rule on top, implemented via the
|
||||
`'moved'` event: when the visible viewport is larger than the world
|
||||
along an axis, the camera is **centred** on that axis.
|
||||
`pixi-viewport`'s default would pin the world to the top-left of
|
||||
the screen, which is jarring at low zoom. The shared
|
||||
`clampZoom({ minScale })` (described above) prevents this case in
|
||||
practice, but the centring rule stays as a defensive layer for
|
||||
windowed-resize transients.
|
||||
|
||||
`pivotZoom` keeps the world point under the cursor stable during
|
||||
zoom. The math is symmetric and tested in
|
||||
|
||||
Reference in New Issue
Block a user