ui/phase-9: PixiJS map renderer with torus and no-wrap modes

Stand up the vector map renderer in ui/frontend/src/map/ on top of
PixiJS v8 + pixi-viewport@^6. Torus mode renders nine container
copies for seamless wrap; no-wrap mode pins the camera at world
bounds and centres on an axis when the viewport exceeds the world
along that axis. Hit-test is a brute-force pass with deterministic
[-priority, distSq, kindOrder, id] ordering and torus-shortest
distance, validated by hand-built unit cases.

The development playground at /__debug/map exposes a window
debug surface for the Playwright spec, which forces WebGPU on
chromium-desktop, WebGL on webkit-desktop, and accepts the
auto-picked backend on mobile projects.

Algorithm spec lives in ui/docs/renderer.md, which also pins the
new deprecation status of galaxy/client (the entire Fyne client
module, including client/world). client/world/README.md and the
Phase 9 stub in ui/PLAN.md gain matching deprecation banners.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-08 14:06:23 +02:00
parent 9d2504c42d
commit db415f8aa4
17 changed files with 2064 additions and 41 deletions
+59 -40
View File
@@ -71,13 +71,16 @@ The intended v1 architecture is:
- Pre-production migration rule from the project root applies: schema
changes are inlined into the existing init schema rather than
producing new migrations; clean rebuilds on every checkout.
- The existing `client/` package is deprecated. New code does not import
from it. Existing types in `pkg/model/client/` are not migrated; UI
types are written from scratch in `ui/core/types/` as needed.
- The `client/world/` algorithm is treated as a reference description
for the new TypeScript renderer. Tile-based spatial indexing is
intentionally omitted in the first iteration; PixiJS native culling
and bounds-based hit testing carry the renderer until profiling
- The existing `galaxy/client` Go module is deprecated in full. New
code does not import from it; this includes `client/world/`, which
is no longer the reference algorithm for the TypeScript renderer.
Existing types in `pkg/model/client/` are not migrated; UI types
are written from scratch in `ui/core/types/` as needed.
- The TypeScript map renderer is specified in `ui/docs/renderer.md`,
derived from the renderer's own requirements rather than from any
earlier Go code. Tile-based spatial indexing is intentionally
omitted in the first iteration; PixiJS native culling and
bounds-based hit testing carry the renderer until profiling
proves otherwise.
- Game math that must stay synchronised between server and client lives
in `pkg/calc/`. The UI client never duplicates calc functions; instead
@@ -949,9 +952,9 @@ Targeted tests (delivered):
invitation removes card and adds the game to My Games. Phase 7
auth flow now also runs over the FlatBuffers wire.
## Phase 9. Map Renderer with Fixture Data
## ~~Phase 9. Map Renderer with Fixture Data~~
Status: pending.
Status: done.
Goal: stand up the PixiJS map renderer with pan/zoom, primitive
drawing, torus wrap behaviour and bounded-plane (no-wrap) mode against
@@ -961,49 +964,65 @@ a deferred nicety.
Artifacts:
- `ui/frontend/src/map/world.ts` data model (`Point2D`, `Primitive`,
`Style`, theme bindings) with fixed-point coordinate handling
- `ui/frontend/src/map/render.ts` PixiJS scene graph: background
layer, primitive container, viewport pan/zoom, torus wrap copies,
dual WebGPU/WebGL backend selection
- `ui/frontend/src/map/hit-test.ts` PixiJS-native hit test wrapping
`eventMode` and per-primitive hit slop
- `ui/frontend/src/map/world.ts` data model (`Primitive` =
`Point | Circle | Line`, `Style`, single-theme bindings) over plain
float64 world coordinates; the renderer is a vector renderer and
Pixi's transform pipeline owns the world→screen mapping
- `ui/frontend/src/map/math.ts` geometry primitives:
`torusShortestDelta`, `distSqPointToSegment`, `clamp`, and
`screenToWorld`/`worldToScreen` round-trip transforms
- `ui/frontend/src/map/render.ts` PixiJS v8 scene graph driven by
`pixi-viewport@^6` for pan/zoom/pinch with WebGPU/WebGL backend
selection via `Application.init({ preference })`; torus wrap is
rendered through nine container copies at `(±W, 0) × (±H, 0)`
- `ui/frontend/src/map/hit-test.ts` brute-force hit-test pass over
the world primitives with `[-priority, distSq, kindOrder, id]`
ordering and torus-shortest distance in `'torus'` mode
- `ui/frontend/src/map/no-wrap.ts` camera clamp helpers
(`CorrectCameraZoom`, `ClampCameraNoWrapViewport`,
`ClampRenderParamsNoWrap`, `PivotZoomCameraNoWrap`) for bounded
plane mode
- `ui/frontend/src/routes/playground/+page.svelte` development page
rendering a fixture world with a mode switch between torus and
no-wrap for visual verification
- topic doc `ui/docs/renderer.md` describing departures from the
Go reference algorithm in `client/world/`, the rationale for
skipping tile-based spatial indexing, and the no-wrap semantics
(`clampCameraNoWrap`, `minScaleNoWrap`, `pivotZoom`) for bounded
plane mode; `pixi-viewport`'s `clamp`/`clampZoom` plugins are
used at the renderer level with a centring hook on `'moved'` so
the viewport-larger-than-world case stays centred
- `ui/frontend/src/map/fixtures.ts` deterministic 1000-primitive
sample world used by the playground and by manual perf checks
- `ui/frontend/src/routes/__debug/map/+page.svelte` development page
rendering the fixture world with a mode switch between torus and
no-wrap, plus a `window.__galaxyMap` debug surface for tests
- topic doc `ui/docs/renderer.md` specifying the data model,
hit-test math, torus copy rule, no-wrap camera semantics, and
the deprecation status of `galaxy/client`
Dependencies: Phase 1.
Acceptance criteria:
- a 1000-primitive fixture world pans and zooms at 60 fps on a
mid-range laptop with WebGPU and falls back cleanly to WebGL in
both torus and no-wrap modes;
- hit testing returns the same primitive as the reference Go algorithm
on a shared set of fixture cursor positions, in both modes;
- torus wrap renders all four corner copies correctly across the
viewport edges;
- a 1000-primitive fixture world pans and zooms on a mid-range
laptop with WebGPU, falling back to WebGL when WebGPU is
unavailable, in both torus and no-wrap modes; the 60 fps target
is documented in `ui/docs/renderer.md` as a manual gate, not a
CI assertion (CI runners vary too much in CPU/GPU);
- hit testing returns the expected primitive on a hand-built
fixture set covering wrap copies, line slop, ring vs filled
circles, ordering, and zoom-dependent slop;
- torus wrap renders all relevant corner copies correctly across
the viewport edges;
- no-wrap mode clamps the camera at world boundaries; pivot zoom
keeps the world point under the cursor stable; viewport never
becomes larger than the world.
Targeted tests:
- Vitest unit tests for fixed-point math, torus-shortest distance,
no-wrap clamps, no-wrap pivot zoom invariants;
- Vitest hit-test parity tests against fixtures derived from the Go
reference, covering both torus and no-wrap fixtures;
- Playwright visual smoke test of the playground page in
`chromium-desktop` and `webkit-desktop`, exercising mode switch
torus → no-wrap and back, and verifying camera clamp behaviour at
bounded-plane edges.
- Vitest unit tests for geometry primitives, torus-shortest
distance, no-wrap clamps, pivot-zoom invariants
(`tests/map-math.test.ts`, `tests/map-no-wrap.test.ts`);
- Vitest hit-test cases for every rule in the algorithm spec
(`tests/map-hit-test.test.ts`, ~22 cases);
- Playwright visual smoke test of the playground page across all
four configured projects (`chromium-desktop` forces WebGPU,
`webkit-desktop` forces WebGL, mobile projects auto-pick),
exercising mode switch torus → no-wrap and back, wheel zoom,
no-wrap clamp after a drag past the edge, and live hit-test
plumbing (`tests/e2e/playground-map.spec.ts`).
## Phase 10. In-Game Shell with View-Replacement Skeleton