themes and styles
This commit is contained in:
+402
-134
@@ -18,6 +18,10 @@ type indexState struct {
|
||||
viewportH int
|
||||
zoomFp int
|
||||
}
|
||||
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.
|
||||
@@ -27,7 +31,14 @@ type World struct {
|
||||
cellSize int
|
||||
rows, cols int
|
||||
objects map[PrimitiveID]MapItem
|
||||
styles *StyleTable
|
||||
|
||||
styles *StyleTable
|
||||
theme StyleTheme
|
||||
themeDefaultLineStyleID StyleID
|
||||
themeDefaultCircleStyleID StyleID
|
||||
themeDefaultPointStyleID StyleID
|
||||
|
||||
circleRadiusScaleFp int // fixed-point, 1.0 == SCALE
|
||||
|
||||
// PrimitiveID allocator state.
|
||||
nextID PrimitiveID
|
||||
@@ -37,7 +48,8 @@ type World struct {
|
||||
indexDirty bool
|
||||
index indexState
|
||||
|
||||
renderState rendererIncrementalState
|
||||
renderState rendererIncrementalState
|
||||
derivedCache map[derivedStyleKey]StyleID
|
||||
}
|
||||
|
||||
// NewWorld constructs a new world with the given real dimensions.
|
||||
@@ -52,103 +64,247 @@ func NewWorld(width, height int) *World {
|
||||
cellSize: 1,
|
||||
objects: make(map[PrimitiveID]MapItem),
|
||||
styles: NewStyleTable(),
|
||||
nextID: 1, // 0 is reserved as "invalid"
|
||||
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 (g *World) allocID() (PrimitiveID, error) {
|
||||
if n := len(g.freeIDs); n > 0 {
|
||||
id := g.freeIDs[n-1]
|
||||
g.freeIDs = g.freeIDs[:n-1]
|
||||
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 g.nextID == PrimitiveID(^uint32(0)) {
|
||||
if w.nextID == PrimitiveID(^uint32(0)) {
|
||||
return 0, errIDExhausted
|
||||
}
|
||||
id := g.nextID
|
||||
g.nextID++
|
||||
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 (g *World) freeID(id PrimitiveID) {
|
||||
func (w *World) freeID(id PrimitiveID) {
|
||||
if id == 0 {
|
||||
return
|
||||
}
|
||||
g.freeIDs = append(g.freeIDs, id)
|
||||
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 (g *World) checkCoordinate(xf, yf int) bool {
|
||||
if xf < 0 || xf >= g.W || yf < 0 || yf >= g.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 (g *World) AddStyleLine(override StyleOverride) StyleID {
|
||||
return g.styles.AddDerived(StyleIDDefaultLine, override)
|
||||
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 (g *World) AddStyleCircle(override StyleOverride) StyleID {
|
||||
return g.styles.AddDerived(StyleIDDefaultCircle, override)
|
||||
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 (g *World) AddStylePoint(override StyleOverride) StyleID {
|
||||
return g.styles.AddDerived(StyleIDDefaultPoint, override)
|
||||
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.
|
||||
// Step 1 behavior:
|
||||
// - Does NOT mutate built-in default styles (1/2/3).
|
||||
// - Materializes three theme default styles as new StyleIDs in the style table.
|
||||
// - New objects (and later, theme-relative ones) can use these IDs.
|
||||
// - Forces next render to full redraw.
|
||||
func (w *World) SetTheme(theme StyleTheme) {
|
||||
if theme == nil {
|
||||
theme = DefaultTheme{}
|
||||
}
|
||||
// fmt.Println("current theme:", w.theme.ID())
|
||||
w.theme = theme
|
||||
// fmt.Println("new theme:", w.theme.ID())
|
||||
|
||||
// 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.reresolveThemeManagedStyles()
|
||||
|
||||
// Full redraw to apply new background and base styles.
|
||||
w.renderState.Reset()
|
||||
// w.forceFullRedraw = true
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func (w *World) reresolveThemeManagedStyles() {
|
||||
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("reresolveThemeManagedStyles: unknown item type")
|
||||
}
|
||||
}
|
||||
|
||||
w.ForceFullRedrawNext()
|
||||
}
|
||||
|
||||
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 (g *World) Remove(id PrimitiveID) error {
|
||||
if _, ok := g.objects[id]; !ok {
|
||||
func (w *World) Remove(id PrimitiveID) error {
|
||||
if _, ok := w.objects[id]; !ok {
|
||||
return errNoSuchObject
|
||||
}
|
||||
delete(g.objects, id)
|
||||
g.freeID(id)
|
||||
delete(w.objects, id)
|
||||
w.freeID(id)
|
||||
|
||||
g.indexDirty = true
|
||||
g.rebuildIndexFromLastState()
|
||||
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 (g *World) Reindex() {
|
||||
g.indexDirty = true
|
||||
g.rebuildIndexFromLastState()
|
||||
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 (g *World) rebuildIndexFromLastState() {
|
||||
if !g.indexDirty {
|
||||
func (w *World) rebuildIndexFromLastState() {
|
||||
if !w.indexDirty {
|
||||
return
|
||||
}
|
||||
if !g.index.initialized {
|
||||
if !w.index.initialized {
|
||||
return
|
||||
}
|
||||
if g.index.viewportW <= 0 || g.index.viewportH <= 0 || g.index.zoomFp <= 0 {
|
||||
if w.index.viewportW <= 0 || w.index.viewportH <= 0 || w.index.zoomFp <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
g.indexOnViewportChangeZoomFp(g.index.viewportW, g.index.viewportH, g.index.zoomFp)
|
||||
g.indexDirty = false
|
||||
w.indexOnViewportChangeZoomFp(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 (g *World) AddPoint(x, y float64, opts ...PointOpt) (PrimitiveID, error) {
|
||||
func (w *World) AddPoint(x, y float64, opts ...PointOpt) (PrimitiveID, error) {
|
||||
xf := fixedPoint(x)
|
||||
yf := fixedPoint(y)
|
||||
if ok := g.checkCoordinate(xf, yf); !ok {
|
||||
if ok := w.checkCoordinate(xf, yf); !ok {
|
||||
return 0, errBadCoordinate
|
||||
}
|
||||
|
||||
@@ -158,24 +314,47 @@ func (g *World) AddPoint(x, y float64, opts ...PointOpt) (PrimitiveID, error) {
|
||||
opt(&o)
|
||||
}
|
||||
}
|
||||
styleID := g.resolvePointStyleID(o)
|
||||
// styleID := g.resolvePointStyleID(o)
|
||||
|
||||
id, err := g.allocID()
|
||||
id, err := w.allocID()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
g.objects[id] = Point{
|
||||
Id: id,
|
||||
X: xf,
|
||||
Y: yf,
|
||||
Priority: o.Priority,
|
||||
StyleID: styleID,
|
||||
obj := Point{
|
||||
Id: id,
|
||||
X: xf,
|
||||
Y: yf,
|
||||
Priority: o.Priority,
|
||||
// StyleID: styleID,
|
||||
HitSlopPx: o.HitSlopPx,
|
||||
}
|
||||
|
||||
g.indexDirty = true
|
||||
g.rebuildIndexFromLastState()
|
||||
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
|
||||
}
|
||||
@@ -183,11 +362,11 @@ func (g *World) AddPoint(x, y float64, opts ...PointOpt) (PrimitiveID, error) {
|
||||
// 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 (g *World) AddCircle(x, y, r float64, opts ...CircleOpt) (PrimitiveID, error) {
|
||||
func (w *World) AddCircle(x, y, r float64, opts ...CircleOpt) (PrimitiveID, error) {
|
||||
xf := fixedPoint(x)
|
||||
yf := fixedPoint(y)
|
||||
|
||||
if ok := g.checkCoordinate(xf, yf); !ok {
|
||||
if ok := w.checkCoordinate(xf, yf); !ok {
|
||||
return 0, errBadCoordinate
|
||||
}
|
||||
if r < 0 {
|
||||
@@ -200,25 +379,46 @@ func (g *World) AddCircle(x, y, r float64, opts ...CircleOpt) (PrimitiveID, erro
|
||||
opt(&o)
|
||||
}
|
||||
}
|
||||
styleID := g.resolveCircleStyleID(o)
|
||||
|
||||
id, err := g.allocID()
|
||||
id, err := w.allocID()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
g.objects[id] = Circle{
|
||||
obj := Circle{
|
||||
Id: id,
|
||||
X: xf,
|
||||
Y: yf,
|
||||
Radius: fixedPoint(r),
|
||||
Priority: o.Priority,
|
||||
StyleID: styleID,
|
||||
HitSlopPx: o.HitSlopPx,
|
||||
}
|
||||
|
||||
g.indexDirty = true
|
||||
g.rebuildIndexFromLastState()
|
||||
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
|
||||
}
|
||||
@@ -226,16 +426,16 @@ func (g *World) AddCircle(x, y, r float64, opts ...CircleOpt) (PrimitiveID, erro
|
||||
// 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 (g *World) AddLine(x1, y1, x2, y2 float64, opts ...LineOpt) (PrimitiveID, error) {
|
||||
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 := g.checkCoordinate(x1f, y1f); !ok {
|
||||
if ok := w.checkCoordinate(x1f, y1f); !ok {
|
||||
return 0, errBadCoordinate
|
||||
}
|
||||
if ok := g.checkCoordinate(x2f, y2f); !ok {
|
||||
if ok := w.checkCoordinate(x2f, y2f); !ok {
|
||||
return 0, errBadCoordinate
|
||||
}
|
||||
|
||||
@@ -245,96 +445,119 @@ func (g *World) AddLine(x1, y1, x2, y2 float64, opts ...LineOpt) (PrimitiveID, e
|
||||
opt(&o)
|
||||
}
|
||||
}
|
||||
styleID := g.resolveLineStyleID(o)
|
||||
// styleID := g.resolveLineStyleID(o)
|
||||
|
||||
id, err := g.allocID()
|
||||
id, err := w.allocID()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
g.objects[id] = Line{
|
||||
Id: id,
|
||||
X1: x1f,
|
||||
Y1: y1f,
|
||||
X2: x2f,
|
||||
Y2: y2f,
|
||||
Priority: o.Priority,
|
||||
StyleID: styleID,
|
||||
obj := Line{
|
||||
Id: id,
|
||||
X1: x1f,
|
||||
Y1: y1f,
|
||||
X2: x2f,
|
||||
Y2: y2f,
|
||||
Priority: o.Priority,
|
||||
// StyleID: styleID,
|
||||
HitSlopPx: o.HitSlopPx,
|
||||
}
|
||||
|
||||
g.indexDirty = true
|
||||
g.rebuildIndexFromLastState()
|
||||
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
|
||||
}
|
||||
|
||||
func (g *World) resolvePointStyleID(o PointOptions) StyleID {
|
||||
if o.hasStyleID {
|
||||
return o.StyleID
|
||||
}
|
||||
if o.Override.IsZero() {
|
||||
return StyleIDDefaultPoint
|
||||
}
|
||||
return g.styles.AddDerived(StyleIDDefaultPoint, o.Override)
|
||||
}
|
||||
// func (g *World) resolvePointStyleID(o PointOptions) StyleID {
|
||||
// if o.hasStyleID {
|
||||
// return o.StyleID
|
||||
// }
|
||||
// if o.Override.IsZero() {
|
||||
// return StyleIDDefaultPoint
|
||||
// }
|
||||
// return g.styles.AddDerived(StyleIDDefaultPoint, o.Override)
|
||||
// }
|
||||
|
||||
func (g *World) resolveCircleStyleID(o CircleOptions) StyleID {
|
||||
if o.hasStyleID {
|
||||
return o.StyleID
|
||||
}
|
||||
if o.Override.IsZero() {
|
||||
return StyleIDDefaultCircle
|
||||
}
|
||||
return g.styles.AddDerived(StyleIDDefaultCircle, o.Override)
|
||||
}
|
||||
// func (g *World) resolveCircleStyleID(o CircleOptions) StyleID {
|
||||
// if o.hasStyleID {
|
||||
// return o.StyleID
|
||||
// }
|
||||
// if o.Override.IsZero() {
|
||||
// return StyleIDDefaultCircle
|
||||
// }
|
||||
// return g.styles.AddDerived(StyleIDDefaultCircle, o.Override)
|
||||
// }
|
||||
|
||||
func (g *World) resolveLineStyleID(o LineOptions) StyleID {
|
||||
if o.hasStyleID {
|
||||
return o.StyleID
|
||||
}
|
||||
if o.Override.IsZero() {
|
||||
return StyleIDDefaultLine
|
||||
}
|
||||
return g.styles.AddDerived(StyleIDDefaultLine, o.Override)
|
||||
}
|
||||
// func (g *World) resolveLineStyleID(o LineOptions) StyleID {
|
||||
// if o.hasStyleID {
|
||||
// return o.StyleID
|
||||
// }
|
||||
// if o.Override.IsZero() {
|
||||
// return StyleIDDefaultLine
|
||||
// }
|
||||
// return g.styles.AddDerived(StyleIDDefaultLine, o.Override)
|
||||
// }
|
||||
|
||||
// worldToCellX converts a fixed-point X coordinate to a grid column index.
|
||||
func (g *World) worldToCellX(x int) int {
|
||||
return worldToCell(x, g.W, g.cols, g.cellSize)
|
||||
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 (g *World) worldToCellY(y int) int {
|
||||
return worldToCell(y, g.H, g.rows, g.cellSize)
|
||||
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 (g *World) resetGrid(cellSize int) {
|
||||
func (w *World) resetGrid(cellSize int) {
|
||||
if cellSize <= 0 {
|
||||
panic("resetGrid: invalid cell size")
|
||||
}
|
||||
|
||||
g.cellSize = cellSize
|
||||
g.cols = ceilDiv(g.W, g.cellSize)
|
||||
g.rows = ceilDiv(g.H, g.cellSize)
|
||||
w.cellSize = cellSize
|
||||
w.cols = ceilDiv(w.W, w.cellSize)
|
||||
w.rows = ceilDiv(w.H, w.cellSize)
|
||||
|
||||
g.grid = make([][][]MapItem, g.rows)
|
||||
for row := range g.grid {
|
||||
g.grid[row] = make([][]MapItem, g.cols)
|
||||
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 (g *World) indexObject(o MapItem) {
|
||||
func (w *World) indexObject(o MapItem) {
|
||||
switch mapItem := o.(type) {
|
||||
case Point:
|
||||
col := g.worldToCellX(mapItem.X)
|
||||
row := g.worldToCellY(mapItem.Y)
|
||||
g.grid[row][col] = append(g.grid[row][col], mapItem)
|
||||
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
|
||||
@@ -342,8 +565,8 @@ func (g *World) indexObject(o MapItem) {
|
||||
x2 := mapItem.X2
|
||||
y2 := mapItem.Y2
|
||||
|
||||
x1, x2 = shortestWrappedDelta(x1, x2, g.W)
|
||||
y1, y2 = shortestWrappedDelta(y1, y2, g.H)
|
||||
x1, x2 = shortestWrappedDelta(x1, x2, w.W)
|
||||
y1, y2 = shortestWrappedDelta(y1, y2, w.H)
|
||||
|
||||
minX := min(x1, x2)
|
||||
maxX := max(x1, x2)
|
||||
@@ -357,10 +580,14 @@ func (g *World) indexObject(o MapItem) {
|
||||
maxY++
|
||||
}
|
||||
|
||||
g.indexBBox(mapItem, minX, maxX, minY, maxY)
|
||||
w.indexBBox(mapItem, minX, maxX, minY, maxY)
|
||||
|
||||
case Circle:
|
||||
g.indexBBox(mapItem, mapItem.MinX(), mapItem.MaxX(), mapItem.MinY(), mapItem.MaxY())
|
||||
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))
|
||||
@@ -370,18 +597,18 @@ func (g *World) indexObject(o 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 (g *World) indexBBox(o MapItem, minX, maxX, minY, maxY int) {
|
||||
rects := splitByWrap(g.W, g.H, minX, maxX, minY, maxY)
|
||||
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 := g.worldToCellX(r.minX)
|
||||
colEnd := g.worldToCellX(r.maxX - 1)
|
||||
colStart := w.worldToCellX(r.minX)
|
||||
colEnd := w.worldToCellX(r.maxX - 1)
|
||||
|
||||
rowStart := g.worldToCellY(r.minY)
|
||||
rowEnd := g.worldToCellY(r.maxY - 1)
|
||||
rowStart := w.worldToCellY(r.minY)
|
||||
rowEnd := w.worldToCellY(r.maxY - 1)
|
||||
|
||||
for col := colStart; col <= colEnd; col++ {
|
||||
for row := rowStart; row <= rowEnd; row++ {
|
||||
g.grid[row][col] = append(g.grid[row][col], o)
|
||||
w.grid[row][col] = append(w.grid[row][col], o)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -389,21 +616,21 @@ func (g *World) indexBBox(o MapItem, minX, maxX, minY, maxY int) {
|
||||
|
||||
// IndexOnViewportChange is called when UI window sizes are changed.
|
||||
// cameraZoom is float64, converted inside world to fixed-point.
|
||||
func (g *World) IndexOnViewportChange(viewportWidthPx, viewportHeightPx int, cameraZoom float64) {
|
||||
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.
|
||||
g.index.initialized = true
|
||||
g.index.viewportW = viewportWidthPx
|
||||
g.index.viewportH = viewportHeightPx
|
||||
g.index.zoomFp = zoomFp
|
||||
w.index.initialized = true
|
||||
w.index.viewportW = viewportWidthPx
|
||||
w.index.viewportH = viewportHeightPx
|
||||
w.index.zoomFp = zoomFp
|
||||
|
||||
g.indexOnViewportChangeZoomFp(viewportWidthPx, viewportHeightPx, zoomFp)
|
||||
g.indexDirty = false
|
||||
w.indexOnViewportChangeZoomFp(viewportWidthPx, viewportHeightPx, zoomFp)
|
||||
w.indexDirty = false
|
||||
}
|
||||
|
||||
// indexOnViewportChangeZoomFp performs indexing logic using fixed-point zoom.
|
||||
func (g *World) indexOnViewportChangeZoomFp(viewportWidthPx, viewportHeightPx int, zoomFp int) {
|
||||
func (w *World) indexOnViewportChangeZoomFp(viewportWidthPx, viewportHeightPx int, zoomFp int) {
|
||||
worldWidth, worldHeight := viewportPxToWorldFixed(viewportWidthPx, viewportHeightPx, zoomFp)
|
||||
|
||||
cellsAcrossMin := 8
|
||||
@@ -412,9 +639,50 @@ func (g *World) indexOnViewportChangeZoomFp(viewportWidthPx, viewportHeightPx in
|
||||
cellSize := visibleMin / cellsAcrossMin
|
||||
cellSize = clamp(cellSize, cellSizeMin, cellSizeMax)
|
||||
|
||||
g.resetGrid(cellSize)
|
||||
w.resetGrid(cellSize)
|
||||
|
||||
for _, o := range g.objects {
|
||||
g.indexObject(o)
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user