[ADD] AS/ES-Koordinatensysteme als K1/K2 exportieren + Kollision/Schleuselemente daran ausrichten

Export (Lisp/export.lsp):
- Neue csv:vfgf-k-kos-strings leitet fuer VF_n/GF_n-Wrapper K1 (AS/KS_EIN) und
  K2 (ES/KS_AUS) aus den verschachtelten AS_Element/ES_Element ab (via
  ssg-collect-nested-inserts) und schreibt sie wie K1-K4 als Positions-String.
  Bisher waren die K-Spalten fuer Strecken leer.

Kollisionspruefung Kreisel<->GF/VF (lib/export_neighbors.py):
- compute_neighbor_ids testet Strecken (GF/VF UND "ILS 2.0 Strecke - Modul")
  nicht mehr ueber die grosse Wrapper-Box, sondern ueber je eine kleine feste
  Box an der AS- (K1) und ES-Position (K2). Fallback auf Wrapper-Box, wenn
  K1/K2 fehlen (Altbestand).

Schleuselement-Positionen (lib/export_neighbors.py):
- compute_strecke_kreisel_schleus setzt Aus-/Einschleuselement ans AS/ES-KOS,
  um eine halbe Box entlang der Foerderachse nach aussen versetzt (AS: davor,
  ES: dahinter) - realistischer als der bisherige Kreiselrand-Punkt.

Keine Magic Numbers (cfg/export.cfg [Boundingbox]):
- element_box_laenge/breite/hoehe_mm (250/100/30) ersetzen die WEICHE_BOX_*-
  Konstanten; gelesen von load_element_box_mm und an alle Rechenfunktionen
  durchgereicht (WEICHE_BOX_* nur noch dokumentierter Fallback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-04 14:00:52 +02:00
parent 29b7bf1a10
commit c88be420be
4 changed files with 254 additions and 40 deletions
+54
View File
@@ -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
+15
View File
@@ -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",
+12 -5
View File
@@ -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
+173 -35
View File
@@ -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)}; "