diff --git a/ui/frontend/src/map/render.ts b/ui/frontend/src/map/render.ts index b94138d..7fb721d 100644 --- a/ui/frontend/src/map/render.ts +++ b/ui/frontend/src/map/render.ts @@ -584,6 +584,7 @@ export async function createRenderer(opts: RendererOptions): Promise = []; let currentLabelsFingerprint: string | null = null; let currentLabelsSelectedId: number | null = null; + let currentPlanetSetFingerprint: string | null = null; const fingerprintPlanetLabels = ( labels: ReadonlyArray, ): string => { @@ -593,6 +594,14 @@ export async function createRenderer(opts: RendererOptions): Promise, + ): string => { + const ids: number[] = []; + for (const l of labels) ids.push(l.planetNumber); + ids.sort((a, b) => a - b); + return ids.join(","); + }; const LABEL_FONT_SIZE_PX = 11; const LABEL_LINE_GAP_PX = 0; const LABEL_FRAME_PADDING_PX = 3; @@ -638,31 +647,34 @@ export async function createRenderer(opts: RendererOptions): Promise { // Text colours flip on selection so the legend reads on the // inverse-fill frame. + const nameVisible = + entry.nameText !== null && entry.nameText.visible; const nameFill = isSelected ? theme.labelInverseText : theme.labelText; const numberFill = isSelected ? theme.labelInverseText - : entry.nameText !== null + : nameVisible ? theme.labelMuted : theme.labelText; if (entry.nameText !== null) { entry.nameText.style.fill = nameFill; } entry.numberText.style.fill = numberFill; - const nameHeight = entry.nameText?.height ?? 0; + const nameHeight = nameVisible ? entry.nameText!.height : 0; const numberHeight = entry.numberText.height; const totalTextHeight = - nameHeight + (entry.nameText !== null ? LABEL_LINE_GAP_PX : 0) + numberHeight; - entry.numberText.y = entry.nameText !== null ? nameHeight + LABEL_LINE_GAP_PX : 0; + nameHeight + (nameVisible ? LABEL_LINE_GAP_PX : 0) + numberHeight; + entry.numberText.y = nameVisible ? nameHeight + LABEL_LINE_GAP_PX : 0; // Refresh the click hit area to match the label's bounding box // plus the padding from the selection frame (so the player can // click anywhere inside the visible legend, not just the glyphs). const widestText = Math.max( - entry.nameText?.width ?? 0, + nameVisible ? entry.nameText!.width : 0, entry.numberText.width, ); const hitWidth = widestText + LABEL_FRAME_PADDING_PX * 2; @@ -846,10 +858,33 @@ export async function createRenderer(opts: RendererOptions): Promise { + if (entry.nameText !== null) { + const desired = data.name ?? ""; + if (entry.nameText.text !== desired) { + entry.nameText.text = desired; + } + entry.nameText.visible = data.name !== null; + } + if (entry.numberText.text !== data.numberLabel) { + entry.numberText.text = data.numberLabel; + } + }; + const simulatePlanetClick = (planetNumber: number): void => { const prim = pointPrimitivesById.get(planetNumber); if (prim === undefined) return; @@ -959,15 +1008,24 @@ export async function createRenderer(opts: RendererOptions): Promise maxScale) viewport.setZoom(maxScale, true); if (newMode === "no-wrap") { viewport.clamp({ direction: "all" }); viewport.on("moved", enforceCentreWhenLarger); @@ -1451,10 +1509,12 @@ export async function createRenderer(opts: RendererOptions): Promise maxScale) viewport.setZoom(maxScale, true); if (mode === "no-wrap") { enforceCentreWhenLarger(); } @@ -1534,6 +1594,23 @@ function rendererBackendName(r: Renderer): "webgl" | "webgpu" | "canvas" { */ const SMOOTH_CIRCLE_SEGMENTS = 32; +/** + * MIN_VISIBLE_WORLD_AT_MAX_ZOOM caps zoom-in so the longest viewport + * axis never covers fewer than this many world units. Tuned on the + * F8-12 owner-feedback round: 5 keeps individual planet glyphs from + * filling half the screen on either mobile (longest ≈ 412 px → max + * scale ≈ 82) or desktop (longest ≈ 1200 px → max scale ≈ 240) — the + * "useful detail" budget at full zoom is still a 5×5-world tile in + * the centre of the viewport. + */ +const MIN_VISIBLE_WORLD_AT_MAX_ZOOM = 5; + +function maxScaleForViewport(widthPx: number, heightPx: number): number { + const longest = Math.max(widthPx, heightPx); + if (longest <= 0) return Number.POSITIVE_INFINITY; + return longest / MIN_VISIBLE_WORLD_AT_MAX_ZOOM; +} + function traceSmoothCircle( g: Graphics, x: number,