sepliste: Bauteil-IDs eintragen, AS/ES ueber dauerhaften Marker sichtbar machen

Stufe 1 (LISP-Export-Pfad): export_raw.json enthaelt jetzt fuer jeden
sepliste-Eintrag (AS/SEP/ES) einer VF_n/GF_n-Kette die ID des realen
Bauteils, das ihn vertritt.

- csv:sep-proxies-erzeugen (Separatoren) und die neuen AS/ES-Marker teilen
  sich csv:nested-welt-records/csv:block-einfuegen-welt zur Platzierung.
- csv:sepliste-paare ordnet sepliste-Eintraege ihrem Bauteil exklusiv nach
  Abstand zu (kuerzester zuerst, dann verbraucht) statt "jeder Eintrag
  nimmt sich das naechste" - ein einzelner fehlerhafter Koordinatenwert
  (bekannter Bug: gespiegeltes x bei lfdnr=2) verschiebt dadurch keine
  andere Zuordnung mehr.
- Neu: ZZ_ASES_MARKER - ein kleines Rechteck an jedem AS/ES-Kettenende,
  das nach dem Export STEHEN BLEIBT (anders als die temporaeren
  Separator-Kopien). Macht das sonst unsichtbare, in der Wrapper-
  Blockdefinition verpackte Kettenende in BricsCAD sichtbar/anklickbar
  und traegt seine ID unsichtbar am Attribut ID - dadurch behaelt ein
  AS/ES seine TeileId ueber Exportlaeufe hinweg. Wird bei jedem Export
  neu aufgebaut (Lage folgt der Kette), Wiederfund ueber ZUORDNUNG+TYP+
  LFDNR, verwaiste Marker werden entfernt.

In BricsCAD verifiziert (5 HundM05-Ketten, zwei Exportlaeufe: gleiche
Marker-IDs, korrekte sepliste-"id"-Felder). Dabei einen realen Bug
gefunden und behoben: die Blockdefinition des Markers uebernahm ueber
einen ungefilterten entnext-Walk versehentlich auch die Attribute des
zuletzt eingefuegten Blocks (VF_5 verlor dadurch alle Attribute inkl.
ID) - jetzt auf LWPOLYLINE/ATTDEF gefiltert.

