feat(ui): F8-12 — map polish (zoom invariance, labels, selection, soft radius) (#55)
Tests · UI / test (push) Waiting to run
Tests · UI / test (pull_request) Failing after 5m16s

* Honest pixel-space sizing for `pointRadiusPx` / `strokeWidthPx`: the
  renderer divides by the current camera scale on every
  `viewport.zoomed` so thin lines / small markers stay the same on-screen
  size at any zoom.
* Known-size planets switch to `pointRadiusWorld`, softened against the
  reference scale by `PLANET_SIZE_ZOOM_ALPHA = 0.33`; unidentified
  planets pin to a 3-px disc.
* New planet label layer renders a two-line `name / #N` legend under
  each planet (`#N` only for unidentified or when the new `planetNames`
  toggle is off). Selection now paints an inverse-fill frame around the
  selected planet's label plus an outline on the disc; the old
  selection-ring primitive is retired.
* Bombing markers swap the separate CirclePrim for a planet-outline
  overlay (damaged / wiped colour); the report deep-link moves to a
  "view bombing report" link in the planet inspector.
* Docs + tests follow: `renderer.md` reflects the new sizing contract +
  label / outline layers, vitest covers the sizing math, label
  formatting, and the new toggle, and the map-toggles e2e adds a
  persistence case for `planetNames`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-27 23:51:16 +02:00
parent ba93a9092e
commit 680ebac919
30 changed files with 1240 additions and 322 deletions
+46 -7
View File
@@ -66,13 +66,48 @@ interface LinePrim extends PrimitiveBase { kind: 'line';
`radius` is in world units. `style.strokeWidthPx` and
`style.pointRadiusPx` are in screen pixels and stay constant under
zoom (Pixi's stroke width is in pixel space when the parent
container is scaled).
zoom — F8-12 / #28 wired the renderer to repaint every affected
`Graphics` on every `viewport.zoomed` event with
`size_in_world = size_in_pixels / cameraScale`. `displayStrokeWidthWorld`
and `displayPointRadiusWorld` (in `src/map/world.ts`) compute those
world-space values; the hit-test reads the same helpers so the click
zone always matches the visible footprint.
`style.pointRadiusWorld` is the alternative sizing rule for planet
discs with a known `size`: the renderer treats the base radius as
world units and softens its growth with the camera scale through
`PLANET_SIZE_ZOOM_ALPHA` (0.33). At `scale = scaleRef` (the
"whole world fits the viewport" zoom) the visible radius equals the
base radius; zooming in grows it sub-linearly so on-screen pixel
size scales as `scale^α`. Setting both `pointRadiusWorld` and
`pointRadiusPx` ignores the pixel-space field.
Default hit slop in screen pixels: point=8, circle=6, line=6.
These are touch-ergonomic defaults; per-primitive `hitSlopPx > 0`
overrides them.
### Planet label layer
Independent of the primitive stream, the renderer mounts a per-copy
`labelLayer` (F8-12 / #29). `RendererHandle.setPlanetLabels(labels,
selectedPlanetId)` replaces the dataset; the renderer keeps each
label container at `(planet.x, planet.y + visibleRadius + gapPx)`
and at `scale = 1 / cameraScale` so the text reads at the same
pixel size regardless of zoom. The selected planet gets an
inverse-fill frame around its label, replacing the retired
`selection-ring` primitive (F8-12 / #30).
### Planet outline overlay
`RendererHandle.setPlanetOutlines(outlines)` paints a thin stroke
around the visible disc of any planet number listed in the spec.
The map view feeds it the union of bombings (damaged / wiped accent
colour, gated by the `bombingMarkers` toggle) and the current
selection (`selectionAccent` colour); selection wins on the same
planet. The radius follows `displayPointRadiusWorld`, so the
outline hugs the disc through every zoom step — softened or
pixel-space alike.
## Theme
A `Theme` is the renderer's full colour palette: the canvas background
@@ -127,11 +162,15 @@ target.
Per-primitive distance:
- **Point**: `distSq ≤ (pointRadiusPx + slopWorld)²`. The visible
disc is part of the click target — a click on any pixel of the
rendered planet registers as a hit, with `slopWorld` adding a
small ergonomic margin on top. `pointRadiusPx` defaults to
`DEFAULT_POINT_RADIUS_PX = 3` when unset.
- **Point**: `distSq ≤ (visibleRadiusWorld + slopWorld)²`. The
visible disc is part of the click target — a click on any pixel of
the rendered planet registers as a hit, with `slopWorld` adding a
small ergonomic margin on top. `visibleRadiusWorld` comes from
`displayPointRadiusWorld` (F8-12 / #28 + #31): pixel-space
`pointRadiusPx / scale` for unidentified planets and most ship
groups, softened-by-zoom `pointRadiusWorld * (scale / scaleRef)^(α-1)`
for planets with a known `size`. `pointRadiusPx` defaults to
`DEFAULT_POINT_RADIUS_PX = 3` when neither field is set.
- **Filled circle**: `distSq ≤ (radius + slopWorld)²` where
`radius` is in world units. The circle counts as filled when
`style.fillColor` is set and `style.fillAlpha > 0`.