From dc2b0b8900c1e7f28e5671e7cc47eaf87da6c9aa Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Tue, 4 Aug 2026 15:18:59 +0200 Subject: [PATCH] Export: ILS-Weiche an Kreisel-Beruehrpunkten + Ein-/Ausschleuselemente an Strecken - compute_kreisel_touch_switches: erzeugt an jedem Beruehrpunkt zweier Kreisel eine "ILS Weiche" (Kapsel-Geometrie, Position=Beruehrpunkt, statische Box 250x100x30 mit Laengsseite senkrecht zur Kreiselachse, Nachbarn=beide Kreisel) - compute_strecke_kreisel_schleus: an Anfang/Ende jeder Gefaellestrecke bzw. jedes Foerderers (ILS 2.0 Strecke) ein Aus-/Einschleuselement an der Kollisionsposition zum naechsten BBox-Nachbar-Kreisel (Nachbarn=Kreisel+Strecke) - TeileId zaehlt fortlaufend aus der hoechsten vorhandenen ID hoch (Weichen zuerst, dann Schleuselemente) - Warnung (Konsole + neue CSV-Spalte "Warnungen"), wenn eine Strecke mehr als zwei Kreisel-BoundingBoxen beruehrt (erwartet: ein Anfang + ein Ende) Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/export_csv.py | 94 ++++++++++++++++++++- lib/export_neighbors.py | 183 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 273 insertions(+), 4 deletions(-) diff --git a/lib/export_csv.py b/lib/export_csv.py index dc91c65..35319fa 100644 --- a/lib/export_csv.py +++ b/lib/export_csv.py @@ -15,12 +15,25 @@ K1-K4/Insertpoint leer, Boundingbox = statische Default-Box (WEICHE_BOX_*), mit der Laengsseite senkrecht zur Achse des beruehrenden Kreisels; die TeileId zaehlt aus der hoechsten bereits vergebenen numerischen ID hoch. +Anschliessend (TeileId-Zaehlung laeuft ueber die Weichen hinweg weiter) wird +an jedem Ende einer Gefaellestrecke/eines Foerderers, an dem ein Kreisel +anschliesst, ein Aus- (Anfang) bzw. Einschleuselement (Ende) an der Kollisions- +position Kreisel<->Strecke angehaengt (siehe build_strecke_schleus_items / +export_neighbors.compute_strecke_kreisel_schleus) - Format wie die Weiche, +Nachbarn = Kreisel-ID + Strecken-ID. Zugeordnet wird je Streckenende der +naechstgelegene ihrer BBox-Kreisel-Nachbarn. + +Warnungen = Spalte fuer Auffaelligkeiten. Aktuell: eine Strecke sollte genau +zwei Kreisel (Anfang + Ende) beruehren - beruehrt ihre (in x/y gepruefte) +Bounding-Box mehr, steht hier ein Hinweis und es wird zusaetzlich eine +Konsolen-WARNUNG ausgegeben (mit exakten 3D-BBoxen sollte das nicht auftreten). + Aufruf: python export_csv.py CSV-Format: Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox; - Insertpoint;K1;K2;K3;K4;Nachbarn;Fehler;Merkmale + Insertpoint;K1;K2;K3;K4;Nachbarn;Fehler;Warnungen;Merkmale Position und Boundingbox stammen aus der von export.lsp (csv:get-bbox, per vla-getboundingbox) ermittelten Bounding-Box je Block: @@ -90,8 +103,10 @@ from export_neighbors import ( compute_sensor_zuordnung, compute_scanner_nearest_separator, compute_kreisel_touch_switches, + compute_strecke_kreisel_schleus, SENSOR_TEILEARTEN, WEICHE_TEILEART, + GF_VF_TEILEARTEN, ) BLOCKPATTERNS = load_patterns() @@ -462,6 +477,65 @@ def build_kreisel_weiche_items(items, planquadrat_cfg, start_elem_nr, dbg=None): return elem_nr +def build_strecke_schleus_items(items, planquadrat_cfg, start_elem_nr, dbg=None): + """Haengt fuer jedes Strecken-Ende mit anschliessendem Kreisel eine + "ILS Ausschleuselement"- (Anfang) bzw. "ILS Einschleuselement"-Zeile + (Ende) an items an (siehe export_neighbors.compute_strecke_kreisel_schleus). + + Muss NACH build_kreisel_weiche_items laufen, damit die TeileId-Zaehlung + (max_numeric_teileid) auch die bereits erzeugten Weichen einschliesst. + Position = Kollisionsposition Kreisel<->Strecke; Nachbarn = Kreisel-ID + + Strecken-ID; Anzahl 1; K1-K4/Insertpoint leer; Boundingbox = die statische, + an der Kreiselachse ausgerichtete Default-Box. + + Rueckgabe: fortgeschriebene Elementnummer. + """ + schleus, warnings = compute_strecke_kreisel_schleus( + items, durchmesser_mm=KREISEL_DURCHMESSER_MM, dbg=dbg) + + # Warnungen (>2 Kreisel-BBox-Beruehrungen je Strecke) auf die Strecken-Zeile + # schreiben (Spalte "Warnungen") und in die Konsole ausgeben. + if warnings: + strecke_by_id = {it.get("teileid", ""): it for it in items + if it.get("teileart", "") in GF_VF_TEILEARTEN} + for sid, text in warnings.items(): + if sid in strecke_by_id: + strecke_by_id[sid]["warnung"] = text + print(f"[export_csv] WARNUNG: Strecke {sid} {text}") + + naechste_id = max_numeric_teileid(items) + elem_nr = start_elem_nr + zaehler = {} + for sp in schleus: + elem_nr += 1 + naechste_id += 1 + teileid = f"{naechste_id:04d}" + teileart = sp["teileart"] + zaehler[teileart] = zaehler.get(teileart, 0) + 1 + planquadrat = (compute_planquadrat(sp["x"], sp["y"], planquadrat_cfg) + if planquadrat_cfg is not None else "") + items.append({ + "nr": elem_nr, + "teileart": teileart, + "teileid": teileid, + "bezeichnung": f"{teileart} :{zaehler[teileart]}", + "planquadrat": planquadrat, + "position": f'{sp["x"]:.2f}, {sp["y"]:.2f}, {sp["z"]:.2f}', + "boundingbox": f'{sp["dx"]:.2f}, {sp["dy"]:.2f}, {sp["dz"]:.2f}', + "insertpoint": "", + "k1": "", "k2": "", "k3": "", "k4": "", + "nachbarn": ", ".join([sp["kreisel_id"], sp["strecke_id"]]), + "fehler": "", + "merkmale": { + "Erzeugt aus": "Kreisel-Strecke-Kollision", + "Kreisel": sp["kreisel_id"], + "Strecke": sp["strecke_id"], + "Streckenende": sp["ende_label"], + }, + }) + return elem_nr + + def process_blocks(blocks, lookup, dbg=None): # x_origin_mm/y_origin_mm einmalig fuer diesen Exportlauf aufloesen (siehe # export_planquadrat.resolve_origins): automatisch aus den Bloecken, falls @@ -565,6 +639,10 @@ def process_blocks(blocks, lookup, dbg=None): "bezeichnung": f"VarioFoerderer :{vf_count}", "planquadrat": planquadrat_column(block, planquadrat_cfg), "merkmale": build_variofoerderer_merkmale(block), + # x/y (Block-Einfuegepunkt = Kettenanfang) fuer die Anfang/Ende- + # Bestimmung der Ein-/Ausschleuselemente (compute_strecke_kreisel_schleus) + "x": block.get("x", 0.0), + "y": block.get("y", 0.0), **bbox_columns(block), }) continue @@ -580,6 +658,10 @@ def process_blocks(blocks, lookup, dbg=None): "bezeichnung": f"Gefaellestrecke :{gf_count}", "planquadrat": planquadrat_column(block, planquadrat_cfg), "merkmale": build_variofoerderer_merkmale(block), + # x/y (Block-Einfuegepunkt = Kettenanfang) fuer die Anfang/Ende- + # Bestimmung der Ein-/Ausschleuselemente (compute_strecke_kreisel_schleus) + "x": block.get("x", 0.0), + "y": block.get("y", 0.0), **bbox_columns(block), }) continue @@ -728,8 +810,11 @@ def process_blocks(blocks, lookup, dbg=None): # Nach der Kollisions-/Nachbarschaftserkennung: an jedem Kreisel- # Beruehrpunkt eine ILS-Weiche anhaengen (eigene Zeilen am Listenende - # sie nehmen an der Nachbarschaftserkennung selbst nicht teil, ihre - # Nachbarn sind fest die beiden beruehrenden Kreisel). - build_kreisel_weiche_items(items, planquadrat_cfg, elem_nr, dbg=dbg) + # Nachbarn sind fest die beiden beruehrenden Kreisel). Danach an jedem + # Strecken-Ende mit anschliessendem Kreisel ein Aus-/Einschleuselement - + # die TeileId-Zaehlung laeuft ueber beide hinweg weiter (Weichen zuerst). + elem_nr = build_kreisel_weiche_items(items, planquadrat_cfg, elem_nr, dbg=dbg) + build_strecke_schleus_items(items, planquadrat_cfg, elem_nr, dbg=dbg) return items @@ -756,6 +841,7 @@ def format_csv_line(item): f';"{csv_quote(item["k4"])}"' f';"{csv_quote(item["nachbarn"])}"' f';"{csv_quote(item["fehler"])}"' + f';"{csv_quote(item.get("warnung", ""))}"' f';{merkmale_json}' ) @@ -812,7 +898,7 @@ def main(): print(f"[export_csv] Kollisions-Debuglog geschrieben: {NEIGHBOR_DEBUG_TARGET}") header = ("Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;" - "Insertpoint;K1;K2;K3;K4;Nachbarn;Fehler;Merkmale") + "Insertpoint;K1;K2;K3;K4;Nachbarn;Fehler;Warnungen;Merkmale") with open(output_csv, "w", encoding="utf-8") as f: f.write(header + "\n") for item in items: diff --git a/lib/export_neighbors.py b/lib/export_neighbors.py index 88f47fe..88987fd 100644 --- a/lib/export_neighbors.py +++ b/lib/export_neighbors.py @@ -83,6 +83,17 @@ KREISEL_SPLIT_TEILEART = "ILS 2.0 Kreisel" # (compute_kreisel_touch_switches / export_csv.py). WEICHE_TEILEART = "ILS Weiche" +# TeileArten der automatisch an den Strecken-Enden (Kollision Kreisel <-> +# Gefaellestrecke/Foerderer) erzeugten Ein-/Ausschleuselemente +# (compute_strecke_kreisel_schleus / export_csv.py). +AUSSCHLEUS_TEILEART = "ILS Ausschleuselement" +EINSCHLEUS_TEILEART = "ILS Einschleuselement" + +# Strecken-TeileArten, die Ein-/Ausschleuselemente bekommen: Gefaellestrecke +# und (Vario-)Foerderer. "ILS 2.0 Strecke - Modul" (Einzelkomponenten) bewusst +# NICHT - nur die kompletten GF_n/VF_n-Bloecke. +GF_VF_TEILEARTEN = {"ILS 2.0 Gefaellestrecke", "ILS 2.0 Strecke"} + # Statische Default-Boundingbox einer solchen ILS-Weiche (Laenge x Breite x # Hoehe in mm). Die Laengsseite (WEICHE_BOX_LAENGE_MM) wird senkrecht zur # Achse desjenigen Kreisels ausgerichtet, dessen Endkreis den Beruehrpunkt @@ -945,3 +956,175 @@ def compute_kreisel_touch_switches(items, tolerance_mm, durchmesser_mm=800.0, db dbg(f"Erzeugte ILS-Weichen: {len(switches)}") return switches + + +# --------------------------------------------------------------------------- +# Kreisel <-> Strecken-Enden -> Ein-/Ausschleuselemente +# --------------------------------------------------------------------------- + +def _closest_pt_on_segment(a, b, p): + """Naechstgelegener Punkt auf der 2D-Strecke [a,b] zum Punkt p.""" + abx, aby = b[0] - a[0], b[1] - a[1] + denom = abx * abx + aby * aby + if denom <= 1e-9: + return (a[0], a[1]) + t = _clamp(((p[0] - a[0]) * abx + (p[1] - a[1]) * aby) / denom, 0.0, 1.0) + return (a[0] + abx * t, a[1] + aby * t) + + +def _strecke_ends(bbox, insert_xy): + """Die beiden Enden einer GF/VF-Strecke als (Anfang, Ende) in Welt-x/y. + + Enden = Mitten der beiden kurzen Stirnseiten der (achsparallelen) Bounding- + Box entlang ihrer laengeren Ausdehnung. "Anfang" ist das Ende, das dem + Block-Einfuegepunkt (insert_xy = Kettenanfang/startpunkt, siehe + ssg-block-wrap-welt) naeher liegt; "Ende" das andere. + """ + cx, cy = bbox.get("cx", 0.0), bbox.get("cy", 0.0) + dx, dy = bbox.get("dx", 0.0), bbox.get("dy", 0.0) + if dx >= dy: + e0, e1 = (cx - dx / 2.0, cy), (cx + dx / 2.0, cy) + else: + e0, e1 = (cx, cy - dy / 2.0), (cx, cy + dy / 2.0) + ix, iy = insert_xy + if math.hypot(e0[0] - ix, e0[1] - iy) <= math.hypot(e1[0] - ix, e1[1] - iy): + return e0, e1 + return e1, e0 + + +def _distinct_kreisel_ids(nachbarn): + """Eindeutige Kreisel-TeileIds aus einem Nachbarn-String ("0009-L, 0002-R, + 0001-L" -> ["0009", "0002", "0001"]). Das Haelften-Suffix -L/-R (siehe + kreisel_half_bboxes) wird entfernt, Reihenfolge bleibt erhalten.""" + ids = [] + for part in (nachbarn or "").split(","): + p = part.strip() + if not p: + continue + if p.endswith("-L") or p.endswith("-R"): + p = p[:-2] + if p not in ids: + ids.append(p) + return ids + + +def compute_strecke_kreisel_schleus(items, durchmesser_mm=800.0, dbg=None): + """Erzeugt je Gefaellestrecke/Foerderer (GF_VF_TEILEARTEN) an Anfang und + Ende ein Aus- bzw. Einschleuselement an der Kollisionsposition zum + anschliessenden Kreisel. + + Grundlage sind NICHT eigene Abstandsschwellen, sondern die bereits per + Bounding-Box ermittelten Kreisel-Nachbarn der Strecke (Spalte "Nachbarn", + siehe compute_neighbor_ids). Jeder der beiden Streckenenden (Anfang = dem + Block-Einfuegepunkt naeheres Ende, siehe _strecke_ends) wird der unter + diesen Nachbarn naechstgelegene Kreisel zugeordnet: + - Am ANFANG entsteht ein Ausschleuselement (AUSSCHLEUS_TEILEART), + - am ENDE ein Einschleuselement (EINSCHLEUS_TEILEART). + Beruehrt eine Strecke nur EINEN Kreisel (z.B. ein Foerderer, der zum selben + Kreisel zurueckfuehrt), wird dieser beiden Enden zugeordnet. Beruehrt sie + KEINEN Kreisel, entstehen keine Elemente. + + Eine Strecke sollte genau zwei Kreisel-Bounding-Boxen beruehren (ein Anfang + + ein Ende). Beruehrt ihre BBox MEHR als zwei (mit exakten 3D-BBoxen sollte + das nicht vorkommen - hier wird in x/y ohne Hoehe geprueft), entsteht eine + Warnung: die beiden Enden bekommen weiterhin ihren jeweils naechsten + Kreisel, die ueberzaehligen werden ignoriert und in der Warnung genannt. + + Position eines Elements = der Punkt auf dem Kreiselrand in Richtung + Streckenende (Beruehrstelle Kreisel<->Strecke). Die statische Box + (WEICHE_BOX_*) wird - wie bei der Weiche - mit ihrer Laengsseite senkrecht + zur Kreiselachse ausgerichtet (Welt-AABB, siehe _oriented_box_aabb). + + Rueckgabe: (result, warnings) + result = Liste von dicts: + {"teileart": AUSSCHLEUS_/EINSCHLEUS_TEILEART, + "x","y","z": Kollisionsposition (mm), + "dx","dy","dz": Welt-AABB der ausgerichteten Box (mm), + "kreisel_id": TeileId des anschliessenden Kreisels, + "strecke_id": TeileId der GF/VF, + "ende_label": "Anfang" | "Ende"} + warnings = dict {strecke_teileid: warntext} fuer Strecken mit > 2 + Kreisel-BBox-Beruehrungen. + """ + kreisel = {} + for it in items: + if it.get("teileart", "") == KREISEL_SPLIT_TEILEART: + kreisel[it.get("teileid", "")] = _kreisel_capsule(it, durchmesser_mm) + + if dbg: + dbg("") + dbg("=== Strecken-Enden <-> Kreisel -> Ein-/Ausschleuselemente ===") + dbg(f"Echte Kreisel: {len(kreisel)}; Zuordnung ueber die BBox-Nachbarn " + f"der Strecke (naechster Kreisel je Ende), erwartet 2 Kreisel/Strecke") + + result = [] + warnings = {} + for it in items: + if it.get("teileart", "") not in GF_VF_TEILEARTEN: + continue + bbox = it.get("_bbox") + if not bbox: + continue + sid = it.get("teileid", "") + nb_ids = [k for k in _distinct_kreisel_ids(it.get("nachbarn", "")) if k in kreisel] + if not nb_ids: + if dbg: + dbg(f" Strecke {sid}: kein Kreisel-Nachbar -> keine Schleuselemente") + continue + + insert_xy = (it.get("x", bbox.get("cx", 0.0)) or 0.0, + it.get("y", bbox.get("cy", 0.0)) or 0.0) + anfang_end, ende_end = _strecke_ends(bbox, insert_xy) + scz = bbox.get("cz", 0.0) + + def _nearest(end_pt): + best = None + for kid in nb_ids: + cap = kreisel[kid] + p = _closest_pt_on_segment(cap["a"], cap["b"], end_pt) + d = math.hypot(p[0] - end_pt[0], p[1] - end_pt[1]) + if best is None or d < best[3]: + best = (kid, cap, p, d) + return best + + a_best = _nearest(anfang_end) + e_best = _nearest(ende_end) + + if len(nb_ids) > 2: + genutzt = {a_best[0], e_best[0]} + extra = [k for k in nb_ids if k not in genutzt] + warnings[sid] = ( + f"beruehrt {len(nb_ids)} Kreisel-BoundingBoxen " + f"(erwartet 2 = Anfang + Ende): {', '.join(nb_ids)}" + + (f"; ueberzaehlig/ignoriert: {', '.join(extra)}" if extra else "") + + " - mit exakten 3D-BoundingBoxen sollte das nicht auftreten") + if dbg: + dbg(f" Strecke {sid}: WARNUNG - {warnings[sid]}") + + for best, teileart, label, end_pt in ( + (a_best, AUSSCHLEUS_TEILEART, "Anfang", anfang_end), + (e_best, EINSCHLEUS_TEILEART, "Ende", ende_end)): + kid, cap, p, d = best + if d > 1e-6: + ux, uy = (end_pt[0] - p[0]) / d, (end_pt[1] - p[1]) / d + contact = (p[0] + ux * cap["r"], p[1] + uy * cap["r"]) + else: + contact = p + dx, dy, dz = _oriented_box_aabb( + cap["rot"], WEICHE_BOX_LAENGE_MM, WEICHE_BOX_BREITE_MM, WEICHE_BOX_HOEHE_MM) + result.append({ + "teileart": teileart, + "x": contact[0], "y": contact[1], "z": (cap["z"] + scz) / 2.0, + "dx": dx, "dy": dy, "dz": dz, + "kreisel_id": kid, "strecke_id": sid, "ende_label": label, + }) + if dbg: + dbg(f" Strecke {sid} {label}: Kreisel {kid} (Abstand Ende<->Achse " + f"{d:.0f}mm) -> {teileart} bei ({contact[0]:.0f}, {contact[1]:.0f}, " + f"{(cap['z'] + scz) / 2.0:.0f}), Box-AABB ({dx:.0f}, {dy:.0f}, {dz:.0f})") + + if dbg: + dbg(f"Erzeugte Ein-/Ausschleuselemente: {len(result)}; " + f"Strecken mit Warnung (>2 Kreisel): {len(warnings)}") + + return result, warnings