Files
galaxy-game/client/world/README.md
T
Ilia Denisov db415f8aa4 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>
2026-05-08 14:06:23 +02:00

166 lines
7.4 KiB
Markdown

# World rendering package
> **Deprecated.** This package belongs to the deprecated
> `galaxy/client` Fyne client. New code must not import it. The
> active map renderer lives in `ui/frontend/src/map/` (TypeScript
> + PixiJS), with its specification in `ui/docs/renderer.md`. The
> sources here remain for historical context only and are not the
> reference algorithm for the new renderer.
## Purpose
`world` is the client-side map model and renderer for a 2D world that normally
behaves like a torus. It owns:
- primitive storage (`Point`, `Line`, `Circle`)
- world-space indexing for render and hit-test queries
- theme and style resolution
- full-frame and incremental rendering onto an expanded canvas
- no-wrap helpers used by the UI when torus scrolling is disabled
The package does not own UI widgets, event loops, or camera policy beyond the
helpers exposed for zoom/clamp calculations.
## Symbol Map
- World creation and mutation: `NewWorld`, `AddPoint`, `AddLine`, `AddCircle`, `Remove`, `Reindex`
- Viewport/index lifecycle: `IndexOnViewportChange`, `SetCircleRadiusScaleFp`
- Rendering: `Render`, `RenderParams`, `RenderOptions`, `PrimitiveDrawer`, `GGDrawer`
- No-wrap camera helpers: `CorrectCameraZoom`, `ClampCameraNoWrapViewport`, `ClampRenderParamsNoWrap`, `PivotZoomCameraNoWrap`
- Hit testing: `HitTest`, `Hit`, `PrimitiveKind`
- Styling and themes: `Style`, `StyleOverride`, `StyleTable`, `StyleTheme`, `DefaultTheme`, `ThemeLight`, `ThemeDark`
## Coordinate Model
- World geometry is stored in fixed-point integers.
- `SCALE == 1000`, so `1.0` world units are represented as `1000`.
- Primitive coordinates, radii, world dimensions, and camera positions use `world-fixed` units.
- Viewport and canvas sizes use integer `canvas px`.
- Rectangles in world space and canvas space are treated as half-open intervals:
`[minX, maxX) x [minY, maxY)`.
- `RenderParams` describes the visible viewport, but rendering happens on the
expanded canvas:
- `canvasWidthPx = viewportWidthPx + 2*marginXPx`
- `canvasHeightPx = viewportHeightPx + 2*marginYPx`
- The camera always points to the center of the visible viewport, not the center
of the expanded canvas.
## Data Model
- `World` stores torus dimensions `W` and `H` in fixed-point units.
- `MapItem` is implemented by `Point`, `Line`, and `Circle`.
- `PrimitiveID` is allocated by `World` and may be reused after removal.
- Each primitive carries:
- geometry in fixed-point world coordinates
- `Priority` for deterministic draw order inside a tile
- resolved `StyleID`
- theme binding metadata (`Base`, `Override`, `Class`)
- optional per-primitive hit slop in pixels
- Themes resolve base styles per primitive kind, then optional class overrides,
then optional user `StyleOverride`.
- Explicit `StyleID` bypasses theme-relative recomputation across theme changes.
## Spatial Index Lifecycle
- Rendering and hit testing depend on the grid index stored in `World.grid`.
- `IndexOnViewportChange` must be called after viewport size or zoom changes.
- The grid cell size is derived from the current visible world span:
- start from roughly `visibleMin / 8`
- clamp into `[16*SCALE, 512*SCALE]`
- `AddPoint`, `AddLine`, `AddCircle`, `Remove`, `SetCircleRadiusScaleFp`, and
`Reindex` mark the index dirty and rebuild it automatically when the last
viewport/zoom state is known.
- Circle indexing uses the effective radius after `circleRadiusScaleFp` is applied.
- Line indexing uses the torus-shortest representation and indexes its wrapped
bounding boxes rather than exact rasterized coverage.
## Render Pipeline
`Render` follows this sequence:
1. Validate `RenderParams` and resolve background color/theme state.
2. Convert zoom to fixed-point and compute the expanded unwrapped world rect.
3. Split that rect into `WorldTile` segments:
- torus mode uses wrapped tiling
- no-wrap mode intersects against the bounded world once
4. Query the spatial grid per tile and deduplicate candidates per tile by `PrimitiveID`.
5. Build a `RenderPlan` containing:
- tile-to-canvas clip rectangles
- per-tile candidate lists
6. Draw background before primitives.
7. Draw primitives tile-by-tile in deterministic order:
- `Priority` ascending
- primitive kind as stable tie-breaker
- `PrimitiveID` ascending
8. For wrapped rendering:
- points and circles emit only the torus copies that intersect the current tile
- lines are split into torus-shortest canonical segments before projection
## Incremental Pan Rendering
- `Render` first tries incremental pan reuse through `ComputePanShiftPx` and
`PlanIncrementalPan`.
- If only camera pan changed and the shift stays inside the configured margins:
- existing pixels are moved with `PrimitiveDrawer.CopyShift`
- newly exposed strips become dirty rects
- dirty rects are cleared, background-redrawn, and clipped primitive redraw is applied
- If geometry changed in a way that breaks reuse, rendering falls back to full redraw.
- Theme changes, circle radius scale changes, and explicit `ForceFullRedrawNext`
reset incremental state.
## No-Wrap Behavior
When `RenderOptions.DisableWrapScroll == true`, the world is treated as a bounded
plane instead of a torus.
- `CorrectCameraZoom` prevents the visible viewport from becoming larger than the world.
- `ClampCameraNoWrapViewport` clamps the camera so the viewport remains inside the world.
- `ClampRenderParamsNoWrap` applies the same rule directly to `RenderParams`.
- `PivotZoomCameraNoWrap` keeps the world point under the cursor stable while zoom changes.
Margins are ignored by viewport clamp on purpose so panning remains usable even
when the expanded canvas extends beyond the world bounds.
## Hit Testing
- `HitTest` expects the grid to be built already.
- Cursor coordinates are passed in viewport pixels relative to the viewport top-left.
- The query path is:
1. convert cursor position into world-fixed coordinates
2. clamp or wrap based on no-wrap mode
3. query a conservative grid search box using default hit slop
4. run exact per-primitive hit checks
- Point hits use disc distance.
- Circle hits distinguish between filled circles and stroke-only rings.
- Line hits use the same torus-shortest segment decomposition as rendering.
- Final ranking is:
- `Priority` descending
- squared distance ascending
- primitive kind ascending
- `PrimitiveID` ascending
## UI Integration Checklist
Typical UI flow:
1. Create the world with `NewWorld`.
2. Add primitives and optional styles/themes.
3. Before each render, compute the current viewport size in pixels.
4. Call `CorrectCameraZoom` when UI zoom changes.
5. Call `IndexOnViewportChange` when viewport size or zoom changes.
6. If no-wrap mode is enabled, call `ClampRenderParamsNoWrap`.
7. Render into a `PrimitiveDrawer` with `Render`.
8. Reuse the same `RenderParams` snapshot for `HitTest`.
The `client` package in this repository follows exactly that pattern.
## Important Invariants and Limits
- `Render` and `HitTest` require the grid to be initialized; otherwise they return `errGridNotBuilt`.
- The package assumes single-goroutine access to hot render scratch buffers stored in `World`.
- `RenderScheduler` is only a coalescing example. It is not a license to call
`Render` on arbitrary background goroutines in real UI code.
- `PrimitiveDrawer` receives final canvas coordinates only; all torus math stays inside `world`.
- Background anchoring can be viewport-relative or world-relative, but dirty redraws
always use the same anchoring logic as full redraws.