From 52264f153114bc300300c32d6cfa81246ac2f0da Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Thu, 23 Jul 2026 14:38:08 +0200 Subject: [PATCH] Export: Insertpoint (24-Zeichen KOS) und K1-K4 (12-Zeichen Position) je Block csv:block-to-json ergaenzt fuer jeden Block einen Insertpoint-String (Position+Rotation als Z-Quaternion, csv:kos-encode) sowie K1-K4 als reine Positions-Strings (csv:trans-encode, 0.1 mm). KS_EIN/KS_AUS werden auf K1/K2 gemappt, falls keine echten K1/K2 vorhanden sind. Neue CSV- Spalten Insertpoint;K1;K2;K3;K4 in export_csv.py. Co-Authored-By: Claude Opus 4.8 (1M context) --- Lisp/export.lsp | 169 +++++++++++++++++++++++++++++++++++++++++++++- lib/export_csv.py | 43 ++++++++++-- 2 files changed, 206 insertions(+), 6 deletions(-) diff --git a/Lisp/export.lsp b/Lisp/export.lsp index 269d226..0455cdb 100644 --- a/Lisp/export.lsp +++ b/Lisp/export.lsp @@ -90,6 +90,158 @@ ) ) +;; ============================================================ +;; KOS-KOMPRIMIERUNG (Position + Rotation als 24-Zeichen Base64-String) +;; ============================================================ +;; Kodiert Position (x,y,z in mm) und Rotation (als Quaternion qx,qy,qz,qw) +;; verlustarm (24 Bit Fixed-Point je Wert) in einen 24-Zeichen Base64-String. +;; Ported aus einer interaktiven Referenz-Routine (c:ExportKOS), hier als +;; reine Funktion fuer den automatischen Export nutzbar. Alle Elemente in +;; diesem Projekt werden ausschliesslich um die Z-Achse gedreht (siehe +;; DREHUNG-Konvention weiter unten), daher genuegt eine Halbwinkel-Quaternion +;; fuer eine reine Z-Rotation (csv:z-angle-to-quat) -- keine vollstaendige +;; Rotationsmatrix/Quaternion-Extraktion noetig. +(setq *csv-b64-chars* "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") + +;; --- Wert auf [min-val, max-val] begrenzen --- +(defun csv:clamp (val min-val max-val) + (max min-val (min val max-val)) +) + +;; --- Rotationswinkel (Grad, reine Z-Drehung) -> Quaternion (qx qy qz qw) --- +(defun csv:z-angle-to-quat (deg / rad) + (setq rad (/ (* deg pi) 180.0)) + (list 0.0 0.0 (sin (/ rad 2.0)) (cos (/ rad 2.0))) +) + +;; --- Liste vorzeichenbehafteter 24-Bit-Integer -> Base64 (je 4 Zeichen) --- +;; Gemeinsamer Kodierer fuer csv:kos-encode und csv:trans-encode. +(defun csv:b64-encode-ints (int-list / b64-str val) + (setq b64-str "") + (foreach val int-list + (if (< val 0) (setq val (+ val 16777216))) ; Zweierkomplement (24 Bit) + (setq b64-str (strcat b64-str + (substr *csv-b64-chars* (1+ (lsh val -18)) 1) + (substr *csv-b64-chars* (1+ (logand (lsh val -12) 63)) 1) + (substr *csv-b64-chars* (1+ (logand (lsh val -6) 63)) 1) + (substr *csv-b64-chars* (1+ (logand val 63)) 1) + )) + ) + b64-str +) + +;; --- Position (mm) + Quaternion -> 24-Zeichen Base64-String --- +;; Fixed-Point: x,y,z (Faktor 1000 = mm) und qx,qy,qz werden je auf 24 Bit +;; gerundet (6 Werte * 4 Zeichen = 24 Zeichen). qw wird nicht mitkodiert, +;; sondern nur zur Vorzeichen-Normierung verwendet (q und -q sind dieselbe +;; Rotation). Fuer den Insertpoint (volle Lage), siehe csv:trans-encode fuer +;; reine Positionen (K1-K4). +(defun csv:kos-encode (x y z qx qy qz qw) + (if (< qw 0.0) + (setq qx (- qx) qy (- qy) qz (- qz)) + ) + (csv:b64-encode-ints (list + (fix (csv:clamp (* x 1000.0) -8388607 8388607)) + (fix (csv:clamp (* y 1000.0) -8388607 8388607)) + (fix (csv:clamp (* z 1000.0) -8388607 8388607)) + (fix (csv:clamp (* qx 8388607.0) -8388607 8388607)) + (fix (csv:clamp (* qy 8388607.0) -8388607 8388607)) + (fix (csv:clamp (* qz 8388607.0) -8388607 8388607)) + )) +) + +;; --- Position (nur x,y,z) -> 12-Zeichen Base64-String --- +;; Fixed-Point Faktor 10.0 (0.1 mm Genauigkeit, Bereich +-838 m), 3 Werte * +;; 4 Zeichen = 12 Zeichen. Fuer die Koordinatensysteme K1-K4 (nur Position, +;; keine Rotation). Ported aus der Referenz-Routine c:ExportTrans. +(defun csv:trans-encode (x y z) + (csv:b64-encode-ints (list + (fix (csv:clamp (* x 10.0) -8388607 8388607)) + (fix (csv:clamp (* y 10.0) -8388607 8388607)) + (fix (csv:clamp (* z 10.0) -8388607 8388607)) + )) +) + +;; --- Lokalen Punkt (relativ zum Block-Ursprung) unter Beruecksichtigung der +;; Eltern-Rotation (reine Z-Drehung um den Einfuegepunkt) in Weltkoordinaten +;; umrechnen. Rueckgabe: (x y z) --- +(defun csv:local-to-world (parent-pt parent-rot-rad local-pt / c s lx ly lz pz) + (setq c (cos parent-rot-rad) s (sin parent-rot-rad)) + (setq lx (car local-pt) ly (cadr local-pt)) + (setq lz (if (caddr local-pt) (caddr local-pt) 0.0)) + (setq pz (if (caddr parent-pt) (caddr parent-pt) 0.0)) + (list + (+ (car parent-pt) (- (* lx c) (* ly s))) + (+ (cadr parent-pt) (+ (* lx s) (* ly c))) + (+ pz lz) + ) +) + +;; --- K1-K4-Koordinatensysteme eines Blocks als Positions-Strings lesen --- +;; ename = Entity-Name des platzierten INSERT (Omniflo Bogen/Weiche/Gerade). +;; pt/rot-rad = bereits ermittelte Welt-Position/-Rotation (radiant) des +;; Eltern-INSERT (siehe csv:block-to-json), keine erneute Abfrage. +;; Sucht in der Block-DEFINITION (wie omni:get-block-hoehe) nach direkten +;; Sub-INSERTs "K1".."K4" und rechnet deren lokale Position ueber die +;; Eltern-Rotation (reine Z-Drehung) in Weltkoordinaten um. Es wird nur die +;; POSITION kodiert (12-Zeichen-String, csv:trans-encode) - keine Rotation. +;; Zusaetzlich werden Strecken-Koordinatensysteme KS_EIN->K1 und KS_AUS->K2 +;; (inkl. der KSYS_-Varianten, siehe ks-normalize-name in vf_core.lsp) gemappt, +;; sofern kein echter K1/K2 am Block vorhanden ist (echte K-Bloecke haben +;; Vorrang: sie werden vorne angehaengt, csv:k-kos liest per assoc den ersten). +;; Rueckgabe: Assoc-Liste (("K1" . "") ("K2" . ...) ...), nur +;; fuer tatsaechlich vorhandene K-Bloecke (nicht jedes Element hat alle 4). +(defun csv:get-k-kos-strings (ename pt rot-rad / ed bname blk-tbl sub-ent sub-ed + sub-bname sub-pt world-pt target is-real result) + (setq ed (entget ename)) + (setq bname (cdr (assoc 2 ed))) + (setq blk-tbl (tblsearch "BLOCK" bname)) + (setq result nil) + (if blk-tbl + (progn + (setq sub-ent (entnext (cdr (assoc -2 blk-tbl)))) + (while sub-ent + (setq sub-ed (entget sub-ent)) + (if (= (cdr (assoc 0 sub-ed)) "INSERT") + (progn + (setq sub-bname (strcase (cdr (assoc 2 sub-ed)))) + ;; Ziel-Slot bestimmen: echte K1-K4 direkt, KS_EIN/KS_AUS gemappt. + (setq is-real (wcmatch sub-bname "K1,K2,K3,K4")) + (setq target + (cond + (is-real sub-bname) + ((or (= sub-bname "KS_EIN") (= sub-bname "KSYS_EIN")) "K1") + ((or (= sub-bname "KS_AUS") (= sub-bname "KSYS_AUS")) "K2") + (t nil))) + ;; Echte K-Bloecke immer aufnehmen (vorne = Vorrang bei assoc); + ;; KS-abgeleitete nur, wenn der Slot noch nicht belegt ist. + (if (and target (or is-real (not (assoc target result)))) + (progn + (setq sub-pt (cdr (assoc 10 sub-ed))) + (setq world-pt (csv:local-to-world pt rot-rad sub-pt)) + (setq result (cons + (cons target + (csv:trans-encode + (car world-pt) (cadr world-pt) (caddr world-pt))) + result)) + ) + ) + ) + ) + (setq sub-ent (entnext sub-ent)) + ) + ) + ) + result +) + +;; --- Wert aus csv:get-k-kos-strings nach Name lesen, "" falls nicht vorhanden --- +(defun csv:k-kos (k-list name / found) + (setq found (assoc name k-list)) + (if found (cdr found) "") +) + + ;; --- Mindesthoehe der Bounding-Box (Z-Ausdehnung), aus cfg/export.cfg --- ;; [Boundingbox] bb_minimum_height_mm, Default 1 (mm) falls nicht gesetzt. ;; Verhindert dz=0 bei flachen 2D-Bloecken (Draufsicht ohne Z-Ausdehnung). @@ -166,7 +318,12 @@ ;; --- Einen INSERT-Block als JSON-Objekt schreiben --- ;; include-bbox = T -> zusaetzlich Bounding-Box (Mitte + Ausdehnung) ermitteln ;; und als "bbox"-Objekt anhaengen (nur fuer EXPORTCSV, nicht fuer EXPORTSIVAS). -(defun csv:block-to-json (ename include-bbox / ed blk-name layer pt rotation handle attribs bbox bbox-json) +;; "insertpoint" (KOS-String von Position+Rotation des Blocks selbst) und +;; "k1".."k4" (KOS-Strings der Koordinatensysteme im Block, siehe +;; csv:get-k-kos-strings) werden immer geschrieben, unabhaengig von +;; 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) (setq ed (entget ename)) (setq blk-name (cdr (assoc 2 ed))) (setq layer (cdr (assoc 8 ed))) @@ -175,6 +332,11 @@ (setq handle (cdr (assoc 5 ed))) (if (null rotation) (setq rotation 0.0)) (setq attribs (csv:read-attribs ename)) + (setq insert-quat (csv:z-angle-to-quat (* (/ rotation pi) 180.0))) + (setq insert-kos (csv:kos-encode + (car pt) (cadr pt) (if (caddr pt) (caddr pt) 0.0) + (nth 0 insert-quat) (nth 1 insert-quat) (nth 2 insert-quat) (nth 3 insert-quat))) + (setq k-list (csv:get-k-kos-strings ename pt rotation)) (setq bbox-json "") (if include-bbox (progn @@ -204,6 +366,11 @@ ",\"z\":" (rtos (if (caddr pt) (caddr pt) 0.0) 2 4) ",\"rotation\":" (rtos (* (/ rotation pi) 180.0) 2 4) ",\"attribs\":" attribs + ",\"insertpoint\":\"" insert-kos "\"" + ",\"k1\":\"" (csv:k-kos k-list "K1") "\"" + ",\"k2\":\"" (csv:k-kos k-list "K2") "\"" + ",\"k3\":\"" (csv:k-kos k-list "K3") "\"" + ",\"k4\":\"" (csv:k-kos k-list "K4") "\"" bbox-json "}" ) diff --git a/lib/export_csv.py b/lib/export_csv.py index 7b745a2..7debb7a 100644 --- a/lib/export_csv.py +++ b/lib/export_csv.py @@ -11,12 +11,18 @@ Aufruf: python export_csv.py CSV-Format: - Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Nachbarn;Fehler;Merkmale + Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox; + Insertpoint;K1;K2;K3;K4;Nachbarn;Fehler;Merkmale Position und Boundingbox stammen aus der von export.lsp (csv:get-bbox, per vla-getboundingbox) ermittelten Bounding-Box je Block: Position = Mittenkoordinate x, y, z Boundingbox = Ausdehnung (Laenge, Breite, Hoehe) in x, y, z +Insertpoint = KOS-String (24-Zeichen Base64, Position+Rotation als Quaternion, +siehe csv:kos-encode in export.lsp) des Block-Einfuegepunkts selbst. +K1-K4 = Positions-Strings der im Block enthaltenen Koordinatensysteme K1..K4 +(12-Zeichen Base64, nur Position ohne Rotation, siehe csv:trans-encode; +nur bei Omniflo Bogen/Weiche/Gerade vorhanden, sonst leer). Planquadrat wird aus der x/y-Koordinate des Blocks berechnet, siehe export_planquadrat.py ([Planquadrate] in cfg/export.cfg). Nachbarn = kommaseparierte TeileId-Liste ueberschneidender Elemente (Bounding- @@ -34,7 +40,10 @@ Omniflo-Elemente brauchen mindestens einen Partner, Gefaellestrecke/ Foerderer/Strecke-Modul mindestens zwei (siehe export_neighbors.py MIN_PARTNER). Position, Boundingbox, Planquadrat, Nachbarn und Fehler sind nur fuer -EXPORTCSV vorhanden (nicht EXPORTSIVAS) - siehe csv:run-export. +EXPORTCSV vorhanden (nicht EXPORTSIVAS) - siehe csv:run-export. Insertpoint/ +K1-K4 stehen zwar in export_raw.json fuer jeden Export zur Verfuegung (siehe +csv:block-to-json), werden aber ebenfalls nur von diesem Skript (EXPORTCSV) +als CSV-Spalten ausgegeben. """ import json @@ -243,21 +252,34 @@ def build_kreisel_merkmale(block): # --------------------------------------------------------------------------- def bbox_columns(block): - """Liest die von export.lsp (csv:get-bbox) ermittelte Bounding-Box. + """Liest die von export.lsp (csv:get-bbox) ermittelte Bounding-Box, sowie + die KOS-Strings von Insertpoint und K1-K4 (csv:kos-encode/csv:block-to-json). Rueckgabe: dict mit "position" (Mittenkoordinate x,y,z) und "boundingbox" (Ausdehnung x,y,z) als Strings, oder leere Strings falls kein bbox-Feld vorhanden ist (z.B. EXPORTSIVAS oder die synthetische Omniflo-Sum-Zeile). + "insertpoint" ist ein 24-Zeichen Base64-KOS-String (Position + Rotation + als Quaternion, csv:kos-encode); "k1".."k4" sind 12-Zeichen Positions- + Strings (nur Position, csv:trans-encode) - leer falls das jeweilige + Koordinatensystem am Block nicht existiert. "_bbox" enthaelt zusaetzlich die rohen Zahlenwerte fuer die Nachbarschafts- erkennung (siehe export_neighbors.py), wird nicht in die CSV geschrieben. """ + kos_cols = { + "insertpoint": block.get("insertpoint", ""), + "k1": block.get("k1", ""), + "k2": block.get("k2", ""), + "k3": block.get("k3", ""), + "k4": block.get("k4", ""), + } bbox = block.get("bbox") if not bbox: - return {"position": "", "boundingbox": "", "_bbox": None} + return {"position": "", "boundingbox": "", "_bbox": None, **kos_cols} return { "position": f'{bbox.get("cx", 0):.2f}, {bbox.get("cy", 0):.2f}, {bbox.get("cz", 0):.2f}', "boundingbox": f'{bbox.get("dx", 0):.2f}, {bbox.get("dy", 0):.2f}, {bbox.get("dz", 0):.2f}', "_bbox": bbox, + **kos_cols, } @@ -468,6 +490,11 @@ def process_blocks(blocks, lookup): len_ap110_mm, len_ap60_mm), "position": "", "boundingbox": "", + "insertpoint": "", + "k1": "", + "k2": "", + "k3": "", + "k4": "", }) neighbor_ids = compute_neighbor_ids( @@ -495,6 +522,11 @@ def format_csv_line(item): f';1' f';"{csv_quote(item["position"])}"' f';"{csv_quote(item["boundingbox"])}"' + f';"{csv_quote(item["insertpoint"])}"' + f';"{csv_quote(item["k1"])}"' + f';"{csv_quote(item["k2"])}"' + f';"{csv_quote(item["k3"])}"' + f';"{csv_quote(item["k4"])}"' f';"{csv_quote(item["nachbarn"])}"' f';"{csv_quote(item["fehler"])}"' f';{merkmale_json}' @@ -528,7 +560,8 @@ def main(): items = process_blocks(blocks, lookup) - header = "Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Nachbarn;Fehler;Merkmale" + header = ("Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;" + "Insertpoint;K1;K2;K3;K4;Nachbarn;Fehler;Merkmale") with open(output_csv, "w", encoding="utf-8") as f: f.write(header + "\n") for item in items: