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