CSV-Export: Nachbarschaft/bbox fuer Kreisel-Kreisel-Weichen, sepliste-Ketten und Separatoren repariert
Kreisel-Kreisel-Weichen fehlten bisher im Kreisel-Umlauf (compute_kreisel_umlauf), wodurch angrenzende Separatoren auf falsche/fehlende Nachbarn zeigten. Die kreisel_bounds-Matching-Box ignorierte ausserdem die tatsaechliche Kreiseldrehung (faelschlich als achsparallel angenommen), was bei gedrehten langen Kreiseln (Mubea Kreisel 0002) zu falschen Nachbarschaften fuehrte. Die sepliste-XDATA (AS/SEP/ES-Reihenfolge je VF_n/GF_n) wurde nie ans Python-Item durchgereicht - die gesamte Ketten-Nachbarschaftserkennung lief dadurch ins Leere. Nach dem Beheben entstand eine Doppelzaehlung: die in einer Kette verpackten Separatoren existieren bereits als echte Proxy-INSERTs (csv:sep-proxies-erzeugen); build_separator_kette_items erzeugte zusaetzlich eigene Zeilen aus der sepliste. Umgebaut zu map_separator_kette_items: ordnet die sepliste-Reihenfolge den vorhandenen Proxy-Items zu (Match ueber Wrapper-ID + INSERT-Punkt) statt neue Zeilen zu erzeugen - keine Doppelzaehlung mehr, Nachbarn korrekt gesetzt. Ausserdem: vla-getboundingbox scheiterte in einem realen Export bei ALLEN Bloecken (fehlende bbox ueberall), der Fehler wurde bisher still verschluckt. REGENALL vor der Export-Schleife behebt das; eine neue Diagnose (exp-bbox-fail) meldet kuenftige Ausfaelle statt sie zu verschlucken. Freie/eingebettete Separatoren ohne bbox bekommen zusaetzlich einen Fallback aus einer konfigurierbaren Symbol-Groesse (cfg/export.cfg [Boundingbox] separator_box_*). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+107
-14
@@ -121,6 +121,56 @@ def load_element_box_mm(cfg_path=None):
|
||||
parser.getfloat("Boundingbox", "element_box_hoehe_mm", fallback=WEICHE_BOX_HOEHE_MM),
|
||||
)
|
||||
|
||||
|
||||
# Fallback-Masse (mm) der festen Separator-Symbol-Bounding-Box, falls
|
||||
# cfg/export.cfg [Boundingbox] separator_box_* nicht gesetzt/lesbar ist (siehe
|
||||
# load_separator_box_mm). LAENGE laengs des Symbols (bei Drehung 0 entlang X),
|
||||
# BREITE quer, HOEHE = Z. Gemessenes Separator_SP-Symbol (~210 x 150 x 14 mm).
|
||||
SEPARATOR_BOX_LAENGE_MM = 210.0
|
||||
SEPARATOR_BOX_BREITE_MM = 150.0
|
||||
SEPARATOR_BOX_HOEHE_MM = 14.0
|
||||
|
||||
|
||||
def load_separator_box_mm(cfg_path=None):
|
||||
"""Feste Bounding-Box-Masse (Laenge, Breite, Hoehe in mm) eines Separator-
|
||||
Symbols aus cfg/export.cfg [Boundingbox] separator_box_laenge_mm /
|
||||
_breite_mm / _hoehe_mm. Gebraucht fuer die in einer VF_n/GF_n-Kette
|
||||
eingebetteten Separatoren (sepliste-XDATA), die kein reales INSERT mehr
|
||||
sind und darum von vla-getboundingbox keine Box bekommen. Fallback auf
|
||||
SEPARATOR_BOX_*_MM, falls die Sektion/Schluessel fehlen.
|
||||
Rueckgabe: (laenge, breite, hoehe)."""
|
||||
if cfg_path is None:
|
||||
cfg_path = cfg_path_from_env()
|
||||
parser = load_export_cfg(cfg_path)
|
||||
return (
|
||||
parser.getfloat("Boundingbox", "separator_box_laenge_mm", fallback=SEPARATOR_BOX_LAENGE_MM),
|
||||
parser.getfloat("Boundingbox", "separator_box_breite_mm", fallback=SEPARATOR_BOX_BREITE_MM),
|
||||
parser.getfloat("Boundingbox", "separator_box_hoehe_mm", fallback=SEPARATOR_BOX_HOEHE_MM),
|
||||
)
|
||||
|
||||
|
||||
def separator_bbox(x, y, rotation_grad, separator_box_mm=None, z=0.0):
|
||||
"""Achsparallele Welt-Bounding-Box (_bbox-dict) eines Separator-Symbols an
|
||||
Weltposition (x, y) mit Drehung rotation_grad. Die feste Symbol-Box
|
||||
(separator_box_mm = (laenge, breite, hoehe)) wird um rotation_grad gedreht
|
||||
und ihre achsparallele Welt-Huelle zurueckgegeben - fuer eingebettete
|
||||
sepliste-Separatoren, die keine echte Zeichnungsgeometrie haben.
|
||||
|
||||
Rueckgabe: dict {"cx","cy","cz","dx","dy","dz"} wie csv:get-bbox / die
|
||||
"_bbox" der realen Bloecke (siehe export_csv.bbox_columns)."""
|
||||
if separator_box_mm is None:
|
||||
separator_box_mm = load_separator_box_mm()
|
||||
laenge, breite, hoehe = separator_box_mm
|
||||
# Laenge liegt bei Drehung 0 entlang X, Breite entlang Y (anders als
|
||||
# _oriented_box_aabb, das die Laengsseite QUER zur Achse legt - hier ist
|
||||
# es die reine Symbol-AABB, konsistent mit der gemessenen Referenz).
|
||||
rot = math.radians(rotation_grad or 0.0)
|
||||
c, s = abs(math.cos(rot)), abs(math.sin(rot))
|
||||
dx = laenge * c + breite * s
|
||||
dy = laenge * s + breite * c
|
||||
return {"cx": x, "cy": y, "cz": z, "dx": dx, "dy": dy, "dz": hoehe}
|
||||
|
||||
|
||||
# Mindestanzahl Partner (Nachbarn) je TeileArt fuer die Fehlerspalte "Fehler"
|
||||
# (siehe compute_neighbor_errors): Kreisel/Eckrad und Omniflo-Elemente
|
||||
# brauchen mindestens einen Partner, Gefaellestrecke/Foerderer/Strecke-Modul
|
||||
@@ -930,7 +980,13 @@ def compute_kreisel_touch_switches(items, tolerance_mm, durchmesser_mm=800.0,
|
||||
|
||||
Rueckgabe: Liste von dicts (in items-Reihenfolge der Paare), je erzeugte
|
||||
Weiche:
|
||||
{"x","y","z": Beruehrpunkt (mm),
|
||||
{"key": "WEICHE#<teileid_a>#<teileid_b>" - stabiler Schluessel, ueber den
|
||||
compute_kreisel_umlauf die Weiche in BEIDE beruehrenden Kreisel-
|
||||
Umlaeufe einhaengt (die Weiche gehoert als Verzweigungspunkt zu
|
||||
beiden, anders als ein Separator oder ein AS/ES-Ende) UND
|
||||
build_kreisel_weiche_items die zugehoerige TeileId nach dem Umlauf-
|
||||
Aufbau wiederfindet,
|
||||
"x","y","z": Beruehrpunkt (mm),
|
||||
"dx","dy","dz": Welt-AABB der ausgerichteten Box (mm),
|
||||
"nachbarn": [teileid_a, teileid_b],
|
||||
"achse_von": teileid des orientierungsgebenden Kreisels,
|
||||
@@ -987,6 +1043,7 @@ def compute_kreisel_touch_switches(items, tolerance_mm, durchmesser_mm=800.0,
|
||||
|
||||
dx, dy, dz = _oriented_box_aabb(achse_rot, box_l, box_b, box_h)
|
||||
switches.append({
|
||||
"key": f"WEICHE#{id_a}#{id_b}",
|
||||
"x": tx, "y": ty, "z": tz,
|
||||
"dx": dx, "dy": dy, "dz": dz,
|
||||
"nachbarn": [id_a, id_b],
|
||||
@@ -1285,7 +1342,7 @@ def compute_sep_kette(items, dbg=None):
|
||||
|
||||
Jeder Sepliste-Eintrag bekommt eine (innerhalb dieser Kette eindeutige)
|
||||
Schluessel-ID "<strecke_teileid>#<lfdnr>" - das ist die ID, mit der
|
||||
build_strecke_schleus_items/build_separator_kette_items die spaeteren
|
||||
build_strecke_schleus_items/map_separator_kette_items die spaeteren
|
||||
globalen TeileIds verknuepft (siehe export_csv.py). Vorgaenger/Nachfolger
|
||||
sind schlicht der vorherige/naechste Eintrag in der Sepliste (die bereits
|
||||
in Baureihenfolge vorliegt) - kein Rateverfahren noetig.
|
||||
@@ -1320,13 +1377,23 @@ def compute_sep_kette(items, dbg=None):
|
||||
return result
|
||||
|
||||
|
||||
def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0, dbg=None):
|
||||
def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
|
||||
touch_switches=None, dbg=None):
|
||||
"""Bringt fuer jeden echten Kreisel (KREISEL_SPLIT_TEILEART) alle dort
|
||||
liegenden Punkte - frei platzierte Separatoren (per compute_sensor_
|
||||
zuordnung bereits einer Kreiselhaelfte zugeordnet) UND die AS/ES-Enden
|
||||
zuordnung bereits einer Kreiselhaelfte zugeordnet), die AS/ES-Enden
|
||||
aller andockenden VF_n/GF_n-Ketten (aus sep_kette, per BBox-Naehe wie
|
||||
compute_strecke_kreisel_schleus zugeordnet) - in eine geschlossene
|
||||
Umlauf-Reihenfolge (zirkulaer, siehe DREHRICHTUNG-Attribut).
|
||||
compute_strecke_kreisel_schleus zugeordnet) UND die Kreisel-Kreisel-
|
||||
Weichen (touch_switches, siehe compute_kreisel_touch_switches) - in eine
|
||||
geschlossene Umlauf-Reihenfolge (zirkulaer, siehe DREHRICHTUNG-Attribut).
|
||||
|
||||
Eine Kreisel-Kreisel-Weiche liegt (anders als ein Separator oder ein
|
||||
AS/ES-Ende) auf GENAU EINEM Punkt, gehoert aber zu ZWEI Kreiseln - man
|
||||
kann von Kreisel A zu Kreisel B UND umgekehrt wechseln. Sie wird darum
|
||||
in punkte_je_kreisel BEIDER beruehrender Kreisel eingetragen (gleiche
|
||||
Weltposition, gleicher "key" - siehe touch_switches), damit sie im
|
||||
Umlauf jedes der beiden Kreisel zwischen den jeweils angrenzenden
|
||||
Separatoren/AS-ES-Enden auftaucht statt spurlos zu fehlen.
|
||||
|
||||
Winkel wird um den Kapsel-Mittelpunkt (Mitte AN8-/SP8-Achse, siehe
|
||||
_kreisel_capsule) gemessen, 0 Grad = Kapsel-Achsrichtung. UZS
|
||||
@@ -1336,12 +1403,15 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
|
||||
|
||||
items = dieselbe Liste wie bei compute_neighbor_ids/compute_sensor_
|
||||
zuordnung. sep_kette = Rueckgabe von compute_sep_kette (fuer die AS/ES-
|
||||
Punkte: x/y + strecke_id).
|
||||
Punkte: x/y + strecke_id). touch_switches = Rueckgabe von
|
||||
compute_kreisel_touch_switches (None/leer = wie bisher ohne Weichen im
|
||||
Umlauf).
|
||||
|
||||
Rueckgabe: dict {kreisel_teileid: [(punkt_key, vorgaenger_key,
|
||||
nachfolger_key), ...]} in Umlaufreihenfolge - punkt_key ist entweder eine
|
||||
Separator-TeileId (freie Separatoren) oder ein sep_kette-Schluessel
|
||||
(AS/ES-Enden). Kreisel ohne zugeordnete Punkte liefern eine leere Liste.
|
||||
nachfolger_key), ...]} in Umlaufreihenfolge - punkt_key ist eine
|
||||
Separator-TeileId (freie Separatoren), ein sep_kette-Schluessel (AS/ES-
|
||||
Enden) oder ein touch_switches-"key" (Kreisel-Kreisel-Weiche). Kreisel
|
||||
ohne zugeordnete Punkte liefern eine leere Liste.
|
||||
"""
|
||||
half_tol = tolerance_mm / 2.0
|
||||
kreisel = {}
|
||||
@@ -1355,13 +1425,27 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
|
||||
# bestimmbar, wie compute_sensor_zuordnung es fuer die "Zuordnung"-Spalte
|
||||
# tut - hier direkt gegen die volle Kreisel-BBox statt der Haelften, weil
|
||||
# nur "gehoert zu diesem Kreisel" gebraucht wird, nicht links/rechts).
|
||||
#
|
||||
# Achsparallele Welt-AABB der Kapsel (a/b + Radius je Achse) statt einer
|
||||
# Box mit fest "dx=durchmesser+achs_len, dy=durchmesser" - letzteres
|
||||
# nimmt STILLSCHWEIGEND an, die Kreiselachse liegt entlang Welt-X. Bei
|
||||
# einem um 90 Grad gedrehten Kreisel (Achse entlang Welt-Y, z.B. ein
|
||||
# 20m-Kreisel laengs der Halle) drehte sich damit auch die Box um 90 Grad
|
||||
# gegen die tatsaechliche Geometrie - schmal in Y (dort, wo der Kreisel in
|
||||
# Wahrheit 20m lang ist) und ueberbreit in X (dort, wo er nur
|
||||
# durchmesser_mm misst). Ergebnis: entfernte Punkte wurden faelschlich
|
||||
# als "auf dem Kreisel" erkannt, waehrend tatsaechlich anliegende
|
||||
# Separatoren durchfielen (realer Bug-Fall: Mubea, Kreisel 0002).
|
||||
kreisel_bounds = {}
|
||||
for tid, (cap, _dr) in kreisel.items():
|
||||
acx = (cap["a"][0] + cap["b"][0]) / 2.0
|
||||
acy = (cap["a"][1] + cap["b"][1]) / 2.0
|
||||
achs_len = math.hypot(cap["b"][0] - cap["a"][0], cap["b"][1] - cap["a"][1])
|
||||
r = cap["r"]
|
||||
ax, ay = cap["a"]
|
||||
bx, by = cap["b"]
|
||||
minx, maxx = min(ax, bx) - r, max(ax, bx) + r
|
||||
miny, maxy = min(ay, by) - r, max(ay, by) + r
|
||||
kreisel_bounds[tid] = _bounds(
|
||||
{"cx": acx, "cy": acy, "dx": durchmesser_mm + achs_len, "dy": durchmesser_mm},
|
||||
{"cx": (minx + maxx) / 2.0, "cy": (miny + maxy) / 2.0,
|
||||
"dx": maxx - minx, "dy": maxy - miny},
|
||||
half_tol)
|
||||
|
||||
punkte_je_kreisel = {tid: [] for tid in kreisel}
|
||||
@@ -1395,6 +1479,15 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
|
||||
if best_tid is not None:
|
||||
punkte_je_kreisel[best_tid].append((key, eintrag["x"], eintrag["y"]))
|
||||
|
||||
# Kreisel-Kreisel-Weichen: die beiden beruehrenden Kreisel-IDs stehen
|
||||
# bereits in sw["nachbarn"] (siehe compute_kreisel_touch_switches) - kein
|
||||
# Naeherungsverfahren wie bei Separator/AS/ES noetig, die Weiche wird in
|
||||
# BEIDE Umlaeufe eingetragen.
|
||||
for sw in (touch_switches or []):
|
||||
for tid in sw.get("nachbarn", []):
|
||||
if tid in punkte_je_kreisel:
|
||||
punkte_je_kreisel[tid].append((sw["key"], sw["x"], sw["y"]))
|
||||
|
||||
result = {}
|
||||
for tid, punkte in punkte_je_kreisel.items():
|
||||
cap, drehrichtung = kreisel[tid]
|
||||
|
||||
Reference in New Issue
Block a user