Compare commits

...

25 Commits

Author SHA1 Message Date
Lukas Gau
ab64703190 added first project documentation 2025-10-18 14:04:44 +02:00
Lukas Gau
3cecdeda39 added test wad for round room 2025-09-30 13:38:39 +02:00
Lukas Gau
ec83b13e1f updated readme with repo url 2025-09-28 14:47:12 +02:00
Doc
0986556c8c updated readme to include test info 2025-09-28 14:06:44 +02:00
Doc
9acc31e6c5 added test for all implementations 2025-09-28 14:04:22 +02:00
Doc
40999b3dd3 updated info when no args are supplied 2025-09-28 13:26:42 +02:00
Doc
2a045a6b8a big readme update to better explain what this project does 2025-09-28 12:57:54 +02:00
Doc
2ae23efda1 added gitignore to ignore .dot and png files 2025-09-28 12:51:56 +02:00
Doc
c908193986 implemented png export so that we get a overlay of the splitt lines and also directly export a node tree visualization of the node tree with graphviz. 2025-09-28 12:50:16 +02:00
Doc
72fa5e900c added comments to dot.go 2025-09-28 12:39:11 +02:00
Doc
db54f63c31 implemented graphviz builderin dot.go to build dot files for graphviz 2025-09-28 12:31:41 +02:00
Doc
6129c31e97 added second more complex test wad 2025-09-28 12:21:06 +02:00
Doc
d68a203eac added a test wad 2025-09-28 11:45:30 +02:00
Doc
b0b7b30f02 added flags and buildbsp to main 2025-09-21 12:50:31 +02:00
Doc
3ef9a65131 implemented bsp metrics 2025-09-21 12:50:16 +02:00
Doc
1b8b6647b8 implemented bsp/build 2025-09-20 12:18:49 +02:00
Doc
ed8d3b709e updated readme 2025-09-10 10:50:59 +02:00
Doc
9e89ce4d95 added bounds function and segline to intersect function 2025-09-10 10:50:03 +02:00
Doc
e69e1967bb added side and split geom logic 2025-09-07 15:34:33 +02:00
Doc
0722b2d9fe added segment convertion to mapfmt 2025-09-04 15:01:03 +02:00
Doc
7dd13e6256 added geom and flag in main for deebug cli calls 2025-08-21 11:31:00 +02:00
Doc
e695c39e5a added todo section to readme 2025-08-18 13:26:33 +02:00
Doc
6391e27c68 added readme 2025-08-18 12:53:21 +02:00
Doc
55981730da added parsing of map lumps 2025-08-18 12:49:15 +02:00
Doc
bb9a47cd6d added comments 2025-08-18 12:48:48 +02:00
24 changed files with 1365 additions and 11 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Visualisierungsausgaben
*.dot
*.png

BIN
MYMAP.wad Normal file

Binary file not shown.

BIN
MYMAP2.wad Normal file

Binary file not shown.

BIN
MYMAP3.wad Normal file

Binary file not shown.

65
README.md Normal file
View File

@@ -0,0 +1,65 @@
## BSPViz
BSPViz ist ein kompaktes Go-Tool zum Analysieren von Doom-WAD-Dateien. Es lädt
Maps, inspiziert Geometriedaten und baut daraus BSP-Bäume samt Statistiken oder
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.

View 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.

View File

