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
+10 -11
View File
@@ -280,20 +280,19 @@ describe("hitTest — empty results and scale", () => {
expect(ids(w, "torus", cam05, cursorOver(516, 500, cam05))).toBe(null);
});
test("pointRadiusWorld scales softly with zoom (F8-12 / #31)", () => {
// world 1000×1000, viewport 200×200 → scaleRef = 0.2 (every
// world unit becomes 0.2 px on screen at the "whole world fits"
// zoom). PLANET_SIZE_ZOOM_ALPHA is 0.33: r_display =
// r_base * (scale / scaleRef)^(α - 1).
test("pointRadiusBasePx scales softly with zoom (F8-12 / #31)", () => {
// world 1000×1000, viewport 200×200 → scaleRef = 0.2. At
// scale=0.5 the on-screen pixel size is
// basePx * (scale/scaleRef)^α
// → 6 * (0.5/0.2)^0.33 ≈ 6 * 1.354 ≈ 8.13 px. In world units
// that becomes ≈ 16.27, plus slop 4/0.5 = 8 → threshold ≈ 24.27.
const cam05 = camAt(500, 500, 0.5);
const wBase = new World(1000, 1000, [
point(1, 500, 500, { style: { pointRadiusWorld: 6 } }),
point(1, 500, 500, { style: { pointRadiusBasePx: 6 } }),
]);
// At scale=0.5 the softening factor is (0.5/0.2)^(0.33-1) ≈ 0.554.
// Visible radius ≈ 3.32 world units, slop 8, threshold ≈ 11.32.
expect(ids(wBase, "torus", cam05, cursorOver(510, 500, cam05))).toBe(1);
// Cursor 12 world units away exceeds the threshold.
expect(ids(wBase, "torus", cam05, cursorOver(512, 500, cam05))).toBe(null);
expect(ids(wBase, "torus", cam05, cursorOver(520, 500, cam05))).toBe(1);
// Cursor 26 world units away exceeds the threshold (~24.27).
expect(ids(wBase, "torus", cam05, cursorOver(526, 500, cam05))).toBe(null);
});
});