[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:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)}; "
|
||||||
|
|||||||
Reference in New Issue
Block a user