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
This commit is contained in:
143
svg-maker/svg/info.go
Normal file
143
svg-maker/svg/info.go
Normal file
@@ -0,0 +1,143 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user