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:
@@ -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