9 neue zeichnungsfreie Unit-Tests (csv:sepliste-paare/*, csv:asmark-
finden/*, csv:proxy-typ/*), 4 neue de/en-Textschluessel (Paritaet
geprueft). bin\run_tests.bat --lisp: 257/257 bestanden.

Noch nicht enthalten (Stufe 2, Python): map_separator_kette_items nutzt
das neue "id"-Feld noch nicht, build_strecke_schleus_items synthetisiert
AS/ES-Zeilen weiterhin selbst statt die exportierten zu lesen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-09-22 13:09:05 +02:00
co-authored by Claude Sonnet 5
parent 9671fe4e04
commit 618ff3d86c
15 changed files with 1205 additions and 216 deletions
+103 -8
View File
@@ -44,6 +44,7 @@ from export_neighbors import (
compute_strecke_kreisel_schleus,
compute_sep_kette,
compute_kreisel_umlauf,
dbg_highlight,
SENSOR_TEILEARTEN,
WEICHE_TEILEART,
GF_VF_TEILEARTEN,
@@ -683,9 +684,30 @@ def map_separator_kette_items(items, sep_kette, sep_kette_by_key=None, dbg=None)
if zuordnung:
sep_items_by_zuordnung.setdefault(zuordnung, []).append(it)
if dbg:
dbg("=== map_separator_kette_items: Kandidatenpools je Zuordnung ===")
for zuordnung, kandidaten in sep_items_by_zuordnung.items():
dbg(f" Zuordnung {zuordnung!r}: "
f"{[k.get('teileid', '') for k in kandidaten]}")
dbg("")
dbg("=== map_separator_kette_items: sepliste-SEP -> Separator-Item ===")
# id(item) -> Liste der sep_kette-Keys, die genau dieses Item als
# naechsten Kandidaten gewaehlt haben - Diagnose der NICHT-exklusiven
# Zuordnung (jeder Key sucht unabhaengig im vollen Pool, siehe Docstring):
# waehlen zwei Keys denselben Kandidaten, bleibt ein anderer Kandidat im
# selben Pool ohne jeden sep_kette-Eintrag und damit ohne Nachbarn/TrackIds.
gewaehlt_von = {}
# Zuordnungen, fuer die es ueberhaupt sepliste-SEP-Schluessel gibt. Nur in
# DIESEN Pools ist ein nicht gewaehlter Kandidat auffaellig; frei auf einem
# Kreisel platzierte Separatoren tragen keine sepliste und bekommen ihre
# Nachbarn aus dem Kreisel-Umlauf (siehe resolve_umlauf_und_sepliste_nachbarn).
zuordnungen_mit_sepliste = set()
for key, eintrag in sep_kette.items():
if eintrag.get("typ") != "SEP":
continue
zuordnungen_mit_sepliste.add(eintrag["strecke_id"])
kandidaten = sep_items_by_zuordnung.get(eintrag["strecke_id"], [])
if not kandidaten:
if dbg:
@@ -694,18 +716,48 @@ def map_separator_kette_items(items, sep_kette, sep_kette_by_key=None, dbg=None)
continue
# Naechster INSERT-Punkt zur sepliste-Koordinate.
sx, sy = eintrag.get("x", 0.0), eintrag.get("y", 0.0)
best = min(kandidaten,
key=lambda it: math.hypot(it.get("_x", 0.0) - sx,
it.get("_y", 0.0) - sy))
entfernungen = sorted(
((math.hypot(it.get("_x", 0.0) - sx, it.get("_y", 0.0) - sy), it)
for it in kandidaten),
key=lambda p: p[0])
best_d, best = entfernungen[0]
sep_kette_by_key[key] = best
gewaehlt_von.setdefault(id(best), []).append(key)
if dbg:
dbg(f" sepliste-SEP {key} -> Separator-Item TeileId "
f"{best.get('teileid', '')!r}")
rangliste = ", ".join(f"{it.get('teileid', '')!r}={d:.0f}mm"
for d, it in entfernungen)
dbg(f" sepliste-SEP {key} (x={sx:.0f},y={sy:.0f}) -> Separator-Item "
f"TeileId {best.get('teileid', '')!r} (d={best_d:.0f}mm); "
f"Kandidaten sortiert: [{rangliste}]"
f"{dbg_highlight(best.get('teileid', ''))}")
if dbg:
dbg("")
mehrfach = {iid: keys for iid, keys in gewaehlt_von.items() if len(keys) > 1}
if mehrfach:
dbg(" WARNUNG: folgende Separator-Items wurden von MEHREREN "
"sepliste-Keys unabhaengig als naechster Kandidat gewaehlt "
"(nicht-exklusive Zuordnung):")
for keys in mehrfach.values():
teileid = sep_kette_by_key[keys[0]].get("teileid", "")
dbg(f" TeileId {teileid!r} gewaehlt von Keys: {keys}"
f"{dbg_highlight(teileid)}")
gewaehlte_ids = set(gewaehlt_von.keys())
for zuordnung, kandidaten in sep_items_by_zuordnung.items():
if zuordnung not in zuordnungen_mit_sepliste:
continue
for it in kandidaten:
if id(it) not in gewaehlte_ids:
teileid = it.get("teileid", "")
dbg(f" Separator-Item TeileId {teileid!r} (Zuordnung "
f"{zuordnung!r}) wurde von KEINEM sepliste-Key gewaehlt "
f"- bleibt ohne sep_kette-Eintrag, damit ohne Nachbarn/"
f"TrackIds{dbg_highlight(teileid)}")
return sep_kette_by_key
def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
sep_kette_by_key, weiche_by_key):
sep_kette_by_key, weiche_by_key, dbg=None):
"""Loest die "Nachbarn"-Spalte aller Zeilen auf, die build_kreisel_weiche_items/
build_strecke_schleus_items/map_separator_kette_items zunaechst leer
gelassen (bzw. deren Item nur registriert) haben, weil ihr(e) Nachbar(n)
@@ -740,13 +792,27 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
if it.get("teileart", "") in KREISEL_UMLAUF_TEILEARTEN}
alle_schluessel = {**sep_kette_by_key, **weiche_by_key, **freipunkt_by_id}
if dbg:
dbg("=== resolve_umlauf_und_sepliste_nachbarn ===")
dbg(f" freipunkt_by_id: {sorted(freipunkt_by_id.keys())}")
dbg(f" sep_kette_by_key: "
f"{[(k, v.get('teileid', '')) for k, v in sep_kette_by_key.items()]}")
dbg(f" weiche_by_key: "
f"{[(k, v.get('teileid', '')) for k, v in weiche_by_key.items()]}")
dbg("")
dbg(" -- Pass 1: aus kreisel_umlauf --")
# Freie Separatoren, Weichen UND die AS/ES-Enden andockender Ketten
# bekommen ihre "Nachbarn"-Spalte aus dem Kreisel-Umlauf.
if kreisel_umlauf:
for punkte in kreisel_umlauf.values():
for kreisel_tid, punkte in kreisel_umlauf.items():
for punkt_key, vorgaenger, nachfolger in punkte:
item = alle_schluessel.get(punkt_key)
if item is None:
if dbg:
dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r}: "
f"KEIN Item in alle_schluessel gefunden - "
f"uebersprungen{dbg_highlight(punkt_key)}")
continue
nachbarn = []
for nachbar_key in (vorgaenger, nachfolger):
@@ -755,8 +821,26 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
nachbar_item = alle_schluessel.get(nachbar_key)
if nachbar_item is not None:
nachbarn.append(nachbar_item.get("teileid", ""))
elif dbg:
dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r}: "
f"Nachbar-Key {nachbar_key!r} nicht in alle_schluessel "
f"aufloesbar{dbg_highlight(item.get('teileid', ''))}")
teileid = item.get("teileid", "")
if nachbarn:
item["nachbarn"] = ", ".join(nachbarn)
if dbg:
dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r} "
f"(TeileId {teileid!r}): nachbarn={nachbarn}"
f"{dbg_highlight(teileid)}")
elif dbg:
dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r} "
f"(TeileId {teileid!r}): KEINE Nachbarn aufgeloest "
f"(vorgaenger={vorgaenger!r}, nachfolger={nachfolger!r})"
f"{dbg_highlight(teileid)}")
if dbg:
dbg("")
dbg(" -- Pass 2: ketteninterne sepliste-Nachbarn --")
# Ketteninterne sepliste-Nachbarn (SEP <-> SEP, SEP <-> AS/ES) - das
# jeweilige Kettenende (vorgaenger/nachfolger ist None, z.B. das AS-Ende
@@ -765,6 +849,10 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
for key, eintrag in sep_kette.items():
item = sep_kette_by_key.get(key)
if item is None:
if dbg:
dbg(f" sepliste-Key {key!r} (typ={eintrag.get('typ')!r}): "
f"KEIN Item in sep_kette_by_key - uebersprungen "
f"(siehe map_separator_kette_items){dbg_highlight(key)}")
continue
nachbarn = []
for nachbar_key in (eintrag.get("vorgaenger"), eintrag.get("nachfolger")):
@@ -773,8 +861,15 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
nachbar_item = alle_schluessel.get(nachbar_key)
if nachbar_item is not None:
nachbarn.append(nachbar_item.get("teileid", ""))
teileid = item.get("teileid", "")
if nachbarn:
item["nachbarn"] = ", ".join(nachbarn)
if dbg:
dbg(f" sepliste-Key {key!r} (TeileId {teileid!r}): "
f"vorgaenger={eintrag.get('vorgaenger')!r} "
f"nachfolger={eintrag.get('nachfolger')!r} -> "
f"nachbarn={nachbarn} item.nachbarn={item.get('nachbarn', '')!r}"
f"{dbg_highlight(teileid)}")
def write_separatorliste_merkmale(items, sep_kette_by_key, dbg=None):
@@ -1333,7 +1428,7 @@ def process_blocks(blocks, lookup, dbg=None):
write_separatorliste_merkmale(items, sep_kette_by_key, dbg=dbg)
resolve_umlauf_und_sepliste_nachbarn(
items, kreisel_umlauf, sep_kette, sep_kette_by_key, weiche_by_key)
items, kreisel_umlauf, sep_kette, sep_kette_by_key, weiche_by_key, dbg=dbg)
# Spalte "TrackIds" je Kreisel: dieselben Umlauf-Punkte (freie
# Separatoren, AS/ES-Enden, Kreisel-Kreisel-Weichen), die oben schon die
+34 -1
View File
@@ -95,6 +95,19 @@ KREISEL_UMLAUF_TEILEARTEN = {
"Eingang", "Ausgang",
}
# 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.
@@ -1480,15 +1493,32 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
for item in items:
if item.get("teileart", "") not in KREISEL_UMLAUF_TEILEARTEN:
continue
teileid = item.get("teileid", "")
bbox = item.get("_bbox")
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)}")
continue
bounds = _bounds(bbox, half_tol)
matched_tid = None
for tid, kb in kreisel_bounds.items():
if _overlaps(bounds, kb):
punkte_je_kreisel[tid].append(
(item.get("teileid", ""), bbox.get("cx", 0.0), bbox.get("cy", 0.0)))
(teileid, bbox.get("cx", 0.0), bbox.get("cy", 0.0)))
matched_tid = tid
break
if dbg:
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)}")
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
@@ -1591,5 +1621,8 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
if dbg:
dbg(f" Kreisel {tid} ({drehrichtung}): Umlauf "
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)}")
return result