From f6461aab0f47ff851935f1aab7c174ae8c183f98 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Wed, 29 Jul 2026 11:21:16 +0200 Subject: [PATCH] [ADD] Kreisel-Split (links/rechts) bei Kollisionspruefung + Separator/Scanner-Zuordnung - export_neighbors.py: kreisel_half_bboxes() teilt die Kreisel-BBox entlang der Achse durch Antriebs-/Spannstation (AN8/SP8) in eine linke und eine rechte Haelfte (je ca. 400mm breit, siehe [Kreisel] durchmesser_mm in cfg/export.cfg, gespiegelt von component_defaults.json). compute_neighbor_ids ersetzt jeden Kreisel-Eintrag in der Kollisionsgruppe durch dieses Paar (TeileId-L/-R) statt einer einzelnen BBox - Nachbarn einer Gefaellestrecke/ eines Foerderers zeigen jetzt, welche Haelfte tatsaechlich beruehrt. - compute_sensor_zuordnung(): ordnet jeden Separator/Scanner per BBox- Ueberschneidung seiner Gefaellestrecke, seinem Foerderer oder einer Kreiselhaelfte zu (Prioritaet in dieser Reihenfolge), ersetzt die ZUORDNUNG-Attribut-Merkmale im CSV-Export bei Treffer. - compute_scanner_nearest_separator(): sucht zusaetzlich, unabhaengig von der Zuordnung, zu jedem Scanner den raeumlich naechstgelegenen Separator. - ssg_id.lsp: ssg-id-collect-blocks erfasst jetzt auch Separator_SP/S-LP/ Scanner-Bloecke, damit ssg-id-check-all ihr (neues) ID-Attribut vor jedem Export befuellt; export_csv.py bevorzugt dieses echte ID-Attribut als TeileId und faellt nur bei leerem Attribut auf eine laufende SEP-/SCN- Ersatznummer zurueck. - data/ils/Scanner_2D.dwg, Scanner_3D.dwg, Separator_SP_2D.dwg: ID-ATTDEF ergaenzt. Co-Authored-By: Claude Sonnet 5 --- Lisp/ssg_id.lsp | 8 +- cfg/export.cfg | 15 ++ data/ils/Scanner_2D.dwg | Bin 18216 -> 18599 bytes data/ils/Scanner_3D.dwg | Bin 18216 -> 20680 bytes data/ils/Separator_SP_2D.dwg | Bin 21830 -> 23206 bytes lib/export_csv.py | 91 +++++++++--- lib/export_neighbors.py | 273 ++++++++++++++++++++++++++++++++++- 7 files changed, 361 insertions(+), 26 deletions(-) diff --git a/Lisp/ssg_id.lsp b/Lisp/ssg_id.lsp index 28aabdd..0bb29ea 100644 --- a/Lisp/ssg_id.lsp +++ b/Lisp/ssg_id.lsp @@ -28,7 +28,12 @@ ;; --- Alle exportierbaren INSERT-Entities sammeln --- -;; Gleiche Filterlogik wie csv:collect-export-blocks +;; Gleiche Filterlogik wie csv:collect-export-blocks (siehe dort auch +;; pattern_separator/pattern_scanner in cfg/export.cfg - Separator_SP/S-LP/ +;; Scanner sind eigenstaendige Sensor-Bloecke, die inzwischen ein eigenes +;; ID-ATTDEF haben und daher hier ebenfalls erfasst werden muessen, sonst +;; bleibt ihr ID-Attribut trotz ATTDEF für immer leer - ssg-attrib-set-on +;; kann nur einen Wert SETZEN, wenn der Block ueberhaupt erst hier landet). (defun ssg-id-collect-blocks ( / ss-all ss-out i ename ed bname) (setq ss-all (ssget "X" (list (cons 0 "INSERT")))) (if (null ss-all) @@ -43,6 +48,7 @@ (wcmatch bname "KR_*,KREISEL_*,ECKRAD_*") (wcmatch (strcase bname) "AP110*,AP_110*,AP60*,AP_60*,APG110*") (wcmatch bname "Vario*,Staustrecke*,AUS_Element*,EIN_Element*,VF_*,GF_*") + (wcmatch bname "Separator_SP*,S-LP*,Scanner*") (wcmatch bname "#*") ) (ssadd ename ss-out) diff --git a/cfg/export.cfg b/cfg/export.cfg index 2ff5962..fb8e7c0 100644 --- a/cfg/export.cfg +++ b/cfg/export.cfg @@ -115,3 +115,18 @@ debug_log=1 # nicht gesetzt. bb_minimum_height_mm=1 +[Kreisel] +# Kreisel-Durchmesser (Antriebs-/Spannstation AN8/SP8) in mm. Quelle der +# Wahrheit ist data/json/component_defaults.json ("kreisel"."durchmesser", +# Default 800.0) - hier fuer Python gespiegelt (Aenderungen bitte an beiden +# Stellen nachziehen, siehe Hinweis oben bei [blockpattern]). +# +# Wird von lib/export_neighbors.py (kreisel_half_bboxes) benutzt, um die +# Bounding-Box jedes Kreisels bei der Kollisionspruefung in eine linke und +# eine rechte Haelfte zu teilen: AN8 (Antriebsstation) liegt lokal bei +# (radius, 0), SP8 (Spannstation) bei (radius+abstand, 0) - siehe +# KreiselInsert.lsp::draw-module - radius = durchmesser_mm/2 (Default 400mm). +# Die gesamte Geometrie ist symmetrisch um die lokale X-Achse (Y=0, die +# Linie durch beide Stationsmitten) - genau diese Linie trennt links/rechts. +durchmesser_mm=800 + diff --git a/data/ils/Scanner_2D.dwg b/data/ils/Scanner_2D.dwg index a190780c446f59c4adece2a147ef8b0b36125397..47a100d071edabaa750939759c22870253eb3280 100644 GIT binary patch delta 6830 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 delta 6193 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 diff --git a/data/ils/Scanner_3D.dwg b/data/ils/Scanner_3D.dwg index c2afd3f74ab21757a13046f0354adb7894a1a680..8daec6f63017b28db36961a012bf2315ac5a4eb1 100644 GIT binary patch delta 9377 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 Pis-j<#@GJM{~G;2wPly1 delta 6855 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 LiJ$a;{3G=rRag$Q diff --git a/data/ils/Separator_SP_2D.dwg b/data/ils/Separator_SP_2D.dwg index 07041706d557234bdd1553e92d118559698b53ba..dc21e42d3046f3f34be13540f06eb5753de93aae 100644 GIT binary patch delta 12868 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 delta 11444 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 diff --git a/lib/export_csv.py b/lib/export_csv.py index 8efd884..e21b6b6 100644 --- a/lib/export_csv.py +++ b/lib/export_csv.py @@ -38,7 +38,20 @@ Fehler = "unverbunden" (keine Partner, aber mindestens einer noetig), (Mindestanzahl erreicht bzw. TeileArt wird nicht geprueft). Kreisel/Eckrad/ Omniflo-Elemente brauchen mindestens einen Partner, Gefaellestrecke/ Foerderer/Strecke-Modul mindestens zwei (siehe export_neighbors.py -MIN_PARTNER). +MIN_PARTNER). Ein Kreisel wird dabei NICHT als eine, sondern als zwei +BBoxen (links/rechts, geteilt entlang der Achse durch Antriebs-/ +Spannstation, siehe export_neighbors.kreisel_half_bboxes) gegen +Gefaellestrecke/Foerderer getestet - deren "Nachbarn"-Spalte zeigt darum +"-L" bzw. "-R" statt nur der Kreisel-TeileId. +Separator/Scanner-Zeilen bekommen zusaetzlich ein berechnetes "Zuordnung"- +Merkmal (Gefaellestrecke > Foerderer > Kreiselhaelfte, per BBox-Ueber- +schneidung - siehe export_neighbors.compute_sensor_zuordnung), das die +ATTRIB-Zuordnung (falls das Attribut ZUORDNUNG in der Zeichnung von Hand +gesetzt wurde) ersetzt, sofern ein Treffer gefunden wird. Scanner-Zeilen +bekommen zusaetzlich ein "Naechster Separator"-Merkmal (raeumlich naechst- +gelegener Separator, euklidischer Abstand der BBox-Mittelpunkte - eigene, +von der Zuordnung unabhaengige Suche, siehe export_neighbors.compute_ +scanner_nearest_separator). Position, Boundingbox, Planquadrat, Nachbarn und Fehler sind nur fuer EXPORTCSV vorhanden (nicht EXPORTSIVAS) - siehe csv:run-export. Insertpoint/ K1-K4 stehen zwar in export_raw.json fuer jeden Export zur Verfuegung (siehe @@ -55,15 +68,20 @@ from export_planquadrat import load_planquadrat_config, resolve_origins, compute from export_neighbors import ( load_neighbor_tolerance_mm, load_omniflo_cell_size_mm, + load_kreisel_durchmesser_mm, load_collision_debug_target, compute_neighbor_ids, compute_neighbor_errors, + compute_sensor_zuordnung, + compute_scanner_nearest_separator, + SENSOR_TEILEARTEN, ) BLOCKPATTERNS = load_patterns() PLANQUADRAT_CFG = load_planquadrat_config() NEIGHBOR_TOLERANCE_MM = load_neighbor_tolerance_mm() NEIGHBOR_OMNIFLO_CELL_SIZE_MM = load_omniflo_cell_size_mm() +KREISEL_DURCHMESSER_MM = load_kreisel_durchmesser_mm() NEIGHBOR_DEBUG_TARGET = load_collision_debug_target() @@ -191,9 +209,9 @@ def build_gerade_merkmale(block): def build_sensor_merkmale(block): """Merkmale-Dict fuer einen eigenstaendigen Separator_SP/S-LP- oder Scanner-Block (siehe pattern_separator/pattern_scanner in cfg/export.cfg). - Diese Bloecke haben (anders als Kreisel/GF/VF) kein ID-Attribut - - ZUORDNUNG/ARTINR/LAYER sind die einzigen ATTDEFs (siehe ils-insert-sensor/ - mubea:build-separator-one).""" + ZUORDNUNG/ARTINR/LAYER (+ neuerdings ID) sind die ATTDEFs (siehe + ils-insert-sensor/mubea:build-separator-one, ssg-id-collect-blocks in + ssg_id.lsp).""" attribs = block.get("attribs", {}) return { "Zuordnung": attribs.get("ZUORDNUNG", ""), @@ -204,13 +222,17 @@ def build_sensor_merkmale(block): def format_sensor_id(prefix, n): - """Erzeugt eine laufende TeileId fuer Separator/Scanner-Zeilen im CSV-Export. + """Fallback-TeileId fuer Separator/Scanner-Zeilen im CSV-Export, falls das + Block-Attribut "ID" (noch) leer ist. - Diese Bloecke haben KEIN persistiertes ID-Attribut in der Zeichnung - (anders als Kreisel/GF/VF, deren "ID" von ssg-id-generate/ssg-id-check-all - beim Insert bzw. vor jedem Export vergeben wird) - der Praefix macht das - sichtbar: eine rein fuer diesen Exportlauf erzeugte, NICHT ueber mehrere - Exportlaeufe hinweg stabile laufende Nummer, keine echte Block-ID. + Separator_SP/Scanner haben seit kurzem ein eigenes ID-ATTDEF (siehe + ssg-id-collect-blocks in ssg_id.lsp) - normalerweise wird also das echte, + ueber ssg-id-generate/ssg-id-check-all vergebene "ID"-Attribut als + TeileId verwendet, genau wie bei Kreisel/GF/VF. Nur wenn das (noch) + fehlt (z.B. bei aelteren, noch nicht neu exportierten Bloecken), erzeugt + diese Funktion eine rein fuer DIESEN Exportlauf gueltige, NICHT ueber + mehrere Exportlaeufe hinweg stabile Ersatznummer - der Praefix macht + sichtbar, dass es keine echte, persistierte Block-ID ist. """ return f"{prefix}-{n:04d}" @@ -489,6 +511,15 @@ def process_blocks(blocks, lookup, dbg=None): "bezeichnung": f"Kreisel :{kreisel_count}", "planquadrat": planquadrat_column(block, planquadrat_cfg), "merkmale": build_kreisel_merkmale(block), + # x/y/rotation/attribs (roh, nicht nur die Merkmale) werden + # von export_neighbors.kreisel_half_bboxes gebraucht, um die + # Links/Rechts-Trennung ueber AN8/SP8/ABSTAND nachzurechnen - + # bbox_columns() liefert nur die fertige Gesamt-BBox, keine + # Rohdaten dafuer. + "x": block.get("x", 0.0), + "y": block.get("y", 0.0), + "rotation": block.get("rotation", 0.0), + "attribs": block.get("attribs", {}), **bbox_columns(block), }) continue @@ -508,18 +539,23 @@ def process_blocks(blocks, lookup, dbg=None): }) continue - # Separator (Separator_SP/S-LP) - eigene TeileArt-Zeile mit laufender - # ID (siehe format_sensor_id; kein echtes ID-Attribut am Block, anders - # als Kreisel/GF/VF). EXPORTSIVAS schliesst denselben Blockpattern - # bewusst aus (siehe export_sivas.py) - dort zaehlt bereits - # ANZAHL_SEPARATOR am zugehoerigen Kreisel/GF/VF. + # Separator (Separator_SP/S-LP) - eigene TeileArt-Zeile. TeileId: das + # echte ID-Attribut, falls vorhanden (Separator_SP/Scanner haben seit + # kurzem ein eigenes ID-ATTDEF, siehe ssg-id-collect-blocks in + # ssg_id.lsp - ssg-id-check-all fuellt es vor jedem Export). Fallback + # format_sensor_id (laufende SEP-/SCN-Nummer NUR fuer diesen Export- + # lauf) fuer aeltere Bloecke, deren ID-Attribut noch leer ist (z.B. + # vor dem naechsten Insert/ssg-id-check-all-Durchlauf). + # EXPORTSIVAS schliesst denselben Blockpattern bewusst aus (siehe + # export_sivas.py) - dort zaehlt bereits ANZAHL_SEPARATOR am + # zugehoerigen Kreisel/GF/VF. if matches_any(bname, BLOCKPATTERNS.get("pattern_separator", [])): elem_nr += 1 separator_count += 1 items.append({ "nr": elem_nr, "teileart": "ILS 2.0 Separator", - "teileid": format_sensor_id("SEP", separator_count), + "teileid": block.get("attribs", {}).get("ID") or format_sensor_id("SEP", separator_count), "bezeichnung": f"Separator :{separator_count}", "planquadrat": planquadrat_column(block, planquadrat_cfg), "merkmale": build_sensor_merkmale(block), @@ -534,7 +570,7 @@ def process_blocks(blocks, lookup, dbg=None): items.append({ "nr": elem_nr, "teileart": "ILS 2.0 Scanner", - "teileid": format_sensor_id("SCN", scanner_count), + "teileid": block.get("attribs", {}).get("ID") or format_sensor_id("SCN", scanner_count), "bezeichnung": f"Scanner :{scanner_count}", "planquadrat": planquadrat_column(block, planquadrat_cfg), "merkmale": build_sensor_merkmale(block), @@ -563,12 +599,31 @@ def process_blocks(blocks, lookup, dbg=None): }) neighbor_ids = compute_neighbor_ids( - items, NEIGHBOR_TOLERANCE_MM, NEIGHBOR_OMNIFLO_CELL_SIZE_MM, dbg=dbg) + items, NEIGHBOR_TOLERANCE_MM, NEIGHBOR_OMNIFLO_CELL_SIZE_MM, + kreisel_durchmesser_mm=KREISEL_DURCHMESSER_MM, dbg=dbg) neighbor_errors = compute_neighbor_errors(items, neighbor_ids) for item, nachbarn, fehler in zip(items, neighbor_ids, neighbor_errors): item["nachbarn"] = nachbarn item["fehler"] = fehler + # Separator/Scanner: Zuordnung zu Gefaellestrecke/Foerderer/Kreiselhaelfte + # per BBox-Ueberschneidung berechnen (Prioritaet siehe compute_sensor_ + # zuordnung) - ersetzt die von Hand gesetzte ZUORDNUNG-Attribut-Merkmale + # nur, wenn tatsaechlich ein Treffer gefunden wurde. + sensor_zuordnung = compute_sensor_zuordnung( + items, NEIGHBOR_TOLERANCE_MM, kreisel_durchmesser_mm=KREISEL_DURCHMESSER_MM, dbg=dbg) + for item, zuordnung in zip(items, sensor_zuordnung): + if item.get("teileart", "") in SENSOR_TEILEARTEN and zuordnung: + item["merkmale"]["Zuordnung"] = zuordnung + + # NACH der Separator-Zuordnung: zu jedem Scanner den raeumlich naechst- + # gelegenen Separator suchen (eigene, unabhaengige Suche - siehe + # compute_scanner_nearest_separator). + nearest_separator = compute_scanner_nearest_separator(items, dbg=dbg) + for item, separator_id in zip(items, nearest_separator): + if item.get("teileart", "") == "ILS 2.0 Scanner" and separator_id: + item["merkmale"]["Naechster Separator"] = separator_id + return items diff --git a/lib/export_neighbors.py b/lib/export_neighbors.py index e442182..1d42d2b 100644 --- a/lib/export_neighbors.py +++ b/lib/export_neighbors.py @@ -33,13 +33,20 @@ MIN_PARTNER) - Ergebnis ist die CSV-Spalte "Fehler" in export_csv.py. Nur von export_csv.py genutzt (EXPORTCSV), nicht von export_sivas.py. """ +import math import os -from export_blockpatterns import cfg_path_from_env, load_export_cfg +from export_blockpatterns import cfg_path_from_env, load_export_cfg, safe_float KREISEL_TEILEARTEN = {"ILS 2.0 Kreisel", "ILS 2.0 Eckrad"} STRECKEN_TEILEARTEN = {"ILS 2.0 Gefaellestrecke", "ILS 2.0 Strecke", "ILS 2.0 Strecke - Modul"} OMNIFLO_TEILEARTEN = {"Omniflo Kurve", "Omniflo Weiche", "Omniflo Gerade"} +SENSOR_TEILEARTEN = {"ILS 2.0 Separator", "ILS 2.0 Scanner"} + +# Nur "ILS 2.0 Kreisel" (echte Kreisel-Bloecke mit AN8/Antrieb + SP8/Spann- +# station + ABSTAND-Attribut) werden links/rechts gesplittet - "ILS 2.0 +# Eckrad" hat keine dieser zwei Stationen und bleibt eine einzelne BBox. +KREISEL_SPLIT_TEILEART = "ILS 2.0 Kreisel" # Mindestanzahl Partner (Nachbarn) je TeileArt fuer die Fehlerspalte "Fehler" # (siehe compute_neighbor_errors): Kreisel/Eckrad und Omniflo-Elemente @@ -74,6 +81,16 @@ def load_omniflo_cell_size_mm(cfg_path=None): return parser.getfloat("Nachbarschaft", "omniflo_zellgroesse_mm", fallback=3000.0) +def load_kreisel_durchmesser_mm(cfg_path=None): + """Liest den Kreisel-Durchmesser (mm) aus export.cfg. Default: 800.0 + (siehe [Kreisel] durchmesser_mm - gespiegelter Wert von + data/json/component_defaults.json "kreisel"."durchmesser").""" + if cfg_path is None: + cfg_path = cfg_path_from_env() + parser = load_export_cfg(cfg_path) + return parser.getfloat("Kreisel", "durchmesser_mm", fallback=800.0) + + def load_collision_debug_target(cfg_path=None): """Zielpfad fuer das Kollisions-/Nachbarschafts-Debuglog aus export.cfg. @@ -120,6 +137,73 @@ def _add_neighbor(result, idx_a, idx_b, id_a, id_b): result[idx_b].append(id_a) +def kreisel_half_bboxes(block, durchmesser_mm): + """Teilt die Welt-Bounding-Box eines "ILS 2.0 Kreisel"-Blocks entlang der + Kreiselachse (Linie durch die Mitten von Antriebs- (AN8) und Spann- + station (SP8)) in eine linke und eine rechte Haelfte. + + Geometrie (siehe KreiselInsert.lsp::draw-module): AN8 liegt in lokalen + (unrotierten) Blockkoordinaten bei (radius, 0), SP8 bei + (radius+abstand, 0) - radius = durchmesser_mm/2 (Default 400mm, siehe + [Kreisel] in cfg/export.cfg). Beide Stationen liegen exakt auf der + lokalen X-Achse (Y=0); die gesamte Geometrie (zwei Kreise + zwei + Tangenten bei Y=+-radius) ist symmetrisch dazu. Die Trennlinie durch die + Stationsmitten IST also die lokale X-Achse selbst, und teilt die + Bounding-Box in zwei GLEICH GROSSE Haelften mit je "radius" (Default + 400mm) Breite - "links" = lokal Y in [0, +radius], "rechts" = lokal Y in + [-radius, 0] (Blickrichtung von Antriebs- zu Spannstation, +X; mathe- + matische Standardkonvention: mathematisch positive/CCW-Drehung von +X + fuehrt nach +Y, also links). + + Block-Insertpunkt (world x,y) und -rotation (world "rotation", Grad, + CCW positiv) kommen direkt aus block (siehe csv:block-to-json). Die + ABSTAND-Attribut wird fuer die lokale Laenge (0..abstand+durchmesser) + benoetigt. + + Rueckgabe: (bbox_links, bbox_rechts), je ein dict {"cx","cy","dx","dy"} - + die Welt-AABB der jeweils rotierten Haelfte (bei Rotationen, die kein + Vielfaches von 90 Grad sind, umschliesst die AABB die tatsaechliche + Haelfte etwas grosszuegiger, wie jede achsparallele Bounding-Box einer + gedrehten Flaeche - konsistent mit jeder anderen Bounding-Box in diesem + Modul). + """ + attribs = block.get("attribs", {}) + abstand_mm = safe_float(attribs.get("ABSTAND", "0"), 0.0) + radius_mm = durchmesser_mm / 2.0 + x0 = block.get("x", 0.0) or 0.0 + y0 = block.get("y", 0.0) or 0.0 + rot_rad = math.radians(block.get("rotation", 0.0) or 0.0) + cos_r, sin_r = math.cos(rot_rad), math.sin(rot_rad) + + def world(local_x, local_y): + return ( + x0 + local_x * cos_r - local_y * sin_r, + y0 + local_x * sin_r + local_y * cos_r, + ) + + # AN8-Kreis reicht lokal bis X=0 (Mitte radius, Radius radius); SP8-Kreis + # bis X=abstand+durchmesser (Mitte radius+abstand, Radius radius). + local_x_max = abstand_mm + durchmesser_mm + + def half_bbox(y_lo, y_hi): + corners = [ + world(0.0, y_lo), world(local_x_max, y_lo), + world(local_x_max, y_hi), world(0.0, y_hi), + ] + xs = [c[0] for c in corners] + ys = [c[1] for c in corners] + minx, maxx = min(xs), max(xs) + miny, maxy = min(ys), max(ys) + return { + "cx": (minx + maxx) / 2.0, "cy": (miny + maxy) / 2.0, + "dx": maxx - minx, "dy": maxy - miny, + } + + bbox_links = half_bbox(0.0, radius_mm) + bbox_rechts = half_bbox(-radius_mm, 0.0) + return bbox_links, bbox_rechts + + def _fmt_bounds(b): """(minx, maxx, miny, maxy) kompakt fuer das Debuglog.""" return f"[x {b[0]:.0f}..{b[1]:.0f} | y {b[2]:.0f}..{b[3]:.0f}]" @@ -132,12 +216,21 @@ def _dbg_pair(dbg, id_a, id_b, overlap): def _test_group_pairs(group, result, dbg=None): """Alle Paare EINER Gruppe gegeneinander testen (O(n^2) - fuer Kreisel/ - Eckrad-Stueckzahlen pro Zeichnung unkritisch).""" + Eckrad-Stueckzahlen pro Zeichnung unkritisch). + + idx_a == idx_b wird uebersprungen: seit dem Kreisel-Links/Rechts-Split + (siehe kreisel_half_bboxes) kann dieselbe Original-idx zweimal in der + Gruppe auftauchen (die linke und die rechte Haelfte DESSELBEN Kreisels) - + die beiden beruehren sich zwangslaeufig an der Trennlinie und waeren + sonst faelschlich gegenseitige "Nachbarn". + """ n = len(group) for a in range(n): idx_a, bounds_a, id_a = group[a] for b in range(a + 1, n): idx_b, bounds_b, id_b = group[b] + if idx_a == idx_b: + continue overlap = _overlaps(bounds_a, bounds_b) _dbg_pair(dbg, id_a, id_b, overlap) if overlap: @@ -195,7 +288,8 @@ def _test_omniflo_grid(group, cell_size_mm, result, dbg=None): _add_neighbor(result, idx_a, idx_b, id_a, id_b) -def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, dbg=None): +def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, + kreisel_durchmesser_mm=800.0, dbg=None): """Ermittelt je Item die IDs (item["teileid"]) benachbarter Elemente. items = Liste von dict mit "teileart", "teileid" und optional "_bbox" @@ -203,12 +297,23 @@ def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, dbg=N ohne _bbox (z.B. die synthetische Omniflo-Sum-Zeile) bleiben ohne Nachbarn. + "ILS 2.0 Kreisel"-Items werden dabei NICHT als eine BBox, sondern als + ZWEI gegeneinander getestet: eine linke und eine rechte Haelfte (siehe + kreisel_half_bboxes) - die Kreisel-Gruppe fuer die Kollisionspruefung + besteht also aus -L/-R statt je Kreisel + ("ILS 2.0 Eckrad" bleibt unveraendert eine einzelne BBox). Ein + angrenzendes Element (z.B. eine Gefaellestrecke) bekommt dadurch in + seiner eigenen Nachbarliste "-L" oder "-R" statt nur + "" - je nachdem, welche Haelfte tatsaechlich ueberschneidet. + dbg = optionale Callable(str) fuer ein Debugprotokoll (Klassifikation, Gruppen, jeder Ueberschneidungstest, Ergebnis). None = kein Protokoll. Rueckgabe: Liste von kommaseparierten Nachbar-ID-Strings, positionsgleich - zu items (nicht ueber die ID dedupliziert, da TeileId nicht zwingend - eindeutig ist). + zu items. Innerhalb eines Items dedupliziert (identische IDs entfernt - + das passiert z.B., wenn ein Kreisel dieselbe Strecke ueber BEIDE + Haelften beruehrt), sonst nicht global ueber alle Items hinweg (TeileId + ist nicht zwingend eindeutig). """ half_tol = tolerance_mm / 2.0 result = [[] for _ in items] @@ -221,7 +326,8 @@ def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, dbg=N if dbg: dbg("=== Nachbarschafts-/Kollisionserkennung ===") dbg(f"Toleranz={tolerance_mm}mm (Bounding-Box je Seite +{half_tol}mm erweitert), " - f"Omniflo-Zellgroesse={omniflo_cell_size_mm}mm") + f"Omniflo-Zellgroesse={omniflo_cell_size_mm}mm, " + f"Kreisel-Durchmesser={kreisel_durchmesser_mm}mm (Radius je Haelfte)") dbg(f"Elemente gesamt: {len(items)}") dbg("") dbg("Klassifikation je Element:") @@ -237,6 +343,20 @@ def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, dbg=N continue teileart = item.get("teileart", "") teileid = item.get("teileid", "") + if teileart == KREISEL_SPLIT_TEILEART: + gruppe = "Kreisel (links/rechts gesplittet)" + bbox_links, bbox_rechts = kreisel_half_bboxes(item, kreisel_durchmesser_mm) + bounds_links = _bounds(bbox_links, half_tol) + bounds_rechts = _bounds(bbox_rechts, half_tol) + kreisel_group.append((idx, bounds_links, f"{teileid}-L")) + kreisel_group.append((idx, bounds_rechts, f"{teileid}-R")) + if dbg: + dbg(f" idx={idx} teileid={teileid!r} teileart={teileart!r} " + f"-> {gruppe}; links bbox(cx={bbox_links['cx']:.0f},cy={bbox_links['cy']:.0f}," + f"dx={bbox_links['dx']:.0f},dy={bbox_links['dy']:.0f}) bounds={_fmt_bounds(bounds_links)}; " + f"rechts bbox(cx={bbox_rechts['cx']:.0f},cy={bbox_rechts['cy']:.0f}," + f"dx={bbox_rechts['dx']:.0f},dy={bbox_rechts['dy']:.0f}) bounds={_fmt_bounds(bounds_rechts)}") + continue bounds = _bounds(bbox, half_tol) if teileart in KREISEL_TEILEARTEN: gruppe = "Kreisel/Eckrad" @@ -276,7 +396,13 @@ def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, dbg=N # 3. Omniflo gegen Omniflo, rasterbasiert vorgefiltert _test_omniflo_grid(omniflo_group, omniflo_cell_size_mm, result, dbg) - neighbor_id_lists = [", ".join(neighbor_ids) for neighbor_ids in result] + # dict.fromkeys() dedupliziert unter Erhalt der Reihenfolge - relevant + # seit dem Kreisel-Split: beruehrt eine Strecke BEIDE Haelften desselben + # Kreisels nicht (normalerweise ausgeschlossen), waere sie sonst nicht + # betroffen; umgekehrt kann derselbe Strecken-Nachbar im Kreisel-Eintrag + # theoretisch doppelt auftauchen, wenn (Rand-/Toleranzfall) beide + # Haelften ihn beruehren. + neighbor_id_lists = [", ".join(dict.fromkeys(neighbor_ids)) for neighbor_ids in result] if dbg: dbg("") @@ -316,3 +442,136 @@ def compute_neighbor_errors(items, neighbor_ids): else: errors.append("") return errors + + +def compute_sensor_zuordnung(items, tolerance_mm, kreisel_durchmesser_mm=800.0, dbg=None): + """Ordnet jeden Separator/Scanner (SENSOR_TEILEARTEN) per Bounding-Box- + Ueberschneidung seiner Gefaellestrecke, seinem Foerderer oder einer + Kreiselhaelfte (links/rechts, siehe kreisel_half_bboxes) zu. + + Eigener, unabhaengiger Durchlauf NACH compute_neighbor_ids (nicht in + dessen Gruppenaufbau integriert, da Separatoren/Scanner dort bewusst + keine eigene Gruppe sind - Nachbarschaft und Zuordnung sind zwei + unterschiedliche Fragen: Nachbarschaft = "beruehrt sich", Zuordnung = + "gehoert zu genau einem Trägerelement"). Prioritaet bei mehreren + Treffern: Gefaellestrecke vor Foerderer vor Kreiselhaelfte (ein + Separator/Scanner haengt fachlich an genau einem dieser drei). + + items = dieselbe Liste wie bei compute_neighbor_ids (braucht "teileart", + "teileid" und "_bbox"). + + Rueckgabe: Liste von Zuordnungs-Strings, positionsgleich zu items - + "" fuer Items, die keine SENSOR_TEILEART sind oder keine BBox haben. + Bei Treffer: die TeileId der Gefaellestrecke/des Foerderers, oder + "-L"/"-R" bei einem Kreisel-Treffer. Kein Treffer bei + vorhandener BBox: "nicht zugeordnet". + """ + half_tol = tolerance_mm / 2.0 + + gf_entries = [] + vf_entries = [] + kreisel_entries = [] + + for item in items: + bbox = item.get("_bbox") + if not bbox: + continue + teileart = item.get("teileart", "") + teileid = item.get("teileid", "") + if teileart == "ILS 2.0 Gefaellestrecke": + gf_entries.append((teileid, _bounds(bbox, half_tol))) + elif teileart in ("ILS 2.0 Strecke", "ILS 2.0 Strecke - Modul"): + vf_entries.append((teileid, _bounds(bbox, half_tol))) + elif teileart == KREISEL_SPLIT_TEILEART: + bbox_links, bbox_rechts = kreisel_half_bboxes(item, kreisel_durchmesser_mm) + kreisel_entries.append((f"{teileid}-L", _bounds(bbox_links, half_tol))) + kreisel_entries.append((f"{teileid}-R", _bounds(bbox_rechts, half_tol))) + + if dbg: + dbg("") + dbg("=== Separator/Scanner-Zuordnung (GF > Foerderer > Kreiselhaelfte) ===") + dbg(f"Kandidaten: {len(gf_entries)} Gefaellestrecke(n), " + f"{len(vf_entries)} Foerderer/Streckenmodul(e), " + f"{len(kreisel_entries)} Kreiselhaelfte(n)") + + def _find(bounds, candidates): + for teileid, other_bounds in candidates: + if _overlaps(bounds, other_bounds): + return teileid + return None + + result = [] + for item in items: + teileart = item.get("teileart", "") + if teileart not in SENSOR_TEILEARTEN: + result.append("") + continue + bbox = item.get("_bbox") + if not bbox: + result.append("") + continue + bounds = _bounds(bbox, half_tol) + treffer = (_find(bounds, gf_entries) + or _find(bounds, vf_entries) + or _find(bounds, kreisel_entries) + or "nicht zugeordnet") + if dbg: + dbg(f" {item.get('teileart', '')} {item.get('teileid', '')!r} " + f"bounds={_fmt_bounds(bounds)} -> {treffer!r}") + result.append(treffer) + return result + + +def compute_scanner_nearest_separator(items, dbg=None): + """Sucht fuer jeden Scanner ("ILS 2.0 Scanner") den raeumlich naechst- + gelegenen Separator ("ILS 2.0 Separator") - euklidischer Abstand der + BBox-Mittelpunkte (cx/cy, Grundriss, Z ignoriert wie ueberall in diesem + Modul). + + Eigener, unabhaengiger Durchlauf, der NACH compute_sensor_zuordnung + aufgerufen wird (die Zuordnung der Separatoren zu ihrer Gefaellestrecke/ + ihrem Foerderer/ihrer Kreiselhaelfte steht damit bereits fest - diese + Funktion aendert daran nichts, sie sucht unabhaengig davon zusaetzlich + den naechsten Separator zu jedem Scanner). Es gibt KEINE Abstands- + obergrenze - "immer der naechstgelegene" bedeutet auch bei grosser + Entfernung einen Treffer, solange ueberhaupt ein Separator existiert. + + Rueckgabe: Liste von TeileId-Strings (des jeweils naechsten Separators), + positionsgleich zu items - "" fuer Nicht-Scanner-Items, fuer Scanner ohne + BBox oder wenn ueberhaupt kein Separator mit BBox in der Zeichnung ist. + """ + separators = [ + (item.get("teileid", ""), item["_bbox"]) + for item in items + if item.get("teileart", "") == "ILS 2.0 Separator" and item.get("_bbox") + ] + + if dbg: + dbg("") + dbg("=== Scanner -> naechstgelegener Separator ===") + dbg(f"Separator-Kandidaten mit BBox: {len(separators)}") + + result = [] + for item in items: + if item.get("teileart", "") != "ILS 2.0 Scanner": + result.append("") + continue + bbox = item.get("_bbox") + if not bbox or not separators: + result.append("") + if dbg: + dbg(f" Scanner {item.get('teileid', '')!r}: " + f"{'keine BBox' if not bbox else 'keine Separatoren vorhanden'} -> kein Treffer") + continue + cx, cy = bbox["cx"], bbox["cy"] + best_id, best_dist = "", None + for sep_id, sep_bbox in separators: + dist = math.hypot(sep_bbox["cx"] - cx, sep_bbox["cy"] - cy) + if best_dist is None or dist < best_dist: + best_dist = dist + best_id = sep_id + if dbg: + dbg(f" Scanner {item.get('teileid', '')!r} -> naechster Separator " + f"{best_id!r} (Abstand {best_dist:.0f}mm)") + result.append(best_id) + return result