diff --git a/Lisp/export.lsp b/Lisp/export.lsp index fd59c30..9af5963 100644 --- a/Lisp/export.lsp +++ b/Lisp/export.lsp @@ -381,6 +381,52 @@ ) ) +;; --- K1 (AS/KS_EIN) / K2 (ES/KS_AUS) eines VF_n/GF_n-Wrappers ableiten --- +;; ename = Entity-Name des platzierten VF_n/GF_n-INSERT. +;; pt/rot-rad = Welt-Einfuegepunkt/-Rotation (radiant) des Wrappers (siehe +;; csv:block-to-json). +;; VF/GF-Wrapper fuehren - anders als Omniflo-Boegen - KEINE direkten K1-K4- +;; oder KS-Sub-INSERTs; ihre Streckenenden stecken je EINE Ebene tiefer: +;; KS_EIN im AS_Element*, KS_AUS im ES_Element* (siehe check_hoehen.lsp, +;; extract-ks-from-block-raw). ssg-collect-nested-inserts findet diese +;; verschachtelten INSERTs mit ihrer Platzierung RELATIV zur Wrapper- +;; Blockdefinition (ename/(x y z)/... - der zweite Eintrag ist der Ursprung +;; des KS-INSERTs, also die AS-/ES-Position). Der lokale Punkt wird ueber die +;; Wrapper-Weltlage (reine Z-Drehung) in Weltkoordinaten gedreht und wie K1-K4 +;; als reiner Positions-String (csv:trans-encode) exportiert. Damit hat auch +;; eine Strecke K1 (AS) und K2 (ES) fuer die Python-Kollisionspruefung +;; (export_neighbors.py: Schleuselemente + Kreisel-Kollision ueber die AS/ES- +;; Position mit fester BBox statt der ganzen Wrapper-Box). +;; Rueckgabe wie csv:get-k-kos-strings: (("K1" . str) ("K2" . str)) - nur die +;; tatsaechlich gefundenen Enden. +(defun csv:vfgf-k-kos-strings (ename pt rot-rad / ed bname as-hits es-hits result + loc world) + (setq ed (entget ename)) + (setq bname (cdr (assoc 2 ed))) + (setq result nil) + ;; KS-INSERTs (KS_EIN/KS_AUS bzw. KSYS_-Varianten) direkt im AS_Element/ + ;; ES_Element suchen. wcmatch ist Gross/Klein-unempfindlich fuer Blocknamen. + (setq as-hits (ssg-collect-nested-inserts bname "KS_EIN,KSYS_EIN")) + (setq es-hits (ssg-collect-nested-inserts bname "KS_AUS,KSYS_AUS")) + (if as-hits + (progn + (setq loc (cadr (car as-hits))) + (setq world (csv:local-to-world pt rot-rad loc)) + (setq result (cons (cons "K1" + (csv:trans-encode (car world) (cadr world) (caddr world))) result)) + ) + ) + (if es-hits + (progn + (setq loc (cadr (car es-hits))) + (setq world (csv:local-to-world pt rot-rad loc)) + (setq result (cons (cons "K2" + (csv:trans-encode (car world) (cadr world) (caddr world))) result)) + ) + ) + result +) + ;; --- Mindesthoehe der Bounding-Box (Z-Ausdehnung), aus cfg/export.cfg --- ;; [Boundingbox] bb_minimum_height_mm, Default 1 (mm) falls nicht gesetzt. @@ -487,6 +533,14 @@ (export:pattern "pattern_gerade" "AP110*,AP_110*"))) (setq k-list (csv:gerade-k-kos-strings ename pt rotation)) ) + ;; VF_n/GF_n-Wrapper: K1 (AS/KS_EIN) und K2 (ES/KS_AUS) eine Ebene tiefer + ;; aus AS_Element/ES_Element ableiten (siehe csv:vfgf-k-kos-strings). Damit + ;; die Python-Kollisionspruefung Kreisel<->Strecke ueber die AS/ES-Position + ;; mit fester BBox statt der ganzen Wrapper-Box arbeiten kann. + (if (and (null k-list) + (wcmatch (strcase blk-name) "VF_*,GF_*")) + (setq k-list (csv:vfgf-k-kos-strings ename pt rotation)) + ) (setq bbox-json "") (if include-bbox (progn diff --git a/cfg/export.cfg b/cfg/export.cfg index 99f3737..f9c6e66 100644 --- a/cfg/export.cfg +++ b/cfg/export.cfg @@ -142,6 +142,21 @@ debug_log=1 # nicht gesetzt. bb_minimum_height_mm=1 +# Feste Bounding-Box-Masse (mm) der synthetischen, beim Export erzeugten +# Elemente OHNE eigene Zeichnungsgeometrie: der ILS-Weichen an Kreisel- +# Beruehrpunkten sowie der ILS-Ein-/Ausschleuselemente an den GF/VF-Strecken- +# enden (lib/export_neighbors.py: compute_kreisel_touch_switches / +# compute_strecke_kreisel_schleus). Dieselben Masse dienen als feste +# Kollisionsbox der AS/ES-Elemente bei der Nachbarschaftspruefung Kreisel<-> +# GF/VF (compute_neighbor_ids, statt der grossen Wrapper-Box). +# LAENGE liegt QUER zur Foerder-/Kreiselachse, BREITE laengs (siehe +# _oriented_box_aabb). Der Halbe-Box-Versatz der Schleuselemente gegenueber +# dem AS/ES-Koordinatensystem betraegt BREITE/2 entlang der Foerderachse. +# Defaults (falls nicht gesetzt): 250 / 100 / 30. +element_box_laenge_mm=250 +element_box_breite_mm=100 +element_box_hoehe_mm=30 + [Kreisel] # Kreisel-Durchmesser (Antriebs-/Spannstation AN8/SP8) in mm. Quelle der # Wahrheit ist data/json/component_defaults.json ("kreisel"."durchmesser", diff --git a/lib/export_csv.py b/lib/export_csv.py index d756c62..5d8ede2 100644 --- a/lib/export_csv.py +++ b/lib/export_csv.py @@ -11,7 +11,8 @@ Nach der Kollisions-/Nachbarschaftserkennung wird ausserdem an jedem Beruehrpunkt zweier Kreisel eine "ILS Weiche"-Zeile angehaengt (siehe build_kreisel_weiche_items / export_neighbors.compute_kreisel_touch_switches): Anzahl 1, Position = Beruehrkoordinaten, Nachbarn = die beiden Kreisel-IDs, -K1-K4/Insertpoint leer, Boundingbox = statische Default-Box (WEICHE_BOX_*), +K1-K4/Insertpoint leer, Boundingbox = feste Element-Box (element_box_mm, +cfg/export.cfg [Boundingbox]), mit der Laengsseite senkrecht zur Achse des beruehrenden Kreisels; die TeileId zaehlt aus der hoechsten bereits vergebenen numerischen ID hoch. @@ -113,6 +114,7 @@ from export_neighbors import ( load_omniflo_cell_size_mm, load_omniflo_ks_tolerance_mm, load_kreisel_durchmesser_mm, + load_element_box_mm, load_collision_debug_target, compute_neighbor_ids, compute_neighbor_errors, @@ -131,6 +133,7 @@ NEIGHBOR_TOLERANCE_MM = load_neighbor_tolerance_mm() NEIGHBOR_OMNIFLO_CELL_SIZE_MM = load_omniflo_cell_size_mm() NEIGHBOR_OMNIFLO_KS_TOLERANCE_MM = load_omniflo_ks_tolerance_mm() KREISEL_DURCHMESSER_MM = load_kreisel_durchmesser_mm() +ELEMENT_BOX_MM = load_element_box_mm() NEIGHBOR_DEBUG_TARGET = load_collision_debug_target() @@ -460,12 +463,14 @@ def build_kreisel_weiche_items(items, planquadrat_cfg, start_elem_nr, dbg=None): (max_numeric_teileid), 4-stellig aufgefuellt wie die Kreisel-IDs. Anzahl ist wie bei allen Zeilen 1; Nachbarn = die beiden Kreisel-IDs; K1-K4 und Insertpoint bleiben leer; Position = Beruehrkoordinaten; Boundingbox = die - statische, an der Kreiselachse ausgerichtete Default-Box (WEICHE_BOX_*). + feste, an der Kreiselachse ausgerichtete Element-Box (element_box_mm, + cfg/export.cfg [Boundingbox]). Rueckgabe: fortgeschriebene Elementnummer (start_elem_nr + Anzahl Weichen). """ switches = compute_kreisel_touch_switches( - items, NEIGHBOR_TOLERANCE_MM, durchmesser_mm=KREISEL_DURCHMESSER_MM, dbg=dbg) + items, NEIGHBOR_TOLERANCE_MM, durchmesser_mm=KREISEL_DURCHMESSER_MM, + element_box_mm=ELEMENT_BOX_MM, dbg=dbg) naechste_id = max_numeric_teileid(items) elem_nr = start_elem_nr @@ -512,7 +517,8 @@ def build_strecke_schleus_items(items, planquadrat_cfg, start_elem_nr, dbg=None) Rueckgabe: fortgeschriebene Elementnummer. """ schleus, warnings = compute_strecke_kreisel_schleus( - items, durchmesser_mm=KREISEL_DURCHMESSER_MM, dbg=dbg) + items, durchmesser_mm=KREISEL_DURCHMESSER_MM, + element_box_mm=ELEMENT_BOX_MM, dbg=dbg) # Warnungen (>2 Kreisel-BBox-Beruehrungen je Strecke) auf die Strecken-Zeile # schreiben (Spalte "Warnungen") und in die Konsole ausgeben. @@ -815,7 +821,8 @@ def process_blocks(blocks, lookup, dbg=None): neighbor_ids = compute_neighbor_ids( items, NEIGHBOR_TOLERANCE_MM, NEIGHBOR_OMNIFLO_CELL_SIZE_MM, kreisel_durchmesser_mm=KREISEL_DURCHMESSER_MM, - omniflo_ks_tolerance_mm=NEIGHBOR_OMNIFLO_KS_TOLERANCE_MM, dbg=dbg) + omniflo_ks_tolerance_mm=NEIGHBOR_OMNIFLO_KS_TOLERANCE_MM, + element_box_mm=ELEMENT_BOX_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 diff --git a/lib/export_neighbors.py b/lib/export_neighbors.py index 88987fd..b91e378 100644 --- a/lib/export_neighbors.py +++ b/lib/export_neighbors.py @@ -94,15 +94,33 @@ EINSCHLEUS_TEILEART = "ILS Einschleuselement" # 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 -# bildet; ausgegeben wird die achsparallele Welt-Bounding-Box dieser -# gedrehten Box (siehe _oriented_box_aabb). +# Fallback-Masse (mm) der festen Element-Bounding-Box, falls cfg/export.cfg +# [Boundingbox] element_box_* nicht gesetzt/lesbar ist (siehe +# load_element_box_mm). NICHT direkt verwenden - die Rechenfunktionen bekommen +# die Masse als Parameter (Default = load_element_box_mm()), damit die cfg-Werte +# greifen. Laengsseite (LAENGE) quer zur Foerder-/Kreiselachse, BREITE laengs +# (siehe _oriented_box_aabb). WEICHE_BOX_LAENGE_MM = 250.0 WEICHE_BOX_BREITE_MM = 100.0 WEICHE_BOX_HOEHE_MM = 30.0 + +def load_element_box_mm(cfg_path=None): + """Feste Bounding-Box-Masse (Laenge, Breite, Hoehe in mm) der synthetischen + Export-Elemente (ILS-Weichen an Kreisel-Beruehrpunkten, ILS-Ein-/Aus- + schleuselemente an GF/VF-Enden) sowie der AS/ES-Kollisionsbox aus + cfg/export.cfg [Boundingbox] element_box_laenge_mm / _breite_mm / _hoehe_mm. + Fallback auf WEICHE_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", "element_box_laenge_mm", fallback=WEICHE_BOX_LAENGE_MM), + parser.getfloat("Boundingbox", "element_box_breite_mm", fallback=WEICHE_BOX_BREITE_MM), + parser.getfloat("Boundingbox", "element_box_hoehe_mm", fallback=WEICHE_BOX_HOEHE_MM), + ) + # 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 @@ -470,7 +488,8 @@ def _test_omniflo(group, cell_size_mm, ks_tol_mm, result, dbg=None): def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, kreisel_durchmesser_mm=800.0, - omniflo_ks_tolerance_mm=10.0, dbg=None): + omniflo_ks_tolerance_mm=10.0, element_box_mm=None, + dbg=None): """Ermittelt je Item die IDs (item["teileid"]) benachbarter Elemente. items = Liste von dict mit "teileart", "teileid" und optional "_bbox" @@ -502,6 +521,8 @@ def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, ist nicht zwingend eindeutig). """ half_tol = tolerance_mm / 2.0 + if element_box_mm is None: + element_box_mm = load_element_box_mm() result = [[] for _ in items] kreisel_group = [] @@ -545,22 +566,44 @@ def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0, f"dx={bbox_rechts['dx']:.0f},dy={bbox_rechts['dy']:.0f}) bounds={_fmt_bounds(bounds_rechts)}") continue bounds = _bounds(bbox, half_tol) + asel_bounds = None if teileart in KREISEL_TEILEARTEN: gruppe = "Kreisel/Eckrad" kreisel_group.append((idx, bounds, teileid)) elif teileart in STRECKEN_TEILEARTEN: - gruppe = "Strecke/Foerderer" - strecken_group.append((idx, bounds, teileid)) + # GF/VF (GF_VF_TEILEARTEN): NICHT die grosse Wrapper-Box gegen die + # Kreisel testen, sondern je eine kleine feste Box (element_box_mm) an + # der AS-Position (K1) und der ES-Position (K2) - nur dort beruehrt + # eine Strecke einen Kreisel. Wie beim Kreisel-Links/Rechts-Split + # koennen dadurch bis zu ZWEI Gruppeneintraege mit derselben idx/ + # teileid entstehen; die spaetere dict.fromkeys()-Deduplizierung + # faengt Doppelnennungen ab. Gilt fuer ALLE Strecken-TeileArten + # (GF/VF UND "ILS 2.0 Strecke - Modul"), sofern sie K1/K2 fuehren - + # fehlt K1/K2 (Altbestand oder Modul ohne AS/ES), liefert + # _asel_collision_boxes None und es bleibt bei der Wrapper-Box. + asel_bounds = _asel_collision_boxes(item, half_tol, element_box_mm) + if asel_bounds is not None: + gruppe = f"Strecke/Foerderer (AS/ES-Box, {len(asel_bounds)}x)" + for b in asel_bounds: + strecken_group.append((idx, b, teileid)) + else: + gruppe = "Strecke/Foerderer" + strecken_group.append((idx, bounds, teileid)) elif teileart in OMNIFLO_TEILEARTEN: gruppe = "Omniflo" omniflo_group.append((idx, bounds, teileid, bbox, _omniflo_kpoints(item))) else: gruppe = "(keine Gruppe - wird nicht geprueft)" if dbg: - dbg(f" idx={idx} teileid={teileid!r} teileart={teileart!r} " - f"-> {gruppe}; bbox(cx={bbox.get('cx', 0):.0f},cy={bbox.get('cy', 0):.0f}," - f"dx={bbox.get('dx', 0):.0f},dy={bbox.get('dy', 0):.0f}) " - f"bounds={_fmt_bounds(bounds)}") + if asel_bounds is not None: + bounds_txt = "; ".join(_fmt_bounds(b) for b in asel_bounds) + dbg(f" idx={idx} teileid={teileid!r} teileart={teileart!r} " + f"-> {gruppe}; AS/ES-Box(en) bounds={bounds_txt}") + else: + dbg(f" idx={idx} teileid={teileid!r} teileart={teileart!r} " + f"-> {gruppe}; bbox(cx={bbox.get('cx', 0):.0f},cy={bbox.get('cy', 0):.0f}," + f"dx={bbox.get('dx', 0):.0f},dy={bbox.get('dy', 0):.0f}) " + f"bounds={_fmt_bounds(bounds)}") if dbg: dbg("") @@ -864,7 +907,8 @@ def _oriented_box_aabb(rot_rad, laenge, breite, hoehe): return (dx, dy, hoehe) -def compute_kreisel_touch_switches(items, tolerance_mm, durchmesser_mm=800.0, dbg=None): +def compute_kreisel_touch_switches(items, tolerance_mm, durchmesser_mm=800.0, + element_box_mm=None, dbg=None): """Erzeugt fuer jedes sich beruehrende Paar echter Kreisel ("ILS 2.0 Kreisel") eine ILS-Weiche am Beruehrpunkt. @@ -877,7 +921,7 @@ def compute_kreisel_touch_switches(items, tolerance_mm, durchmesser_mm=800.0, db Bounding-Boxen, nicht den exakten Beruehrpunkt liefert. Beruehrpunkt (= Weichen-Position) = Mittelpunkt der beiden naechstgelegenen - Segmentpunkte. Die Weichen-Box (WEICHE_BOX_*) wird mit ihrer Laengsseite + Segmentpunkte. Die Weichen-Box (element_box_mm) wird mit ihrer Laengsseite senkrecht zur Achse desjenigen Kreisels ausgerichtet, dessen beruehrendes Merkmal ein Endkreis ist (naechster Segmentpunkt am Segmentende). Beruehren sich zwei Endkreise oder zwei Tangentenflanken, wird der erstgenannte @@ -893,6 +937,9 @@ def compute_kreisel_touch_switches(items, tolerance_mm, durchmesser_mm=800.0, db "beruehrung": "Kreis/Tangente" | "Kreis/Kreis" | "Tangente/Tangente"} """ eps = 1e-6 + if element_box_mm is None: + element_box_mm = load_element_box_mm() + box_l, box_b, box_h = element_box_mm kreisel = [] for item in items: if item.get("teileart", "") != KREISEL_SPLIT_TEILEART: @@ -938,8 +985,7 @@ def compute_kreisel_touch_switches(items, tolerance_mm, durchmesser_mm=800.0, db achse_id, achse_rot = id_a, cap_a["rot"] beruehrung = "Kreis/Kreis" if cap1 else "Tangente/Tangente" - dx, dy, dz = _oriented_box_aabb( - achse_rot, WEICHE_BOX_LAENGE_MM, WEICHE_BOX_BREITE_MM, WEICHE_BOX_HOEHE_MM) + dx, dy, dz = _oriented_box_aabb(achse_rot, box_l, box_b, box_h) switches.append({ "x": tx, "y": ty, "z": tz, "dx": dx, "dy": dy, "dz": dz, @@ -992,6 +1038,54 @@ def _strecke_ends(bbox, insert_xy): return e1, e0 +def _asel_kos_xy(item): + """(k1_xy, k2_xy) einer GF/VF-Strecke = Welt-(x,y) von AS (K1/KS_EIN) und + ES (K2/KS_AUS), dekodiert aus item["k1"]/["k2"] (csv:trans-encode, siehe + csv:vfgf-k-kos-strings in export.lsp). Fehlender/leerer String -> None. + Rueckgabe (None, None), wenn die Strecke keine K1/K2 fuehrt (Altbestand + ohne den neuen Export).""" + return _decode_trans_xy(item.get("k1", "")), _decode_trans_xy(item.get("k2", "")) + + +def _asel_axis_rot(k1_xy, k2_xy): + """Rotationswinkel (rad) der Foerderachse AS->ES = Richtung K1->K2. Fuer die + Ausrichtung der festen AS/ES-Kollisionsbox (element_box_mm, Laengsseite quer + zur Achse - konsistent mit _oriented_box_aabb / den Schleuselementen). + Fehlt ein Punkt oder fallen beide zusammen, 0.0 (achsparallele Box).""" + if k1_xy is None or k2_xy is None: + return 0.0 + dx, dy = k2_xy[0] - k1_xy[0], k2_xy[1] - k1_xy[1] + if math.hypot(dx, dy) < 1e-9: + return 0.0 + return math.atan2(dy, dx) + + +def _asel_collision_boxes(item, half_tol, box_dims): + """Kollisions-Bounding-Boxen einer GF/VF-Strecke fuer den Test gegen die + Kreisel: statt der GROSSEN Wrapper-Box je EINE kleine, fest dimensionierte + Box (box_dims, siehe load_element_box_mm) an der AS-Position (K1) und der + ES-Position (K2) - das sind die einzigen Stellen, an denen eine Strecke + einen Kreisel beruehrt. Die Box liegt mit ihrer Laengsseite quer zur + Foerderachse AS->ES (_asel_axis_rot / _oriented_box_aabb). + + box_dims = (laenge, breite, hoehe) in mm. + + Rueckgabe: Liste von (minx, maxx, miny, maxy)-bounds (0-2 Eintraege) oder + None, wenn die Strecke KEINE K1/K2 fuehrt (dann faellt der Aufrufer auf die + Wrapper-Box zurueck - Altbestand ohne AS/ES-Export).""" + k1_xy, k2_xy = _asel_kos_xy(item) + if k1_xy is None and k2_xy is None: + return None + rot = _asel_axis_rot(k1_xy, k2_xy) + dx, dy, _dz = _oriented_box_aabb(rot, box_dims[0], box_dims[1], box_dims[2]) + boxes = [] + for pt in (k1_xy, k2_xy): + if pt is None: + continue + boxes.append(_bounds({"cx": pt[0], "cy": pt[1], "dx": dx, "dy": dy}, half_tol)) + return boxes + + 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 @@ -1008,7 +1102,8 @@ def _distinct_kreisel_ids(nachbarn): return ids -def compute_strecke_kreisel_schleus(items, durchmesser_mm=800.0, dbg=None): +def compute_strecke_kreisel_schleus(items, durchmesser_mm=800.0, + element_box_mm=None, dbg=None): """Erzeugt je Gefaellestrecke/Foerderer (GF_VF_TEILEARTEN) an Anfang und Ende ein Aus- bzw. Einschleuselement an der Kollisionsposition zum anschliessenden Kreisel. @@ -1030,10 +1125,18 @@ def compute_strecke_kreisel_schleus(items, durchmesser_mm=800.0, dbg=None): 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). + Position eines Elements = das AS-/ES-Koordinatensystem (K1/K2, siehe + csv:vfgf-k-kos-strings), um eine HALBE Box (Box-Breite/2 entlang der + Foerderachse, siehe element_box_mm) nach aussen versetzt: das + Ausschleuselement (AS/Anfang) VOR das AS-KOS (entgegen der Achse), das + Einschleuselement (ES/Ende) HINTER das ES-KOS (in Achsrichtung) - beide + also zum anschliessenden Kreisel hin, sodass das Element realistisch + zwischen Streckenende und Kreisel liegt. Fehlen K1/K2 (Altbestand ohne + AS/ES-Export), werden die Streckenenden aus der Wrapper-BBox genommen + (_strecke_ends). Die feste Box (element_box_mm) wird mit ihrer Laengsseite + senkrecht zur Foerderachse ausgerichtet (Welt-AABB, _oriented_box_aabb). + + element_box_mm = (laenge, breite, hoehe) in mm; None -> load_element_box_mm(). Rueckgabe: (result, warnings) result = Liste von dicts: @@ -1046,6 +1149,9 @@ def compute_strecke_kreisel_schleus(items, durchmesser_mm=800.0, dbg=None): warnings = dict {strecke_teileid: warntext} fuer Strecken mit > 2 Kreisel-BBox-Beruehrungen. """ + if element_box_mm is None: + element_box_mm = load_element_box_mm() + box_l, box_b, box_h = element_box_mm kreisel = {} for it in items: if it.get("teileart", "") == KREISEL_SPLIT_TEILEART: @@ -1074,7 +1180,34 @@ def compute_strecke_kreisel_schleus(items, durchmesser_mm=800.0, dbg=None): 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) + # Streckenenden bevorzugt aus den AS/ES-Koordinatensystemen (K1=AS, + # K2=ES, siehe csv:vfgf-k-kos-strings) - das ist die tatsaechliche + # Position der Ein-/Ausschleuspunkte. "Anfang" bleibt das dem Block- + # Einfuegepunkt naehere Ende. Fehlen K1/K2 (Altbestand ohne AS/ES- + # Export), Fallback auf die Stirnseiten-Mitten der Wrapper-BBox. + k1_xy, k2_xy = _asel_kos_xy(it) + if k1_xy is not None and k2_xy is not None: + ix, iy = insert_xy + if math.hypot(k1_xy[0] - ix, k1_xy[1] - iy) <= math.hypot(k2_xy[0] - ix, k2_xy[1] - iy): + anfang_end, ende_end = k1_xy, k2_xy + else: + anfang_end, ende_end = k2_xy, k1_xy + else: + anfang_end, ende_end = _strecke_ends(bbox, insert_xy) + # Foerderachse (Einheitsvektor Anfang->Ende) fuer den Halbe-Box-Versatz + # der Schleuselemente. Ohne verwertbare Richtung (Enden fallen zusammen) + # bleibt der Versatz 0. + adx, ady = ende_end[0] - anfang_end[0], ende_end[1] - anfang_end[1] + alen = math.hypot(adx, ady) + axis_u = (adx / alen, ady / alen) if alen > 1e-9 else (0.0, 0.0) + # Versatz = halbe Box-Ausdehnung ENTLANG der Foerderachse. Die feste Box + # (element_box_mm) liegt mit ihrer Breite laengs der Achse (siehe + # _oriented_box_aabb), also ist die halbe Achs-Ausdehnung Breite/2. + halbe_box = box_b / 2.0 + # Ausrichtung der Schleuselement-Box: entlang der Foerderachse (nicht + # mehr der Kreiselachse) - das Element sitzt am AS/ES-KOS in + # Foerderrichtung, konsistent mit den AS/ES-Kollisionsboxen. + achs_rot = math.atan2(ady, adx) if alen > 1e-9 else 0.0 scz = bbox.get("cz", 0.0) def _nearest(end_pt): @@ -1101,27 +1234,32 @@ def compute_strecke_kreisel_schleus(items, durchmesser_mm=800.0, dbg=None): 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)): + # Versatz der Schleuselement-Position gegenueber dem AS/ES-KOS: eine + # HALBE Box entlang der Foerderachse, jeweils NACH AUSSEN (weg von der + # Streckenmitte, zum anschliessenden Kreisel hin) - damit das Element + # realistisch zwischen Streckenende und Kreisel liegt statt exakt auf + # dem KOS. Vorzeichen: Ausschleuselement (AS/Anfang) "vor das AS-KOS" + # = entgegen der Achse (-u), Einschleuselement (ES/Ende) "hinter das + # ES-KOS" = in Achsrichtung (+u). + for best, teileart, label, end_pt, vorzeichen in ( + (a_best, AUSSCHLEUS_TEILEART, "Anfang", anfang_end, -1.0), + (e_best, EINSCHLEUS_TEILEART, "Ende", ende_end, 1.0)): 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) + pos = (end_pt[0] + vorzeichen * axis_u[0] * halbe_box, + end_pt[1] + vorzeichen * axis_u[1] * halbe_box) + dx, dy, dz = _oriented_box_aabb(achs_rot, box_l, box_b, box_h) result.append({ "teileart": teileart, - "x": contact[0], "y": contact[1], "z": (cap["z"] + scz) / 2.0, + "x": pos[0], "y": pos[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})") + f"{d:.0f}mm) -> {teileart} bei ({pos[0]:.0f}, {pos[1]:.0f}, " + f"{(cap['z'] + scz) / 2.0:.0f}) = KOS ({end_pt[0]:.0f}, {end_pt[1]:.0f}) " + f"{'+' if vorzeichen > 0 else '-'} halbe Box ({halbe_box:.0f}mm) entlang Achse, " + f"Box-AABB ({dx:.0f}, {dy:.0f}, {dz:.0f})") if dbg: dbg(f"Erzeugte Ein-/Ausschleuselemente: {len(result)}; "