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
144 lines
3.8 KiB
Go
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
|
|
}
|