basic tui implementation
This commit is contained in:
405
cmd/tui/main.go
Normal file
405
cmd/tui/main.go
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
m.setFocus(0)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.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 "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.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 • 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
go.mod
30
go.mod
@@ -1,3 +1,33 @@
|
|||||||
module bspviz
|
module bspviz
|
||||||
|
|
||||||
go 1.25.0
|
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/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
|
||||||
|
)
|
||||||
|
|||||||
55
go.sum
Normal file
55
go.sum
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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/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=
|
||||||
@@ -2,7 +2,7 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -33,9 +33,14 @@ type Options struct {
|
|||||||
DotOut string
|
DotOut string
|
||||||
TreePNG string
|
TreePNG string
|
||||||
Overlay string
|
Overlay string
|
||||||
|
Out io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(opts Options) error {
|
func Run(opts Options) error {
|
||||||
|
if opts.Out == nil {
|
||||||
|
opts.Out = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(opts.WadPath) == "" {
|
if strings.TrimSpace(opts.WadPath) == "" {
|
||||||
return fmt.Errorf("wad path required")
|
return fmt.Errorf("wad path required")
|
||||||
}
|
}
|
||||||
@@ -47,9 +52,9 @@ func Run(opts Options) error {
|
|||||||
defer w.Close()
|
defer w.Close()
|
||||||
|
|
||||||
if opts.ListOnly {
|
if opts.ListOnly {
|
||||||
fmt.Printf("WAD: %s\n", opts.WadPath)
|
fmt.Fprintf(opts.Out, "WAD: %s\n", opts.WadPath)
|
||||||
for i, d := range w.Dir() {
|
for i, d := range w.Dir() {
|
||||||
fmt.Printf("%3d: %-8s size=%-7d pos=%-8d\n", i, d.Name(), d.Size, d.FilePos)
|
fmt.Fprintf(opts.Out, "%3d: %-8s size=%-7d pos=%-8d\n", i, d.Name(), d.Size, d.FilePos)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -58,7 +63,7 @@ func Run(opts Options) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("find map: %w", err)
|
return fmt.Errorf("find map: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Map %s: Directory [%d, %d)\n", strings.ToUpper(opts.MapMarker), start, end)
|
fmt.Fprintf(opts.Out, "Map %s: Directory [%d, %d)\n", strings.ToUpper(opts.MapMarker), start, end)
|
||||||
|
|
||||||
if opts.Info {
|
if opts.Info {
|
||||||
lumps, err := w.LoadMapLumps(opts.MapMarker, "VERTEXES", "LINEDEFS")
|
lumps, err := w.LoadMapLumps(opts.MapMarker, "VERTEXES", "LINEDEFS")
|
||||||
@@ -74,18 +79,18 @@ func Run(opts Options) error {
|
|||||||
|
|
||||||
verts := len(vb) / 4
|
verts := len(vb) / 4
|
||||||
lines := len(lb) / 14
|
lines := len(lb) / 14
|
||||||
fmt.Printf("VERTEXES: bytes=%d count=%d\n", len(vb), verts)
|
fmt.Fprintf(opts.Out, "VERTEXES: bytes=%d count=%d\n", len(vb), verts)
|
||||||
fmt.Printf("LINEDEFS: bytes=%d count=%d\n", len(lb), lines)
|
fmt.Fprintf(opts.Out, "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.Fprintf(opts.Out, "Map has %d vertices and %d linedefs\n", len(m.Vertices), len(m.Linedefs))
|
||||||
fmt.Printf("First vertex: %+v\n", m.Vertices[0])
|
fmt.Fprintf(opts.Out, "First vertex: %+v\n", m.Vertices[0])
|
||||||
fmt.Printf("First linedef: %+v\n", m.Linedefs[0])
|
fmt.Fprintf(opts.Out, "First linedef: %+v\n", m.Linedefs[0])
|
||||||
|
|
||||||
if len(vb)%4 != 0 {
|
if len(vb)%4 != 0 {
|
||||||
fmt.Println("WARN: VERTEXES size ist kein Vielfaches von 4 → Format prüfen")
|
fmt.Fprintln(opts.Out, "WARN: VERTEXES size ist kein Vielfaches von 4 → Format prüfen")
|
||||||
}
|
}
|
||||||
if len(lb)%14 != 0 {
|
if len(lb)%14 != 0 {
|
||||||
fmt.Println("WARN: LINEDEFS size ist kein Vielfaches von 14 → Format prüfen")
|
fmt.Fprintln(opts.Out, "WARN: LINEDEFS size ist kein Vielfaches von 14 → Format prüfen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +107,7 @@ func Run(opts Options) error {
|
|||||||
if err := os.WriteFile(dst, data, 0o644); err != nil {
|
if err := os.WriteFile(dst, data, 0o644); err != nil {
|
||||||
return fmt.Errorf("write %s: %w", dst, err)
|
return fmt.Errorf("write %s: %w", dst, err)
|
||||||
}
|
}
|
||||||
fmt.Printf("wrote %s (%d bytes)\n", dst, len(data))
|
fmt.Fprintf(opts.Out, "wrote %s (%d bytes)\n", dst, len(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,9 +122,9 @@ func Run(opts Options) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
segs := mapfmt.LinedefsToSegs(m.Vertices, m.Linedefs)
|
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))
|
fmt.Fprintf(opts.Out, "GEOM: vertices=%d linedefs=%d segs=%d\n", len(m.Vertices), len(m.Linedefs), len(segs))
|
||||||
if len(segs) == 0 {
|
if len(segs) == 0 {
|
||||||
fmt.Println("GEOM: keine Segmente gefunden – prüfe LINEDEFS/VERTEXES")
|
fmt.Fprintln(opts.Out, "GEOM: keine Segmente gefunden – prüfe LINEDEFS/VERTEXES")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +135,7 @@ func Run(opts Options) error {
|
|||||||
bb := geom.Bounds(pts)
|
bb := geom.Bounds(pts)
|
||||||
w := bb.Max.X - bb.Min.X
|
w := bb.Max.X - bb.Min.X
|
||||||
h := bb.Max.Y - bb.Min.Y
|
h := bb.Max.Y - bb.Min.Y
|
||||||
fmt.Printf("AABB: min=(%.1f,%.1f) max=(%.1f,%.1f) size=(%.1f×%.1f)\n",
|
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)
|
bb.Min.X, bb.Min.Y, bb.Max.X, bb.Max.Y, w, h)
|
||||||
|
|
||||||
O := segs[0].A
|
O := segs[0].A
|
||||||
@@ -152,8 +157,8 @@ func Run(opts Options) error {
|
|||||||
splits++
|
splits++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf("PROBE-SPLIT: O=(%.1f,%.1f) D=(%.1f,%.1f)\n", O.X, O.Y, D.X, D.Y)
|
fmt.Fprintf(opts.Out, "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",
|
fmt.Fprintf(opts.Out, "PROBE-SPLIT: left=%d right=%d splits=%d degens=%d (EPS=%.1e)\n",
|
||||||
left, right, splits, degens, geom.EPS)
|
left, right, splits, degens, geom.EPS)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -177,23 +182,23 @@ func Run(opts Options) error {
|
|||||||
root := bsp.Build(segs, p)
|
root := bsp.Build(segs, p)
|
||||||
st := bsp.Measure(root)
|
st := bsp.Measure(root)
|
||||||
|
|
||||||
fmt.Printf("BSP built.\n")
|
fmt.Fprintln(opts.Out, "BSP built.")
|
||||||
fmt.Printf(" nodes=%d leaves=%d maxDepth=%d totalLeafSegs=%d\n",
|
fmt.Fprintf(opts.Out, " nodes=%d leaves=%d maxDepth=%d totalLeafSegs=%d\n",
|
||||||
st.Nodes, st.Leaves, st.MaxDepth, st.TotalSegs)
|
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",
|
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)
|
p.Alpha, p.Beta, p.Eps, p.LeafMax, p.MaxDepth, p.Cands, p.Seed)
|
||||||
|
|
||||||
if opts.DotOut != "" {
|
if opts.DotOut != "" {
|
||||||
if err := viz.EmitDOT(root, opts.DotOut); err != nil {
|
if err := viz.EmitDOT(root, opts.DotOut); err != nil {
|
||||||
return fmt.Errorf("write DOT: %w", err)
|
return fmt.Errorf("write DOT: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("DOT export geschrieben: %s\n", opts.DotOut)
|
fmt.Fprintf(opts.Out, "DOT export geschrieben: %s\n", opts.DotOut)
|
||||||
|
|
||||||
if opts.TreePNG != "" {
|
if opts.TreePNG != "" {
|
||||||
if err := viz.RunGraphviz(opts.DotOut, opts.TreePNG); err != nil {
|
if err := viz.RunGraphviz(opts.DotOut, opts.TreePNG); err != nil {
|
||||||
log.Printf("Graphviz fehlgeschlagen: %v", err)
|
fmt.Fprintf(opts.Out, "Graphviz fehlgeschlagen: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Graphviz PNG gebaut: %s\n", opts.TreePNG)
|
fmt.Fprintf(opts.Out, "Graphviz PNG gebaut: %s\n", opts.TreePNG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,7 +207,7 @@ func Run(opts Options) error {
|
|||||||
if err := viz.RenderPNG(m, root, opts.Overlay); err != nil {
|
if err := viz.RenderPNG(m, root, opts.Overlay); err != nil {
|
||||||
return fmt.Errorf("write overlay PNG: %w", err)
|
return fmt.Errorf("write overlay PNG: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Overlay PNG geschrieben: %s\n", opts.Overlay)
|
fmt.Fprintf(opts.Out, "Overlay PNG geschrieben: %s\n", opts.Overlay)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user