export_csv: AS/ES direkt aus ZZ_ASES_MARKER klassifizieren, Kreisel<->Strecke-Kollisionserzeugung entfernen

Stufe 2 (Python-Seite von "sepliste: Bauteil-IDs eintragen"): AS/ES-Kettenenden
sind jetzt ganz normale Items, direkt aus dem dauerhaften ZZ_ASES_MARKER
klassifiziert (TeileId/Position/Boundingbox vom Marker-Block selbst) - keine
Kollisionsberechnung mehr, die sie erfindet.

- lib/export_neighbors.py: compute_strecke_kreisel_schleus entfernt (suchte
  bisher per Kollisionstest den anschliessenden Kreisel und erzeugte dabei bei
  JEDEM Export eine neue TeileId fuer denselben physischen Punkt). AUSSCHLEUS_/
  EINSCHLEUS_TEILEART sind jetzt Teil von KREISEL_UMLAUF_TEILEARTEN - ein AS/ES
  bringt seine eigene Bounding-Box vom Marker mit und wird dadurch, wie jeder
  freie Separator, ueber die normale BBox-Ueberschneidung einem Kreisel
  zugeordnet; die gesonderte "naechster Kreisel per sep_kette"-Suche in
  compute_kreisel_umlauf (dessen sep_kette-Parameter damit entfaellt) ist
  ueberfluessig geworden. compute_sep_kette gibt je Eintrag zusaetzlich das
  Feld "id" weiter (Bauteil-TeileId, siehe vorheriger Commit).
- lib/export_csv.py: process_blocks klassifiziert ZZ_ASES_MARKER direkt (TYP-
  Attribut waehlt Ausschleus-/Einschleuselement); build_strecke_schleus_items
  entfaellt komplett. map_separator_kette_items -> map_sepliste_kette_items:
  ordnet jetzt AS/SEP/ES exakt per ID-Gleichheit zu (sepliste-Feld "id" gegen
  die TeileId der klassifizierten Items) statt per Koordinaten-Naehe - SEP
  faellt bei fehlendem "id"-Feld weiterhin auf den alten Naehe-Abgleich zurueck
  (Altbestand), AS/ES ohne "id" bleiben unaufgeloest.
- Neu: pruefe_ases_in_beiden_containern prueft am Ende von process_blocks, ob
  jede AS-/ES-Zeile sowohl in den TrackIds ihrer eigenen Strecke als auch in
  denen mindestens eines Kreisels auftaucht, und traegt bei Abweichung eine
  Warnung ein (Spalte "Warnungen") - Konsistenzpruefung fuer genau die beiden
  "Container", zu denen ein AS/ES gehoert.
- Aufgeraeumt (tote Routinen): _strecke_ends, _closest_pt_on_segment,
  _distinct_kreisel_ids (nur von den entfernten Funktionen genutzt), sowie der
  temporaere Diagnose-Filter DEBUG_HIGHLIGHT_TEILEIDS/dbg_highlight (an die
  jetzt behobene Koordinaten-Matching-Ambiguitaet gebunden, in seinem eigenen
  Kommentar bereits als Wegwerf-Code markiert). LISP-seitiger Dead-Code-Scan
  (export.lsp, ssg_ks_insert.lsp - die diese Woche bearbeiteten Module):
  keine Funde.

