Compare commits
4 Commits
main
...
ui-wrapper
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25e965506e | ||
| eec2ca5cc9 | |||
| a5a28840b1 | |||
| bfb1c7bef6 |
BIN
MYMAP3.wad
BIN
MYMAP3.wad
Binary file not shown.
@@ -59,7 +59,7 @@ dot -Tpng tree.dot -o tree.png
|
||||
|
||||
### Entwicklung
|
||||
- Code formatieren: `gofmt -w .`
|
||||
- Tests ausführen: `go test ./...` (Top-Level-Paket `bspviz` enthält bewusst keine eigenen Tests; der Aufruf über `./...` zieht die Test-Suites der `internal/*`-Pakete automatisch nach.)
|
||||
- Abhängig vom Fokus können Teilbereiche separat geprüft werden, z. B. `go test ./internal/geom` oder `go test ./internal/bsp`.
|
||||
- Die vorhandenen Tests decken u. a. Geometrie-Primitive, BSP-Build-Heuristiken, Map-/WAD-Parsing sowie DOT-/PNG-Export ab.
|
||||
- Temporäre Artefakte (DOT/PNG) sind über `.gitignore` bereits ausgeschlossen.
|
||||
- Tests ausführen: `go test ./...`
|
||||
- Temporäre Artefakte (DOT/PNG) sind über `.gitignore` bereits ausgeschlossen.
|
||||
|
||||
Beiträge, Erweiterungen oder neue Heuristiken sind willkommen.
|
||||
|
||||
471
cmd/tui/main.go
Normal file
471
cmd/tui/main.go
Normal file
@@ -0,0 +1,471 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/filepicker"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"bspviz/internal/app"
|
||||
"bspviz/internal/geom"
|
||||
)
|
||||
|
||||
const (
|
||||
inputWadPath = iota
|
||||
inputMapMarker
|
||||
inputExtract
|
||||
inputOutDir
|
||||
inputDotOut
|
||||
inputTreePNG
|
||||
inputOverlay
|
||||
inputAlpha
|
||||
inputBeta
|
||||
inputEps
|
||||
inputLeaf
|
||||
inputDepth
|
||||
inputCands
|
||||
inputSeed
|
||||
inputCount
|
||||
)
|
||||
|
||||
const (
|
||||
toggleListOnly = iota
|
||||
toggleInfo
|
||||
toggleGeomTest
|
||||
toggleBuildBSP
|
||||
toggleCount
|
||||
)
|
||||
|
||||
var (
|
||||
indicatorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true)
|
||||
statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
|
||||
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Bold(true)
|
||||
boxStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder()).Padding(0, 1)
|
||||
)
|
||||
|
||||
type runResultMsg struct {
|
||||
output string
|
||||
err error
|
||||
}
|
||||
|
||||
type model struct {
|
||||
inputs []textinput.Model
|
||||
inputLabels []string
|
||||
toggles []bool
|
||||
toggleLabels []string
|
||||
focusIndex int
|
||||
status string
|
||||
output string
|
||||
errMsg string
|
||||
running bool
|
||||
showPicker bool
|
||||
picker filepicker.Model
|
||||
}
|
||||
|
||||
func initialModel() model {
|
||||
m := model{
|
||||
inputs: make([]textinput.Model, inputCount),
|
||||
inputLabels: []string{
|
||||
"WAD", "Map", "Extract", "OutDir", "DotOut", "TreePNG", "Overlay",
|
||||
"Alpha", "Beta", "Eps", "LeafMax", "MaxDepth", "Cands", "Seed",
|
||||
},
|
||||
toggles: make([]bool, toggleCount),
|
||||
toggleLabels: []string{"List directory", "Info", "Geom test", "Build BSP"},
|
||||
status: "Bereit",
|
||||
}
|
||||
|
||||
for i := range m.inputs {
|
||||
ti := textinput.New()
|
||||
ti.CharLimit = 0
|
||||
ti.Prompt = ""
|
||||
switch i {
|
||||
case inputWadPath:
|
||||
ti.Placeholder = "Pfad zur WAD"
|
||||
case inputMapMarker:
|
||||
ti.SetValue("MAP01")
|
||||
case inputExtract:
|
||||
ti.Placeholder = "z.B. VERTEXES,LINEDEFS"
|
||||
case inputOutDir:
|
||||
ti.SetValue(".")
|
||||
case inputAlpha:
|
||||
ti.SetValue(fmt.Sprintf("%g", 10.0))
|
||||
case inputBeta:
|
||||
ti.SetValue(fmt.Sprintf("%g", 1.0))
|
||||
case inputEps:
|
||||
ti.SetValue(fmt.Sprintf("%g", geom.EPS))
|
||||
case inputLeaf:
|
||||
ti.SetValue("12")
|
||||
case inputDepth:
|
||||
ti.SetValue("32")
|
||||
case inputCands:
|
||||
ti.SetValue("16")
|
||||
case inputSeed:
|
||||
ti.SetValue("0")
|
||||
}
|
||||
m.inputs[i] = ti
|
||||
}
|
||||
|
||||
picker := filepicker.New()
|
||||
picker.AllowedTypes = []string{".wad", ".WAD"}
|
||||
picker.DirAllowed = false
|
||||
picker.FileAllowed = true
|
||||
picker.ShowPermissions = false
|
||||
picker.ShowSize = true
|
||||
picker.ShowHidden = false
|
||||
picker.AutoHeight = false
|
||||
picker.Height = 12
|
||||
m.picker = picker
|
||||
|
||||
m.setFocus(0)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if m.showPicker {
|
||||
switch key := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
if key.Type == tea.KeyEsc {
|
||||
m.showPicker = false
|
||||
m.status = "Dateiauswahl abgebrochen"
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.picker, cmd = m.picker.Update(msg)
|
||||
if selected, path := m.picker.DidSelectFile(msg); selected {
|
||||
m.inputs[inputWadPath].SetValue(path)
|
||||
m.showPicker = false
|
||||
m.status = "WAD gewählt"
|
||||
m.errMsg = ""
|
||||
m.setFocus(inputWadPath)
|
||||
return m, cmd
|
||||
}
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc":
|
||||
return m, tea.Quit
|
||||
case "tab", "down":
|
||||
m.setFocus(m.focusIndex + 1)
|
||||
case "shift+tab", "up":
|
||||
m.setFocus(m.focusIndex - 1)
|
||||
case "enter":
|
||||
if m.focusIndex < len(m.inputs) {
|
||||
m.setFocus(m.focusIndex + 1)
|
||||
} else {
|
||||
idx := m.focusIndex - len(m.inputs)
|
||||
if idx >= 0 && idx < len(m.toggles) {
|
||||
m.toggles[idx] = !m.toggles[idx]
|
||||
}
|
||||
}
|
||||
case " ":
|
||||
if m.focusIndex >= len(m.inputs) {
|
||||
idx := m.focusIndex - len(m.inputs)
|
||||
if idx >= 0 && idx < len(m.toggles) {
|
||||
m.toggles[idx] = !m.toggles[idx]
|
||||
}
|
||||
}
|
||||
case "f2":
|
||||
if !m.running {
|
||||
current := strings.TrimSpace(m.inputs[inputWadPath].Value())
|
||||
if current != "" {
|
||||
info, err := os.Stat(current)
|
||||
if err == nil {
|
||||
if info.IsDir() {
|
||||
m.picker.CurrentDirectory = current
|
||||
} else {
|
||||
m.picker.CurrentDirectory = filepath.Dir(current)
|
||||
}
|
||||
} else {
|
||||
m.picker.CurrentDirectory = filepath.Dir(current)
|
||||
}
|
||||
} else {
|
||||
m.picker.CurrentDirectory = "."
|
||||
}
|
||||
m.showPicker = true
|
||||
m.status = "Datei auswählen"
|
||||
m.errMsg = ""
|
||||
return m, m.picker.Init()
|
||||
}
|
||||
case "ctrl+r":
|
||||
if !m.running {
|
||||
var cmd tea.Cmd
|
||||
m, cmd = m.run()
|
||||
return m, cmd
|
||||
}
|
||||
}
|
||||
case runResultMsg:
|
||||
m.running = false
|
||||
if msg.err != nil {
|
||||
m.errMsg = msg.err.Error()
|
||||
m.status = "Fehlgeschlagen"
|
||||
} else {
|
||||
m.errMsg = ""
|
||||
m.status = "Fertig"
|
||||
}
|
||||
m.output = strings.TrimSpace(msg.output)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
cmds := make([]tea.Cmd, len(m.inputs))
|
||||
for i := range m.inputs {
|
||||
m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("BSPViz Bubble Tea UI\n")
|
||||
b.WriteString("====================\n\n")
|
||||
|
||||
labelWidth := 12
|
||||
for i := range m.inputs {
|
||||
indicator := " "
|
||||
if m.focusIndex == i {
|
||||
indicator = indicatorStyle.Render(">")
|
||||
}
|
||||
label := fmt.Sprintf("%-*s", labelWidth, m.inputLabels[i]+":")
|
||||
fmt.Fprintf(&b, "%s %s %s\n", indicator, label, m.inputs[i].View())
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
for i := range m.toggles {
|
||||
indicator := " "
|
||||
if m.focusIndex == len(m.inputs)+i {
|
||||
indicator = indicatorStyle.Render(">")
|
||||
}
|
||||
mark := " "
|
||||
if m.toggles[i] {
|
||||
mark = "x"
|
||||
}
|
||||
fmt.Fprintf(&b, "%s [%s] %s\n", indicator, mark, m.toggleLabels[i])
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
|
||||
statusText := "Bereit"
|
||||
if m.running {
|
||||
statusText = "Läuft..."
|
||||
} else if m.status != "" {
|
||||
statusText = m.status
|
||||
}
|
||||
b.WriteString(statusStyle.Render("Status: " + statusText))
|
||||
b.WriteString("\n")
|
||||
|
||||
if m.errMsg != "" {
|
||||
b.WriteString(errorStyle.Render("Fehler: " + m.errMsg))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
if m.showPicker {
|
||||
b.WriteString("\nDatei auswählen (Enter übernehmen, Esc abbrechen)\n")
|
||||
b.WriteString(boxStyle.Render(m.picker.View()))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
if m.output != "" {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(boxStyle.Render(m.output))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString("Steuerung: TAB/Shift+TAB navigieren • Space toggeln • Ctrl+R ausführen • F2 Datei wählen • Ctrl+C beenden")
|
||||
b.WriteString("\n")
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m model) run() (model, tea.Cmd) {
|
||||
opts, err := m.buildOptions()
|
||||
if err != nil {
|
||||
m.errMsg = err.Error()
|
||||
m.status = "Fehler"
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.running = true
|
||||
m.status = "Läuft..."
|
||||
m.errMsg = ""
|
||||
m.output = ""
|
||||
|
||||
return m, runAppCmd(opts)
|
||||
}
|
||||
|
||||
func (m model) buildOptions() (app.Options, error) {
|
||||
var opts app.Options
|
||||
|
||||
wadPath := strings.TrimSpace(m.inputs[inputWadPath].Value())
|
||||
if wadPath == "" {
|
||||
return opts, fmt.Errorf("WAD-Pfad darf nicht leer sein")
|
||||
}
|
||||
opts.WadPath = wadPath
|
||||
|
||||
mapMarker := strings.TrimSpace(m.inputs[inputMapMarker].Value())
|
||||
if mapMarker == "" {
|
||||
mapMarker = "MAP01"
|
||||
}
|
||||
opts.MapMarker = mapMarker
|
||||
|
||||
extractRaw := strings.TrimSpace(m.inputs[inputExtract].Value())
|
||||
if extractRaw != "" {
|
||||
parts := strings.Split(extractRaw, ",")
|
||||
opts.Extract = make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
n := strings.ToUpper(strings.TrimSpace(p))
|
||||
if n == "" {
|
||||
continue
|
||||
}
|
||||
opts.Extract = append(opts.Extract, n)
|
||||
}
|
||||
}
|
||||
|
||||
outDir := strings.TrimSpace(m.inputs[inputOutDir].Value())
|
||||
if outDir == "" {
|
||||
outDir = "."
|
||||
}
|
||||
opts.OutDir = outDir
|
||||
|
||||
opts.DotOut = strings.TrimSpace(m.inputs[inputDotOut].Value())
|
||||
opts.TreePNG = strings.TrimSpace(m.inputs[inputTreePNG].Value())
|
||||
opts.Overlay = strings.TrimSpace(m.inputs[inputOverlay].Value())
|
||||
|
||||
alphaStr := strings.TrimSpace(m.inputs[inputAlpha].Value())
|
||||
if alphaStr == "" {
|
||||
opts.Alpha = 10.0
|
||||
} else {
|
||||
v, err := strconv.ParseFloat(alphaStr, 64)
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("Alpha ungültig: %w", err)
|
||||
}
|
||||
opts.Alpha = v
|
||||
}
|
||||
|
||||
betaStr := strings.TrimSpace(m.inputs[inputBeta].Value())
|
||||
if betaStr == "" {
|
||||
opts.Beta = 1.0
|
||||
} else {
|
||||
v, err := strconv.ParseFloat(betaStr, 64)
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("Beta ungültig: %w", err)
|
||||
}
|
||||
opts.Beta = v
|
||||
}
|
||||
|
||||
epsStr := strings.TrimSpace(m.inputs[inputEps].Value())
|
||||
if epsStr == "" {
|
||||
opts.Eps = geom.EPS
|
||||
} else {
|
||||
v, err := strconv.ParseFloat(epsStr, 64)
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("Eps ungültig: %w", err)
|
||||
}
|
||||
opts.Eps = v
|
||||
}
|
||||
|
||||
leafStr := strings.TrimSpace(m.inputs[inputLeaf].Value())
|
||||
if leafStr == "" {
|
||||
opts.LeafMax = 12
|
||||
} else {
|
||||
v, err := strconv.Atoi(leafStr)
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("LeafMax ungültig: %w", err)
|
||||
}
|
||||
opts.LeafMax = v
|
||||
}
|
||||
|
||||
depthStr := strings.TrimSpace(m.inputs[inputDepth].Value())
|
||||
if depthStr == "" {
|
||||
opts.MaxDepth = 32
|
||||
} else {
|
||||
v, err := strconv.Atoi(depthStr)
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("MaxDepth ungültig: %w", err)
|
||||
}
|
||||
opts.MaxDepth = v
|
||||
}
|
||||
|
||||
candsStr := strings.TrimSpace(m.inputs[inputCands].Value())
|
||||
if candsStr == "" {
|
||||
opts.Cands = 16
|
||||
} else {
|
||||
v, err := strconv.Atoi(candsStr)
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("Cands ungültig: %w", err)
|
||||
}
|
||||
opts.Cands = v
|
||||
}
|
||||
|
||||
seedStr := strings.TrimSpace(m.inputs[inputSeed].Value())
|
||||
if seedStr == "" {
|
||||
opts.Seed = 0
|
||||
} else {
|
||||
v, err := strconv.ParseInt(seedStr, 10, 64)
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("Seed ungültig: %w", err)
|
||||
}
|
||||
opts.Seed = v
|
||||
}
|
||||
|
||||
opts.ListOnly = m.toggles[toggleListOnly]
|
||||
opts.Info = m.toggles[toggleInfo]
|
||||
opts.GeomTest = m.toggles[toggleGeomTest]
|
||||
opts.BuildBSP = m.toggles[toggleBuildBSP]
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func runAppCmd(opts app.Options) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
var buf bytes.Buffer
|
||||
opts.Out = &buf
|
||||
err := app.Run(opts)
|
||||
return runResultMsg{output: buf.String(), err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) setFocus(index int) {
|
||||
total := len(m.inputs) + len(m.toggles)
|
||||
if total == 0 {
|
||||
return
|
||||
}
|
||||
if index < 0 {
|
||||
index = total - 1
|
||||
}
|
||||
if index >= total {
|
||||
index = 0
|
||||
}
|
||||
m.focusIndex = index
|
||||
for i := range m.inputs {
|
||||
if i == m.focusIndex {
|
||||
m.inputs[i].Focus()
|
||||
} else {
|
||||
m.inputs[i].Blur()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if _, err := tea.NewProgram(initialModel(), tea.WithAltScreen()).Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "UI error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
# BSPViz Projektdokumentation
|
||||
|
||||
## 1. Projektueberblick
|
||||
- **Zielsetzung:** BSPViz ist ein Go-gestuetztes CLI-Tool zum Analysieren klassischer Doom-WAD-Dateien. Es entstand im Rahmen einer Seminararbeit, um Parsing, Geometrieverarbeitung und BSP-Heuristiken prototypisch zu untersuchen.
|
||||
- **Funktionsumfang:** Lesen von IWAD/PWAD-Archiven, Extraktion einzelner Map-Lumps, Generieren von Geometrie-Statistiken, Aufbau und Bewertung von BSP-Baeumen sowie Export als DOT- oder Overlay-PNG.
|
||||
- **Technologiestack:** Go 1.25, optionale Nutzung von Graphviz (dot) fuer PNG-Renderings, reine Standardbibliothek ohne externe Abhaengigkeiten.
|
||||
- **Artefakte im Repository:** Beispiel-WAD-Dateien (`MYMAP*.wad`), Einstiegspunkt `main.go`, modulare Pakete unter `internal/`.
|
||||
|
||||
## 2. Architektur und Codeorganisation
|
||||
- **CLI (main.go):** Verantwortlich fuer Flag-Parsing, orchestriert WAD-Laden, optionales Listing, Informationsausgabe, Lump-Export und BSP-Erstellung. Triggert Visualisierungen ueber `internal/viz`.
|
||||
- **WAD-Layer (`internal/wad`):** Kapselt Dateizugriff, Directory-Parsen und sichere Lump-Lesungen. `Open` validiert Header und Directory (Dateigroesse, Grenzen, Marker), `LoadMapLumps` liefert selektierte Rohbytes fuer Map-spezifische Lumps.
|
||||
- **Map-Parsing (`internal/mapfmt`):** Wandelt Lump-Rohdaten (`VERTEXES`, `LINEDEFS`) in typsichere Strukturen (`MapData`) und erzeugt Segmentlisten fuer BSP-Builds.
|
||||
- **Geometrie (`internal/geom`):** Stellt Vektor-Operationen, AABB-Berechnung, Seitenklassifikation, Segment-Splitting und Line-Segment-Schnittpunkte bereit. Alle Funktionen operieren auf float64 und nutzen ein globales Epsilon (`EPS`).
|
||||
- **BSP-Engine (`internal/bsp`):** Implementiert heuristischen Baumaufbau. `Build` rekursiert ueber Segmente, waehlt Splitkandidaten per Kostenfunktion (`evalCost`) und liefert Knoten/Leaf-Strukturen inklusive Metriken (`Measure`).
|
||||
- **Visualisierung (`internal/viz`):**
|
||||
- `dot.go`: Serialisiert BSP-Baeume in Graphviz-DOT, optionaler Aufruf von `dot` fuer PNG.
|
||||
- `png.go`: Rendered Map-Linien, Split-Ebenen und Leaf-Segmente in ein Overlay-Bild (RGB, Bresenham-basierte Linien).
|
||||
|
||||
Die Module folgen einer klaren Einbahn-Abhaengigkeit: `main` -> `wad` -> `mapfmt`/`geom` -> `bsp` -> `viz`. Tests fuer Kernpakete liegen jeweils in `*_test.go`.
|
||||
|
||||
## 3. Ablauf und Datenfluss
|
||||
1. **CLI-Start:** Flags validieren (`-wad` Pflicht, Default-Map `MAP01`). Bei `-list` erfolgt ein frueher Exit nach Directory-Ausgabe.
|
||||
2. **Map-Lesen:** `wad.Open` laedt das WAD, `FindMap` bestimmt Marker-Bereich, `LoadMapLumps` liest benoetigte Lumps in Memory.
|
||||
3. **Parsing:** `mapfmt.LoadMap` interpretiert Rohbytes zu `MapData` (Vertices, Linedefs). `LinedefsToSegs` erstellt Segmentreprasentationen fuer weitere Verarbeitung.
|
||||
4. **Geometrieanalyse:** Je nach Flag:
|
||||
- `-info`: Zaehlt Vertex/Linedef-Anzahlen, Pruefung auf Formatabweichungen.
|
||||
- `-geomtest`: Fuehrt Segment-Split-Probe inklusive Bounding-Box-Analyse aus.
|
||||
- `-extract`: Schreibt gewuenschte Lumps als `.lmp` in Zielordner.
|
||||
5. **BSP-Bau (`-buildbsp`):**
|
||||
- `bsp.Build` waehlt Split-Ebenen anhand von Gewichtung (`alpha` Splits, `beta` Balance).
|
||||
- Rekursion stoppt kontrolliert via `leafmax`, `maxdepth`.
|
||||
- `bsp.Measure` liefert Kennzahlen wie Knotenzahl, Leaf-Segmente, Baumtiefe.
|
||||
6. **Visualisierung:**
|
||||
- `-dot`: Speichert DOT-Datei; `-treepng` ruft optional Graphviz fuer Renderings.
|
||||
- `-overlay`: Zeichnet Map und Splits als PNG mit zufaellig gefaerbten Leaves.
|
||||
|
||||
## 4. Kommandozeilenreferenz
|
||||
| Flag | Pflicht | Beschreibung |
|
||||
|------|---------|--------------|
|
||||
| `-wad <pfad>` | Ja | Pfad zu IWAD/PWAD-Datei. |
|
||||
| `-map <name>` | Nein | Map-Marker (Default `MAP01`). |
|
||||
| `-list` | Nein | Listet Directory und beendet. |
|
||||
| `-info` | Nein | Zeigt Vertex-/Linedef-Statistiken nach Parsing. |
|
||||
| `-extract <L1,L2>` | Nein | Extrahiert benannte Lumps als `.lmp`. Nutzt `-out` als Ziel (Default `.`). |
|
||||
| `-geomtest` | Nein | Fuehrt Geometriediagnose (Seg-Splits, AABB) durch. |
|
||||
| `-buildbsp` | Nein | Startet BSP-Aufbau mit Parametern `-alpha`, `-beta`, `-eps`, `-leafmax`, `-maxdepth`, `-cands`, `-seed`. |
|
||||
| `-dot <pfad>` | Nein | Speichert DOT-Export (setzt `-buildbsp` voraus). |
|
||||
| `-treepng <pfad>` | Nein | Erstellt PNG ueber Graphviz (benoetigt installierten `dot`). |
|
||||
| `-overlay <pfad>` | Nein | Zeichnet Map+Splits als PNG ohne Graphviz-Abhaengigkeit. |
|
||||
|
||||
### Beispiel-Workflows
|
||||
- Map-Verzeichnis einsehen: `go run ./main.go -wad MYMAP.wad -list`
|
||||
- Statistik-Aufruf: `go run ./main.go -wad MYMAP3.wad -map MAP01 -info`
|
||||
- Lumps extrahieren: `go run ./main.go -wad MYMAP2.wad -map E1M1 -extract VERTEXES,LINEDEFS -out dumps`
|
||||
- BSP bauen & visualisieren:
|
||||
```
|
||||
go run ./main.go -wad MYMAP.wad -map MAP01 -buildbsp -alpha 8 -beta 2 -dot tree.dot -overlay overlay.png
|
||||
dot -Tpng tree.dot -o tree.png # optionales Rendering
|
||||
```
|
||||
|
||||
## 5. Implementierungsdetails
|
||||
- **Robustes WAD-Parsen:** Integritaetschecks fuer Header, Directory-Grenzen sowie Lump-Adressen verhindern Out-of-Bounds-Zugriffe. `trimName` normalisiert Lump-Namen auf 8 Zeichen.
|
||||
- **Geometrie-Splitting:** `geom.SplitSeg` klassifiziert Segmente anhand der orientierten Distanz (`Side`). Schnittpunkte werden nur akzeptiert, falls sie klar innerhalb des Segments liegen (`EPS`-Toleranz).
|
||||
- **BSP-Heuristik:** `selectSplit` sampelt Segmente in regulierten Schritten (`Cands`). Die Kostenfunktion kombiniert Split-Anzahl und Balance; Parameter koennen zur Evaluierung verschiedener Heuristiken variiert werden.
|
||||
- **Overlay-Rendering:** Transformation `worldToScreen` skaliert Doom-Koordinaten auf Bildgroesse, invertiert Y-Achse und ergaenzt Rand. Splits erscheinen als gelbe Linien, Leafs werden farblich zufaellig hervorgehoben.
|
||||
- **Determinismus:** `-seed` beeinflusst den RNG der BSP-Heuristik; `seed=0` wird intern auf `1` gesetzt, um nachvollziehbare Ergebnisse zu erhalten.
|
||||
|
||||
## 6. Entwicklungs- und Testleitfaden
|
||||
- **Abhaengigkeiten:** Go >= 1.25, optional Graphviz (`dot`) fuer DOT-zu-PNG-Konvertierung.
|
||||
- **Build:** `go build ./...` erzeugt ein lokales Binary.
|
||||
- **Direktausfuehrung:** `go run ./main.go <flags>` vermeidet separaten Build.
|
||||
- **Codeformat:** `gofmt -w .`
|
||||
- **Tests:** `go test ./internal/...` (Pakete `wad`, `mapfmt`, `geom`, `bsp`, `viz` verfuegen ueber eigenstaendige Test-Suites). Top-Level-`main` enthaelt bewusst keine Tests.
|
||||
- **Beispieldaten:** Die mitgelieferten `MYMAP*.wad` dienen als kleinskalige Input-Setups fuer manuelle und automatisierte Checks.
|
||||
|
||||
## 7. Grenzen und Erweiterungsideen
|
||||
- **Nicht abgedeckt:** Parsing weiterer Lumps (SEGS, SSECTORS etc.), 3D-Features moderner Ports, automatisierte DOT->PNG-Pipeline ohne Graphviz-Installation.
|
||||
- **Moegliche Erweiterungen:**
|
||||
1. Zusae tzliche Heuristiken (z.B. zufaellige Kandidatenauswahl, Hybridkosten).
|
||||
2. Erweiterte Statistiken (Split-Histogramme, Leaf-Flachenberechnung).
|
||||
3. CLI-Subcommands oder Konfigdatei-Unterstuetzung.
|
||||
4. Export weiterer Formate (JSON-Serialisierung der BSP-Struktur).
|
||||
|
||||
## 8. Projektressourcen
|
||||
- **Quellcode:** Einstieg `main.go`, Modulverzeichnis `internal/`.
|
||||
- **Dokumentation:** Diese Datei, bestehende README.md als Schnellstart.
|
||||
- **Kontakt & Lizenz:** Nicht im Repository hinterlegt; fuer Seminarzwecke bitte Betreuer bzw. Repository-Inhaber ansprechen.
|
||||
|
||||
> Hinweis: Alle Beschreibungen basieren auf dem Stand der Quelldateien vom 30.09.2025. Aenderungen im Code sollten zeitnah in dieser Dokumentation nachvollzogen werden.
|
||||
|
||||
31
go.mod
31
go.mod
@@ -1,3 +1,34 @@
|
||||
module bspviz
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbles v0.18.0
|
||||
github.com/charmbracelet/bubbletea v0.25.0
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
)
|
||||
|
||||
57
go.sum
Normal file
57
go.sum
Normal file
@@ -0,0 +1,57 @@
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
|
||||
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
|
||||
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
|
||||
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
216
internal/app/app.go
Normal file
216
internal/app/app.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"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
|
||||
Out io.Writer
|
||||
}
|
||||
|
||||
func Run(opts Options) error {
|
||||
if opts.Out == nil {
|
||||
opts.Out = os.Stdout
|
||||
}
|
||||
|
||||
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.Fprintf(opts.Out, "WAD: %s\n", opts.WadPath)
|
||||
for i, d := range w.Dir() {
|
||||
fmt.Fprintf(opts.Out, "%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.Fprintf(opts.Out, "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.Fprintf(opts.Out, "VERTEXES: bytes=%d count=%d\n", len(vb), verts)
|
||||
fmt.Fprintf(opts.Out, "LINEDEFS: bytes=%d count=%d\n", len(lb), lines)
|
||||
|
||||
fmt.Fprintf(opts.Out, "Map has %d vertices and %d linedefs\n", len(m.Vertices), len(m.Linedefs))
|
||||
fmt.Fprintf(opts.Out, "First vertex: %+v\n", m.Vertices[0])
|
||||
fmt.Fprintf(opts.Out, "First linedef: %+v\n", m.Linedefs[0])
|
||||
|
||||
if len(vb)%4 != 0 {
|
||||
fmt.Fprintln(opts.Out, "WARN: VERTEXES size ist kein Vielfaches von 4 → Format prüfen")
|
||||
}
|
||||
if len(lb)%14 != 0 {
|
||||
fmt.Fprintln(opts.Out, "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.Fprintf(opts.Out, "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.Fprintf(opts.Out, "GEOM: vertices=%d linedefs=%d segs=%d\n", len(m.Vertices), len(m.Linedefs), len(segs))
|
||||
if len(segs) == 0 {
|
||||
fmt.Fprintln(opts.Out, "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.Fprintf(opts.Out, "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.Fprintf(opts.Out, "PROBE-SPLIT: O=(%.1f,%.1f) D=(%.1f,%.1f)\n", O.X, O.Y, D.X, D.Y)
|
||||
fmt.Fprintf(opts.Out, "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.Fprintln(opts.Out, "BSP built.")
|
||||
fmt.Fprintf(opts.Out, " nodes=%d leaves=%d maxDepth=%d totalLeafSegs=%d\n",
|
||||
st.Nodes, st.Leaves, st.MaxDepth, st.TotalSegs)
|
||||
fmt.Fprintf(opts.Out, " 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.Fprintf(opts.Out, "DOT export geschrieben: %s\n", opts.DotOut)
|
||||
|
||||
if opts.TreePNG != "" {
|
||||
if err := viz.RunGraphviz(opts.DotOut, opts.TreePNG); err != nil {
|
||||
fmt.Fprintf(opts.Out, "Graphviz fehlgeschlagen: %v\n", err)
|
||||
} else {
|
||||
fmt.Fprintf(opts.Out, "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.Fprintf(opts.Out, "Overlay PNG geschrieben: %s\n", opts.Overlay)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package bsp
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"bspviz/internal/geom"
|
||||
)
|
||||
|
||||
func TestStopUsesDefaults(t *testing.T) {
|
||||
segs := make([]geom.Seg, 13)
|
||||
if stop(segs, 0, Params{}) {
|
||||
t.Fatalf("stop should continue when using default LeafMax with 13 segs")
|
||||
}
|
||||
if !stop(segs, 33, Params{}) {
|
||||
t.Fatalf("stop should trigger when depth exceeds default MaxDepth")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalCostSplitsAndBalance(t *testing.T) {
|
||||
line := []geom.Seg{
|
||||
{A: geom.V(-1, 1), B: geom.V(1, 1)},
|
||||
{A: geom.V(-1, -1), B: geom.V(1, -1)},
|
||||
{A: geom.V(-1, -1), B: geom.V(1, 1)},
|
||||
}
|
||||
p := Params{Alpha: 10, Beta: 2, Eps: geom.EPS}
|
||||
splits, balance, cost := evalCost(line, geom.V(0, 0), geom.V(1, 0), p)
|
||||
if splits != 1 {
|
||||
t.Fatalf("splits=%d want 1", splits)
|
||||
}
|
||||
if balance != 0 {
|
||||
t.Fatalf("balance=%d want 0", balance)
|
||||
}
|
||||
if cost != 10 {
|
||||
t.Fatalf("cost=%v want 10", cost)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitAllProducesFrontAndBack(t *testing.T) {
|
||||
segs := []geom.Seg{{A: geom.V(-1, -1), B: geom.V(1, 1)}}
|
||||
left, right, splits := splitAll(segs, geom.V(0, 0), geom.V(1, 0), Params{Eps: geom.EPS})
|
||||
if splits != 1 {
|
||||
t.Fatalf("splits=%d want 1", splits)
|
||||
}
|
||||
if len(left) != 1 || len(right) != 1 {
|
||||
t.Fatalf("expected split into two segments, left=%d right=%d", len(left), len(right))
|
||||
}
|
||||
share := func(a, b geom.Vec) bool {
|
||||
return math.Abs(a.X-b.X) < geom.EPS && math.Abs(a.Y-b.Y) < geom.EPS
|
||||
}
|
||||
if !share(left[0].A, right[0].B) && !share(left[0].B, right[0].A) {
|
||||
t.Fatalf("split point mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRespectsLeafAndDepth(t *testing.T) {
|
||||
segs := []geom.Seg{
|
||||
{A: geom.V(-2, -2), B: geom.V(2, -2)},
|
||||
{A: geom.V(2, -2), B: geom.V(2, 2)},
|
||||
{A: geom.V(2, 2), B: geom.V(-2, 2)},
|
||||
{A: geom.V(-2, 2), B: geom.V(-2, -2)},
|
||||
{A: geom.V(-2, -2), B: geom.V(2, 2)},
|
||||
{A: geom.V(-2, 2), B: geom.V(2, -2)},
|
||||
}
|
||||
p := Params{LeafMax: 2, MaxDepth: 3, Seed: 1, Cands: 4, Alpha: 5, Beta: 1, Eps: geom.EPS}
|
||||
root := Build(segs, p)
|
||||
if root == nil {
|
||||
t.Fatalf("Build returned nil")
|
||||
}
|
||||
|
||||
stats := Measure(root)
|
||||
if stats.MaxDepth > p.MaxDepth {
|
||||
t.Fatalf("MaxDepth=%d exceeds limit %d", stats.MaxDepth, p.MaxDepth)
|
||||
}
|
||||
if stats.Leaves == 0 {
|
||||
t.Fatalf("expected some leaves in tree")
|
||||
}
|
||||
verifyLeaves(t, root, 0, p)
|
||||
}
|
||||
|
||||
func verifyLeaves(t *testing.T, n *Node, depth int, p Params) {
|
||||
t.Helper()
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
if n.Leaf != nil {
|
||||
if depth < p.MaxDepth && len(n.Leaf.Segs) > p.LeafMax {
|
||||
t.Fatalf("leaf at depth %d has %d segs (limit %d)", depth, len(n.Leaf.Segs), p.LeafMax)
|
||||
}
|
||||
return
|
||||
}
|
||||
if depth >= p.MaxDepth {
|
||||
t.Fatalf("internal node at depth %d exceeds MaxDepth %d", depth, p.MaxDepth)
|
||||
}
|
||||
verifyLeaves(t, n.Left, depth+1, p)
|
||||
verifyLeaves(t, n.Right, depth+1, p)
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package geom
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSide(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
P, O, D Vec
|
||||
want float64
|
||||
}{
|
||||
{"point left of line", V(1, 1), V(0, 0), V(1, 0), 1},
|
||||
{"point right of line", V(1, -1), V(0, 0), V(1, 0), -1},
|
||||
{"point on line", V(2, 0), V(0, 0), V(1, 0), 0},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := Side(tc.P, tc.O, tc.D); got < tc.want-EPS || got > tc.want+EPS {
|
||||
t.Fatalf("%s: Side()=%v want %v", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSegLineIntersect(t *testing.T) {
|
||||
O := V(0, 0)
|
||||
D := V(1, 0)
|
||||
tests := []struct {
|
||||
name string
|
||||
A, B Vec
|
||||
wantOK bool
|
||||
wantT float64
|
||||
}{
|
||||
{"hits interior", V(0, -1), V(0, 1), true, 0.5},
|
||||
{"parallel", V(0, 1), V(1, 1), false, 0},
|
||||
{"touches endpoint", V(0, 0), V(0, 1), false, 0},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
ok, tVal := SegLineIntersect(tc.A, tc.B, O, D)
|
||||
if ok != tc.wantOK {
|
||||
t.Fatalf("%s: ok=%v want %v", tc.name, ok, tc.wantOK)
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if diff := tVal - tc.wantT; diff > 1e-6 || diff < -1e-6 {
|
||||
t.Fatalf("%s: t=%v want %v", tc.name, tVal, tc.wantT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSeg(t *testing.T) {
|
||||
O := V(0, 0)
|
||||
D := V(1, 0)
|
||||
seg := Seg{A: V(0, -1), B: V(0, 1)}
|
||||
front, back := SplitSeg(seg, O, D)
|
||||
if len(front) != 1 || len(back) != 1 {
|
||||
t.Fatalf("expected split into two pieces got front=%d back=%d", len(front), len(back))
|
||||
}
|
||||
share := func(a, b Vec) bool {
|
||||
return math.Abs(a.X-b.X) < EPS && math.Abs(a.Y-b.Y) < EPS
|
||||
}
|
||||
if !share(front[0].A, back[0].B) && !share(front[0].B, back[0].A) {
|
||||
t.Fatalf("split point mismatch: front=%v back=%v", front[0], back[0])
|
||||
}
|
||||
|
||||
sameSide := Seg{A: V(0, 1), B: V(1, 1)}
|
||||
front, back = SplitSeg(sameSide, O, D)
|
||||
if len(back) != 0 || len(front) != 1 {
|
||||
t.Fatalf("expected no split for same side got front=%d back=%d", len(front), len(back))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBounds(t *testing.T) {
|
||||
pts := []Vec{V(-1, 2), V(3, -4), V(0, 0)}
|
||||
box := Bounds(pts)
|
||||
if box.Min != V(-1, -4) {
|
||||
t.Fatalf("Min=%v want (-1,-4)", box.Min)
|
||||
}
|
||||
if box.Max != V(3, 2) {
|
||||
t.Fatalf("Max=%v want (3,2)", box.Max)
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package mapfmt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseVertices(t *testing.T) {
|
||||
data := []byte{
|
||||
0, 0, 0, 0, // (0,0)
|
||||
255, 255, 10, 0, // (-1,10)
|
||||
}
|
||||
verts, err := ParseVertices(data)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseVertices error: %v", err)
|
||||
}
|
||||
if len(verts) != 2 {
|
||||
t.Fatalf("got %d vertices", len(verts))
|
||||
}
|
||||
if verts[1].X != -1 || verts[1].Y != 10 {
|
||||
t.Fatalf("unexpected vertex %+v", verts[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVerticesBadSize(t *testing.T) {
|
||||
if _, err := ParseVertices([]byte{0}); err == nil {
|
||||
t.Fatalf("expected error on odd sized vertex data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLinedefs(t *testing.T) {
|
||||
data := make([]byte, 14)
|
||||
data[0] = 1 // V1
|
||||
data[2] = 2 // V2
|
||||
lines, err := ParseLinedefs(data)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseLinedefs error: %v", err)
|
||||
}
|
||||
if len(lines) != 1 {
|
||||
t.Fatalf("expected one linedef")
|
||||
}
|
||||
if lines[0].V1 != 1 || lines[0].V2 != 2 {
|
||||
t.Fatalf("unexpected linedef %+v", lines[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLinedefsBadSize(t *testing.T) {
|
||||
if _, err := ParseLinedefs(make([]byte, 13)); err == nil {
|
||||
t.Fatalf("expected error on odd sized linedef data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMap(t *testing.T) {
|
||||
raw := map[string][]byte{
|
||||
"VERTEXES": {0, 0, 0, 0},
|
||||
"LINEDEFS": make([]byte, 14),
|
||||
}
|
||||
m, err := LoadMap(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMap error: %v", err)
|
||||
}
|
||||
if len(m.Vertices) != 1 || len(m.Linedefs) != 1 {
|
||||
t.Fatalf("unexpected map data %+v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinedefsToSegs(t *testing.T) {
|
||||
verts := []Vertex{{X: 0, Y: 0}, {X: 10, Y: 0}, {X: 10, Y: 0}}
|
||||
lines := []Linedef{{V1: 0, V2: 1}, {V1: 1, V2: 2}} // second is degenerate
|
||||
segs := LinedefsToSegs(verts, lines)
|
||||
if len(segs) != 1 {
|
||||
t.Fatalf("expected one segment, got %d", len(segs))
|
||||
}
|
||||
if segs[0].A.X != 0 || segs[0].B.X != 10 {
|
||||
t.Fatalf("unexpected segment: %+v", segs[0])
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package viz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"bspviz/internal/bsp"
|
||||
"bspviz/internal/geom"
|
||||
"bspviz/internal/mapfmt"
|
||||
)
|
||||
|
||||
func TestEmitDOT(t *testing.T) {
|
||||
root := &bsp.Node{
|
||||
O: geom.V(0, 0),
|
||||
D: geom.V(1, 0),
|
||||
Left: &bsp.Node{
|
||||
Leaf: &bsp.Leaf{Segs: []geom.Seg{{A: geom.V(0, 0), B: geom.V(1, 1)}}},
|
||||
},
|
||||
Right: &bsp.Node{
|
||||
Leaf: &bsp.Leaf{},
|
||||
},
|
||||
}
|
||||
out := filepath.Join(t.TempDir(), "tree.dot")
|
||||
if err := EmitDOT(root, out); err != nil {
|
||||
t.Fatalf("EmitDOT error: %v", err)
|
||||
}
|
||||
got, err := os.ReadFile(out)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile: %v", err)
|
||||
}
|
||||
expect := "digraph BSP {\n" +
|
||||
" node [fontname=\"Helvetica\"];\n" +
|
||||
" n0 [label=\"Split\\nO=(0,0) D=(1,0)\"];\n" +
|
||||
" n1 [label=\"Leaf\\nSegs=1\", shape=ellipse, style=filled, fillcolor=lightgray];\n" +
|
||||
" n2 [label=\"Leaf\\nSegs=0\", shape=ellipse, style=filled, fillcolor=lightgray];\n" +
|
||||
" n0 -> n1 [label=\"L\"];\n" +
|
||||
" n0 -> n2 [label=\"R\"];\n" +
|
||||
"}\n"
|
||||
if string(got) != expect {
|
||||
t.Fatalf("unexpected DOT output:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderPNGProducesFile(t *testing.T) {
|
||||
m := &mapfmt.MapData{
|
||||
Vertices: []mapfmt.Vertex{{X: 0, Y: 0}, {X: 64, Y: 0}, {X: 64, Y: 64}},
|
||||
Linedefs: []mapfmt.Linedef{
|
||||
{V1: 0, V2: 1},
|
||||
{V1: 1, V2: 2},
|
||||
{V1: 2, V2: 0},
|
||||
},
|
||||
}
|
||||
root := &bsp.Node{
|
||||
O: geom.V(0, 0),
|
||||
D: geom.V(1, 0),
|
||||
Left: &bsp.Node{Leaf: &bsp.Leaf{Segs: []geom.Seg{{A: geom.V(0, 0), B: geom.V(64, 64)}}}},
|
||||
Right: &bsp.Node{Leaf: &bsp.Leaf{}},
|
||||
}
|
||||
out := filepath.Join(t.TempDir(), "render.png")
|
||||
if err := RenderPNG(m, root, out); err != nil {
|
||||
t.Fatalf("RenderPNG error: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(out)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile: %v", err)
|
||||
}
|
||||
pngMagic := []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}
|
||||
if len(data) < len(pngMagic) || !bytes.Equal(data[:len(pngMagic)], pngMagic) {
|
||||
t.Fatalf("output is not a PNG")
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package wad
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type lumpDef struct {
|
||||
name string
|
||||
data []byte
|
||||
}
|
||||
|
||||
func writeTestWAD(t *testing.T, lumps []lumpDef) string {
|
||||
t.Helper()
|
||||
tmp, err := os.CreateTemp(t.TempDir(), "wad-*.wad")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTemp: %v", err)
|
||||
}
|
||||
|
||||
hdr := header{Ident: [4]byte{'P', 'W', 'A', 'D'}, NumLumps: int32(len(lumps))}
|
||||
if err := binary.Write(tmp, binary.LittleEndian, hdr); err != nil {
|
||||
t.Fatalf("write header: %v", err)
|
||||
}
|
||||
|
||||
dir := make([]DirEntry, 0, len(lumps))
|
||||
for _, l := range lumps {
|
||||
pos, err := tmp.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
t.Fatalf("seek: %v", err)
|
||||
}
|
||||
if len(l.data) > 0 {
|
||||
if _, err := tmp.Write(l.data); err != nil {
|
||||
t.Fatalf("write lump %s: %v", l.name, err)
|
||||
}
|
||||
}
|
||||
var name [8]byte
|
||||
copy(name[:], []byte(l.name))
|
||||
dir = append(dir, DirEntry{
|
||||
FilePos: int32(pos),
|
||||
Size: int32(len(l.data)),
|
||||
Name8: name,
|
||||
})
|
||||
}
|
||||
dirOffset, err := tmp.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
t.Fatalf("seek dir: %v", err)
|
||||
}
|
||||
if err := binary.Write(tmp, binary.LittleEndian, dir); err != nil {
|
||||
t.Fatalf("write dir: %v", err)
|
||||
}
|
||||
|
||||
hdr.DirOffset = int32(dirOffset)
|
||||
if _, err := tmp.Seek(0, io.SeekStart); err != nil {
|
||||
t.Fatalf("seek hdr: %v", err)
|
||||
}
|
||||
if err := binary.Write(tmp, binary.LittleEndian, hdr); err != nil {
|
||||
t.Fatalf("rewrite header: %v", err)
|
||||
}
|
||||
|
||||
if err := tmp.Close(); err != nil {
|
||||
t.Fatalf("close temp wad: %v", err)
|
||||
}
|
||||
|
||||
return tmp.Name()
|
||||
}
|
||||
|
||||
func TestOpenAndReadLump(t *testing.T) {
|
||||
name := writeTestWAD(t, []lumpDef{
|
||||
{name: "MAP01", data: nil},
|
||||
{name: "DATA", data: []byte{1, 2, 3}},
|
||||
})
|
||||
w, err := Open(name)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = w.Close() })
|
||||
|
||||
if len(w.Dir()) != 2 {
|
||||
t.Fatalf("Dir entries=%d want 2", len(w.Dir()))
|
||||
}
|
||||
|
||||
data, _, err := w.ReadLumpByName("data")
|
||||
if err != nil {
|
||||
t.Fatalf("ReadLumpByName: %v", err)
|
||||
}
|
||||
if !bytes.Equal(data, []byte{1, 2, 3}) {
|
||||
t.Fatalf("unexpected lump data: %v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindMapAndLoadMapLumps(t *testing.T) {
|
||||
verts := make([]byte, 4)
|
||||
lines := make([]byte, 14)
|
||||
name := writeTestWAD(t, []lumpDef{
|
||||
{name: "MAP01", data: nil},
|
||||
{name: "VERTEXES", data: verts},
|
||||
{name: "LINEDEFS", data: lines},
|
||||
{name: "MAP02", data: nil},
|
||||
})
|
||||
w, err := Open(name)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = w.Close() })
|
||||
|
||||
start, end, err := w.FindMap("map01")
|
||||
if err != nil {
|
||||
t.Fatalf("FindMap: %v", err)
|
||||
}
|
||||
if start != 0 || end != 3 {
|
||||
t.Fatalf("unexpected marker bounds start=%d end=%d", start, end)
|
||||
}
|
||||
|
||||
lumps, err := w.LoadMapLumps("map01", "VERTEXES", "LINEDEFS")
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMapLumps: %v", err)
|
||||
}
|
||||
if len(lumps) != 2 {
|
||||
t.Fatalf("expected 2 lumps, got %d", len(lumps))
|
||||
}
|
||||
if _, ok := lumps["VERTEXES"]; !ok {
|
||||
t.Fatalf("missing VERTEXES lump")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMapLumpsMissing(t *testing.T) {
|
||||
name := writeTestWAD(t, []lumpDef{{name: "MAP01", data: nil}})
|
||||
w, err := Open(name)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = w.Close() })
|
||||
|
||||
if _, err := w.LoadMapLumps("map01", "VERTEXES"); err == nil {
|
||||
t.Fatalf("expected error for missing lump")
|
||||
}
|
||||
}
|
||||
202
main.go
202
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,42 @@ 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,
|
||||
Out: os.Stdout,
|
||||
}
|
||||
|
||||
if err := app.Run(opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user