diff --git a/internal/viz/dot.go b/internal/viz/dot.go index b059589..526db22 100644 --- a/internal/viz/dot.go +++ b/internal/viz/dot.go @@ -5,6 +5,7 @@ import ( "bytes" "fmt" "os" + "os/exec" ) // EmitDOT serialisiert den BSP-Baum mit Wurzel root und schreibt ihn als DOT-Datei nach path. @@ -39,3 +40,12 @@ func EmitDOT(root *bsp.Node, path string) error { buf.WriteString("}\n") return os.WriteFile(path, buf.Bytes(), 0644) } + +func RunGraphviz(dotFile, pngFile string) error { + cmd := exec.Command("dot", "-Tpng", dotFile, "-o", pngFile) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("graphviz failed: %v\n%s", err, string(out)) + } + return nil +} diff --git a/internal/viz/png.go b/internal/viz/png.go index ec22ea8..6c58953 100644 --- a/internal/viz/png.go +++ b/internal/viz/png.go @@ -1,3 +1,177 @@ package viz -//init +import ( + "bspviz/internal/bsp" + "bspviz/internal/geom" + "bspviz/internal/mapfmt" + "image" + "image/color" + "image/png" + "math" + "math/rand" + "os" + "time" +) + +// worldToScreen transformiert Doom-Koordinaten in Bildkoordinaten +type worldToScreen struct { + minX, minY float64 + scale float64 + W, H int + margin float64 +} + +func makeTransform(verts []mapfmt.Vertex, W, H int) worldToScreen { + minX, minY := math.MaxFloat64, math.MaxFloat64 + maxX, maxY := -math.MaxFloat64, -math.MaxFloat64 + for _, v := range verts { + x, y := float64(v.X), float64(v.Y) + if x < minX { + minX = x + } + if y < minY { + minY = y + } + if x > maxX { + maxX = x + } + if y > maxY { + maxY = y + } + } + margin := 20.0 + w := maxX - minX + h := maxY - minY + if w < 1 { + w = 1 + } + if h < 1 { + h = 1 + } + scale := math.Min((float64(W)-2*margin)/w, (float64(H)-2*margin)/h) + return worldToScreen{minX, minY, scale, W, H, margin} +} + +func (t worldToScreen) P(p geom.Vec) (int, int) { + x := t.margin + (p.X-t.minX)*t.scale + y := t.margin + (p.Y-t.minY)*t.scale + // Y-Achse flippen (Doom: +Y nach oben, Bild: +Y nach unten) + yy := float64(t.H) - y + return int(x + 0.5), int(yy + 0.5) +} + +// einfache Bresenham-Linie +func drawLine(img *image.RGBA, x0, y0, x1, y1 int, c color.Color) { + dx := int(math.Abs(float64(x1 - x0))) + dy := -int(math.Abs(float64(y1 - y0))) + sx := -1 + if x0 < x1 { + sx = 1 + } + sy := -1 + if y0 < y1 { + sy = 1 + } + err := dx + dy + for { + if x0 >= 0 && y0 >= 0 && x0 < img.Bounds().Dx() && y0 < img.Bounds().Dy() { + img.Set(x0, y0, c) + } + if x0 == x1 && y0 == y1 { + break + } + e2 := 2 * err + if e2 >= dy { + err += dy + x0 += sx + } + if e2 <= dx { + err += dx + y0 += sy + } + } +} + +// RenderPNG zeichnet die Map und den BSP-Baum +func RenderPNG(m *mapfmt.MapData, root *bsp.Node, outPath string) error { + W, H := 1200, 900 + img := image.NewRGBA(image.Rect(0, 0, W, H)) + + // Hintergrund + bg := color.RGBA{20, 20, 24, 255} + for y := 0; y < H; y++ { + for x := 0; x < W; x++ { + img.Set(x, y, bg) + } + } + + tr := makeTransform(m.Vertices, W, H) + + // 1) Linedefs grau + gray := color.RGBA{180, 180, 180, 255} + for _, L := range m.Linedefs { + a := m.Vertices[L.V1] + b := m.Vertices[L.V2] + x0, y0 := tr.P(geom.V(float64(a.X), float64(a.Y))) + x1, y1 := tr.P(geom.V(float64(b.X), float64(b.Y))) + drawLine(img, x0, y0, x1, y1, gray) + } + + // 2) Split-Linien (gelb) + yellow := color.RGBA{240, 210, 40, 255} + var drawSplits func(*bsp.Node) + drawSplits = func(n *bsp.Node) { + if n == nil || n.Leaf != nil { + return + } + D := n.D + L := geom.Len(D) + if L < 1e-9 { + return + } + dx, dy := D.X/L, D.Y/L + k := 1e6 // "lange" Linie + p0 := geom.V(n.O.X-k*dx, n.O.Y-k*dy) + p1 := geom.V(n.O.X+k*dx, n.O.Y+k*dy) + x0, y0 := tr.P(p0) + x1, y1 := tr.P(p1) + drawLine(img, x0, y0, x1, y1, yellow) + drawSplits(n.Left) + drawSplits(n.Right) + } + drawSplits(root) + + // 3) Leaves farbig + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + var paintLeaves func(*bsp.Node) + paintLeaves = func(n *bsp.Node) { + if n == nil { + return + } + if n.Leaf != nil { + col := color.RGBA{ + uint8(100 + rng.Intn(100)), + uint8(100 + rng.Intn(100)), + uint8(100 + rng.Intn(100)), + 255, + } + for _, s := range n.Leaf.Segs { + x0, y0 := tr.P(s.A) + x1, y1 := tr.P(s.B) + drawLine(img, x0, y0, x1, y1, col) + } + return + } + paintLeaves(n.Left) + paintLeaves(n.Right) + } + paintLeaves(root) + + // speichern + f, err := os.Create(outPath) + if err != nil { + return err + } + defer f.Close() + return png.Encode(f, img) +} diff --git a/main.go b/main.go index f4ff756..295344f 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,8 @@ func main() { cands := flag.Int("cands", 16, "Anzahl Kandidaten (Subsample)") seed := flag.Int64("seed", 0, "RNG-Seed (0 = default)") dotOut := flag.String("dot", "", "DOT-Export-Datei (optional)") + treePNG := flag.String("treepng", "", "Graphviz-Baum als PNG (optional, benötigt -dot)") + overlay := flag.String("overlay", "", "Map-Overlay als PNG (optional)") flag.Parse() @@ -197,8 +199,22 @@ func main() { if err := viz.EmitDOT(root, *dotOut); err != nil { log.Fatalf("write DOT: %v", err) } - fmt.Printf("DOT export geschrieben: %s (mit 'dot -Tpng %s -o tree.png' rendern)\n", - *dotOut, *dotOut) + fmt.Printf("DOT export geschrieben: %s\n", *dotOut) + + if *treePNG != "" { + if err := viz.RunGraphviz(*dotOut, *treePNG); err != nil { + log.Printf("Graphviz fehlgeschlagen: %v", err) + } else { + fmt.Printf("Graphviz PNG gebaut: %s\n", *treePNG) + } + } + } + + if *overlay != "" { + if err := viz.RenderPNG(m, root, *overlay); err != nil { + log.Fatalf("write overlay PNG: %v", err) + } + fmt.Printf("Overlay PNG geschrieben: %s\n", *overlay) } return }