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:
+90
-4
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user