@@ -1,3 +1,160 @@
package bsp
//init
import (
"bspviz/internal/geom"
"math"
"math/rand"
)
type Leaf struct {
Segs []geom.Seg
}
type Node struct {
O, D geom.Vec // Trennlinie: O + t*D
Left *Node
Right *Node
Leaf *Leaf // != nil => Blatt/Subsector
}
// Parameter für die Heuristik/Build:
type Params struct {
Alpha float64 // Gewicht Splits
Beta float64 // Gewicht Balance
Eps float64 // Toleranz (reicht: geom.EPS, aber als Kopie)
MaxDepth int // z. B. 32
LeafMax int // Max. Segmente pro Leaf (z. B. 12)
Cands int // Anzahl Kandidaten (Subsample), z. B. 16
Seed int64 // RNG-Seed (0 => Zeit)
}
// stop entscheidet, ob die Rekursion an dieser Stelle endet und ein Blatt entsteht.
func stop(segs []geom.Seg, depth int, p Params) bool {
if p.MaxDepth <= 0 {
p.MaxDepth = 32
}
if p.LeafMax <= 0 {
p.LeafMax = 12
}
return depth >= p.MaxDepth || len(segs) <= p.LeafMax
}
// evalCost bewertet eine Kandidaten-Splitebene nach Split-Anzahl, Balance und Gesamtkosten.
func evalCost(segs []geom.Seg, O, D geom.Vec, p Params) (splits int, balance int, cost float64) {
left, right := 0, 0
for _, s := range segs {
sa := geom.Side(s.A, O, D)
sb := geom.Side(s.B, O, D)
if sa >= -p.Eps && sb >= -p.Eps {
left++
} else if sa <= p.Eps && sb <= p.Eps {
right++
} else {
splits++
}
}
if p.Alpha == 0 {
p.Alpha = 10
}
if p.Beta == 0 {
p.Beta = 1
}
balance = int(math.Abs(float64(left - right)))
cost = p.Alpha*float64(splits) + p.Beta*float64(balance)
return
}
// selectSplit wählt die heuristisch beste Partitionsebene aus den vorhandenen Segmenten.
func selectSplit(segs []geom.Seg, p Params, rng *rand.Rand) (O, D geom.Vec) {
n := len(segs)
if n == 0 {
return geom.Vec{}, geom.Vec{}
}
// Wie viele Kandidaten?
k := p.Cands
if k <= 0 {
k = 16
}
if k > n {
k = n
}
// Simple Strategie: gleichmäßig sampeln; bei großem n kleine Randomisierung.
step := int(math.Max(1, float64(n)/float64(k)))
bestCost := math.MaxFloat64
for i := 0; i < n; i += step {
s := segs[i]
Oc := s.A
Dc := geom.Sub(s.B, s.A)
if geom.Len(Dc) < 1e-9 {
continue
}
_, _, c := evalCost(segs, Oc, Dc, p)
if c < bestCost {
bestCost = c
O = Oc
D = Dc
}
}
// Optionale Randomisierung unter Top-K wäre ein nächster Schritt.
return
}
// splitAll teilt alle Segmente entlang der Geraden O+t*D und filtert Degenerationsfälle heraus.
func splitAll(segs []geom.Seg, O, D geom.Vec, p Params) (leftSet, rightSet []geom.Seg, numSplits int) {
leftSet = make([]geom.Seg, 0, len(segs))
rightSet = make([]geom.Seg, 0, len(segs))
for _, s := range segs {
f, b := geom.SplitSeg(s, O, D)
if len(f) > 0 && len(b) > 0 {
numSplits++
}
// optional: sehr kurze Teilstücke verwerfen
for _, x := range f {
if geom.Len(geom.Sub(x.B, x.A)) >= 5*p.Eps {
leftSet = append(leftSet, x)
}
}
for _, x := range b {
if geom.Len(geom.Sub(x.B, x.A)) >= 5*p.Eps {
rightSet = append(rightSet, x)
}
}
}
return
}
// Build erzeugt einen BSP-Baum aus den Segmenten gemäß den übergebenen Parametern.
func Build(segs []geom.Seg, p Params) *Node {
if p.Eps == 0 {
p.Eps = geom.EPS
}
var seed int64 = p.Seed
if seed == 0 {
seed = 1
} // deterministisch machen, wenn gewünscht
rng := rand.New(rand.NewSource(seed))
return buildRec(segs, 0, p, rng)
}
// buildRec realisiert den rekursiven Aufbau und verteilt Segmente auf linke und rechte Subbäume.
func buildRec(segs []geom.Seg, depth int, p Params, rng *rand.Rand) *Node {
if stop(segs, depth, p) {
return &Node{Leaf: &Leaf{Segs: segs}}
}
O, D := selectSplit(segs, p, rng)
if geom.Len(D) < 1e-9 {
// Fallback: notgedrungen Leaf
return &Node{Leaf: &Leaf{Segs: segs}}
}
leftSet, rightSet, _ := splitAll(segs, O, D, p)
return &Node{
O: O, D: D,
Left: buildRec(leftSet, depth+1, p, rng),
Right: buildRec(rightSet, depth+1, p, rng),
}
}

