implemented png export so that we get a overlay of the splitt lines and also directly export a node tree visualization of the node tree with graphviz.
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EmitDOT serialisiert den BSP-Baum mit Wurzel root und schreibt ihn als DOT-Datei nach path.
|
// 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")
|
buf.WriteString("}\n")
|
||||||
return os.WriteFile(path, buf.Bytes(), 0644)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,177 @@
|
|||||||
package viz
|
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)
|
||||||
|
}
|
||||||
|
|||||||
20
main.go
20
main.go
@@ -32,6 +32,8 @@ func main() {
|
|||||||
cands := flag.Int("cands", 16, "Anzahl Kandidaten (Subsample)")
|
cands := flag.Int("cands", 16, "Anzahl Kandidaten (Subsample)")
|
||||||
seed := flag.Int64("seed", 0, "RNG-Seed (0 = default)")
|
seed := flag.Int64("seed", 0, "RNG-Seed (0 = default)")
|
||||||
dotOut := flag.String("dot", "", "DOT-Export-Datei (optional)")
|
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()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -197,8 +199,22 @@ func main() {
|
|||||||
if err := viz.EmitDOT(root, *dotOut); err != nil {
|
if err := viz.EmitDOT(root, *dotOut); err != nil {
|
||||||
log.Fatalf("write DOT: %v", err)
|
log.Fatalf("write DOT: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("DOT export geschrieben: %s (mit 'dot -Tpng %s -o tree.png' rendern)\n",
|
fmt.Printf("DOT export geschrieben: %s\n", *dotOut)
|
||||||
*dotOut, *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
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user