Compare commits
15 Commits
1b8b6647b8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab64703190 | ||
|
|
3cecdeda39 | ||
|
|
ec83b13e1f | ||
| 0986556c8c | |||
| 9acc31e6c5 | |||
| 40999b3dd3 | |||
| 2a045a6b8a | |||
| 2ae23efda1 | |||
| c908193986 | |||
| 72fa5e900c | |||
| db54f63c31 | |||
| 6129c31e97 | |||
| d68a203eac | |||
| b0b7b30f02 | |||
| 3ef9a65131 |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Visualisierungsausgaben
|
||||||
|
*.dot
|
||||||
|
*.png
|
||||||
BIN
MYMAP2.wad
Normal file
BIN
MYMAP2.wad
Normal file
Binary file not shown.
BIN
MYMAP3.wad
Normal file
BIN
MYMAP3.wad
Normal file
Binary file not shown.
67
README.md
67
README.md
@@ -1,6 +1,65 @@
|
|||||||
## BSPViz
|
## BSPViz
|
||||||
Mein BSP Node builder und renderer für meine Seminararbeit
|
|
||||||
|
|
||||||
# Todo:
|
BSPViz ist ein kompaktes Go-Tool zum Analysieren von Doom-WAD-Dateien. Es lädt
|
||||||
- [x] Implement geom utils
|
Maps, inspiziert Geometriedaten und baut daraus BSP-Bäume samt Statistiken oder
|
||||||
- [x] Implement BSP
|
Visualisierungen. Das Projekt entstand im Rahmen einer Seminararbeit und dient
|
||||||
|
als Baukasten, um verschiedene BSP-Build-Heuristiken auszuprobieren.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Lädt klassische Doom-WADs und listet die enthaltenen Lumps
|
||||||
|
- Extrahiert einzelne Lumps als Dateien zum Debuggen
|
||||||
|
- Führt Geometrie-Diagnosen (Segment-Splits, Bounding Boxes) durch
|
||||||
|
- Baut BSP-Bäume mit konfigurierbaren Parametern und misst resultierende Metriken
|
||||||
|
- Exportiert BSP-Strukturen als DOT, optional weiterverarbeitbar zu PNG
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
- Go >= 1.25 (siehe `go.mod`)
|
||||||
|
- Graphviz (nur nötig, wenn DOT-Dateien gerendert werden sollen)
|
||||||
|
- Eine Doom-kompatible WAD-Datei als Eingabe
|
||||||
|
|
||||||
|
#### Graphviz installieren
|
||||||
|
- **Linux (Debian/Ubuntu):** `sudo apt install graphviz`
|
||||||
|
- **macOS (Homebrew):** `brew install graphviz`
|
||||||
|
- **Windows (Chocolatey):** `choco install graphviz`
|
||||||
|
|
||||||
|
Alternativ stehen aktuelle Pakete und Installationshinweise auch auf der offiziellen Seite:
|
||||||
|
<https://graphviz.org/download/>
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
```bash
|
||||||
|
git clone https://git.protron.dev/Seminar/bspviz.git
|
||||||
|
cd bspviz
|
||||||
|
go build ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternativ lässt sich das Tool ohne vorherigen Build direkt ausführen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./main.go -wad MYMAP.wad -map MAP01 -info
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verwendung
|
||||||
|
Wichtige Flags (vollständige Übersicht mit `-h`):
|
||||||
|
- `-wad <pfad>` (Pflicht): Pfad zur WAD-Datei.
|
||||||
|
- `-map <name>`: Map-Marker wie `MAP01`, `E1M1` oder benutzerdefinierte Namen.
|
||||||
|
- `-list`: WAD-Verzeichnis anzeigen und beenden.
|
||||||
|
- `-info`: Basisstatistiken zu Vertices und Linedefs ausgeben.
|
||||||
|
- `-extract <L1,L2>`: Angegebene Lumps (z. B. `VERTEXES,LINEDEFS`) nach `-out` speichern.
|
||||||
|
- `-geomtest`: Segment- und Split-Diagnose für die gewählte Map ausgeben.
|
||||||
|
- `-buildbsp`: BSP-Baum erzeugen; Parameter wie `-alpha`, `-beta`, `-leafmax`,
|
||||||
|
`-maxdepth`, `-cands` und `-seed` steuern das Verhalten.
|
||||||
|
- `-dot <datei.dot>`: Gebauten BSP als DOT exportieren (setzt `-buildbsp` voraus;
|
||||||
|
benötigt ein installiertes Graphviz für die weitere Verarbeitung).
|
||||||
|
|
||||||
|
Beispiel: BSP bauen und als PNG visualisieren (Graphviz vorausgesetzt):
|
||||||
|
```bash
|
||||||
|
go run ./main.go -wad MYMAP.wad -map MAP01 -buildbsp -dot tree.dot
|
||||||
|
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.
|
||||||
90
docs/ProjektDokumentation.md
Normal file
90
docs/ProjektDokumentation.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
97
internal/bsp/build_test.go
Normal file
97
internal/bsp/build_test.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
31
internal/bsp/metrics.go
Normal file
31
internal/bsp/metrics.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package bsp
|
||||||
|
|
||||||
|
type Stats struct {
|
||||||
|
Nodes int
|
||||||
|
Leaves int
|
||||||
|
MaxDepth int
|
||||||
|
TotalSegs int // Summe Segmente in Leaves (nach Splits)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Measure(n *Node) Stats {
|
||||||
|
var st Stats
|
||||||
|
var rec func(*Node, int)
|
||||||
|
rec = func(x *Node, d int) {
|
||||||
|
if x == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if d > st.MaxDepth {
|
||||||
|
st.MaxDepth = d
|
||||||
|
}
|
||||||
|
if x.Leaf != nil {
|
||||||
|
st.Leaves++
|
||||||
|
st.TotalSegs += len(x.Leaf.Segs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
st.Nodes++
|
||||||
|
rec(x.Left, d+1)
|
||||||
|
rec(x.Right, d+1)
|
||||||
|
}
|
||||||
|
rec(n, 0)
|
||||||
|
return st
|
||||||
|
}
|
||||||
83
internal/geom/geom_test.go
Normal file
83
internal/geom/geom_test.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
76
internal/mapfmt/map_test.go
Normal file
76
internal/mapfmt/map_test.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
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,3 +1,51 @@
|
|||||||
package viz
|
package viz
|
||||||
|
|
||||||
//init
|
import (
|
||||||
|
"bspviz/internal/bsp"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EmitDOT serialisiert den BSP-Baum mit Wurzel root und schreibt ihn als DOT-Datei nach path.
|
||||||
|
func EmitDOT(root *bsp.Node, path string) error {
|
||||||
|
// buf sammelt den DOT-Text, bevor wir ihn speichern.
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString("digraph BSP {\n")
|
||||||
|
buf.WriteString(" node [fontname=\"Helvetica\"];\n")
|
||||||
|
|
||||||
|
// id vergibt fortlaufende Nummern für alle ausgegebenen Knoten.
|
||||||
|
id := 0
|
||||||
|
var walk func(*bsp.Node) int
|
||||||
|
// walk läuft den Baum in Tiefe-zuerst-Reihenfolge ab und liefert die DOT-Knoten-ID zurück.
|
||||||
|
walk = func(n *bsp.Node) int {
|
||||||
|
my := id
|
||||||
|
id++
|
||||||
|
if n.Leaf != nil {
|
||||||
|
fmt.Fprintf(&buf, " n%d [label=\"Leaf\\nSegs=%d\", shape=ellipse, style=filled, fillcolor=lightgray];\n",
|
||||||
|
my, len(n.Leaf.Segs))
|
||||||
|
return my
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&buf, " n%d [label=\"Split\\nO=(%.0f,%.0f) D=(%.0f,%.0f)\"];\n",
|
||||||
|
my, n.O.X, n.O.Y, n.D.X, n.D.Y)
|
||||||
|
l := walk(n.Left)
|
||||||
|
r := walk(n.Right)
|
||||||
|
fmt.Fprintf(&buf, " n%d -> n%d [label=\"L\"];\n", my, l)
|
||||||
|
fmt.Fprintf(&buf, " n%d -> n%d [label=\"R\"];\n", my, r)
|
||||||
|
return my
|
||||||
|
}
|
||||||
|
walk(root)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
73
internal/viz/viz_test.go
Normal file
73
internal/viz/viz_test.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
140
internal/wad/wad_test.go
Normal file
140
internal/wad/wad_test.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
79
main.go
79
main.go
@@ -1,8 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bspviz/internal/bsp"
|
||||||
"bspviz/internal/geom"
|
"bspviz/internal/geom"
|
||||||
"bspviz/internal/mapfmt"
|
"bspviz/internal/mapfmt"
|
||||||
|
"bspviz/internal/viz"
|
||||||
"bspviz/internal/wad"
|
"bspviz/internal/wad"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -12,6 +14,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func usageAndExit(msg string, code int) {
|
||||||
|
fmt.Fprintf(os.Stderr, "Fehler: %s\n\n", msg)
|
||||||
|
fmt.Fprintf(os.Stderr, "Beispiel:\n go run ./main.go -wad MYMAP.wad -map MAP01 -info\n\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "Verfügbare Flags:\n")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Flags
|
// Flags
|
||||||
wadPath := flag.String("wad", "", "Pfad zur WAD (required)")
|
wadPath := flag.String("wad", "", "Pfad zur WAD (required)")
|
||||||
@@ -21,14 +31,22 @@ func main() {
|
|||||||
extract := flag.String("extract", "", "Kommagetrennte Lump-Namen aus der Map extrahieren (z.B. VERTEXES,LINEDEFS)")
|
extract := flag.String("extract", "", "Kommagetrennte Lump-Namen aus der Map extrahieren (z.B. VERTEXES,LINEDEFS)")
|
||||||
outdir := flag.String("out", ".", "Zielordner für -extract")
|
outdir := flag.String("out", ".", "Zielordner für -extract")
|
||||||
geomtest := flag.Bool("geomtest", false, "Geometrie-Check: Segmente/AABB/Probe-Split ausgeben")
|
geomtest := flag.Bool("geomtest", false, "Geometrie-Check: Segmente/AABB/Probe-Split ausgeben")
|
||||||
|
buildbsp := flag.Bool("buildbsp", false, "BSP bauen und Metriken ausgeben")
|
||||||
|
alpha := flag.Float64("alpha", 10, "Kosten: Gewicht für Splits")
|
||||||
|
beta := flag.Float64("beta", 1, "Kosten: Gewicht für Balance")
|
||||||
|
eps := flag.Float64("eps", geom.EPS, "Epsilon für Geometrie")
|
||||||
|
leaf := flag.Int("leafmax", 12, "max. Segmente pro Leaf")
|
||||||
|
depth := flag.Int("maxdepth", 32, "max. Rekursionstiefe")
|
||||||
|
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()
|
flag.Parse()
|
||||||
|
|
||||||
if *wadPath == "" {
|
if strings.TrimSpace(*wadPath) == "" {
|
||||||
fmt.Fprintf(os.Stderr, "Usage:\n")
|
usageAndExit("Flag -wad fehlt. Bitte Pfad zu einer Doom-kompatiblen WAD-Datei angeben.", 2)
|
||||||
fmt.Fprintf(os.Stderr, " go run ./cmd/bspviz -wad MYMAP.wad -list\n")
|
|
||||||
fmt.Fprintf(os.Stderr, " go run ./cmd/bspviz -wad MYMAP.wad -map MYMAP -info\n")
|
|
||||||
fmt.Fprintf(os.Stderr, " go run ./cmd/bspviz -wad MYMAP.wad -map MYMAP -extract VERTEXES,LINEDEFS -out dumps/\n")
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w, err := wad.Open(*wadPath)
|
w, err := wad.Open(*wadPath)
|
||||||
@@ -156,4 +174,53 @@ func main() {
|
|||||||
|
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user