View 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
View 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
}

25
internal/geom/fit.go Normal file
View File

@@ -0,0 +1,25 @@
package geom
import "math"
type AABB struct{ Min, Max Vec }
func Bounds(pts []Vec) AABB {
minX, minY := math.Inf(1), math.Inf(1)
maxX, maxY := math.Inf(-1), math.Inf(-1)
for _, p := range pts {
if p.X < minX {
minX = p.X
}
if p.Y < minY {
minY = p.Y
}
if p.X > maxX {
maxX = p.X
}
if p.Y > maxY {
maxY = p.Y
}
}
return AABB{Min: V(minX, minY), Max: V(maxX, maxY)}
}

View File

@@ -1,3 +1,20 @@
package geom
//init
import "math"
const EPS = 1e-6
type Vec struct {
X, Y float64
}
type Seg struct {
A, B Vec
}
func V(x, y float64) Vec { return Vec{x, y} }
func Sub(a, b Vec) Vec { return Vec{a.X - b.X, a.Y - b.Y} }
func Dot(a, b Vec) float64 { return a.X*b.X + a.Y*b.Y }
func Cross(a, b Vec) float64 { return a.X*b.Y - a.Y*b.X }
func Len(a Vec) float64 { return math.Hypot(a.X, a.Y) }
func NearlyZero(x float64) bool { return math.Abs(x) < EPS }

View 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)
}
}

View File

@@ -0,0 +1,11 @@
package geom
func SegLineIntersect(A, B, O, D Vec) (bool, float64) {
r := Sub(B, A)
den := Cross(r, D)
if NearlyZero(den) {
return false, 0
}
t := Cross(Sub(O, A), D) / den
return t > EPS && t < 1-EPS, t
}

5
internal/geom/side.go Normal file
View File

@@ -0,0 +1,5 @@
package geom
func Side(P, O, D Vec) float64 {
return Cross(D, Sub(P, O))
}

29
internal/geom/split.go Normal file
View File

@@ -0,0 +1,29 @@
package geom
func SplitSeg(s Seg, O, D Vec) (front, back []Seg) {
sa := Side(s.A, O, D)
sb := Side(s.B, O, D)
if sa >= -EPS && sb >= -EPS {
return []Seg{s}, nil
}
if sa <= EPS && sb <= EPS {
return nil, []Seg{s}
}
ok, t := SegLineIntersect(s.A, s.B, O, D)
if !ok {
if sa >= 0 {
return []Seg{s}, nil
}
return nil, []Seg{s}
}
M := Vec{s.A.X + t*(s.B.X-s.A.X), s.A.Y + t*(s.B.Y-s.A.Y)}
a := Seg{s.A, M}
b := Seg{M, s.B}
if sa > 0 {
return []Seg{a}, []Seg{b}
}
return []Seg{b}, []Seg{a}
}

View File

