680ebac919
* 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>
84 lines
2.8 KiB
TypeScript
84 lines
2.8 KiB
TypeScript
// Coverage for the F8-12 sizing helpers in src/map/world.ts:
|
||
// `displayPointRadiusWorld` (the union of the pixel-space and the
|
||
// softened-by-zoom rules) and `displayStrokeWidthWorld` (pixel-space
|
||
// stroke widths). Both are pure math, so this file stays Pixi-free.
|
||
|
||
import { describe, expect, test } from "vitest";
|
||
|
||
import {
|
||
DEFAULT_POINT_RADIUS_PX,
|
||
PLANET_SIZE_ZOOM_ALPHA,
|
||
displayPointRadiusWorld,
|
||
displayStrokeWidthWorld,
|
||
} from "../src/map/world";
|
||
|
||
describe("displayPointRadiusWorld — pixel-space (pointRadiusPx)", () => {
|
||
test("returns pixel size divided by scale at scale=1", () => {
|
||
expect(displayPointRadiusWorld({ pointRadiusPx: 5 }, 1, 0.2)).toBe(5);
|
||
});
|
||
|
||
test("shrinks the world footprint as zoom grows", () => {
|
||
expect(displayPointRadiusWorld({ pointRadiusPx: 6 }, 3, 0.2)).toBeCloseTo(2);
|
||
});
|
||
|
||
test("falls back to DEFAULT_POINT_RADIUS_PX when the style is bare", () => {
|
||
expect(displayPointRadiusWorld({}, 2, 0.2)).toBeCloseTo(
|
||
DEFAULT_POINT_RADIUS_PX / 2,
|
||
);
|
||
});
|
||
|
||
test("zero-scale guard returns the raw pixel size", () => {
|
||
expect(displayPointRadiusWorld({ pointRadiusPx: 4 }, 0, 0.2)).toBe(4);
|
||
});
|
||
});
|
||
|
||
describe("displayPointRadiusWorld — softened by zoom (pointRadiusWorld)", () => {
|
||
test("at scale=scaleRef the visible radius equals the base radius", () => {
|
||
const radius = displayPointRadiusWorld(
|
||
{ pointRadiusWorld: 6 },
|
||
0.2,
|
||
0.2,
|
||
);
|
||
expect(radius).toBeCloseTo(6);
|
||
});
|
||
|
||
test("zooming in grows the radius sub-linearly", () => {
|
||
const r1 = displayPointRadiusWorld({ pointRadiusWorld: 6 }, 0.2, 0.2);
|
||
const r10 = displayPointRadiusWorld({ pointRadiusWorld: 6 }, 2.0, 0.2);
|
||
// On-screen pixel size grows by scale^α (α = 0.33) instead of
|
||
// linearly: 10x zoom → ~10^0.33 ≈ 2.15x growth.
|
||
const onScreenAt1 = r1 * 0.2;
|
||
const onScreenAt10 = r10 * 2.0;
|
||
expect(onScreenAt10 / onScreenAt1).toBeCloseTo(
|
||
Math.pow(10, PLANET_SIZE_ZOOM_ALPHA),
|
||
3,
|
||
);
|
||
});
|
||
|
||
test("ignores pointRadiusPx when pointRadiusWorld is set", () => {
|
||
const r = displayPointRadiusWorld(
|
||
{ pointRadiusPx: 99, pointRadiusWorld: 4 },
|
||
0.4,
|
||
0.2,
|
||
);
|
||
// World radius is the base softened by (0.4/0.2)^(α-1).
|
||
expect(r).toBeCloseTo(4 * Math.pow(2, PLANET_SIZE_ZOOM_ALPHA - 1), 4);
|
||
});
|
||
});
|
||
|
||
describe("displayStrokeWidthWorld", () => {
|
||
test("returns width / scale at any zoom", () => {
|
||
expect(displayStrokeWidthWorld({ strokeWidthPx: 2 }, 1)).toBe(2);
|
||
expect(displayStrokeWidthWorld({ strokeWidthPx: 2 }, 4)).toBeCloseTo(0.5);
|
||
expect(displayStrokeWidthWorld({ strokeWidthPx: 2 }, 0.5)).toBeCloseTo(4);
|
||
});
|
||
|
||
test("falls back to 1 when strokeWidthPx is omitted", () => {
|
||
expect(displayStrokeWidthWorld({}, 2)).toBeCloseTo(0.5);
|
||
});
|
||
|
||
test("zero-scale guard returns the raw pixel value", () => {
|
||
expect(displayStrokeWidthWorld({ strokeWidthPx: 3 }, 0)).toBe(3);
|
||
});
|
||
});
|