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
+18 -7
View File
@@ -12,7 +12,11 @@
// booting a Pixi `Application`.
import { torusShortestDelta } from "./math";
import { DEFAULT_POINT_RADIUS_PX, type PointPrim, type PrimitiveID } from "./world";
import {
displayPointRadiusWorld,
type PointPrim,
type PrimitiveID,
} from "./world";
/**
* PickModeOptions configures a pick-mode session. The caller is
@@ -110,11 +114,15 @@ export function computePickOverlay(
pointPrimitivesById: ReadonlyMap<PrimitiveID, PointPrim>,
allPrimitiveIds: Iterable<PrimitiveID>,
world: { width: number; height: number } | null = null,
cameraScale: number = 1,
scaleRef: number = 1,
): PickOverlaySpec {
const sourcePrim = pointPrimitivesById.get(options.sourcePrimitiveId);
const sourceRadius =
(sourcePrim?.style.pointRadiusPx ?? DEFAULT_POINT_RADIUS_PX) +
ANCHOR_PADDING_WORLD;
const sourceVisibleRadius =
sourcePrim === undefined
? 0
: displayPointRadiusWorld(sourcePrim.style, cameraScale, scaleRef);
const sourceRadius = sourceVisibleRadius + ANCHOR_PADDING_WORLD;
const dimmed = new Set<PrimitiveID>();
for (const id of allPrimitiveIds) {
@@ -160,12 +168,15 @@ export function computePickOverlay(
) {
const target = pointPrimitivesById.get(hoveredId);
if (target !== undefined) {
const targetRadius = displayPointRadiusWorld(
target.style,
cameraScale,
scaleRef,
);
hoverOutline = {
x: target.x,
y: target.y,
radius:
(target.style.pointRadiusPx ?? DEFAULT_POINT_RADIUS_PX) +
HOVER_PADDING_WORLD,
radius: targetRadius + HOVER_PADDING_WORLD,
};
}
}