@@ -1,3 +1,73 @@
// Parsen der Lump aus wad.go zu go structs
package mapfmt
//init
import (
"encoding/binary"
"fmt"
)
type Vertex struct {
X int16
Y int16
}
type Linedef struct {
V1, V2 uint16
Flags uint16
Special uint16
Tag uint16
RightSide int16
LeftSide int16
}
type MapData struct {
Vertices []Vertex
Linedefs []Linedef
}
// Parsen der Vertices Lumps
func ParseVertices(data []byte) ([]Vertex, error) {
if len(data)%4 != 0 {
return nil, fmt.Errorf("VERTEXES size %d not multiple of 4", len(data))
}
n := len(data) / 4
verts := make([]Vertex, n)
for i := 0; i < n; i++ {
verts[i].X = int16(binary.LittleEndian.Uint16(data[i*4:]))
verts[i].Y = int16(binary.LittleEndian.Uint16(data[i*4+2:]))
}
return verts, nil
}
// Parsen der Linedef Lumps
func ParseLinedefs(data []byte) ([]Linedef, error) {
if len(data)%14 != 0 {
return nil, fmt.Errorf("LINEDEFS size %d not multiple of 14", len(data))
}
n := len(data) / 14
lines := make([]Linedef, n)
for i := 0; i < n; i++ {
base := i * 14
lines[i].V1 = binary.LittleEndian.Uint16(data[base:])
lines[i].V2 = binary.LittleEndian.Uint16(data[base+2:])
lines[i].Flags = binary.LittleEndian.Uint16(data[base+4:])
lines[i].Special = binary.LittleEndian.Uint16(data[base+6:])
lines[i].Tag = binary.LittleEndian.Uint16(data[base+8:])
lines[i].RightSide = int16(binary.LittleEndian.Uint16(data[base+10:]))
lines[i].LeftSide = int16(binary.LittleEndian.Uint16(data[base+12:]))
}
return lines, nil
}
// Generieren der MapData aus den von LoadMapLumps geliferten Daten und Parsen per funcs
func LoadMap(raw map[string][]byte) (*MapData, error) {
v, err := ParseVertices(raw["VERTEXES"])
if err != nil {
return nil, err
}
l, err := ParseLinedefs(raw["LINEDEFS"])
if err != nil {
return nil, err
}
return &MapData{Vertices: v, Linedefs: l}, nil
}

View 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])
}
}

18
internal/mapfmt/toseg.go Normal file
View File

@@ -0,0 +1,18 @@
package mapfmt
import "bspviz/internal/geom" // anpassen
func LinedefsToSegs(verts []Vertex, lines []Linedef) []geom.Seg {
segs := make([]geom.Seg, 0, len(lines))
for _, L := range lines {
a := verts[L.V1]
b := verts[L.V2]
A := geom.V(float64(a.X), float64(a.Y))
B := geom.V(float64(b.X), float64(b.Y))
if geom.Len(geom.Sub(B, A)) < 1e-9 {
continue // degenerat
}
segs = append(segs, geom.Seg{A: A, B: B})
}
return segs
}

View File

@@ -1,3 +1,51 @@
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
}

View File

