HundM05-Testfall und DXF-Analyse-Skripte

- lib/dxf_scan_components.py: erkennt ILS-/Omniflo-Komponenten in fremden
  Projektzeichnungen und schreibt sie im Testdaten-JSON-Schema aus
- lib/dxf_abbild.py: getreue Abbildung der Bauteile einer Fremdzeichnung
  (Attribute, Weltkoordinaten, Unterkomponenten) ohne Interpretation
- tests/test_hundm05.{lsp,py}, testdata/hundm05.json, conftest-Fixtures und
  alltests.json-Eintrag fuer den Kreisel-Abschnitt aus ST500592_05.dxf
- menu: TEST_HUNDM05 im Testmenue, Connection_Insert/Edit in SSG_LIB.cui
- Doku: hartkodierte Pfade durch (getenv "DXFMAKRO") ersetzt

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-24 12:10:58 +02:00
parent e91608d7cc
commit 7242e815f8
15 changed files with 3112 additions and 63 deletions
+2 -1
View File
@@ -50,7 +50,7 @@ Menue einmalig in BricsCAD geladen wurde (siehe unten: "Menue einrichten").
Alternativ per LISP-Kommandozeile in BricsCAD:
```lisp
(command "_.MENULOAD" "C:/10-Develop/gitrepos/dxfmakros/menu/SSG_LIB.mnu")
(command "_.MENULOAD" (strcat (getenv "DXFMAKRO") "/menu/SSG_LIB.mnu"))
```
Das Menue bleibt dauerhaft registriert. Nach einem Update der `.mnu`-Datei
@@ -157,6 +157,7 @@ Abhaengigkeiten: `ezdxf`, Standard-Library. Benoetigen Umgebungsvariablen (`DXFM
| `export_sivas.py` | Erzeugt Sivas-Export-CSV mit Summierungszeilen (ILS Automation, Omniflo Sum). Aufruf: `python export_sivas.py <export_raw.json> <data_dir> <output.csv>` |
| `export_csv.py` | Erzeugt einfache Item-Liste als CSV (ohne Summierung). Aufruf: `python export_csv.py <export_raw.json> <data_dir> <output.csv>` |
| `split_block_library.py` | Teilt `ils_library.dxf` in einzelne DXF-Dateien pro Block auf (mit Abhaengigkeiten). Export-Prefixe: Vario_, Staustrecke_, _3D_AS_, _3D_ES_ |
| `dxf_scan_components.py` | Erkennt ILS-/Omniflo-Komponenten in einer **fremden** Projektzeichnung (ohne unsere Wrapper-Bloecke) und schreibt sie im JSON-Schema aus `tests/testdata/object_data.md` — Grundlage fuer Nachbau-Testfaelle wie `tests/testdata/hundm05.json`. Liest binaere DXF im GB-Bereich streamend per mmap (ezdxf-DOM waere zu gross), loest INSERTs inkl. OCS/Extrusionsrichtung rekursiv in Weltkoordinaten auf, paart AN8/SP8 zu Kreiseln und Antrieb/Umlenkung zu Foerderern. Aufruf: `python dxf_scan_components.py <dxf> <data_dir> <output.json> [--fenster X0 Y0 X1 Y1]` — ohne `--fenster` liefert eine Anlagenzeichnung leicht ueber 1300 Eintraege, zuviel fuer einen Testlauf |
### Entwickler-Tools (`lib/`) - kein ezdxf, reine Text-/AutoLISP-Verarbeitung
+1 -1
View File
@@ -20,7 +20,7 @@ alle Feature-Module). Details: siehe `../CLAUDE.md` Abschnitt "Lademechanismus".
**Manuell laden (Notfall / Entwicklung):**
```lisp
(load "C:/10-Develop/gitrepos/dxfmakros/Lisp/ssg_load.lsp")
(load (strcat (getenv "DXFMAKRO") "/Lisp/ssg_load.lsp"))
```
`ssg_load.lsp` laedt alle Module in der richtigen Reihenfolge ohne Menueabhaengigkeit.
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+333
View File
@@ -0,0 +1,333 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
dxf_abbild.py - Getreue Abbildung der Bauteile einer Fremdzeichnung als JSON.
Abgrenzung zu lib/dxf_scan_components.py: dieses Skript beschreibt, WAS in der
Zeichnung steht - nicht, wie unsere Makros es nachbauen sollen. Es interpretiert
so wenig wie moeglich und schreibt alles mit, was im DXF wirklich vorhanden ist:
Blockname, Layer, Weltposition, Rotation, Skalierung, Extrusionsrichtung,
saemtliche Blockattribute und die verschachtelten Unterkomponenten. Wo eine
Angabe nicht im DXF steht, steht sie auch nicht in der Ausgabe - es wird nichts
geraten und nichts mit Defaults aufgefuellt.
Der Typ eines Bauteils kommt bevorzugt aus den Attributen (BESCHR, BEZEICHNUNG,
VERW), nicht aus dem Blocknamen: in ST500592_05.dxf tragen 15730 INSERTs
Attribute mit 80 verschiedenen Tags, darunter Klartext wie
BESCHR="TEF-Profil-Bogen innen 67,5 Grad R400". Das ist belastbarer als jede
Blocknamen-Heuristik.
Was das DXF NICHT enthaelt und was hier darum fehlt:
- Flussrichtung/Pfeile: kein Blockname enthaelt Pfeil/arrow/flow/Richtung,
die Pfeile sind gezeichnete Geometrie (SOLID/Polylinie). Nicht ableitbar.
- Explizite Verbindungsgraphen: die Zeichnung hat 0 GROUP-Objekte. Die
vorhandenen XDATA sind Fremd-Metadaten (TeklaStructures, CSTABVIEWER).
Was an Beziehung drinsteckt, steckt in den Kennzeichnungs-Strings und wird
unter "beziehungen" als GELESENER Text abgebildet, nicht als Topologie.
- Zeichnungsgeometrie (LINE/ARC/LWPOLYLINE): bewusst nicht erfasst.
Aufruf:
python dxf_abbild.py <dxf> <output.json> [--fenster X0 Y0 X1 Y1]
[--max-tiefe N] [--ohne-geometrie-container]
Beispiel:
python lib/dxf_abbild.py results/ST500592_05.dxf results/hundm05_abbild.json
"""
import argparse
import collections
import json
import math
import os
import re
import sys
from ezdxf.math import Matrix44
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from dxf_scan_components import ( # noqa: E402
RUND, familie_von, insert_matrix, lese_inserts, im_fenster)
PREFIX = "[dxf_abbild]"
# Attribut-Tags, die ein Bauteil identifizieren. Reihenfolge = Aussagekraft.
ID_TAGS = ["ID", "ARTINR", "OriginalName", "KENNZEICHNUNG", "POSITION",
"DETAILS", "IO"]
# Attribut-Tags, aus denen sich der Bauteiltyp im Klartext ergibt.
TYP_TAGS = ["BESCHR", "BEZEICHNUNG", "VERW"]
# Attribut-Tags, die eine Zugehoerigkeit ausdruecken (Gruppe, Steuerung,
# Anlagenkennzeichen). Werden als gelesener Text uebernommen.
BEZUG_TAGS = ["KENNZEICHNUNG", "GRUPPE", "SPS", "AUFLOESE", "ETIKETTE"]
# Blocknamen, die reine Geometrie-Container sind (Volumenkoerper-Huellen aus
# dem CAD-Import). Sie tragen keine Bauteilinformation, blaehen die Ausgabe aber
# stark auf - mit --ohne-geometrie-container werden sie nur gezaehlt.
GEOMETRIE_CONTAINER = re.compile(r"^(sgBrepBlock__|\*[A-Z])")
# EPLAN-Kennzeichnung: =Anlage +Ort -Betriebsmittel
KENNZ_RE = re.compile(r"(?:=([^+\-=]+))?(?:\+([^+\-=]+))?(?:-(.+))?$")
# Verweis in Klartext-Bezeichnungen, z.B. "in Separator (ILS-CV M2111)"
VERWEIS_RE = re.compile(r"\(([^)]+)\)")
def identifikatoren(attribs):
"""Alle vorhandenen Identifikatoren eines Bauteils - nur echte Werte."""
ids = {}
for tag in ID_TAGS:
wert = attribs.get(tag)
if wert not in (None, ""):
ids[tag] = wert
return ids
def typ_von(attribs, blockname):
"""Bauteiltyp ermitteln.
Rueckgabe: (typ, quelle). "quelle" nennt das Attribut, aus dem der Typ
stammt, bzw. "blockname" als Rueckfallebene - damit im JSON sichtbar
bleibt, wie belastbar die Angabe ist.
"""
for tag in TYP_TAGS:
wert = attribs.get(tag)
if wert not in (None, ""):
return wert, tag
return blockname, "blockname"
def kennzeichnung_teile(wert):
"""=Anlage +Ort -Betriebsmittel aufschluesseln, soweit vorhanden."""
m = KENNZ_RE.match(wert.strip())
if not m:
return {}
teile = {}
for name, gruppe in (("anlage", 1), ("ort", 2), ("betriebsmittel", 3)):
if m.group(gruppe):
teile[name] = m.group(gruppe).strip()
return teile
def beziehungen(attribs):
"""Zugehoerigkeiten, die als Text in den Attributen stehen.
Bewusst KEINE Topologie: die Zeichnung enthaelt keinen Verbindungsgraphen
(0 GROUP-Objekte, XDATA nur von Fremd-Anwendungen). Hier steht nur, was
wirklich geschrieben ist - die Auswertung bleibt dem Leser.
"""
bez = {}
for tag in BEZUG_TAGS:
wert = attribs.get(tag)
if wert not in (None, ""):
bez[tag] = wert
kennz = attribs.get("KENNZEICHNUNG")
if kennz:
teile = kennzeichnung_teile(kennz)
if teile:
bez["kennzeichnung_teile"] = teile
# Verweis auf ein benanntes Foerdermittel, z.B. "(ILS-CV M2111)"
for tag in ("BEZEICHNUNG", "BESCHR"):
wert = attribs.get(tag)
if wert:
m = VERWEIS_RE.search(wert)
if m:
bez["verweis"] = m.group(1).strip()
break
return bez
def welt_daten(rec, mutter, block_base):
"""Weltposition, Rotation und Gesamtmatrix eines INSERT."""
mat = insert_matrix(rec, block_base) @ mutter
ursprung = mat.transform((0.0, 0.0, 0.0))
xachse = mat.transform((1.0, 0.0, 0.0)) - ursprung
rot = math.degrees(math.atan2(xachse.y, xachse.x)) % 360.0
return mat, ursprung, rot
def element_dict(rec, ursprung, rot, tiefe):
"""Einen INSERT als Abbild-Eintrag beschreiben."""
attribs = dict(rec["attribs"])
typ, typ_quelle = typ_von(attribs, rec["name"])
eintrag = collections.OrderedDict()
eintrag["block"] = rec["name"]
eintrag["layer"] = rec["layer"] or ""
eintrag["typ"] = typ
eintrag["typ_quelle"] = typ_quelle
eintrag["position"] = [round(ursprung.x, RUND) + 0.0,
round(ursprung.y, RUND) + 0.0,
round(ursprung.z, RUND) + 0.0]
eintrag["rotation"] = round(rot, RUND) + 0.0
if [round(s, 6) for s in rec["s"]] != [1.0, 1.0, 1.0]:
eintrag["skalierung"] = [round(s, 6) for s in rec["s"]]
if [round(v, 6) for v in rec["ext"]] != [0.0, 0.0, 1.0]:
# Nur wenn abweichend: die Extrusionsrichtung erklaert, warum ein
# Bauteil gekippt/gespiegelt im Raum liegt (13,7 % der INSERTs).
eintrag["extrusion"] = [round(v, 6) for v in rec["ext"]]
ids = identifikatoren(attribs)
if ids:
eintrag["ids"] = ids
bez = beziehungen(attribs)
if bez:
eintrag["beziehungen"] = bez
if attribs:
eintrag["attribute"] = attribs
fam = familie_von(rec["name"])
if fam:
# Unsere eigene Klassifikation - klar als solche gekennzeichnet, damit
# sie nicht mit den Angaben aus der Zeichnung verwechselt wird.
eintrag["ssg_familie"] = fam
eintrag["tiefe"] = tiefe
return eintrag
def baue_abbild(ms_inserts, block_inserts, block_base, fenster=None,
max_tiefe=12, ohne_container=False):
"""Alle Modelspace-INSERTs als Abbild-Baum beschreiben.
Rueckgabe: (elemente, statistik). Jedes Element der obersten Ebene enthaelt
seine aufgeloesten Unterkomponenten unter "unterkomponenten".
"""
stat = collections.Counter()
def kinder_von(name, mat, tiefe, pfad):
"""Verschachtelte INSERTs eines Blocks rekursiv beschreiben."""
if tiefe > max_tiefe:
return []
ergebnis = []
for rec in block_inserts.get(name, []):
if not rec["name"]:
continue
if ohne_container and GEOMETRIE_CONTAINER.match(rec["name"]):
stat["container_uebersprungen"] += 1
continue
kind_mat, ursprung, rot = welt_daten(rec, mat, block_base)
eintrag = element_dict(rec, ursprung, rot, tiefe)
stat["unterkomponenten"] += 1
if rec["name"] not in pfad:
enkel = kinder_von(rec["name"], kind_mat, tiefe + 1,
pfad | {rec["name"]})
if enkel:
eintrag["unterkomponenten"] = enkel
ergebnis.append(eintrag)
return ergebnis
elemente = []
for nr, rec in enumerate(ms_inserts, start=1):
if not rec["name"]:
continue
mat, ursprung, rot = welt_daten(rec, Matrix44(), block_base)
if not im_fenster(ursprung.x, ursprung.y, fenster):
stat["ausserhalb"] += 1
continue
if ohne_container and GEOMETRIE_CONTAINER.match(rec["name"]):
stat["container_uebersprungen"] += 1
continue
eintrag = element_dict(rec, ursprung, rot, 0)
eintrag["nr"] = nr
eintrag.move_to_end("nr", last=False)
kinder = kinder_von(rec["name"], mat, 1, frozenset({rec["name"]}))
if kinder:
eintrag["unterkomponenten"] = kinder
elemente.append(eintrag)
stat["elemente"] += 1
if rec["attribs"]:
stat["mit_attributen"] += 1
return elemente, stat
def main():
parser = argparse.ArgumentParser(
description="Schreibt die Bauteile einer Fremdzeichnung als getreue "
"JSON-Abbildung (keine Bau-Anweisung).")
parser.add_argument("dxf", help="Quellzeichnung (binaeres DXF)")
parser.add_argument("output", help="Ziel-JSON")
parser.add_argument("--fenster", nargs=4, type=float,
metavar=("X0", "Y0", "X1", "Y1"),
help="Nur Elemente in diesem XY-Fenster (mm)")
parser.add_argument("--max-tiefe", type=int, default=12,
help="Maximale Verschachtelungstiefe (Default 12)")
parser.add_argument("--ohne-geometrie-container", action="store_true",
help="Volumenkoerper-Huellen (sgBrepBlock_*, *A/*B/...) "
"nur zaehlen, nicht ausgeben")
args = parser.parse_args()
if not os.path.isfile(args.dxf):
print("%s FEHLER: DXF nicht gefunden: %s" % (PREFIX, args.dxf))
sys.exit(1)
fenster = None
if args.fenster:
x0, y0, x1, y1 = args.fenster
fenster = (min(x0, x1), min(y0, y1), max(x0, x1), max(y0, y1))
groesse = os.path.getsize(args.dxf) / (1024.0 * 1024.0)
print("%s lese %s (%.1f MB) ..." % (PREFIX, args.dxf, groesse))
ms_inserts, block_inserts, block_base, statistik = lese_inserts(args.dxf)
print("%s %d INSERTs im Modelspace, %d INSERTs gesamt, %d mit gekippter OCS"
% (PREFIX, len(ms_inserts), statistik["inserts"],
statistik["ocs_gekippt"]))
elemente, stat = baue_abbild(ms_inserts, block_inserts, block_base,
fenster=fenster, max_tiefe=args.max_tiefe,
ohne_container=args.ohne_geometrie_container)
# Kennzahlen, die belegen, wie belastbar die Typangaben sind
quellen = collections.Counter()
layer = collections.Counter()
def zaehle(liste):
for e in liste:
quellen[e["typ_quelle"]] += 1
layer[e["layer"]] += 1
zaehle(e.get("unterkomponenten", []))
zaehle(elemente)
abbild = collections.OrderedDict()
abbild["quelle"] = collections.OrderedDict([
("datei", os.path.basename(args.dxf)),
("groesse_mb", round(groesse, 1)),
("inserts_gesamt", statistik["inserts"]),
("inserts_modelspace", len(ms_inserts)),
("inserts_gekippte_ocs", statistik["ocs_gekippt"]),
("fenster", list(fenster) if fenster else None),
("hinweis", "Getreue Abbildung der Bauteile. Enthaelt KEINE "
"Flussrichtung (im DXF nicht vorhanden), keinen "
"Verbindungsgraphen (0 GROUP-Objekte) und keine "
"Zeichnungsgeometrie (bewusst ausgelassen). Felder mit "
"typ_quelle=blockname sind NICHT durch ein Attribut "
"belegt."),
])
abbild["kennzahlen"] = collections.OrderedDict([
("elemente_oberste_ebene", stat["elemente"]),
("unterkomponenten", stat["unterkomponenten"]),
("mit_attributen_oberste_ebene", stat["mit_attributen"]),
("typ_quellen", dict(quellen.most_common())),
("layer_top15", dict(layer.most_common(15))),
])
abbild["elemente"] = elemente
ziel_dir = os.path.dirname(os.path.abspath(args.output))
if ziel_dir and not os.path.isdir(ziel_dir):
os.makedirs(ziel_dir)
with open(args.output, "w", encoding="utf-8") as fh:
json.dump(abbild, fh, ensure_ascii=False, indent=2)
fh.write("\n")
mb = os.path.getsize(args.output) / (1024.0 * 1024.0)
print("%s %d Elemente (oberste Ebene) + %d Unterkomponenten -> %s (%.1f MB)"
% (PREFIX, stat["elemente"], stat["unterkomponenten"], args.output,
mb))
if stat["ausserhalb"]:
print("%s %d Elemente ausserhalb des Fensters ausgelassen"
% (PREFIX, stat["ausserhalb"]))
if stat["container_uebersprungen"]:
print("%s %d Geometrie-Container uebersprungen"
% (PREFIX, stat["container_uebersprungen"]))
print("%s Typ-Quellen: %s" % (PREFIX, dict(quellen.most_common())))
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+110 -52
View File
@@ -639,132 +639,156 @@
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00134">
<Macro>
<Name>Verbindung einfuegen</Name>
<Command>^C^C(ssg-ensure "Connection_Edit") CONNECTION_INSERT</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00135">
<Macro>
<Name>Verbindung bearbeiten</Name>
<Command>^C^C(ssg-ensure "Connection_Edit") CONNECTION_EDIT</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00138">
<Macro>
<Name>Export Sivas</Name>
<Command>^C^C(ssg-ensure "count_sep_scan") ZAEHLE_SEP_SCAN (ssg-ensure "export") EXPORTSIVAS</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00135">
<MenuMacro UID="MM_00139">
<Macro>
<Name>Export CSV</Name>
<Command>^C^C(ssg-ensure "export") EXPORTCSV</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00137">
<MenuMacro UID="MM_00141">
<Macro>
<Name>Umschalten 2D und 3D</Name>
<Command>^C^C(ssg-ensure "SSG_LIB_Commands") SSG_DIM_SWITCH</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00138">
<MenuMacro UID="MM_00142">
<Macro>
<Name>Alle Bloecke auf 2D</Name>
<Command>^C^C(ssg-ensure "SSG_LIB_Commands") SSG_DIM_ALL_2D</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00139">
<MenuMacro UID="MM_00143">
<Macro>
<Name>Alle Bloecke auf 3D</Name>
<Command>^C^C(ssg-ensure "SSG_LIB_Commands") SSG_DIM_ALL_3D</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00140">
<MenuMacro UID="MM_00144">
<Macro>
<Name>Sprache</Name>
<Command>^C^C(ssg-ensure "SSG_LIB_Commands") SSG_SPRACHE</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00143">
<MenuMacro UID="MM_00147">
<Macro>
<Name>Alle Tests ausfuehren</Name>
<Command>^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_run_all.lsp")) SSG_RUN_ALL_TESTS</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00145">
<MenuMacro UID="MM_00149">
<Macro>
<Name>Kreisel</Name>
<Command>^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_kreisel.lsp")) TEST_KREISEL</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00146">
<MenuMacro UID="MM_00150">
<Macro>
<Name>Foerderer</Name>
<Command>^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_foerderer.lsp")) TEST_FOERDERER</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00147">
<MenuMacro UID="MM_00151">
<Macro>
<Name>Foerderer-Linienzug</Name>
<Command>^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_linienzug.lsp")) TEST_LINIENZUG</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00148">
<MenuMacro UID="MM_00152">
<Macro>
<Name>Omniflo</Name>
<Command>^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_omniflo.lsp")) TEST_OMNIFLO</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00149">
<MenuMacro UID="MM_00153">
<Macro>
<Name>Omniflo Strecke</Name>
<Command>^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_omniflo_strecke.lsp")) TEST_OMNIFLO_STRECKE</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00150">
<MenuMacro UID="MM_00154">
<Macro>
<Name>Gefaellestrecke</Name>
<Command>^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_gefaellestrecke.lsp")) TEST_GEFAELLESTRECKE</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00151">
<MenuMacro UID="MM_00155">
<Macro>
<Name>Mubea Gesamtmodell</Name>
<Command>^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_mubea.lsp")) TEST_MUBEA</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00152">
<MenuMacro UID="MM_00156">
<Macro>
<Name>HundM05 Kreisel-Abschnitt</Name>
<Command>^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_hundm05.lsp")) TEST_HUNDM05</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00157">
<Macro>
<Name>KS 2D/3D pruefen (CSV)</Name>
<Command>^C^C(load (strcat (getenv "DXFMAKRO") "/Lisp/ks_pruefung.lsp")) KS_PRUEFEN</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00153">
<MenuMacro UID="MM_00158">
<Macro>
<Name>Export CSV/Sivas ALL</Name>
<Command>^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_export_all.lsp")) TEST_EXPORT_ALL</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00165">
<MenuMacro UID="MM_00170">
<Macro>
<Name>Kreisel bearbeiten</Name>
<Command>(ssg-ensure "KreiselInsert") KreiselEdit</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00166">
<MenuMacro UID="MM_00171">
<Macro>
<Name>Omniflo Element bearbeiten</Name>
<Command>(ssg-ensure "OmniModulInsert") OMNI_EDIT</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00167">
<MenuMacro UID="MM_00172">
<Macro>
<Name>Gefaellestrecke bearbeiten</Name>
<Command>(ssg-ensure "Gefaellestrecke") GEFAELLESTRECKE_EDIT</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00168">
<MenuMacro UID="MM_00173">
<Macro>
<Name>VarioFoerderer bearbeiten</Name>
<Command>(ssg-ensure "VarioFoerderer") VARIOFOERDERER_EDIT</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00169">
<MenuMacro UID="MM_00174">
<Macro>
<Name>TRO-Marker bearbeiten</Name>
<Command>(ssg-ensure "Tro_Edit") TRO_EDIT</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00171">
<MenuMacro UID="MM_00175">
<Macro>
<Name>Verbindung bearbeiten</Name>
<Command>(ssg-ensure "Connection_Edit") CONNECTION_EDIT</Command>
</Macro>
</MenuMacro>
<MenuMacro UID="MM_00177">
<Macro>
<Name>SSG Bearbeiten (Auto)</Name>
<Command>(ssg-ensure "SSG_LIB_Commands") SSG_BLOCKEDIT</Command>
@@ -783,8 +807,9 @@
<PopMenuRef pUID="PM_00047"/>
<PopMenuRef pUID="PM_00129"/>
<PopMenuRef pUID="PM_00133"/>
<PopMenuRef pUID="PM_00136"/>
<PopMenuRef pUID="PM_00142"/>
<PopMenuRef pUID="PM_00137"/>
<PopMenuRef pUID="PM_00140"/>
<PopMenuRef pUID="PM_00146"/>
</PopMenu>
<PopMenu UID="PM_00005">
<Name>ILS</Name>
@@ -1484,44 +1509,65 @@
</PopMenuItem>
</PopMenu>
<PopMenu UID="PM_00133">
<Name>Export</Name>
<Name>Connections</Name>
<PopMenuItem>
<NameRef>Export Sivas</NameRef>
<NameRef>Verbindung einfuegen</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00134"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Export CSV</NameRef>
<NameRef>Verbindung bearbeiten</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00135"/>
</MenuItem>
</PopMenuItem>
</PopMenu>
<PopMenu UID="PM_00136">
<Name>Konfiguration</Name>
<PopMenuItem>
<NameRef>Umschalten 2D und 3D</NameRef>
<NameRef>Bearbeiten (Auto)</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00137"/>
<MacroRef MenuMacroID="MM_00132"/>
</MenuItem>
</PopMenuItem>
</PopMenu>
<PopMenu UID="PM_00137">
<Name>Export</Name>
<PopMenuItem>
<NameRef>Alle Bloecke auf 2D</NameRef>
<NameRef>Export Sivas</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00138"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Alle Bloecke auf 3D</NameRef>
<NameRef>Export CSV</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00139"/>
</MenuItem>
</PopMenuItem>
</PopMenu>
<PopMenu UID="PM_00140">
<Name>Konfiguration</Name>
<PopMenuItem>
<NameRef>Umschalten 2D und 3D</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00141"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Alle Bloecke auf 2D</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00142"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Alle Bloecke auf 3D</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00143"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Sprache</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00140"/>
<MacroRef MenuMacroID="MM_00144"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
@@ -1531,108 +1577,120 @@
</MenuItem>
</PopMenuItem>
</PopMenu>
<PopMenu UID="PM_00142">
<PopMenu UID="PM_00146">
<Name>Tests</Name>
<PopMenuItem>
<NameRef>Alle Tests ausfuehren</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00143"/>
<MacroRef MenuMacroID="MM_00147"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem IsSeparator="true"/>
<PopMenuItem>
<NameRef>Kreisel</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00145"/>
<MacroRef MenuMacroID="MM_00149"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Foerderer</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00146"/>
<MacroRef MenuMacroID="MM_00150"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Foerderer-Linienzug</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00147"/>
<MacroRef MenuMacroID="MM_00151"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Omniflo</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00148"/>
<MacroRef MenuMacroID="MM_00152"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Omniflo Strecke</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00149"/>
<MacroRef MenuMacroID="MM_00153"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Gefaellestrecke</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00150"/>
<MacroRef MenuMacroID="MM_00154"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Mubea Gesamtmodell</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00151"/>
<MacroRef MenuMacroID="MM_00155"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>HundM05 Kreisel-Abschnitt</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00156"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>KS 2D/3D pruefen (CSV)</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00152"/>
<MacroRef MenuMacroID="MM_00157"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Export CSV/Sivas ALL</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00153"/>
<MacroRef MenuMacroID="MM_00158"/>
</MenuItem>
</PopMenuItem>
</PopMenu>
<PopMenu UID="PM_00164">
<PopMenu UID="PM_00169">
<Alias>POP501</Alias>
<Name>SSG_LIB Edit Context</Name>
<PopMenuItem>
<NameRef>Kreisel bearbeiten</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00165"/>
<MacroRef MenuMacroID="MM_00170"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Omniflo Element bearbeiten</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00166"/>
<MacroRef MenuMacroID="MM_00171"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Gefaellestrecke bearbeiten</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00167"/>
<MacroRef MenuMacroID="MM_00172"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>VarioFoerderer bearbeiten</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00168"/>
<MacroRef MenuMacroID="MM_00173"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>TRO-Marker bearbeiten</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00169"/>
<MacroRef MenuMacroID="MM_00174"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem>
<NameRef>Verbindung bearbeiten</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00175"/>
</MenuItem>
</PopMenuItem>
<PopMenuItem IsSeparator="true"/>
<PopMenuItem>
<NameRef>SSG Bearbeiten (Auto)</NameRef>
<MenuItem>
<MacroRef MenuMacroID="MM_00171"/>
<MacroRef MenuMacroID="MM_00177"/>
</MenuItem>
</PopMenuItem>
</PopMenu>
+1
View File
@@ -153,6 +153,7 @@
[Omniflo Strecke]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_omniflo_strecke.lsp")) TEST_OMNIFLO_STRECKE
[Gefaellestrecke]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_gefaellestrecke.lsp")) TEST_GEFAELLESTRECKE
[Mubea Gesamtmodell]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_mubea.lsp")) TEST_MUBEA
[HundM05 Kreisel-Abschnitt]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_hundm05.lsp")) TEST_HUNDM05
[KS 2D/3D pruefen (CSV)]^C^C(load (strcat (getenv "DXFMAKRO") "/Lisp/ks_pruefung.lsp")) KS_PRUEFEN
[<-Export CSV/Sivas ALL]^C^C(load (strcat (getenv "DXFMAKRO") "/tests/test_export_all.lsp")) TEST_EXPORT_ALL
+2 -1
View File
@@ -5,5 +5,6 @@
{ "name": "omniflo", "save": "dxf", "module": "OmniModulInsert" },
{ "name": "omniflo_strecke", "save": "dxf", "module": "OmniModulInsert" },
{ "name": "gefaellestrecke", "save": "dwg", "module": "Gefaellestrecke" },
{ "name": "mubea", "save": "dxf", "module": "KreiselInsert" }
{ "name": "mubea", "save": "dxf", "module": "KreiselInsert" },
{ "name": "hundm05", "save": "dxf", "module": "KreiselInsert" }
]
+34
View File
@@ -187,3 +187,37 @@ def mubea_dxf():
if not os.path.exists(path):
pytest.skip("mubea_tests.dxf nicht vorhanden - TESTRUN (save:dxf) in BricsCAD ausfuehren")
return ezdxf.readfile(path)
# --- HundM05 Fixtures ---
@pytest.fixture
def hundm05_testdata():
"""Laedt die HundM05-Testdefinitionen aus testdata/hundm05.json.
Erzeugt von lib/dxf_scan_components.py aus results/ST500592_05.dxf
(Fenster 216000 144000 228000 156000, Kreisel-Abschnitt).
"""
path = os.path.join(_testdata_dir(), "hundm05.json")
return _load_json(path)
@pytest.fixture
def hundm05_results():
"""Laedt die HundM05-Ergebnisse aus output/hundm05_results.json."""
path = os.path.join(_output_dir(), "hundm05_results.json")
if not os.path.exists(path):
pytest.skip("hundm05_results.json nicht vorhanden - TEST_HUNDM05 in BricsCAD ausfuehren")
data = _load_json(path)
if not data:
pytest.skip("hundm05_results.json ist leer")
return data
@pytest.fixture
def hundm05_dxf():
"""Laedt die erzeugte HundM05-DXF aus output/hundm05_tests.dxf."""
path = os.path.join(_output_dir(), "hundm05_tests.dxf")
if not os.path.exists(path):
pytest.skip("hundm05_tests.dxf nicht vorhanden - TESTRUN (save:dxf) in BricsCAD ausfuehren")
return ezdxf.readfile(path)
+601
View File
@@ -0,0 +1,601 @@
;; ============================================================
;; test_hundm05.lsp - Integrationstest: baut den Kreisel-Abschnitt der
;; Anlage HundM aus der Fremdzeichnung ST500592_05.dxf nach.
;;
;; Liest tests/testdata/hundm05.json (erzeugt von lib/dxf_scan_components.py
;; mit --fenster 216000 144000 228000 156000) und baut in EINER Zeichnung:
;; - Kreisel (Eintraege mit "id", OHNE "type") Block KREISEL_*
;; "function": "connect" -> kreisel-connect-script (Start-/
;; Endpunkt, abstand+rotation werden intern berechnet)
;; "function": "insert" -> kreisel-insert-script
;; - Vario (test_id "VF_*") Block VF_* via variofoerderer-einfuegen
;; - Gefaelle (test_id "GF_*") Block GF_* via gefaellestrecke-einfuegen
;; - Gerade (test_id "GERADE_*") waagrechte Foerderstrecke via
;; insert-hz-scaled-block-by-ks (Staustrecke_SP_1000_mm)
;; - Omniflo (Eintraege mit "type") - NICHT gebaut, siehe hundm05:build-omniflo
;; - Sensoren (Eintraege mit "block") via _.INSERT
;;
;; Speichert (via SSG_RUN_ALL_TESTS bei "save":"dxf"):
;; tests/output/hundm05_tests.dxf
;; tests/output/hundm05_results.json (via hundm05:export-results)
;;
;; Unterschiede zu tests/test_mubea.lsp - warum eine eigene Datei und kein
;; gemeinsamer Treiber:
;; 1. Der Kreisel kommt hier als "connect" (zwei Punkte aus dem DXF), bei
;; Mubea als "insert" (Punkt + abstand + rotation von Hand).
;; 2. Die VarioFoerderer nennen ihren Startpunkt "startpunkt" (Liste, wie
;; object_data.md 3.1) statt x/y/z, und sie haben kein "L_GF1" - der
;; L_GF-Split ist hier "gleichmaessig" (Default nach object_data.md).
;; 3. Es gibt den Eintragstyp "gerade", den mubea.json nicht kennt.
;; 4. Eintraege mit "type" (Omniflo) muessen VOR "id" abgefragt werden, weil
;; sie beides tragen - bei Mubea gab es diesen Fall nicht.
;; test_mubea.lsp bleibt dadurch unangetastet und die bestehende Suite
;; unveraendert lauffaehig.
;;
;; Voraussetzungen:
;; - SSG_LIB geladen (KreiselInsert, VarioFoerderer, Gefaellestrecke,
;; SSG_LIB_Commands, ssg_core, vf_core)
;; - Umgebungsvariable DXFMAKRO gesetzt
;;
;; Aufruf in BricsCAD:
;; (load (strcat (getenv "DXFMAKRO") "/tests/test_hundm05.lsp"))
;; TEST_HUNDM05
;; ============================================================
;; --- Ergebnis eines Bau-Schritts als JSON-String erzeugen ---
(defun hundm05:result-json (test-id kind status block-name block-handle insert-point attribs /
json tag val first)
(setq json (strcat " {\n"
" \"test_id\": \"" test-id "\",\n"
" \"kind\": \"" kind "\",\n"
" \"status\": \"" status "\",\n"
" \"block_name\": \"" (if block-name block-name "") "\",\n"
" \"block_handle\": \"" (if block-handle block-handle "") "\",\n"
" \"insert_point\": ["
(if insert-point
(strcat (rtos (car insert-point) 2 1) ", "
(rtos (cadr insert-point) 2 1) ", "
(rtos (if (caddr insert-point) (caddr insert-point) 0.0) 2 1))
"0.0, 0.0, 0.0")
"],\n"
" \"actual_attributes\": {"))
(setq first T)
(foreach att attribs
(setq tag (car att) val (cdr att))
(if (not first) (setq json (strcat json ",")))
(setq json (strcat json "\n \"" tag "\": \"" (if val val "") "\""))
(setq first nil))
(setq json (strcat json "\n }\n }"))
json
)
;; --- Aus einer Block-ENAME ein Ergebnis-JSON erzeugen ---
(defun hundm05:ent-json (test-id kind ent / ed)
(if ent
(progn
(setq ed (entget ent))
(hundm05:result-json test-id kind "executed"
(cdr (assoc 2 ed)) (cdr (assoc 5 ed)) (cdr (assoc 10 ed))
(ssg-attrib-read ent)))
(hundm05:result-json test-id kind "failed" nil nil nil nil))
)
;; --- Letzten INSERT mit Blocknamen-Praefix (rueckwaerts) suchen ---
(defun hundm05:last-insert-prefix (pref / ent found ed bn)
(setq ent (entlast))
(while (and ent (null found))
(setq ed (entget ent) bn (cdr (assoc 2 ed)))
(if (and (= (cdr (assoc 0 ed)) "INSERT") bn
(>= (strlen bn) (strlen pref))
(= (substr bn 1 (strlen pref)) pref))
(setq found ent))
(setq ent (entprev ent)))
found
)
;; --- Zahl aus JSON lesen, mit Default statt Fehler bei fehlendem Feld ---
;; (float nil) bricht in AutoLISP ab. hundm05.json laesst - anders als
;; mubea.json - bewusst Felder weg (z.B. "winkel" und "L_GF1" beim Vario, damit
;; der Solver selbst waehlt), darum hier grundsaetzlich abgesichert lesen.
(defun hundm05:num (e key default / v)
(setq v (ssg-val e key))
(if (numberp v) (float v) default)
)
;; --- Punkt-Liste aus JSON lesen ("startpunkt"/"start_mm": [x, y, z]) ---
;; Liefert NIEMALS einen Ersatzpunkt. Ein Ursprungs-Fallback (0 0 0) waere hier
;; fatal: er baut die Anlage sichtbar, aber komplett an der falschen Stelle -
;; genau das passiert, wenn das Array in der JSON ueber mehrere Zeilen
;; umgebrochen ist (siehe hundm05:startpunkt).
(defun hundm05:pt (e key / v)
(setq v (ssg-val e key))
(if (and v (listp v) (>= (length v) 3)) (mapcar 'float v) nil)
)
;; --- Startpunkt eines Eintrags lesen: Array-Feld, sonst flache x/y/z ---
;; Rueckgabe: Punkt oder nil. Bei nil MUSS der Aufrufer den Eintrag als
;; "failed" melden und darf nichts bauen.
;;
;; Haeufigste Ursache fuer nil: ssg-parse-json-array (ssg_core.lsp) liest die
;; JSON ZEILENWEISE, und ssg-cfg-parse-array braucht das ganze Array in einer
;; Zeile. Ein von json.dump(indent=2) umgebrochenes Array
;; "start_mm": [
;; 219540.85,
;; ...
;; ]
;; liefert nil. Die Ausgabe von lib/dxf_scan_components.py zieht Zahlen-Arrays
;; darum wieder auf eine Zeile (arrays_einzeilig).
(defun hundm05:startpunkt (e feld / p)
(setq p (hundm05:pt e feld))
;; Ersatzweise flache x/y/z-Felder zulassen (Schema wie mubea.json)
(if (and (null p) (numberp (ssg-val e "x")))
(setq p (list (hundm05:num e "x" 0.0) (hundm05:num e "y" 0.0)
(hundm05:num e "z" 0.0))))
p
)
;; --- Meldung fuer einen nicht lesbaren Startpunkt ---
(defun hundm05:kein-startpunkt (tid feld)
(princ (strcat "\n FEHLER: " tid " - Feld \"" feld "\" nicht lesbar."
" Steht das Array in EINER Zeile? ssg-cfg-parse-array kann"
" keine umgebrochenen Arrays lesen. Nichts gebaut."))
)
;; ============================================================
;; Bau-Funktionen je Elementtyp
;; ============================================================
;; --- Kreisel: "connect" (Start-/Endpunkt) oder "insert" (Punkt+abstand) ---
;; Der Scanner liefert fuer AN8/SP8-Paare "connect": beide Stationen stehen im
;; DXF, abstand und rotation berechnet kreisel-connect-script daraus selbst
;; (abstand = Distanz - *kreisel-durchmesser*). "insert" bleibt fuer von Hand
;; gepflegte Eintraege moeglich (Schema wie mubea.json).
(defun hundm05:build-kreisel (e / id fn typ hoehe ptStart ptEnd x y z abstand rotation ent)
(setq id (ssg-val e "id")
fn (ssg-val e "function")
typ (ssg-val e "typ")
hoehe (ssg-val e "hoehe"))
(if (null typ) (setq typ "STANDARD"))
(if (numberp hoehe) (setq hoehe (float hoehe)) (setq hoehe nil))
(if (= fn "connect")
(progn
(setq ptStart (list (hundm05:num e "start_x" 0.0)
(hundm05:num e "start_y" 0.0)
(hundm05:num e "start_z" 0.0)))
(setq ptEnd (list (hundm05:num e "end_x" 0.0)
(hundm05:num e "end_y" 0.0)
(hundm05:num e "end_z" 0.0)))
(princ (strcat "\n [KREISEL] " id " connect ("
(rtos (car ptStart) 2 0) "," (rtos (cadr ptStart) 2 0) ") -> ("
(rtos (car ptEnd) 2 0) "," (rtos (cadr ptEnd) 2 0) ")"
" H=" (if hoehe (rtos hoehe 2 0) "-")))
(setq ent (kreisel-connect-script ptStart ptEnd typ hoehe)))
(progn
(setq x (hundm05:num e "x" 0.0)
y (hundm05:num e "y" 0.0)
z (hundm05:num e "z" 0.0)
abstand (hundm05:num e "abstand" *kreisel-default-laenge*)
rotation (hundm05:num e "rotation" 0.0))
(princ (strcat "\n [KREISEL] " id " insert (" (rtos x 2 0) "," (rtos y 2 0)
"," (rtos z 2 0) ") A=" (rtos abstand 2 0)
" R=" (rtos rotation 2 1)))
(setq ent (kreisel-insert-script (list x y z) abstand rotation typ
(if hoehe hoehe z)))))
(hundm05:ent-json id "kreisel" ent)
)
;; --- Vario (VF_*) ---
;; Wie mubea:build-vario-one, aber ohne "winkel"-Zwang und ohne L_GF1-Ziel:
;; hundm05.json gibt nur deltaL/deltaH/richtung/seite/hz vor, den Bogenwinkel
;; waehlt berechne-alle-winkel selbst (kleinster gueltiger Kandidat) und der
;; L_GF-Split ist "gleichmaessig" (beide Haelften L_GF/2, Default nach
;; object_data.md 3.1). Ist "L_GF1" doch gesetzt, wird es als Vorlauf-Ziel
;; verwendet - dann verhaelt sich der Treiber wie der Mubea-Treiber.
(defun hundm05:build-vario (e / tid typ richtung deltaL deltaH seite hz startpunkt
gf1-ziel ergebnis best-winkel L_GF L_VF gf1 gf2
hoehe-von hoehe-bis lastEnt ent vf-nummer)
(setq tid (ssg-val e "test_id")
typ (ssg-val e "typ")
richtung (ssg-val e "richtung")
deltaL (hundm05:num e "deltaL" 0.0)
deltaH (hundm05:num e "deltaH" 0.0)
seite (ssg-val e "seite")
hz (hundm05:num e "hz" 0.0)
gf1-ziel (ssg-val e "L_GF1"))
(if (or (null typ) (= typ "")) (setq typ "standard"))
(if (or (null seite) (= seite "")) (setq seite "rechts"))
(if (or (null richtung) (= richtung "")) (setq richtung "Auf"))
(setq startpunkt (hundm05:startpunkt e "startpunkt"))
(if (null startpunkt) (hundm05:kein-startpunkt tid "startpunkt"))
(princ (strcat "\n [VARIO] " tid " (" typ ") -> deltaL=" (rtos deltaL 2 0)
" deltaH=" (rtos deltaH 2 0) " " richtung
" hz=" (rtos hz 2 0)))
(setq ergebnis
(if (= typ "etage")
(berechne-etage deltaL deltaH richtung seite)
(berechne-alle-winkel deltaL deltaH richtung nil)))
(setq best-winkel (car ergebnis) L_GF (cadr ergebnis) L_VF (caddr ergebnis))
;; startpunkt MUSS mit in die Bedingung: ohne ihn wuerde am Ursprung gebaut.
(if (and startpunkt best-winkel L_GF L_VF (> L_GF 0) (> L_VF 0))
(progn
;; L_GF-Split: mit L_GF1-Vorgabe wie bei Mubea (Vorlauf-Ziel), sonst
;; gleichmaessig auf Vor- und Nachlauf verteilt.
(if (numberp gf1-ziel)
(setq gf1 (min (float gf1-ziel) L_GF))
(setq gf1 (/ L_GF 2.0)))
(setq gf2 (max 0.0 (- L_GF gf1)))
(princ (strcat " -> Winkel=" (itoa best-winkel) " L_GF=" (rtos L_GF 2 1)
" L_VF=" (rtos L_VF 2 1) " L_GF1=" (rtos gf1 2 1)
" L_GF2=" (rtos gf2 2 1)))
;; Absolute Hoehen eintragen. hoehe-von/hoehe-bis gehen NUR in Label und
;; Attribute (MONTAGEHOEHE_m, HOEHE_VON_mm - vf_core.lsp), die Geometrie
;; kommt aus startpunkt. test_mubea.lsp uebergibt hier 0 und traegt damit
;; RELATIVE Hoehen ein; fuer diese Zeichnung stehen die echten Z-Werte im
;; DXF, also werden sie auch verwendet - sonst exportiert EXPORTCSV eine
;; falsche Montagehoehe (bei VF_HundM05_11 waeren es 2.914 m statt
;; -1.028 m).
(setq hoehe-von (caddr startpunkt))
(setq hoehe-bis (+ hoehe-von (if (= richtung "Auf") deltaH (- deltaH))))
(setq lastEnt (vf-lastent-ohne-attribute))
(if (= typ "etage")
(etage-foerderanlage-einfuegen deltaL deltaH richtung best-winkel gf1 gf2 L_VF startpunkt seite hz)
(variofoerderer-einfuegen deltaL deltaH richtung best-winkel gf1 gf2 L_VF startpunkt seite hz))
;; Blocknummer wie in der Produktion ueber vf-next-number holen (NICHT
;; fest auf 1) - sonst trifft das _.BLOCK des zweiten Foerderers auf einen
;; schon vorhandenen Namen, BricsCAD fragt "Neu definieren?" und
;; verschluckt die feste Antwortfolge (siehe test_mubea.lsp).
(setq vf-nummer (vf-next-number))
(setq ent (vf-block-erstellen typ seite vf-nummer 0 hoehe-von hoehe-bis
deltaH deltaL L_VF gf1 gf2 richtung best-winkel
startpunkt lastEnt hz "0" "Schoenenberger Geruest"))
(if (or (null ent) (/= (type ent) 'ENAME))
(setq ent (hundm05:last-insert-prefix "VF_")))
;; SSG_VF_EDIT-XDATA schreiben, sonst verweigert VARIOFOERDERER_EDIT beim
;; Doppelklick die Bearbeitung (vfs-edit-keine-xdata).
(if (and ent (= (type ent) 'ENAME) (member typ '("standard" "etage")))
(vf-edit-xdata-schreiben ent typ hz))
(hundm05:ent-json tid "vario" ent))
(progn
;; Ursache benennen, nicht pauschal "nicht loesbar" melden: ein fehlender
;; Startpunkt hat nichts mit dem Winkel-Solver zu tun, und eine falsche
;; Diagnose schickt die Fehlersuche in die falsche Richtung.
(if (null startpunkt)
(princ (strcat "\n FEHLER: " tid " nicht gebaut - Startpunkt fehlt"
" (siehe Meldung oben)."))
(princ (strcat "\n FEHLER: " tid " deltaL=" (rtos deltaL 2 0)
" deltaH=" (rtos deltaH 2 0) " nicht loesbar"
" (kein gueltiges L_GF/L_VF).")))
(hundm05:result-json tid "vario" "failed" nil nil startpunkt nil)))
)
;; --- Gefaellestrecke: Init-Konflikt umgehen -----------------------------
;; gefaellestrecke-einfuegen prueft "(if (not *lib-initialized*)
;; (gf-init-bibliothek))" - dasselbe Flag, das init-bibliothek (Vario) fuer
;; SEINE eigenen aus-dx/ein-dx setzt. Wird zuerst ein Vario gebaut, kaeme
;; gf-init-bibliothek nie mehr zum Zug und aus-dx/ein-dx blieben die
;; Vario-Werte. Darum einmal je Lauf erzwingen (wie in test_mubea.lsp).
(defun hundm05:ensure-gf-init ( / )
(if (not *hundm05-gf-init-done*)
(progn
(setq *lib-initialized* nil)
(gf-init-bibliothek)
(setq *lib-initialized* t)
(setq *hundm05-gf-init-done* T)))
)
;; --- Eine Gefaellestrecke bauen (Modus 1) ---
;; Aufbau ausschliesslich aus Anfangspunkt und horizontaler Gesamtlaenge
;; deltaL - KEIN Endpunkt (wie Produktion/gefaellestrecke.dcl Modus 1, siehe
;; object_data.md 2.1). L_stau aus deltaL abgeleitet, Formel identisch zu
;; test_gefaellestrecke.lsp/test_mubea.lsp.
(defun hundm05:build-gf (e / tid start-mm deltaL hz as-seite es-seite as-winkel
es-winkel winkel)
(setq tid (ssg-val e "test_id")
start-mm (hundm05:startpunkt e "start_mm")
deltaL (hundm05:num e "deltaL_mm" 0.0)
hz (hundm05:num e "hz_grad" 0.0)
as-seite (ssg-val e "as_seite")
es-seite (ssg-val e "es_seite")
as-winkel(ssg-val e "as_winkel")
es-winkel(ssg-val e "es_winkel")
winkel (hundm05:num e "winkel_grad" 3.0))
(if (or (null as-seite) (= as-seite "")) (setq as-seite "links"))
(if (or (null es-seite) (= es-seite "")) (setq es-seite "links"))
(if (or (null as-winkel) (= as-winkel "")) (setq as-winkel "90"))
(if (or (null es-winkel) (= es-winkel "")) (setq es-winkel "90"))
(if (null start-mm)
(progn
(hundm05:kein-startpunkt tid "start_mm")
(hundm05:result-json tid "gefaellestrecke" "failed" nil nil nil nil))
(hundm05:build-gf-punkt tid start-mm deltaL hz as-seite es-seite
as-winkel es-winkel winkel))
)
;; --- Gefaellestrecke mit gepruefter Startkoordinate bauen ---
(defun hundm05:build-gf-punkt (tid start-mm deltaL hz as-seite es-seite
as-winkel es-winkel winkel / rad sep-x L_stau ent)
(hundm05:ensure-gf-init)
;; Element-Masse zur gewaehlten AS/ES-Variante setzen, damit aus-dx/ein-dx
;; zur Variante (90/30 Grad) passen; danach auf 90 zuruecksetzen.
(vf-set-as-masse as-winkel as-seite)
(vf-set-es-masse es-winkel es-seite)
(setq rad (* winkel (/ pi 180.0)))
(setq sep-x (* 300.0 (cos rad)))
(setq L_stau (/ (- deltaL (float aus-dx) (float ein-dx) sep-x) (cos rad)))
(princ (strcat "\n [GF] " tid " -> Start=(" (rtos (car start-mm) 2 0)
"," (rtos (cadr start-mm) 2 0) "," (rtos (caddr start-mm) 2 0)
") hz=" (rtos hz 2 0) " deltaL=" (rtos deltaL 2 0)
" winkel=" (rtos winkel 2 2) " L_stau=" (rtos L_stau 2 1)))
(if (<= L_stau 0.0)
(progn
(princ (strcat " -> uebersprungen: L_stau <= 0 (deltaL " (rtos deltaL 2 0)
" zu kurz fuer AS/ES-Elemente + Separator)"))
(vf-set-as-masse "90" as-seite)
(vf-set-es-masse "90" es-seite)
(hundm05:result-json tid "gefaellestrecke" "failed" nil nil start-mm nil))
(progn
(setq ent (car (gefaellestrecke-einfuegen
L_stau winkel start-mm as-seite es-seite hz
deltaL as-winkel es-winkel "0" "Schoenenberger Geruest")))
(vf-set-as-masse "90" as-seite)
(vf-set-es-masse "90" es-seite)
(hundm05:ent-json tid "gefaellestrecke" ent)))
)
;; --- Waagrechte Foerderstrecke (GERADE_*) ---
;; Antrieb/Umlenk-Paare ohne Hoehendifferenz sind keine VarioFoerderer (deren
;; Winkel-Solver braucht eine Hoehendifferenz, siehe GERADE_MAX_DELTA_H in
;; lib/dxf_scan_components.py). Gebaut wird mit derselben Produktions-Primitive,
;; die der Etage-Foerderer fuer seine waagrechten Abschnitte nutzt:
;; insert-hz-scaled-block-by-ks skaliert Staustrecke_SP_1000_mm auf die Laenge
;; und richtet sie an hz aus (vert-winkel 0 = waagrecht).
;; Rueckgabe der Primitive ist der Ausgangspunkt, nicht die ENAME - der Block
;; wird darum ueber (entlast) eingesammelt.
(defun hundm05:build-gerade (e / tid laenge hz startpunkt blk ent)
(setq tid (ssg-val e "test_id")
laenge (hundm05:num e "laenge" 0.0)
hz (hundm05:num e "hz" 0.0)
startpunkt (hundm05:startpunkt e "startpunkt"))
(if (null startpunkt)
(hundm05:kein-startpunkt tid "startpunkt")
(princ (strcat "\n [GERADE] " tid " -> Start=(" (rtos (car startpunkt) 2 0)
"," (rtos (cadr startpunkt) 2 0) "," (rtos (caddr startpunkt) 2 0)
") laenge=" (rtos laenge 2 0) " hz=" (rtos hz 2 0))))
(if (or (null startpunkt) (<= laenge 0.0))
(progn
(if startpunkt (princ " -> uebersprungen: laenge <= 0"))
(hundm05:result-json tid "gerade" "failed" nil nil startpunkt nil))
(progn
;; Block VORHER laden und pruefen: insert-hz-scaled-block-by-ks ruft bei
;; fehlendem Block (exit) auf und wuerde den gesamten Testlauf abbrechen.
(setq blk (ssg-ils-block-laden "Staustrecke_SP_1000_mm"))
(if (null blk)
(progn
(princ " -> uebersprungen: Block Staustrecke_SP_1000_mm nicht ladbar")
(hundm05:result-json tid "gerade" "failed" "Staustrecke_SP_1000_mm"
nil startpunkt nil))
(progn
(insert-hz-scaled-block-by-ks "Staustrecke_SP_1000_mm"
startpunkt laenge hz 0)
(setq ent (entlast))
(if (and ent (atoms-family 1 '("ssg-id-generate")))
(ssg-id-generate ent))
(hundm05:ent-json tid "gerade" ent)))))
)
;; --- Omniflo (Eintraege mit "type": "bogen"/"weiche") ---
;; BEWUSST nicht gebaut. omni:insert-dxf (Lisp/OmniModulInsert.lsp) holt den
;; Einfuegepunkt zwingend per (command "_.INSERT" dxf-pfad pause ...) - es gibt
;; in Produktion keinen Aufrufpfad, der X/Y programmgesteuert uebergibt (siehe
;; object_data.md 4.1). Ein JSON-Treiber muesste den _.INSERT selbst nachbauen.
;; Im gewaehlten Abschnitt (Kreisel-Bereich) kommt kein einziger Omniflo-Eintrag
;; vor, darum wird hier nichts auf Verdacht implementiert, was der Testlauf nie
;; ausfuehren koennte. Der eigene Zweig ist trotzdem noetig: Omniflo-Eintraege
;; tragen ein "id"-Feld und wuerden sonst als Kreisel gebaut werden.
(defun hundm05:build-omniflo (e / tid)
(setq tid (cond ((ssg-val e "id")) ((ssg-val e "sivasnr")) ("OMNIFLO")))
(princ (strcat "\n [OMNIFLO] " tid " -> uebersprungen (nicht scriptbar, "
"siehe object_data.md 4.1)"))
(hundm05:result-json tid "omniflo" "unsupported"
(ssg-val e "sivasnr") nil
(list (hundm05:num e "x" 0.0) (hundm05:num e "y" 0.0)
(hundm05:num e "z" 0.0))
nil)
)
;; --- Ein einzelner Sensor/Block: Blockdefinition laden, dann per Name einfuegen ---
;; Wie mubea:build-separator-one. Der in JSON genannte Block wird ueber
;; ssg-ils-block-laden vorab definiert (loest 2D/3D-Suffix auf); schlaegt das
;; fehl, wird auf Separator_SP zurueckgefallen - genau das greift bei den
;; Blocknamen "S-LP"/"S-SP", die es in data/ils/ nicht gibt.
(defun hundm05:build-sensor (e idx / blockname x y z rot tid eff n pt)
(setq blockname (ssg-val e "block")
x (hundm05:num e "x" 0.0)
y (hundm05:num e "y" 0.0)
z (hundm05:num e "z" 0.0)
rot (hundm05:num e "rotation" 0.0)
tid (cond ((ssg-val e "test_id"))
((strcat "SENSOR_" (itoa idx)))))
(setq pt (list x y z))
(princ (strcat "\n [SENSOR] " tid " '" (if blockname blockname "?")
"' -> (" (rtos x 2 0) "," (rtos y 2 0) "," (rtos z 2 0)
") R=" (rtos rot 2 0)))
;; NICHT (and blockname (ssg-ils-block-laden ...)) verwenden: AutoLISPs AND
;; liefert T statt des letzten Wertes, das _.INSERT wuerde dann einen Block
;; namens T einfuegen (siehe Kommentar in test_mubea.lsp).
(setq eff (if blockname (ssg-ils-block-laden blockname)))
(if (null eff) (setq eff (ssg-ils-block-laden "Separator_SP")))
(if eff
(progn
(command "_.INSERT" eff pt "" "" rot)
;; Verbleibende Prompts GEZAEHLT mit Enter abfangen - ein Prompt, der die
;; Leer-Antwort ablehnt, haelt CMDACTIVE dauerhaft > 0 und eine ungebremste
;; Schleife wuerde BricsCAD haengen lassen.
(setq n 0)
(while (and (> (getvar "CMDACTIVE") 0) (< n 5))
(command "")
(setq n (1+ n)))
(if (> (getvar "CMDACTIVE") 0)
(progn
(command)
(princ (strcat "\n WARNUNG: INSERT von '" eff
"' nicht sauber beendet - abgebrochen."))))
(if (atoms-family 1 '("ssg-id-generate"))
(ssg-id-generate (entlast)))
(hundm05:ent-json tid "sensor" (entlast)))
(progn
;; Kein Block gefunden -> sichtbaren Platzhalter zeichnen
(if (not (tblsearch "LAYER" "HUNDM05_SENSOR"))
(entmake (list '(0 . "LAYER") '(2 . "HUNDM05_SENSOR") '(70 . 0) '(62 . 1))))
(entmake (list '(0 . "CIRCLE") (cons 10 pt) (cons 40 150.0)
'(8 . "HUNDM05_SENSOR")))
(princ " (Block fehlt - Platzhalter)")
(hundm05:result-json tid "sensor" "placeholder"
(if blockname blockname "Separator_SP") nil pt nil)))
)
;; --- JSON-Export der Ergebnisse ---
(defun hundm05:export-results (tests-out-dir / out-json f first)
(if (null *hundm05-test-results*)
(princ "\n Keine HundM05-Ergebnisse vorhanden.")
(progn
(vl-mkdir tests-out-dir)
(setq out-json (strcat tests-out-dir "/hundm05_results.json"))
(setq f (open out-json "w"))
(if f
(progn
(write-line "[" f)
(setq first T)
(foreach r *hundm05-test-results*
(if (not first) (write-line "," f))
(write-line r f)
(setq first nil))
(write-line "]" f)
(close f)
(princ (strcat "\n Ergebnisse: " out-json)))
(princ (strcat "\n FEHLER: Kann " out-json " nicht schreiben!")))))
)
;; ============================================================
;; C:TEST_HUNDM05 - Hauptbefehl
;; ============================================================
(defun c:TEST_HUNDM05 ( / json-datei daten eintrag tid res r
results-list sensor-idx anz-ok anz-fehler)
(dbgopen "hundm05_id.dbg" "DXFM_LOG")
;; Benoetigte Feature-Module laden
(ssg-ensure "KreiselInsert")
(ssg-ensure "VarioFoerderer")
(ssg-ensure "Gefaellestrecke")
(ssg-ensure "SSG_LIB_Commands")
(ssg-start "TEST_HUNDM05" '(("OSMODE") ("CECOLOR") ("ATTREQ") ("ATTDIA")))
(setvar "OSMODE" 0)
(setvar "ATTREQ" 0)
(setvar "ATTDIA" 0)
;; Block-Bibliothek (Bogen-/AS-/ES-Masse) initialisieren - fuer Vario+GF noetig
(if (or (not *lib-initialized*) (null bogen-auf))
(init-bibliothek))
;; Cache fuer den GF-Init-Workaround pro Lauf zuruecksetzen
(setq *hundm05-gf-init-done* nil)
;; Testdaten laden
(setq json-datei (strcat (getenv "DXFMAKRO") "/tests/testdata/hundm05.json"))
(if (not (findfile json-datei))
(progn
(princ (strcat "\n[TEST_HUNDM05] FEHLER: " json-datei " nicht gefunden!"))
(ssg-end)
(dbgclose)
(exit)))
(setq daten (ssg-load-json json-datei))
(if (null daten)
(progn
(princ "\n[TEST_HUNDM05] FEHLER: Testdaten konnten nicht geladen werden.")
(ssg-end)
(dbgclose)
(exit)))
(princ "\n\n================================================================")
(princ "\n TEST_HUNDM05 - HundM Kreisel-Abschnitt (ST500592_05.dxf)")
(princ (strcat "\n " (itoa (length daten)) " Eintraege geladen"))
(princ "\n================================================================")
(setq results-list '() sensor-idx 0 anz-ok 0 anz-fehler 0)
(foreach eintrag daten
(setq tid (ssg-val eintrag "test_id"))
(setq res
(cond
;; Sensor/Insert: hat "block"
((ssg-val eintrag "block")
(setq sensor-idx (1+ sensor-idx))
(hundm05:build-sensor eintrag sensor-idx))
;; Omniflo VOR Kreisel pruefen: diese Eintraege tragen "id" UND "type"
((ssg-val eintrag "type")
(hundm05:build-omniflo eintrag))
;; Kreisel: hat "id" (ohne "type")
((ssg-val eintrag "id")
(hundm05:build-kreisel eintrag))
;; Vario: test_id "VF_*"
((and tid (>= (strlen tid) 3) (= (substr tid 1 3) "VF_"))
(hundm05:build-vario eintrag))
;; Gefaellestrecke: test_id "GF_*"
((and tid (>= (strlen tid) 3) (= (substr tid 1 3) "GF_"))
(hundm05:build-gf eintrag))
;; Waagrechte Strecke: test_id "GERADE_*"
((and tid (>= (strlen tid) 7) (= (substr tid 1 7) "GERADE_"))
(hundm05:build-gerade eintrag))
(T
(princ "\n WARNUNG: Unbekannter Eintrag uebersprungen")
nil)))
;; Alle Builder liefern hier je einen String (keine Templates mit "anzahl"
;; wie bei Mubea) - der Vollstaendigkeit halber trotzdem Listen zulassen.
(if res
(progn
(if (= (type res) 'STR) (setq res (list res)))
(foreach r res
(setq results-list (cons r results-list))
(if (or (vl-string-search "\"executed\"" r)
(vl-string-search "\"placeholder\"" r)
(vl-string-search "\"unsupported\"" r))
(setq anz-ok (1+ anz-ok))
(setq anz-fehler (1+ anz-fehler)))))))
(princ "\n================================================================")
(princ (strcat "\n Ergebnis: " (itoa anz-ok) " OK, " (itoa anz-fehler) " Fehler"))
(princ "\n================================================================")
(setq *hundm05-test-results* (reverse results-list))
(ssg-end)
(princ "\n TEST_HUNDM05 abgeschlossen.")
(princ)
(dbgclose)
)
+260
View File
@@ -0,0 +1,260 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_hundm05.py - Validiert den HundM-Kreisel-Abschnitt.
Prueft die von TEST_HUNDM05 (test_hundm05.lsp) erzeugten Ergebnisse:
1. Alle Eintraege aus hundm05.json wurden verarbeitet (results.json)
2. Statuswerte (executed / placeholder fuer Sensoren / unsupported Omniflo)
3. Kreisel-Attribute (HOEHE, KREISELART, Block-Praefix)
4. Vario: VF_-Block, L_GF gleichmaessig verteilt (kein L_GF1 in den Daten)
5. DXF-Geometrie: Block-Anzahlen je Typ (ezdxf)
Die Testdaten stammen aus lib/dxf_scan_components.py (Fenster
216000 144000 228000 156000 der Zeichnung results/ST500592_05.dxf) und nicht
aus Handarbeit - anders als bei mubea.json.
"""
import pytest
# ============================================================
# Klassifikation der hundm05.json-Eintraege
# ============================================================
# Reihenfolge wie im Dispatcher von test_hundm05.lsp: Omniflo VOR Kreisel,
# weil Omniflo-Eintraege sowohl "id" als auch "type" tragen.
def _is_sensor(item):
return "block" in item
def _is_omniflo(item):
return "type" in item and "block" not in item
def _is_kreisel(item):
return "id" in item and "type" not in item and "block" not in item
def _hat_prefix(item, prefix):
return str(item.get("test_id", "")).startswith(prefix)
def _is_vario(item):
return _hat_prefix(item, "VF_")
def _is_gefaelle(item):
return _hat_prefix(item, "GF_")
def _is_gerade(item):
return _hat_prefix(item, "GERADE_")
def _expected_counts(testdata):
return {
"kreisel": sum(1 for t in testdata if _is_kreisel(t)),
"vario": sum(1 for t in testdata if _is_vario(t)),
"gefaellestrecke": sum(1 for t in testdata if _is_gefaelle(t)),
"gerade": sum(1 for t in testdata if _is_gerade(t)),
"sensor": sum(1 for t in testdata if _is_sensor(t)),
"omniflo": sum(1 for t in testdata if _is_omniflo(t)),
}
# ============================================================
# Testdaten-Pruefung (hundm05.json selbst)
# ============================================================
class TestHundM05Testdaten:
def test_jeder_eintrag_klassifizierbar(self, hundm05_testdata):
"""Kein Eintrag darf durch alle Zweige des Dispatchers fallen."""
for i, t in enumerate(hundm05_testdata):
erkannt = (_is_sensor(t) or _is_omniflo(t) or _is_kreisel(t)
or _is_vario(t) or _is_gefaelle(t) or _is_gerade(t))
assert erkannt, \
f"Eintrag {i} ist keinem Typ zuzuordnen: {sorted(t)}"
def test_kreisel_vorhanden(self, hundm05_testdata):
"""Der Abschnitt wurde wegen des Kreisels gewaehlt - er muss drin sein."""
kreisel = [t for t in hundm05_testdata if _is_kreisel(t)]
assert kreisel, "kein Kreisel in hundm05.json"
def test_vario_hat_hoehendifferenz(self, hundm05_testdata):
"""VarioFoerderer brauchen deltaH > 0 - der Winkel-Solver findet sonst
keinen Bogenwinkel. Waagrechte Paare gehoeren als 'gerade' ins JSON
(siehe GERADE_MAX_DELTA_H in lib/dxf_scan_components.py)."""
for t in hundm05_testdata:
if _is_vario(t):
assert float(t.get("deltaH", 0)) > 0, \
f'{t["test_id"]}: deltaH={t.get("deltaH")} - gehoert als ' \
f'"gerade" klassifiziert'
def test_gerade_hat_laenge(self, hundm05_testdata):
for t in hundm05_testdata:
if _is_gerade(t):
assert float(t.get("laenge", 0)) > 0, \
f'{t["test_id"]}: laenge={t.get("laenge")}'
def test_arrays_einzeilig(self):
"""Jedes Array muss komplett in EINER Zeile stehen.
ssg-parse-json-array (Lisp/ssg_core.lsp) liest die Datei zeilenweise,
und ssg-cfg-parse-array braucht das ganze Array in einem String. Ein
von json.dump(indent=2) umgebrochenes Array wird zu nil - der Treiber
baut das Element dann nicht an seiner Position. Das ist genau der
Fehler, der die Anlage einmal komplett am Ursprung aufgebaut hat, und
er ist in der Zeichnung nicht als Datenfehler zu erkennen.
"""
import os
pfad = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"testdata", "hundm05.json")
with open(pfad, encoding="utf-8") as fh:
for nr, zeile in enumerate(fh, start=1):
offen = zeile.count("[") - zeile.count("]")
# Die aeusserste Klammer des Dokuments darf allein stehen.
if zeile.strip() in ("[", "]"):
continue
assert offen == 0, \
f"Zeile {nr}: Array ueber mehrere Zeilen umgebrochen - " \
f"ssg-cfg-parse-array kann das nicht lesen: {zeile.strip()!r}"
# ============================================================
# Ergebnis-Pruefung (results.json vs. testdata)
# ============================================================
class TestHundM05Results:
def test_alle_verarbeitet(self, hundm05_testdata, hundm05_results):
"""Je Kind muss die erwartete Anzahl an Ergebnissen vorliegen."""
expected = _expected_counts(hundm05_testdata)
got = {}
for r in hundm05_results:
got[r["kind"]] = got.get(r["kind"], 0) + 1
for kind, n in expected.items():
assert got.get(kind, 0) == n, \
f'{kind}: erwartet {n}, verarbeitet {got.get(kind, 0)}'
def test_status_ok(self, hundm05_results):
"""Sensoren duerfen 'placeholder', Omniflo ist 'unsupported'
(omni:insert-dxf ist nicht scriptbar, siehe object_data.md 4.1),
alles andere muss 'executed' sein."""
for r in hundm05_results:
if r["kind"] == "sensor":
erlaubt = ("executed", "placeholder")
elif r["kind"] == "omniflo":
erlaubt = ("unsupported",)
else:
erlaubt = ("executed",)
assert r["status"] in erlaubt, \
f'{r["test_id"]} ({r["kind"]}): Status={r["status"]}, ' \
f'erlaubt {erlaubt}'
def test_kreisel_attribute(self, hundm05_testdata, hundm05_results):
"""Kreisel: KREISELART und HOEHE stimmen mit der Vorgabe, Blockname
traegt das Praefix KREISEL_. HOEHE wird von kreisel-connect-script aus
'hoehe' bzw. dem Z des Startpunkts gebildet (rtos ... 2 0, also
ganzzahlig)."""
by_id = {r["test_id"]: r for r in hundm05_results}
for t in hundm05_testdata:
if not _is_kreisel(t):
continue
r = by_id.get(t["id"])
assert r, f'Kreisel {t["id"]} fehlt im Ergebnis'
assert r["block_name"].startswith("KREISEL_"), \
f'{t["id"]}: Block "{r["block_name"]}" ohne Praefix KREISEL_'
assert r["actual_attributes"].get("KREISELART") == \
t.get("typ", "STANDARD"), \
f'{t["id"]}: KREISELART ist=' \
f'{r["actual_attributes"].get("KREISELART")}'
soll = t.get("hoehe", t.get("start_z", 0.0))
assert r["actual_attributes"].get("HOEHE") == str(int(round(soll))), \
f'{t["id"]}: HOEHE ist={r["actual_attributes"].get("HOEHE")}, ' \
f'erwartet={int(round(soll))}'
def test_kreisel_abstand(self, hundm05_testdata, hundm05_results):
"""Bei 'connect' berechnet kreisel-connect-script den Abstand als
Distanz(Start,Ende) - Kreiseldurchmesser (800mm). Der Block muss also
auf dem Startpunkt sitzen."""
by_id = {r["test_id"]: r for r in hundm05_results}
for t in hundm05_testdata:
if not _is_kreisel(t) or t.get("function") != "connect":
continue
r = by_id.get(t["id"])
assert r, f'Kreisel {t["id"]} fehlt im Ergebnis'
ip = r["insert_point"]
assert abs(ip[0] - t["start_x"]) < 1.0, \
f'{t["id"]}: Einfuegepunkt X {ip[0]} != start_x {t["start_x"]}'
assert abs(ip[1] - t["start_y"]) < 1.0, \
f'{t["id"]}: Einfuegepunkt Y {ip[1]} != start_y {t["start_y"]}'
def test_vario_block_und_split(self, hundm05_testdata, hundm05_results):
"""Vario: VF_-Block, und ohne L_GF1-Vorgabe wird L_GF gleichmaessig auf
Vor- und Nachlauf verteilt (Default nach object_data.md 3.1)."""
by_id = {r["test_id"]: r for r in hundm05_results}
for t in hundm05_testdata:
if not _is_vario(t):
continue
r = by_id.get(t["test_id"])
assert r, f'Vario {t["test_id"]} fehlt im Ergebnis'
assert r["block_name"].startswith("VF_"), \
f'{t["test_id"]}: Block "{r["block_name"]}" ist kein VF_-Block'
attrs = r["actual_attributes"]
if "L_GF1" in t or "L_GF_m" not in attrs:
continue
teile = attrs["L_GF_m"].split(",")
assert len(teile) == 2, \
f'{t["test_id"]}: L_GF_m Format "{attrs["L_GF_m"]}"'
vorne, hinten = float(teile[0]), float(teile[1])
assert abs(vorne - hinten) < 0.002, \
f'{t["test_id"]}: L_GF nicht gleichmaessig verteilt ' \
f'({vorne} / {hinten})'
def test_gefaelle_block(self, hundm05_testdata, hundm05_results):
by_id = {r["test_id"]: r for r in hundm05_results}
for t in hundm05_testdata:
if not _is_gefaelle(t):
continue
r = by_id.get(t["test_id"])
assert r, f'Gefaellestrecke {t["test_id"]} fehlt im Ergebnis'
assert r["block_name"].startswith("GF_"), \
f'{t["test_id"]}: Block "{r["block_name"]}" ist kein GF_-Block'
# ============================================================
# DXF Geometrie-Pruefung (ezdxf)
# ============================================================
class TestHundM05Geometry:
def _counts(self, dxf):
c = {}
for e in dxf.modelspace():
if e.dxftype() == "INSERT":
c[e.dxf.name] = c.get(e.dxf.name, 0) + 1
return c
def test_block_anzahlen(self, hundm05_dxf, hundm05_testdata):
"""Anzahl der KREISEL_/VF_/GF_-Bloecke >= Vorgabe aus hundm05.json."""
c = self._counts(hundm05_dxf)
exp = _expected_counts(hundm05_testdata)
n_kr = sum(v for n, v in c.items() if n.startswith("KREISEL_"))
n_vf = sum(v for n, v in c.items() if n.startswith("VF_"))
n_gf = sum(v for n, v in c.items() if n.startswith("GF_"))
assert n_kr >= exp["kreisel"], f"KREISEL_: {n_kr} < {exp['kreisel']}"
assert n_vf >= exp["vario"], f"VF_: {n_vf} < {exp['vario']}"
assert n_gf >= exp["gefaellestrecke"], \
f"GF_: {n_gf} < {exp['gefaellestrecke']}"
def test_handles_im_modelspace(self, hundm05_dxf, hundm05_results):
"""Jedes gebaute Block-Handle (executed) liegt im Modelspace."""
handles = {e.dxf.handle for e in hundm05_dxf.modelspace()
if e.dxftype() == "INSERT"}
for r in hundm05_results:
if r["status"] == "executed" and r["block_handle"]:
assert r["block_handle"] in handles, \
f'{r["test_id"]}: Handle {r["block_handle"]} nicht im ' \
f'Modelspace'
+573
View File
@@ -0,0 +1,573 @@
[
{
"id": "Kreisel_HundM05_01",
"function": "connect",
"start_x": 216120.19,
"start_y": 151070.0,
"start_z": 0.0,
"end_x": 239701.9,
"end_y": 151070.0,
"end_z": 0.0,
"typ": "STANDARD",
"hoehe": 0.0,
"comment": "aus DXF: AN8 (Layer 1ANTR-800) -> SP8 (Layer 1SP-800), Abstand 23581.71; daraus folgt produktionsseitig abstand = 23581.71 - 800.0 = 22781.71 und rotation = 0.00 Grad. ACHTUNG hoehe: AN8 und SP8 sind in dieser Zeichnung flache 2D-Symbole (nur Linien/Kreis/Text auf Z=0) und auf Z=0 eingefuegt - die Einfuegehoehe des Kreisels steht NICHT im DXF und muss hier von Hand gesetzt werden (vgl. mubea.json, wo z ebenfalls Handarbeit ist)."
},
{
"test_id": "GF_HundM05_04",
"modus": 1,
"deltaL_mm": 2094.45,
"winkel_grad": 48.21,
"hz_grad": 125.48,
"start_mm": [219540.85, 152832.91, 2342.96],
"as_seite": "links",
"es_seite": "links",
"as_winkel": "90",
"es_winkel": "90",
"comment": "aus DXF: 3D_FF_Antrieb_rechts -> Umlenkung FF (Layer _3D_Fingerförderer / z-3), XY-Abstand 2094.45, Neigung 48.21 Grad; Gefaellestrecke, da fallend und Neigung >= 3 Grad. as_/es_seite und -winkel sind Defaults (im DXF nicht ableitbar)."
},
{
"test_id": "VF_HundM05_05",
"typ": "standard",
"richtung": "Auf",
"deltaL": 1694.19,
"deltaH": 2057.58,
"startpunkt": [219542.27, 153175.18, 0.0],
"seite": "rechts",
"hz": 135.86,
"comment": "aus DXF: Antrieb FF -> 3D_FF_Umlenk_rechts (Layer z-3 / _3D_Fingerförderer), XY-Abstand 1694.19, Neigung 50.53 Grad; VarioFoerderer. 'winkel' bewusst nicht gesetzt - der Solver waehlt den kleinsten gueltigen Kandidaten. 'seite' ist Default (im DXF nicht ableitbar)."
},
{
"test_id": "GERADE_HundM05_08",
"typ": "gerade",
"laenge": 3305.26,
"hz": 143.35,
"startpunkt": [223150.89, 152953.65, 0.0],
"endpunkt": [220499.01, 154926.53, 0.0],
"comment": "aus DXF: Antrieb FF -> Umlenkung FF (Layer z-3 / z-3), XY-Abstand 3305.26, Neigung 0.00 Grad; waagrechte Foerderstrecke (Hoehendifferenz 0.00 mm < 10 mm) - KEIN VarioFoerderer, da dessen Winkel-Solver eine Hoehendifferenz braucht."
},
{
"test_id": "GF_HundM05_09",
"modus": 1,
"deltaL_mm": 3817.56,
"winkel_grad": 6.75,
"hz_grad": 107.51,
"start_mm": [223151.48, 152603.13, 2337.66],
"as_seite": "links",
"es_seite": "links",
"as_winkel": "90",
"es_winkel": "90",
"comment": "aus DXF: 3D_FF_Antrieb_rechts -> 3D_FF_Umlenk_rechts (Layer _3D_Fingerförderer / _3D_Fingerförderer), XY-Abstand 3817.56, Neigung 6.75 Grad; Gefaellestrecke, da fallend und Neigung >= 3 Grad. as_/es_seite und -winkel sind Defaults (im DXF nicht ableitbar)."
},
{
"test_id": "GERADE_HundM05_10",
"typ": "gerade",
"laenge": 11940.15,
"hz": 33.61,
"startpunkt": [225207.36, 149949.84, 0.0],
"endpunkt": [235151.11, 156559.61, -3.45],
"comment": "aus DXF: Antrieb FF -> Umlenkung FF (Layer z-3 / z-3), XY-Abstand 11940.15, Neigung 0.02 Grad; waagrechte Foerderstrecke (Hoehendifferenz 3.45 mm < 10 mm) - KEIN VarioFoerderer, da dessen Winkel-Solver eine Hoehendifferenz braucht."
},
{
"test_id": "VF_HundM05_11",
"typ": "standard",
"richtung": "Auf",
"deltaL": 11769.21,
"deltaH": 5827.59,
"startpunkt": [225207.48, 149949.9, -3941.61],
"seite": "rechts",
"hz": 32.34,
"comment": "aus DXF: Antrieb FF -> 3D_FF_Umlenk_rechts (Layer z-3 / _3D_Fingerförderer), XY-Abstand 11769.21, Neigung 26.34 Grad; VarioFoerderer. 'winkel' bewusst nicht gesetzt - der Solver waehlt den kleinsten gueltigen Kandidaten. 'seite' ist Default (im DXF nicht ableitbar)."
},
{
"test_id": "VF_HundM05_12",
"typ": "standard",
"richtung": "Auf",
"deltaL": 3450.94,
"deltaH": 1885.98,
"startpunkt": [227298.92, 152942.18, 0.0],
"seite": "rechts",
"hz": 106.79,
"comment": "aus DXF: Antrieb FF -> 3D_FF_Umlenk_rechts (Layer z-3 / _3D_Fingerförderer), XY-Abstand 3450.94, Neigung 28.66 Grad; VarioFoerderer. 'winkel' bewusst nicht gesetzt - der Solver waehlt den kleinsten gueltigen Kandidaten. 'seite' ist Default (im DXF nicht ableitbar)."
},
{
"test_id": "GF_HundM05_13",
"modus": 1,
"deltaL_mm": 4078.06,
"winkel_grad": 29.82,
"hz_grad": 104.2,
"start_mm": [227300.62, 152605.32, 2337.74],
"as_seite": "links",
"es_seite": "links",
"as_winkel": "90",
"es_winkel": "90",
"comment": "aus DXF: 3D_FF_Antrieb_rechts -> Umlenkung FF (Layer _3D_Fingerförderer / z-3), XY-Abstand 4078.06, Neigung 29.82 Grad; Gefaellestrecke, da fallend und Neigung >= 3 Grad. as_/es_seite und -winkel sind Defaults (im DXF nicht ableitbar)."
},
{
"function": "insert",
"block": "Vario_Motorstation_500mm_rechts",
"x": 224346.52,
"y": 149549.9,
"z": 21677.04,
"rotation": 90.0,
"comment": "aus DXF: Block 'Antrieb FF', Layer 'z-3', auf Makro-Block 'Vario_Motorstation_500mm_rechts' abgebildet; ACHTUNG geraten (im DXF nicht ableitbar): Seite=rechts - bei Bedarf hier im JSON korrigieren"
},
{
"function": "insert",
"block": "Vario_Motorstation_500mm_rechts",
"x": 223998.19,
"y": 149549.84,
"z": 2384.8,
"rotation": 180.0,
"comment": "aus DXF: Block '3D_FF_Antrieb_rechts', Layer '_3D_Fingerförderer', auf Makro-Block 'Vario_Motorstation_500mm_rechts' abgebildet"
},
{
"function": "insert",
"block": "Vario_Motorstation_500mm_rechts",
"x": 224346.4,
"y": 149549.84,
"z": 0.0,
"rotation": 90.0,
"comment": "aus DXF: Block 'Antrieb FF', Layer 'z-3', auf Makro-Block 'Vario_Motorstation_500mm_rechts' abgebildet; ACHTUNG geraten (im DXF nicht ableitbar): Seite=rechts - bei Bedarf hier im JSON korrigieren"
},
{
"function": "insert",
"block": "Vario_Kurve_rechts_30_TEF_aussen",
"x": 219480.78,
"y": 153174.98,
"z": 2230.86,
"rotation": 270.0,
"comment": "aus DXF: Block '3D_FF_Bogen_außen_30°', Layer '_3D_Fingerförderer', auf Makro-Block 'Vario_Kurve_rechts_30_TEF_aussen' abgebildet; ACHTUNG geraten (im DXF nicht ableitbar): Seite=rechts - bei Bedarf hier im JSON korrigieren"
},
{
"function": "insert",
"block": "Vario_Kurve_rechts_30_TEF_innen",
"x": 220438.78,
"y": 154981.88,
"z": 1726.02,
"rotation": 90.0,
"comment": "aus DXF: Block '3D_FF_Bogen_innen_30°', Layer '_3D_Fingerförderer', auf Makro-Block 'Vario_Kurve_rechts_30_TEF_innen' abgebildet; ACHTUNG geraten (im DXF nicht ableitbar): Seite=rechts - bei Bedarf hier im JSON korrigieren"
},
{
"function": "insert",
"block": "Vario_Kurve_rechts_30_TEF_aussen",
"x": 219535.71,
"y": 153172.45,
"z": 2349.22,
"rotation": 270.0,
"comment": "aus DXF: Block '3D_ILS_Bogen_30°', Layer '_3D_ILS_Bogen_30°', auf Makro-Block 'Vario_Kurve_rechts_30_TEF_aussen' abgebildet; ACHTUNG geraten (im DXF nicht ableitbar): Lage=aussen, Seite=rechts - bei Bedarf hier im JSON korrigieren"
},
{
"function": "insert",
"block": "Vario_Kurve_rechts_30_TEF_aussen",
"x": 220500.01,
"y": 154973.55,
"z": 1850.0,
"rotation": 90.0,
"comment": "aus DXF: Block '3D_ILS_Bogen_30°', Layer '_3D_ILS_Bogen_30°', auf Makro-Block 'Vario_Kurve_rechts_30_TEF_aussen' abgebildet; ACHTUNG geraten (im DXF nicht ableitbar): Lage=aussen, Seite=rechts - bei Bedarf hier im JSON korrigieren"
},
{
"function": "insert",
"block": "Vario_Kurve_rechts_30_TEF_aussen",
"x": 222776.13,
"y": 149549.84,
"z": 2335.65,
"rotation": 0.0,
"comment": "aus DXF: Block '3D_ILS_Bogen_30°', Layer '_3D_ILS_Bogen_30°', auf Makro-Block 'Vario_Kurve_rechts_30_TEF_aussen' abgebildet; ACHTUNG geraten (im DXF nicht ableitbar): Lage=aussen, Seite=rechts - bei Bedarf hier im JSON korrigieren"
},
{
"function": "insert",
"block": "Vario_Kurve_rechts_30_TEF_aussen",
"x": 223249.77,
"y": 149949.81,
"z": 2360.47,
"rotation": 0.0,
"comment": "aus DXF: Block '3D_ILS_Bogen_30°', Layer '_3D_ILS_Bogen_30°', auf Makro-Block 'Vario_Kurve_rechts_30_TEF_aussen' abgebildet; ACHTUNG geraten (im DXF nicht ableitbar): Lage=aussen, Seite=rechts - bei Bedarf hier im JSON korrigieren"
},
{
"function": "insert",
"block": "AS_Element_90_rechts",
"x": 217765.98,
"y": 151472.13,
"z": 2099.94,
"rotation": 0.0,
"comment": "aus DXF: Block 'Ausschleuselement_V3', Layer '_3D_ILS_AE', auf Makro-Block 'AS_Element_90_rechts' abgebildet; ACHTUNG geraten (im DXF nicht ableitbar): Winkel=90, Seite=rechts - bei Bedarf hier im JSON korrigieren"
},
{
"function": "insert",
"block": "AS_Element_90_rechts",
"x": 221440.62,
"y": 151470.0,
"z": 2067.0,
"rotation": 0.0,
"comment": "aus DXF: Block 'Ausschleuselement_V3', Layer '_3D_ILS_AE', auf Makro-Block 'AS_Element_90_rechts' abgebildet; ACHTUNG geraten (im DXF nicht ableitbar): Winkel=90, Seite=rechts - bei Bedarf hier im JSON korrigieren"
},
{
"function": "insert",
"block": "AS_Element_90_rechts",
"x": 225739.77,
"y": 151470.0,
"z": 2067.0,
"rotation": 0.0,
"comment": "aus DXF: Block 'Ausschleuselement_V3', Layer '_3D_ILS_AE', auf Makro-Block 'AS_Element_90_rechts' abgebildet; ACHTUNG geraten (im DXF nicht ableitbar): Winkel=90, Seite=rechts - bei Bedarf hier im JSON korrigieren"
},
{
"function": "insert",
"block": "S-LP",
"x": 216525.22,
"y": 150670.0,
"z": 0.0,
"rotation": 0.0,
"comment": "aus DXF: Block 'S-LP', Layer '5SEP-LP'; Block 'S-LP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-LP",
"x": 217622.88,
"y": 151470.0,
"z": 0.0,
"rotation": 180.0,
"comment": "aus DXF: Block 'S-LP', Layer '5SEP-LP'; Block 'S-LP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-LP",
"x": 221298.51,
"y": 151470.0,
"z": 0.0,
"rotation": 180.0,
"comment": "aus DXF: Block 'S-LP', Layer '5SEP-LP'; Block 'S-LP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-LP",
"x": 224908.15,
"y": 150670.0,
"z": 0.0,
"rotation": 0.0,
"comment": "aus DXF: Block 'S-LP', Layer '5SEP-LP'; Block 'S-LP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-LP",
"x": 225508.15,
"y": 150670.0,
"z": 0.0,
"rotation": 0.0,
"comment": "aus DXF: Block 'S-LP', Layer '5SEP-LP'; Block 'S-LP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-LP",
"x": 225597.66,
"y": 151470.0,
"z": 0.0,
"rotation": 180.0,
"comment": "aus DXF: Block 'S-LP', Layer '5SEP-LP'; Block 'S-LP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-SP",
"x": 218325.21,
"y": 154009.54,
"z": 0.0,
"rotation": 270.0,
"comment": "aus DXF: Block 'S-SP', Layer '5SEP-SP'; Block 'S-SP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-SP",
"x": 219542.38,
"y": 151961.76,
"z": 0.0,
"rotation": 90.0,
"comment": "aus DXF: Block 'S-SP', Layer '5SEP-SP'; Block 'S-SP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-SP",
"x": 220498.95,
"y": 155645.73,
"z": 0.09,
"rotation": 90.0,
"comment": "aus DXF: Block 'S-SP', Layer '5SEP-SP'; Block 'S-SP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-SP",
"x": 220498.95,
"y": 155953.39,
"z": 0.09,
"rotation": 90.0,
"comment": "aus DXF: Block 'S-SP', Layer '5SEP-SP'; Block 'S-SP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-SP",
"x": 222000.85,
"y": 155698.7,
"z": 0.0,
"rotation": 270.0,
"comment": "aus DXF: Block 'S-SP', Layer '5SEP-SP'; Block 'S-SP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-SP",
"x": 222153.05,
"y": 149875.67,
"z": 0.0,
"rotation": 330.0,
"comment": "aus DXF: Block 'S-SP', Layer '5SEP-SP'; Block 'S-SP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-SP",
"x": 223150.84,
"y": 151972.72,
"z": -7.54,
"rotation": 90.0,
"comment": "aus DXF: Block 'S-SP', Layer '5SEP-SP'; Block 'S-SP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-SP",
"x": 223617.86,
"y": 149949.84,
"z": 0.0,
"rotation": 0.0,
"comment": "aus DXF: Block 'S-SP', Layer '5SEP-SP'; Block 'S-SP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-SP",
"x": 226300.0,
"y": 155718.69,
"z": 0.0,
"rotation": 270.0,
"comment": "aus DXF: Block 'S-SP', Layer '5SEP-SP'; Block 'S-SP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-SP",
"x": 227300.01,
"y": 151981.61,
"z": -7.54,
"rotation": 90.0,
"comment": "aus DXF: Block 'S-SP', Layer '5SEP-SP'; Block 'S-SP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "Scanner",
"x": 217622.88,
"y": 151190.0,
"z": 0.0,
"rotation": 270.0,
"comment": "aus DXF: Block 'SCAN', Layer 'ZZSCANNER', auf Makro-Block 'Scanner' abgebildet"
},
{
"function": "insert",
"block": "Scanner",
"x": 220218.95,
"y": 155609.84,
"z": 0.09,
"rotation": 180.0,
"comment": "aus DXF: Block 'SCAN', Layer 'ZZSCANNER', auf Makro-Block 'Scanner' abgebildet"
},
{
"function": "insert",
"block": "Scanner",
"x": 221298.51,
"y": 151190.0,
"z": 0.0,
"rotation": 270.0,
"comment": "aus DXF: Block 'SCAN', Layer 'ZZSCANNER', auf Makro-Block 'Scanner' abgebildet"
},
{
"function": "insert",
"block": "Scanner",
"x": 222013.05,
"y": 149633.18,
"z": 0.0,
"rotation": 240.0,
"comment": "aus DXF: Block 'SCAN', Layer 'ZZSCANNER', auf Makro-Block 'Scanner' abgebildet"
},
{
"function": "insert",
"block": "Scanner",
"x": 223617.86,
"y": 149669.84,
"z": 0.0,
"rotation": 270.0,
"comment": "aus DXF: Block 'SCAN', Layer 'ZZSCANNER', auf Makro-Block 'Scanner' abgebildet"
},
{
"function": "insert",
"block": "Scanner",
"x": 224882.1,
"y": 150392.28,
"z": 0.0,
"rotation": 270.0,
"comment": "aus DXF: Block 'SCAN', Layer 'ZZSCANNER', auf Makro-Block 'Scanner' abgebildet"
},
{
"function": "insert",
"block": "Scanner",
"x": 225597.66,
"y": 151190.0,
"z": 0.0,
"rotation": 270.0,
"comment": "aus DXF: Block 'SCAN', Layer 'ZZSCANNER', auf Makro-Block 'Scanner' abgebildet"
},
{
"function": "insert",
"block": "Separator_SP",
"x": 218326.39,
"y": 153912.84,
"z": 2066.06,
"rotation": 0.0,
"comment": "aus DXF: Block 'Separator 200000070', Layer '_3D_Separator', auf Makro-Block 'Separator_SP' abgebildet"
},
{
"function": "insert",
"block": "Separator_SP",
"x": 219544.61,
"y": 152086.0,
"z": 2318.68,
"rotation": 180.29,
"comment": "aus DXF: Block 'Separator 200000070', Layer '_3D_Separator', auf Makro-Block 'Separator_SP' abgebildet"
},
{
"function": "insert",
"block": "Separator_SP",
"x": 220500.0,
"y": 155763.99,
"z": 1876.16,
"rotation": 180.0,
"comment": "aus DXF: Block 'Separator 200000070', Layer '_3D_Separator', auf Makro-Block 'Separator_SP' abgebildet"
},
{
"function": "insert",
"block": "Separator_SP",
"x": 222001.54,
"y": 155600.71,
"z": 1923.14,
"rotation": 359.97,
"comment": "aus DXF: Block 'Separator 200000070', Layer '_3D_Separator', auf Makro-Block 'Separator_SP' abgebildet"
},
{
"function": "insert",
"block": "Separator_SP",
"x": 222001.69,
"y": 155900.21,
"z": 1905.78,
"rotation": 359.97,
"comment": "aus DXF: Block 'Separator 200000070', Layer '_3D_Separator', auf Makro-Block 'Separator_SP' abgebildet"
},
{
"function": "insert",
"block": "Separator_SP",
"x": 222201.22,
"y": 149847.53,
"z": 2318.04,
"rotation": 60.49,
"comment": "aus DXF: Block 'Separator 200000070', Layer '_3D_Separator', auf Makro-Block 'Separator_SP' abgebildet"
},
{
"function": "insert",
"block": "Separator_SP",
"x": 223149.87,
"y": 152086.02,
"z": 2318.68,
"rotation": 180.29,
"comment": "aus DXF: Block 'Separator 200000070', Layer '_3D_Separator', auf Makro-Block 'Separator_SP' abgebildet"
},
{
"function": "insert",
"block": "Separator_SP",
"x": 223747.95,
"y": 149950.46,
"z": 2386.66,
"rotation": 90.81,
"comment": "aus DXF: Block 'Separator 200000070', Layer '_3D_Separator', auf Makro-Block 'Separator_SP' abgebildet"
},
{
"function": "insert",
"block": "Separator_SP",
"x": 226300.69,
"y": 155600.71,
"z": 1923.14,
"rotation": 359.97,
"comment": "aus DXF: Block 'Separator 200000070', Layer '_3D_Separator', auf Makro-Block 'Separator_SP' abgebildet"
},
{
"function": "insert",
"block": "Separator_SP",
"x": 226300.84,
"y": 155900.21,
"z": 1905.78,
"rotation": 359.97,
"comment": "aus DXF: Block 'Separator 200000070', Layer '_3D_Separator', auf Makro-Block 'Separator_SP' abgebildet"
},
{
"function": "insert",
"block": "Separator_SP",
"x": 227298.05,
"y": 152086.02,
"z": 2318.68,
"rotation": 180.29,
"comment": "aus DXF: Block 'Separator 200000070', Layer '_3D_Separator', auf Makro-Block 'Separator_SP' abgebildet"
},
{
"function": "insert",
"block": "S-LP",
"x": 216630.12,
"y": 150670.0,
"z": 2167.0,
"rotation": 90.0,
"comment": "aus DXF: Block 'Separator LP 3D', Layer '_3D_Separator', auf Makro-Block 'S-LP' abgebildet; Block 'S-LP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-LP",
"x": 217517.98,
"y": 151470.0,
"z": 2167.0,
"rotation": 270.0,
"comment": "aus DXF: Block 'Separator LP 3D', Layer '_3D_Separator', auf Makro-Block 'S-LP' abgebildet; Block 'S-LP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-LP",
"x": 221193.61,
"y": 151470.0,
"z": 2167.0,
"rotation": 270.0,
"comment": "aus DXF: Block 'Separator LP 3D', Layer '_3D_Separator', auf Makro-Block 'S-LP' abgebildet; Block 'S-LP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-LP",
"x": 225013.06,
"y": 150670.0,
"z": 2167.0,
"rotation": 90.0,
"comment": "aus DXF: Block 'Separator LP 3D', Layer '_3D_Separator', auf Makro-Block 'S-LP' abgebildet; Block 'S-LP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-LP",
"x": 225492.76,
"y": 151470.0,
"z": 2167.0,
"rotation": 270.0,
"comment": "aus DXF: Block 'Separator LP 3D', Layer '_3D_Separator', auf Makro-Block 'S-LP' abgebildet; Block 'S-LP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
},
{
"function": "insert",
"block": "S-LP",
"x": 225613.06,
"y": 150670.0,
"z": 2167.0,
"rotation": 90.0,
"comment": "aus DXF: Block 'Separator LP 3D', Layer '_3D_Separator', auf Makro-Block 'S-LP' abgebildet; Block 'S-LP' ist in data/ils/ NICHT vorhanden - erzeugt im Testlauf einen Platzhalter"
}
]
+37 -7
View File
@@ -52,7 +52,7 @@
"function": "insert",
"block": "S-LP",
"x": 400,
"y": -3815,
"y": -4038,
"z": 1467,
"rotation": 90,
"assigned_to": "Kreisel_links"
@@ -62,7 +62,7 @@
"function": "insert",
"block": "Scanner",
"x": 400,
"y": -3925,
"y": -4083,
"z": 1467,
"rotation": 0,
"assigned_to": "Kreisel_links"
@@ -83,7 +83,7 @@
"block": "Scanner",
"x": 10310,
"y": -610,
"z": 1467,
"z": 1941,
"rotation": 0,
"assigned_to": "Kreisel_rechts"
},
@@ -264,13 +264,23 @@
"rotation": 180,
"assigned_to": "Kreisel_unten"
},
{
"comment": "Scanner unterer Kreisel",
"function": "insert",
"block": "Scanner",
"x": 1415,
"y": -5100,
"z": 1467,
"rotation": 270,
"assigned_to": "Kreisel_unten"
},
{
"comment": "Separator S-LP #11 Y-Abgleich mit Gefaellestrecken (Sep-Y=8777, GF_Mubea_04-Y=9126, diff=349mm, enger Treffer)",
"function": "insert",
"block": "S-LP",
"x": 9510,
"y": 13649,
"z": 1467,
"y": 14334,
"z": 1941,
"rotation": 270,
"assigned_to": "Kreisel_rechts"
},
@@ -293,13 +303,33 @@
"z": 1941,
"rotation": 90,
"assigned_to": "Kreisel_rechts"
},
{
"comment": "Separator Eingang 3 Rechter_Kreisel",
"function": "insert",
"block": "S-LP",
"x": 10310,
"y": -5073,
"z": 1941,
"rotation": 90,
"assigned_to": "Kreisel_rechts"
},
{
"comment": "",
"function": "insert",
"block": "S-LP",
"x": 8725,
"y": -4743,
"z": 1941,
"rotation": 180,
"assigned_to": "GF_Mubea"
},
{
"comment": "Separator S-LP #8 Y-Abgleich mit Gefaellestrecken (Sep-Y=-4823, GF_Mubea_20-Y=-3673, diff=1150mm, naechstgelegene Reihe, kein enger Treffer)",
"function": "insert",
"block": "S-LP",
"x": 9510,
"y": -3887,
"y": -3636,
"z": 1941,
"rotation": 270,
"assigned_to": "GF_Mubea_20"
@@ -309,7 +339,7 @@
"function": "insert",
"block": "Scanner",
"x": 9510,
"y": -3787,
"y": -3557,
"z": 1941,
"rotation": 0,
"assigned_to": "Kreisel_rechts"
+48 -1
View File
@@ -49,6 +49,7 @@ Grad). Richtung `hz`/`hz_grad`: 0=Ost(+X), 90=Nord(+Y), 180=West(-X),
| 4.3 | Omniflo TEF/Zubehoer | `omni:insert-dxf` (fester Sivasnr) | Position ja, Punkt interaktiv | 1 Punkt (nur via Wrapper) | [#43](#43-omniflo-tefzubehoer) |
| 4.4 | Omniflo TV_*/APBW_* | — (DUMMY) | **nein** | — | [#44](#44-omniflo-tv_-apbw_) |
| 5 | Separator/Scanner/Sensor | `ils-insert-sensor (fname)` + `_.INSERT` | **nein** (Schleife) | 1 Punkt `x,y,z` (nur via Wrapper) | [#5](#5-separatorscannersensor) |
| 6 | Waagrechte Foerderstrecke (`gerade`) | `insert-hz-scaled-block-by-ks (blockname startpunkt laenge hz vert)` | **ja** | 1 Punkt `startpunkt` (+ `endpunkt` als Doku) | [#6](#6-waagrechte-foerderstrecke-gerade) |
Legende Scriptbar-Spalte:
- **ja** — direkt per JSON aufrufbar.
@@ -541,7 +542,53 @@ Vollstaendiges JSON (wie in `mubea.json`s Separator-Eintraegen):
---
## 6. Zusammenfassung: scriptbar vs. nicht scriptbar
## 6. Waagrechte Foerderstrecke (`gerade`)
Produktionsfunktion:
`insert-hz-scaled-block-by-ks (blockname startpunkt laenge hz-winkel vert-winkel)`
(`Lisp/vf_etage.lsp`) — dieselbe Primitive, mit der der Etage-Foerderer seine
waagrechten Abschnitte baut. Skaliert `Staustrecke_SP_1000_mm` auf `laenge`
(Massstab = `laenge / 1000`) und richtet sie ueber `KS_EIN`/`KS_AUS` am
`startpunkt` aus.
Dieser Typ entstand beim Nachbau von `results/ST500592_05.dxf`: ein
Antriebs-/Umlenkstation-Paar **ohne** Hoehendifferenz ist fachlich kein
VarioFoerderer. Dessen Solver (`berechne-standard`
`berechne-alle-winkel`) muss einen Bogenwinkel aus der Kandidatenliste
3…51 Grad waehlen, was bei `deltaH ≈ 0` nicht aufgeht — der Eintrag waere
unbaubar. `lib/dxf_scan_components.py` trennt solche Paare darum ueber
`GERADE_MAX_DELTA_H` (10 mm) ab und gibt sie als `gerade` aus.
| Feld | Typ | Pflicht | Bedeutung | Default |
|---|---|---|---|---|
| `test_id` | string | **ja** | Kennung, Praefix `GERADE_` (steuert den Dispatcher) | - |
| `typ` | string | nein | `"gerade"` (Dokumentation, nicht ausgewertet) | - |
| `laenge` | real (mm) | **ja** | horizontale Laenge; Massstab der Staustrecke | - |
| `hz` | real (Grad) | nein | Fahrtrichtung in der XY-Ebene | 0.0 |
| `startpunkt` | [x,y,z] (mm) | **ja** | Anfangspunkt (`KS_EIN` wird hierhin gelegt) | - |
| `endpunkt` | [x,y,z] (mm) | nein | Endpunkt — reine Dokumentation/Herkunft, **kein** Parameter; die Geometrie folgt aus `startpunkt` + `laenge` + `hz` | - |
`vert-winkel` wird vom Treiber fest auf `0` gesetzt (waagrecht). Die Funktion
gibt den **Ausgangspunkt** zurueck, nicht die Entity — ein Treiber holt den
Block ueber `(entlast)`. Bei fehlender Blockdatei ruft sie `(exit)` auf und
wuerde einen Testlauf abbrechen, darum vorher mit `ssg-ils-block-laden`
pruefen (so macht es `hundm05:build-gerade` in `tests/test_hundm05.lsp`).
Vollstaendiges JSON (wie in `hundm05.json`):
```json
{
"test_id": "GERADE_HundM05_08",
"typ": "gerade",
"laenge": 3305.26,
"hz": 143.35,
"startpunkt": [223150.89, 152953.65, 0.0],
"endpunkt": [220499.01, 154926.53, 0.0]
}
```
---
## 7. Zusammenfassung: scriptbar vs. nicht scriptbar
| Element | Produktionsfunktion | Scriptbar? |
|---|---|---|