world refactor
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
# World rendering package
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user