perf(ui): F8-12 — pixel-space planet sizing + single-copy label/outline layers (#55)
Tests · UI / test (push) Has been cancelled
Tests · UI / test (pull_request) Successful in 3m20s

* Planet size formula moves to pixel-space:
  `pointRadiusBasePx = 2 + 2 * cbrt(size / SIZE_NORMALIZER)`. The
  on-screen disc now reads ~4-7 px at the reference zoom regardless
  of how large the world rectangle is — the previous `world-units`
  formulation blew up on small maps and made Source-class planets
  swallow their neighbours.
* Labels + outlines live in the origin copy only. The 9× replication
  across torus copies was the dominant cost on a 100+ planet map
  (Pixi.Text creation + Graphics rebuilds on every zoom step); the
  origin-copy layout is what the camera-wrap listener guarantees
  the user actually sees.
* `setPlanetLabels` and `setPlanetOutlines` skip Pixi-object
  rebuilds when the input fingerprint is unchanged — toggle flips
  and selection changes now keep the existing Text / Graphics
  instances alive and only repaint the affected pieces.
* `renderer.md` updated to the new contract.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-28 00:39:19 +02:00
parent 75a4211373
commit 6996a79286
6 changed files with 218 additions and 167 deletions
+23 -16
View File
@@ -26,11 +26,12 @@ export type WrapMode = "torus" | "no-wrap";
// thickening that the old contract promised but never delivered is
// gone.
//
// `pointRadiusWorld` is the opposite intent: a planet's known
// `size` produces a base radius in world units, and the renderer
// softens its growth with the camera scale through
// `PLANET_SIZE_ZOOM_ALPHA` (F8-12 / #31). When `pointRadiusWorld`
// is set on a `PointPrim`, `pointRadiusPx` is ignored.
// `pointRadiusBasePx` is the opposite intent: a planet's known
// `size` produces a base on-screen pixel radius at the "whole world
// fits" reference zoom, and the renderer grows it sub-linearly with
// the camera scale through `PLANET_SIZE_ZOOM_ALPHA` (F8-12 / #31).
// When `pointRadiusBasePx` is set on a `PointPrim`, `pointRadiusPx`
// is ignored.
export interface Style {
fillColor?: number; // 0xRRGGBB
fillAlpha?: number; // 0..1
@@ -38,7 +39,7 @@ export interface Style {
strokeAlpha?: number; // 0..1
strokeWidthPx?: number; // screen pixels at any zoom
pointRadiusPx?: number; // screen pixels at any zoom (for kind === 'point')
pointRadiusWorld?: number; // world units, softened by PLANET_SIZE_ZOOM_ALPHA
pointRadiusBasePx?: number; // screen pixels at scaleRef, softened by PLANET_SIZE_ZOOM_ALPHA
// strokeDashPx — when set on a `LinePrim`, the line is rendered as
// a dashed pattern whose dash and gap are both this length. When
// unset (or zero), the stroke is solid. Interpreted in world-unit
@@ -231,12 +232,13 @@ export const PLANET_SIZE_ZOOM_ALPHA = 0.33;
/**
* displayPointRadiusWorld returns the world-space radius the renderer
* should draw a `PointPrim` with at the current camera scale. When
* `style.pointRadiusWorld` is set (known-size planets), the radius is
* the base world radius softened by `PLANET_SIZE_ZOOM_ALPHA` relative
* to `scaleRef` — at `scale = scaleRef` it equals the base radius;
* zooming in grows it sub-linearly. Otherwise the radius collapses to
* `pointRadiusPx / cameraScale` so the on-screen disc stays the same
* pixel size regardless of zoom.
* `style.pointRadiusBasePx` is set (known-size planets), the radius
* is the base pixel size at `scaleRef`, grown by
* `(scale / scaleRef)^α` and converted back into world units —
* `α = PLANET_SIZE_ZOOM_ALPHA`. At `scale = scaleRef` the visible
* pixel size equals the base; a 10× zoom-in only grows it ~2.15×.
* Otherwise the radius collapses to `pointRadiusPx / cameraScale` so
* the on-screen disc stays the same pixel size regardless of zoom.
*
* Used by both the renderer (`render.ts:drawPoint`) and the hit-test
* (`hit-test.ts:matchPoint`) so the visible disc and the click zone
@@ -247,12 +249,17 @@ export function displayPointRadiusWorld(
cameraScale: number,
scaleRef: number,
): number {
if (style.pointRadiusWorld !== undefined) {
const softening = Math.pow(cameraScale / scaleRef, PLANET_SIZE_ZOOM_ALPHA - 1);
return style.pointRadiusWorld * softening;
if (cameraScale <= 0) {
return style.pointRadiusBasePx ?? style.pointRadiusPx ?? DEFAULT_POINT_RADIUS_PX;
}
if (style.pointRadiusBasePx !== undefined) {
const refScale = scaleRef > 0 ? scaleRef : cameraScale;
const screenPx =
style.pointRadiusBasePx *
Math.pow(cameraScale / refScale, PLANET_SIZE_ZOOM_ALPHA);
return screenPx / cameraScale;
}
const px = style.pointRadiusPx ?? DEFAULT_POINT_RADIUS_PX;
if (cameraScale <= 0) return px;
return px / cameraScale;
}