@@ -1,3 +1,177 @@
package viz
//init
import (
"bspviz/internal/bsp"
"bspviz/internal/geom"
"bspviz/internal/mapfmt"
"image"
"image/color"
"image/png"
"math"
"math/rand"
"os"
"time"
)
// worldToScreen transformiert Doom-Koordinaten in Bildkoordinaten
type worldToScreen struct {
minX, minY float64
scale float64
W, H int
margin float64
}
func makeTransform(verts []mapfmt.Vertex, W, H int) worldToScreen {
minX, minY := math.MaxFloat64, math.MaxFloat64
maxX, maxY := -math.MaxFloat64, -math.MaxFloat64
for _, v := range verts {
x, y := float64(v.X), float64(v.Y)
if x < minX {
minX = x
}
if y < minY {
minY = y
}
if x > maxX {
maxX = x
}
if y > maxY {
maxY = y
}
}
margin := 20.0
w := maxX - minX
h := maxY - minY
if w < 1 {
w = 1
}
if h < 1 {
h = 1
}
scale := math.Min((float64(W)-2*margin)/w, (float64(H)-2*margin)/h)
return worldToScreen{minX, minY, scale, W, H, margin}
}
func (t worldToScreen) P(p geom.Vec) (int, int) {
x := t.margin + (p.X-t.minX)*t.scale
y := t.margin + (p.Y-t.minY)*t.scale
// Y-Achse flippen (Doom: +Y nach oben, Bild: +Y nach unten)
yy := float64(t.H) - y
return int(x + 0.5), int(yy + 0.5)
}
// einfache Bresenham-Linie
func drawLine(img *image.RGBA, x0, y0, x1, y1 int, c color.Color) {
dx := int(math.Abs(float64(x1 - x0)))
dy := -int(math.Abs(float64(y1 - y0)))
sx := -1
if x0 < x1 {
sx = 1
}
sy := -1
if y0 < y1 {
sy = 1
}
err := dx + dy
for {
if x0 >= 0 && y0 >= 0 && x0 < img.Bounds().Dx() && y0 < img.Bounds().Dy() {
img.Set(x0, y0, c)
}
if x0 == x1 && y0 == y1 {
break
}
e2 := 2 * err
if e2 >= dy {
err += dy
x0 += sx
}
if e2 <= dx {
err += dx
y0 += sy
}
}
}
// RenderPNG zeichnet die Map und den BSP-Baum
func RenderPNG(m *mapfmt.MapData, root *bsp.Node, outPath string) error {
W, H := 1200, 900
img := image.NewRGBA(image.Rect(0, 0, W, H))
// Hintergrund
bg := color.RGBA{20, 20, 24, 255}
for y := 0; y < H; y++ {
for x := 0; x < W; x++ {
img.Set(x, y, bg)
}
}
tr := makeTransform(m.Vertices, W, H)
// 1) Linedefs grau
gray := color.RGBA{180, 180, 180, 255}
for _, L := range m.Linedefs {
a := m.Vertices[L.V1]
b := m.Vertices[L.V2]
x0, y0 := tr.P(geom.V(float64(a.X), float64(a.Y)))
x1, y1 := tr.P(geom.V(float64(b.X), float64(b.Y)))
drawLine(img, x0, y0, x1, y1, gray)
}
// 2) Split-Linien (gelb)
yellow := color.RGBA{240, 210, 40, 255}
var drawSplits func(*bsp.Node)
drawSplits = func(n *bsp.Node) {
if n == nil || n.Leaf != nil {
return
}
D := n.D
L := geom.Len(D)
if L < 1e-9 {
return
}
dx, dy := D.X/L, D.Y/L
k := 1e6 // "lange" Linie
p0 := geom.V(n.O.X-k*dx, n.O.Y-k*dy)
p1 := geom.V(n.O.X+k*dx, n.O.Y+k*dy)
x0, y0 := tr.P(p0)
x1, y1 := tr.P(p1)
drawLine(img, x0, y0, x1, y1, yellow)
drawSplits(n.Left)
drawSplits(n.Right)
}
drawSplits(root)
// 3) Leaves farbig
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
var paintLeaves func(*bsp.Node)
paintLeaves = func(n *bsp.Node) {
if n == nil {
return
}
if n.Leaf != nil {
col := color.RGBA{
uint8(100 + rng.Intn(100)),
uint8(100 + rng.Intn(100)),
uint8(100 + rng.Intn(100)),
255,
}
for _, s := range n.Leaf.Segs {
x0, y0 := tr.P(s.A)
x1, y1 := tr.P(s.B)
drawLine(img, x0, y0, x1, y1, col)
}
return
}
paintLeaves(n.Left)
paintLeaves(n.Right)
}
paintLeaves(root)
// speichern
f, err := os.Create(outPath)
if err != nil {
return err
}
defer f.Close()
return png.Encode(f, img)
}

73
internal/viz/viz_test.go Normal file
View 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")
}
}

View File

