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
+11 -9
View File
@@ -82,10 +82,10 @@ describe("isCategoryVisible", () => {
expect(isCategoryVisible("planet-unidentified", t)).toBe(false);
});
test("battle and bombing markers have independent toggles", () => {
const t = toggles({ battleMarkers: false, bombingMarkers: true });
test("battleMarker toggle hides battle X-crosses without touching other layers", () => {
const t = toggles({ battleMarkers: false });
expect(isCategoryVisible("battleMarker", t)).toBe(false);
expect(isCategoryVisible("bombingMarker", t)).toBe(true);
expect(isCategoryVisible("planet-foreign", t)).toBe(true);
});
});
@@ -202,6 +202,9 @@ describe("computeHiddenPlanetNumbers", () => {
});
describe("computeHiddenIds", () => {
// F8-12 / #30: bombings no longer ride the cascade as their own
// primitive — they paint a planet outline directly. The fixture
// here mirrors what `reportToWorld` currently emits.
const categories: Map<PrimitiveID, MapCategory> = new Map<
PrimitiveID,
MapCategory
@@ -212,11 +215,10 @@ describe("computeHiddenIds", () => {
[150, "hyperspaceGroup"],
[200, "incomingGroup"],
[300, "battleMarker"],
[400, "bombingMarker"],
]);
const planetDependents = new Map<number, ReadonlySet<PrimitiveID>>([
[1, new Set([1])],
[2, new Set([2, 100, 150, 200, 300, 400])],
[2, new Set([2, 100, 150, 200, 300])],
]);
test("category-toggle off hides every primitive in that category", () => {
@@ -239,10 +241,10 @@ describe("computeHiddenIds", () => {
new Set([2]),
toggles(),
);
expect(hidden).toEqual(new Set([2, 100, 150, 200, 300, 400]));
expect(hidden).toEqual(new Set([2, 100, 150, 200, 300]));
});
test("battle / bombing markers have independent toggles", () => {
test("battle markers honour the battleMarkers toggle independently", () => {
const hidden = computeHiddenIds(
categories,
planetDependents,
@@ -250,7 +252,7 @@ describe("computeHiddenIds", () => {
toggles({ battleMarkers: false }),
);
expect(hidden.has(300)).toBe(true);
expect(hidden.has(400)).toBe(false);
expect(hidden.has(150)).toBe(false);
});
test("planet cascade and category toggle compose without duplicates", () => {
@@ -262,7 +264,7 @@ describe("computeHiddenIds", () => {
);
// 300 is already present from the cascade; the category toggle
// re-adds it but Set semantics dedupe.
expect(hidden).toEqual(new Set([2, 100, 150, 200, 300, 400]));
expect(hidden).toEqual(new Set([2, 100, 150, 200, 300]));
});
});