diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..100ada7 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,211 @@ +package app + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "bspviz/internal/bsp" + "bspviz/internal/geom" + "bspviz/internal/mapfmt" + "bspviz/internal/viz" + "bspviz/internal/wad" +) + +type Options struct { + WadPath string + MapMarker string + ListOnly bool + Info bool + Extract []string + OutDir string + GeomTest bool + BuildBSP bool + Alpha float64 + Beta float64 + Eps float64 + LeafMax int + MaxDepth int + Cands int + Seed int64 + DotOut string + TreePNG string + Overlay string +} + +func Run(opts Options) error { + if strings.TrimSpace(opts.WadPath) == "" { + return fmt.Errorf("wad path required") + } + + w, err := wad.Open(opts.WadPath) + if err != nil { + return fmt.Errorf("open wad: %w", err) + } + defer w.Close() + + if opts.ListOnly { + fmt.Printf("WAD: %s\n", opts.WadPath) + for i, d := range w.Dir() { + fmt.Printf("%3d: %-8s size=%-7d pos=%-8d\n", i, d.Name(), d.Size, d.FilePos) + } + return nil + } + + start, end, err := w.FindMap(opts.MapMarker) + if err != nil { + return fmt.Errorf("find map: %w", err) + } + fmt.Printf("Map %s: Directory [%d, %d)\n", strings.ToUpper(opts.MapMarker), start, end) + + if opts.Info { + lumps, err := w.LoadMapLumps(opts.MapMarker, "VERTEXES", "LINEDEFS") + if err != nil { + return fmt.Errorf("load map lumps: %w", err) + } + vb := lumps["VERTEXES"] + lb := lumps["LINEDEFS"] + m, err := mapfmt.LoadMap(lumps) + if err != nil { + return err + } + + verts := len(vb) / 4 + lines := len(lb) / 14 + fmt.Printf("VERTEXES: bytes=%d count=%d\n", len(vb), verts) + fmt.Printf("LINEDEFS: bytes=%d count=%d\n", len(lb), lines) + + fmt.Printf("Map has %d vertices and %d linedefs\n", len(m.Vertices), len(m.Linedefs)) + fmt.Printf("First vertex: %+v\n", m.Vertices[0]) + fmt.Printf("First linedef: %+v\n", m.Linedefs[0]) + + if len(vb)%4 != 0 { + fmt.Println("WARN: VERTEXES size ist kein Vielfaches von 4 → Format prüfen") + } + if len(lb)%14 != 0 { + fmt.Println("WARN: LINEDEFS size ist kein Vielfaches von 14 → Format prüfen") + } + } + + if len(opts.Extract) > 0 { + lumps, err := w.LoadMapLumps(opts.MapMarker, opts.Extract...) + if err != nil { + return fmt.Errorf("extract: %w", err) + } + if err := os.MkdirAll(opts.OutDir, 0o755); err != nil { + return fmt.Errorf("mkdir %s: %w", opts.OutDir, err) + } + for name, data := range lumps { + dst := filepath.Join(opts.OutDir, fmt.Sprintf("%s.lmp", name)) + if err := os.WriteFile(dst, data, 0o644); err != nil { + return fmt.Errorf("write %s: %w", dst, err) + } + fmt.Printf("wrote %s (%d bytes)\n", dst, len(data)) + } + } + + if opts.GeomTest { + raw, err := w.LoadMapLumps(opts.MapMarker, "VERTEXES", "LINEDEFS") + if err != nil { + return fmt.Errorf("load map lumps: %w", err) + } + m, err := mapfmt.LoadMap(raw) + if err != nil { + return fmt.Errorf("parse map: %w", err) + } + + segs := mapfmt.LinedefsToSegs(m.Vertices, m.Linedefs) + fmt.Printf("GEOM: vertices=%d linedefs=%d segs=%d\n", len(m.Vertices), len(m.Linedefs), len(segs)) + if len(segs) == 0 { + fmt.Println("GEOM: keine Segmente gefunden – prüfe LINEDEFS/VERTEXES") + return nil + } + + pts := make([]geom.Vec, 0, len(m.Vertices)) + for _, v := range m.Vertices { + pts = append(pts, geom.V(float64(v.X), float64(v.Y))) + } + bb := geom.Bounds(pts) + w := bb.Max.X - bb.Min.X + h := bb.Max.Y - bb.Min.Y + fmt.Printf("AABB: min=(%.1f,%.1f) max=(%.1f,%.1f) size=(%.1f×%.1f)\n", + bb.Min.X, bb.Min.Y, bb.Max.X, bb.Max.Y, w, h) + + O := segs[0].A + D := geom.Sub(segs[0].B, segs[0].A) + if geom.Len(D) < 1e-9 && len(segs) > 1 { + O = segs[1].A + D = geom.Sub(segs[1].B, segs[1].A) + } + var left, right, splits, degens int + for _, s := range segs { + f, b := geom.SplitSeg(s, O, D) + if len(f) == 0 && len(b) == 0 { + degens++ + continue + } + left += len(f) + right += len(b) + if len(f) > 0 && len(b) > 0 { + splits++ + } + } + fmt.Printf("PROBE-SPLIT: O=(%.1f,%.1f) D=(%.1f,%.1f)\n", O.X, O.Y, D.X, D.Y) + fmt.Printf("PROBE-SPLIT: left=%d right=%d splits=%d degens=%d (EPS=%.1e)\n", + left, right, splits, degens, geom.EPS) + + return nil + } + + if opts.BuildBSP { + raw, err := w.LoadMapLumps(opts.MapMarker, "VERTEXES", "LINEDEFS") + if err != nil { + return fmt.Errorf("load map lumps: %w", err) + } + m, err := mapfmt.LoadMap(raw) + if err != nil { + return fmt.Errorf("parse map: %w", err) + } + + segs := mapfmt.LinedefsToSegs(m.Vertices, m.Linedefs) + p := bsp.Params{ + Alpha: opts.Alpha, Beta: opts.Beta, Eps: opts.Eps, + MaxDepth: opts.MaxDepth, LeafMax: opts.LeafMax, Cands: opts.Cands, Seed: opts.Seed, + } + root := bsp.Build(segs, p) + st := bsp.Measure(root) + + fmt.Printf("BSP built.\n") + fmt.Printf(" nodes=%d leaves=%d maxDepth=%d totalLeafSegs=%d\n", + st.Nodes, st.Leaves, st.MaxDepth, st.TotalSegs) + fmt.Printf(" params: alpha=%.2f beta=%.2f eps=%.1e leafMax=%d maxDepth=%d cands=%d seed=%d\n", + p.Alpha, p.Beta, p.Eps, p.LeafMax, p.MaxDepth, p.Cands, p.Seed) + + if opts.DotOut != "" { + if err := viz.EmitDOT(root, opts.DotOut); err != nil { + return fmt.Errorf("write DOT: %w", err) + } + fmt.Printf("DOT export geschrieben: %s\n", opts.DotOut) + + if opts.TreePNG != "" { + if err := viz.RunGraphviz(opts.DotOut, opts.TreePNG); err != nil { + log.Printf("Graphviz fehlgeschlagen: %v", err) + } else { + fmt.Printf("Graphviz PNG gebaut: %s\n", opts.TreePNG) + } + } + } + + if opts.Overlay != "" { + if err := viz.RenderPNG(m, root, opts.Overlay); err != nil { + return fmt.Errorf("write overlay PNG: %w", err) + } + fmt.Printf("Overlay PNG geschrieben: %s\n", opts.Overlay) + } + return nil + } + + return nil +} diff --git a/main.go b/main.go index 57cd91f..8ce7c30 100644 --- a/main.go +++ b/main.go @@ -1,16 +1,12 @@ package main import ( - "bspviz/internal/bsp" + "bspviz/internal/app" "bspviz/internal/geom" - "bspviz/internal/mapfmt" - "bspviz/internal/viz" - "bspviz/internal/wad" "flag" "fmt" "log" "os" - "path/filepath" "strings" ) @@ -49,178 +45,41 @@ func main() { usageAndExit("Flag -wad fehlt. Bitte Pfad zu einer Doom-kompatiblen WAD-Datei angeben.", 2) } - w, err := wad.Open(*wadPath) - if err != nil { - log.Fatalf("open wad: %v", err) - } - defer w.Close() - - if *listOnly { - fmt.Printf("WAD: %s\n", *wadPath) - for i, d := range w.Dir() { - fmt.Printf("%3d: %-8s size=%-7d pos=%-8d\n", i, d.Name(), d.Size, d.FilePos) - } - return - } - - start, end, err := w.FindMap(*mapMarker) - if err != nil { - log.Fatalf("find map: %v", err) - } - fmt.Printf("Map %s: Directory [%d, %d)\n", strings.ToUpper(*mapMarker), start, end) - - //info über die daten in WAD - if *info { - lumps, err := w.LoadMapLumps(*mapMarker, "VERTEXES", "LINEDEFS") - if err != nil { - log.Fatalf("load map lumps: %v", err) - } - vb := lumps["VERTEXES"] - lb := lumps["LINEDEFS"] - m, err := mapfmt.LoadMap(lumps) - if err != nil { - log.Fatal(err) - } - - verts := len(vb) / 4 - lines := len(lb) / 14 - fmt.Printf("VERTEXES: bytes=%d count=%d\n", len(vb), verts) - fmt.Printf("LINEDEFS: bytes=%d count=%d\n", len(lb), lines) - - fmt.Printf("Map has %d vertices and %d linedefs\n", len(m.Vertices), len(m.Linedefs)) - fmt.Printf("First vertex: %+v\n", m.Vertices[0]) - fmt.Printf("First linedef: %+v\n", m.Linedefs[0]) - - if len(vb)%4 != 0 { - fmt.Println("WARN: VERTEXES size ist kein Vielfaches von 4 → Format prüfen") - } - if len(lb)%14 != 0 { - fmt.Println("WARN: LINEDEFS size ist kein Vielfaches von 14 → Format prüfen") - } - } - - // Generiert einzelne Lump Dateien zum Debugen - if *extract != "" { + var extractLumps []string + if strings.TrimSpace(*extract) != "" { want := strings.Split(*extract, ",") - for i := range want { - want[i] = strings.ToUpper(strings.TrimSpace(want[i])) - } - lumps, err := w.LoadMapLumps(*mapMarker, want...) - if err != nil { - log.Fatalf("extract: %v", err) - } - if err := os.MkdirAll(*outdir, 0o755); err != nil { - log.Fatalf("mkdir %s: %v", *outdir, err) - } - for name, data := range lumps { - dst := filepath.Join(*outdir, fmt.Sprintf("%s.lmp", name)) - if err := os.WriteFile(dst, data, 0o644); err != nil { - log.Fatalf("write %s: %v", dst, err) - } - fmt.Printf("wrote %s (%d bytes)\n", dst, len(data)) - } - } - - // Zum debug test der geo functions - if *geomtest { - raw, err := w.LoadMapLumps(*mapMarker, "VERTEXES", "LINEDEFS") - if err != nil { - log.Fatalf("load map lumps: %v", err) - } - m, err := mapfmt.LoadMap(raw) - if err != nil { - log.Fatalf("parse map: %v", err) - } - - segs := mapfmt.LinedefsToSegs(m.Vertices, m.Linedefs) - fmt.Printf("GEOM: vertices=%d linedefs=%d segs=%d\n", len(m.Vertices), len(m.Linedefs), len(segs)) - if len(segs) == 0 { - fmt.Println("GEOM: keine Segmente gefunden – prüfe LINEDEFS/VERTEXES") - return - } - - pts := make([]geom.Vec, 0, len(m.Vertices)) - for _, v := range m.Vertices { - pts = append(pts, geom.V(float64(v.X), float64(v.Y))) - } - bb := geom.Bounds(pts) - w := bb.Max.X - bb.Min.X - h := bb.Max.Y - bb.Min.Y - fmt.Printf("AABB: min=(%.1f,%.1f) max=(%.1f,%.1f) size=(%.1f×%.1f)\n", - bb.Min.X, bb.Min.Y, bb.Max.X, bb.Max.Y, w, h) - - O := segs[0].A - D := geom.Sub(segs[0].B, segs[0].A) - if geom.Len(D) < 1e-9 && len(segs) > 1 { - O = segs[1].A - D = geom.Sub(segs[1].B, segs[1].A) - } - var left, right, splits, degens int - for _, s := range segs { - f, b := geom.SplitSeg(s, O, D) - if len(f) == 0 && len(b) == 0 { - degens++ + extractLumps = make([]string, 0, len(want)) + for _, name := range want { + n := strings.ToUpper(strings.TrimSpace(name)) + if n == "" { continue } - left += len(f) - right += len(b) - if len(f) > 0 && len(b) > 0 { - splits++ - } + extractLumps = append(extractLumps, n) } - fmt.Printf("PROBE-SPLIT: O=(%.1f,%.1f) D=(%.1f,%.1f)\n", O.X, O.Y, D.X, D.Y) - fmt.Printf("PROBE-SPLIT: left=%d right=%d splits=%d degens=%d (EPS=%.1e)\n", - left, right, splits, degens, geom.EPS) - - return } - if *buildbsp { - raw, err := w.LoadMapLumps(*mapMarker, "VERTEXES", "LINEDEFS") - if err != nil { - log.Fatalf("load map lumps: %v", err) - } - m, err := mapfmt.LoadMap(raw) - if err != nil { - log.Fatalf("parse map: %v", err) - } - - segs := mapfmt.LinedefsToSegs(m.Vertices, m.Linedefs) - p := bsp.Params{ - Alpha: *alpha, Beta: *beta, Eps: *eps, - MaxDepth: *depth, LeafMax: *leaf, Cands: *cands, Seed: *seed, - } - root := bsp.Build(segs, p) - st := bsp.Measure(root) - - fmt.Printf("BSP built.\n") - fmt.Printf(" nodes=%d leaves=%d maxDepth=%d totalLeafSegs=%d\n", - st.Nodes, st.Leaves, st.MaxDepth, st.TotalSegs) - fmt.Printf(" params: alpha=%.2f beta=%.2f eps=%.1e leafMax=%d maxDepth=%d cands=%d seed=%d\n", - p.Alpha, p.Beta, p.Eps, p.LeafMax, p.MaxDepth, p.Cands, p.Seed) - - if *dotOut != "" { - if err := viz.EmitDOT(root, *dotOut); err != nil { - log.Fatalf("write DOT: %v", err) - } - 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 + opts := app.Options{ + WadPath: *wadPath, + MapMarker: *mapMarker, + ListOnly: *listOnly, + Info: *info, + Extract: extractLumps, + OutDir: *outdir, + GeomTest: *geomtest, + BuildBSP: *buildbsp, + Alpha: *alpha, + Beta: *beta, + Eps: *eps, + LeafMax: *leaf, + MaxDepth: *depth, + Cands: *cands, + Seed: *seed, + DotOut: *dotOut, + TreePNG: *treePNG, + Overlay: *overlay, } + if err := app.Run(opts); err != nil { + log.Fatal(err) + } }