import { describe, expect, it } from "vitest"; import { MAX_ZOOM, MIN_ZOOM } from "../src/map/camera"; import { childOffset, labelBox, layoutScene, markerAt, planLabels } from "../src/map/layout"; import { buildScene } from "../src/map/scene"; import { mapStyle } from "../src/map/style"; import { samplePlayerState } from "./render"; const NOW = Date.parse("2100-01-01T00:00:00Z"); const view = { width: 1280, height: 800 }; const scene = buildScene(samplePlayerState(), NOW); /** Labels about as wide as the real ones: ~6.5 px a character at 11 px. */ const NAMES = new Map(scene.locations.map((l) => [l.id, l.name])); const labelSize = (id: string) => ({ width: (NAMES.get(id) ?? id).length * 6.5, height: 14 }); const cameraAt = (zoom: number) => ({ zoom, panX: 0, panY: 0 }); const JUPITER = "jupiter-cloud-platform"; const CHILDREN = ["io-sulfur-works", "europa-ice-station"]; describe("the Jupiter system on the map (Step 16.8, decision 0.5.105)", () => { it("draws Io and Europa exactly at their configured offsets from Jupiter, at any zoom", () => { for (const zoom of [MIN_ZOOM, 1, 2.5, 10, MAX_ZOOM]) { const layout = layoutScene(scene, view, cameraAt(zoom)); const at = (id: string) => layout.locations.find((l) => l.id === id)!.center; for (const child of CHILDREN) { const offset = childOffset(child, at(JUPITER), layout.sun.center); expect(at(child).x).toBeCloseTo(at(JUPITER).x + offset.x, 9); expect(at(child).y).toBeCloseTo(at(JUPITER).y + offset.y, 9); expect(Math.hypot(offset.x, offset.y)).toBeCloseTo( mapStyle.childOffsets[child]!.distancePx, ); } } }); it('shows only Jupiter\'s label, with "+2", when zoomed out', () => { const layout = layoutScene(scene, view, cameraAt(1)); const { decorations, markers } = planLabels(layout, labelSize, 1); expect(decorations.get(JUPITER)!.shown).toBe(true); for (const child of CHILDREN) expect(decorations.get(child)!.shown, child).toBe(false); expect(markers).toEqual([expect.objectContaining({ parentId: JUPITER, count: 2 })]); // The marker sits just right of Jupiter's label. const label = labelBox(decorations.get(JUPITER)!.label, labelSize(JUPITER)); expect(markers[0]!.center.x).toBeGreaterThan(label.right); }); it("shows all three labels, without overlapping, once zoomed past the threshold", () => { const zoom = mapStyle.labelDeclutter[JUPITER]!.expandZoom; const layout = layoutScene(scene, view, cameraAt(zoom)); const { decorations, markers } = planLabels(layout, labelSize, zoom); expect(markers).toEqual([]); const family = [JUPITER, ...CHILDREN]; for (const id of family) expect(decorations.get(id)!.shown, id).toBe(true); const boxes = family.map((id) => labelBox(decorations.get(id)!.label, labelSize(id))); for (let i = 0; i < boxes.length; i++) { for (let j = i + 1; j < boxes.length; j++) { const [a, b] = [boxes[i]!, boxes[j]!]; const overlap = a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom; expect(overlap, `${family[i]} / ${family[j]}`).toBe(false); } } }); it('opens the folded labels when "+2" is tapped', () => { const layout = layoutScene(scene, view, cameraAt(1)); const { markers } = planLabels(layout, labelSize, 1); const marker = markerAt(markers, markers[0]!.center)!; expect(marker.parentId).toBe(JUPITER); expect(markerAt(markers, { x: -500, y: -500 })).toBeNull(); const opened = planLabels(layout, labelSize, 1, new Set([JUPITER])); for (const child of CHILDREN) expect(opened.decorations.get(child)!.shown).toBe(true); expect(opened.markers).toEqual([]); }); it("keeps Earth and the Moon labelled while their labels don't collide", () => { const layout = layoutScene(scene, view, cameraAt(1)); const { decorations, markers } = planLabels(layout, labelSize, 1); expect(decorations.get("moon")!.shown).toBe(true); expect(markers.some((m) => m.parentId === "earth")).toBe(false); }); it("gives every place and the marker a tap target of at least 44 px", () => { for (const zoom of [MIN_ZOOM, 1, MAX_ZOOM]) { const layout = layoutScene(scene, view, cameraAt(zoom)); for (const l of layout.locations) expect(l.tap, l.id).toBeGreaterThanOrEqual(44); for (const m of planLabels(layout, labelSize, zoom).markers) { expect(m.tap).toBeGreaterThanOrEqual(44); } } }); it("draws Venus at its own place, not beside another body", () => { const layout = layoutScene(scene, view, cameraAt(1)); const venus = layout.locations.find((l) => l.id === "venus-cloud-city")!; for (const other of layout.locations.filter((l) => l.id !== venus.id)) { expect( Math.hypot(venus.center.x - other.center.x, venus.center.y - other.center.y), ).toBeGreaterThan((venus.size + other.size) / 2); } }); });