Compare commits
24 Commits
2af5bd222e
...
ui-wrapper
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25e965506e | ||
| eec2ca5cc9 | |||
| a5a28840b1 | |||
| bfb1c7bef6 | |||
| 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.
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 ./...`
|
||||
- Temporäre Artefakte (DOT/PNG) sind über `.gitignore` bereits ausgeschlossen.
|
||||
|
||||
Beiträge, Erweiterungen oder neue Heuristiken sind willkommen.
|
||||
471
cmd/tui/main.go
Normal file
471
cmd/tui/main.go
Normal file
@@ -0,0 +1,471 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/filepicker"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"bspviz/internal/app"
|
||||
"bspviz/internal/geom"
|
||||
)
|
||||
|
||||
const (
|
||||
inputWadPath = iota
|
||||
inputMapMarker
|
||||
inputExtract
|
||||
inputOutDir
|
||||
inputDotOut
|
||||
inputTreePNG
|
||||
inputOverlay
|
||||
inputAlpha
|
||||
inputBeta
|
||||
inputEps
|
||||
inputLeaf
|
||||
inputDepth
|
||||
inputCands
|
||||
inputSeed
|
||||
inputCount
|
||||
)
|
||||
|
||||
const (
|
||||
toggleListOnly = iota
|
||||
toggleInfo
|
||||
toggleGeomTest
|
||||
toggleBuildBSP
|
||||
toggleCount
|
||||
)
|
||||
|
||||
var (
|
||||
indicatorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true)
|
||||
statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("39"))
|
||||
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Bold(true)
|
||||
boxStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder()).Padding(0, 1)
|
||||
)
|
||||
|
||||
type runResultMsg struct {
|
||||
output string
|
||||
err error
|
||||
}
|
||||
|
||||
type model struct {
|
||||
inputs []textinput.Model
|
||||
inputLabels []string
|
||||
toggles []bool
|
||||
toggleLabels []string
|
||||
focusIndex int
|
||||
status string
|
||||
output string
|
||||
errMsg string
|
||||
running bool
|
||||
showPicker bool
|
||||
picker filepicker.Model
|
||||
}
|
||||
|
||||
func initialModel() model {
|
||||
m := model{
|
||||
inputs: make([]textinput.Model, inputCount),
|
||||
inputLabels: []string{
|
||||
"WAD", "Map", "Extract", "OutDir", "DotOut", "TreePNG", "Overlay",
|
||||
"Alpha", "Beta", "Eps", "LeafMax", "MaxDepth", "Cands", "Seed",
|
||||
},
|
||||
toggles: make([]bool, toggleCount),
|
||||
toggleLabels: []string{"List directory", "Info", "Geom test", "Build BSP"},
|
||||
status: "Bereit",
|
||||
}
|
||||
|
||||
for i := range m.inputs {
|
||||
ti := textinput.New()
|
||||
ti.CharLimit = 0
|
||||
ti.Prompt = ""
|
||||
switch i {
|
||||
case inputWadPath:
|
||||
ti.Placeholder = "Pfad zur WAD"
|
||||
case inputMapMarker:
|
||||
ti.SetValue("MAP01")
|
||||
case inputExtract:
|
||||
ti.Placeholder = "z.B. VERTEXES,LINEDEFS"
|
||||
case inputOutDir:
|
||||
ti.SetValue(".")
|
||||
case inputAlpha:
|
||||
ti.SetValue(fmt.Sprintf("%g", 10.0))
|
||||
case inputBeta:
|
||||
ti.SetValue(fmt.Sprintf("%g", 1.0))
|
||||
case inputEps:
|
||||
ti.SetValue(fmt.Sprintf("%g", geom.EPS))
|
||||
case inputLeaf:
|
||||
ti.SetValue("12")
|
||||
case inputDepth:
|
||||
ti.SetValue("32")
|
||||
case inputCands:
|
||||
ti.SetValue("16")
|
||||
case inputSeed:
|
||||
ti.SetValue("0")
|
||||
}
|
||||
m.inputs[i] = ti
|
||||
}
|
||||
|
||||
picker := filepicker.New()
|
||||
picker.AllowedTypes = []string{".wad", ".WAD"}
|
||||
picker.DirAllowed = false
|
||||
picker.FileAllowed = true
|
||||
picker.ShowPermissions = false
|
||||
picker.ShowSize = true
|
||||
picker.ShowHidden = false
|
||||
picker.AutoHeight = false
|
||||
picker.Height = 12
|
||||
m.picker = picker
|
||||
|
||||
m.setFocus(0)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if m.showPicker {
|
||||
switch key := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
if key.Type == tea.KeyEsc {
|
||||
m.showPicker = false
|
||||
m.status = "Dateiauswahl abgebrochen"
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.picker, cmd = m.picker.Update(msg)
|
||||
if selected, path := m.picker.DidSelectFile(msg); selected {
|
||||
m.inputs[inputWadPath].SetValue(path)
|
||||
m.showPicker = false
|
||||
m.status = "WAD gewählt"
|
||||
m.errMsg = ""
|
||||
m.setFocus(inputWadPath)
|
||||
return m, cmd
|
||||
}
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc":
|
||||
return m, tea.Quit
|
||||
case "tab", "down":
|
||||
m.setFocus(m.focusIndex + 1)
|
||||
case "shift+tab", "up":
|
||||
m.setFocus(m.focusIndex - 1)
|
||||
case "enter":
|
||||
if m.focusIndex < len(m.inputs) {
|
||||
m.setFocus(m.focusIndex + 1)
|
||||
} else {
|
||||
idx := m.focusIndex - len(m.inputs)
|
||||
if idx >= 0 && idx < len(m.toggles) {
|
||||
m.toggles[idx] = !m.toggles[idx]
|
||||
}
|
||||
}
|
||||
case " ":
|
||||
if m.focusIndex >= len(m.inputs) {
|
||||
idx := m.focusIndex - len(m.inputs)
|
||||
if idx >= 0 && idx < len(m.toggles) {
|
||||
m.toggles[idx] = !m.toggles[idx]
|
||||
}
|
||||
}
|
||||
case "f2":
|
||||
if !m.running {
|
||||
current := strings.TrimSpace(m.inputs[inputWadPath].Value())
|
||||
if current != "" {
|
||||
info, err := os.Stat(current)
|
||||
if err == nil {
|
||||
if info.IsDir() {
|
||||
m.picker.CurrentDirectory = current
|
||||
} else {
|
||||
m.picker.CurrentDirectory = filepath.Dir(current)
|
||||
}
|
||||
} else {
|
||||
m.picker.CurrentDirectory = filepath.Dir(current)
|
||||
}
|
||||
} else {
|
||||
m.picker.CurrentDirectory = "."
|
||||
}
|
||||
m.showPicker = true
|
||||
m.status = "Datei auswählen"
|
||||
m.errMsg = ""
|
||||
return m, m.picker.Init()
|
||||
}
|
||||
case "ctrl+r":
|
||||
if !m.running {
|
||||
var cmd tea.Cmd
|
||||
m, cmd = m.run()
|
||||
return m, cmd
|
||||
}
|
||||
}
|
||||
case runResultMsg:
|
||||
m.running = false
|
||||
if msg.err != nil {
|
||||
m.errMsg = msg.err.Error()
|
||||
m.status = "Fehlgeschlagen"
|
||||
} else {
|
||||
m.errMsg = ""
|
||||
m.status = "Fertig"
|
||||
}
|
||||
m.output = strings.TrimSpace(msg.output)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
cmds := make([]tea.Cmd, len(m.inputs))
|
||||
for i := range m.inputs {
|
||||
m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("BSPViz Bubble Tea UI\n")
|
||||
b.WriteString("====================\n\n")
|
||||
|
||||
labelWidth := 12
|
||||
for i := range m.inputs {
|
||||
indicator := " "
|
||||
if m.focusIndex == i {
|
||||
indicator = indicatorStyle.Render(">")
|
||||
}
|
||||
label := fmt.Sprintf("%-*s", labelWidth, m.inputLabels[i]+":")
|
||||
fmt.Fprintf(&b, "%s %s %s\n", indicator, label, m.inputs[i].View())
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
for i := range m.toggles {
|
||||
indicator := " "
|
||||
if m.focusIndex == len(m.inputs)+i {
|
||||
indicator = indicatorStyle.Render(">")
|
||||
}
|
||||
mark := " "
|
||||
if m.toggles[i] {
|
||||
mark = "x"
|
||||
}
|
||||
fmt.Fprintf(&b, "%s [%s] %s\n", indicator, mark, m.toggleLabels[i])
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
|
||||
statusText := "Bereit"
|
||||
if m.running {
|
||||
statusText = "Läuft..."
|
||||
} else if m.status != "" {
|
||||
statusText = m.status
|
||||
}
|
||||
b.WriteString(statusStyle.Render("Status: " + statusText))
|
||||
b.WriteString("\n")
|
||||
|
||||
if m.errMsg != "" {
|
||||
b.WriteString(errorStyle.Render("Fehler: " + m.errMsg))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
if m.showPicker {
|
||||
b.WriteString("\nDatei auswählen (Enter übernehmen, Esc abbrechen)\n")
|
||||
b.WriteString(boxStyle.Render(m.picker.View()))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
if m.output != "" {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(boxStyle.Render(m.output))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString("Steuerung: TAB/Shift+TAB navigieren • Space toggeln • Ctrl+R ausführen • F2 Datei wählen • Ctrl+C beenden")
|
||||
b.WriteString("\n")
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m model) run() (model, tea.Cmd) {
|
||||
opts, err := m.buildOptions()
|
||||
if err != nil {
|
||||
m.errMsg = err.Error()
|
||||
m.status = "Fehler"
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.running = true
|
||||
m.status = "Läuft..."
|
||||
m.errMsg = ""
|
||||
m.output = ""
|
||||
|
||||
return m, runAppCmd(opts)
|
||||
}
|
||||
|
||||
func (m model) buildOptions() (app.Options, error) {
|
||||
var opts app.Options
|
||||
|
||||
wadPath := strings.TrimSpace(m.inputs[inputWadPath].Value())
|
||||
if wadPath == "" {
|
||||
return opts, fmt.Errorf("WAD-Pfad darf nicht leer sein")
|
||||
}
|
||||
opts.WadPath = wadPath
|
||||
|
||||
mapMarker := strings.TrimSpace(m.inputs[inputMapMarker].Value())
|
||||
if mapMarker == "" {
|
||||
mapMarker = "MAP01"
|
||||
}
|
||||
opts.MapMarker = mapMarker
|
||||
|
||||
extractRaw := strings.TrimSpace(m.inputs[inputExtract].Value())
|
||||
if extractRaw != "" {
|
||||
parts := strings.Split(extractRaw, ",")
|
||||
opts.Extract = make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
n := strings.ToUpper(strings.TrimSpace(p))
|
||||
if n == "" {
|
||||
continue
|
||||
}
|
||||
opts.Extract = append(opts.Extract, n)
|
||||
}
|
||||
}
|
||||
|
||||
outDir := strings.TrimSpace(m.inputs[inputOutDir].Value())
|
||||
if outDir == "" {
|
||||
outDir = "."
|
||||
}
|
||||
opts.OutDir = outDir
|
||||
|
||||
opts.DotOut = strings.TrimSpace(m.inputs[inputDotOut].Value())
|
||||
opts.TreePNG = strings.TrimSpace(m.inputs[inputTreePNG].Value())
|
||||
opts.Overlay = strings.TrimSpace(m.inputs[inputOverlay].Value())
|
||||
|
||||
alphaStr := strings.TrimSpace(m.inputs[inputAlpha].Value())
|
||||
if alphaStr == "" {
|
||||
opts.Alpha = 10.0
|
||||
} else {
|
||||
v, err := strconv.ParseFloat(alphaStr, 64)
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("Alpha ungültig: %w", err)
|
||||
}
|
||||
opts.Alpha = v
|
||||
}
|
||||
|
||||
betaStr := strings.TrimSpace(m.inputs[inputBeta].Value())
|
||||
if betaStr == "" {
|
||||
opts.Beta = 1.0
|
||||
} else {
|
||||
v, err := strconv.ParseFloat(betaStr, 64)
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("Beta ungültig: %w", err)
|
||||
}
|
||||
opts.Beta = v
|
||||
}
|
||||
|
||||
epsStr := strings.TrimSpace(m.inputs[inputEps].Value())
|
||||
if epsStr == "" {
|
||||
opts.Eps = geom.EPS
|
||||
} else {
|
||||
v, err := strconv.ParseFloat(epsStr, 64)
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("Eps ungültig: %w", err)
|
||||
}
|
||||
opts.Eps = v
|
||||
}
|
||||
|
||||
leafStr := strings.TrimSpace(m.inputs[inputLeaf].Value())
|
||||
if leafStr == "" {
|
||||
opts.LeafMax = 12
|
||||
} else {
|
||||
v, err := strconv.Atoi(leafStr)
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("LeafMax ungültig: %w", err)
|
||||
}
|
||||
opts.LeafMax = v
|
||||
}
|
||||
|
||||
depthStr := strings.TrimSpace(m.inputs[inputDepth].Value())
|
||||
if depthStr == "" {
|
||||
opts.MaxDepth = 32
|
||||
} else {
|
||||
v, err := strconv.Atoi(depthStr)
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("MaxDepth ungültig: %w", err)
|
||||
}
|
||||
opts.MaxDepth = v
|
||||
}
|
||||
|
||||
candsStr := strings.TrimSpace(m.inputs[inputCands].Value())
|
||||
if candsStr == "" {
|
||||
opts.Cands = 16
|
||||
} else {
|
||||
v, err := strconv.Atoi(candsStr)
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("Cands ungültig: %w", err)
|
||||
}
|
||||
opts.Cands = v
|
||||
}
|
||||
|
||||
seedStr := strings.TrimSpace(m.inputs[inputSeed].Value())
|
||||
if seedStr == "" {
|
||||
opts.Seed = 0
|
||||
} else {
|
||||
v, err := strconv.ParseInt(seedStr, 10, 64)
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("Seed ungültig: %w", err)
|
||||
}
|
||||
opts.Seed = v
|
||||
}
|
||||
|
||||
opts.ListOnly = m.toggles[toggleListOnly]
|
||||
opts.Info = m.toggles[toggleInfo]
|
||||
opts.GeomTest = m.toggles[toggleGeomTest]
|
||||
opts.BuildBSP = m.toggles[toggleBuildBSP]
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func runAppCmd(opts app.Options) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
var buf bytes.Buffer
|
||||
opts.Out = &buf
|
||||
err := app.Run(opts)
|
||||
return runResultMsg{output: buf.String(), err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) setFocus(index int) {
|
||||
total := len(m.inputs) + len(m.toggles)
|
||||
if total == 0 {
|
||||
return
|
||||
}
|
||||
if index < 0 {
|
||||
index = total - 1
|
||||
}
|
||||
if index >= total {
|
||||
index = 0
|
||||
}
|
||||
m.focusIndex = index
|
||||
for i := range m.inputs {
|
||||
if i == m.focusIndex {
|
||||
m.inputs[i].Focus()
|
||||
} else {
|
||||
m.inputs[i].Blur()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if _, err := tea.NewProgram(initialModel(), tea.WithAltScreen()).Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "UI error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
31
go.mod
31
go.mod
@@ -1,3 +1,34 @@
|
||||
module bspviz
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbles v0.18.0
|
||||
github.com/charmbracelet/bubbletea v0.25.0
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
)
|
||||
|
||||
57
go.sum
Normal file
57
go.sum
Normal file
@@ -0,0 +1,57 @@
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
|
||||
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
|
||||
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
|
||||
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
216
internal/app/app.go
Normal file
216
internal/app/app.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"bspviz/internal/bsp"
|
||||
"bspviz/internal/geom"
|
||||
"bspviz/internal/mapfmt"
|
||||
"bspviz/internal/viz"
|
||||
"bspviz/internal/wad"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
WadPath string
|
||||
MapMarker string
|
||||
ListOnly bool
|
||||
Info bool
|
||||
Extract []string
|
||||
OutDir string
|
||||
GeomTest bool
|
||||
BuildBSP bool
|
||||
Alpha float64
|
||||
Beta float64
|
||||
Eps float64
|
||||
LeafMax int
|
||||
MaxDepth int
|
||||
Cands int
|
||||
Seed int64
|
||||
DotOut string
|
||||
TreePNG string
|
||||
Overlay string
|
||||
Out io.Writer
|
||||
}
|
||||
|
||||
func Run(opts Options) error {
|
||||
if opts.Out == nil {
|
||||
opts.Out = os.Stdout
|
||||
}
|
||||
|
||||
if strings.TrimSpace(opts.WadPath) == "" {
|
||||
return fmt.Errorf("wad path required")
|
||||
}
|
||||
|
||||
w, err := wad.Open(opts.WadPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open wad: %w", err)
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
if opts.ListOnly {
|
||||
fmt.Fprintf(opts.Out, "WAD: %s\n", opts.WadPath)
|
||||
for i, d := range w.Dir() {
|
||||
fmt.Fprintf(opts.Out, "%3d: %-8s size=%-7d pos=%-8d\n", i, d.Name(), d.Size, d.FilePos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
start, end, err := w.FindMap(opts.MapMarker)
|
||||
if err != nil {
|
||||
return fmt.Errorf("find map: %w", err)
|
||||
}
|
||||
fmt.Fprintf(opts.Out, "Map %s: Directory [%d, %d)\n", strings.ToUpper(opts.MapMarker), start, end)
|
||||
|
||||
if opts.Info {
|
||||
lumps, err := w.LoadMapLumps(opts.MapMarker, "VERTEXES", "LINEDEFS")
|
||||
if err != nil {
|
||||
return fmt.Errorf("load map lumps: %w", err)
|
||||
}
|
||||
vb := lumps["VERTEXES"]
|
||||
lb := lumps["LINEDEFS"]
|
||||
m, err := mapfmt.LoadMap(lumps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
verts := len(vb) / 4
|
||||
lines := len(lb) / 14
|
||||
fmt.Fprintf(opts.Out, "VERTEXES: bytes=%d count=%d\n", len(vb), verts)
|
||||
fmt.Fprintf(opts.Out, "LINEDEFS: bytes=%d count=%d\n", len(lb), lines)
|
||||
|
||||
fmt.Fprintf(opts.Out, "Map has %d vertices and %d linedefs\n", len(m.Vertices), len(m.Linedefs))
|
||||
fmt.Fprintf(opts.Out, "First vertex: %+v\n", m.Vertices[0])
|
||||
fmt.Fprintf(opts.Out, "First linedef: %+v\n", m.Linedefs[0])
|
||||
|
||||
if len(vb)%4 != 0 {
|
||||
fmt.Fprintln(opts.Out, "WARN: VERTEXES size ist kein Vielfaches von 4 → Format prüfen")
|
||||
}
|
||||
if len(lb)%14 != 0 {
|
||||
fmt.Fprintln(opts.Out, "WARN: LINEDEFS size ist kein Vielfaches von 14 → Format prüfen")
|
||||
}
|
||||
}
|
||||
|
||||
if len(opts.Extract) > 0 {
|
||||
lumps, err := w.LoadMapLumps(opts.MapMarker, opts.Extract...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("extract: %w", err)
|
||||
}
|
||||
if err := os.MkdirAll(opts.OutDir, 0o755); err != nil {
|
||||
return fmt.Errorf("mkdir %s: %w", opts.OutDir, err)
|
||||
}
|
||||
for name, data := range lumps {
|
||||
dst := filepath.Join(opts.OutDir, fmt.Sprintf("%s.lmp", name))
|
||||
if err := os.WriteFile(dst, data, 0o644); err != nil {
|
||||
return fmt.Errorf("write %s: %w", dst, err)
|
||||
}
|
||||
fmt.Fprintf(opts.Out, "wrote %s (%d bytes)\n", dst, len(data))
|
||||
}
|
||||
}
|
||||
|
||||
if opts.GeomTest {
|
||||
raw, err := w.LoadMapLumps(opts.MapMarker, "VERTEXES", "LINEDEFS")
|
||||
if err != nil {
|
||||
return fmt.Errorf("load map lumps: %w", err)
|
||||
}
|
||||
m, err := mapfmt.LoadMap(raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse map: %w", err)
|
||||
}
|
||||
|
||||
segs := mapfmt.LinedefsToSegs(m.Vertices, m.Linedefs)
|
||||
fmt.Fprintf(opts.Out, "GEOM: vertices=%d linedefs=%d segs=%d\n", len(m.Vertices), len(m.Linedefs), len(segs))
|
||||
if len(segs) == 0 {
|
||||
fmt.Fprintln(opts.Out, "GEOM: keine Segmente gefunden – prüfe LINEDEFS/VERTEXES")
|
||||
return nil
|
||||
}
|
||||
|
||||
pts := make([]geom.Vec, 0, len(m.Vertices))
|
||||
for _, v := range m.Vertices {
|
||||
pts = append(pts, geom.V(float64(v.X), float64(v.Y)))
|
||||
}
|
||||
bb := geom.Bounds(pts)
|
||||
w := bb.Max.X - bb.Min.X
|
||||
h := bb.Max.Y - bb.Min.Y
|
||||
fmt.Fprintf(opts.Out, "AABB: min=(%.1f,%.1f) max=(%.1f,%.1f) size=(%.1f×%.1f)\n",
|
||||
bb.Min.X, bb.Min.Y, bb.Max.X, bb.Max.Y, w, h)
|
||||
|
||||
O := segs[0].A
|
||||
D := geom.Sub(segs[0].B, segs[0].A)
|
||||
if geom.Len(D) < 1e-9 && len(segs) > 1 {
|
||||
O = segs[1].A
|
||||
D = geom.Sub(segs[1].B, segs[1].A)
|
||||
}
|
||||
var left, right, splits, degens int
|
||||
for _, s := range segs {
|
||||
f, b := geom.SplitSeg(s, O, D)
|
||||
if len(f) == 0 && len(b) == 0 {
|
||||
degens++
|
||||
continue
|
||||
}
|
||||
left += len(f)
|
||||
right += len(b)
|
||||
if len(f) > 0 && len(b) > 0 {
|
||||
splits++
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(opts.Out, "PROBE-SPLIT: O=(%.1f,%.1f) D=(%.1f,%.1f)\n", O.X, O.Y, D.X, D.Y)
|
||||
fmt.Fprintf(opts.Out, "PROBE-SPLIT: left=%d right=%d splits=%d degens=%d (EPS=%.1e)\n",
|
||||
left, right, splits, degens, geom.EPS)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.BuildBSP {
|
||||
raw, err := w.LoadMapLumps(opts.MapMarker, "VERTEXES", "LINEDEFS")
|
||||
if err != nil {
|
||||
return fmt.Errorf("load map lumps: %w", err)
|
||||
}
|
||||
m, err := mapfmt.LoadMap(raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse map: %w", err)
|
||||
}
|
||||
|
||||
segs := mapfmt.LinedefsToSegs(m.Vertices, m.Linedefs)
|
||||
p := bsp.Params{
|
||||
Alpha: opts.Alpha, Beta: opts.Beta, Eps: opts.Eps,
|
||||
MaxDepth: opts.MaxDepth, LeafMax: opts.LeafMax, Cands: opts.Cands, Seed: opts.Seed,
|
||||
}
|
||||
root := bsp.Build(segs, p)
|
||||
st := bsp.Measure(root)
|
||||
|
||||
fmt.Fprintln(opts.Out, "BSP built.")
|
||||
fmt.Fprintf(opts.Out, " nodes=%d leaves=%d maxDepth=%d totalLeafSegs=%d\n",
|
||||
st.Nodes, st.Leaves, st.MaxDepth, st.TotalSegs)
|
||||
fmt.Fprintf(opts.Out, " params: alpha=%.2f beta=%.2f eps=%.1e leafMax=%d maxDepth=%d cands=%d seed=%d\n",
|
||||
p.Alpha, p.Beta, p.Eps, p.LeafMax, p.MaxDepth, p.Cands, p.Seed)
|
||||
|
||||
if opts.DotOut != "" {
|
||||
if err := viz.EmitDOT(root, opts.DotOut); err != nil {
|
||||
return fmt.Errorf("write DOT: %w", err)
|
||||
}
|
||||
fmt.Fprintf(opts.Out, "DOT export geschrieben: %s\n", opts.DotOut)
|
||||
|
||||
if opts.TreePNG != "" {
|
||||
if err := viz.RunGraphviz(opts.DotOut, opts.TreePNG); err != nil {
|
||||
fmt.Fprintf(opts.Out, "Graphviz fehlgeschlagen: %v\n", err)
|
||||
} else {
|
||||
fmt.Fprintf(opts.Out, "Graphviz PNG gebaut: %s\n", opts.TreePNG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Overlay != "" {
|
||||
if err := viz.RenderPNG(m, root, opts.Overlay); err != nil {
|
||||
return fmt.Errorf("write overlay PNG: %w", err)
|
||||
}
|
||||
fmt.Fprintf(opts.Out, "Overlay PNG geschrieben: %s\n", opts.Overlay)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,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),
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
124
main.go
124
main.go
@@ -1,15 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bspviz/internal/wad"
|
||||
"bspviz/internal/app"
|
||||
"bspviz/internal/geom"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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,75 +26,61 @@ 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)
|
||||
if err != nil {
|
||||
log.Fatalf("open wad: %v", err)
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
if *listOnly {
|
||||
fmt.Printf("WAD: %s\n", *wadPath)
|
||||
for i, d := range w.Dir() {
|
||||
fmt.Printf("%3d: %-8s size=%-7d pos=%-8d\n", i, d.Name(), d.Size, d.FilePos)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
start, end, err := w.FindMap(*mapMarker)
|
||||
if err != nil {
|
||||
log.Fatalf("find map: %v", err)
|
||||
}
|
||||
fmt.Printf("Map %s: Directory [%d, %d)\n", strings.ToUpper(*mapMarker), start, end)
|
||||
|
||||
if *info {
|
||||
lumps, err := w.LoadMapLumps(*mapMarker, "VERTEXES", "LINEDEFS")
|
||||
if err != nil {
|
||||
log.Fatalf("load map lumps: %v", err)
|
||||
}
|
||||
vb := lumps["VERTEXES"]
|
||||
lb := lumps["LINEDEFS"]
|
||||
|
||||
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)
|
||||
|
||||
if len(vb)%4 != 0 {
|
||||
fmt.Println("WARN: VERTEXES size ist kein Vielfaches von 4 → Format prüfen")
|
||||
}
|
||||
if len(lb)%14 != 0 {
|
||||
fmt.Println("WARN: LINEDEFS size ist kein Vielfaches von 14 → Format prüfen")
|
||||
}
|
||||
}
|
||||
|
||||
if *extract != "" {
|
||||
var extractLumps []string
|
||||
if strings.TrimSpace(*extract) != "" {
|
||||
want := strings.Split(*extract, ",")
|
||||
for i := range want {
|
||||
want[i] = strings.ToUpper(strings.TrimSpace(want[i]))
|
||||
}
|
||||
lumps, err := w.LoadMapLumps(*mapMarker, want...)
|
||||
if err != nil {
|
||||
log.Fatalf("extract: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(*outdir, 0o755); err != nil {
|
||||
log.Fatalf("mkdir %s: %v", *outdir, err)
|
||||
}
|
||||
for name, data := range lumps {
|
||||
dst := filepath.Join(*outdir, fmt.Sprintf("%s.lmp", name))
|
||||
if err := os.WriteFile(dst, data, 0o644); err != nil {
|
||||
log.Fatalf("write %s: %v", dst, err)
|
||||
extractLumps = make([]string, 0, len(want))
|
||||
for _, name := range want {
|
||||
n := strings.ToUpper(strings.TrimSpace(name))
|
||||
if n == "" {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("wrote %s (%d bytes)\n", dst, len(data))
|
||||
extractLumps = append(extractLumps, n)
|
||||
}
|
||||
}
|
||||
|
||||
opts := app.Options{
|
||||
WadPath: *wadPath,
|
||||
MapMarker: *mapMarker,
|
||||
ListOnly: *listOnly,
|
||||
Info: *info,
|
||||
Extract: extractLumps,
|
||||
OutDir: *outdir,
|
||||
GeomTest: *geomtest,
|
||||
BuildBSP: *buildbsp,
|
||||
Alpha: *alpha,
|
||||
Beta: *beta,
|
||||
Eps: *eps,
|
||||
LeafMax: *leaf,
|
||||
MaxDepth: *depth,
|
||||
Cands: *cands,
|
||||
Seed: *seed,
|
||||
DotOut: *dotOut,
|
||||
TreePNG: *treePNG,
|
||||
Overlay: *overlay,
|
||||
Out: os.Stdout,
|
||||
}
|
||||
|
||||
if err := app.Run(opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user