Verifiziert an den echten HundM05-Daten (results/export_raw.json, 45 Bloecke):
TeileId-Bereich luecken- und duplikatfrei 1-45 (vorher 1-55 mit Luecke 24-33 +
Zusatz-IDs 46-55), alle 10 AS/ES-IDs korrekt in ihrer Strecke UND im Kreisel
06 TrackIds, keine Warnungen der neuen Pruefung. Zwei Testdateien auf die neue
Architektur umgestellt (test_export_kreisel_umlauf.py: AS/ES als Item statt
Kollisions-Erzeugung, neue Klasse TestAsEsImKreiselUmlauf fuer die BBox-
Teilnahme; test_export_separatorliste.py: id-basierte Zuordnung, Klasse
TestMarkerIdWiederverwendung durch TestMapSeplisteKetteItemsFallback ersetzt
- die alte testete inzwischen entfernte Mint-Logik). Volle Suite: 224 bestanden
(4 vorbestehende, unabhaengige Fixture-Failures unveraendert).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-09-22 14:48:45 +02:00
co-authored by Claude Sonnet 5
parent 719ffc2385
commit ab3958e0e4
5 changed files with 543 additions and 793 deletions
+50 -276
View File
@@ -88,26 +88,21 @@ SENSOR_TEILEARTEN = {"ILS 2.0 Separator", "ILS 2.0 Scanner"}
# pattern_ausgang in cfg/export.cfg) werden hier wie BTMT als frei platzierte
# Umlauf-Punkte gefuehrt - so tauchen sie, wenn sie auf einer Kreiselbahn
# liegen, in dessen "TrackIds"-Spalte auf (Nutzeranforderung: wie
# Separator/AS/ES behandeln). Die TeileArt-Strings sind bewusst OHNE "ILS 2.0"-
# Praefix (siehe export_csv.py) - dieselben Strings muessen hier stehen.
# Separator/AS/ES behandeln). AUSSCHLEUS_TEILEART/EINSCHLEUS_TEILEART (AS/ES-
# Kettenenden, siehe unten): seit sie als dauerhafter ZZ_ASES_MARKER real in
# der Zeichnung stehen (statt wie frueher nur ueber eine Kreisel<->Strecke-
# Kollisionsberechnung synthetisiert zu werden, siehe git-Historie
# compute_strecke_kreisel_schleus), reicht ihre EIGENE Bounding-Box fuer die
# ganz normale BBox-Ueberschneidung mit einem Kreisel - keine gesonderte
# Naeherungs-/Kollisionslogik mehr noetig (siehe compute_kreisel_umlauf, das
# vorher zusaetzlich sep_kette nach der naechstgelegenen Kreisel-Achse absuchte).
# Die TeileArt-Strings sind bewusst OHNE "ILS 2.0"-Praefix (siehe
# export_csv.py) - dieselben Strings muessen hier stehen.
KREISEL_UMLAUF_TEILEARTEN = {
"ILS 2.0 Separator", "ILS 2.0 BTMT Beladung", "ILS 2.0 BTMT Entladung", "ILS Weiche",
"Eingang", "Ausgang",
"Eingang", "Ausgang", "ILS Ausschleuselement", "ILS Einschleuselement",
}
# Temporaerer Diagnose-Filter fuers Debuglog (siehe [Nachbarschaft] debug_log
# in cfg/export.cfg): markiert Zeilen zu diesen TeileIds im .dbg-Log
# besonders, um die fehlende Nachbarn-/TrackIds-Zuordnung einzelner
# Separatoren (0025/0029/0031/0035) nachzuvollziehen. Nach Abschluss der
# Diagnose wieder auf einen leeren Set zuruecksetzen oder entfernen.
DEBUG_HIGHLIGHT_TEILEIDS = {"0025", "0029", "0031", "0035"}
def dbg_highlight(teileid):
"""Suffix fuer eine Debuglog-Zeile, wenn teileid in
DEBUG_HIGHLIGHT_TEILEIDS steht - sonst leerer String."""
return " <<<< ZIEL-ID" if str(teileid) in DEBUG_HIGHLIGHT_TEILEIDS else ""
# Nur "ILS 2.0 Kreisel" (echte Kreisel-Bloecke mit AN8/Antrieb + SP8/Spann-
# station + ABSTAND-Attribut) werden links/rechts gesplittet - "ILS 2.0
# Eckrad" hat keine dieser zwei Stationen und bleibt eine einzelne BBox.
@@ -117,9 +112,10 @@ 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).
# TeileArten der Ein-/Ausschleuselemente an den Strecken-Enden (AS/ES) einer
# Gefaellestrecke/eines Foerderers - siehe process_blocks (export_csv.py):
# direkt aus dem dauerhaften ZZ_ASES_MARKER klassifiziert (dessen Attribut
# TYP="AS"/"ES" waehlt die TeileArt), keine gesonderte Erzeugung mehr hier.
AUSSCHLEUS_TEILEART = "ILS Ausschleuselement"
EINSCHLEUS_TEILEART = "ILS Einschleuselement"
@@ -1096,39 +1092,12 @@ def compute_kreisel_touch_switches(items, tolerance_mm, durchmesser_mm=800.0,
# ---------------------------------------------------------------------------
# Kreisel <-> Strecken-Enden -> Ein-/Ausschleuselemente
# Kreisel <-> Strecken-Enden: verfeinerte Kollisionsbox fuer compute_neighbor_
# ids (die "Nachbarn"-Spalte einer GF/VF-Strecke soll nur an ihrem echten AS-/
# ES-Ende auf einen Kreisel reagieren, nicht auf die gesamte, oft lange
# Wrapper-Bounding-Box).
# ---------------------------------------------------------------------------
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 _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
@@ -1177,188 +1146,6 @@ def _asel_collision_boxes(item, half_tol, box_dims):
return boxes
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,
element_box_mm=None, 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 = das AS-/ES-Koordinatensystem (K1/K2, siehe
csv:vfgf-k-kos-strings), um eine HALBE Box (Box-Breite/2 entlang der
Foerderachse, siehe element_box_mm) nach aussen versetzt: das
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)
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.
"""
if element_box_mm is None:
element_box_mm = load_element_box_mm()
box_l, box_b, box_h = element_box_mm
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)
# 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)
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]}")
# Versatz der Schleuselement-Position gegenueber dem AS/ES-KOS: eine
# HALBE Box entlang der Foerderachse, jeweils NACH AUSSEN (weg von der
# 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
pos = (end_pt[0] + vorzeichen * axis_u[0] * halbe_box,
end_pt[1] + vorzeichen * axis_u[1] * halbe_box)
dx, dy, dz = _oriented_box_aabb(achs_rot, box_l, box_b, box_h)
result.append({
"teileart": teileart,
"x": pos[0], "y": pos[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 ({pos[0]:.0f}, {pos[1]:.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:
dbg(f"Erzeugte Ein-/Ausschleuselemente: {len(result)}; "
f"Strecken mit Warnung (>2 Kreisel): {len(warnings)}")
return result, warnings
# ---------------------------------------------------------------------------
# Sepliste (LISP-XDATA, siehe ssg-sepliste-xdata-schreiben) -> Nachbarschaft
# ---------------------------------------------------------------------------
@@ -1376,15 +1163,19 @@ def compute_sep_kette(items, dbg=None):
Jeder Sepliste-Eintrag bekommt eine (innerhalb dieser Kette eindeutige)
Schluessel-ID "<strecke_teileid>#<lfdnr>" - das ist die ID, mit der
build_strecke_schleus_items/map_separator_kette_items die spaeteren
globalen TeileIds verknuepft (siehe export_csv.py). Vorgaenger/Nachfolger
sind schlicht der vorherige/naechste Eintrag in der Sepliste (die bereits
in Baureihenfolge vorliegt) - kein Rateverfahren noetig.
map_sepliste_kette_items die spaeteren globalen TeileIds verknuepft
(siehe export_csv.py). Vorgaenger/Nachfolger sind schlicht der vorherige/
naechste Eintrag in der Sepliste (die bereits in Baureihenfolge vorliegt)
- kein Rateverfahren noetig.
Rueckgabe: dict {key: {"typ", "x", "y", "strecke_id", "vorgaenger",
Rueckgabe: dict {key: {"typ", "x", "y", "id", "strecke_id", "vorgaenger",
"nachfolger"}} - vorgaenger/nachfolger sind wieder solche keys, oder None
am jeweiligen Kettenende (Verkettung mit dem Kreisel-Umlauf uebernimmt
compute_kreisel_umlauf).
compute_kreisel_umlauf). "id" ist die TeileId des Bauteils, das diesen
Eintrag in der Zeichnung vertritt (AS/ES ueber den dauerhaften
ZZ_ASES_MARKER, SEP ueber die beim Export ausgepackte temporaere
Separator_SP-Kopie - siehe csv:sepliste-ids-setzen, Lisp/export.lsp) -
leerer String, wenn (noch) keine ID zugewiesen wurde (Altbestand).
"""
result = {}
for item in items:
@@ -1401,6 +1192,7 @@ def compute_sep_kette(items, dbg=None):
"typ": entry.get("typ", ""),
"x": entry.get("x", 0.0),
"y": entry.get("y", 0.0),
"id": str(entry.get("id", "")).strip(),
"strecke_id": sid,
"vorgaenger": keys[i - 1] if i > 0 else None,
"nachfolger": keys[i + 1] if i < len(keys) - 1 else None,
@@ -1411,16 +1203,18 @@ def compute_sep_kette(items, dbg=None):
return result
def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
def compute_kreisel_umlauf(items, tolerance_mm, durchmesser_mm=800.0,
touch_switches=None, dbg=None):
"""Bringt fuer jeden echten Kreisel (KREISEL_SPLIT_TEILEART) alle dort
liegenden Punkte - frei platzierte Separatoren UND BTMT-Beladung/
-Entladung (KREISEL_UMLAUF_TEILEARTEN, per BBox-Ueberschneidung
wie compute_sensor_zuordnung einer Kreiselhaelfte zugeordnet), die AS/ES-
Enden aller andockenden VF_n/GF_n-Ketten (aus sep_kette, per BBox-Naehe
wie compute_strecke_kreisel_schleus zugeordnet) UND die Kreisel-Kreisel-
Weichen (touch_switches, siehe compute_kreisel_touch_switches) - in eine
geschlossene Umlauf-Reihenfolge (zirkulaer, siehe DREHRICHTUNG-Attribut).
liegenden Punkte - frei platzierte Separatoren, BTMT-Beladung/-Entladung
UND die AS/ES-Enden aller andockenden VF_n/GF_n-Ketten (alle zusammen
KREISEL_UMLAUF_TEILEARTEN, einheitlich per BBox-Ueberschneidung einer
Kreiselhaelfte/eines Kreisels zugeordnet - wie compute_sensor_zuordnung;
ein AS/ES bringt seine eigene Bounding-Box vom dauerhaften ZZ_ASES_MARKER
mit, seit es als reales Item existiert - keine gesonderte Naeherungslogik
mehr noetig) UND die Kreisel-Kreisel-Weichen (touch_switches, siehe
compute_kreisel_touch_switches) - in eine geschlossene Umlauf-Reihenfolge
(zirkulaer, siehe DREHRICHTUNG-Attribut).
Eine Kreisel-Kreisel-Weiche liegt (anders als ein Separator oder ein
AS/ES-Ende) auf GENAU EINEM Punkt, gehoert aber zu ZWEI Kreiseln - man
@@ -1441,17 +1235,15 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
"TrackIds"-Spalte (siehe export_csv.py write_kreisel_separatorliste_merkmale).
items = dieselbe Liste wie bei compute_neighbor_ids/compute_sensor_
zuordnung. sep_kette = Rueckgabe von compute_sep_kette (fuer die AS/ES-
Punkte: x/y + strecke_id). touch_switches = Rueckgabe von
compute_kreisel_touch_switches (None/leer = wie bisher ohne Weichen im
Umlauf).
zuordnung. touch_switches = Rueckgabe von compute_kreisel_touch_switches
(None/leer = wie bisher ohne Weichen im Umlauf).
Rueckgabe: dict {kreisel_teileid: [(punkt_key, vorgaenger_key,
nachfolger_key), ...]} in Umlaufreihenfolge, beginnend bei Winkel 0 Grad -
punkt_key ist die TeileId eines freien Punkts (Separator oder BTMT-
Beladung/-Entladung, siehe KREISEL_UMLAUF_TEILEARTEN), ein
sep_kette-Schluessel (AS/ES-Enden) oder ein touch_switches-"key" (Kreisel-
Kreisel-Weiche). Kreisel ohne zugeordnete Punkte liefern eine leere Liste.
punkt_key ist die TeileId eines freien Punkts (Separator, BTMT-Beladung/
-Entladung ODER AS/ES-Ende, siehe KREISEL_UMLAUF_TEILEARTEN) oder ein
touch_switches-"key" (Kreisel-Kreisel-Weiche). Kreisel ohne zugeordnete
Punkte liefern eine leere Liste.
"""
half_tol = tolerance_mm / 2.0
kreisel = {}
@@ -1498,8 +1290,7 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
if not bbox:
if dbg:
dbg(f" Umlauf-Kandidat {teileid!r} ({item.get('teileart', '')!r}): "
f"KEINE Bounding-Box - kann keinem Kreisel zugeordnet werden"
f"{dbg_highlight(teileid)}")
f"KEINE Bounding-Box - kann keinem Kreisel zugeordnet werden")
continue
bounds = _bounds(bbox, half_tol)
matched_tid = None
@@ -1513,28 +1304,11 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
if matched_tid is not None:
dbg(f" Umlauf-Kandidat {teileid!r} ({item.get('teileart', '')!r}) "
f"bbox(cx={bbox.get('cx', 0):.0f},cy={bbox.get('cy', 0):.0f}) "
f"-> Kreisel {matched_tid}{dbg_highlight(teileid)}")
f"-> Kreisel {matched_tid}")
else:
dbg(f" Umlauf-Kandidat {teileid!r} ({item.get('teileart', '')!r}) "
f"bbox(cx={bbox.get('cx', 0):.0f},cy={bbox.get('cy', 0):.0f}) "
f"-> KEIN Kreisel in Reichweite (bounds={_fmt_bounds(bounds)})"
f"{dbg_highlight(teileid)}")
# AS/ES-Enden jeder Kette mit sepliste: naechstgelegener Kreisel (wie
# compute_strecke_kreisel_schleus._nearest), aber direkt aus sep_kette
# statt ueber die grobe BBox-Nachbarschaft der Strecke.
for key, eintrag in sep_kette.items():
if eintrag["typ"] not in ("AS", "ES"):
continue
p = (eintrag["x"], eintrag["y"])
best_tid, best_d = None, None
for tid, (cap, _dr) in kreisel.items():
q = _closest_pt_on_segment(cap["a"], cap["b"], p)
d = math.hypot(q[0] - p[0], q[1] - p[1])
if best_d is None or d < best_d:
best_tid, best_d = tid, d
if best_tid is not None:
punkte_je_kreisel[best_tid].append((key, eintrag["x"], eintrag["y"]))
f"-> KEIN Kreisel in Reichweite (bounds={_fmt_bounds(bounds)})")
# Kreisel-Kreisel-Weichen: die beiden beruehrenden Kreisel-IDs stehen
# bereits in sw["nachbarn"] (siehe compute_kreisel_touch_switches) - kein
@@ -1623,6 +1397,6 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
f"{' -> '.join(p[0] for p in sortiert)}")
for punkt_key, vorgaenger, nachfolger in umlauf:
dbg(f" Punkt {punkt_key!r}: vorgaenger={vorgaenger!r} "
f"nachfolger={nachfolger!r}{dbg_highlight(punkt_key)}")
f"nachfolger={nachfolger!r}")
return result