diff --git a/Lisp/Gefaellestrecke.lsp b/Lisp/Gefaellestrecke.lsp index 883d509..9a31d24 100644 --- a/Lisp/Gefaellestrecke.lsp +++ b/Lisp/Gefaellestrecke.lsp @@ -1164,7 +1164,7 @@ es-seite geruest-einzelmodul geruest-typ as-winkel es-winkel / gf-bname gf-ss ent gf-insert typ-str anzahl-gf - gf-winkel-str i gf-g10z gf-korr-dz gf-osmode-alt) + gf-winkel-str i gf-g10z gf-korr-dz gf-osmode-alt gf-sepliste) (if (null geruest-einzelmodul) (setq geruest-einzelmodul "1")) (if (null geruest-typ) (setq geruest-typ (ssg-geruest-idx-to-typ (ssg-geruest-typ-to-idx nil)))) ;; Beschriftung: Mitte der ersten Geraden, 400mm senkrecht versetzt @@ -1207,6 +1207,9 @@ (ssadd ent gf-ss) (setq ent (entnext ent)) ) + ;; Sepliste (Separator/AS/ES-Reihenfolge, siehe ssg_ks_insert.lsp) VOR dem + ;; Sweep sammeln - danach existieren die Sub-Entities nicht mehr einzeln. + (setq gf-sepliste (ssg-sepliste-sammeln last-ent)) ;; Block definieren und einfuegen. ssg-block-wrap-welt schaltet dabei den ;; Objektfang (OSMODE=0), die Elevation (0) und das BKS (Welt) sauber ab - ;; sonst wird der uebergebene Einfuegepunkt umgefangen bzw. der OCS-Skew der @@ -1218,6 +1221,7 @@ ;; Ziel-Layer der Baugruppe aus cfg/layer.cfg (ohne Eintrag bleibt die ;; Referenz auf dem aktuellen Layer, wie vor der Umstellung). (ssg-layer-setzen gf-insert "gefaellestrecke") + (ssg-sepliste-xdata-schreiben gf-insert "SSG_GF_SEP" gf-sepliste) ;; Kette wurde bei Z=0 gebaut; der OSMODE=0-Wrap loest den OCS-Skew der ;; gekippten Elemente sauber zur logischen Position (Z=0) auf. Den fertigen ;; GF_n-Block daher nur noch senkrecht auf die Zielhoehe heben (OSMODE=0, diff --git a/Lisp/export.lsp b/Lisp/export.lsp index b0b5caf..182ebad 100644 --- a/Lisp/export.lsp +++ b/Lisp/export.lsp @@ -588,7 +588,8 @@ ;; include-bbox -- Kodierung ueber csv:kos-encode (siehe dort). (defun csv:block-to-json (ename include-bbox / ed blk-name layer pt rotation handle attribs bbox bbox-json insert-quat insert-kos k-list warnung-json warnung-eintrag - zuordnung-fix-json zuordnung-fix-eintrag) + zuordnung-fix-json zuordnung-fix-eintrag sepliste-json sepliste-app sepliste + sepliste-teile sepliste-first) (setq ed (entget ename)) (setq blk-name (cdr (assoc 2 ed))) (setq layer (cdr (assoc 8 ed))) @@ -667,6 +668,40 @@ (setq zuordnung-fix-json (strcat ",\"zuordnung_fix\":\"" (csv:json-escape (cdr zuordnung-fix-eintrag)) "\"")) ) + ;; VF_n/GF_n-Wrapper: Separator-/AS-/ES-Reihenfolge aus der beim Bau + ;; geschriebenen XDATA lesen (siehe ssg-sepliste-xdata-lesen, + ;; ssg_ks_insert.lsp) und als "sepliste"-Array einbetten. Nur gesetzt, wenn + ;; die XDATA vorhanden ist (Altbestand-Wrapper ohne die neue XDATA bleiben + ;; ohne dieses Feld - export_neighbors.py faellt dann auf den bisherigen + ;; BBox-Kollisionspfad zurueck). + (setq sepliste-json "") + (if (car (atoms-family 1 '("SSG-SEPLISTE-XDATA-LESEN"))) + (progn + (setq sepliste-app + (cond + ((wcmatch (strcase blk-name) "VF_*") "SSG_VF_SEP") + ((wcmatch (strcase blk-name) "GF_*") "SSG_GF_SEP") + (t nil))) + (if sepliste-app + (setq sepliste (ssg-sepliste-xdata-lesen ename sepliste-app))) + (if sepliste + (progn + (setq sepliste-teile "" sepliste-first t) + (foreach e sepliste + (setq sepliste-teile + (strcat sepliste-teile (if sepliste-first "" ",") + "{\"typ\":\"" (nth 0 e) "\"" + ",\"lfdnr\":" (itoa (nth 1 e)) + ",\"x\":" (rtos (nth 2 e) 2 2) + ",\"y\":" (rtos (nth 3 e) 2 2) + "}")) + (setq sepliste-first nil) + ) + (setq sepliste-json (strcat ",\"sepliste\":[" sepliste-teile "]")) + ) + ) + ) + ) (strcat " {\"block_name\":\"" (csv:json-escape blk-name) "\"" ",\"layer\":\"" (csv:json-escape layer) "\"" @@ -684,6 +719,7 @@ bbox-json warnung-json zuordnung-fix-json + sepliste-json "}" ) ) diff --git a/Lisp/ssg_ks_insert.lsp b/Lisp/ssg_ks_insert.lsp index 6bc1781..85a8a7c 100644 --- a/Lisp/ssg_ks_insert.lsp +++ b/Lisp/ssg_ks_insert.lsp @@ -354,4 +354,148 @@ ) ) +;; ------------------------------------------------------------ +;; SEPLISTE: Separator-/AS-/ES-Reihenfolge als XDATA am fertigen Wrapper +;; ------------------------------------------------------------ +;; Sammelt beim Bau einer VF_n/GF_n-Kette die Weltposition + Baureihenfolge +;; aller "Staustrecke_Separator_SP_300_mm"/"AS_Element_*"/"ES_Element_*"- +;; Sub-Bloecke, BEVOR der abschliessende _.-BLOCK-Sweep (ssg-block-wrap-welt) +;; sie in die Wrapper-Blockdefinition einsaugt und ihre Modellraum-Existenz +;; damit beendet. Genutzt von vf-block-erstellen (vf_core.lsp), +;; vfl-block-erstellen (vf_linienzug.lsp) und gf-block-erstellen +;; (Gefaellestrecke.lsp) - an allen drei Stellen identisch aufgerufen, daher +;; hier zentral (vgl. Projektregel "Wiederverwendung vor Neuschaffung"). +;; +;; Traegt bewusst KEINE ssg-id-generate-ID (kein ID-ATTDEF an diesen +;; Bloecken, Sinn/Zweck rein interne Geometrie-Traeger) - jeder Eintrag +;; bekommt nur eine laufende Nummer innerhalb der Liste. Der Export bildet +;; daraus bei Bedarf eigene, global fortlaufende TeileIds. + +;; Einfaches Chunking eines Strings in Stuecke <= n Zeichen (DXF-Limit einer +;; 1000-Gruppe ist 255 Byte) - eigenstaendige Kopie statt vfl-chunk-string +;; (vf_linienzug.lsp), da diese Datei laut Lademechanismus VOR vf_linienzug +;; geladen wird und dessen Funktionen hier noch nicht existieren. +(defun ssg-sepliste-chunk-string (s n / out len) + (setq out '()) + (while (> (setq len (strlen s)) n) + (setq out (cons (substr s 1 n) out)) + (setq s (substr s (1+ n)))) + (if (> (strlen s) 0) (setq out (cons s out))) + (reverse out)) + +;; Sammelt alle seit lastEnt neu erzeugten Separator-/AS-/ES-Entities in +;; Baureihenfolge (identische entnext-Wanderung wie die Auswahlsatz-Sammlung +;; der Aufrufer, nur mit wcmatch-Filter). Rueckgabe: Liste von +;; (typ lfdnr x y), typ = "SEP"/"AS"/"ES", lfdnr 1-basiert in Baureihenfolge. +(defun ssg-sepliste-sammeln (lastEnt / ent bname typ lfdnr pt liste) + (setq liste '() lfdnr 0) + (setq ent (if lastEnt (entnext lastEnt) (entnext))) + (while ent + (if (= (cdr (assoc 0 (entget ent))) "INSERT") + (progn + (setq bname (cdr (assoc 2 (entget ent)))) + (setq typ (cond + ((wcmatch bname "Staustrecke_Separator_SP_300_mm*") "SEP") + ((wcmatch bname "AS_Element_*") "AS") + ((wcmatch bname "ES_Element_*") "ES") + (t nil))) + (if typ + (progn + (setq lfdnr (1+ lfdnr)) + (setq pt (cdr (assoc 10 (entget ent)))) + (setq liste (cons (list typ lfdnr (car pt) (cadr pt)) liste)) + ) + ) + ) + ) + (setq ent (entnext ent)) + ) + (reverse liste) +) + +;; Serialisiert die Sepliste zu "TYP:LFDNR:X:Y,TYP:LFDNR:X:Y,...". +(defun ssg-sepliste->string (liste / eintrag out first) + (setq out "" first t) + (foreach eintrag liste + (setq eintrag + (strcat (nth 0 eintrag) ":" (itoa (nth 1 eintrag)) ":" + (rtos (nth 2 eintrag) 2 2) ":" (rtos (nth 3 eintrag) 2 2))) + (if first (progn (setq out eintrag) (setq first nil)) + (setq out (strcat out "," eintrag))) + ) + out +) + +;; Gegenstueck zu ssg-sepliste->string - Rueckgabe Liste von (typ lfdnr x y). +(defun ssg-sepliste-string->liste (s / out teile eintraege feld) + (setq out '()) + (if (> (strlen s) 0) + (progn + (setq eintraege '() teile "") + (foreach c (ssg-sepliste-explode-chars s) + (if (= c ",") + (progn (setq eintraege (cons teile eintraege)) (setq teile "")) + (setq teile (strcat teile c)) + ) + ) + (setq eintraege (reverse (cons teile eintraege))) + (foreach eintrag eintraege + (setq feld (ssg-sepliste-split-feld eintrag ":")) + (if (= (length feld) 4) + (setq out (cons (list (nth 0 feld) (atoi (nth 1 feld)) + (atof (nth 2 feld)) (atof (nth 3 feld))) out)) + ) + ) + ) + ) + (reverse out) +) +(defun ssg-sepliste-explode-chars (s / i n out) + (setq out '() i 1 n (strlen s)) + (while (<= i n) (setq out (cons (substr s i 1) out)) (setq i (1+ i))) + (reverse out) +) +(defun ssg-sepliste-split-feld (s sep / pos out) + (setq out '()) + (while (setq pos (vl-string-search sep s)) + (setq out (cons (substr s 1 pos) out)) + (setq s (substr s (+ pos 1 (strlen sep))))) + (reverse (cons s out)) +) + +;; Schreibt die Sepliste als XDATA (App app-name, z.B. "SSG_VF_SEP"/ +;; "SSG_GF_SEP") auf den fertigen Wrapper-Block ent. Layout der 1000-Gruppen: +;; [0]="sepliste" (Marker), [1..]=serialisierte Liste in 250-Byte-Chunks +;; (DXF-Limit 255 Byte je 1000-Gruppe) - identisches Muster wie +;; vfl-journal-xdata-schreiben (vf_linienzug.lsp). +(defun ssg-sepliste-xdata-schreiben (ent app-name liste / chunks appentry) + (if (and ent liste) + (progn + (regapp app-name) + (setq chunks (ssg-sepliste-chunk-string (ssg-sepliste->string liste) 250)) + (setq appentry + (cons app-name + (cons (cons 1000 "sepliste") + (mapcar (function (lambda (c) (cons 1000 c))) chunks)))) + (entmod (append (entget ent) (list (list -3 appentry)))) + ) + ) + ent +) + +;; Liest die Sepliste-XDATA. Rueckgabe: Liste von (typ lfdnr x y), oder nil +;; (kein/unbekanntes Sepliste-XDATA am Entity). +(defun ssg-sepliste-xdata-lesen (ent app-name / xd app-data werte) + (setq xd (entget ent (list app-name))) + (setq app-data (cdr (assoc -3 xd))) + (if app-data + (progn + (setq werte (mapcar 'cdr (cdr (car app-data)))) + (if (and werte (= (car werte) "sepliste")) + (ssg-sepliste-string->liste (apply 'strcat (cdr werte))) + nil)) + nil + ) +) + (princ) diff --git a/Lisp/vf_core.lsp b/Lisp/vf_core.lsp index 3e23a49..c9537d4 100644 --- a/Lisp/vf_core.lsp +++ b/Lisp/vf_core.lsp @@ -831,7 +831,8 @@ hoehe-von hoehe-bis deltaH deltaL L_VF L_GF1 L_GF2 richtung best-winkel startpunkt lastEnt hz geruest-einzelmodul geruest-typ - / vf-bname vf-ss vf-e vf-insert montagehoehe-m attdef-ypos L_GF_m-str) + / vf-bname vf-ss vf-e vf-insert montagehoehe-m attdef-ypos L_GF_m-str + vf-sepliste) (if (null hz) (setq hz 0.0)) (if (null geruest-einzelmodul) (setq geruest-einzelmodul "1")) (if (null geruest-typ) (setq geruest-typ (ssg-geruest-idx-to-typ (ssg-geruest-typ-to-idx nil)))) @@ -868,6 +869,10 @@ (setq vf-e (entnext vf-e)) ) + ;; Sepliste (Separator/AS/ES-Reihenfolge, siehe ssg_ks_insert.lsp) VOR dem + ;; Sweep sammeln - danach existieren die Sub-Entities nicht mehr einzeln. + (setq vf-sepliste (ssg-sepliste-sammeln lastEnt)) + ;; Block erstellen, einfuegen, Attribute setzen (setq vf-bname (strcat "VF_" (itoa vf-nummer))) (if (> (sslength vf-ss) 0) @@ -880,6 +885,7 @@ ;; Ziel-Layer der Baugruppe aus cfg/layer.cfg (ohne Eintrag bleibt die ;; Referenz auf dem aktuellen Layer, wie vor der Umstellung). (ssg-layer-setzen vf-insert "variofoerderer") + (ssg-sepliste-xdata-schreiben vf-insert "SSG_VF_SEP" vf-sepliste) (setq montagehoehe-m (/ (+ hoehe-von hoehe-bis) 2000.0)) ;; L_GF_m: kommagetrennte Segmentlaengen in Metern (vorne,hinten), ;; analog L_GF_m in Gefaellestrecke.lsp. diff --git a/Lisp/vf_linienzug.lsp b/Lisp/vf_linienzug.lsp index f5a1790..8643009 100644 --- a/Lisp/vf_linienzug.lsp +++ b/Lisp/vf_linienzug.lsp @@ -3258,7 +3258,7 @@ ;; ============================================================ (defun vfl-block-erstellen (vfl-nummer anzahl-gf anzahl-vf hoehe-von hoehe-bis delta-l-total as-seite es-seite startpunkt lastEnt / - vfl-bname vfl-ss ent vfl-insert typ-str) + vfl-bname vfl-ss ent vfl-insert typ-str vfl-sepliste) ;; TYP: einsegmentige Gefaellestrecke ohne Bogen -> "Gefaellestrecke"; ;; mit VF, Bogen oder mehreren GF-Segmenten -> "Streckengruppe". (setq typ-str @@ -3289,6 +3289,9 @@ (ssadd ent vfl-ss) (setq ent (entnext ent)) ) + ;; Sepliste (Separator/AS/ES-Reihenfolge, siehe ssg_ks_insert.lsp) VOR dem + ;; Sweep sammeln - danach existieren die Sub-Entities nicht mehr einzeln. + (setq vfl-sepliste (ssg-sepliste-sammeln lastEnt)) ;; Block definieren und einfuegen mit garantiert weltparallelem BKS. ;; startpunkt ist ein Welt-Punkt; ssg-block-wrap-welt setzt das BKS ;; temporaer auf Welt (verhindert den 31.95mm-Z-Versatz bei abweichendem @@ -3297,6 +3300,7 @@ ;; Ziel-Layer der Baugruppe aus cfg/layer.cfg (ohne Eintrag bleibt die ;; Referenz auf dem aktuellen Layer, wie vor der Umstellung). (ssg-layer-setzen vfl-insert "variofoerderer") + (ssg-sepliste-xdata-schreiben vfl-insert "SSG_VF_SEP" vfl-sepliste) ;; Werte (volle Liste; nicht vorhandene Tags ignoriert ssg-attrib-set-on) (ssg-attrib-set-on vfl-insert (list diff --git a/lib/export_csv.py b/lib/export_csv.py index 77935d4..9b50866 100644 --- a/lib/export_csv.py +++ b/lib/export_csv.py @@ -122,6 +122,8 @@ from export_neighbors import ( compute_scanner_nearest_separator, compute_kreisel_touch_switches, compute_strecke_kreisel_schleus, + compute_sep_kette, + compute_kreisel_umlauf, SENSOR_TEILEARTEN, WEICHE_TEILEART, GF_VF_TEILEARTEN, @@ -529,28 +531,50 @@ def build_kreisel_weiche_items(items, planquadrat_cfg, start_elem_nr, dbg=None): return elem_nr -def build_strecke_schleus_items(items, planquadrat_cfg, start_elem_nr, dbg=None): +def _strecke_typ_feld(teileart): + """Merkmals-Feldname fuer die Ketten-Zugehoerigkeit einer AS/ES-/Separator- + Zeile, je Kettentyp unterschiedlich (User-Vorgabe): VF_n ("ILS 2.0 + Strecke") -> "Förderer", GF_n ("ILS 2.0 Gefaellestrecke") -> "Strecke" + (wie bisher).""" + return "Strecke" if teileart == "ILS 2.0 Gefaellestrecke" else "Förderer" + + +def build_strecke_schleus_items(items, planquadrat_cfg, start_elem_nr, sep_kette=None, dbg=None): """Haengt fuer jedes Strecken-Ende mit anschliessendem Kreisel eine "ILS Ausschleuselement"- (Anfang) bzw. "ILS Einschleuselement"-Zeile - (Ende) an items an (siehe export_neighbors.compute_strecke_kreisel_schleus). + (Ende) an items an (siehe export_neighbors.compute_strecke_kreisel_schleus + fuer Position/Boundingbox - reine Geometrie, unveraendert). Muss NACH build_kreisel_weiche_items laufen, damit die TeileId-Zaehlung (max_numeric_teileid) auch die bereits erzeugten Weichen einschliesst. - Position = Kollisionsposition Kreisel<->Strecke; Nachbarn = Kreisel-ID + - Strecken-ID; Anzahl 1; K1-K4/Insertpoint leer; Boundingbox = die statische, - an der Kreiselachse ausgerichtete Default-Box. + Anzahl 1; K1-K4/Insertpoint leer; Boundingbox = die statische, an der + Kreiselachse ausgerichtete Default-Box. + + sep_kette = Rueckgabe von export_neighbors.compute_sep_kette (keys + "#" -> Eintrag mit typ/vorgaenger/nachfolger). Traegt + die zugehoerige Strecke eine "sepliste" (neue LISP-XDATA, siehe + ssg-sepliste-xdata-schreiben), enthaelt "Nachbarn" NUR noch die IDs des + ketteninternen Vorgaengers/Nachfolgers (Separator oder je anderem + Kreisel-Ende) statt Kreisel-ID + Strecken-ID; die Kreisel-/Foerderer- + Zugehoerigkeit steht dann stattdessen in den Merkmalen ("Kreisel" + + "Förderer"/"Strecke" je Typ, analog zum "Zuordnung"-Feld der Separatoren). + Fehlt die sepliste (Altbestand-Wrapper ohne die neue XDATA), bleibt der + bisherige Nachbarn-Inhalt [kreisel_id, strecke_id] unveraendert erhalten. Rueckgabe: fortgeschriebene Elementnummer. """ + if sep_kette is None: + sep_kette = {} schleus, warnings = compute_strecke_kreisel_schleus( items, durchmesser_mm=KREISEL_DURCHMESSER_MM, element_box_mm=ELEMENT_BOX_MM, dbg=dbg) + strecke_by_id = {it.get("teileid", ""): it for it in items + if it.get("teileart", "") in GF_VF_TEILEARTEN} + # Warnungen (>2 Kreisel-BBox-Beruehrungen je Strecke) auf die Strecken-Zeile # schreiben (Spalte "Warnungen") und in die Konsole ausgeben. if warnings: - strecke_by_id = {it.get("teileid", ""): it for it in items - if it.get("teileart", "") in GF_VF_TEILEARTEN} for sid, text in warnings.items(): if sid in strecke_by_id: strecke_by_id[sid]["warnung"] = text @@ -567,6 +591,26 @@ def build_strecke_schleus_items(items, planquadrat_cfg, start_elem_nr, dbg=None) zaehler[teileart] = zaehler.get(teileart, 0) + 1 planquadrat = (compute_planquadrat(sp["x"], sp["y"], planquadrat_cfg) if planquadrat_cfg is not None else "") + + strecke = strecke_by_id.get(sp["strecke_id"]) + sepliste = strecke.get("sepliste") if strecke else None + merkmale = {"Erzeugt aus": "Kreisel-Strecke-Kollision"} + if sepliste: + # Neue XDATA vorhanden: Nachbarn nur noch aus der Ketten-internen + # Vorgaenger/Nachfolger-Beziehung (AS/ES ist immer das erste bzw. + # letzte Sepliste-Element seiner Strecke). + lfdnr = sepliste[0]["lfdnr"] if sp["ende_label"] == "Anfang" else sepliste[-1]["lfdnr"] + key = f"{sp['strecke_id']}#{lfdnr}" + eintrag = sep_kette.get(key, {}) + nachbarn = [n for n in (eintrag.get("vorgaenger"), eintrag.get("nachfolger")) if n] + merkmale["Kreisel"] = sp["kreisel_id"] + merkmale[_strecke_typ_feld(strecke.get("teileart", ""))] = sp["strecke_id"] + else: + nachbarn = [sp["kreisel_id"], sp["strecke_id"]] + merkmale["Kreisel"] = sp["kreisel_id"] + merkmale["Strecke"] = sp["strecke_id"] + merkmale["Streckenende"] = sp["ende_label"] + items.append({ "nr": elem_nr, "teileart": teileart, @@ -577,14 +621,64 @@ def build_strecke_schleus_items(items, planquadrat_cfg, start_elem_nr, dbg=None) "boundingbox": f'{sp["dx"]:.2f}, {sp["dy"]:.2f}, {sp["dz"]:.2f}', "insertpoint": "", "k1": "", "k2": "", "k3": "", "k4": "", - "nachbarn": ", ".join([sp["kreisel_id"], sp["strecke_id"]]), + "nachbarn": ", ".join(nachbarn), "fehler": "", - "merkmale": { - "Erzeugt aus": "Kreisel-Strecke-Kollision", - "Kreisel": sp["kreisel_id"], - "Strecke": sp["strecke_id"], - "Streckenende": sp["ende_label"], - }, + "merkmale": merkmale, + }) + return elem_nr + + +def build_separator_kette_items(items, planquadrat_cfg, start_elem_nr, sep_kette, dbg=None): + """Haengt fuer jeden in einer VF_n/GF_n-Kette eingebetteten Separator + ("Staustrecke_Separator_SP_300_mm", ueber die neue "sepliste"-XDATA + bekannt - siehe ssg-sepliste-xdata-schreiben/compute_sep_kette) eine + eigene "ILS 2.0 Separator"-Zeile an. Bisher wurden diese internen + Ketten-Separatoren nur gezaehlt (ANZAHL_SEPARATOR), nicht einzeln + exportiert - anders als frei platzierte Separator_SP-Symbole. + + Nachbarn = ketteninterner Vorgaenger/Nachfolger (Separator oder AS/ES), + analog zur AS/ES-Nachbarn-Spalte in build_strecke_schleus_items. + Merkmale bekommen "Zuordnung" = Strecken-TeileId (wie bei frei + platzierten Separatoren ueber compute_sensor_zuordnung). + + Rueckgabe: fortgeschriebene Elementnummer. + """ + strecke_by_id = {it.get("teileid", ""): it for it in items + if it.get("teileart", "") in GF_VF_TEILEARTEN} + + naechste_id = max_numeric_teileid(items) + elem_nr = start_elem_nr + zaehler = 0 + for key, eintrag in sep_kette.items(): + if eintrag.get("typ") != "SEP": + continue + elem_nr += 1 + naechste_id += 1 + zaehler += 1 + teileid = f"{naechste_id:04d}" + planquadrat = (compute_planquadrat(eintrag["x"], eintrag["y"], planquadrat_cfg) + if planquadrat_cfg is not None else "") + nachbarn = [n for n in (eintrag.get("vorgaenger"), eintrag.get("nachfolger")) if n] + strecke = strecke_by_id.get(eintrag["strecke_id"]) + merkmale = { + "Erzeugt aus": "VF/GF-Sepliste", + "Zuordnung": eintrag["strecke_id"], + } + if strecke: + merkmale[_strecke_typ_feld(strecke.get("teileart", ""))] = eintrag["strecke_id"] + items.append({ + "nr": elem_nr, + "teileart": "ILS 2.0 Separator", + "teileid": teileid, + "bezeichnung": f"Separator :{zaehler}", + "planquadrat": planquadrat, + "position": f'{eintrag["x"]:.2f}, {eintrag["y"]:.2f}, 0.00', + "boundingbox": "", + "insertpoint": "", + "k1": "", "k2": "", "k3": "", "k4": "", + "nachbarn": ", ".join(nachbarn), + "fehler": "", + "merkmale": merkmale, }) return elem_nr @@ -921,14 +1015,42 @@ def process_blocks(blocks, lookup, dbg=None): if item.get("teileart", "") == "ILS 2.0 Scanner" and separator_id: item["merkmale"]["Naechster Separator"] = separator_id + # 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). + sep_kette = compute_sep_kette(items, dbg=dbg) + + # Umlauf-Reihenfolge an jedem Kreisel: freie Separatoren (per Zuordnung + # oben bereits einem Kreisel zugeordnet) UND die AS/ES-Enden andockender + # Ketten (aus sep_kette), sortiert nach Winkel um die Kreiselachse (siehe + # compute_kreisel_umlauf - DREHRICHTUNG-Attribut bestimmt den Umlaufsinn). + # Freie Separator-Zeilen bekommen ihre "Nachbarn"-Spalte hier aus dem + # Umlauf statt leer zu bleiben; die AS/ES-Nachbarn selbst setzt weiterhin + # build_strecke_schleus_items (ketteninterner Vorgaenger/Nachfolger). + kreisel_umlauf = compute_kreisel_umlauf( + items, sep_kette, NEIGHBOR_TOLERANCE_MM, + durchmesser_mm=KREISEL_DURCHMESSER_MM, dbg=dbg) + if kreisel_umlauf: + separator_by_id = {it.get("teileid", ""): it for it in items + if it.get("teileart", "") == "ILS 2.0 Separator"} + for punkte in kreisel_umlauf.values(): + for punkt_key, vorgaenger, nachfolger in punkte: + item = separator_by_id.get(punkt_key) + if item is None: + continue + nachbarn = [n for n in (vorgaenger, nachfolger) if n] + item["nachbarn"] = ", ".join(nachbarn) + # Nach der Kollisions-/Nachbarschaftserkennung: an jedem Kreisel- # Beruehrpunkt eine ILS-Weiche anhaengen (eigene Zeilen am Listenende - # sie nehmen an der Nachbarschaftserkennung selbst nicht teil, ihre # Nachbarn sind fest die beiden beruehrenden Kreisel). Danach an jedem - # Strecken-Ende mit anschliessendem Kreisel ein Aus-/Einschleuselement - - # die TeileId-Zaehlung laeuft ueber beide hinweg weiter (Weichen zuerst). + # Strecken-Ende mit anschliessendem Kreisel ein Aus-/Einschleuselement, + # gefolgt von je einer Zeile pro in einer Kette eingebettetem Separator - + # die TeileId-Zaehlung laeuft ueber alle drei hinweg weiter. elem_nr = build_kreisel_weiche_items(items, planquadrat_cfg, elem_nr, dbg=dbg) - build_strecke_schleus_items(items, planquadrat_cfg, elem_nr, dbg=dbg) + elem_nr = build_strecke_schleus_items(items, planquadrat_cfg, elem_nr, sep_kette=sep_kette, dbg=dbg) + build_separator_kette_items(items, planquadrat_cfg, elem_nr, sep_kette, dbg=dbg) # Nach dem Sammelheimen aller Zeilen: sortiert nach TeileId ausgeben, damit # Elementnummer (erste Spalte) und TeileId nach Moeglichkeit uebereinstimmen diff --git a/lib/export_neighbors.py b/lib/export_neighbors.py index b91e378..58d98a9 100644 --- a/lib/export_neighbors.py +++ b/lib/export_neighbors.py @@ -1266,3 +1266,156 @@ def compute_strecke_kreisel_schleus(items, durchmesser_mm=800.0, f"Strecken mit Warnung (>2 Kreisel): {len(warnings)}") return result, warnings + + +# --------------------------------------------------------------------------- +# Sepliste (LISP-XDATA, siehe ssg-sepliste-xdata-schreiben) -> Nachbarschaft +# --------------------------------------------------------------------------- +# +# "sepliste" (item["sepliste"], von csv:block-to-json aus SSG_VF_SEP/ +# SSG_GF_SEP-XDATA gelesen) ist die beim Bau in Baureihenfolge gesammelte +# Liste der Separator-/AS-/ES-Sub-Bloecke EINER VF_n/GF_n-Kette: +# [{"typ": "AS"|"SEP"|"ES", "lfdnr": int, "x": float, "y": float}, ...]. +# Ersetzt das bisherige Raten der Ketten-internen Reihenfolge (die Linie +# selbst wird nicht exportiert) durch die tatsaechliche Bau-Reihenfolge. + +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. + + Jeder Sepliste-Eintrag bekommt eine (innerhalb dieser Kette eindeutige) + Schluessel-ID "#" - das ist die ID, mit der + build_strecke_schleus_items/build_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. + + Rueckgabe: dict {key: {"typ", "x", "y", "strecke_id", "vorgaenger", + "nachfolger"}} - vorgaenger/nachfolger sind wieder solche keys, oder None + am jeweiligen Kettenende (Verkettung mit dem Kreisel-Umlauf uebernimmt + compute_kreisel_umlauf). + """ + result = {} + for item in items: + if item.get("teileart", "") not in GF_VF_TEILEARTEN: + continue + sepliste = item.get("sepliste") + if not sepliste: + continue + sid = item.get("teileid", "") + keys = [f"{sid}#{e.get('lfdnr')}" for e in sepliste] + for i, entry in enumerate(sepliste): + key = keys[i] + result[key] = { + "typ": entry.get("typ", ""), + "x": entry.get("x", 0.0), + "y": entry.get("y", 0.0), + "strecke_id": sid, + "vorgaenger": keys[i - 1] if i > 0 else None, + "nachfolger": keys[i + 1] if i < len(keys) - 1 else None, + } + if dbg: + kette_txt = " -> ".join(f"{e.get('typ')}:{e.get('lfdnr')}" for e in sepliste) + dbg(f" Strecke {sid}: Sepliste-Kette {kette_txt}") + return result + + +def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0, dbg=None): + """Bringt fuer jeden echten Kreisel (KREISEL_SPLIT_TEILEART) alle dort + liegenden Punkte - frei platzierte Separatoren (per compute_sensor_ + zuordnung bereits einer Kreiselhaelfte zugeordnet) UND die AS/ES-Enden + aller andockenden VF_n/GF_n-Ketten (aus sep_kette, per BBox-Naehe wie + compute_strecke_kreisel_schleus zugeordnet) - in eine geschlossene + Umlauf-Reihenfolge (zirkulaer, siehe DREHRICHTUNG-Attribut). + + Winkel wird um den Kapsel-Mittelpunkt (Mitte AN8-/SP8-Achse, siehe + _kreisel_capsule) gemessen, 0 Grad = Kapsel-Achsrichtung. UZS + (Uhrzeigersinn, Default) sortiert nach fallendem Winkel, GUZ nach + steigendem - beides ergibt einen konsistenten Umlauf, die Achsrichtung + selbst ist nur der Nullpunkt. + + 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). + + Rueckgabe: dict {kreisel_teileid: [(punkt_key, vorgaenger_key, + nachfolger_key), ...]} in Umlaufreihenfolge - punkt_key ist entweder eine + Separator-TeileId (freie Separatoren) oder ein sep_kette-Schluessel + (AS/ES-Enden). Kreisel ohne zugeordnete Punkte liefern eine leere Liste. + """ + half_tol = tolerance_mm / 2.0 + kreisel = {} + for item in items: + if item.get("teileart", "") == KREISEL_SPLIT_TEILEART: + kreisel[item.get("teileid", "")] = ( + _kreisel_capsule(item, durchmesser_mm), + (item.get("attribs", {}) or {}).get("DREHRICHTUNG", "UZS")) + + # Freie Separatoren -> ihre Kreiselhaelfte (bereits per BBox-Ueberschneidung + # bestimmbar, wie compute_sensor_zuordnung es fuer die "Zuordnung"-Spalte + # tut - hier direkt gegen die volle Kreisel-BBox statt der Haelften, weil + # nur "gehoert zu diesem Kreisel" gebraucht wird, nicht links/rechts). + kreisel_bounds = {} + for tid, (cap, _dr) in kreisel.items(): + acx = (cap["a"][0] + cap["b"][0]) / 2.0 + acy = (cap["a"][1] + cap["b"][1]) / 2.0 + achs_len = math.hypot(cap["b"][0] - cap["a"][0], cap["b"][1] - cap["a"][1]) + kreisel_bounds[tid] = _bounds( + {"cx": acx, "cy": acy, "dx": durchmesser_mm + achs_len, "dy": durchmesser_mm}, + half_tol) + + punkte_je_kreisel = {tid: [] for tid in kreisel} + + for item in items: + if item.get("teileart", "") != "ILS 2.0 Separator": + continue + bbox = item.get("_bbox") + if not bbox: + continue + bounds = _bounds(bbox, half_tol) + 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))) + break + + # 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"])) + + result = {} + for tid, punkte in punkte_je_kreisel.items(): + cap, drehrichtung = kreisel[tid] + acx = (cap["a"][0] + cap["b"][0]) / 2.0 + acy = (cap["a"][1] + cap["b"][1]) / 2.0 + achs_winkel = math.atan2(cap["b"][1] - cap["a"][1], cap["b"][0] - cap["a"][0]) + + def _winkel(pxy): + return math.atan2(pxy[2] - acy, pxy[1] - acx) - achs_winkel + + reverse = (drehrichtung or "UZS").strip().upper() != "GUZ" + sortiert = sorted(punkte, key=lambda p: _winkel(p), reverse=reverse) + n = len(sortiert) + umlauf = [] + for i, p in enumerate(sortiert): + vorgaenger = sortiert[(i - 1) % n][0] if n > 1 else None + nachfolger = sortiert[(i + 1) % n][0] if n > 1 else None + umlauf.append((p[0], vorgaenger, nachfolger)) + result[tid] = umlauf + if dbg: + dbg(f" Kreisel {tid} ({drehrichtung}): Umlauf " + f"{' -> '.join(p[0] for p in sortiert)}") + + return result diff --git a/tests/test_run_all.lsp b/tests/test_run_all.lsp index 52a093c..c97976e 100644 --- a/tests/test_run_all.lsp +++ b/tests/test_run_all.lsp @@ -181,7 +181,6 @@ (progn (setq auto-export *ssg-run-all-auto-export*) (setq *ssg-run-all-auto-export* nil) ;; Vorauswahl nur einmal gueltig - (makunbound '*ssg-run-all-auto-export*) ) (setq auto-export nil) ) diff --git a/tests/test_unit.lsp b/tests/test_unit.lsp index bc3264e..406e669 100644 --- a/tests/test_unit.lsp +++ b/tests/test_unit.lsp @@ -386,6 +386,44 @@ ) +;; ------------------------------------------------------------ +;; TESTS: ssg_ks_insert.lsp - Sepliste-Serialisierung +;; ------------------------------------------------------------ +(defun tu-tests-sepliste () + (princ "\n[ssg_ks_insert/sepliste]") + + ;; ssg-sepliste-chunk-string: Stuecke <= n, nichts verloren + (tu-eq "ssg-sepliste-chunk-string/glatt" '("ab" "cd") (ssg-sepliste-chunk-string "abcd" 2)) + (tu-eq "ssg-sepliste-chunk-string/rest" '("ab" "cd" "e") (ssg-sepliste-chunk-string "abcde" 2)) + (tu-eq "ssg-sepliste-chunk-string/kuerzer" '("abc") (ssg-sepliste-chunk-string "abc" 10)) + + ;; ssg-sepliste-split-feld: Trenner-Aufteilung, inkl. leeres Feld + (tu-eq "ssg-sepliste-split-feld/normal" '("SEP" "1" "10.00" "20.00") + (ssg-sepliste-split-feld "SEP:1:10.00:20.00" ":")) + + ;; ssg-sepliste->string: Serialisierung eines Eintrags + (tu-eq "ssg-sepliste->string/ein-eintrag" + "SEP:1:10.00:20.00" + (ssg-sepliste->string '(("SEP" 1 10.0 20.0)))) + (tu-eq "ssg-sepliste->string/mehrere" + "SEP:1:10.00:20.00,AS:2:30.00:40.00" + (ssg-sepliste->string '(("SEP" 1 10.0 20.0) ("AS" 2 30.0 40.0)))) + + ;; ssg-sepliste-string->liste: Gegenstueck zu ssg-sepliste->string + (tu-eq "ssg-sepliste-string->liste/leer" nil (ssg-sepliste-string->liste "")) + (tu-eq "ssg-sepliste-string->liste/ein-eintrag" + '(("SEP" 1 10.0 20.0)) + (ssg-sepliste-string->liste "SEP:1:10.00:20.00")) + + ;; Roundtrip ->string->liste: entscheidend, da der Export genau diesen + ;; Weg (XDATA schreiben/lesen) fuer die Separator-Reihenfolge nutzt. + (setq _sep '(("SEP" 1 10.0 20.0) ("AS" 2 30.5 40.5) ("ES" 3 -5.0 0.0))) + (tu-eqf "sepliste-roundtrip/mehrere-typen" + _sep + (ssg-sepliste-string->liste (ssg-sepliste->string _sep))) +) + + ;; ------------------------------------------------------------ ;; REPORT SCHREIBEN ;; ------------------------------------------------------------ @@ -428,6 +466,7 @@ (tu-tests-gf) (tu-tests-omni) (tu-tests-kreisel) + (tu-tests-sepliste) (princ "\n\n----------------------------------------------------------------") (princ (strcat "\n Gesamt: " (itoa (+ *tu-pass* *tu-fail*))