Files
dxfmakros/lib/vf_journal_export.py
T
s.ayadi bbc9e9b9f9 [FEAT] Werkzeuge zum Auslesen fremder Zeichnungen und der VF-Eingabe-Journale
Grundlage fuer reproduzierbare Nachbau-Testfaelle aus echten Projekt-
zeichnungen (HundM05).

- lib/vf_journal_export.py (neu): liest die Eingabe-Journale der VF_n-Bloecke
  aus der XDATA (App SSG_VF_EDIT, Marker "linienzug") und schreibt sie als
  Frage-Antwort-Protokoll im Schema von tests/testdata/linienzug_tests.json -
  Gegenstueck zu vfl-entry->string, ohne ezdxf (reine Gruppencode-Lesung).
  Optional --csv fuer Strecken-ID und Erwartungswerte je Kette.
- lib/extract_polylines.py (neu): LWPOLYLINE-Objekte als JSON, inkl.
  OCS->Welt-Umrechnung fuer gekippte Ebenen (Handles, Segment-/Gesamtlaengen).
- lib/dxf_scan_components.py: erkennt jetzt zusaetzlich, ob ein Block ein
  2D-Schema oder ein 3D-Modell ist (TYPEN_3D, ist_3d_block/darstellung_von) -
  eine Anlagenzeichnung enthaelt beides. Dazu Sensor-Erfassung,
  Zwillings-Zusammenfuehrung und Hoehenschaetzung aus der Umgebung.
- lib/dxf_abbild.py: gibt die erkannte Darstellung je Element mit aus.
- tests/test_vf_journal_grammatik.py (neu): prueft den Journal-Dekoder gegen
  handgebaute Journale - ohne CAD und ohne die grosse DXF. Deckt die zwei
  Zweige ab, die in den echten Ketten nicht vorkommen: Glied "Linie-VF" und
  eine VF-Einheit mit gewinkeltem Erstkoerper (dort fragt vfl-vf-einheit
  KEINE Separator-/Endpunkt-Fragen vorab, anders als beim horizontalen).
- data/polylines.md, tests/testdata/hundm05_linienzug_polylines.json:
  ausgewertete Streckenzuege der Quellzeichnung.
- .gitignore: die Quellzeichnungen selbst (data/polylines.dxf 124 MB,
  data/polylines.dwg, tests/HM5.dwg) bleiben draussen - eingecheckt ist die
  Auswertung, nicht die Zeichnung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 10:13:56 +02:00

