From 0e4ea7e1485a903f2804ad497a1a38d49338d723 Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Thu, 17 Sep 2026 14:26:11 +0200 Subject: [PATCH] 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 --- lib/export_neighbors.py | 68 ++++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 15 deletions(-) diff --git a/lib/export_neighbors.py b/lib/export_neighbors.py index ec5412f..a832530 100644 --- a/lib/export_neighbors.py +++ b/lib/export_neighbors.py @@ -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)