fix(export): freie Separatoren nach Lage in die Kette ihrer Strecke einsortieren
Eigenstaendige Separatoren, deren Zuordnung eine VF/GF nennt, die aber nicht beim Bau der Strecke entstanden, fehlten in deren sepliste und damit in TrackIds und Nachbarn (Mubea VF_3: 0037/0038). Sie werden jetzt beim Export nach Lage zwischen AS und ES aufgenommen - nur im Speicher des Laufs, die sepliste-XDATA bleibt Bauprotokoll (Lisp paart sie per Anzahl je Typ mit den Bausteinen und fuehrt reservierte AS/ES-IDs unter der lfdnr). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,7 @@ from export_neighbors import (
|
||||
compute_kreisel_touch_switches,
|
||||
lisp_touch_switches,
|
||||
load_lisp_kreisel_weichen,
|
||||
einsortieren_freie_separatoren,
|
||||
compute_sep_kette,
|
||||
compute_kreisel_umlauf,
|
||||
SENSOR_TEILEARTEN,
|
||||
@@ -1543,6 +1544,12 @@ def process_blocks(blocks, lookup, dbg=None, lisp_weichen=None):
|
||||
if item.get("teileart", "") == "ILS 2.0 Scanner" and separator_id:
|
||||
item["merkmale"]["Naechster Separator"] = separator_id
|
||||
|
||||
# Freie Separatoren, deren Zuordnung eine VF_n/GF_n nennt, die aber nicht
|
||||
# beim Bau der Strecke entstanden sind (fehlen darum in ihrer sepliste),
|
||||
# nach Lage in die Kette aufnehmen - nur fuer diesen Lauf, die XDATA am
|
||||
# Block bleibt unveraendert (siehe einsortieren_freie_separatoren).
|
||||
einsortieren_freie_separatoren(items, NEIGHBOR_TOLERANCE_MM, dbg=dbg)
|
||||
|
||||
# Ketteninterne Separator-/AS-/ES-Reihenfolge aus der neuen "sepliste"-
|
||||
# XDATA (siehe ssg-sepliste-xdata-schreiben) - leeres dict, wenn keine
|
||||
# Strecke eine sepliste traegt (reiner Altbestand ohne neue XDATA).
|
||||
|
||||
@@ -1255,6 +1255,190 @@ def _asel_collision_boxes(item, half_tol, box_dims):
|
||||
# Ersetzt das bisherige Raten der Ketten-internen Reihenfolge (die Linie
|
||||
# selbst wird nicht exportiert) durch die tatsaechliche Bau-Reihenfolge.
|
||||
|
||||
def sepliste_einsortier_positionen(kette_xy, kandidaten):
|
||||
"""Reiner Rechenkern fuer einsortieren_freie_separatoren: wohin gehoert
|
||||
jeder Kandidat in eine vorhandene Kette?
|
||||
|
||||
kette_xy = [(x, y), ...] Lage der Kettenglieder in Kettenfolge (AS ...
|
||||
ES), mindestens zwei Punkte. Sie bilden einen Polygonzug - so
|
||||
folgt die Zuordnung auch einer Kette, die um eine Kurve laeuft,
|
||||
statt nur einer Geraden AS->ES.
|
||||
kandidaten = [(key, x, y), ...]
|
||||
|
||||
Jeder Kandidat wird dem naechstgelegenen Abschnitt (Glied i -> i+1)
|
||||
zugeordnet. Liegt er dabei VOR dem ersten oder HINTER dem letzten Glied,
|
||||
wird er verworfen - eingefuegt wird nur zwischen AS und ES.
|
||||
|
||||
Rueckgabe (einfuegen, verworfen): einfuegen = {i: [key, ...]} - nach
|
||||
Glied i einzufuegen, untereinander in Kettenrichtung sortiert;
|
||||
verworfen = [key, ...].
|
||||
"""
|
||||
abschnitte = []
|
||||
for i in range(len(kette_xy) - 1):
|
||||
(ax, ay), (bx, by) = kette_xy[i], kette_xy[i + 1]
|
||||
dx, dy = bx - ax, by - ay
|
||||
l2 = dx * dx + dy * dy
|
||||
if l2 > 1e-12:
|
||||
abschnitte.append((i, ax, ay, dx, dy, l2))
|
||||
letzter = len(kette_xy) - 2
|
||||
|
||||
einfuegen = {}
|
||||
verworfen = []
|
||||
for key, x, y in kandidaten:
|
||||
best = None
|
||||
for i, ax, ay, dx, dy, l2 in abschnitte:
|
||||
t = ((x - ax) * dx + (y - ay) * dy) / l2
|
||||
tc = min(max(t, 0.0), 1.0)
|
||||
d = math.hypot(x - (ax + tc * dx), y - (ay + tc * dy))
|
||||
if best is None or d < best[0] - 1e-9:
|
||||
best = (d, i, t)
|
||||
if best is None:
|
||||
verworfen.append(key)
|
||||
continue
|
||||
_d, i, t = best
|
||||
if (i == 0 and t <= 0.0) or (i == letzter and t >= 1.0):
|
||||
verworfen.append(key)
|
||||
continue
|
||||
einfuegen.setdefault(i, []).append((min(max(t, 0.0), 1.0), key))
|
||||
|
||||
return ({i: [key for _t, key in sorted(liste)] for i, liste in einfuegen.items()},
|
||||
verworfen)
|
||||
|
||||
|
||||
def _kettenglied_xy(entry, item, strecke):
|
||||
"""Lage eines sepliste-Glieds fuer den Polygonzug: AS/ES ueber K1/K2 der
|
||||
Strecke (Ein-/Auslauf - der ZZ_ASES_MARKER sitzt auf dem Einfuegepunkt
|
||||
des Elements und kann bis ~600 mm seitlich davon liegen), sonst bzw.
|
||||
ohne K1/K2 der Bounding-Box-Mittelpunkt des vertretenden Items."""
|
||||
typ = entry.get("typ", "")
|
||||
if typ in ("AS", "ES"):
|
||||
k1_xy, k2_xy = _asel_kos_xy(strecke)
|
||||
pt = k1_xy if typ == "AS" else k2_xy
|
||||
if pt is not None:
|
||||
return pt
|
||||
bbox = (item or {}).get("_bbox")
|
||||
if not bbox:
|
||||
return None
|
||||
return (bbox.get("cx", 0.0), bbox.get("cy", 0.0))
|
||||
|
||||
|
||||
def einsortieren_freie_separatoren(items, tolerance_mm, dbg=None):
|
||||
"""Nimmt freie Separatoren in die sepliste ihrer VF_n/GF_n-Strecke auf.
|
||||
|
||||
Die sepliste entsteht beim BAU der Strecke (ssg-sepliste-sammeln) und
|
||||
kennt nur die Separatoren, die dabei erzeugt wurden. Ein eigenstaendiger
|
||||
Separator_SP-Block, der spaeter (oder aus einer frueheren Fassung der
|
||||
Strecke) auf ihr liegt, fehlt darin - obwohl seine ZUORDNUNG
|
||||
(cs-zuordnung-lauf) die Strecke nennt. Ohne diesen Schritt fehlt er in
|
||||
TrackIds und Nachbarn (realer Fall Mubea VF_3: 0037/0038 zwischen 0062
|
||||
und 0063).
|
||||
|
||||
Aufgenommen wird ein Separator nur, wenn
|
||||
- seine Zuordnung die Strecke nennt,
|
||||
- er in keiner sepliste steht,
|
||||
- sein Mittelpunkt in der Bounding-Box der Strecke liegt (+ tolerance_mm/2),
|
||||
- er laengs der Kette zwischen AS und ES liegt
|
||||
(sepliste_einsortier_positionen).
|
||||
|
||||
Bewusst NUR im Speicher dieses Exportlaufs: die sepliste-XDATA am Block
|
||||
bleibt das Bauprotokoll. Lisp ordnet ihre Eintraege den eingebauten
|
||||
Bausteinen ueber die Anzahl je Typ zu (csv:sepliste-kette-paare) und
|
||||
fuehrt reservierte AS/ES-IDs unter der lfdnr - ein in die XDATA
|
||||
eingefuegter fremder Separator verschoebe beides. Ausserdem schreibt jeder
|
||||
Neuaufbau der Strecke die XDATA neu; hier wird bei jedem Export frisch
|
||||
aus dem aktuellen Zeichnungsstand gerechnet.
|
||||
|
||||
Aendert items in-place: item["sepliste"] der Strecke bekommt neue
|
||||
SEP-Eintraege (lfdnr "frei<TeileId>", "id" = TeileId des Separators,
|
||||
"frei": True); der Separator bekommt "_in_strecke_kette" (Strecken-
|
||||
TeileId, damit compute_kreisel_umlauf ihn nicht zusaetzlich einem
|
||||
Kreisel zuschlaegt) und einen Hinweis in "warnung".
|
||||
Muss NACH der Separator-Zuordnung und VOR compute_sep_kette laufen.
|
||||
"""
|
||||
half_tol = tolerance_mm / 2.0
|
||||
by_teileid = {it.get("teileid", ""): it for it in items if it.get("teileid", "")}
|
||||
in_ketten = set()
|
||||
for it in items:
|
||||
if it.get("teileart", "") in GF_VF_TEILEARTEN:
|
||||
for e in it.get("sepliste") or []:
|
||||
eid = str(e.get("id", "")).strip()
|
||||
if eid:
|
||||
in_ketten.add(eid)
|
||||
|
||||
if dbg:
|
||||
dbg("=== einsortieren_freie_separatoren ===")
|
||||
|
||||
for strecke in items:
|
||||
if strecke.get("teileart", "") not in GF_VF_TEILEARTEN:
|
||||
continue
|
||||
sepliste = strecke.get("sepliste")
|
||||
sbbox = strecke.get("_bbox")
|
||||
if not sepliste or not sbbox:
|
||||
continue
|
||||
sid = strecke.get("teileid", "")
|
||||
sbounds = _bounds(sbbox, half_tol)
|
||||
|
||||
kandidaten = []
|
||||
for it in items:
|
||||
if it.get("teileart", "") != "ILS 2.0 Separator":
|
||||
continue
|
||||
tid = it.get("teileid", "")
|
||||
if not tid or tid in in_ketten:
|
||||
continue
|
||||
zuordnung = ((it.get("merkmale") or {}).get("Zuordnung")
|
||||
or it.get("_zuordnung_attr") or "").strip()
|
||||
if zuordnung != sid:
|
||||
continue
|
||||
bbox = it.get("_bbox")
|
||||
if not bbox:
|
||||
continue
|
||||
cx, cy = bbox.get("cx", 0.0), bbox.get("cy", 0.0)
|
||||
if not (sbounds[0] <= cx <= sbounds[1] and sbounds[2] <= cy <= sbounds[3]):
|
||||
if dbg:
|
||||
dbg(f" Strecke {sid}: Separator {tid} ausserhalb der Strecken-BBox "
|
||||
f"- nicht einsortiert")
|
||||
continue
|
||||
kandidaten.append((tid, cx, cy))
|
||||
if not kandidaten:
|
||||
continue
|
||||
|
||||
kette_xy = []
|
||||
for e in sepliste:
|
||||
pt = _kettenglied_xy(e, by_teileid.get(str(e.get("id", "")).strip()), strecke)
|
||||
if pt is None:
|
||||
break
|
||||
kette_xy.append(pt)
|
||||
if len(kette_xy) != len(sepliste) or len(kette_xy) < 2:
|
||||
if dbg:
|
||||
dbg(f" Strecke {sid}: Kettenglieder nicht vollstaendig lokalisierbar "
|
||||
f"- freie Separatoren {[k[0] for k in kandidaten]} nicht einsortiert")
|
||||
continue
|
||||
|
||||
einfuegen, verworfen = sepliste_einsortier_positionen(kette_xy, kandidaten)
|
||||
if dbg and verworfen:
|
||||
dbg(f" Strecke {sid}: {verworfen} liegen nicht zwischen AS und ES "
|
||||
f"- nicht einsortiert")
|
||||
if not einfuegen:
|
||||
continue
|
||||
|
||||
neu = []
|
||||
for i, entry in enumerate(sepliste):
|
||||
neu.append(entry)
|
||||
for tid in einfuegen.get(i, []):
|
||||
neu.append({"typ": "SEP", "lfdnr": f"frei{tid}", "id": tid, "frei": True})
|
||||
sep = by_teileid[tid]
|
||||
sep["_in_strecke_kette"] = sid
|
||||
hinweis = (f"nicht im Bau von {sid} erzeugt - per Lage in dessen "
|
||||
f"Kette einsortiert")
|
||||
bisherige = sep.get("warnung", "")
|
||||
sep["warnung"] = f"{bisherige}; {hinweis}" if bisherige else hinweis
|
||||
in_ketten.add(tid)
|
||||
strecke["sepliste"] = neu
|
||||
if dbg:
|
||||
dbg(f" Strecke {sid}: Kette jetzt "
|
||||
f"{' -> '.join(str(e.get('id', '')) for e in neu)}")
|
||||
|
||||
|
||||
def compute_sep_kette(items, dbg=None):
|
||||
"""Baut je VF_n/GF_n-Item MIT "sepliste" die ketteninterne Vorgaenger-/
|
||||
Nachfolger-Beziehung AS -> Sep1 -> ... -> SepN -> ES.
|
||||
@@ -1392,6 +1576,11 @@ def compute_kreisel_umlauf(items, tolerance_mm, durchmesser_mm=800.0,
|
||||
for item in items:
|
||||
if item.get("teileart", "") not in KREISEL_UMLAUF_TEILEARTEN:
|
||||
continue
|
||||
# Freier Separator, den einsortieren_freie_separatoren in die Kette
|
||||
# einer Strecke aufgenommen hat - gehoert nicht zusaetzlich auf einen
|
||||
# Kreisel, auch wenn seine Box einen beruehrt.
|
||||
if item.get("_in_strecke_kette"):
|
||||
continue
|
||||
teileid = item.get("teileid", "")
|
||||
bbox = item.get("_bbox")
|
||||
if not bbox:
|
||||
|
||||
Reference in New Issue
Block a user