feat(ui): F8-12 — map polish (zoom invariance, labels, selection, soft radius) (#55)
* 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:
@@ -177,6 +177,15 @@ bottom-tabs bar.
|
||||
/>
|
||||
<span>{i18n.t("game.map.toggles.unreachable_planets")}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
data-testid="map-toggles-planet-names"
|
||||
checked={store.mapToggles.planetNames}
|
||||
onchange={(e) => setFlag("planetNames", e)}
|
||||
/>
|
||||
<span>{i18n.t("game.map.toggles.planet_names")}</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{i18n.t("game.map.toggles.section.view")}</legend>
|
||||
|
||||
@@ -26,12 +26,13 @@ preference the store already manages.
|
||||
import {
|
||||
createRenderer,
|
||||
minScaleNoWrap,
|
||||
type PlanetOutlineSpec,
|
||||
type RendererHandle,
|
||||
} from "../../map/index";
|
||||
import { buildCargoRouteLines } from "../../map/cargo-routes";
|
||||
import { buildPlanetLabels } from "../../map/labels";
|
||||
import { buildPendingSendLines } from "../../map/pending-send-routes";
|
||||
import { computeReachCircles } from "../../map/reach-circles";
|
||||
import { computeSelectionRing } from "../../map/selection-ring";
|
||||
import { reachStore } from "$lib/calculator/reach.svelte";
|
||||
import { theme as themeStore } from "$lib/theme/theme.svelte";
|
||||
import {
|
||||
@@ -216,6 +217,7 @@ preference the store already manages.
|
||||
void toggles.cargoRoutes;
|
||||
void toggles.battleMarkers;
|
||||
void toggles.bombingMarkers;
|
||||
void toggles.planetNames;
|
||||
void toggles.visibleHyperspace;
|
||||
|
||||
// Subscribe to the calculator's published reach so the rings
|
||||
@@ -253,11 +255,9 @@ preference the store already manages.
|
||||
reachOrigin === null
|
||||
? ""
|
||||
: `${reachOrigin.x},${reachOrigin.y},${reachStore.speedPerTurn}`;
|
||||
const selectedPlanetId =
|
||||
selection?.selected?.kind === "planet" ? selection.selected.id : null;
|
||||
const extrasFingerprint =
|
||||
`cr=${toggles.cargoRoutes ? "1" : "0"}|hp=${hiddenPlanetFingerprint}|` +
|
||||
`reach=${reachFingerprint}|sel=${selectedPlanetId ?? ""}|` +
|
||||
`reach=${reachFingerprint}|` +
|
||||
computeRoutesFingerprint(report.routes) +
|
||||
"|" +
|
||||
computePendingSendFingerprint(draftCommands, draftStatuses);
|
||||
@@ -363,19 +363,7 @@ preference the store already manages.
|
||||
palette,
|
||||
)
|
||||
: [];
|
||||
const selectedPlanetId =
|
||||
selection?.selected?.kind === "planet" ? selection.selected.id : null;
|
||||
const selectionRing = computeSelectionRing(
|
||||
report.planets,
|
||||
selectedPlanetId,
|
||||
palette,
|
||||
);
|
||||
return [
|
||||
...cargo,
|
||||
...pending,
|
||||
...reach,
|
||||
...(selectionRing === null ? [] : [selectionRing]),
|
||||
];
|
||||
return [...cargo, ...pending, ...reach];
|
||||
}
|
||||
|
||||
function applyVisibilityState(
|
||||
@@ -394,6 +382,55 @@ preference the store already manages.
|
||||
const fogCircles = computeFogCircles(report, toggles);
|
||||
currentFogCircles = fogCircles;
|
||||
handle.setVisibilityFog(fogCircles);
|
||||
applyPlanetLabels(report, toggles);
|
||||
}
|
||||
|
||||
function applyPlanetLabels(
|
||||
report: NonNullable<GameStateStore["report"]>,
|
||||
toggles: MapToggles,
|
||||
): void {
|
||||
if (handle === null) return;
|
||||
const labels = buildPlanetLabels(report, {
|
||||
showNames: toggles.planetNames,
|
||||
});
|
||||
const selectedPlanetId =
|
||||
selection?.selected?.kind === "planet" ? selection.selected.id : null;
|
||||
handle.setPlanetLabels(labels, selectedPlanetId);
|
||||
applyPlanetOutlines(report, toggles, selectedPlanetId);
|
||||
}
|
||||
|
||||
function applyPlanetOutlines(
|
||||
report: NonNullable<GameStateStore["report"]>,
|
||||
toggles: MapToggles,
|
||||
selectedPlanetId: number | null,
|
||||
): void {
|
||||
if (handle === null) return;
|
||||
const palette = mountedPalette ?? DARK_THEME;
|
||||
const outlines: PlanetOutlineSpec[] = [];
|
||||
// Bombing outline (F8-12 / #30): every bombed planet gets the
|
||||
// damaged / wiped accent painted around its disc. The
|
||||
// `bombingMarkers` toggle hides the visual cue while leaving
|
||||
// the data intact.
|
||||
if (toggles.bombingMarkers) {
|
||||
for (const bombing of report.bombings) {
|
||||
if (bombing.planetNumber === selectedPlanetId) continue;
|
||||
outlines.push({
|
||||
planetNumber: bombing.planetNumber,
|
||||
color: bombing.wiped
|
||||
? palette.bombingWiped
|
||||
: palette.bombingDamaged,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Selection outline overrides bombing on the same planet so the
|
||||
// player can always tell which one is currently focused.
|
||||
if (selectedPlanetId !== null) {
|
||||
outlines.push({
|
||||
planetNumber: selectedPlanetId,
|
||||
color: palette.selectionAccent,
|
||||
});
|
||||
}
|
||||
handle.setPlanetOutlines(outlines);
|
||||
}
|
||||
|
||||
async function runSerializedMount(
|
||||
@@ -718,30 +755,9 @@ preference the store already manages.
|
||||
// current selection. The Phase 19 ship-group surface dispatches
|
||||
// through the same `hit-test` plumbing — the hitLookup map keyed
|
||||
// by primitive id resolves a hit back to either a planet or a
|
||||
// ship-group selection variant.
|
||||
// scrollToBombingRow waits for the report's bombing row for the
|
||||
// given planet to mount, then scrolls it into view. The map context
|
||||
// menu switches to the report view through a store mutation, so the
|
||||
// section renders on a later frame; a short bounded poll bridges
|
||||
// that gap without coupling the map to the report's render timing.
|
||||
function scrollToBombingRow(planet: number): void {
|
||||
if (typeof document === "undefined") return;
|
||||
let attempts = 60;
|
||||
const tick = (): void => {
|
||||
const row = document.querySelector(
|
||||
`[data-testid="report-bombing-row"][data-planet="${planet}"]`,
|
||||
);
|
||||
if (row instanceof HTMLElement) {
|
||||
row.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
return;
|
||||
}
|
||||
attempts -= 1;
|
||||
if (attempts <= 0) return;
|
||||
requestAnimationFrame(tick);
|
||||
};
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
// ship-group selection variant. F8-12 / #30 retired the separate
|
||||
// bombing-ring click; bombing → report navigation now starts in
|
||||
// the inspector via `scrollToBombingRow` (`lib/report-nav.ts`).
|
||||
function handleMapClick(cursorPx: { x: number; y: number }): void {
|
||||
if (handle === null || store?.report === undefined || store.report === null) {
|
||||
return;
|
||||
@@ -768,15 +784,6 @@ preference the store already manages.
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "bombing": {
|
||||
activeView.select("report");
|
||||
// The report sections render reactively after the view
|
||||
// switches above, so there is no navigation promise to
|
||||
// await; poll a bounded number of animation frames for
|
||||
// the bombing row, then scroll it into view.
|
||||
scrollToBombingRow(target.planet);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user