Kreisel-Umlauf: Abfolge ueber BB-Halbachsen statt Winkel um Mittelpunkt

compute_kreisel_umlauf sortierte Punkte bisher per atan2 um den
Kapsel-Mittelpunkt. Auf der langen Geraden (dx >> dy) liegen alle
echten Winkel in einem winzigen Band nahe 0 Grad - ein grosser,
asymmetrischer Block wie BTMT-Beladung/-Entladung hat einen Bbox-
Mittelpunkt, der deutlich weiter quer zur Achse absteht als der
Belt-Radius, was seinen Winkel unverhaeltnismaessig aufblies und ihn
mehrere eng benachbarte Punkte ueberholen liess, obwohl er entlang der
Achse klar der letzte ist (realer Fall: HundM05, BTMT-Entladung
"0023", landete zwischen 0039/0016 statt hinter 0018/0020).

Neu: Punkt in lokale Achsrichtung drehen und auf die BB-Halbachsen
(halbe Achslaenge, Radius) normieren; welche der beiden Komponenten
ueberwiegt, legt nur noch die Seite fest (Stirnseite/Langseite) - auf
den Langseiten bestimmt ausschliesslich die Achsrichtung die Position,
der quer abstehende Anteil kippt bestenfalls die Seiten-Zuordnung,
verzerrt aber nicht mehr die Reihenfolge selbst. Mit den realen
HundM05-Koordinaten verifiziert; test_export_kreisel_umlauf.py und
test_export_graph.py weiterhin gruen (20/20), keine neuen Regressionen
in der breiteren Suite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-09-17 14:26:11 +02:00
co-authored by Claude Sonnet 5
parent d0d112e740
commit 0e4ea7e148
+53 -15
View File
@@ -1521,26 +1521,64 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
acx = (cap["a"][0] + cap["b"][0]) / 2.0
acy = (cap["a"][1] + cap["b"][1]) / 2.0
achs_winkel = math.atan2(cap["b"][1] - cap["a"][1], cap["b"][0] - cap["a"][0])
cos_achs, sin_achs = math.cos(achs_winkel), math.sin(achs_winkel)
# Halbe Achslaenge (Mitte -> a bzw. b) und Radius der Kapsel = die
# beiden Halbachsen der Kreisel-BB im lokalen (gedrehten) Koordinaten-
# system - siehe kreisel_bounds oben, dieselbe "maximale Ausdehnung".
halblaenge = math.hypot(cap["b"][0] - cap["a"][0], cap["b"][1] - cap["a"][1]) / 2.0
radius = cap["r"]
uzs = (drehrichtung or "UZS").strip().upper() != "GUZ"
def _winkel(pxy):
# Rohwinkel relativ zur Achsrichtung, auf [0, 2*pi) normiert IN
# UMLAUFRICHTUNG. Vorher (reine Vorgaenger-/Nachfolger-Ermittlung
# ohne Startpunkt-Anspruch) wurde UZS durch reverse=True auf dem
# rohen mathematischen Winkel erreicht (= "absteigend sortieren");
# absteigend sortieren auf theta ist aequivalent zu aufsteigend
# sortieren auf -theta - darum wird HIER fuer UZS negiert (NICHT
# fuer GUZ, das dem unveraenderten mathematischen Winkel bereits
# aufsteigend entspricht). Ohne dieses Vorzeichen liefe der UZS-
# Umlauf in die falsche (zu GUZ spiegelverkehrte) Richtung - siehe
# Abfolge ueber die BB des Kreisels statt ueber einen Winkel um den
# Mittelpunkt: Punkt zuerst ins lokale (um achs_winkel gedrehte)
# Koordinatensystem der Kreiselachse bringen (lx = Achsrichtung,
# ly = quer dazu), dann auf die BB-Halbachsen normieren
# (u = lx/halblaenge, v = ly/radius). Je nachdem, welche der beiden
# Groessen (u oder v) betragsmaessig ueberwiegt, liegt der Punkt auf
# der rechten/linken Stirnseite (u dominiert) oder der oberen/
# unteren Langseite (v dominiert) der BB - dort bestimmt NUR die
# jeweils andere (die entlang der Seite laufende) Koordinate die
# Position, der ueberwiegende Anteil legt nur noch die Seite fest.
# Ein grosser, asymmetrischer Block (z.B. BTMT-Beladung/-Entladung)
# hat einen Bbox-Mittelpunkt, der deutlich weiter quer zur Achse
# absteht als der Belt-Radius - beim reinen Winkel-um-Mittelpunkt
# (frueherer Ansatz) blies das seinen Winkel unverhaeltnismaessig
# auf und liess ihn mehrere eng benachbarte Punkte in der engen
# Sortierung ueberholen, obwohl er entlang der Achse klar der
# letzte ist (realer Bug-Fall: HundM05, BTMT-Entladung "0023").
# Hier wirkt sich dieselbe Ueberhoehung nicht mehr aus, solange sie
# nicht die Seiten-Zuordnung selbst umkippt.
#
# t liegt in [0,4) und laeuft GEGEN den Uhrzeigersinn (mathematische
# Standardrichtung) einmal um die BB, beginnend bei t=0 auf der
# rechten Stirnseite in Achsrichtung (lx=halblaenge, ly=0) - das
# ist derselbe Nullpunkt wie beim frueheren "0 Grad = Achsrichtung".
# 0..1 rechte Stirnseite (ly steigend), 1..2 obere Langseite (lx
# fallend), 2..3 linke Stirnseite (ly fallend), 3..4 untere
# Langseite (lx steigend).
dx0 = pxy[1] - acx
dy0 = pxy[2] - acy
lx = dx0 * cos_achs + dy0 * sin_achs
ly = -dx0 * sin_achs + dy0 * cos_achs
u = lx / halblaenge if halblaenge > 1e-9 else 0.0
v = ly / radius if radius > 1e-9 else 0.0
if u >= abs(v):
t = v / 2.0
elif v >= abs(u):
t = 1.0 - u / 2.0
elif u <= -abs(v):
t = 2.0 - v / 2.0
else:
t = 3.0 + u / 2.0
# UZS (Uhrzeigersinn, Default) negiert t (= absteigend sortieren
# auf t ist aequivalent zu aufsteigend auf -t) - identisches Muster
# wie beim vorherigen Winkel-Ansatz, siehe
# tests/test_export_kreisel_umlauf.py::TestKreiselUmlaufStartetBeiNullGrad.
# Die Normierung auf [0, 2*pi) sorgt zusaetzlich dafuer, dass
# Index 0 nach dem Sortieren der kleinste Winkel >= 0 Grad ist
# (der gewuenschte Start bei 0 Grad).
roh = math.atan2(pxy[2] - acy, pxy[1] - acx) - achs_winkel
roh = -roh if uzs else roh
return roh % (2.0 * math.pi)
t = -t if uzs else t
return t % 4.0
sortiert = sorted(punkte, key=lambda p: _winkel(p))
n = len(sortiert)