Skip to content

fix: fit an opened map to the window it is opened in - #1828

Merged
Azgaar merged 2 commits into
masterfrom
fix/fit-loaded-maps-to-viewport
Sep 9, 2026
Merged

Azgaar merged 2 commits into
masterfrom
fix/fit-loaded-maps-to-viewport

Conversation

@Azgaar

@Azgaar Azgaar commented Sep 9, 2026

Copy link
Copy Markdown
Owner

The bug

A map opened on a screen that is not the one it was made on no longer fills the window. 1.152.0 changed two things at once in fitMapToScreen():

setViewport(Math.min(graph.width, wanted.width), Math.min(graph.height, wanted.height));
setZoomExtent(Math.max(options.app.zoomExtent.min, coverMin), max);

Each half breaks one direction:

  • Map smaller than the window — the #map svg is sized to the map's extent, so the map sits in the top-left with dead canvas right and bottom.
  • Map larger than the window — the cover scale is below 1, but the default zoomExtent.min of 1 wins, so the map can never be zoomed out to fit.

Reproduced with 1.108.12.map (extent 1440×778) at 1600×900 and at 1000×600, and confirmed against a build of d00e3311^.

What it used to do

public/modules/ui/options.js sized the svg from the user's canvas request (which follows the window), and the floor was the derived cover value, which also overwrote the control:

svgWidth = Math.min(+mapWidthInput.value, window.innerWidth);
const zoomMin = rn(Math.max(svgWidth / graphWidth, svgHeight / graphHeight), 3);
zoomExtentMin.value = zoomMin;
setZoomExtent(zoomMin, +zoomExtentMax.value);

The fix

  • canvas.ts — the viewport follows the window (or the viewport the user pinned); the extent no longer bounds it. The zoom floor is max(viewport / extent), rounded up — rounding to nearest can leave a sub-pixel strip of canvas, which makes d3's constrain centre the map instead of pinning it. It is written to app.zoomExtent.min and into the panel, so the control shows the limit the canvas enforces.
  • zoom.ts — new constrainZoom() re-applies the current transform through the behavior, because d3 enforces scaleExtent/translateExtent on gestures only; a raised floor otherwise did nothing until the user touched the map. resetZoom() targets the floor rather than a bare identity, and skips the transition at duration 0 (Submap and Transform call resetZoom(0) right after a fit).
  • load.tsfitMapToScreen() runs before focusOn() (matching the generation path, so a ?scale=/?burg= deep link still wins), followed by resetZoom(0) so an opened map opens at the fitted view.
  • options-tab.ts — a hand-set floor applies as typed and stands until the next fit re-derives it, as before. "Restore default zoom extent" re-derives the floor instead of forcing 1, which would have re-broken the invariant.

Verified

1.108.12.map, extent 1440×778:

Window #map svg Zoom floor Opens at
1000×600 (smaller) 1000×600 0.772 0.772 — whole map fitted
1600×900 (bigger) 1600×900 1.157 1.157 — scaled up, no bands

setMapZoom(0.1) clamps to the floor; zooming to 4 and then resizing keeps k=4 and only re-derives the floor, so a resize never yanks the user's view; regenerating resets the extent to the window with floor 1.

942 unit tests, tsc and biome check src/ pass. tests/e2e/canvas-size.spec.ts covers both directions; per the repo rule Playwright was not run locally, so CI is the first run.

Notes

  • app.zoomExtent.min is now a derived value the canvas writes rather than a stored preference — that is what 1.151 did, and docs/architecture/configuration.md is updated to say so, but it sits slightly against that file's "preferences are what the user asked for" framing. Worth a second opinion.
  • The version bump to 1.152.1 was already in the working tree; the sync-version pre-commit hook pulls package.json and the lockfile along with it.

🤖 Generated with Claude Code

1.152.0 clamped the viewport to the map's own extent and derived the zoom
floor as max(zoomExtent.min, cover). Both halves of that broke opening a
map made on a different screen:

- a map smaller than the window was letterboxed - the #map svg was sized
  to the extent, leaving dead canvas right and bottom;
- a map larger than the window could not zoom out to fit, because the
  default zoomExtent.min of 1 won over the cover scale of <1.

Before 1.152 the svg followed the window and the floor was the derived
cover value, which also overwrote the zoomExtentMin control. Restore both:

- the viewport follows the window (or the viewport the user pinned); the
  extent no longer bounds it;
- the zoom floor is max(viewport / extent), rounded up so the rounding
  cannot leave a hairline of canvas, written to app.zoomExtent.min and
  shown in the panel;
- constrainZoom() re-applies the current transform through the behavior,
  since d3 enforces a new extent on gestures only;
- an opened map opens at that floor - the fitted view - via resetZoom(0),
  which no longer goes through a transition;
- restoreDefaultZoomExtent re-derives the floor instead of forcing 1,
  which would have re-broken the invariant.

A hand-set floor still applies as typed and stands until the next fit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@netlify

netlify Bot commented Sep 9, 2026

Copy link
Copy Markdown

Deploy Preview for afmg ready!

Name Link
🔨 Latest commit b0d4809
🔍 Latest deploy log https://app.netlify.com/projects/afmg/deploys/6aa1e0c4011c9c00085bc519
😎 Deploy Preview https://deploy-preview-1828--afmg.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@Azgaar Azgaar self-assigned this Sep 9, 2026
@github-actions github-actions Bot added the theme: ui-editors UI / UX & Editors label Sep 9, 2026
#labels font-size is written by applyLabelsZoomSize from the current
zoom, not by any preset. It baselined as 100px only because a loaded map
used to open at scale 1; now that a map opens fitted to the window, it
tracks the window size against the map's extent - 1.112.1.map is
1680x849, so at the suite's 1280x720 viewport the floor is 0.849 and the
size lands at 108.89px.

That is the same category as the scale bar layout the helper already
strips, so drop it there and from both baselines.
@Azgaar
Azgaar merged commit c06dd7a into master Sep 9, 2026
11 checks passed
@Azgaar
Azgaar deleted the fix/fit-loaded-maps-to-viewport branch September 9, 2026 22:54
@github-project-automation github-project-automation Bot moved this from Backlog to Done in FMG dev board Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

theme: ui-editors UI / UX & Editors

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant