diff --git a/ui/frontend/src/map/render.ts b/ui/frontend/src/map/render.ts index 7fb721d..ea79376 100644 --- a/ui/frontend/src/map/render.ts +++ b/ui/frontend/src/map/render.ts @@ -515,6 +515,24 @@ export async function createRenderer(opts: RendererOptions): Promise, + wrap: WrapMode, + ): string => { + const parts: string[] = [wrap]; + for (const c of circles) { + parts.push(`${c.x};${c.y};${c.radius}`); + } + return parts.join("|"); + }; const applyHiddenStateTo = (id: PrimitiveID, list: Graphics[]): void => { const visible = !hiddenIds.has(id); for (const g of list) g.visible = visible; @@ -532,6 +550,10 @@ export async function createRenderer(opts: RendererOptions): Promise { for (const c of copies) { const g = new Graphics(); + // F8-12 perf round 4: cull off-screen primitives so a + // 9 × N scene graph (700 planets → 6300 disc Graphics on a + // legacy report) doesn't drag the per-frame paint cost. + g.cullable = true; drawPrimitiveInto(prim, g); c.addChild(g); let list = primitiveGraphics.get(prim.id); @@ -650,29 +672,22 @@ export async function createRenderer(opts: RendererOptions): Promise { - // Text colours flip on selection so the legend reads on the - // inverse-fill frame. + // `paintLabelLayout` keeps the label container's hit area + line + // positions in sync with the current text visibility / content. It + // never mutates `style.fill`, so Pixi does not invalidate the + // underlying Text textures — this is the path the F8-12 / #29 + // `planetNames` toggle walks for all 700+ planets and absolutely + // has to stay cheap. The selection-frame update is split into + // `paintLabelSelection` below because that one *does* need to + // touch fills. + const paintLabelLayout = (entry: LabelGfx): void => { const nameVisible = entry.nameText !== null && entry.nameText.visible; - const nameFill = isSelected ? theme.labelInverseText : theme.labelText; - const numberFill = isSelected - ? theme.labelInverseText - : nameVisible - ? theme.labelMuted - : theme.labelText; - if (entry.nameText !== null) { - entry.nameText.style.fill = nameFill; - } - entry.numberText.style.fill = numberFill; const nameHeight = nameVisible ? entry.nameText!.height : 0; const numberHeight = entry.numberText.height; const totalTextHeight = 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( nameVisible ? entry.nameText!.width : 0, entry.numberText.width, @@ -685,24 +700,85 @@ export async function createRenderer(opts: RendererOptions): Promise