[ADD] lib/dxf_anlage_export.py: gebaute und von Hand nachbearbeitete Anlage aus der Zeichnung zurueck in Nachbau-Testdaten (Kreisel, GF, VF standard/etage, VF-Linienzug als unveraendertes Journal, Einzelbloecke) - tests/testdata/dematic_arvato.json jetzt aus Dematic_TST.dxf; [ADD] dxf_scan_components.py: Gefaellestrecken aus Staustrecken-LINEs (Layer 6-SP), Start/Ende auf die Kreisel-Ketten verlaengert, AS-Element und Separator auf der Achse werden verbraucht, Steigfoerderer erkannt, --ohne-hoehen und --kreisel-hoehe; [ADD] vf_spec.lsp: Replay-Kern vsp-bau-journal aus vsp-bau-aus-spec herausgeloest (auch fuer aufgezeichnete Journale); [CHANGE] TEST_DEMATIC_ARVATO baut GF, VF standard/etage und Linienzug-Journale (mubea:gf-bauen/-vario-bauen wiederverwendet); [FIX] GF-Winkel aus Kreisel-Hoehen: AS/ES-Abfall mit falschem Vorzeichen an gf-winkel-berechnen uebergeben (3.79 statt ~2.49 Grad, GF endete 258 mm zu tief); [ADD] Tests test_dxf_scan_gf.py, test_dxf_anlage_export.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 13:35:46 +02:00
parent 2f9cb7eaed
commit ba20d63f63
9 changed files with 2024 additions and 1328 deletions
+201 -24
View File
@@ -1,30 +1,53 @@
;; ============================================================
;; test_dematic_arvato.lsp - Nachbau des Abschnitts Flowpuffer/Kreisel der
;; Anlage Dematic/Arvato aus der Scanner-Vorlage.
;; Anlage Dematic/Arvato.
;;
;; Datenquelle: tests/testdata/dematic_arvato.json, erzeugt mit
;; python lib/dxf_scan_components.py \
;; C:/develop/sps_skel/data/Dematic_arvato.dxf data \
;; tests/testdata/dematic_arvato.json --projekt Arvato \
;; --fenster -3000 159000 49000 193000
;; Datenquelle: tests/testdata/dematic_arvato.json. Entstehung in zwei
;; Stufen:
;; 1. Vorlage aus der Fremdzeichnung (Scanner, raet vieles):
;; python lib/dxf_scan_components.py \
;; C:/develop/sps_skel/data/Dematic_arvato_reduced.dxf data \
;; tests/testdata/dematic_arvato.json --projekt Arvato \
;; --fenster -2534 -4549 49466 29451 --ohne-hoehen \
;; --kreisel-hoehe 35860.37 12519.72 2437 \
;; --kreisel-hoehe 36661.24 24878.8 1826
;; 2. Vorlage mit diesem Treiber bauen, in BricsCAD von Hand korrigieren
;; und ergaenzen (VF-Ketten, Hoehen, Sensoren aufgeraeumt), als DXF
;; speichern und daraus die Testdaten neu schreiben - AKTUELLER STAND:
;; python lib/dxf_anlage_export.py \
;; C:/develop/sps_skel/data/Dematic_TST.dxf \
;; tests/testdata/dematic_arvato.json --projekt Arvato
;;
;; Die Datei ist eine VORLAGE fuer den Nachbau, kein fertiger Testfall:
;; Hoehen fehlen (flaches 2D-Schema, alles auf Z=0), Seiten/Winkel einiger
;; Bloecke sind geraten (siehe "comment" je Eintrag).
;;
;; Eintragsarten (Schema von dxf_scan_components.py):
;; Eintragsarten:
;; "function": "insert" ohne "block" -> Kreisel (hundm05:bau-kreisel ->
;; kreisel-insert-script; x/y = Einfuegepunkt
;; einen Radius vor der AN8-Radmitte, abstand =
;; Mittenabstand AN8-SP8)
;; "function": "insert" + "block" -> Einzelblock (hundm05:bau-sensor,
;; Fallback Separator_SP bei unbekanntem Block)
;; "typ": "gerade" -> waagrechter Foerderer Antrieb->Umlenkung -
;; dafuer gibt es noch keinen Bau-Pfad, wird
;; gemeldet und uebersprungen
;; kreisel-insert-script; x/y = Einfuegepunkt,
;; abstand = Mittenabstand AN8-SP8)
;; "function": "insert" + "block" -> Einzelblock (hundm05:bau-sensor)
;; "typ": "standard"/"etage" + "test_id"
;; -> VarioFoerderer ueber mubea:vario-bauen
;; (mubea-Schema: deltaL, deltaH, richtung,
;; winkel, L_GF1, seite, hz, x/y/z), optional
;; "motorseite"
;; "typ": "linienzug_journal"
;; -> Linienzug-Kette: das aufgezeichnete Eingabe-
;; Journal (SSG_VF_EDIT) wird 1:1 abgespielt
;; (vsp-bau-journal) - keine Spec, weil der
;; Dialog neuer ist als die Spec-Grammatik
;; "modus": 1 -> Gefaellestrecke ueber mubea:gf-bauen
;; (start_mm + deltaL_mm, kein Endpunkt). Mit
;; hoehe_ende_mm (Hoehe des Kreisels am Ende)
;; rechnet dematic_arvato:gf-winkel den Winkel,
;; sonst gilt winkel_grad.
;; "typ": "gerade" -> waagrechter Foerderer aus der Scanner-Vorlage -
;; kein Bau-Pfad, wird gemeldet und uebersprungen
;;
;; Reihenfolge wie mubea.json (VF vor GF): gf-init-bibliothek setzt die
;; globalen AS/ES-Masse um, siehe mubea:ensure-gf-init.
;;
;; Keine eigene Bau-Logik: hundm05:bau-kreisel/-sensor aus
;; tests/test_hundm05.lsp wird mitgeladen (die Datei definiert nur Funktionen).
;; tests/test_hundm05.lsp und mubea:gf-bauen/-vario-bauen aus
;; tests/test_mubea.lsp werden mitgeladen (beide Dateien definieren nur
;; Funktionen), Linienzug-Ketten laufen ueber vsp-bau-journal (vf_spec.lsp).
;;
;; Aufruf in BricsCAD:
;; (load (strcat (getenv "DXFMAKRO") "/tests/test_dematic_arvato.lsp"))
@@ -40,12 +63,30 @@
(ssg-ensure "KreiselInsert")
(ssg-ensure "VarioFoerderer")
(ssg-ensure "SSG_LIB_Commands")
(ssg-ensure "Gefaellestrecke")
(if (null (car (atoms-family 1 '("HUNDM05:BAU-SENSOR"))))
(load (strcat (getenv "DXFMAKRO") "/tests/test_hundm05.lsp")))
(if (null (car (atoms-family 1 '("MUBEA:VARIO-BAUEN"))))
(load (strcat (getenv "DXFMAKRO") "/tests/test_mubea.lsp")))
(defun dematic_arvato:datei ()
(strcat (getenv "DXFMAKRO") "/tests/testdata/dematic_arvato.json"))
;; Record um den gebauten Block ergaenzen (oder als Abbruch markieren).
(defun dematic_arvato:rec-block (rec ent fehlertext / ed)
(if (and ent (= (type ent) 'ENAME))
(progn
(setq ed (entget ent))
(princ (strcat " -> Block " (cdr (assoc 2 ed))))
(setq rec (vsp-set rec "block_name" (cdr (assoc 2 ed))))
(setq rec (vsp-set rec "block_handle" (cdr (assoc 5 ed))))
(setq rec (vsp-set rec "insert_point" (cdr (assoc 10 ed))))
(setq rec (vsp-set rec "attribs" (ssg-attrib-read ent)))
(vsp-set rec "status" "executed"))
(progn
(princ (strcat " FEHLER: " fehlertext))
(vsp-set (vsp-set rec "status" "abbruch") "fehler_text" fehlertext))))
;; Foerderer "gerade": noch kein Bau-Pfad - nur melden.
(defun dematic_arvato:gerade-melden (obj / tid rec)
(setq tid (ssg-val obj "test_id"))
@@ -56,15 +97,146 @@
(vsp-set (vsp-set rec "status" "uebersprungen") "fehler_text"
"kein Bau-Pfad fuer typ gerade"))
(defun dematic_arvato:bau-eintrag (obj / kind res)
;; Gefaellewinkel aus Start- und Endhoehe - derselbe Loeser wie Modus 3
;; (gf-winkel-berechnen, Gefaellestrecke.lsp) mit den AS/ES-Massen der
;; aktuellen 2D/3D-Bibliothek. Die Kette ist eine einzige Gerade plus der
;; eine Separator vor dem ES (extra-sep 1); ihre Laenge ohne AS/ES/Separator
;; ist deltaL - aus-dx - ein-dx - 300, damit ergibt der Loeser als
;; Gesamtlaenge wieder genau deltaL (deltaL reicht von AS- bis ES-Ursprung).
;; VORZEICHEN: vf-set-as-masse (vf_core) misst dz als KS_AUS.z - KS_EIN.z
;; (bei AS/ES negativ), gf-winkel-berechnen erwartet die Konvention von
;; gf-init-bibliothek (KS_EIN.z - KS_AUS.z, positiv) und zieht den Wert ab.
;; Mit der VF-Konvention wurde der feste AS/ES-Abfall addiert statt
;; abgezogen - real gebaut: 3.79 statt ~2.49 Grad, GF endete 258 mm unter
;; dem Kreisel (HOEHE_BIS 1568 statt 1826). Darum hier der Betrag.
(defun dematic_arvato:gf-winkel (deltaL hz as-seite es-seite as-winkel es-winkel
z-start z-ende / winkel)
(mubea:ensure-gf-init)
(vf-set-as-masse as-winkel as-seite)
(vf-set-es-masse es-winkel es-seite)
(setq aus-dz (abs (float aus-dz)) ein-dz (abs (float ein-dz)))
(setq winkel
(gf-winkel-berechnen
(list (list "Linie" hz (- deltaL (float aus-dx) (float ein-dx) 300.0) 0 "links"))
(- z-start z-ende) 1))
(vf-set-as-masse "90" as-seite)
(vf-set-es-masse "90" es-seite)
winkel)
;; Gefaellestrecke (Modus 1) bauen - Felder wie mubea:build-gf, ohne dessen
;; anzahl/delta-Vervielfaeltigung (jede GF steht einzeln in der Datei).
;; Mit "hoehe_ende_mm" (Hoehe des Kreisels am Ende) wird der Winkel daraus
;; gerechnet, sonst gilt "winkel_grad" (Default 3.0).
(defun dematic_arvato:bau-gf (obj / tid start deltaL hz winkel z-ende
as-seite es-seite as-winkel es-winkel rec)
(setq tid (ssg-val obj "test_id")
start (ssg-val obj "start_mm")
deltaL (ssg-val obj "deltaL_mm")
hz (ssg-val obj "hz_grad")
winkel (ssg-val obj "winkel_grad")
z-ende (ssg-val obj "hoehe_ende_mm")
as-seite (cond ((ssg-val obj "as_seite")) ("links"))
es-seite (cond ((ssg-val obj "es_seite")) ("links"))
as-winkel (cond ((ssg-val obj "as_winkel")) ("90"))
es-winkel (cond ((ssg-val obj "es_winkel")) ("90")))
(setq rec (vsp-rec-neu (if tid tid "(ohne id)")))
(setq rec (vsp-set rec "kind" "gefaellestrecke"))
(setq rec (vsp-set rec "dimension" (vsp-dim-text)))
(cond
((not (and (= (type start) 'LIST) (= (length start) 3)
(numberp deltaL) (numberp hz)))
(princ (strcat "\n [GF] " (if tid tid "?")
" - FEHLER: start_mm, deltaL_mm und hz_grad muessen gesetzt sein"))
(vsp-set (vsp-set rec "status" "spec-fehler") "fehler_text"
"start_mm, deltaL_mm oder hz_grad fehlt bzw. ist ungueltig"))
((and (numberp z-ende) (>= z-ende (caddr start)))
(princ (strcat "\n [GF] " (if tid tid "?")
" - FEHLER: hoehe_ende_mm liegt nicht unter der Starthoehe"))
(vsp-set (vsp-set rec "status" "spec-fehler") "fehler_text"
"hoehe_ende_mm >= start_mm.z - kein Gefaelle"))
(T
(setq winkel
(cond
((numberp z-ende)
(dematic_arvato:gf-winkel (float deltaL) (float hz) as-seite es-seite
as-winkel es-winkel
(float (caddr start)) (float z-ende)))
((numberp winkel) (float winkel))
(T 3.0)))
;; Winkel und Soll-Endhoehe als Meldung (das Record-Schema aus vf_spec
;; kennt dafuer kein Feld); die Ist-Endhoehe steht in attribs HOEHE_BIS.
(setq rec (vsp-set rec "meldungen"
(list (strcat "winkel_grad " (rtos winkel 2 3)
(if (numberp z-ende)
(strcat " aus Hoehe " (rtos (caddr start) 2 1)
" -> " (rtos z-ende 2 1))
"")))))
(dematic_arvato:rec-block rec
(mubea:gf-bauen tid (mapcar 'float start) (float deltaL) (float hz)
as-seite es-seite as-winkel es-winkel winkel)
"gefaellestrecke-einfuegen lieferte keinen GF-Block"))))
;; VarioFoerderer "standard"/"etage" - mubea-Schema, Bau ueber
;; mubea:vario-bauen. "motorseite" wirkt wie im Edit-Dialog ueber den
;; Override *vf-motorseite* (vf-block-erstellen liest ihn).
(defun dematic_arvato:bau-vf (obj / tid typ rec ent)
(setq tid (ssg-val obj "test_id")
typ (cond ((ssg-val obj "typ")) ("standard")))
(setq rec (vsp-rec-neu (if tid tid "(ohne id)")))
(setq rec (vsp-set rec "kind" "vario"))
(setq rec (vsp-set rec "dimension" (vsp-dim-text)))
(setq *vf-motorseite* (ssg-val obj "motorseite"))
(setq ent
(vl-catch-all-apply 'mubea:vario-bauen
(list tid typ (ssg-val obj "richtung")
(float (ssg-val obj "deltaL")) (float (ssg-val obj "deltaH"))
(ssg-val obj "winkel") (float (cond ((ssg-val obj "L_GF1")) (0.0)))
(ssg-val obj "seite") (ssg-val obj "hz")
(list (float (ssg-val obj "x")) (float (ssg-val obj "y"))
(float (ssg-val obj "z"))))))
(setq *vf-motorseite* nil)
(if (vl-catch-all-error-p ent)
(vsp-set (vsp-set rec "status" "abbruch") "fehler_text"
(vl-catch-all-error-message ent))
(dematic_arvato:rec-block rec ent
"kein VF-Block (deltaL beim gewuenschten Winkel nicht loesbar?)")))
;; Linienzug-Kette: aufgezeichnetes Journal ("ART:wert"-Tokens, wie in der
;; SSG_VF_EDIT-XDATA) 1:1 abspielen. Headless-Rahmen wie vsp-bau-datei; er
;; MUSS auch nach einem Fehler zurueck, sonst bleiben getstring/getpoint der
;; Sitzung ersetzt.
(defun dematic_arvato:bau-journal (obj / tid journal res)
(setq tid (cond ((ssg-val obj "test_id")) ("(ohne id)")))
(setq journal (mapcar 'vfl-string->entry (ssg-val obj "journal")))
(vsp-headless-an)
(setq res (vl-catch-all-apply 'vsp-bau-journal
(list tid journal nil
(if journal nil (list "Feld \"journal\" fehlt oder ist leer")))))
(vsp-headless-aus)
(if (vl-catch-all-error-p res)
(vsp-set (vsp-set (vsp-rec-neu tid) "status" "abbruch") "fehler_text"
(vl-catch-all-error-message res))
res))
(defun dematic_arvato:bau-eintrag (obj / kind res typ)
(setq typ (ssg-val obj "typ"))
(cond
((ssg-val obj "modus")
(setq kind "gefaellestrecke")
(setq res (vl-catch-all-apply 'dematic_arvato:bau-gf (list obj))))
((= typ "linienzug_journal")
(setq kind "linienzug")
(setq res (dematic_arvato:bau-journal obj)))
((and (member typ '("standard" "etage")) (ssg-val obj "test_id"))
(setq kind "vario")
(setq res (dematic_arvato:bau-vf obj)))
((and (= (ssg-val obj "function") "insert") (ssg-val obj "block"))
(setq kind "sensor")
(setq res (vl-catch-all-apply 'hundm05:bau-sensor (list obj))))
((= (ssg-val obj "function") "insert")
(setq kind "kreisel")
(setq res (vl-catch-all-apply 'hundm05:bau-kreisel (list obj))))
((= (ssg-val obj "typ") "gerade")
((= typ "gerade")
(setq kind "gerade")
(setq res (dematic_arvato:gerade-melden obj)))
(T
@@ -76,6 +248,7 @@
(princ (strcat "\n [" (strcase kind) "] Ausnahme: "
(vl-catch-all-error-message res)))
(vsp-set (vsp-set (vsp-set (vsp-rec-neu (cond ((ssg-val obj "id"))
((ssg-val obj "test_id"))
((ssg-val obj "block"))
("?")))
"kind" kind) "status" "abbruch")
@@ -85,11 +258,11 @@
(defun c:TEST_DEMATIC_ARVATO ( / datei daten out zaehler st)
(setq datei (dematic_arvato:datei))
(princ "\n\n================================================================")
(princ "\n TEST_DEMATIC_ARVATO - Abschnitt Flowpuffer/Kreisel (Vorlage)")
(princ "\n TEST_DEMATIC_ARVATO - Abschnitt Flowpuffer/Kreisel")
(princ (strcat "\n " datei))
(princ "\n================================================================")
(cond
((null (car (atoms-family 1 '("VSP-REC-NEU"))))
((null (car (atoms-family 1 '("VSP-BAU-JOURNAL"))))
(princ "\n[TEST_DEMATIC_ARVATO] FEHLER: vf_spec.lsp nicht geladen"
" (VarioFoerderer laden).")
(setq *dematic-arvato-records* nil))
@@ -104,6 +277,10 @@
(setvar "OSMODE" 0)
(setvar "ATTREQ" 0)
(setvar "ATTDIA" 0)
;; Vario-Bibliothek (Bogen-/AS-/ES-Masse) wie C:TEST_MUBEA, und die
;; GF-Elementmasse in jeder Zeichnung neu ermitteln.
(if (vsp-lib-noetig-p) (init-bibliothek))
(setq *mubea-gf-init-done* nil)
(setq out '())
(foreach obj daten
(setq out (cons (dematic_arvato:bau-eintrag obj) out)))
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_dxf_anlage_export.py - Gebaute Anlage -> Nachbau-Testdaten
(lib/dxf_anlage_export.py). Laeuft ohne BricsCAD und ohne DXF: die
Wrapper-Daten sind die Werte aus Dematic_TST.dxf, von Hand nachgestellt
(Form wie lese_zeichnung sie liefert).
1. Kreisel: abstand exakt aus der Blockdefinition (AN8 -> SP8), nicht aus
dem ganzzahligen ABSTAND-Attribut.
2. GF auf zwei Kreiselketten: Endhoehe = Hoehe des Kreisels am Ende,
der (falsch) gebaute Winkel wird NICHT uebernommen.
3. GF ohne Kreisel am Ende: gebauter Winkel bleibt.
4. VF "standard": Parameter wie vfs-edit-basis-lesen, L_GF1 aus L_GF_m.
5. VF "linienzug": Journal unveraendert als Token-Liste.
6. Einzelblock: _2D/_3D-Suffix entfaellt.
"""
import os
import sys
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "lib"))
import dxf_anlage_export as dae # noqa: E402
def _insert(name, p, rot=0.0, attribs=None, xdata=None):
return {"name": name, "handle": "1", "p": list(p), "rot": rot,
"attribs": attribs or {}, "xdata": xdata or {}, "ps": 0}
KREISEL_OBEN = _insert("KREISEL_2_STANDARD_33044", (35860.37, 12519.72, 2437.0),
180.0, {"ID": "0002", "NUMMER": "2", "ABSTAND": "33044",
"KREISELART": "STANDARD"})
KREISEL_UNTEN = _insert("KREISEL_3_STANDARD_33470", (36661.24, 24878.8, 1826.0),
180.0, {"ID": "0003", "NUMMER": "3", "ABSTAND": "33470",
"KREISELART": "STANDARD"})
BLOCK_INHALT = {
"KREISEL_2_STANDARD_33044": [{"name": "AN8_3D", "p": [400.0, 0.0, 0.0]},
{"name": "SP8_3D", "p": [33444.28, 0.0, 0.0]}],
"KREISEL_3_STANDARD_33470": [{"name": "AN8_3D", "p": [400.0, 0.0, 0.0]},
{"name": "SP8_3D", "p": [33870.18, 0.0, 0.0]}],
}
def _geo():
return [dae.kreisel_geo(dae.kreisel_eintrag(k, BLOCK_INHALT, "T"))
for k in (KREISEL_OBEN, KREISEL_UNTEN)]
def test_kreisel_abstand_aus_blockdefinition():
k = dae.kreisel_eintrag(KREISEL_OBEN, BLOCK_INHALT, "T")
assert k["id"] == "Kreisel_T_02"
assert k["abstand"] == pytest.approx(33044.28, abs=0.01)
assert (k["x"], k["y"], k["z"], k["rotation"]) == (35860.37, 12519.72, 2437.0, 180.0)
ohne_block = dae.kreisel_eintrag(KREISEL_OBEN, {}, "T")
assert ohne_block["abstand"] == 33044.0
def test_gf_zwischen_zwei_kreiseln_bekommt_endhoehe():
gf = _insert("GF_1", (5833.56, 12919.72, 2437.0), 0.0,
{"ID": "0005", "SEITE_AS": "links", "SEITE_ES": "links",
"HOEHE_BIS_mm": "1568"},
{"SSG_GF_EDIT": ["90", "3.7929", "90", "90", "2437", "11559.08"]})
hinweise = []
e = dae.gf_eintrag(gf, _geo(), "T", hinweise)
assert e["test_id"] == "GF_T_01"
assert e["start_mm"] == [5833.56, 12919.72, 2437.0]
assert e["deltaL_mm"] == 11559.08 and e["hz_grad"] == 90.0
assert (e["kreisel_start"], e["kreisel_ende"]) == ("Kreisel_T_02", "Kreisel_T_03")
assert e["hoehe_ende_mm"] == 1826.0
assert "winkel_grad" not in e
assert hinweise == []
def test_gf_ohne_kreisel_behaelt_gebauten_winkel():
gf = _insert("GF_7", (0.0, 0.0, 1000.0), 0.0, {},
{"SSG_GF_EDIT": ["0", "3", "90", "30", "1000", "5000"]})
e = dae.gf_eintrag(gf, _geo(), "T", [])
assert e["winkel_grad"] == 3.0
assert "hoehe_ende_mm" not in e and "kreisel_ende" not in e
assert e["es_winkel"] == "30"
def test_gf_ohne_xdata_wird_gemeldet():
hinweise = []
assert dae.gf_eintrag(_insert("GF_9", (0, 0, 0)), [], "T", hinweise) is None
assert "SSG_GF_EDIT" in hinweise[0]
def test_vf_standard():
vf = _insert("VF_2", (3314.07, 24478.8, 1826.0), 0.0,
{"ID": "0082", "DELTA_L_mm": "11559", "DELTA_H_mm": "611",
"ANTRIEBFAHRTRICHTUNG": "Auf", "SEITE_AS": "links",
"MOTORSEITE": "rechts", "VF_WINKEL": "15",
"L_GF_m": "1.907,1.907"},
{"SSG_VF_EDIT": ["standard", "270"]})
e = dae.vf_standard_eintrag(vf, "T", "standard")
assert e["test_id"] == "VF_T_02"
assert (e["richtung"], e["deltaL"], e["deltaH"], e["winkel"]) == ("Auf", 11559.0, 611.0, 15)
assert e["L_GF1"] == 1907.0 and e["hz"] == 270.0
assert (e["seite"], e["motorseite"]) == ("links", "rechts")
assert (e["x"], e["y"], e["z"]) == (3314.07, 24478.8, 1826.0)
def test_vf_linienzug_journal_unveraendert():
roh = ["linienzug", "PT:42161.679193,2448.319538,0^JREAL:2177^JSTR:1^JSTR:9",
"0^JSTEP:Linie-VF^JDL:9370.410886"]
vf = _insert("VF_26", (42161.68, 2448.32, 2177.0), 0.0, {"ID": "0184"},
{"SSG_VF_EDIT": roh})
e = dae.vf_journal_eintrag(vf, "T", 1)
assert e["typ"] == "linienzug_journal" and e["test_id"] == "VF_T_LZ_01"
# Chunk-Grenze mitten im Token ("STR:9" + "0") wird korrekt zusammengesetzt
assert e["journal"] == ["PT:42161.679193,2448.319538,0", "REAL:2177", "STR:1",
"STR:90", "STEP:Linie-VF", "DL:9370.410886"]
def test_einzelblock_ohne_dimensionssuffix():
e = dae.einzel_eintrag(_insert("Separator_SP_3D", (3038.72, 12119.73, 2473.0),
0.0, {"ID": "0043", "ZUORDNUNG": "0002"}))
assert e["block"] == "Separator_SP"
assert e["function"] == "insert" and e["z"] == 2473.0
assert "ID 0043" in e["comment"]
def test_reihenfolge_vf_vor_gf():
inserts = [
_insert("GF_1", (5833.56, 12919.72, 2437.0), 0.0, {},
{"SSG_GF_EDIT": ["90", "3", "90", "90", "2437", "11559.08"]}),
_insert("Scanner_3D", (1, 2, 3)),
_insert("VF_2", (0, 0, 0), 0.0, {}, {"SSG_VF_EDIT": ["standard", "0"]}),
KREISEL_OBEN, KREISEL_UNTEN,
_insert("*U12", (0, 0, 0)),
]
eintraege, _ = dae.baue_eintraege(inserts, BLOCK_INHALT, "T")
art = ["K" if "abstand" in e else "GF" if "modus" in e else
"VF" if "typ" in e else e["block"] for e in eintraege]
assert art == ["K", "K", "VF", "GF", "Scanner"]
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_dxf_scan_gf.py - Gefaellestrecken aus Staustrecken-Linien
(gf_eintraege_aus_linien in lib/dxf_scan_components.py). Laeuft ohne
BricsCAD und ohne DXF: die Linien und Ausschleuselemente sind die
gemessenen Werte aus Dematic_arvato_reduced.dxf, von Hand nachgestellt.
1. Fallende Linie mit AS am oberen Ende -> GF, AS wird verbraucht.
2. Der Nachbar-AS der Linie daneben (800 mm Raster, auf der anderen Seite
naeher dran) wird NICHT gegriffen - die Seite aus dem Blocknamen
entscheidet, nicht der kleinste Abstand.
3. Rueckwaerts gezeichnete GF (tiefes Ende zuerst) -> Start bleibt oben.
4. AS am UNTEREN Ende -> Steigfoerderer, keine GF, AS bleibt unverbraucht.
5. Waagrechte Linie -> keine GF.
6. Anschluss an die Kreisel: Start/Ende werden auf die Ketten geschoben,
deltaL reicht von Kette zu Kette; Hoehen kommen von den Kreiseln.
7. Separator auf der Achse wird verbraucht, einer daneben nicht.
8. hoehen_entfernen laesst nur Anwender-Hoehen stehen.
"""
import os
import sys
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "lib"))
import dxf_scan_components as dsc # noqa: E402
AS_LINKS = "Ausschleuselement links 200000217 Draufsicht"
def _as(x, y, z=3191.63, block=AS_LINKS):
return {"familie": "schleuselement", "block": block,
"x": x, "y": y, "z": z, "rot": 270.0}
def _linie(a, b, layer="6-SP"):
return {"layer": layer, "a": list(a), "b": list(b)}
@pytest.fixture(autouse=True)
def _projekt(monkeypatch):
monkeypatch.setattr(dsc, "PROJEKT", "Test")
# Die beiden Kreisel am GF-Feld (Radmitten AN8 -> SP8, r = 400), wie
# baue_eintraege sie als kreisel_geo anlegt. Ketten: oben y = 12919.72,
# unten y = 24478.8.
def _kreisel_oben(z=2437.0):
return {"id": "K_oben", "a": (35460.37, 12519.72), "u": (-1.0, 0.0),
"laenge": 33044.28, "r": 400.0, "z_anwender": z}
def _kreisel_unten(z=1826.0):
return {"id": "K_unten", "a": (36261.24, 24878.8), "u": (-1.0, 0.0),
"laenge": 33470.18, "r": 400.0, "z_anwender": z}
def test_gf_mit_as_am_oberen_ende():
linie = _linie((5833.56, 13348.38, 3191.63), (5833.44, 24002.54, 2821.63))
as_eigen = _as(5367.10, 14505.61)
eintraege, belegt, hinweise = dsc.gf_eintraege_aus_linien([linie], [as_eigen])
assert len(eintraege) == 1
gf = eintraege[0]
assert gf["test_id"] == "GF_Test_01"
assert gf["modus"] == 1
# ohne Kreisel: Linienende bleibt stehen, Hoehe unbekannt -> Platzhalter
assert gf["start_mm"] == [5833.56, 13348.38, 0.0]
assert gf["deltaL_mm"] == pytest.approx(10654.16, abs=0.01)
assert gf["hz_grad"] == 90.0
assert gf["winkel_grad"] == 3.0
assert gf["hoehe_unbekannt"] is True
assert (gf["as_seite"], gf["as_winkel"]) == ("links", "90")
assert belegt == {id(as_eigen)}
assert len(hinweise) == 2 and all("kein Kreisel" in h for h in hinweise)
def test_anschluss_an_beide_kreisel_mit_hoehen():
linie = _linie((5833.56, 13348.38, 3191.63), (5833.44, 24002.54, 2821.63))
eintraege, _, hinweise = dsc.gf_eintraege_aus_linien(
[linie], [_as(5367.10, 14505.61)], [_kreisel_oben(), _kreisel_unten()])
gf = eintraege[0]
assert gf["start_mm"][1] == pytest.approx(12919.72, abs=0.01)
assert gf["start_mm"][2] == 2437.0
assert gf["hoehe_ende_mm"] == 1826.0
assert gf["hoehe_quelle"] == "Anwender"
assert "winkel_grad" not in gf # rechnet der LISP-Treiber
assert gf["deltaL_mm"] == pytest.approx(24478.8 - 12919.72, abs=0.05)
assert (gf["kreisel_start"], gf["kreisel_ende"]) == ("K_oben", "K_unten")
assert hinweise == []
def test_kreisel_ohne_hoehe_gibt_platzhalter():
linie = _linie((5833.56, 13348.38, 3191.63), (5833.44, 24002.54, 2821.63))
eintraege, _, _ = dsc.gf_eintraege_aus_linien(
[linie], [], [_kreisel_oben(z=None), _kreisel_unten()])
assert eintraege[0]["kreisel_start"] == "K_oben"
assert "hoehe_ende_mm" not in eintraege[0]
assert eintraege[0]["winkel_grad"] == 3.0
def test_separator_auf_achse_wird_verbraucht():
linie = _linie((5833.56, 13348.38, 3191.63), (5833.44, 24002.54, 2821.63))
sep_achse = {"familie": "sensor", "block": "S-SP", "x": 5834.0, "y": 23810.0}
sep_daneben = {"familie": "sensor", "block": "S-SP", "x": 6734.0, "y": 23810.0}
scanner = {"familie": "sensor", "block": "Scanner", "x": 5834.0, "y": 20000.0}
_, belegt, _ = dsc.gf_eintraege_aus_linien(
[linie], [], [_kreisel_oben(), _kreisel_unten()],
[sep_achse, sep_daneben, scanner])
assert belegt == {id(sep_achse)}
def test_hoehen_entfernen_laesst_anwender_hoehen_stehen():
eintraege = [
{"id": "K1", "x": 0, "y": 0, "z": 2437.0, "hoehe_quelle": "Anwender"},
{"block": "S-SP", "x": 0, "y": 0, "z": 2821.63, "comment": ""},
{"test_id": "G", "typ": "gerade", "startpunkt": [0, 0, 5.0],
"endpunkt": [1, 0, 5.0]},
]
assert dsc.hoehen_entfernen(eintraege) == 2
assert eintraege[0]["z"] == 2437.0
assert eintraege[1]["z"] == 0.0 and eintraege[1]["hoehe_unbekannt"] is True
assert eintraege[2]["startpunkt"][2] == 0.0 and eintraege[2]["endpunkt"][2] == 0.0
def test_nachbar_as_auf_falscher_seite_wird_nicht_gegriffen():
# Linie bei x=5834; ihr AS 467 mm links, der AS der Nachbarlinie
# (x=6734) 433 mm RECHTS - also naeher, aber auf der falschen Seite.
linie = _linie((5833.56, 13348.38, 3191.63), (5833.44, 24002.54, 2821.63))
as_eigen = _as(5367.10, 14505.61)
as_nachbar = _as(6267.10, 14505.61)
_, belegt, _ = dsc.gf_eintraege_aus_linien([linie], [as_nachbar, as_eigen])
assert belegt == {id(as_eigen)}
def test_rueckwaerts_gezeichnete_gf_startet_oben():
linie = _linie((5833.44, 24002.54, 2821.63), (5833.56, 13348.38, 3191.63))
eintraege, _, _ = dsc.gf_eintraege_aus_linien([linie], [_as(5367.10, 14505.61)])
assert eintraege[0]["start_mm"][:2] == [5833.56, 13348.38]
assert eintraege[0]["hz_grad"] == 90.0
def test_as_am_unteren_ende_ist_steigfoerderer():
# Gemessen: Linie x=2801 von 2822 hinauf auf 3192, AS am tiefen Ende.
linie = _linie((2801.42, 24043.0, 2821.63), (2801.42, 13389.0, 3191.63))
as_unten = _as(3267.48, 22892.92, z=2821.63)
eintraege, belegt, hinweise = dsc.gf_eintraege_aus_linien([linie], [as_unten])
assert eintraege == []
assert belegt == set()
assert len(hinweise) == 1 and "Steigfoerderer" in hinweise[0]
def test_waagrechte_linie_ist_keine_gf():
linie = _linie((36661.0, 10279.0, 2821.63), (36661.0, 24879.0, 2821.63))
eintraege, belegt, hinweise = dsc.gf_eintraege_aus_linien([linie], [])
assert eintraege == []
assert belegt == set()
assert any("waagrechte" in h for h in hinweise)
def test_ohne_as_defaults_und_hinweis():
linie = _linie((0.0, 0.0, 1000.0), (5000.0, 0.0, 800.0))
eintraege, _, hinweise = dsc.gf_eintraege_aus_linien([linie], [])
assert eintraege[0]["hz_grad"] == 0.0
assert eintraege[0]["as_seite"] == dsc.DEFAULT_SEITE
assert any("ohne Ausschleuselement" in h for h in hinweise)
+24 -7
View File
@@ -146,8 +146,18 @@
;; gueltigen Winkel zu nehmen. L_GF1 aus JSON ist der Vorlauf-Zielwert;
;; L_GF2 (Nachlauf) ist der Rest (L_GF_gesamt - L_GF1).
(defun mubea:build-vario-one (tid typ richtung deltaL deltaH winkel-soll gf1-ziel seite hz
startpunkt / ergebnis winkel-treffer best-winkel L_GF L_VF
gf1 gf2 hoehe-bis lastEnt ent vf-nummer)
startpunkt)
(mubea:ent-json tid "vario"
(mubea:vario-bauen tid typ richtung deltaL deltaH winkel-soll gf1-ziel seite hz
startpunkt)))
;; Der eigentliche Bau ohne Ergebnis-Format - auch von
;; tests/test_dematic_arvato.lsp genutzt (dort mit vsp-Records).
;; Rueckgabe: Entity des VF_n-Blocks oder nil (auch, wenn deltaL beim
;; gewuenschten Winkel nicht loesbar ist).
(defun mubea:vario-bauen (tid typ richtung deltaL deltaH winkel-soll gf1-ziel seite hz
startpunkt / ergebnis winkel-treffer best-winkel L_GF L_VF
gf1 gf2 hoehe-bis lastEnt ent vf-nummer)
(if (or (null seite) (= seite "")) (setq seite "links"))
(if (or (null typ) (= typ "")) (setq typ "standard"))
(if (null hz) (setq hz 0.0) (setq hz (float hz)))
@@ -207,12 +217,12 @@
;; Linienzug/Altbestand bleiben unmarkiert.
(if (and ent (= (type ent) 'ENAME) (member typ '("standard" "etage")))
(vf-edit-xdata-schreiben ent typ hz))
(mubea:ent-json tid "vario" ent))
(if (and ent (= (type ent) 'ENAME)) ent nil))
(progn
(princ (strcat "\n FEHLER: deltaL=" (rtos deltaL 2 0)
" bei Winkel " (if winkel-soll (rtos winkel-soll 2 0) "auto")
" nicht loesbar (kein gueltiges L_GF/L_VF)."))
(mubea:result-json tid "vario" "failed" nil nil nil nil)))
nil))
)
;; --- Vario(s) aus einem Template-Eintrag bauen ---
@@ -290,8 +300,15 @@
;; sep_x = 300 * cos(v), v = Element-Winkel (winkel_grad, Default 3 Grad)
;; Die Zielhoehe ergibt sich aus L_stau/Winkel (kein Ziel-Hoehen-Parameter).
;; Rueckgabe: Ergebnis-JSON-String.
(defun mubea:build-gf-one (tid start-mm deltaL hz as-seite es-seite as-winkel es-winkel winkel /
rad sep-x test-L_stau ent)
(defun mubea:build-gf-one (tid start-mm deltaL hz as-seite es-seite as-winkel es-winkel winkel)
(mubea:ent-json tid "gefaellestrecke"
(mubea:gf-bauen tid start-mm deltaL hz as-seite es-seite as-winkel es-winkel winkel)))
;; Der eigentliche Bau ohne Ergebnis-Format - auch von
;; tests/test_dematic_arvato.lsp genutzt (dort mit vsp-Records).
;; Rueckgabe: Entity des GF_n-Blocks oder nil.
(defun mubea:gf-bauen (tid start-mm deltaL hz as-seite es-seite as-winkel es-winkel winkel /
rad sep-x test-L_stau ent)
(mubea:ensure-gf-init)
;; Element-Masse fuer die gewaehlte AS/ES-Variante setzen, damit aus-dx/ein-dx
;; zur Variante (90/30 Grad) passen. Danach wieder auf 90 zuruecksetzen.
@@ -312,7 +329,7 @@
deltaL as-winkel es-winkel "0" "Schoenenberger Geruest")))
(vf-set-as-masse "90" as-seite)
(vf-set-es-masse "90" es-seite)
(mubea:ent-json tid "gefaellestrecke" ent)
ent
)
;; --- Gefaellestrecke(n) aus einem Template-Eintrag bauen ---
+646 -1284
View File
File diff suppressed because it is too large Load Diff