Sepliste-XDATA fuer Separator/AS/ES-Reihenfolge + Unit-Tests, makunbound-Fix
Sammelt beim Bau von VF_n/GF_n-Ketten die Baureihenfolge der Separator-/ AS-/ES-Sub-Bloecke vor dem Block-Sweep und schreibt sie als XDATA auf den fertigen Wrapper (ssg_ks_insert.lsp); export.lsp liest sie fuer das neue "sepliste"-Feld im CSV-Export, export_neighbors.py nutzt es als Fallback zur BBox-Kollisionspruefung. Dazu Unit-Tests fuer die reinen Serialisierungs- funktionen (Chunking, Roundtrip) in test_unit.lsp. Ausserdem: makunbound (existiert nicht in BricsCAD-AutoLISP) aus test_run_all.lsp entfernt - verursachte Laufzeitfehler bei SSG_RUN_ALL_TESTS_EXPORT/_OFFEN. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
+37
-1
@@ -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
|
||||
"}"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+7
-1
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+139
-17
@@ -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
|
||||
"<strecke_id>#<lfdnr>" -> 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
|
||||
|
||||
@@ -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 "<strecke_teileid>#<lfdnr>" - 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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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*))
|
||||
|
||||
Reference in New Issue
Block a user