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:
co-authored by
Claude Sonnet 5
parent
719ffc2385
commit
ab3958e0e4
+50
-276
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user