Export: ILS-Weiche an Kreisel-Beruehrpunkten + Ein-/Ausschleuselemente an Strecken

- compute_kreisel_touch_switches: erzeugt an jedem Beruehrpunkt zweier Kreisel
  eine "ILS Weiche" (Kapsel-Geometrie, Position=Beruehrpunkt, statische Box
  250x100x30 mit Laengsseite senkrecht zur Kreiselachse, Nachbarn=beide Kreisel)
- compute_strecke_kreisel_schleus: an Anfang/Ende jeder Gefaellestrecke bzw.
  jedes Foerderers (ILS 2.0 Strecke) ein Aus-/Einschleuselement an der
  Kollisionsposition zum naechsten BBox-Nachbar-Kreisel (Nachbarn=Kreisel+Strecke)
- TeileId zaehlt fortlaufend aus der hoechsten vorhandenen ID hoch (Weichen
  zuerst, dann Schleuselemente)
- Warnung (Konsole + neue CSV-Spalte "Warnungen"), wenn eine Strecke mehr als
  zwei Kreisel-BoundingBoxen beruehrt (erwartet: ein Anfang + ein Ende)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-04 15:18:59 +02:00
parent a664c938b7
commit dc2b0b8900
2 changed files with 273 additions and 4 deletions
+90 -4
View File
@@ -15,12 +15,25 @@ K1-K4/Insertpoint leer, Boundingbox = statische Default-Box (WEICHE_BOX_*),
mit der Laengsseite senkrecht zur Achse des beruehrenden Kreisels; die TeileId
zaehlt aus der hoechsten bereits vergebenen numerischen ID hoch.
Anschliessend (TeileId-Zaehlung laeuft ueber die Weichen hinweg weiter) wird
an jedem Ende einer Gefaellestrecke/eines Foerderers, an dem ein Kreisel
anschliesst, ein Aus- (Anfang) bzw. Einschleuselement (Ende) an der Kollisions-
position Kreisel<->Strecke angehaengt (siehe build_strecke_schleus_items /
export_neighbors.compute_strecke_kreisel_schleus) - Format wie die Weiche,
Nachbarn = Kreisel-ID + Strecken-ID. Zugeordnet wird je Streckenende der
naechstgelegene ihrer BBox-Kreisel-Nachbarn.
Warnungen = Spalte fuer Auffaelligkeiten. Aktuell: eine Strecke sollte genau
zwei Kreisel (Anfang + Ende) beruehren - beruehrt ihre (in x/y gepruefte)
Bounding-Box mehr, steht hier ein Hinweis und es wird zusaetzlich eine
Konsolen-WARNUNG ausgegeben (mit exakten 3D-BBoxen sollte das nicht auftreten).
Aufruf:
python export_csv.py <export_raw.json> <data_dir> <output.csv>
CSV-Format:
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;
Insertpoint;K1;K2;K3;K4;Nachbarn;Fehler;Merkmale
Insertpoint;K1;K2;K3;K4;Nachbarn;Fehler;Warnungen;Merkmale
Position und Boundingbox stammen aus der von export.lsp (csv:get-bbox, per
vla-getboundingbox) ermittelten Bounding-Box je Block:
@@ -90,8 +103,10 @@ from export_neighbors import (
compute_sensor_zuordnung,
compute_scanner_nearest_separator,
compute_kreisel_touch_switches,
compute_strecke_kreisel_schleus,
SENSOR_TEILEARTEN,
WEICHE_TEILEART,
GF_VF_TEILEARTEN,
)
BLOCKPATTERNS = load_patterns()
@@ -462,6 +477,65 @@ def build_kreisel_weiche_items(items, planquadrat_cfg, start_elem_nr, dbg=None):
return elem_nr
def build_strecke_schleus_items(items, planquadrat_cfg, start_elem_nr, dbg=None):
"""Haengt fuer jedes Strecken-Ende mit anschliessendem Kreisel eine
"ILS Ausschleuselement"- (Anfang) bzw. "ILS Einschleuselement"-Zeile
(Ende) an items an (siehe export_neighbors.compute_strecke_kreisel_schleus).
Muss NACH build_kreisel_weiche_items laufen, damit die TeileId-Zaehlung
(max_numeric_teileid) auch die bereits erzeugten Weichen einschliesst.
Position = Kollisionsposition Kreisel<->Strecke; Nachbarn = Kreisel-ID +
Strecken-ID; Anzahl 1; K1-K4/Insertpoint leer; Boundingbox = die statische,
an der Kreiselachse ausgerichtete Default-Box.
Rueckgabe: fortgeschriebene Elementnummer.
"""
schleus, warnings = compute_strecke_kreisel_schleus(
items, durchmesser_mm=KREISEL_DURCHMESSER_MM, dbg=dbg)
# Warnungen (>2 Kreisel-BBox-Beruehrungen je Strecke) auf die Strecken-Zeile
# schreiben (Spalte "Warnungen") und in die Konsole ausgeben.
if warnings:
strecke_by_id = {it.get("teileid", ""): it for it in items
if it.get("teileart", "") in GF_VF_TEILEARTEN}
for sid, text in warnings.items():
if sid in strecke_by_id:
strecke_by_id[sid]["warnung"] = text
print(f"[export_csv] WARNUNG: Strecke {sid} {text}")
naechste_id = max_numeric_teileid(items)
elem_nr = start_elem_nr
zaehler = {}
for sp in schleus:
elem_nr += 1
naechste_id += 1
teileid = f"{naechste_id:04d}"
teileart = sp["teileart"]
zaehler[teileart] = zaehler.get(teileart, 0) + 1
planquadrat = (compute_planquadrat(sp["x"], sp["y"], planquadrat_cfg)
if planquadrat_cfg is not None else "")
items.append({
"nr": elem_nr,
"teileart": teileart,
"teileid": teileid,
"bezeichnung": f"{teileart} :{zaehler[teileart]}",
"planquadrat": planquadrat,
"position": f'{sp["x"]:.2f}, {sp["y"]:.2f}, {sp["z"]:.2f}',
"boundingbox": f'{sp["dx"]:.2f}, {sp["dy"]:.2f}, {sp["dz"]:.2f}',
"insertpoint": "",
"k1": "", "k2": "", "k3": "", "k4": "",
"nachbarn": ", ".join([sp["kreisel_id"], sp["strecke_id"]]),
"fehler": "",
"merkmale": {
"Erzeugt aus": "Kreisel-Strecke-Kollision",
"Kreisel": sp["kreisel_id"],
"Strecke": sp["strecke_id"],
"Streckenende": sp["ende_label"],
},
})
return elem_nr
def process_blocks(blocks, lookup, dbg=None):
# x_origin_mm/y_origin_mm einmalig fuer diesen Exportlauf aufloesen (siehe
# export_planquadrat.resolve_origins): automatisch aus den Bloecken, falls
@@ -565,6 +639,10 @@ def process_blocks(blocks, lookup, dbg=None):
"bezeichnung": f"VarioFoerderer :{vf_count}",
"planquadrat": planquadrat_column(block, planquadrat_cfg),
"merkmale": build_variofoerderer_merkmale(block),
# x/y (Block-Einfuegepunkt = Kettenanfang) fuer die Anfang/Ende-
# Bestimmung der Ein-/Ausschleuselemente (compute_strecke_kreisel_schleus)
"x": block.get("x", 0.0),
"y": block.get("y", 0.0),
**bbox_columns(block),
})
continue
@@ -580,6 +658,10 @@ def process_blocks(blocks, lookup, dbg=None):
"bezeichnung": f"Gefaellestrecke :{gf_count}",
"planquadrat": planquadrat_column(block, planquadrat_cfg),
"merkmale": build_variofoerderer_merkmale(block),
# x/y (Block-Einfuegepunkt = Kettenanfang) fuer die Anfang/Ende-
# Bestimmung der Ein-/Ausschleuselemente (compute_strecke_kreisel_schleus)
"x": block.get("x", 0.0),
"y": block.get("y", 0.0),
**bbox_columns(block),
})
continue
@@ -728,8 +810,11 @@ def process_blocks(blocks, lookup, dbg=None):
# Nach der Kollisions-/Nachbarschaftserkennung: an jedem Kreisel-
# Beruehrpunkt eine ILS-Weiche anhaengen (eigene Zeilen am Listenende -
# sie nehmen an der Nachbarschaftserkennung selbst nicht teil, ihre
# Nachbarn sind fest die beiden beruehrenden Kreisel).
build_kreisel_weiche_items(items, planquadrat_cfg, elem_nr, dbg=dbg)
# Nachbarn sind fest die beiden beruehrenden Kreisel). Danach an jedem
# Strecken-Ende mit anschliessendem Kreisel ein Aus-/Einschleuselement -
# die TeileId-Zaehlung laeuft ueber beide hinweg weiter (Weichen zuerst).
elem_nr = build_kreisel_weiche_items(items, planquadrat_cfg, elem_nr, dbg=dbg)
build_strecke_schleus_items(items, planquadrat_cfg, elem_nr, dbg=dbg)
return items
@@ -756,6 +841,7 @@ def format_csv_line(item):
f';"{csv_quote(item["k4"])}"'
f';"{csv_quote(item["nachbarn"])}"'
f';"{csv_quote(item["fehler"])}"'
f';"{csv_quote(item.get("warnung", ""))}"'
f';{merkmale_json}'
)
@@ -812,7 +898,7 @@ def main():
print(f"[export_csv] Kollisions-Debuglog geschrieben: {NEIGHBOR_DEBUG_TARGET}")
header = ("Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;"
"Insertpoint;K1;K2;K3;K4;Nachbarn;Fehler;Merkmale")
"Insertpoint;K1;K2;K3;K4;Nachbarn;Fehler;Warnungen;Merkmale")
with open(output_csv, "w", encoding="utf-8") as f:
f.write(header + "\n")
for item in items:
+183
View File
@@ -83,6 +83,17 @@ KREISEL_SPLIT_TEILEART = "ILS 2.0 Kreisel"
# (compute_kreisel_touch_switches / export_csv.py).
WEICHE_TEILEART = "ILS Weiche"
# TeileArten der automatisch an den Strecken-Enden (Kollision Kreisel <->
# Gefaellestrecke/Foerderer) erzeugten Ein-/Ausschleuselemente
# (compute_strecke_kreisel_schleus / export_csv.py).
AUSSCHLEUS_TEILEART = "ILS Ausschleuselement"
EINSCHLEUS_TEILEART = "ILS Einschleuselement"
# Strecken-TeileArten, die Ein-/Ausschleuselemente bekommen: Gefaellestrecke
# und (Vario-)Foerderer. "ILS 2.0 Strecke - Modul" (Einzelkomponenten) bewusst
# 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
@@ -945,3 +956,175 @@ def compute_kreisel_touch_switches(items, tolerance_mm, durchmesser_mm=800.0, db
dbg(f"Erzeugte ILS-Weichen: {len(switches)}")
return switches
# ---------------------------------------------------------------------------
# Kreisel <-> Strecken-Enden -> Ein-/Ausschleuselemente
# ---------------------------------------------------------------------------
def _closest_pt_on_segment(a, b, p):
"""Naechstgelegener Punkt auf der 2D-Strecke [a,b] zum Punkt p."""
abx, aby = b[0] - a[0], b[1] - a[1]
denom = abx * abx + aby * aby
if denom <= 1e-9:
return (a[0], a[1])
t = _clamp(((p[0] - a[0]) * abx + (p[1] - a[1]) * aby) / denom, 0.0, 1.0)
return (a[0] + abx * t, a[1] + aby * t)
def _strecke_ends(bbox, insert_xy):
"""Die beiden Enden einer GF/VF-Strecke als (Anfang, Ende) in Welt-x/y.
Enden = Mitten der beiden kurzen Stirnseiten der (achsparallelen) Bounding-
Box entlang ihrer laengeren Ausdehnung. "Anfang" ist das Ende, das dem
Block-Einfuegepunkt (insert_xy = Kettenanfang/startpunkt, siehe
ssg-block-wrap-welt) naeher liegt; "Ende" das andere.
"""
cx, cy = bbox.get("cx", 0.0), bbox.get("cy", 0.0)
dx, dy = bbox.get("dx", 0.0), bbox.get("dy", 0.0)
if dx >= dy:
e0, e1 = (cx - dx / 2.0, cy), (cx + dx / 2.0, cy)
else:
e0, e1 = (cx, cy - dy / 2.0), (cx, cy + dy / 2.0)
ix, iy = insert_xy
if math.hypot(e0[0] - ix, e0[1] - iy) <= math.hypot(e1[0] - ix, e1[1] - iy):
return e0, e1
return e1, e0
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
kreisel_half_bboxes) wird entfernt, Reihenfolge bleibt erhalten."""
ids = []
for part in (nachbarn or "").split(","):
p = part.strip()
if not p:
continue
if p.endswith("-L") or p.endswith("-R"):
p = p[:-2]
if p not in ids:
ids.append(p)
return ids
def compute_strecke_kreisel_schleus(items, durchmesser_mm=800.0, dbg=None):
"""Erzeugt je Gefaellestrecke/Foerderer (GF_VF_TEILEARTEN) an Anfang und
Ende ein Aus- bzw. Einschleuselement an der Kollisionsposition zum
anschliessenden Kreisel.
Grundlage sind NICHT eigene Abstandsschwellen, sondern die bereits per
Bounding-Box ermittelten Kreisel-Nachbarn der Strecke (Spalte "Nachbarn",
siehe compute_neighbor_ids). Jeder der beiden Streckenenden (Anfang = dem
Block-Einfuegepunkt naeheres Ende, siehe _strecke_ends) wird der unter
diesen Nachbarn naechstgelegene Kreisel zugeordnet:
- Am ANFANG entsteht ein Ausschleuselement (AUSSCHLEUS_TEILEART),
- am ENDE ein Einschleuselement (EINSCHLEUS_TEILEART).
Beruehrt eine Strecke nur EINEN Kreisel (z.B. ein Foerderer, der zum selben
Kreisel zurueckfuehrt), wird dieser beiden Enden zugeordnet. Beruehrt sie
KEINEN Kreisel, entstehen keine Elemente.
Eine Strecke sollte genau zwei Kreisel-Bounding-Boxen beruehren (ein Anfang
+ ein Ende). Beruehrt ihre BBox MEHR als zwei (mit exakten 3D-BBoxen sollte
das nicht vorkommen - hier wird in x/y ohne Hoehe geprueft), entsteht eine
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).
Rueckgabe: (result, warnings)
result = Liste von dicts:
{"teileart": AUSSCHLEUS_/EINSCHLEUS_TEILEART,
"x","y","z": Kollisionsposition (mm),
"dx","dy","dz": Welt-AABB der ausgerichteten Box (mm),
"kreisel_id": TeileId des anschliessenden Kreisels,
"strecke_id": TeileId der GF/VF,
"ende_label": "Anfang" | "Ende"}
warnings = dict {strecke_teileid: warntext} fuer Strecken mit > 2
Kreisel-BBox-Beruehrungen.
"""
kreisel = {}
for it in items:
if it.get("teileart", "") == KREISEL_SPLIT_TEILEART:
kreisel[it.get("teileid", "")] = _kreisel_capsule(it, durchmesser_mm)
if dbg:
dbg("")
dbg("=== Strecken-Enden <-> Kreisel -> Ein-/Ausschleuselemente ===")
dbg(f"Echte Kreisel: {len(kreisel)}; Zuordnung ueber die BBox-Nachbarn "
f"der Strecke (naechster Kreisel je Ende), erwartet 2 Kreisel/Strecke")
result = []
warnings = {}
for it in items:
if it.get("teileart", "") not in GF_VF_TEILEARTEN:
continue
bbox = it.get("_bbox")
if not bbox:
continue
sid = it.get("teileid", "")
nb_ids = [k for k in _distinct_kreisel_ids(it.get("nachbarn", "")) if k in kreisel]
if not nb_ids:
if dbg:
dbg(f" Strecke {sid}: kein Kreisel-Nachbar -> keine Schleuselemente")
continue
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)
scz = bbox.get("cz", 0.0)
def _nearest(end_pt):
best = None
for kid in nb_ids:
cap = kreisel[kid]
p = _closest_pt_on_segment(cap["a"], cap["b"], end_pt)
d = math.hypot(p[0] - end_pt[0], p[1] - end_pt[1])
if best is None or d < best[3]:
best = (kid, cap, p, d)
return best
a_best = _nearest(anfang_end)
e_best = _nearest(ende_end)
if len(nb_ids) > 2:
genutzt = {a_best[0], e_best[0]}
extra = [k for k in nb_ids if k not in genutzt]
warnings[sid] = (
f"beruehrt {len(nb_ids)} Kreisel-BoundingBoxen "
f"(erwartet 2 = Anfang + Ende): {', '.join(nb_ids)}"
+ (f"; ueberzaehlig/ignoriert: {', '.join(extra)}" if extra else "")
+ " - mit exakten 3D-BoundingBoxen sollte das nicht auftreten")
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)):
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)
result.append({
"teileart": teileart,
"x": contact[0], "y": contact[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})")
if dbg:
dbg(f"Erzeugte Ein-/Ausschleuselemente: {len(result)}; "
f"Strecken mit Warnung (>2 Kreisel): {len(warnings)}")
return result, warnings