Compare commits
25 Commits
2af5bd222e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab64703190 | ||
|
|
3cecdeda39 | ||
|
|
ec83b13e1f | ||
| 0986556c8c | |||
| 9acc31e6c5 | |||
| 40999b3dd3 | |||
| 2a045a6b8a | |||
| 2ae23efda1 | |||
| c908193986 | |||
| 72fa5e900c | |||
| db54f63c31 | |||
| 6129c31e97 | |||
| d68a203eac | |||
| b0b7b30f02 | |||
| 3ef9a65131 | |||
| 1b8b6647b8 | |||
| ed8d3b709e | |||
| 9e89ce4d95 | |||
| e69e1967bb | |||
| 0722b2d9fe | |||
| 7dd13e6256 | |||
| e695c39e5a | |||
| 6391e27c68 | |||
| 55981730da | |||
| bb9a47cd6d |
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.
65
README.md
Normal file
65
README.md
Normal 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.
|
||||
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.
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
25
internal/geom/fit.go
Normal file
25
internal/geom/fit.go
Normal 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)}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
11
internal/geom/intersect.go
Normal file
11
internal/geom/intersect.go
Normal 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
5
internal/geom/side.go
Normal 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
29
internal/geom/split.go
Normal 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}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
}
|
||||
18
internal/mapfmt/toseg.go
Normal file
18
internal/mapfmt/toseg.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
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")
|
||||
}
|
||||
}
|
||||
@@ -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
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")
|
||||
}
|
||||
}
|
||||
146
main.go
146
main.go
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user