[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>
This commit is contained in:
2026-09-03 10:13:56 +02:00
parent 0015d6aa5b
commit bbc9e9b9f9
8 changed files with 1651 additions and 39 deletions
+18 -7
View File
@@ -46,7 +46,8 @@ 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)
RUND, darstellung_von, familie_von, insert_matrix, lese_inserts,
im_fenster)
PREFIX = "[dxf_abbild]"
@@ -146,13 +147,19 @@ def welt_daten(rec, mutter, block_base):
return mat, ursprung, rot
def element_dict(rec, ursprung, rot, tiefe):
def element_dict(rec, ursprung, rot, tiefe, block_info=None,
dim_memo=None):
"""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 ""
if block_info is not None:
# 2D-Schema oder 3D-Modell - die Zeichnung enthaelt beides fuer
# dieselbe Anlage (siehe dxf_scan_components.TYPEN_3D).
eintrag["darstellung"] = darstellung_von(rec["name"], block_info,
dim_memo)
eintrag["typ"] = typ
eintrag["typ_quelle"] = typ_quelle
eintrag["position"] = [round(ursprung.x, RUND) + 0.0,
@@ -183,13 +190,14 @@ def element_dict(rec, ursprung, rot, tiefe):
def baue_abbild(ms_inserts, block_inserts, block_base, fenster=None,
max_tiefe=12, ohne_container=False):
max_tiefe=12, ohne_container=False, block_info=None):
"""Alle Modelspace-INSERTs als Abbild-Baum beschreiben.
Rueckgabe: (elemente, statistik). Jedes Element der obersten Ebene enthaelt
seine aufgeloesten Unterkomponenten unter "unterkomponenten".
"""
stat = collections.Counter()
dim_memo = {}
def kinder_von(name, mat, tiefe, pfad):
"""Verschachtelte INSERTs eines Blocks rekursiv beschreiben."""
@@ -203,7 +211,8 @@ def baue_abbild(ms_inserts, block_inserts, block_base, fenster=None,
stat["container_uebersprungen"] += 1
continue
kind_mat, ursprung, rot = welt_daten(rec, mat, block_base)
eintrag = element_dict(rec, ursprung, rot, tiefe)
eintrag = element_dict(rec, ursprung, rot, tiefe, block_info,
dim_memo)
stat["unterkomponenten"] += 1
if rec["name"] not in pfad:
enkel = kinder_von(rec["name"], kind_mat, tiefe + 1,
@@ -224,7 +233,7 @@ def baue_abbild(ms_inserts, block_inserts, block_base, fenster=None,
if ohne_container and GEOMETRIE_CONTAINER.match(rec["name"]):
stat["container_uebersprungen"] += 1
continue
eintrag = element_dict(rec, ursprung, rot, 0)
eintrag = element_dict(rec, ursprung, rot, 0, block_info, dim_memo)
eintrag["nr"] = nr
eintrag.move_to_end("nr", last=False)
kinder = kinder_von(rec["name"], mat, 1, frozenset({rec["name"]}))
@@ -264,14 +273,16 @@ def main():
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)
(ms_inserts, block_inserts, block_base, statistik,
block_info) = 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)
ohne_container=args.ohne_geometrie_container,
block_info=block_info)
# Kennzahlen, die belegen, wie belastbar die Typangaben sind
quellen = collections.Counter()