Files
galaxy-game/client/world
2026-03-17 16:27:14 +02:00
..
2026-03-17 12:48:05 +03:00
2026-03-17 12:48:05 +03:00
2026-03-17 12:48:05 +03:00
2026-03-08 15:31:17 +02:00
2026-03-17 12:48:05 +03:00
2026-03-17 12:48:05 +03:00
2026-03-17 12:48:05 +03:00
2026-03-17 12:48:05 +03:00
2026-03-17 12:48:05 +03:00
2026-03-17 16:27:14 +02:00
2026-03-17 12:48:05 +03:00
2026-03-17 12:48:05 +03:00
2026-03-17 12:48:05 +03:00
2026-03-17 16:27:14 +02:00

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.