Files
galaxy-game/client/world/world.go
T
Ilia Denisov a7793f5416 ui calculator
2026-03-30 19:38:24 +02:00

1015 lines
27 KiB
Go

package world
import (
"errors"
"fmt"
)
var (
errBadCoordinate = errors.New("invalid coordinates")
errBadRadius = errors.New("invalid radius")
errNoSuchObject = errors.New("no such object")
errIDExhausted = errors.New("primitive id exhausted")
)
// indexState stores the viewport-dependent parameters required to rebuild the
// spatial grid after object or style-affecting mutations.
type indexState struct {
initialized bool
viewportW int
viewportH int
zoomFp int
}
// derivedStyleKey identifies one cached derived style by its base style and
// stable override fingerprint.
type derivedStyleKey struct {
base StyleID
fp uint64
}
// World stores torus world dimensions, all registered objects,
// and the grid-based spatial index built for the current viewport settings.
type World struct {
W, H int // Fixed-point world size.
grid [][][]MapItem
cellSize int
rows, cols int
objects map[PrimitiveID]MapItem
styles *StyleTable
theme StyleTheme
themeDefaultLineStyleID StyleID
themeDefaultCircleStyleID StyleID
themeDefaultPointStyleID StyleID
circleRadiusScaleFp int // fixed-point, 1.0 == SCALE
// PrimitiveID allocator state.
nextID PrimitiveID
freeIDs []PrimitiveID
// Index dirty flag for add/remove updates.
indexDirty bool
index indexState
renderState rendererIncrementalState
derivedCache map[derivedStyleKey]StyleID
// scratch buffers for hot render path (single goroutine assumption).
scratchDrawItems []drawItem
scratchWrapShifts []wrapShift
scratchLineSegs []lineSeg
scratchLineSegsTmp []lineSeg
// candidate dedupe scratch (hot path for plan building).
candStamp []uint32
candEpoch uint32
scratchCandidates []MapItem
}
// NewWorld constructs a new world with the given real dimensions.
// The dimensions are converted to fixed-point and must be positive.
func NewWorld(width, height int) *World {
if width <= 0 || height <= 0 {
panic("invalid width or height")
}
return &World{
W: width * SCALE,
H: height * SCALE,
cellSize: 1,
objects: make(map[PrimitiveID]MapItem),
styles: NewStyleTable(),
theme: DefaultTheme{},
// At startup, "theme defaults" point to conservative built-ins.
themeDefaultLineStyleID: StyleIDDefaultLine,
themeDefaultCircleStyleID: StyleIDDefaultCircle,
themeDefaultPointStyleID: StyleIDDefaultPoint,
circleRadiusScaleFp: SCALE,
derivedCache: make(map[derivedStyleKey]StyleID, 128),
nextID: 1, // 0 is reserved as "invalid"
}
}
// allocID allocates a new PrimitiveID using a free-list (reusable IDs) and a monotonic counter.
// It returns an error if the ID space is exhausted.
func (w *World) allocID() (PrimitiveID, error) {
if n := len(w.freeIDs); n > 0 {
id := w.freeIDs[n-1]
w.freeIDs = w.freeIDs[:n-1]
return id, nil
}
if w.nextID == PrimitiveID(^uint32(0)) {
return 0, errIDExhausted
}
id := w.nextID
w.nextID++
return id, nil
}
// freeID returns an id back to the pool. It is safe to call only after the object is removed.
func (w *World) freeID(id PrimitiveID) {
if id == 0 {
return
}
w.freeIDs = append(w.freeIDs, id)
}
// checkCoordinate reports whether the fixed-point coordinate (xf, yf)
// lies inside the world bounds: [0, W) x [0, H).
func (w *World) checkCoordinate(xf, yf int) bool {
if xf < 0 || xf >= w.W || yf < 0 || yf >= w.H {
return false
}
return true
}
// AddStyleLine creates a new line style derived from the default line style.
func (w *World) AddStyleLine(override StyleOverride) StyleID {
return w.styles.AddDerived(StyleIDDefaultLine, override)
}
// AddStyleCircle creates a new circle style derived from the default circle style.
func (w *World) AddStyleCircle(override StyleOverride) StyleID {
return w.styles.AddDerived(StyleIDDefaultCircle, override)
}
// AddStylePoint creates a new point style derived from the default point style.
func (w *World) AddStylePoint(override StyleOverride) StyleID {
return w.styles.AddDerived(StyleIDDefaultPoint, override)
}
// Theme returns the current theme. It is never nil.
func (w *World) Theme() StyleTheme {
if w.theme == nil {
return DefaultTheme{}
}
return w.theme
}
// SetTheme updates the world's current theme.
func (w *World) SetTheme(theme StyleTheme) {
if theme == nil {
theme = DefaultTheme{}
}
w.theme = theme
// Drop derived cache when theme changes to avoid unbounded growth.
for k := range w.derivedCache {
delete(w.derivedCache, k)
}
// Materialize theme base styles as new IDs.
w.themeDefaultLineStyleID = w.styles.AddStyle(theme.LineStyle())
w.themeDefaultCircleStyleID = w.styles.AddStyle(theme.CircleStyle())
w.themeDefaultPointStyleID = w.styles.AddStyle(theme.PointStyle())
w.refreshThemeManagedStyles()
// Full redraw to apply new background and base styles.
w.renderState.Reset()
w.ForceFullRedrawNext()
}
func (w *World) themeBaseStyleID(base styleBase) StyleID {
switch base {
case styleBaseThemeLine:
return w.themeDefaultLineStyleID
case styleBaseThemeCircle:
return w.themeDefaultCircleStyleID
case styleBaseThemePoint:
return w.themeDefaultPointStyleID
default:
return StyleIDInvalid
}
}
// refreshThemeManagedStyles recomputes resolved StyleID values for primitives
// that track the active theme rather than a fixed explicit style.
func (w *World) refreshThemeManagedStyles() {
th := w.Theme()
for id, it := range w.objects {
switch v := it.(type) {
case Point:
if v.Base == styleBaseFixed {
continue
}
baseID := w.themeBaseStyleID(v.Base)
if baseID == StyleIDInvalid {
continue
}
classOv := StyleOverride{}
if ov, ok := th.PointClassOverride(v.Class); ok {
classOv = ov
}
merged := mergeOverrides(classOv, v.Override)
v.StyleID = w.derivedStyleID(baseID, merged)
w.objects[id] = v
case Circle:
if v.Base == styleBaseFixed {
continue
}
baseID := w.themeBaseStyleID(v.Base)
if baseID == StyleIDInvalid {
continue
}
classOv := StyleOverride{}
if ov, ok := th.CircleClassOverride(v.Class); ok {
classOv = ov
}
merged := mergeOverrides(classOv, v.Override)
v.StyleID = w.derivedStyleID(baseID, merged)
w.objects[id] = v
case Line:
if v.Base == styleBaseFixed {
continue
}
baseID := w.themeBaseStyleID(v.Base)
if baseID == StyleIDInvalid {
continue
}
classOv := StyleOverride{}
if ov, ok := th.LineClassOverride(v.Class); ok {
classOv = ov
}
merged := mergeOverrides(classOv, v.Override)
v.StyleID = w.derivedStyleID(baseID, merged)
w.objects[id] = v
default:
panic("refreshThemeManagedStyles: unknown item type")
}
}
w.ForceFullRedrawNext()
}
// derivedStyleID resolves a derived style, reusing the per-world cache when an
// identical base style and override combination has already been materialized.
func (w *World) derivedStyleID(base StyleID, ov StyleOverride) StyleID {
if ov.IsZero() {
return base
}
k := derivedStyleKey{base: base, fp: ov.fingerprint()}
if id, ok := w.derivedCache[k]; ok {
return id
}
id := w.styles.AddDerived(base, ov)
w.derivedCache[k] = id
return id
}
// Remove deletes an object by id. It returns errNoSuchObject if the id is unknown.
// It marks the spatial index dirty and triggers an autonomous rebuild if possible.
func (w *World) Remove(id PrimitiveID) error {
if _, ok := w.objects[id]; !ok {
return errNoSuchObject
}
delete(w.objects, id)
w.freeID(id)
w.indexDirty = true
w.rebuildIndexFromLastState()
return nil
}
// Reindex forces rebuilding the spatial index (grid) if the renderer has enough last-state
// information to choose a grid cell size. If not enough info exists yet, it keeps indexDirty=true.
func (w *World) Reindex() {
w.indexDirty = true
w.rebuildIndexFromLastState()
}
// rebuildIndexFromLastState rebuilds the index using last known viewport sizes and zoomFp
// from renderer state. If that state is not initialized, it does nothing.
func (w *World) rebuildIndexFromLastState() {
if !w.indexDirty {
return
}
if !w.index.initialized {
return
}
if w.index.viewportW <= 0 || w.index.viewportH <= 0 || w.index.zoomFp <= 0 {
return
}
w.rebuildIndexForViewportZoomFp(w.index.viewportW, w.index.viewportH, w.index.zoomFp)
w.indexDirty = false
}
// AddPoint validates and stores a point primitive in the world.
// The input coordinates are given in real world units and are converted
// to fixed-point before validation.
func (w *World) AddPoint(x, y float64, opts ...PointOpt) (PrimitiveID, error) {
xf := fixedPoint(x)
yf := fixedPoint(y)
if ok := w.checkCoordinate(xf, yf); !ok {
return 0, errBadCoordinate
}
o := defaultPointOptions()
for _, opt := range opts {
if opt != nil {
opt(&o)
}
}
// styleID := g.resolvePointStyleID(o)
id, err := w.allocID()
if err != nil {
return 0, err
}
obj := Point{
Id: id,
X: xf,
Y: yf,
Priority: o.Priority,
// StyleID: styleID,
HitSlopPx: o.HitSlopPx,
}
obj.Class = o.Class
if o.hasStyleID {
obj.Base = styleBaseFixed
obj.StyleID = o.StyleID
obj.Override = StyleOverride{}
} else {
obj.Base = styleBaseThemePoint
baseID := w.themeDefaultPointStyleID
// class override from current theme (may be absent)
classOv := StyleOverride{}
if th := w.Theme(); th != nil {
if ov, ok := th.PointClassOverride(obj.Class); ok {
classOv = ov
}
}
merged := mergeOverrides(classOv, o.Override)
obj.Override = o.Override // store only user override; class override comes from theme
obj.StyleID = w.derivedStyleID(baseID, merged)
}
w.objects[id] = obj
w.indexDirty = true
w.rebuildIndexFromLastState()
return id, nil
}
// AddCircle validates and stores a circle primitive in the world.
// The center and radius are given in real world units and are converted
// to fixed-point before validation. A zero radius is allowed.
func (w *World) AddCircle(x, y, r float64, opts ...CircleOpt) (PrimitiveID, error) {
xf := fixedPoint(x)
yf := fixedPoint(y)
if ok := w.checkCoordinate(xf, yf); !ok {
return 0, errBadCoordinate
}
if r < 0 {
return 0, errBadRadius
}
o := defaultCircleOptions()
for _, opt := range opts {
if opt != nil {
opt(&o)
}
}
id, err := w.allocID()
if err != nil {
return 0, err
}
obj := Circle{
Id: id,
X: xf,
Y: yf,
Radius: fixedPoint(r),
Priority: o.Priority,
HitSlopPx: o.HitSlopPx,
}
obj.Class = o.Class
if o.hasStyleID {
obj.Base = styleBaseFixed
obj.StyleID = o.StyleID
obj.Override = StyleOverride{}
} else {
obj.Base = styleBaseThemeCircle
baseID := w.themeDefaultCircleStyleID
// class override from current theme (may be absent)
classOv := StyleOverride{}
if th := w.Theme(); th != nil {
if ov, ok := th.CircleClassOverride(obj.Class); ok {
classOv = ov
}
}
merged := mergeOverrides(classOv, o.Override)
obj.Override = o.Override // store only user override; class override comes from theme
obj.StyleID = w.derivedStyleID(baseID, merged)
}
w.objects[id] = obj
w.indexDirty = true
w.rebuildIndexFromLastState()
return id, nil
}
// AddLine validates and stores a line primitive in the world.
// The endpoints are given in real world units and are converted
// to fixed-point before validation.
func (w *World) AddLine(x1, y1, x2, y2 float64, opts ...LineOpt) (PrimitiveID, error) {
x1f := fixedPoint(x1)
y1f := fixedPoint(y1)
x2f := fixedPoint(x2)
y2f := fixedPoint(y2)
if ok := w.checkCoordinate(x1f, y1f); !ok {
return 0, errBadCoordinate
}
if ok := w.checkCoordinate(x2f, y2f); !ok {
return 0, errBadCoordinate
}
o := defaultLineOptions()
for _, opt := range opts {
if opt != nil {
opt(&o)
}
}
// styleID := g.resolveLineStyleID(o)
id, err := w.allocID()
if err != nil {
return 0, err
}
obj := Line{
Id: id,
X1: x1f,
Y1: y1f,
X2: x2f,
Y2: y2f,
Priority: o.Priority,
// StyleID: styleID,
HitSlopPx: o.HitSlopPx,
}
obj.Class = o.Class
if o.hasStyleID {
obj.Base = styleBaseFixed
obj.StyleID = o.StyleID
obj.Override = StyleOverride{}
} else {
obj.Base = styleBaseThemeLine
baseID := w.themeDefaultLineStyleID
// class override from current theme (may be absent)
classOv := StyleOverride{}
if th := w.Theme(); th != nil {
if ov, ok := th.LineClassOverride(obj.Class); ok {
classOv = ov
}
}
merged := mergeOverrides(classOv, o.Override)
obj.Override = o.Override // store only user override; class override comes from theme
obj.StyleID = w.derivedStyleID(baseID, merged)
}
w.objects[id] = obj
w.indexDirty = true
w.rebuildIndexFromLastState()
return id, nil
}
// worldToCellX converts a fixed-point X coordinate to a grid column index.
func (w *World) worldToCellX(x int) int {
return worldToCell(x, w.W, w.cols, w.cellSize)
}
// worldToCellY converts a fixed-point Y coordinate to a grid row index.
func (w *World) worldToCellY(y int) int {
return worldToCell(y, w.H, w.rows, w.cellSize)
}
// resetGrid recreates the spatial grid with the given cell size
// and clears all previous indexing state.
func (w *World) resetGrid(cellSize int) {
if cellSize <= 0 {
panic("resetGrid: invalid cell size")
}
w.cellSize = cellSize
w.cols = ceilDiv(w.W, w.cellSize)
w.rows = ceilDiv(w.H, w.cellSize)
w.grid = make([][][]MapItem, w.rows)
for row := range w.grid {
w.grid[row] = make([][]MapItem, w.cols)
}
}
// indexObject inserts a single object into all grid cells touched by its
// indexing representation. Points are inserted into one cell, while circles
// and lines are inserted by their torus-aware bbox coverage.
func (w *World) indexObject(o MapItem) {
switch mapItem := o.(type) {
case Point:
col := w.worldToCellX(mapItem.X)
row := w.worldToCellY(mapItem.Y)
w.grid[row][col] = append(w.grid[row][col], mapItem)
case Line:
x1 := mapItem.X1
y1 := mapItem.Y1
x2 := mapItem.X2
y2 := mapItem.Y2
x1, x2 = shortestWrappedDelta(x1, x2, w.W)
y1, y2 = shortestWrappedDelta(y1, y2, w.H)
minX := min(x1, x2)
maxX := max(x1, x2)
minY := min(y1, y2)
maxY := max(y1, y2)
if minX == maxX {
maxX++
}
if minY == maxY {
maxY++
}
w.indexBBox(mapItem, minX, maxX, minY, maxY)
case Circle:
rEff := circleRadiusEffFp(mapItem.Radius, w.circleRadiusScaleFp)
w.indexBBox(mapItem,
mapItem.X-rEff, mapItem.X+rEff,
mapItem.Y-rEff, mapItem.Y+rEff,
)
default:
panic(fmt.Sprintf("indexing: unknown element %T", mapItem))
}
}
// indexBBox indexes an object by a half-open fixed-point bbox that may cross
// torus boundaries. The bbox is split into wrapped in-world rectangles first,
// then all covered grid cells are populated.
func (w *World) indexBBox(o MapItem, minX, maxX, minY, maxY int) {
rects := splitByWrap(w.W, w.H, minX, maxX, minY, maxY)
for _, r := range rects {
colStart := w.worldToCellX(r.minX)
colEnd := w.worldToCellX(r.maxX - 1)
rowStart := w.worldToCellY(r.minY)
rowEnd := w.worldToCellY(r.maxY - 1)
for col := colStart; col <= colEnd; col++ {
for row := rowStart; row <= rowEnd; row++ {
w.grid[row][col] = append(w.grid[row][col], o)
}
}
}
}
// IndexOnViewportChange is called when UI window sizes are changed.
// cameraZoom is float64, converted inside world to fixed-point.
func (w *World) IndexOnViewportChange(viewportWidthPx, viewportHeightPx int, cameraZoom float64) {
zoomFp := mustCameraZoomToWorldFixed(cameraZoom) // must-version is ok here, matches your existing code
// Remember params for autonomous reindex after Add/Remove.
w.index.initialized = true
w.index.viewportW = viewportWidthPx
w.index.viewportH = viewportHeightPx
w.index.zoomFp = zoomFp
w.rebuildIndexForViewportZoomFp(viewportWidthPx, viewportHeightPx, zoomFp)
w.indexDirty = false
}
// rebuildIndexForViewportZoomFp rebuilds the spatial grid for a particular
// viewport size and fixed-point zoom.
//
// The chosen cell size is derived from the currently visible world span and
// then clamped into the package-wide cell-size bounds.
func (w *World) rebuildIndexForViewportZoomFp(viewportWidthPx, viewportHeightPx int, zoomFp int) {
worldWidth, worldHeight := viewportPxToWorldFixed(viewportWidthPx, viewportHeightPx, zoomFp)
cellsAcrossMin := 8
visibleMin := min(worldWidth, worldHeight)
cellSize := visibleMin / cellsAcrossMin
cellSize = clamp(cellSize, cellSizeMin, cellSizeMax)
w.resetGrid(cellSize)
for _, o := range w.objects {
w.indexObject(o)
}
}
// CircleRadiusScaleFp returns the current circle radius scale (fixed-point).
func (w *World) CircleRadiusScaleFp() int {
return w.circleRadiusScaleFp
}
// SetCircleRadiusScaleFp sets the circle radius scale (fixed-point).
// scaleFp must be > 0. This affects indexing, rendering and hit-testing,
// so it forces a full redraw and triggers reindex when possible.
func (w *World) SetCircleRadiusScaleFp(scaleFp int) error {
if scaleFp <= 0 {
return errors.New("invalid circle radius scale")
}
if scaleFp == w.circleRadiusScaleFp {
return nil
}
w.circleRadiusScaleFp = scaleFp
// Radius scale affects circle bbox => spatial index must be rebuilt.
w.indexDirty = true
w.rebuildIndexFromLastState()
// Visual change => full redraw.
w.ForceFullRedrawNext()
return nil
}
// circleRadiusEffFp converts a raw circle radius (world-fixed) into effective radius (world-fixed)
// using g.circleRadiusScaleFp.
func circleRadiusEffFp(rawRadiusFp, circleRadiusScaleFp int) int {
// Use int64 to avoid overflow.
v := (int64(rawRadiusFp) * int64(circleRadiusScaleFp)) / int64(SCALE)
if v < 0 {
return 0
}
if v > int64(^uint(0)>>1) {
// Defensive; should never happen with sane inputs on 64-bit.
return int(^uint(0) >> 1)
}
return int(v)
}
// PointClassID classifies Point primitives for theme-level style overrides.
//
// Themes may use the class to derive a final style from the point base style
// without changing the primitive geometry itself.
type PointClassID uint8
const (
// PointClassDefault selects the theme's default point styling.
PointClassDefault PointClassID = iota
// PointClassTrackUnknown marks a point as an unknown track marker.
PointClassTrackUnknown
// PointClassTrackIncoming marks a point as an incoming track marker.
PointClassTrackIncoming
// PointClassTrackOutgoing marks a point as an outgoing track marker.
PointClassTrackOutgoing
// PointClassUnidentifiedPlanet marks an unidentified planet without visivle size.
PointClassUnidentifiedPlanet
)
// LineClassID classifies Line primitives for theme-level style overrides.
type LineClassID uint8
const (
// LineClassDefault selects the theme's default line styling.
LineClassDefault LineClassID = iota
// LineClassTrackIncoming marks a line as an incoming track.
LineClassTrackIncoming
// LineCLassTrackOutgoing marks a line as an outgoing track.
// The unusual spelling is preserved for backward compatibility.
LineCLassTrackOutgoing
// LineClassMeasurement marks a line as a measurement helper.
LineClassMeasurement
)
// CircleClassID classifies Circle primitives for theme-level style overrides.
type CircleClassID uint8
const (
// CircleClassDefault selects the theme's default circle styling.
CircleClassDefault CircleClassID = iota
// CircleClassLocalPlanet marks a circle as a player-owned planet.
CircleClassLocalPlanet
// CircleClassOthersPlanet marks a circle as an occupied planet.
CircleClassOthersPlanet
// CircleClassFreePlanet marks a circle as a free planet.
CircleClassFreePlanet
)
// PrimitiveID is a compact stable identifier for primitives stored in the World.
// It is allocated by the World and may be reused after deletion (free-list).
type PrimitiveID uint32
// MapItem is the common interface implemented by all world primitives.
type MapItem interface {
ID() PrimitiveID
}
// styleBase describes how a primitive resolves its base style across theme changes.
type styleBase uint8
const (
styleBaseFixed styleBase = iota
styleBaseThemeLine
styleBaseThemeCircle
styleBaseThemePoint
)
// Point is a point primitive in fixed-point world coordinates.
type Point struct {
Id PrimitiveID
X, Y int
// Priority controls per-object draw ordering. Smaller draws earlier.
Priority int
// StyleID references a resolved style in the world's style table.
StyleID StyleID
// Theme style binding. If Base==styleBaseFixed => StyleID stays as-is across theme changes.
Base styleBase
// Override is applied relative to current theme base style (only when Base is theme* and Override is non-zero).
Override StyleOverride
Class PointClassID
// HitSlopPx expands hit-test radius in screen pixels (per-object override).
// 0 means "use primitive default".
HitSlopPx int
}
// Line is a line segment primitive in fixed-point world coordinates.
type Line struct {
Id PrimitiveID
X1, Y1 int
X2, Y2 int
// Priority controls per-object draw ordering. Smaller draws earlier.
Priority int
// StyleID references a resolved style in the world's style table.
StyleID StyleID
// Theme style binding. If Base==styleBaseFixed => StyleID stays as-is across theme changes.
Base styleBase
// Override is applied relative to current theme base style (only when Base is theme* and Override is non-zero).
Override StyleOverride
Class LineClassID
// HitSlopPx expands hit-test radius in screen pixels (per-object override).
// 0 means "use primitive default".
HitSlopPx int
}
// Circle is a circle primitive in fixed-point world coordinates.
type Circle struct {
Id PrimitiveID
X, Y int
Radius int
// Priority controls per-object draw ordering. Smaller draws earlier.
Priority int
// StyleID references a resolved style in the world's style table.
StyleID StyleID
// Theme style binding. If Base==styleBaseFixed => StyleID stays as-is across theme changes.
Base styleBase
// Override is applied relative to current theme base style (only when Base is theme* and Override is non-zero).
Override StyleOverride
Class CircleClassID
// HitSlopPx expands hit-test radius in screen pixels (per-object override).
// 0 means "use primitive default".
HitSlopPx int
}
// ID returns the point identifier.
func (p Point) ID() PrimitiveID { return p.Id }
// ID returns the line identifier.
func (l Line) ID() PrimitiveID { return l.Id }
// ID returns the circle identifier.
func (c Circle) ID() PrimitiveID { return c.Id }
// MinX returns the minimum X endpoint coordinate of the line.
func (l Line) MinX() int { return min(l.X1, l.X2) }
// MaxX returns the maximum X endpoint coordinate of the line.
func (l Line) MaxX() int { return max(l.X1, l.X2) }
// MinY returns the minimum Y endpoint coordinate of the line.
func (l Line) MinY() int { return min(l.Y1, l.Y2) }
// MaxY returns the maximum Y endpoint coordinate of the line.
func (l Line) MaxY() int { return max(l.Y1, l.Y2) }
// MinX returns the minimum X coordinate of the circle bbox.
func (c Circle) MinX() int { return c.X - c.Radius }
// MaxX returns the maximum X coordinate of the circle bbox.
func (c Circle) MaxX() int { return c.X + c.Radius }
// MinY returns the minimum Y coordinate of the circle bbox.
func (c Circle) MinY() int { return c.Y - c.Radius }
// MaxY returns the maximum Y coordinate of the circle bbox.
func (c Circle) MaxY() int { return c.Y + c.Radius }
// PointOpt applies optional point-construction parameters to PointOptions.
type PointOpt func(*PointOptions)
// PointOptions stores optional arguments accepted by World.AddPoint.
//
// Defaults are resolved before applying user-provided PointOpt values.
type PointOptions struct {
Priority int
StyleID StyleID
Override StyleOverride
Class PointClassID
HitSlopPx int
hasStyleID bool
}
// defaultPointOptions returns the default option set used by World.AddPoint.
func defaultPointOptions() PointOptions {
return PointOptions{
Priority: DefaultPriorityPoint,
StyleID: StyleIDDefaultPoint,
Class: PointClassDefault,
}
}
// PointWithPriority sets point draw priority.
//
// Lower priorities render earlier within the same tile.
func PointWithPriority(p int) PointOpt {
return func(o *PointOptions) {
o.Priority = p
}
}
// PointWithStyleID forces the point to use a pre-registered style.
func PointWithStyleID(id StyleID) PointOpt {
return func(o *PointOptions) {
o.StyleID = id
o.hasStyleID = true
// Explicit style ID wins over overrides.
o.Override = StyleOverride{}
}
}
// PointWithClass selects the theme class used for point style resolution.
func PointWithClass(c PointClassID) PointOpt {
return func(o *PointOptions) { o.Class = c }
}
// PointWithStyleOverride applies a user override on top of the resolved point base style.
//
// If PointWithStyleID is also supplied, the explicit style ID wins.
func PointWithStyleOverride(ov StyleOverride) PointOpt {
return func(o *PointOptions) {
o.Override = ov
}
}
// PointWithHitSlopPx overrides the default point hit slop in screen pixels.
func PointWithHitSlopPx(px int) PointOpt {
return func(o *PointOptions) { o.HitSlopPx = px }
}
// CircleOpt applies optional circle-construction parameters to CircleOptions.
type CircleOpt func(*CircleOptions)
// CircleOptions stores optional arguments accepted by World.AddCircle.
type CircleOptions struct {
Priority int
StyleID StyleID
Override StyleOverride
Class CircleClassID
HitSlopPx int
hasStyleID bool
}
// defaultCircleOptions returns the default option set used by World.AddCircle.
func defaultCircleOptions() CircleOptions {
return CircleOptions{
Priority: DefaultPriorityCircle,
StyleID: StyleIDDefaultCircle,
Class: CircleClassDefault,
}
}
// CircleWithPriority sets circle draw priority.
func CircleWithPriority(p int) CircleOpt {
return func(o *CircleOptions) {
o.Priority = p
}
}
// CircleWithStyleID forces the circle to use a pre-registered style.
func CircleWithStyleID(id StyleID) CircleOpt {
return func(o *CircleOptions) {
o.StyleID = id
o.hasStyleID = true
o.Override = StyleOverride{}
}
}
// CircleWithClass selects the theme class used for circle style resolution.
func CircleWithClass(c CircleClassID) CircleOpt {
return func(o *CircleOptions) { o.Class = c }
}
// CircleWithStyleOverride applies a user override on top of the resolved circle base style.
func CircleWithStyleOverride(ov StyleOverride) CircleOpt {
return func(o *CircleOptions) {
o.Override = ov
}
}
// CircleWithHitSlopPx overrides the default circle hit slop in screen pixels.
func CircleWithHitSlopPx(px int) CircleOpt {
return func(o *CircleOptions) { o.HitSlopPx = px }
}
// LineOpt applies optional line-construction parameters to LineOptions.
type LineOpt func(*LineOptions)
// LineOptions stores optional arguments accepted by World.AddLine.
type LineOptions struct {
Priority int
StyleID StyleID
Override StyleOverride
Class LineClassID
HitSlopPx int
hasStyleID bool
}
// defaultLineOptions returns the default option set used by World.AddLine.
func defaultLineOptions() LineOptions {
return LineOptions{
Priority: DefaultPriorityLine,
StyleID: StyleIDDefaultLine,
Class: LineClassDefault,
}
}
// LineWithPriority sets line draw priority.
func LineWithPriority(p int) LineOpt {
return func(o *LineOptions) {
o.Priority = p
}
}
// LineWithStyleID forces the line to use a pre-registered style.
func LineWithStyleID(id StyleID) LineOpt {
return func(o *LineOptions) {
o.StyleID = id
o.hasStyleID = true
o.Override = StyleOverride{}
}
}
// LineWithClass selects the theme class used for line style resolution.
func LineWithClass(c LineClassID) LineOpt {
return func(o *LineOptions) { o.Class = c }
}
// LineWithStyleOverride applies a user override on top of the resolved line base style.
func LineWithStyleOverride(ov StyleOverride) LineOpt {
return func(o *LineOptions) {
o.Override = ov
}
}
// LineWithHitSlopPx overrides the default line hit slop in screen pixels.
func LineWithHitSlopPx(px int) LineOpt {
return func(o *LineOptions) { o.HitSlopPx = px }
}