Files
bspviz/internal/bsp/build.go
2025-09-20 12:18:49 +02:00

161 lines
3.8 KiB
Go

package bsp
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),
}
}