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