[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 --- ;; --- Mindesthoehe der Bounding-Box (Z-Ausdehnung), aus cfg/export.cfg ---
;; [Boundingbox] bb_minimum_height_mm, Default 1 (mm) falls nicht gesetzt. ;; [Boundingbox] bb_minimum_height_mm, Default 1 (mm) falls nicht gesetzt.
@@ -487,6 +533,14 @@
(export:pattern "pattern_gerade" "AP110*,AP_110*"))) (export:pattern "pattern_gerade" "AP110*,AP_110*")))
(setq k-list (csv:gerade-k-kos-strings ename pt rotation)) (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 "") (setq bbox-json "")
(if include-bbox (if include-bbox
(progn (progn
+15
View File
@@ -142,6 +142,21 @@ debug_log=1
# nicht gesetzt. # nicht gesetzt.
bb_minimum_height_mm=1 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]
# Kreisel-Durchmesser (Antriebs-/Spannstation AN8/SP8) in mm. Quelle der # Kreisel-Durchmesser (Antriebs-/Spannstation AN8/SP8) in mm. Quelle der
# Wahrheit ist data/json/component_defaults.json ("kreisel"."durchmesser", # 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 Beruehrpunkt zweier Kreisel eine "ILS Weiche"-Zeile angehaengt (siehe
build_kreisel_weiche_items / export_neighbors.compute_kreisel_touch_switches): build_kreisel_weiche_items / export_neighbors.compute_kreisel_touch_switches):
Anzahl 1, Position = Beruehrkoordinaten, Nachbarn = die beiden Kreisel-IDs, 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 mit der Laengsseite senkrecht zur Achse des beruehrenden Kreisels; die TeileId
zaehlt aus der hoechsten bereits vergebenen numerischen ID hoch. zaehlt aus der hoechsten bereits vergebenen numerischen ID hoch.
@@ -113,6 +114,7 @@ from export_neighbors import (
load_omniflo_cell_size_mm, load_omniflo_cell_size_mm,
load_omniflo_ks_tolerance_mm, load_omniflo_ks_tolerance_mm,
load_kreisel_durchmesser_mm, load_kreisel_durchmesser_mm,
load_element_box_mm,
load_collision_debug_target, load_collision_debug_target,
compute_neighbor_ids, compute_neighbor_ids,
compute_neighbor_errors, 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_CELL_SIZE_MM = load_omniflo_cell_size_mm()
NEIGHBOR_OMNIFLO_KS_TOLERANCE_MM = load_omniflo_ks_tolerance_mm() NEIGHBOR_OMNIFLO_KS_TOLERANCE_MM = load_omniflo_ks_tolerance_mm()
KREISEL_DURCHMESSER_MM = load_kreisel_durchmesser_mm() KREISEL_DURCHMESSER_MM = load_kreisel_durchmesser_mm()
ELEMENT_BOX_MM = load_element_box_mm()
NEIGHBOR_DEBUG_TARGET = load_collision_debug_target() 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 (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 ist wie bei allen Zeilen 1; Nachbarn = die beiden Kreisel-IDs; K1-K4 und
Insertpoint bleiben leer; Position = Beruehrkoordinaten; Boundingbox = die 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). Rueckgabe: fortgeschriebene Elementnummer (start_elem_nr + Anzahl Weichen).
""" """
switches = compute_kreisel_touch_switches( 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) naechste_id = max_numeric_teileid(items)
elem_nr = start_elem_nr 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. Rueckgabe: fortgeschriebene Elementnummer.
""" """
schleus, warnings = compute_strecke_kreisel_schleus( 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 # Warnungen (>2 Kreisel-BBox-Beruehrungen je Strecke) auf die Strecken-Zeile
# schreiben (Spalte "Warnungen") und in die Konsole ausgeben. # schreiben (Spalte "Warnungen") und in die Konsole ausgeben.
@@ -815,7 +821,8 @@ def process_blocks(blocks, lookup, dbg=None):
neighbor_ids = compute_neighbor_ids( neighbor_ids = compute_neighbor_ids(
items, NEIGHBOR_TOLERANCE_MM, NEIGHBOR_OMNIFLO_CELL_SIZE_MM, items, NEIGHBOR_TOLERANCE_MM, NEIGHBOR_OMNIFLO_CELL_SIZE_MM,
kreisel_durchmesser_mm=KREISEL_DURCHMESSER_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) neighbor_errors = compute_neighbor_errors(items, neighbor_ids)
for item, nachbarn, fehler in zip(items, neighbor_ids, neighbor_errors): for item, nachbarn, fehler in zip(items, neighbor_ids, neighbor_errors):
item["nachbarn"] = nachbarn item["nachbarn"] = nachbarn
+173 -35
View File
@@ -94,15 +94,33 @@ EINSCHLEUS_TEILEART = "ILS Einschleuselement"
# NICHT - nur die kompletten GF_n/VF_n-Bloecke. # NICHT - nur die kompletten GF_n/VF_n-Bloecke.
GF_VF_TEILEARTEN = {"ILS 2.0 Gefaellestrecke", "ILS 2.0 Strecke"} GF_VF_TEILEARTEN = {"ILS 2.0 Gefaellestrecke", "ILS 2.0 Strecke"}
# Statische Default-Boundingbox einer solchen ILS-Weiche (Laenge x Breite x # Fallback-Masse (mm) der festen Element-Bounding-Box, falls cfg/export.cfg
# Hoehe in mm). Die Laengsseite (WEICHE_BOX_LAENGE_MM) wird senkrecht zur # [Boundingbox] element_box_* nicht gesetzt/lesbar ist (siehe
# Achse desjenigen Kreisels ausgerichtet, dessen Endkreis den Beruehrpunkt # load_element_box_mm). NICHT direkt verwenden - die Rechenfunktionen bekommen
# bildet; ausgegeben wird die achsparallele Welt-Bounding-Box dieser # die Masse als Parameter (Default = load_element_box_mm()), damit die cfg-Werte
# gedrehten Box (siehe _oriented_box_aabb). # greifen. Laengsseite (LAENGE) quer zur Foerder-/Kreiselachse, BREITE laengs
# (siehe _oriented_box_aabb).
WEICHE_BOX_LAENGE_MM = 250.0 WEICHE_BOX_LAENGE_MM = 250.0
WEICHE_BOX_BREITE_MM = 100.0 WEICHE_BOX_BREITE_MM = 100.0
WEICHE_BOX_HOEHE_MM = 30.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" # Mindestanzahl Partner (Nachbarn) je TeileArt fuer die Fehlerspalte "Fehler"
# (siehe compute_neighbor_errors): Kreisel/Eckrad und Omniflo-Elemente # (siehe compute_neighbor_errors): Kreisel/Eckrad und Omniflo-Elemente
# brauchen mindestens einen Partner, Gefaellestrecke/Foerderer/Strecke-Modul # 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, def compute_neighbor_ids(items, tolerance_mm, omniflo_cell_size_mm=3000.0,
kreisel_durchmesser_mm=800.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. """Ermittelt je Item die IDs (item["teileid"]) benachbarter Elemente.
items = Liste von dict mit "teileart", "teileid" und optional "_bbox" 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). ist nicht zwingend eindeutig).
""" """
half_tol = tolerance_mm / 2.0 half_tol = tolerance_mm / 2.0
if element_box_mm is None:
element_box_mm = load_element_box_mm()
result = [[] for _ in items] result = [[] for _ in items]
kreisel_group = [] 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)}") f"dx={bbox_rechts['dx']:.0f},dy={bbox_rechts['dy']:.0f}) bounds={_fmt_bounds(bounds_rechts)}")
continue continue
bounds = _bounds(bbox, half_tol) bounds = _bounds(bbox, half_tol)
asel_bounds = None
if teileart in KREISEL_TEILEARTEN: if teileart in KREISEL_TEILEARTEN:
gruppe = "Kreisel/Eckrad" gruppe = "Kreisel/Eckrad"
kreisel_group.append((idx, bounds, teileid)) kreisel_group.append((idx, bounds, teileid))
elif teileart in STRECKEN_TEILEARTEN: elif teileart in STRECKEN_TEILEARTEN:
gruppe = "Strecke/Foerderer" # GF/VF (GF_VF_TEILEARTEN): NICHT die grosse Wrapper-Box gegen die
strecken_group.append((idx, bounds, teileid)) # 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: elif teileart in OMNIFLO_TEILEARTEN:
gruppe = "Omniflo" gruppe = "Omniflo"
omniflo_group.append((idx, bounds, teileid, bbox, _omniflo_kpoints(item))) omniflo_group.append((idx, bounds, teileid, bbox, _omniflo_kpoints(item)))
else: else:
gruppe = "(keine Gruppe - wird nicht geprueft)" gruppe = "(keine Gruppe - wird nicht geprueft)"
if dbg: if dbg:
dbg(f" idx={idx} teileid={teileid!r} teileart={teileart!r} " if asel_bounds is not None:
f"-> {gruppe}; bbox(cx={bbox.get('cx', 0):.0f},cy={bbox.get('cy', 0):.0f}," bounds_txt = "; ".join(_fmt_bounds(b) for b in asel_bounds)
f"dx={bbox.get('dx', 0):.0f},dy={bbox.get('dy', 0):.0f}) " dbg(f" idx={idx} teileid={teileid!r} teileart={teileart!r} "
f"bounds={_fmt_bounds(bounds)}") 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: if dbg:
dbg("") dbg("")
@@ -864,7 +907,8 @@ def _oriented_box_aabb(rot_rad, laenge, breite, hoehe):
return (dx, dy, 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 """Erzeugt fuer jedes sich beruehrende Paar echter Kreisel ("ILS 2.0
Kreisel") eine ILS-Weiche am Beruehrpunkt. 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. Bounding-Boxen, nicht den exakten Beruehrpunkt liefert.
Beruehrpunkt (= Weichen-Position) = Mittelpunkt der beiden naechstgelegenen 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 senkrecht zur Achse desjenigen Kreisels ausgerichtet, dessen beruehrendes
Merkmal ein Endkreis ist (naechster Segmentpunkt am Segmentende). Beruehren Merkmal ein Endkreis ist (naechster Segmentpunkt am Segmentende). Beruehren
sich zwei Endkreise oder zwei Tangentenflanken, wird der erstgenannte 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"} "beruehrung": "Kreis/Tangente" | "Kreis/Kreis" | "Tangente/Tangente"}
""" """
eps = 1e-6 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 = [] kreisel = []
for item in items: for item in items:
if item.get("teileart", "") != KREISEL_SPLIT_TEILEART: 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"] achse_id, achse_rot = id_a, cap_a["rot"]
beruehrung = "Kreis/Kreis" if cap1 else "Tangente/Tangente" beruehrung = "Kreis/Kreis" if cap1 else "Tangente/Tangente"
dx, dy, dz = _oriented_box_aabb( dx, dy, dz = _oriented_box_aabb(achse_rot, box_l, box_b, box_h)
achse_rot, WEICHE_BOX_LAENGE_MM, WEICHE_BOX_BREITE_MM, WEICHE_BOX_HOEHE_MM)
switches.append({ switches.append({
"x": tx, "y": ty, "z": tz, "x": tx, "y": ty, "z": tz,
"dx": dx, "dy": dy, "dz": dz, "dx": dx, "dy": dy, "dz": dz,
@@ -992,6 +1038,54 @@ def _strecke_ends(bbox, insert_xy):
return e1, e0 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): def _distinct_kreisel_ids(nachbarn):
"""Eindeutige Kreisel-TeileIds aus einem Nachbarn-String ("0009-L, 0002-R, """Eindeutige Kreisel-TeileIds aus einem Nachbarn-String ("0009-L, 0002-R,
0001-L" -> ["0009", "0002", "0001"]). Das Haelften-Suffix -L/-R (siehe 0001-L" -> ["0009", "0002", "0001"]). Das Haelften-Suffix -L/-R (siehe
@@ -1008,7 +1102,8 @@ def _distinct_kreisel_ids(nachbarn):
return ids 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 """Erzeugt je Gefaellestrecke/Foerderer (GF_VF_TEILEARTEN) an Anfang und
Ende ein Aus- bzw. Einschleuselement an der Kollisionsposition zum Ende ein Aus- bzw. Einschleuselement an der Kollisionsposition zum
anschliessenden Kreisel. 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 Warnung: die beiden Enden bekommen weiterhin ihren jeweils naechsten
Kreisel, die ueberzaehligen werden ignoriert und in der Warnung genannt. Kreisel, die ueberzaehligen werden ignoriert und in der Warnung genannt.
Position eines Elements = der Punkt auf dem Kreiselrand in Richtung Position eines Elements = das AS-/ES-Koordinatensystem (K1/K2, siehe
Streckenende (Beruehrstelle Kreisel<->Strecke). Die statische Box csv:vfgf-k-kos-strings), um eine HALBE Box (Box-Breite/2 entlang der
(WEICHE_BOX_*) wird - wie bei der Weiche - mit ihrer Laengsseite senkrecht Foerderachse, siehe element_box_mm) nach aussen versetzt: das
zur Kreiselachse ausgerichtet (Welt-AABB, siehe _oriented_box_aabb). 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) Rueckgabe: (result, warnings)
result = Liste von dicts: 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 warnings = dict {strecke_teileid: warntext} fuer Strecken mit > 2
Kreisel-BBox-Beruehrungen. 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 = {} kreisel = {}
for it in items: for it in items:
if it.get("teileart", "") == KREISEL_SPLIT_TEILEART: 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, insert_xy = (it.get("x", bbox.get("cx", 0.0)) or 0.0,
it.get("y", bbox.get("cy", 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) scz = bbox.get("cz", 0.0)
def _nearest(end_pt): def _nearest(end_pt):
@@ -1101,27 +1234,32 @@ def compute_strecke_kreisel_schleus(items, durchmesser_mm=800.0, dbg=None):
if dbg: if dbg:
dbg(f" Strecke {sid}: WARNUNG - {warnings[sid]}") dbg(f" Strecke {sid}: WARNUNG - {warnings[sid]}")
for best, teileart, label, end_pt in ( # Versatz der Schleuselement-Position gegenueber dem AS/ES-KOS: eine
(a_best, AUSSCHLEUS_TEILEART, "Anfang", anfang_end), # HALBE Box entlang der Foerderachse, jeweils NACH AUSSEN (weg von der
(e_best, EINSCHLEUS_TEILEART, "Ende", ende_end)): # 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 kid, cap, p, d = best
if d > 1e-6: pos = (end_pt[0] + vorzeichen * axis_u[0] * halbe_box,
ux, uy = (end_pt[0] - p[0]) / d, (end_pt[1] - p[1]) / d end_pt[1] + vorzeichen * axis_u[1] * halbe_box)
contact = (p[0] + ux * cap["r"], p[1] + uy * cap["r"]) dx, dy, dz = _oriented_box_aabb(achs_rot, box_l, box_b, box_h)
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({ result.append({
"teileart": teileart, "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, "dx": dx, "dy": dy, "dz": dz,
"kreisel_id": kid, "strecke_id": sid, "ende_label": label, "kreisel_id": kid, "strecke_id": sid, "ende_label": label,
}) })
if dbg: if dbg:
dbg(f" Strecke {sid} {label}: Kreisel {kid} (Abstand Ende<->Achse " dbg(f" Strecke {sid} {label}: Kreisel {kid} (Abstand Ende<->Achse "
f"{d:.0f}mm) -> {teileart} bei ({contact[0]:.0f}, {contact[1]:.0f}, " f"{d:.0f}mm) -> {teileart} bei ({pos[0]:.0f}, {pos[1]:.0f}, "
f"{(cap['z'] + scz) / 2.0:.0f}), Box-AABB ({dx:.0f}, {dy:.0f}, {dz:.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: if dbg:
dbg(f"Erzeugte Ein-/Ausschleuselemente: {len(result)}; " dbg(f"Erzeugte Ein-/Ausschleuselemente: {len(result)}; "