Files
agent-tools/svg-maker/svg/info.go
claude 36631c7709 svg-maker: svgc - .svgd text format -> SVG for agent-helm sharing
Line-based DSL (shapes, text, groups, color vars) with strict
validation and line-numbered errors, SVG emitter with visibility
defaults, truecolor terminal preview (same half-block technique as
spritec), info with bbox + outside-canvas warnings. Verified
end-to-end: svgc build -> helmd share -> visible through the hub.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011KikHkfCiC3yELbsMN8fT9
2026-08-07 00:37:51 +02:00

144 lines
3.8 KiB
Go

package svg
import (
"fmt"
"math"
"sort"
"strings"
)
// Info summarizes a parsed document: element counts, colors, the
// drawing's bounding box and warnings (things drawn outside the
// canvas), so an agent can verify a build without looking at pixels.
func Info(d *Doc) string {
counts := map[string]int{}
colors := map[string]bool{}
minX, minY := math.Inf(1), math.Inf(1)
maxX, maxY := math.Inf(-1), math.Inf(-1)
var warnings []string
var walk func(els []*Elem)
walk = func(els []*Elem) {
for _, el := range els {
if el.Kind == "group" {
counts["group"]++
walk(el.Kids)
continue
}
counts[el.Kind]++
for _, key := range []string{"fill", "stroke"} {
if c := el.Attrs[key]; c != "" && c != "none" && c != "transparent" {
colors[c] = true
}
}
x0, y0, x1, y1, ok := bbox(el)
if !ok {
continue
}
minX = math.Min(minX, x0)
minY = math.Min(minY, y0)
maxX = math.Max(maxX, x1)
maxY = math.Max(maxY, y1)
if x1 < 0 || y1 < 0 || x0 > d.W || y0 > d.H {
warnings = append(warnings, fmt.Sprintf("line %d: %s is entirely outside the canvas", el.Line, el.Kind))
} else if x0 < 0 || y0 < 0 || x1 > d.W || y1 > d.H {
warnings = append(warnings, fmt.Sprintf("line %d: %s sticks outside the canvas", el.Line, el.Kind))
}
}
}
walk(d.Elems)
var b strings.Builder
fmt.Fprintf(&b, "canvas: %gx%g", d.W, d.H)
if d.Bg != "" {
fmt.Fprintf(&b, " bg: %s", d.Bg)
}
b.WriteString("\n")
kinds := make([]string, 0, len(counts))
for k := range counts {
kinds = append(kinds, k)
}
sort.Strings(kinds)
total := 0
parts := make([]string, 0, len(kinds))
for _, k := range kinds {
parts = append(parts, fmt.Sprintf("%s:%d", k, counts[k]))
if k != "group" {
total += counts[k]
}
}
fmt.Fprintf(&b, "elements: %d (%s)\n", total, strings.Join(parts, " "))
if len(colors) > 0 {
cl := make([]string, 0, len(colors))
for c := range colors {
cl = append(cl, c)
}
sort.Strings(cl)
fmt.Fprintf(&b, "colors: %s\n", strings.Join(cl, " "))
}
if total > 0 && !math.IsInf(minX, 1) {
fmt.Fprintf(&b, "drawing bbox: %.4g,%.4g .. %.4g,%.4g\n", minX, minY, maxX, maxY)
}
for _, w := range warnings {
fmt.Fprintf(&b, "warning: %s\n", w)
}
return b.String()
}
// bbox computes an element's geometric bounding box (stroke width and
// transforms not included — good enough for out-of-canvas warnings).
func bbox(el *Elem) (x0, y0, x1, y1 float64, ok bool) {
switch el.Kind {
case "rect":
return el.Nums[0], el.Nums[1], el.Nums[0] + el.Nums[2], el.Nums[1] + el.Nums[3], true
case "circle":
cx, cy, r := el.Nums[0], el.Nums[1], el.Nums[2]
return cx - r, cy - r, cx + r, cy + r, true
case "ellipse":
cx, cy, rx, ry := el.Nums[0], el.Nums[1], el.Nums[2], el.Nums[3]
return cx - rx, cy - ry, cx + rx, cy + ry, true
case "line":
return math.Min(el.Nums[0], el.Nums[2]), math.Min(el.Nums[1], el.Nums[3]),
math.Max(el.Nums[0], el.Nums[2]), math.Max(el.Nums[1], el.Nums[3]), true
case "polyline", "polygon":
return pointsBBox(el.Points)
case "path":
subs, err := flattenPath(el.D)
if err != nil {
return 0, 0, 0, 0, false
}
var all [][2]float64
for _, sp := range subs {
all = append(all, sp...)
}
return pointsBBox(all)
case "text":
size := attrFloat(el.Attrs, "font-size", 16)
w := 0.6 * size * float64(len([]rune(el.Text)))
x, y := el.Nums[0], el.Nums[1]
switch el.Attrs["text-anchor"] {
case "middle":
x -= w / 2
case "end":
x -= w
}
return x, y - size, x + w, y, true
}
return 0, 0, 0, 0, false
}
func pointsBBox(pts [][2]float64) (x0, y0, x1, y1 float64, ok bool) {
if len(pts) == 0 {
return 0, 0, 0, 0, false
}
x0, y0 = pts[0][0], pts[0][1]
x1, y1 = x0, y0
for _, p := range pts {
x0 = math.Min(x0, p[0])
y0 = math.Min(y0, p[1])
x1 = math.Max(x1, p[0])
y1 = math.Max(y1, p[1])
}
return x0, y0, x1, y1, true
}