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 ) var ( transparentColor color.Color = &color.RGBA{A: 0} ) func TransparentFill() color.Color { return transparentColor } // 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 }