@@ -1,3 +1,5 @@
// Extrahieren der "Rohen" Lump-Bytes einer WAD-Datei
// siehe def Lumps: https://doomwiki.org/wiki/Lump
package wad
import (
@@ -31,6 +33,7 @@ type Wad struct {
path string
}
// Öffnen der WAD und checks
func Open(path string) (*Wad, error) {
f, err := os.Open(path)
if err != nil {
@@ -90,6 +93,7 @@ func Open(path string) (*Wad, error) {
return w, nil
}
// WAD Closing
func (w *Wad) Close() error {
if w == nil || w.f == nil {
return nil
@@ -101,6 +105,7 @@ func (w *Wad) Close() error {
func (w *Wad) Dir() []DirEntry { return w.dir }
// Lessen der Lump roh Bytes
func (w *Wad) ReadLump(i int) (name string, data []byte, err error) {
if i < 0 || i >= len(w.dir) {
return "", nil, fmt.Errorf("lump index out of range: %d", i)
@@ -118,6 +123,7 @@ func (w *Wad) ReadLump(i int) (name string, data []byte, err error) {
return name, buf, nil
}
// Extrahieren der Lump Namen aus den rohen Bytes
func (w *Wad) ReadLumpByName(name string) ([]byte, int, error) {
want := strings.ToUpper(name)
for i, d := range w.dir {
@@ -129,6 +135,7 @@ func (w *Wad) ReadLumpByName(name string) ([]byte, int, error) {
return nil, -1, fmt.Errorf("lump %q not found", want)
}
// Finden der Map-Marker(start und ende) in der WAD
func (w *Wad) FindMap(marker string) (start, end int, err error) {
m := strings.ToUpper(marker)
start = -1
@@ -151,6 +158,7 @@ func (w *Wad) FindMap(marker string) (start, end int, err error) {
return start, end, nil
}
// Laden der Map Lumps
func (w *Wad) LoadMapLumps(marker string, names ...string) (map[string][]byte, error) {
start, end, err := w.FindMap(marker)
if err != nil {

140
internal/wad/wad_test.go Normal file
View 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")
}
}

146
main.go
View File

@@ -1,6 +1,10 @@
package main
import (
"bspviz/internal/bsp"
"bspviz/internal/geom"
"bspviz/internal/mapfmt"
"bspviz/internal/viz"
"bspviz/internal/wad"
"flag"
"fmt"
@@ -10,6 +14,14 @@ import (
"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() {
// Flags
wadPath := flag.String("wad", "", "Pfad zur WAD (required)")
@@ -18,14 +30,23 @@ func main() {
info := flag.Bool("info", false, "Roh-Infos zur Map (Counts von 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")
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()
if *wadPath == "" {
fmt.Fprintf(os.Stderr, "Usage:\n")
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)
if strings.TrimSpace(*wadPath) == "" {
usageAndExit("Flag -wad fehlt. Bitte Pfad zu einer Doom-kompatiblen WAD-Datei angeben.", 2)
}
w, err := wad.Open(*wadPath)
@@ -48,6 +69,7 @@ func main() {
}
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 {
@@ -55,12 +77,20 @@ func main() {
}
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")
}
@@ -69,6 +99,7 @@ func main() {
}
}
// Generiert einzelne Lump Dateien zum Debugen
if *extract != "" {
want := strings.Split(*extract, ",")
for i := range want {
@@ -89,4 +120,107 @@ func main() {
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++
continue
}
left += len(f)
right += len(b)
if len(f) > 0 && len(b) > 0 {
splits++
}
}
fmt.Printf("PROBE-SPLIT: O=(%.1f,%.1f) D=(%.1f,%.1f)\n", O.X, O.Y, D.X, D.Y)
fmt.Printf("PROBE-SPLIT: left=%d right=%d splits=%d degens=%d (EPS=%.1e)\n",
left, right, splits, degens, geom.EPS)
return
}
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
}
}