655 lines
28 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
vf_journal_export.py - Liest die SSG_VF_EDIT-Eingabe-Journale der VF_n-Bloecke
aus einer Zeichnung und schreibt sie als Frage-Antwort-Protokoll (JSON) im
Schema von tests/testdata/linienzug_tests.json.
Hintergrund: Der Linienzug-Modus (Lisp/vf_linienzug.lsp) ist nicht scriptbar -
er fragt jede Eingabe einzeln ab (getpoint/getstring/getint/getreal) und
zeichnet sie in einem Journal auf, das nach dem Bau als XDATA (App
SSG_VF_EDIT, Marker "linienzug") am fertigen VF_n-Block landet. Dieses Journal
ist die vollstaendige, wiederabspielbare Bau-Anleitung der Kette
(vfl-journal-replay-start). Dieses Skript dreht die Serialisierung aus
vfl-entry->string wieder zurueck und annotiert jeden Wert mit der Frage, die
er beantwortet - abgeleitet aus dem Frageablauf von vf-linienzug-modus.
Bewusst OHNE ezdxf: gebraucht werden nur INSERT-Kopf (Blockname, Einfuegepunkt,
Drehung) und die 1000er-Gruppen der XDATA - das ist reine Gruppencode-Paar-
Lesung und funktioniert auch bei ASCII-DXF im 100-MB-Bereich.
Zwei Eigenheiten der Quelldaten, die hier aufgeloest werden:
1. Das Newline zwischen zwei Journal-Eintraegen steht im ASCII-DXF als
Steuerzeichen-Notation "^J" (zwei Zeichen) - ein echtes 0x0A wuerde die
zeilenweise DXF-Struktur zerreissen. Getrennt wird darum an "^J".
2. Kopierte Ketten teilen ihr Journal: die vier VF_46-Bloecke in
data/polylines.dxf sind Kopien EINER gebauten Kette und tragen deshalb
Wort fuer Wort dasselbe Journal - inklusive des Startpunkts der zuerst
gebauten Kette. Alle uebrigen Eintraege sind relativ (Distanz, Winkel,
Zielhoehen), nur dieser eine Punkt ist absolut. Er wird darum je Instanz
auf deren echten Blockeinfuegepunkt gezogen (--kein-startpunkt-fix
schaltet das ab), damit jeder Eintrag fuer sich abspielbar ist.
Aufruf:
python vf_journal_export.py <dxf> <output.json> [--csv <sivas_export.csv>]
[--kein-startpunkt-fix]
Beispiel (erzeugt tests/testdata/hundm05.json):
python lib/vf_journal_export.py data/polylines.dxf \
tests/testdata/hundm05.json --csv results/HundM_export.csv
"""
import argparse
import csv
import json
import os
import re
import sys
PREFIX = "[vf_journal_export]"
XDATA_APP = "SSG_VF_EDIT"
MARKER = "linienzug"
# Steuerzeichen-Notation des Eintrags-Trenners im ASCII-DXF (siehe Kopf).
TRENNER = "^J"
RUND = 6
# Antwort-Klartexte der Menues aus vf_linienzug.lsp (nur zur Annotation).
JA_NEIN = {"1": "Ja", "2": "Nein"}
SEITE = {"1": "links", "2": "rechts"}
VARIANTE = {"1": "aussen", "2": "innen"}
GF_MODUS = {"1": "ueber Zielhoehe", "2": "ueber Neigungswinkel"}
ENDPUNKT3 = {"1": "Ja, nur Motorstation", "2": "Nein, weiterbauen",
"3": "Ja, Motorstation + Kettenende am Zielpunkt"}
KETTENENDE3 = {"1": "Ja, mit ES-Element", "2": "Ja, ohne ES-Element",
"3": "Nein, weiterbauen"}
KETTENENDE2 = {"1": "Ja, Separator + ES-Element", "2": "Nein, weiterbauen"}
VERTEILUNG = {"1": "halbe Staustrecke am Ausgang", "2": "alles am Einlauf"}
IM_VF = {"1": "Horizontaler Foerderer", "2": "Vario-Kurve",
"3": "Auf/Ab-Foerderer"}
MENU_START = {"1": "Neue Linie: GF", "2": "Neue Linie: Ab/Auf VF",
"3": "Horizontal-VF", "4": "Neue Linie bis Kettenende"}
MENU_WEITER = {"1": "GF-Bogen", "2": "Neue Linie: GF",
"3": "Neue Linie: Ab/Auf VF", "4": "Horizontal-VF",
"5": "Neue Linie bis Kettenende"}
STEP_KOMMENTAR = ("Glied-Marker (vfl-journal-mark) - beim Replay "
"uebersprungen, Label = gewaehlter Segmenttyp")
# ============================================================
# DXF lesen
# ============================================================
def gruppen(dxf_pfad):
"""Alle (Gruppencode, Wert)-Paare der Datei."""
with open(dxf_pfad, "r", encoding="utf-8", errors="replace") as fh:
zeilen = fh.read().splitlines()
return [(zeilen[i].strip(), zeilen[i + 1])
for i in range(0, len(zeilen) - 1, 2)]
def lese_vf_inserts(dxf_pfad):
"""INSERTs mit SSG_VF_EDIT-XDATA einsammeln.
Rueckgabe: Liste von Dicts (handle, block, punkt, rotation, marker,
journal_roh). Das Journal steckt in den 1000er-Gruppen NACH dem Marker
und ist auf 250 Zeichen je Gruppe gestueckelt (DXF-Limit 255).
"""
paare = gruppen(dxf_pfad)
treffer = []
i = 0
while i < len(paare):
code, wert = paare[i]
if code != "0" or wert != "INSERT":
i += 1
continue
e = {"handle": None, "block": None, "x": 0.0, "y": 0.0, "z": 0.0,
"rotation": 0.0, "marker": None, "chunks": []}
in_app = False
j = i + 1
while j < len(paare) and paare[j][0] != "0":
c, v = paare[j]
if c == "5" and e["handle"] is None:
e["handle"] = v
elif c == "2" and e["block"] is None:
e["block"] = v
elif c == "10":
e["x"] = float(v)
elif c == "20":
e["y"] = float(v)
elif c == "30":
e["z"] = float(v)
elif c == "50":
e["rotation"] = float(v)
elif c == "1001":
in_app = (v == XDATA_APP)
elif c == "1000" and in_app:
if e["marker"] is None:
e["marker"] = v
else:
e["chunks"].append(v)
j += 1
if e["marker"] is not None:
e["punkt"] = [e["x"], e["y"], e["z"]]
e["journal_roh"] = "".join(e["chunks"])
del e["chunks"]
treffer.append(e)
i = j
return treffer
# ============================================================
# Journal dekodieren
# ============================================================
class Journal(object):
"""Ein Journal (kind:payload je Zeile) in das Frage-Antwort-Protokoll
uebersetzen. Der Ablauf folgt vf-linienzug-modus (Modus 1):
Praeambel: Startpunkt, Starthoehe, AS-Element (Ja/Nein, Winkel, Seite)
Hauptschleife je Glied: STEP-Marker, Menuewahl, dann die Fragen des
gewaehlten Segmenttyps
Innerhalb einer VF-Einheit (Horizontal-VF / Linie): Separator vor/nach,
"Ist der Endpunkt der Foerderer?", danach Horizontal / Vario-Kurve /
Auf-Ab bis die Einheit schliesst
Kettenende: ES-Glied (Winkel, Seite)
"""
def __init__(self, roh):
self.tok = []
for zeile in roh.split(TRENNER):
if zeile:
art, _, wert = zeile.partition(":")
self.tok.append((art, wert))
self.pos = 0
self.out = []
self.erster_step = True
self.erster_dl = True
# --- Tokenzugriff ---
def art(self):
return self.tok[self.pos][0] if self.pos < len(self.tok) else None
def label(self):
return self.tok[self.pos][1] if self.pos < len(self.tok) else None
def nimm(self, erwartet):
if self.pos >= len(self.tok):
raise ValueError("Journal endet vor %s" % erwartet)
art, wert = self.tok[self.pos]
if art != erwartet:
raise ValueError("Token %d: %s erwartet, %s:%s gefunden"
% (self.pos, erwartet, art, wert))
self.pos += 1
return wert
# --- Ausgabe ---
def add(self, typ, kommentar, **felder):
eintrag = {"typ": typ}
eintrag.update(felder)
eintrag["kommentar"] = kommentar
self.out.append(eintrag)
def frage_str(self, kommentar_fmt, tabelle=None):
w = self.nimm("STR")
self.add("string", kommentar_fmt % (tabelle.get(w, "?") if tabelle
else w), wert=w)
return w
def frage_real(self, kommentar_fmt):
w = float(self.nimm("REAL"))
self.add("real", kommentar_fmt % w, wert=w)
return w
def frage_int(self, kommentar_fmt):
w = int(self.nimm("INT"))
self.add("int", kommentar_fmt % w, wert=w)
return w
def marker(self, label):
self.add("step", STEP_KOMMENTAR, wert=label)
def frage_linie(self, was):
"""Segmentlaenge; die gesnappte Fahrtrichtung steht nur beim ersten
Segment der Kette im Journal (vfl-in-abstand), danach erbt jedes
Segment die Richtung des Vorgaengers."""
dl = float(self.nimm("DL"))
if self.erster_dl and self.art() == "REAL":
hz = float(self.nimm("REAL"))
self.erster_dl = False
self.add("point_rel",
"getpoint %s: dL=%.1f mm, gesnappte Fahrtrichtung "
"hz=%.0f Grad (nur beim ersten Segment der Kette "
"journalisiert)" % (was, dl, hz), dL=dl, hz=hz)
else:
self.erster_dl = False
self.add("point_rel",
"getpoint %s: dL=%.1f mm (Fahrtrichtung vom "
"Vorgaenger geerbt)" % (was, dl), dL=dl)
return dl
# --- Ablauf ---
def dekodiere(self):
self.praeambel()
while self.pos < len(self.tok):
label = self.nimm("STEP")
self.marker(label)
if label == "ES":
self.glied_es(marker_gelesen=True)
continue
if self.erster_step:
tabelle, kopf = MENU_START, (
"Naechstes Element am Kettenanfang (1=Neue Linie GF, "
"2=Neue Linie Ab/Auf VF, 3=Horizontal-VF, 4=Neue Linie "
"bis Kettenende) -> %s")
else:
tabelle, kopf = MENU_WEITER, (
"Naechstes Element (1=GF-Bogen, 2=Neue Linie GF, "
"3=Neue Linie Ab/Auf VF, 4=Horizontal-VF, 5=Neue Linie "
"bis Kettenende) -> %s")
self.frage_str("getstring " + kopf, tabelle)
self.erster_step = False
if label == "Linie-GF":
self.glied_linie_gf()
elif label == "Linie-VF":
self.glied_linie_vf()
elif label == "Horizontal-VF":
self.glied_horizontal_vf()
elif label == "Linie":
self.glied_linie_kettenende()
elif label == "GF-Bogen":
self.frage_int("getint GF-Bogen Winkel (30/60/90 Grad) "
"-> %d")
self.frage_str("getstring GF-Bogen Seite (1=links, "
"2=rechts) -> %s", SEITE)
else:
raise ValueError("unbekanntes Glied: %s" % label)
if self.pos != len(self.tok):
raise ValueError("nicht alle Tokens verbraucht (%d von %d)"
% (self.pos, len(self.tok)))
return self.out
def praeambel(self):
punkt = [float(v) for v in self.nimm("PT").split(",")]
self.add("point_abs", "getpoint Startpunkt der Kette (absoluter "
"Weltpunkt, einziger Anker der ganzen Kette)", wert=punkt)
self.frage_real("getreal Hoehe (Z) Startpunkt -> %.1f mm")
if self.frage_str("getstring AS-Element setzen? (1=Ja, 2=Nein) "
"-> %s", JA_NEIN) == "1":
w = self.nimm("STR")
self.add("string", "getstring AS-Element Winkel (Wert direkt, "
"90 oder 30 Grad) -> %s Grad" % w, wert=w)
self.frage_str("getstring AS-Seite (1=links, 2=rechts) -> %s",
SEITE)
def glied_linie_gf(self):
self.frage_linie("Endpunkt der GF-Linie")
if self.frage_str("getstring Gefaelle festlegen (1=ueber Zielhoehe, "
"2=ueber Neigungswinkel) -> %s", GF_MODUS) == "2":
self.frage_real("getreal Neigungswinkel -> %.1f Grad")
else:
self.frage_real("getreal Zielhoehe (Z) Linienendpunkt "
"-> %.1f mm")
if self.frage_str("getstring Ist das Kettenende? (1=Ja mit ES, "
"2=Ja ohne ES, 3=Nein weiterbauen) -> %s",
KETTENENDE3) == "1":
self.glied_es()
def glied_horizontal_vf(self):
"""VF-Einheit mit HORIZONTALEM Anfangskoerper (winkel1=0 in
vfl-vf-einheit): dort stellt vfl-baue-horizontal-koerper im
Ziel-Modus die Separator-/Endpunkt-Fragen VOR der
Laengenberechnung."""
self.frage_linie("Endpunkt des horizontalen VF-Stuecks")
self.vf_einheit(auto_ende=False, erster_horizontal=True)
def glied_linie_vf(self):
""""Neue Linie: Ab/Auf VF" - Laenge, Zielhoehe, ggf. Winkelwahl
(vfl-vf-entscheidung), dann eine VF-Einheit mit GEWINKELTEM
Anfangskoerper. Zu dem Koerper wird nichts gefragt (vfl-vf-einheit
baut ihn direkt), die erste Frage der Einheit ist also die
Endpunkt-Frage der Fortsetzungsschleife."""
self.frage_linie("Endpunkt des Ab/Auf-VF-Segments")
self.frage_real("getreal Zielhoehe (Z) Linienendpunkt -> %.1f mm")
self.frage_winkelwahl()
self.vf_einheit(auto_ende=False, erster_horizontal=False)
def glied_linie_kettenende(self):
""""Neue Linie bis Kettenende": Laenge + Zielhoehe, danach entscheidet
vfl-segment-entscheidung selbst. Reine Gefaellestrecke -> ES-Element
ohne weitere Frage; VF-Einheit -> vf_einheit mit auto-ende."""
self.frage_linie("Endpunkt der Linie bis Kettenende (Ziel-XY der "
"Kette)")
self.frage_real("getreal Zielhoehe (Z) am Kettenende -> %.1f mm")
self.frage_winkelwahl()
if self.art() == "STEP" and self.label() == "ES":
self.glied_es()
elif self.art() == "STR":
# vfl-segment-entscheidung liefert fuer typ="VF" immer einen
# Winkel > 0 (die VF-Winkelstufen beginnen bei 3 Grad), der erste
# Koerper ist hier also nie horizontal.
self.vf_einheit(auto_ende=True, erster_horizontal=False)
def glied_es(self, marker_gelesen=False):
if not marker_gelesen:
label = self.nimm("STEP")
if label != "ES":
raise ValueError("ES-Marker erwartet, %s gefunden" % label)
self.marker(label)
w = self.nimm("STR")
self.add("string", "getstring ES-Element Winkel (Wert direkt, 90 "
"oder 30 Grad) -> %s Grad" % w, wert=w)
self.frage_str("getstring ES-Seite (1=links, 2=rechts) -> %s", SEITE)
def frage_winkelwahl(self):
"""Winkelwahl (vfl-waehle-winkel) steht nur im Journal, wenn mehrere
Kandidatenwinkel gueltig waren - bei genau einem faellt die Frage
weg."""
if self.art() == "INT":
self.frage_int("getint Winkelwahl aus den gueltigen "
"Kandidaten (vfl-waehle-winkel) -> Option %d")
def horizontal_koerper(self):
"""vfl-baue-horizontal-koerper im Ziel-Modus: Separator vor/nach und
die Endpunkt-Frage stehen VOR der Laengenberechnung. Rueckgabe: die
Endpunkt-Antwort, die die Fortsetzungsschleife weiterverwendet."""
self.frage_str("getstring Separator VOR dem horizontalen Stueck? "
"(1=Ja, 2=Nein) -> %s", JA_NEIN)
self.frage_str("getstring Separator NACH dem horizontalen Stueck? "
"(1=Ja, 2=Nein) -> %s", JA_NEIN)
return self.frage_str("getstring Ist der Endpunkt der Foerderer? "
"(1=Ja nur Motorstation, 2=Nein weiterbauen, "
"3=Ja Motorstation + Kettenende) -> %s",
ENDPUNKT3)
def vario_kurve(self):
label = self.nimm("STEP")
if label != "Vario-Kurve":
raise ValueError("Vario-Kurve-Marker erwartet, %s" % label)
self.marker(label)
self.frage_int("getint Vario-Kurve Winkel (30/60/90 Grad, Wert "
"direkt) -> %d Grad")
self.frage_str("getstring Vario-Kurve Seite (1=links, 2=rechts) "
"-> %s", SEITE)
self.frage_str("getstring Vario-Kurve Variante (1=aussen, 2=innen) "
"-> %s", VARIANTE)
def vf_einheit(self, auto_ende, erster_horizontal):
"""Eine VF-Einheit: GF-Verteilung, erster Koerper, dann die
Fortsetzungsschleife (vfl-vf-einheit) bis Motorstation/Kettenende,
danach der Abschluss (vfl-vf-einheit-abschluss).
erster_horizontal spiegelt die Verzweigung ueber winkel1 in
vfl-vf-einheit: nur der horizontale Erstkoerper laeuft durch
vfl-baue-horizontal-koerper und fragt darum Separator vor/nach und
die Endpunkt-Frage schon vorab (deren Antwort die Schleife dann
uebernimmt, statt erneut zu fragen). Ein gewinkelter Erstkoerper
wird ohne jede Frage gebaut."""
self.frage_str("getstring GF-Verteilung (1=halbe Staustrecke am "
"Ausgang, 2=alles am Einlauf) -> %s", VERTEILUNG)
vorantwort = self.horizontal_koerper() if erster_horizontal else None
ziel_ende = False
es_gewuenscht = True
while True:
if vorantwort is None:
antwort = self.frage_str(
"getstring Ist der Endpunkt der Foerderer? (1=Ja nur "
"Motorstation, 2=Nein weiterbauen, 3=Ja Motorstation + "
"Kettenende) -> %s", ENDPUNKT3)
else:
antwort, vorantwort = vorantwort, None
if antwort == "1":
break
if antwort == "3":
es_gewuenscht = self.frage_str(
"getstring ES-Element setzen? (1=Ja, 2=Nein - Kette "
"endet direkt am Zielpunkt) -> %s", JA_NEIN) != "2"
self.frage_linie("Zielpunkt des Kettenendes")
self.frage_real("getreal Zielhoehe (Z) am Kettenende "
"-> %.1f mm")
self.frage_winkelwahl()
ziel_ende = True
break
wahl = self.frage_str("getstring Naechstes in der VF-Einheit "
"(1=Horizontaler Foerderer, "
"2=Vario-Kurve, 3=Auf/Ab-Foerderer) "
"-> %s", IM_VF)
if wahl == "1":
self.frage_linie("Endpunkt des horizontalen VF-Stuecks")
vorantwort = self.horizontal_koerper()
elif wahl == "2":
self.vario_kurve()
else:
self.frage_linie("Endpunkt des Auf/Ab-VF-Segments")
self.frage_real("getreal Zielhoehe (Z) am Segmentende "
"-> %.1f mm")
self.frage_winkelwahl()
if ziel_ende and not es_gewuenscht:
return
if auto_ende or ziel_ende:
self.glied_es()
return
if self.frage_str("getstring Ist das Kettenende? (1=Ja, Separator + "
"ES-Element, 2=Nein, weiterbauen) -> %s",
KETTENENDE2) == "1":
self.glied_es()
else:
self.frage_str("getstring Separator an dieser Stelle einfuegen? "
"(1=Ja, 2=Nein) -> %s", JA_NEIN)
# ============================================================
# Sivas-Export als Quervergleich
# ============================================================
def lese_strecken(csv_pfad):
"""Die "ILS 2.0 Strecke"-Zeilen eines Sivas-Exports (EXPORTSIVAS).
Dient nur der Zuordnung/Dokumentation: die Merkmale (DeltaL, DeltaH,
L_GF_m, L_VF_m, ...) sind die Attribute des fertigen VF_n-Blocks und
damit der Erwartungswert zu einem nachgebauten Foerderer.
"""
strecken = []
with open(csv_pfad, encoding="utf-8") as fh:
for zeile in csv.DictReader(fh, delimiter=";"):
if zeile.get("TeileArt") != "ILS 2.0 Strecke":
continue
if not zeile.get("Nachbarn"):
continue # unverbunden - gehoert zu keinem Kreisel
strecken.append({
"id": zeile["TeileId"],
"bezeichnung": zeile["Bezeichnung"],
"planquadrat": zeile["Planquadrat"],
"x": float(zeile["Position"].split(",")[0]),
"merkmale": json.loads(zeile["Merkmale"]),
})
return strecken
def erwartung_hinweis(strecke):
m = strecke["merkmale"]
felder = ["DeltaL_mm", "DeltaH_mm", "Hoehe_Von_mm", "Hoehe_Bis_mm",
"L_GF_m", "L_VF_m", "VF_Winkel", "Anzahl_Separator"]
werte = ", ".join("%s=%s" % (f, m[f]) for f in felder if f in m)
return ("Sivas-Export (EXPORTSIVAS), Strecke %s: %s"
% (strecke["id"], werte))
# ============================================================
# Kurzbeschreibung der Kette
# ============================================================
def kurzbeschreibung(eintraege):
"""Kette in einer Zeile: die Glied-Marker mit ihren Kernwerten."""
teile = []
for i, e in enumerate(eintraege):
if e["typ"] != "step":
continue
folge = eintraege[i + 1:i + 8]
strings = [x["wert"] for x in folge if x["typ"] == "string"]
ints = [x["wert"] for x in folge if x["typ"] == "int"]
reals = [x["wert"] for x in folge if x["typ"] == "real"]
laengen = [x["dL"] for x in folge if x["typ"] == "point_rel"]
label = e["wert"]
if label == "Vario-Kurve":
teile.append("Vario-Kurve %d %s %s"
% (ints[0], SEITE.get(strings[0], "?"),
VARIANTE.get(strings[1], "?")))
elif label == "ES":
teile.append("ES %s %s" % (strings[0], SEITE.get(strings[1], "?")))
elif label == "Linie-GF":
teile.append("GF-Linie %.0f mm / %.0f Grad"
% (laengen[0], reals[0]))
elif label == "Horizontal-VF":
teile.append("Horizontal-VF %.0f mm" % laengen[0])
elif label == "Linie":
teile.append("Linie bis Kettenende %.0f mm auf Z%.0f"
% (laengen[0], reals[0]))
elif label == "GF-Bogen":
teile.append("GF-Bogen %d %s" % (ints[0],
SEITE.get(strings[0], "?")))
return " -> ".join(teile)
# ============================================================
# Testfaelle bauen
# ============================================================
def baue_testfaelle(inserts, strecken, startpunkt_fix, quelle_datei,
ziel_datei):
"""FLACHE Testfall-Liste: je Kette ein Kopf-Objekt (traegt "test_id"),
danach ihre Eingaben (tragen "typ") - Struktur wie linienzug_tests.json.
Bewusst NICHT verschachtelt ("eingaben": [...]): ssg-load-json /
ssg-parse-json-array (Lisp/ssg_core.lsp) liest die Datei ZEILENWEISE und
behandelt jedes "{" als Objektanfang. Ein verschachteltes Array wuerde die
Kopf-Objekte verschlucken. Der Treiber gruppiert wieder ueber "test_id"
(siehe hundm05:gruppiere in tests/test_hundm05.lsp).
"""
# Anlagenkuerzel der test_id kommt aus dem Namen der Ziel-JSON (die
# Testfall-Sammlung), nicht aus dem Zeichnungsnamen: tests/testdata/
# hundm05.json -> VF_hundm05_LZ_01 .. _nn.
praefix = os.path.splitext(os.path.basename(ziel_datei))[0]
faelle = []
for nr, e in enumerate(sorted(inserts, key=lambda i: i["x"]), start=1):
eintraege = Journal(e["journal_roh"]).dekodiere()
journal_start = list(eintraege[0]["wert"])
punkt = [round(v, RUND) for v in e["punkt"]]
if startpunkt_fix:
eintraege[0]["wert"] = punkt
eintraege[1]["wert"] = punkt[2]
eintraege[1]["kommentar"] = ("getreal Hoehe (Z) Startpunkt "
"-> %.1f mm" % punkt[2])
else:
punkt = [round(v, RUND) for v in journal_start]
strecke = None
if strecken:
strecke = min(strecken, key=lambda s: abs(s["x"] - e["x"]))
kopf = {
"test_id": "VF_%s_LZ_%02d" % (praefix, nr),
# "anlagetyp", nicht "typ": "typ" markiert im flachen Array einen
# EINGABE-Eintrag (point_abs/real/string/int/step) - ein Kopf mit
# "typ" waere davon nicht zu unterscheiden.
"anlagetyp": "linienzug",
"modus": 1,
"anzahl_eingaben": len(eintraege),
"block": e["block"],
"dxf_handle": e["handle"],
"startpunkt_mm": punkt,
"rotation_grad": round(e["rotation"], 4),
"beschreibung": "Linienzug (Modus 1), Start Z%.0f: %s"
% (punkt[2], kurzbeschreibung(eintraege)),
}
if strecke:
kopf["sivas_id"] = strecke["id"]
kopf["sivas_bezeichnung"] = strecke["bezeichnung"]
kopf["planquadrat"] = strecke["planquadrat"]
kopf["erwartung_hinweis"] = erwartung_hinweis(strecke)
# Ohne Anfuehrungszeichen im Text: ssg-cfg-parse-value schneidet einen
# String am ersten/letzten Zeichen ab und kennt keine Escapes.
kopf["quelle"] = ("INSERT %s (Block %s) in %s, XDATA-App %s, Marker "
"%s; im Journal aufgezeichneter Kettenstart: [%s]"
% (e["handle"], e["block"],
os.path.basename(quelle_datei), XDATA_APP,
e["marker"],
", ".join("%.6f" % v for v in journal_start)))
faelle.append(kopf)
faelle.extend(eintraege)
return faelle
def arrays_einzeilig(text):
"""Rein numerische JSON-Arrays auf eine Zeile ziehen (wie
dxf_scan_components.py) - ssg-parse-json-array (Lisp/ssg_core.lsp) liest
zeilenweise und kann ein umgebrochenes Zahlen-Array nicht lesen."""
def zusammen(m):
return "[" + " ".join(m.group(1).split()) + "]"
return re.sub(r"\[([\s\d.,+\-eE]*?)\]", zusammen, text)
def main():
p = argparse.ArgumentParser(
description="Liest die SSG_VF_EDIT-Eingabe-Journale der VF_n-Bloecke "
"einer Zeichnung und schreibt sie als Frage-Antwort-"
"Protokoll (Schema linienzug_tests.json).")
p.add_argument("dxf", help="Quellzeichnung (ASCII-DXF)")
p.add_argument("output", help="Ziel-JSON")
p.add_argument("--csv", help="Sivas-Export (EXPORTSIVAS) der gleichen "
"Zeichnung - liefert Strecken-ID und "
"Erwartungswerte je Kette")
p.add_argument("--kein-startpunkt-fix", action="store_true",
help="Kettenstart NICHT auf den Blockeinfuegepunkt "
"ziehen (Journal unveraendert uebernehmen)")
args = p.parse_args()
if not os.path.isfile(args.dxf):
print("%s FEHLER: DXF nicht gefunden: %s" % (PREFIX, args.dxf))
sys.exit(1)
mb = os.path.getsize(args.dxf) / (1024.0 * 1024.0)
print("%s lese %s (%.1f MB) ..." % (PREFIX, args.dxf, mb))
inserts = lese_vf_inserts(args.dxf)
print("%s %d INSERT(s) mit %s-XDATA gefunden"
% (PREFIX, len(inserts), XDATA_APP))
inserts = [e for e in inserts if e["marker"].startswith(MARKER)]
if not inserts:
print("%s FEHLER: kein Linienzug-Journal (Marker \"%s\") in der "
"Zeichnung" % (PREFIX, MARKER))
sys.exit(1)
strecken = lese_strecken(args.csv) if args.csv else []
if args.csv:
print("%s %d verbundene Strecken im Sivas-Export"
% (PREFIX, len(strecken)))
faelle = baue_testfaelle(inserts, strecken,
not args.kein_startpunkt_fix, args.dxf,
args.output)
koepfe = [f for f in faelle if "test_id" in f]
for f in koepfe:
print("%s %s %s (%s) %d Eingaben"
% (PREFIX, f["test_id"], f["block"],
f.get("sivas_bezeichnung", "ohne Sivas-Zuordnung"),
f["anzahl_eingaben"]))
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:
fh.write(arrays_einzeilig(json.dumps(faelle, ensure_ascii=False,
indent=2)))
fh.write("\n")
print("%s %d Ketten / %d JSON-Objekte geschrieben -> %s"
% (PREFIX, len(koepfe), len(faelle), args.output))
if __name__ == "__main__":
main()