diff --git a/bin/tro_annotate.bat b/bin/tro_annotate.bat new file mode 100644 index 0000000..8ea58d8 --- /dev/null +++ b/bin/tro_annotate.bat @@ -0,0 +1,33 @@ +@echo off +REM ================================================================ +REM SPS_SKEL - TROs in die BricsCAD-Zeichnung einblenden +REM ================================================================ +REM Aufruf: +REM bin\tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --check +REM bin\tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --flow --fb --legend +REM +REM Die CSV-Datei und die Zeichnung werden in %SKEL_DATA% gesucht (oder als +REM vollstaendiger Pfad angegeben). Das beschriftete DXF liegt in +REM %SKEL_RESULTS%, der gepruefte Lagebezug in %SKEL_CFG%. +REM +REM Die Originalzeichnung wird nie veraendert. DWG wird nicht unterstuetzt. +REM ================================================================ + +setlocal + +REM Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_CFG, SKEL_LIB, PYTHONPATH ...) +call "%~dp0setenv.bat" >nul + +REM Virtuelle Umgebung nutzen (ezdxf und pydantic werden benoetigt) +if exist "%SPS_SKEL%\.venv\Scripts\activate.bat" ( + call "%SPS_SKEL%\.venv\Scripts\activate.bat" +) + +REM Python-Interpreter bestimmen (py-Launcher bevorzugt, sonst python) +set "PY=py" +py --version >nul 2>&1 || set "PY=python" + +"%PY%" "%SKEL_LIB%\tro_annotate.py" %* +set "RC=%ERRORLEVEL%" + +endlocal & exit /b %RC% diff --git a/bin/tro_annotate.sh b/bin/tro_annotate.sh new file mode 100644 index 0000000..9cfe92a --- /dev/null +++ b/bin/tro_annotate.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# ================================================================ +# SPS_SKEL - TROs in die BricsCAD-Zeichnung einblenden +# ================================================================ +# Aufruf: +# bash bin/tro_annotate.sh --file mubea.csv --dxf 500573_60_1.dxf --check +# bash bin/tro_annotate.sh --file mubea.csv --dxf 500573_60_1.dxf --flow --fb --legend +# +# Die CSV-Datei und die Zeichnung werden in $SKEL_DATA gesucht (oder als +# vollstaendiger Pfad angegeben). Das beschriftete DXF liegt in $SKEL_RESULTS, +# der gepruefte Lagebezug in $SKEL_CFG. +# +# Die Originalzeichnung wird nie veraendert. DWG wird nicht unterstuetzt. +# ================================================================ + +set -eu + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Umgebung setzen (SKEL_DATA, SKEL_RESULTS, SKEL_CFG, SKEL_LIB, PYTHONPATH ...) +# shellcheck source=/dev/null +. "$SCRIPT_DIR/setenv.sh" >/dev/null + +# Virtuelle Umgebung nutzen (ezdxf und pydantic werden benoetigt) +if [ -f "$SPS_SKEL/.venv/bin/activate" ]; then + # shellcheck source=/dev/null + . "$SPS_SKEL/.venv/bin/activate" +fi + +# Python-Interpreter bestimmen +PY="python3" +command -v python3 >/dev/null 2>&1 || PY="python" + +exec "$PY" "$SKEL_LIB/tro_annotate.py" "$@" diff --git a/cfg/dxf_registration.json b/cfg/dxf_registration.json new file mode 100644 index 0000000..4866bee --- /dev/null +++ b/cfg/dxf_registration.json @@ -0,0 +1,30 @@ +{ + "mubea.csv|500573_60_1.dxf": { + "dx": 50905.678327433096, + "dy": 8414.490373798908, + "rotation": 0.0, + "source": "auto", + "anchor": "20 exakte Treffer (<= 50 mm) ueber die Rollen Scanner:4, Separator:31; 20 Stimmen im 100-mm-Raster, 6 Kandidaten geprueft", + "matched": 34, + "candidates": 35, + "residual_max": 452.04557170439375, + "residual_mean": 102.6006793267277, + "csv_file": "mubea.csv", + "dxf_file": "500573_60_1.dxf", + "created": "2026-07-30 15:32" + }, + "mubea.csv|Mubea.dxf": { + "dx": 50905.67832743315, + "dy": 8454.490373798795, + "rotation": 0.0, + "source": "auto", + "anchor": "14 exakte Treffer (<= 50 mm) ueber die Rollen Scanner:4, Separator:31; 14 Stimmen im 100-mm-Raster, 6 Kandidaten geprueft; ACHTUNG mehrdeutig - Kandidat 2 hat ebenfalls 14 exakte Treffer (Restfehler 0.0 mm)", + "matched": 27, + "candidates": 35, + "residual_max": 544.8224945439861, + "residual_mean": 122.81360370388875, + "csv_file": "mubea.csv", + "dxf_file": "Mubea.dxf", + "created": "2026-07-30 15:38" + } +} diff --git a/lib/tro_annotate.py b/lib/tro_annotate.py new file mode 100644 index 0000000..ea37539 --- /dev/null +++ b/lib/tro_annotate.py @@ -0,0 +1,560 @@ +# -*- coding: utf-8 -*- +# Absichtlich ohne Shebang: der Windows-py-Launcher wuerde daraus "python3" +# ableiten und scheitert, wenn keine PythonCore-Installation registriert ist. +""" +tro_annotate.py - TRO-Informationen in die BricsCAD-Zeichnung einblenden. + +Nimmt die Originalzeichnung (DXF), den CSV-Export und die daraus ermittelten +TROs und schreibt eine **Kopie** der Zeichnung mit zusaetzlicher Beschriftung: + + * je TRO ein farbiger Marker als Block mit Attributen + * TRO-Kennung, TRO-Typ, optional der Siemens-Funktionsbaustein + * optional Pfeile fuer den gerichteten Materialfluss zwischen den TROs + * optional eine Legende + +Die Originalzeichnung wird nie veraendert. Alle Ergaenzungen liegen auf eigenen +Layern (Vorgabe: Praefix "TRO_") und tragen XDATA der Anwendungskennung +SPS_SKEL_TRO. Ein erneuter Lauf entfernt genau diese Objekte und baut sie neu - +gelaeuscht wird nach XDATA, nicht nach Layername, damit eigene Objekte auf +denselben Layern unangetastet bleiben. + +Marker als Block mit Attributen +------------------------------- +Je TRO-Typ wird eine Blockdefinition angelegt (TRO_SYM_) und an der +Anlagenkoordinate eingefuegt. Die Daten stehen als Attribute am Block: + + TRO_ID TRO03 + TRO_TYPE 1Sep + FB_BLOCK FB_ILS_MTRO_1Sep + ITEMS 1x Separator + CONFIDENCE mittel + +Damit sind sie in BricsCAD auswertbar und aenderbar - und ein korrigiertes +TRO_ID kann spaeter zurueck in die Generierung laufen. + +Lagebezug +--------- +CSV und Zeichnung stehen in verschiedenen Koordinatensystemen. Der Versatz wird +von lib/dxf_registration.py ermittelt, nachgewiesen und in SKEL_CFG abgelegt; +spaetere Laeufe verwenden den abgelegten Wert. --re-register erzwingt eine neue +Ermittlung, --offset setzt ihn von Hand. + +Ausgabe (in SKEL_RESULTS) +------------------------- + _annotated.dxf Kopie der Zeichnung mit Beschriftung + _registration.json verwendeter Lagebezug samt Nachweis + +Aufruf ueber bin/tro_annotate.bat bzw. bin/tro_annotate.sh. +Benoetigt ezdxf (siehe requirements.txt). DWG wird nicht unterstuetzt - die +Zeichnung muss als DXF vorliegen. +""" + +from __future__ import annotations + +import argparse +import collections +import csv as csv_module +import json +import math +import sys +from pathlib import Path + +import ezdxf +from ezdxf.enums import TextEntityAlignment + +from dxf_registration import ( + ANCHOR_BLOCKS, + RegistrationError, + Transform, + derive_transform, + load_transform, + save_transform, + verify_transform, +) +from material_flow import build_graph, env_dir, read_elements, resolve_input +from tro_catalog import STYLES, get_tro +from tro_flow import Analysis, analyse_tros + +# --------------------------------------------------------------------------- +# Konstanten +# --------------------------------------------------------------------------- + +APPID = "SPS_SKEL_TRO" +LAYER_PREFIX = "TRO_" +BLOCK_PREFIX = "TRO_SYM_" + +# Zeichnungsgroessen in mm, wenn nicht anders angegeben +DEFAULT_MARKER = 300.0 +DEFAULT_TEXT = 220.0 +# Restfehler, ab dem der Lagebezug abgelehnt wird +DEFAULT_MAX_RESIDUAL = 600.0 + + +def hex_to_rgb(value: str) -> tuple[int, int, int]: + """'#e8d4f0' -> (232, 212, 240)""" + text = value.lstrip("#") + return (int(text[0:2], 16), int(text[2:4], 16), int(text[4:6], 16)) + + +# --------------------------------------------------------------------------- +# Lagebezug +# --------------------------------------------------------------------------- + + +def anchor_points_csv(elements) -> dict[str, list[tuple[float, float]]]: + """Punktartige CSV-Objekte je Rolle (Separator, Scanner).""" + points: dict[str, list[tuple[float, float]]] = collections.defaultdict(list) + for element in elements: + if element.kind in ANCHOR_BLOCKS and element.position: + points[element.kind].append((element.x, element.y)) + return dict(points) + + +def anchor_points_dxf(msp) -> dict[str, list[tuple[float, float]]]: + """Einfuegepunkte der Anker-Bloecke je Rolle.""" + wanted = {name: role for role, names in ANCHOR_BLOCKS.items() for name in names} + points: dict[str, list[tuple[float, float]]] = collections.defaultdict(list) + for ref in msp.query("INSERT"): + role = wanted.get(ref.dxf.name) + if role: + points[role].append((ref.dxf.insert.x, ref.dxf.insert.y)) + return dict(points) + + +def resolve_transform( + args, elements, msp, cfg_dir: Path, csv_name: str, dxf_name: str, + warnings: list[str], +) -> Transform: + """Lagebezug bestimmen: Kommandozeile, Konfiguration oder Ermittlung.""" + csv_points = anchor_points_csv(elements) + dxf_points = anchor_points_dxf(msp) + + if args.offset: + try: + dx, dy = (float(v) for v in args.offset.split(",")) + except ValueError: + raise RegistrationError( + f"--offset '{args.offset}' nicht lesbar, erwartet 'dx,dy'" + ) from None + transform = Transform(dx=dx, dy=dy, source="cli", csv_file=csv_name, + dxf_file=dxf_name, anchor="von Hand gesetzt") + verify_transform(transform, csv_points, dxf_points) + return transform + + if not args.re_register: + stored = load_transform(cfg_dir, csv_name, dxf_name) + if stored: + # Auch der abgelegte Bezug wird gegen die Zeichnung nachgerechnet + verify_transform(stored, csv_points, dxf_points) + return stored + + transform = derive_transform(csv_points, dxf_points, csv_name, dxf_name) + saved = save_transform(cfg_dir, transform) + warnings.append(f"Lagebezug neu ermittelt und abgelegt: {saved}") + return transform + + +# --------------------------------------------------------------------------- +# Zeichnung vorbereiten +# --------------------------------------------------------------------------- + + +def ensure_appid(doc) -> None: + if APPID not in doc.appids: + doc.appids.add(APPID) + + +def remove_previous(msp) -> int: + """Objekte frueherer Laeufe entfernen - erkannt an ihren XDATA.""" + doomed = [] + for entity in msp: + try: + if entity.get_xdata(APPID): + doomed.append(entity) + except (ezdxf.DXFValueError, ezdxf.DXFTypeError): + continue + for entity in doomed: + msp.delete_entity(entity) + return len(doomed) + + +def tag(entity, tro_id: str = "") -> None: + """Objekt als von diesem Programm erzeugt kennzeichnen.""" + entity.set_xdata(APPID, [(1000, "tro_annotate"), (1000, tro_id or "-")]) + + +def build_layers(doc, prefix: str) -> dict[str, str]: + """Layer je Farbgruppe plus Label-, Fluss- und Legendenlayer.""" + names: dict[str, str] = {} + for style in STYLES: + name = f"{prefix}MARKER_{style.group}" + names[style.group] = name + if name not in doc.layers: + doc.layers.add(name, dxfattribs={ + "true_color": ezdxf.rgb2int(hex_to_rgb(style.fill)), + }) + for key, colour in (("label", "#404040"), ("flow", "#2f5597"), + ("legend", "#808080")): + name = f"{prefix}{key.upper()}" + names[key] = name + if name not in doc.layers: + doc.layers.add(name, dxfattribs={ + "true_color": ezdxf.rgb2int(hex_to_rgb(colour)), + }) + return names + + +def build_symbol(doc, type_name: str, marker: float, text: float, show_fb: bool) -> str: + """Blockdefinition fuer einen TRO-Typ anlegen (Marker + Attributdefinitionen).""" + name = f"{BLOCK_PREFIX}{type_name}" + if name in doc.blocks: + return name + blk = doc.blocks.new(name=name) + + # Marker: Ring mit Mittelpunkt, Farbe kommt vom Layer des INSERT + blk.add_circle((0, 0), marker, dxfattribs={"color": 256}) + blk.add_circle((0, 0), marker * 0.18, dxfattribs={"color": 256}) + blk.add_line((-marker, 0), (marker, 0), dxfattribs={"color": 256}) + blk.add_line((0, -marker), (0, marker), dxfattribs={"color": 256}) + + gap = marker * 1.35 + rows = [ + ("TRO_ID", type_name and "TRO??", text * 1.35, 0), + ("TRO_TYPE", type_name, text, 0), + ] + if show_fb: + rows.append(("FB_BLOCK", "FB", text * 0.85, 0)) + + offset = 0.0 + for tag_name, default, height, _ in rows: + blk.add_attdef( + tag_name, + insert=(gap, offset), + height=height, + dxfattribs={"style": "Standard", "color": 256, "text": default}, + ) + offset -= height * 1.55 + + # nicht sichtbare Attribute: reine Daten + for tag_name in ("ITEMS", "CONFIDENCE", "SEPARATORS"): + blk.add_attdef( + tag_name, + insert=(gap, offset), + height=text * 0.7, + dxfattribs={"style": "Standard", "color": 256, "text": "", + "invisible": 1}, + ) + offset -= text + return name + + +# --------------------------------------------------------------------------- +# Beschriftung zeichnen +# --------------------------------------------------------------------------- + + +def place_markers( + msp, doc, analysis: Analysis, transform: Transform, layers: dict[str, str], + marker: float, text: float, show_fb: bool, warnings: list[str], +) -> dict[str, tuple[float, float]]: + """Je TRO einen Blockmarker mit Attributen einfuegen.""" + placed: dict[str, tuple[float, float]] = {} + for tro in analysis.tros: + if not tro.position: + warnings.append(f"{tro.tro_id}: keine Koordinate - nicht beschriftet") + continue + x, y = transform.to_dxf(tro.position[0], tro.position[1]) + placed[tro.tro_id] = (x, y) + + definition = get_tro(tro.type_name) + group = definition.get_group() if definition else "ext" + block = build_symbol(doc, tro.type_name, marker, text, show_fb) + + values = { + "TRO_ID": tro.tro_id, + "TRO_TYPE": tro.type_name, + "ITEMS": ", ".join(f"{c}x {i}" for i, c in sorted(tro.items.items())), + "CONFIDENCE": tro.confidence, + "SEPARATORS": ", ".join(tro.separators), + } + if show_fb: + values["FB_BLOCK"] = tro.fb_block or "-" + + ref = msp.add_blockref(block, (x, y), dxfattribs={ + "layer": layers.get(group, layers["ext"]), + }) + ref.add_auto_attribs(values) + tag(ref, tro.tro_id) + return placed + + +def draw_flow( + msp, analysis: Analysis, placed: dict[str, tuple[float, float]], + layers: dict[str, str], marker: float, +) -> int: + """Pfeile fuer den gerichteten Materialfluss zwischen den TROs.""" + head = marker * 0.9 + drawn = 0 + for src, dst in analysis.edges: + if src not in placed or dst not in placed: + continue + x1, y1 = placed[src] + x2, y2 = placed[dst] + length = math.dist((x1, y1), (x2, y2)) + if length <= 2 * marker * 1.1: + continue + ux, uy = (x2 - x1) / length, (y2 - y1) / length + # An beiden Enden um den Marker kuerzen, damit der Pfeil ihn nicht deckt + sx, sy = x1 + ux * marker * 1.1, y1 + uy * marker * 1.1 + ex, ey = x2 - ux * marker * 1.1, y2 - uy * marker * 1.1 + shaft = msp.add_lwpolyline([(sx, sy), (ex, ey)], + dxfattribs={"layer": layers["flow"]}) + tag(shaft, f"{src}->{dst}") + # Pfeilspitze als gefuellte Flaeche + bx, by = ex - ux * head, ey - uy * head + px, py = -uy * head * 0.38, ux * head * 0.38 + point = msp.add_solid( + [(ex, ey), (bx + px, by + py), (bx - px, by - py)], + dxfattribs={"layer": layers["flow"]}, + ) + tag(point, f"{src}->{dst}") + drawn += 1 + return drawn + + +def draw_legend( + msp, analysis: Analysis, placed: dict[str, tuple[float, float]], + layers: dict[str, str], marker: float, text: float, source: str, +) -> None: + """Legende links unterhalb der beschrifteten Objekte.""" + if not placed: + return + x0 = min(p[0] for p in placed.values()) - marker * 12 + y0 = min(p[1] for p in placed.values()) - marker * 6 + row = text * 2.2 + + title = msp.add_text( + f"TRO-Beschriftung - {source}", + height=text * 1.5, + dxfattribs={"layer": layers["legend"], "style": "Standard"}, + ) + title.set_placement((x0, y0 + row)) + tag(title) + + used = {t.type_name for t in analysis.tros} + groups = [] + for style in STYLES: + types = sorted(n for n in used + if (get_tro(n).get_group() if get_tro(n) else "ext") == style.group) + if types: + groups.append((style, types)) + + for index, (style, types) in enumerate(groups): + y = y0 - index * row + box = msp.add_solid( + [(x0, y), (x0 + text, y), (x0 + text, y - text), (x0, y - text)], + dxfattribs={"layer": layers[style.group]}, + ) + tag(box) + label = msp.add_text( + f"{style.label}: {', '.join(types)}", + height=text, + dxfattribs={"layer": layers["legend"], "style": "Standard"}, + ) + label.set_placement((x0 + text * 1.8, y - text)) + tag(label) + + +# --------------------------------------------------------------------------- +# Bericht +# --------------------------------------------------------------------------- + + +def report( + analysis: Analysis, transform: Transform, csv_file: Path, dxf_file: Path, + out_file: Path | None, reg_file: Path | None, counts: dict, warnings: list[str], +) -> None: + print("") + print("================================================================") + print("TRO-BESCHRIFTUNG") + print("================================================================") + print(f"CSV = {csv_file}") + print(f"Zeichnung = {dxf_file}") + print(f"Lagebezug = {transform.describe()}") + print(f" Herkunft = {transform.source}" + + (f", ermittelt {transform.created}" if transform.created else "")) + print(f" Nachweis = {transform.anchor}") + print(f" Restfehler = max {transform.residual_max:.1f} mm, " + f"mittel {transform.residual_mean:.1f} mm " + f"({transform.matched}/{transform.candidates} Punkte)") + print(f"TROs = {len(analysis.tros)}, beschriftet: {counts['marker']}") + print(f"Flusspfeile = {counts['flow']}") + print(f"Entfernte Objekte= {counts['removed']} (aus frueherem Lauf)") + print(f"Layer = {counts['layers']}, Bloecke: {counts['blocks']}") + print(f"DXF = {out_file if out_file else '- (--check)'}") + print(f"Lagebezug-Datei = {reg_file if reg_file else '-'}") + print("================================================================") + if warnings: + print("") + print(f"Hinweise ({len(warnings)}):") + for warning in warnings: + print(f" ! {warning}") + print("") + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + prog="tro_annotate", + description="Blendet die ermittelten TROs in eine Kopie der " + "BricsCAD-Zeichnung ein (DXF).", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="Beispiele:\n" + " tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --check\n" + " tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --flow --fb\n" + "\n" + "Exit-Codes:\n" + " 0 ok\n" + " 1 Eingabe- oder Aufruffehler\n" + " 4 Lagebezug nicht nachweisbar\n", + ) + parser.add_argument("--file", default="export.csv", metavar="NAME", + help="CSV-Export in %%SKEL_DATA%% oder ein Pfad. " + "Standard: %(default)s") + parser.add_argument("--dxf", required=True, metavar="NAME", + help="Originalzeichnung als DXF (Pfad oder Name in " + "%%SKEL_DATA%%).") + parser.add_argument("--out", metavar="NAME", + help="Name der Ausgabedatei in %%SKEL_RESULTS%%. " + "Standard: _annotated.dxf") + parser.add_argument("--flow", action="store_true", + help="Pfeile fuer den Materialfluss zwischen den TROs " + "zeichnen.") + parser.add_argument("--fb", action="store_true", + help="Den Siemens-Funktionsbaustein als sichtbares " + "Attribut am Marker anzeigen.") + parser.add_argument("--legend", action="store_true", + help="Legende der Farbgruppen einzeichnen.") + parser.add_argument("--marker-size", type=float, default=DEFAULT_MARKER, + metavar="MM", help="Markerradius in mm. Standard: %(default)s") + parser.add_argument("--text-height", type=float, default=DEFAULT_TEXT, + metavar="MM", help="Texthoehe in mm. Standard: %(default)s") + parser.add_argument("--layer-prefix", default=LAYER_PREFIX, metavar="P", + help="Praefix der erzeugten Layer. Standard: %(default)s") + parser.add_argument("--offset", metavar="DX,DY", + help="Lagebezug von Hand setzen statt ermitteln.") + parser.add_argument("--re-register", action="store_true", + help="Lagebezug neu ermitteln, auch wenn in %%SKEL_CFG%% " + "einer abgelegt ist.") + parser.add_argument("--max-residual", type=float, default=DEFAULT_MAX_RESIDUAL, + metavar="MM", + help="Groesster zugelassener Restfehler des Lagebezugs. " + "Standard: %(default)s") + parser.add_argument("--check", action="store_true", + help="Nur pruefen und berichten, keine Datei schreiben.") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + warnings: list[str] = [] + + csv_file = resolve_input(args.file) + if not csv_file.is_file(): + print(f"FEHLER: CSV nicht gefunden: {csv_file}", file=sys.stderr) + return 1 + + dxf_file = Path(args.dxf) + if not dxf_file.is_absolute() and dxf_file.parent == Path("."): + dxf_file = env_dir("SKEL_DATA", "data") / args.dxf + if not dxf_file.is_file(): + print(f"FEHLER: Zeichnung nicht gefunden: {dxf_file}", file=sys.stderr) + return 1 + if dxf_file.suffix.lower() == ".dwg": + print("FEHLER: DWG wird nicht unterstuetzt - bitte als DXF exportieren.", + file=sys.stderr) + return 1 + + results = env_dir("SKEL_RESULTS", "results") + cfg_dir = env_dir("SKEL_CFG", "cfg") + results.mkdir(parents=True, exist_ok=True) + + # TROs aus dem CSV + read_warnings: list[str] = [] + try: + elements = read_elements(csv_file, read_warnings) + except (OSError, ValueError, csv_module.Error) as exc: + print(f"FEHLER: {csv_file} nicht lesbar: {exc}", file=sys.stderr) + return 1 + graph = build_graph(elements, csv_file.name, read_warnings) + analysis = analyse_tros(graph, elements) + if not analysis.tros: + print(f"FEHLER: keine TROs aus {csv_file} ableitbar.", file=sys.stderr) + return 1 + + # Zeichnung + try: + doc = ezdxf.readfile(str(dxf_file)) + except (OSError, ezdxf.DXFError) as exc: + print(f"FEHLER: {dxf_file} nicht lesbar: {exc}", file=sys.stderr) + return 1 + msp = doc.modelspace() + + # Lagebezug + try: + transform = resolve_transform(args, elements, msp, cfg_dir, + csv_file.name, dxf_file.name, warnings) + except RegistrationError as exc: + print(f"FEHLER: Lagebezug nicht bestimmbar: {exc}", file=sys.stderr) + return 4 + if transform.residual_max > args.max_residual: + print(f"FEHLER: Restfehler des Lagebezugs {transform.residual_max:.1f} mm " + f"ueber der Grenze von {args.max_residual:.1f} mm.", file=sys.stderr) + print(f" {transform.describe()}", file=sys.stderr) + print(" Mit --offset von Hand setzen oder --max-residual anheben.", + file=sys.stderr) + return 4 + + counts = {"marker": 0, "flow": 0, "removed": 0, "layers": 0, "blocks": 0} + + if args.check: + counts["marker"] = sum(1 for t in analysis.tros if t.position) + report(analysis, transform, csv_file, dxf_file, None, None, counts, warnings) + return 0 + + ensure_appid(doc) + counts["removed"] = remove_previous(msp) + layers = build_layers(doc, args.layer_prefix) + counts["layers"] = len({v for v in layers.values()}) + + placed = place_markers(msp, doc, analysis, transform, layers, + args.marker_size, args.text_height, args.fb, warnings) + counts["marker"] = len(placed) + counts["blocks"] = sum(1 for b in doc.blocks if b.name.startswith(BLOCK_PREFIX)) + + if args.flow: + counts["flow"] = draw_flow(msp, analysis, placed, layers, args.marker_size) + if args.legend: + draw_legend(msp, analysis, placed, layers, args.marker_size, + args.text_height, csv_file.name) + + out_file = results / (args.out or f"{csv_file.stem}_annotated.dxf") + doc.saveas(str(out_file)) + + reg_file = results / f"{csv_file.stem}_registration.json" + reg_file.write_text( + json.dumps(transform.model_dump(), indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + + report(analysis, transform, csv_file, dxf_file, out_file, reg_file, + counts, warnings) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/requirements.txt b/requirements.txt index 1f402e9..b014d57 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,11 @@ pydantic >= 2.0.0 +# DXF lesen und schreiben (tro_annotate). DWG wird nicht unterstuetzt. +ezdxf >= 1.4.0 + +# Nur fuer Vorschaubilder eines DXF (ezdxf.addons.drawing) - optional: +# matplotlib >= 3.8 + # Noch nicht benoetigt: # pytest >= 9.0.0