feat: primitive styling
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user