Weichen werden bei der Kollisionspunkten zweier Kreisel erzeugt und im csv Export mit heraus geschrieben

This commit is contained in:
2026-08-04 12:55:31 +02:00
parent 49652b2744
commit 6648156d12
2 changed files with 281 additions and 0 deletions
+76
View File
@@ -7,6 +7,14 @@ Fuer Omniflo-Elemente (Bogen/Weiche/Gerade) wird am Ende zusaetzlich eine
"Omniflo Sum"-Zeile mit Anzahl Boegen/Weichentypen und Gesamtlaenge AP110/AP60 "Omniflo Sum"-Zeile mit Anzahl Boegen/Weichentypen und Gesamtlaenge AP110/AP60
angehaengt (siehe build_omni_sum_merkmale). angehaengt (siehe build_omni_sum_merkmale).
Nach der Kollisions-/Nachbarschaftserkennung wird ausserdem an jedem
Beruehrpunkt zweier Kreisel eine "ILS Weiche"-Zeile angehaengt (siehe
build_kreisel_weiche_items / export_neighbors.compute_kreisel_touch_switches):
Anzahl 1, Position = Beruehrkoordinaten, Nachbarn = die beiden Kreisel-IDs,
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.
Aufruf: Aufruf:
python export_csv.py <export_raw.json> <data_dir> <output.csv> python export_csv.py <export_raw.json> <data_dir> <output.csv>
@@ -81,7 +89,9 @@ from export_neighbors import (
compute_neighbor_errors, compute_neighbor_errors,
compute_sensor_zuordnung, compute_sensor_zuordnung,
compute_scanner_nearest_separator, compute_scanner_nearest_separator,
compute_kreisel_touch_switches,
SENSOR_TEILEARTEN, SENSOR_TEILEARTEN,
WEICHE_TEILEART,
) )
BLOCKPATTERNS = load_patterns() BLOCKPATTERNS = load_patterns()
@@ -392,6 +402,66 @@ SKIP_BLOCKS = set(BLOCKPATTERNS.get("pattern_ks_subblocks", ["K1", "K2", "K3", "
# Bloecke verarbeiten (einfache Liste, keine Summierung) # Bloecke verarbeiten (einfache Liste, keine Summierung)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def max_numeric_teileid(items):
"""Hoechste rein-numerische TeileId in items (z.B. "0008" -> 8). Nicht-
numerische IDs (SEP-0001, autogenerated_of_json, ...) werden ignoriert.
0, wenn keine numerische ID existiert. Basis fuer die fortlaufende
Nummerierung der am Kreisel-Beruehrpunkt erzeugten ILS-Weichen (siehe
'Zaehle die Id aus der letzten enthaltenen ID hoch')."""
max_id = 0
for item in items:
tid = str(item.get("teileid", "")).strip()
if tid.isdigit():
max_id = max(max_id, int(tid))
return max_id
def build_kreisel_weiche_items(items, planquadrat_cfg, start_elem_nr, dbg=None):
"""Haengt fuer jeden Kreisel-Beruehrpunkt eine "ILS Weiche"-Zeile an items
an (siehe export_neighbors.compute_kreisel_touch_switches).
TeileId zaehlt aus der hoechsten bereits enthaltenen numerischen ID hoch
(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
Insertpoint bleiben leer; Position = Beruehrkoordinaten; Boundingbox = die
statische, an der Kreiselachse ausgerichtete Default-Box (WEICHE_BOX_*).
Rueckgabe: fortgeschriebene Elementnummer (start_elem_nr + Anzahl Weichen).
"""
switches = compute_kreisel_touch_switches(
items, NEIGHBOR_TOLERANCE_MM, durchmesser_mm=KREISEL_DURCHMESSER_MM, dbg=dbg)
naechste_id = max_numeric_teileid(items)
elem_nr = start_elem_nr
for n, sw in enumerate(switches, start=1):
elem_nr += 1
naechste_id += 1
teileid = f"{naechste_id:04d}"
planquadrat = (compute_planquadrat(sw["x"], sw["y"], planquadrat_cfg)
if planquadrat_cfg is not None else "")
items.append({
"nr": elem_nr,
"teileart": WEICHE_TEILEART,
"teileid": teileid,
"bezeichnung": f"ILS Weiche :{n}",
"planquadrat": planquadrat,
"position": f'{sw["x"]:.2f}, {sw["y"]:.2f}, {sw["z"]:.2f}',
"boundingbox": f'{sw["dx"]:.2f}, {sw["dy"]:.2f}, {sw["dz"]:.2f}',
"insertpoint": "",
"k1": "", "k2": "", "k3": "", "k4": "",
"nachbarn": ", ".join(sw["nachbarn"]),
"fehler": "",
"merkmale": {
"Erzeugt aus": "Kreisel-Beruehrpunkt",
"Kreisel A": sw["nachbarn"][0],
"Kreisel B": sw["nachbarn"][1],
"Achse von Kreisel": sw["achse_von"],
"Beruehrung": sw["beruehrung"],
},
})
return elem_nr
def process_blocks(blocks, lookup, dbg=None): def process_blocks(blocks, lookup, dbg=None):
# x_origin_mm/y_origin_mm einmalig fuer diesen Exportlauf aufloesen (siehe # x_origin_mm/y_origin_mm einmalig fuer diesen Exportlauf aufloesen (siehe
# export_planquadrat.resolve_origins): automatisch aus den Bloecken, falls # export_planquadrat.resolve_origins): automatisch aus den Bloecken, falls
@@ -655,6 +725,12 @@ def process_blocks(blocks, lookup, dbg=None):
if item.get("teileart", "") == "ILS 2.0 Scanner" and separator_id: if item.get("teileart", "") == "ILS 2.0 Scanner" and separator_id:
item["merkmale"]["Naechster Separator"] = separator_id item["merkmale"]["Naechster Separator"] = separator_id
# 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)
return items return items
+205
View File
@@ -79,6 +79,19 @@ SENSOR_TEILEARTEN = {"ILS 2.0 Separator", "ILS 2.0 Scanner"}
# Eckrad" hat keine dieser zwei Stationen und bleibt eine einzelne BBox. # Eckrad" hat keine dieser zwei Stationen und bleibt eine einzelne BBox.
KREISEL_SPLIT_TEILEART = "ILS 2.0 Kreisel" KREISEL_SPLIT_TEILEART = "ILS 2.0 Kreisel"
# TeileArt der automatisch am Kreisel-Beruehrpunkt erzeugten Weiche
# (compute_kreisel_touch_switches / export_csv.py).
WEICHE_TEILEART = "ILS Weiche"
# 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
# bildet; ausgegeben wird die achsparallele Welt-Bounding-Box dieser
# gedrehten Box (siehe _oriented_box_aabb).
WEICHE_BOX_LAENGE_MM = 250.0
WEICHE_BOX_BREITE_MM = 100.0
WEICHE_BOX_HOEHE_MM = 30.0
# 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
@@ -740,3 +753,195 @@ def compute_scanner_nearest_separator(items, dbg=None):
f"{best_id!r} (Abstand {best_dist:.0f}mm)") f"{best_id!r} (Abstand {best_dist:.0f}mm)")
result.append(best_id) result.append(best_id)
return result return result
# ---------------------------------------------------------------------------
# Kreisel-Beruehrpunkte -> ILS-Weichen
# ---------------------------------------------------------------------------
def _clamp(v, lo, hi):
return lo if v < lo else (hi if v > hi else v)
def _kreisel_capsule(item, durchmesser_mm):
"""Achsen-Segment (Kapsel) eines "ILS 2.0 Kreisel"-Blocks in Welt-
koordinaten.
Geometrie siehe kreisel_half_bboxes / KreiselInsert.lsp::draw-module: die
AN8-(Antriebs-) Stationsmitte liegt lokal bei (radius, 0), die SP8-(Spann-)
Stationsmitte bei (radius+abstand, 0), radius = durchmesser_mm/2. Beide auf
der lokalen X-Achse. Die gesamte Kreisel-Flaeche ist das um radius
"aufgeblasene" Segment zwischen diesen beiden Mitten (Stadium/Kapsel: zwei
Endkreise mit Radius radius + zwei Tangenten bei Y=+-radius).
Rueckgabe: dict {"a": (ax,ay), "b": (bx,by), "r": radius, "rot": rot_rad,
"z": z-Mitte} - a = AN8-Mitte, b = SP8-Mitte (Welt).
"""
attribs = item.get("attribs", {})
abstand = safe_float(attribs.get("ABSTAND", "0"), 0.0)
r = durchmesser_mm / 2.0
x0 = item.get("x", 0.0) or 0.0
y0 = item.get("y", 0.0) or 0.0
rot = math.radians(item.get("rotation", 0.0) or 0.0)
cos_r, sin_r = math.cos(rot), math.sin(rot)
def world(lx, ly):
return (x0 + lx * cos_r - ly * sin_r, y0 + lx * sin_r + ly * cos_r)
bbox = item.get("_bbox") or {}
z = bbox.get("cz", item.get("z", 0.0) or 0.0)
return {
"a": world(r, 0.0),
"b": world(r + abstand, 0.0),
"r": r,
"rot": rot,
"z": z,
}
def _closest_pts_segments(p1, q1, p2, q2):
"""Naechstgelegene Punkte zweier 2D-Strecken [p1,q1] und [p2,q2] (Algorithmus
aus Ericson, Real-Time Collision Detection). Rueckgabe (c1, c2, s, t) mit
c1 auf Strecke 1, c2 auf Strecke 2 und Parametern s,t in [0,1] (0=Anfang,
1=Ende) - s/t am Rand bedeutet: der naechste Punkt liegt auf dem Endkreis
(Cap), dazwischen auf der Tangentenflanke."""
eps = 1e-9
d1 = (q1[0] - p1[0], q1[1] - p1[1])
d2 = (q2[0] - p2[0], q2[1] - p2[1])
r = (p1[0] - p2[0], p1[1] - p2[1])
a = d1[0] * d1[0] + d1[1] * d1[1]
e = d2[0] * d2[0] + d2[1] * d2[1]
f = d2[0] * r[0] + d2[1] * r[1]
if a <= eps and e <= eps:
s = t = 0.0
elif a <= eps:
s = 0.0
t = _clamp(f / e, 0.0, 1.0)
else:
c = d1[0] * r[0] + d1[1] * r[1]
if e <= eps:
t = 0.0
s = _clamp(-c / a, 0.0, 1.0)
else:
b = d1[0] * d2[0] + d1[1] * d2[1]
denom = a * e - b * b
s = _clamp((b * f - c * e) / denom, 0.0, 1.0) if denom > eps else 0.0
t = (b * s + f) / e
if t < 0.0:
t = 0.0
s = _clamp(-c / a, 0.0, 1.0)
elif t > 1.0:
t = 1.0
s = _clamp((b - c) / a, 0.0, 1.0)
c1 = (p1[0] + d1[0] * s, p1[1] + d1[1] * s)
c2 = (p2[0] + d2[0] * t, p2[1] + d2[1] * t)
return c1, c2, s, t
def _oriented_box_aabb(rot_rad, laenge, breite, hoehe):
"""Achsparallele Welt-Bounding-Box (dx,dy,dz) einer Box, deren Laengsseite
(laenge) senkrecht zur Achsenrichtung rot_rad liegt und deren Breite
(breite) entlang der Achse - konsistent mit allen anderen BBox-Spalten
(Ausdehnung der gedrehten Flaeche in Welt-X/Y).
Achse = (cos rot, sin rot); Laengsseite senkrecht dazu = (-sin rot, cos rot).
"""
c = abs(math.cos(rot_rad))
s = abs(math.sin(rot_rad))
dx = laenge * s + breite * c
dy = laenge * c + breite * s
return (dx, dy, hoehe)
def compute_kreisel_touch_switches(items, tolerance_mm, durchmesser_mm=800.0, dbg=None):
"""Erzeugt fuer jedes sich beruehrende Paar echter Kreisel ("ILS 2.0
Kreisel") eine ILS-Weiche am Beruehrpunkt.
Kreisel-Modell: eine Kapsel (Stadium) - das Achsensegment AN8-Mitte ->
SP8-Mitte, aufgeblasen um r = durchmesser_mm/2 (siehe _kreisel_capsule).
Zwei Kreisel beruehren sich, wenn der minimale Abstand ihrer Achsen-
segmente <= 2r + tolerance_mm ist (2r = durchmesser_mm). Eigenstaendige,
praezise Geometriepruefung - unabhaengig vom groben Links/Rechts-AABB-
Nachbarschaftstest (compute_neighbor_ids), der nur ueberschneidende
Bounding-Boxen, nicht den exakten Beruehrpunkt liefert.
Beruehrpunkt (= Weichen-Position) = Mittelpunkt der beiden naechstgelegenen
Segmentpunkte. Die Weichen-Box (WEICHE_BOX_*) wird mit ihrer Laengsseite
senkrecht zur Achse desjenigen Kreisels ausgerichtet, dessen beruehrendes
Merkmal ein Endkreis ist (naechster Segmentpunkt am Segmentende). Beruehren
sich zwei Endkreise oder zwei Tangentenflanken, wird der erstgenannte
Kreisel des Paares (kleinerer items-Index) genommen. Ausgegeben wird die
achsparallele Welt-Bounding-Box der gedrehten Box (siehe _oriented_box_aabb).
Rueckgabe: Liste von dicts (in items-Reihenfolge der Paare), je erzeugte
Weiche:
{"x","y","z": Beruehrpunkt (mm),
"dx","dy","dz": Welt-AABB der ausgerichteten Box (mm),
"nachbarn": [teileid_a, teileid_b],
"achse_von": teileid des orientierungsgebenden Kreisels,
"beruehrung": "Kreis/Tangente" | "Kreis/Kreis" | "Tangente/Tangente"}
"""
eps = 1e-6
kreisel = []
for item in items:
if item.get("teileart", "") != KREISEL_SPLIT_TEILEART:
continue
kreisel.append((item.get("teileid", ""), _kreisel_capsule(item, durchmesser_mm)))
if dbg:
dbg("")
dbg("=== Kreisel-Beruehrpunkte -> ILS-Weichen ===")
dbg(f"Echte Kreisel ('{KREISEL_SPLIT_TEILEART}'): {len(kreisel)}; "
f"Beruehrschwelle Abstand <= {durchmesser_mm + tolerance_mm:.0f}mm "
f"(2r={durchmesser_mm:.0f} + Toleranz {tolerance_mm:.0f})")
switches = []
for i in range(len(kreisel)):
id_a, cap_a = kreisel[i]
for j in range(i + 1, len(kreisel)):
id_b, cap_b = kreisel[j]
c1, c2, s, t = _closest_pts_segments(cap_a["a"], cap_a["b"],
cap_b["a"], cap_b["b"])
dist = math.hypot(c1[0] - c2[0], c1[1] - c2[1])
schwelle = cap_a["r"] + cap_b["r"] + tolerance_mm
if dist > schwelle:
if dbg:
dbg(f" {id_a} <-> {id_b}: kein Kontakt "
f"(Achsabstand {dist:.0f}mm > {schwelle:.0f}mm)")
continue
# Beruehrpunkt = Mitte der beiden naechstgelegenen Segmentpunkte
tx = (c1[0] + c2[0]) / 2.0
ty = (c1[1] + c2[1]) / 2.0
tz = (cap_a["z"] + cap_b["z"]) / 2.0
# Endkreis (Cap) beruehrt, wenn der naechste Segmentpunkt am Ende
# liegt (s bzw. t == 0 oder 1); dazwischen Tangentenflanke.
cap1 = s <= eps or s >= 1.0 - eps
cap2 = t <= eps or t >= 1.0 - eps
if cap1 and not cap2:
achse_id, achse_rot, beruehrung = id_a, cap_a["rot"], "Kreis/Tangente"
elif cap2 and not cap1:
achse_id, achse_rot, beruehrung = id_b, cap_b["rot"], "Kreis/Tangente"
else:
achse_id, achse_rot = id_a, cap_a["rot"]
beruehrung = "Kreis/Kreis" if cap1 else "Tangente/Tangente"
dx, dy, dz = _oriented_box_aabb(
achse_rot, WEICHE_BOX_LAENGE_MM, WEICHE_BOX_BREITE_MM, WEICHE_BOX_HOEHE_MM)
switches.append({
"x": tx, "y": ty, "z": tz,
"dx": dx, "dy": dy, "dz": dz,
"nachbarn": [id_a, id_b],
"achse_von": achse_id,
"beruehrung": beruehrung,
})
if dbg:
dbg(f" {id_a} <-> {id_b}: BERUEHRUNG ({beruehrung}, Achsabstand "
f"{dist:.0f}mm) -> Weiche bei ({tx:.0f}, {ty:.0f}, {tz:.0f}), "
f"Box-AABB ({dx:.0f}, {dy:.0f}, {dz:.0f}), Achse von Kreisel {achse_id}")
if dbg:
dbg(f"Erzeugte ILS-Weichen: {len(switches)}")
return switches