bbc9e9b9f9
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>
125 lines
4.6 KiB
Python
125 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
extract_polylines.py - Liest LWPOLYLINE-Objekte aus einem DXF und schreibt
|
|
sie als JSON (Handle, Layer, Stuetzpunkte in OCS und Weltkoordinaten,
|
|
Segment-/Gesamtlaengen).
|
|
|
|
Hintergrund: data/polylines.dxf enthaelt 5 LWPOLYLINE-Objekte auf Layer
|
|
"Z-6" - Streckenzuege, die reale Foerderer-Pfade der HundM05-Zeichnung
|
|
nachbilden (siehe data/polylines.md, von Hand ausgewertet). Dieses Skript
|
|
macht dieselbe Auswertung reproduzierbar und maschinenlesbar, als Grundlage
|
|
fuer einen neuen Testfall (siehe tests/testdata/object_data.md fuer das
|
|
allgemeine JSON-Schema).
|
|
|
|
Eine Polylinie kann auf einer gekippten Ebene liegen (Gruppencode
|
|
210/220/230 != (0,0,1)); die Gruppencodes 10/20 sind dann OCS-, keine
|
|
Weltkoordinaten. Die Umrechnung nutzt ezdxf (Arbitrary-Axis-Algorithmus),
|
|
darum reicht hier ezdxf.readfile() statt eines eigenen Streaming-Parsers -
|
|
polylines.dxf ist mit ca. 124 MB ASCII-DXF deutlich kleiner als die
|
|
binaeren GB-Zeichnungen, fuer die dxf_scan_components.py den mmap-Weg
|
|
braucht.
|
|
|
|
Aufruf:
|
|
python extract_polylines.py <dxf> <output.json> [--layer Z-6]
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
import ezdxf
|
|
|
|
PREFIX = "[extract_polylines]"
|
|
|
|
RUND = 4
|
|
|
|
|
|
def arrays_einzeilig(text):
|
|
"""Rein numerische JSON-Arrays auf eine Zeile ziehen (wie in
|
|
dxf_scan_components.py) - noetig, falls die Datei spaeter ueber
|
|
ssg-parse-json-array (zeilenweiser LISP-Parser) gelesen wird."""
|
|
def zusammen(m):
|
|
return "[" + " ".join(m.group(1).split()) + "]"
|
|
return re.sub(r"\[([\s\d.,+\-eE]*?)\]", zusammen, text)
|
|
|
|
|
|
def lese_polylinien(dxf_path, layer):
|
|
doc = ezdxf.readfile(dxf_path)
|
|
msp = doc.modelspace()
|
|
ergebnis = []
|
|
for pl in msp.query("LWPOLYLINE"):
|
|
if layer and pl.dxf.layer != layer:
|
|
continue
|
|
elevation = pl.dxf.elevation
|
|
ocs = pl.ocs()
|
|
punkte_ocs = [(p[0], p[1]) for p in pl.get_points("xy")]
|
|
punkte_welt = [ocs.to_wcs((x, y, elevation)) for x, y in punkte_ocs]
|
|
|
|
laengen = [0.0]
|
|
for i in range(1, len(punkte_welt)):
|
|
a, b = punkte_welt[i - 1], punkte_welt[i]
|
|
laengen.append(math.dist((a.x, a.y, a.z), (b.x, b.y, b.z)))
|
|
|
|
ergebnis.append({
|
|
"handle": pl.dxf.handle,
|
|
"layer": pl.dxf.layer,
|
|
"geschlossen": bool(pl.closed),
|
|
"elevation": round(elevation, RUND),
|
|
"extrusion": [round(v, RUND) for v in tuple(pl.dxf.extrusion)],
|
|
"anzahl_punkte": len(punkte_welt),
|
|
"punkte_ocs": [[round(x, RUND), round(y, RUND)]
|
|
for x, y in punkte_ocs],
|
|
"punkte_welt": [[round(p.x, RUND), round(p.y, RUND),
|
|
round(p.z, RUND)] for p in punkte_welt],
|
|
"segment_laengen": [round(l, RUND) for l in laengen],
|
|
"gesamtlaenge": round(sum(laengen), RUND),
|
|
})
|
|
return ergebnis
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Liest LWPOLYLINE-Objekte aus einem DXF und schreibt "
|
|
"sie (inkl. OCS->Welt-Umrechnung) als JSON.")
|
|
parser.add_argument("dxf", help="Quellzeichnung (ASCII- oder "
|
|
"binaeres DXF)")
|
|
parser.add_argument("output", help="Ziel-JSON")
|
|
parser.add_argument("--layer", default="Z-6",
|
|
help="Nur Polylinien auf diesem Layer (Default: "
|
|
"Z-6, leer = alle Layer)")
|
|
args = parser.parse_args()
|
|
|
|
if not os.path.isfile(args.dxf):
|
|
print("%s FEHLER: DXF nicht gefunden: %s" % (PREFIX, args.dxf))
|
|
sys.exit(1)
|
|
|
|
groesse = os.path.getsize(args.dxf) / (1024.0 * 1024.0)
|
|
print("%s lese %s (%.1f MB) ..." % (PREFIX, args.dxf, groesse))
|
|
|
|
ergebnis = lese_polylinien(args.dxf, args.layer)
|
|
print("%s %d LWPOLYLINE-Objekte auf Layer '%s' gefunden"
|
|
% (PREFIX, len(ergebnis), args.layer or "*"))
|
|
for e in ergebnis:
|
|
print("%s Handle %-4s %d Punkte Gesamtlaenge %.2f mm%s"
|
|
% (PREFIX, e["handle"], e["anzahl_punkte"], e["gesamtlaenge"],
|
|
" (gekippte Ebene)" if e["extrusion"] != [0.0, 0.0, 1.0]
|
|
else ""))
|
|
|
|
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(ergebnis, ensure_ascii=False,
|
|
indent=2)))
|
|
fh.write("\n")
|
|
print("%s %d Eintraege geschrieben -> %s"
|
|
% (PREFIX, len(ergebnis), args.output))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|