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
+22 -58
View File
@@ -1,6 +1,8 @@
// Phase 27 battle and bombing markers on the map.
//
// Two visual markers per planet:
// Phase 27 battle markers on the map. Bombing markers used to live
// here as a separate ring primitive but F8-12 / #30 turned them into
// a planet-outline overlay drawn by `render.ts.setPlanetOutlines`,
// driven from `map.svelte`. The remaining surface here is the battle
// X-cross:
//
// * Battle marker — an X cross drawn through the corners of the
// square that circumscribes the planet circle. Two yellow
@@ -8,18 +10,10 @@
// shots: 1 shot → 1px, 100+ shots → 5px (capped). Clicking
// either line opens the Battle Viewer for the corresponding
// UUID.
// * Bombing marker — a thin stroke-only circle slightly larger
// than the planet circle. Yellow on damaged planets, red on
// wiped planets. Clicking it deep-links to the bombings row in
// the Reports view for the planet number.
//
// Both markers are wired into `state-binding.ts` so they live in the
// same `world` / `hitLookup` plumbing as planets and ship groups.
import type { GameReport, ReportPlanet } from "../api/game-state";
import {
DARK_THEME,
type CirclePrim,
type LinePrim,
type Primitive,
type PrimitiveID,
@@ -27,20 +21,17 @@ import {
type Theme,
} from "./world";
/** Battle and bombing marker primitive ids use a high-bit prefix to
* avoid colliding with planet numbers or cargo-route line ids. */
/** Battle marker primitive ids use a high-bit prefix to avoid
* colliding with planet numbers or cargo-route line ids. */
export const BATTLE_MARKER_ID_PREFIX = 0xa0000000;
export const BOMBING_MARKER_ID_PREFIX = 0xc0000000;
const PLANET_RADIUS_WORLD = 6;
const BOMBING_RING_RADIUS = PLANET_RADIUS_WORLD + 3;
const BATTLE_CROSS_HALF = PLANET_RADIUS_WORLD + 2;
/** Battle marker priority sits between planets (1..4) and cargo
* routes; the cross is over the planet but loses clicks against the
* planet glyph itself. */
const BATTLE_MARKER_PRIORITY = 9;
const BOMBING_MARKER_PRIORITY = 10;
const BATTLE_LINE_INDEX_A = 0;
const BATTLE_LINE_INDEX_B = 1;
@@ -51,21 +42,16 @@ export interface BattleMarkerTarget {
planet: number;
}
export interface BombingMarkerTarget {
kind: "bombing";
planet: number;
}
export type MarkerTarget = BattleMarkerTarget | BombingMarkerTarget;
export type MarkerTarget = BattleMarkerTarget;
/**
* MarkerCategory tags every emitted primitive with the toggleable
* surface it belongs to so the Phase 29 hide-set machinery can flip
* each independently. Battles and bombings have their own toggles —
* a player can hide the bombing rings while keeping the battle
* crosses visible.
* each independently. Battle markers are the only category left here;
* the `bombingMarker` toggle now hides the planet-outline overlay
* built in `map.svelte.applyPlanetOutlines` (F8-12 / #30).
*/
export type MarkerCategory = "battleMarker" | "bombingMarker";
export type MarkerCategory = "battleMarker";
export interface BuildMarkersResult {
primitives: Primitive[];
@@ -92,11 +78,13 @@ export function battleMarkerStrokeWidth(shots: number): number {
}
/**
* buildBattleAndBombingMarkers emits battle and bombing marker
* primitives plus a hit-lookup mapping for the current-turn report.
* Battles whose planet is not visible (e.g. observer-only without a
* report.planets entry) are skipped — they have no on-map location
* to anchor against.
* buildBattleAndBombingMarkers emits battle X-cross primitives plus a
* hit-lookup mapping for the current-turn report. Battles whose
* planet is not visible (e.g. observer-only without a report.planets
* entry) are skipped — they have no on-map location to anchor
* against. Bombing visuals are no longer produced here (F8-12 / #30);
* the renderer paints them as a planet-outline overlay driven from
* `map.svelte.applyPlanetOutlines`.
*/
export function buildBattleAndBombingMarkers(
report: GameReport,
@@ -167,32 +155,8 @@ export function buildBattleAndBombingMarkers(
addDependent(battle.planet, lineB.id);
}
for (let i = 0; i < report.bombings.length; i++) {
const bombing = report.bombings[i];
const planet = planetByNumber.get(bombing.planetNumber);
if (planet === undefined) continue;
const color = bombing.wiped ? theme.bombingWiped : theme.bombingDamaged;
const style: Style = {
strokeColor: color,
strokeAlpha: 0.9,
strokeWidthPx: 1.5,
};
const id = BOMBING_MARKER_ID_PREFIX | i;
const ring: CirclePrim = {
kind: "circle",
id,
priority: BOMBING_MARKER_PRIORITY,
style,
hitSlopPx: 0,
x: planet.x,
y: planet.y,
radius: BOMBING_RING_RADIUS,
};
primitives.push(ring);
lookup.set(id, { kind: "bombing", planet: bombing.planetNumber });
categories.set(id, "bombingMarker");
addDependent(bombing.planetNumber, id);
}
// Bombing visuals are produced by `setPlanetOutlines` in the
// renderer (F8-12 / #30); the data still lives on
// `report.bombings`, but no primitive is emitted here.
return { primitives, lookup, categories, planetDependents };
}