202 lines
4.9 KiB
Go
202 lines
4.9 KiB
Go
package world
|
|
|
|
import (
|
|
"image/color"
|
|
"sync"
|
|
)
|
|
|
|
// StyleID references a fully-materialized style stored in StyleTable.
|
|
type StyleID int
|
|
|
|
const (
|
|
// StyleIDInvalid means "no style". It should not be used for rendering.
|
|
StyleIDInvalid StyleID = 0
|
|
|
|
// Built-in default styles (stable IDs).
|
|
StyleIDDefaultLine StyleID = 1
|
|
StyleIDDefaultCircle StyleID = 2
|
|
StyleIDDefaultPoint StyleID = 3
|
|
)
|
|
|
|
// Default priorities (smaller draws earlier), step=100.
|
|
const (
|
|
DefaultPriorityLine = 100
|
|
DefaultPriorityCircle = 200
|
|
DefaultPriorityPoint = 300
|
|
)
|
|
|
|
// Style is a fully resolved style used by the renderer.
|
|
// All fields are concrete values; no "optional" markers here.
|
|
// Optionality is handled by StyleOverride during style creation.
|
|
type Style struct {
|
|
// FillColor is used for Fill() operations (points/circles typically).
|
|
// If nil, the renderer may treat it as "do not fill" depending on primitive.
|
|
FillColor color.Color
|
|
|
|
// StrokeColor is used for Stroke() operations (lines typically).
|
|
// If nil, the renderer may treat it as "do not stroke" depending on primitive.
|
|
StrokeColor color.Color
|
|
|
|
// StrokeWidthPx is a screen-space stroke width in pixels.
|
|
StrokeWidthPx float64
|
|
|
|
// StrokeDashes is the dash pattern in pixels. nil/empty means "solid".
|
|
StrokeDashes []float64
|
|
|
|
// StrokeDashOffset is the dash phase in pixels.
|
|
StrokeDashOffset float64
|
|
|
|
// PointRadiusPx is a screen-space radius for Point markers.
|
|
PointRadiusPx float64
|
|
}
|
|
|
|
// StyleOverride describes partial modifications applied to a base Style.
|
|
// Fields set to nil mean "do not override".
|
|
type StyleOverride struct {
|
|
FillColor color.Color
|
|
StrokeColor color.Color
|
|
StrokeWidthPx *float64
|
|
StrokeDashes *[]float64
|
|
StrokeDashOffset *float64
|
|
PointRadiusPx *float64
|
|
}
|
|
|
|
// IsZero reports whether override does not specify any fields.
|
|
func (o StyleOverride) IsZero() bool {
|
|
return o.FillColor == nil &&
|
|
o.StrokeColor == nil &&
|
|
o.StrokeWidthPx == nil &&
|
|
o.StrokeDashes == nil &&
|
|
o.StrokeDashOffset == nil &&
|
|
o.PointRadiusPx == nil
|
|
}
|
|
|
|
// Apply applies override to base style and returns a new fully resolved style.
|
|
// It copies slices defensively to avoid aliasing.
|
|
func (o StyleOverride) Apply(base Style) Style {
|
|
out := base
|
|
|
|
if o.FillColor != nil {
|
|
out.FillColor = o.FillColor
|
|
}
|
|
if o.StrokeColor != nil {
|
|
out.StrokeColor = o.StrokeColor
|
|
}
|
|
if o.StrokeWidthPx != nil {
|
|
out.StrokeWidthPx = *o.StrokeWidthPx
|
|
}
|
|
if o.StrokeDashes != nil {
|
|
// Copy to avoid future mutation by caller.
|
|
src := *o.StrokeDashes
|
|
if src == nil {
|
|
out.StrokeDashes = nil
|
|
} else {
|
|
dst := make([]float64, len(src))
|
|
copy(dst, src)
|
|
out.StrokeDashes = dst
|
|
}
|
|
}
|
|
if o.StrokeDashOffset != nil {
|
|
out.StrokeDashOffset = *o.StrokeDashOffset
|
|
}
|
|
if o.PointRadiusPx != nil {
|
|
out.PointRadiusPx = *o.PointRadiusPx
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// StyleTable stores fully resolved styles and provides stable lookups by StyleID.
|
|
// It also holds three built-in defaults for Line/Circle/Point.
|
|
type StyleTable struct {
|
|
mu sync.RWMutex
|
|
nextID StyleID
|
|
styles map[StyleID]Style
|
|
}
|
|
|
|
// NewStyleTable creates a new style table with built-in default styles.
|
|
// The default values are intentionally simple and stable.
|
|
func NewStyleTable() *StyleTable {
|
|
t := &StyleTable{
|
|
nextID: StyleIDDefaultPoint + 1,
|
|
styles: make(map[StyleID]Style, 16),
|
|
}
|
|
|
|
// Defaults: conservative, deterministic.
|
|
// Colors: opaque black. (Callers can override.)
|
|
white := color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
|
|
|
t.styles[StyleIDDefaultLine] = Style{
|
|
FillColor: nil,
|
|
StrokeColor: white,
|
|
StrokeWidthPx: 2.0,
|
|
StrokeDashes: nil,
|
|
StrokeDashOffset: 0,
|
|
PointRadiusPx: 0,
|
|
}
|
|
|
|
t.styles[StyleIDDefaultCircle] = Style{
|
|
FillColor: white,
|
|
StrokeColor: nil,
|
|
StrokeWidthPx: 0,
|
|
StrokeDashes: nil,
|
|
StrokeDashOffset: 0,
|
|
PointRadiusPx: 0,
|
|
}
|
|
|
|
t.styles[StyleIDDefaultPoint] = Style{
|
|
FillColor: white,
|
|
StrokeColor: nil,
|
|
StrokeWidthPx: 0,
|
|
StrokeDashes: nil,
|
|
StrokeDashOffset: 0,
|
|
PointRadiusPx: 2.0,
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
// Get returns a style by id.
|
|
func (t *StyleTable) Get(id StyleID) (Style, bool) {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
s, ok := t.styles[id]
|
|
if !ok {
|
|
return Style{}, false
|
|
}
|
|
// Defensive copy of slices.
|
|
if s.StrokeDashes != nil {
|
|
cp := make([]float64, len(s.StrokeDashes))
|
|
copy(cp, s.StrokeDashes)
|
|
s.StrokeDashes = cp
|
|
}
|
|
return s, true
|
|
}
|
|
|
|
// AddDerived creates a new style based on baseID with an override applied.
|
|
// It returns the new style ID.
|
|
func (t *StyleTable) AddDerived(baseID StyleID, override StyleOverride) StyleID {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
base, ok := t.styles[baseID]
|
|
if !ok {
|
|
panic("StyleTable.AddDerived: unknown base style ID")
|
|
}
|
|
|
|
derived := override.Apply(base)
|
|
|
|
id := t.nextID
|
|
t.nextID++
|
|
|
|
// Defensive copy of slices on store.
|
|
if derived.StrokeDashes != nil {
|
|
cp := make([]float64, len(derived.StrokeDashes))
|
|
copy(cp, derived.StrokeDashes)
|
|
derived.StrokeDashes = cp
|
|
}
|
|
|
|
t.styles[id] = derived
|
|
return id
|
|
}
|