From 1b8b6647b856c9316a408d83b900bfe6274f7924 Mon Sep 17 00:00:00 2001 From: Doc Date: Sat, 20 Sep 2025 12:18:49 +0200 Subject: [PATCH] implemented bsp/build --- internal/bsp/build.go | 159 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 1 deletion(-) diff --git a/internal/bsp/build.go b/internal/bsp/build.go index c30d81c..7945d2b 100644 --- a/internal/bsp/build.go +++ b/internal/bsp/build.go @@ -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), + } +}