Files
sps_skel/lib/tro_annotate.py
T
s.ayadi ecc9c8fb53 Move the CAD dialog to dxfmakros and target DXFM_LISP
The TRO edit dialog belongs with the other SSG_LIB edit commands, so
cad/tro_edit.lsp and cad/tro_edit.dcl move to dxfmakros (Lisp/Tro_Edit.lsp,
dcl/tro_edit.dcl) where they use the ssg_* helpers, the DXFM_DCL path and the
existing SSG_BLOCKEDIT dispatcher instead of standing on their own. Keeping a
second copy here would only let the two drift apart before the repos are merged.

--emit-lisp now writes the generated type list to DXFM_LISP when that variable is
set, so it lands next to the SSG_LIB modules that read it. Without the variable it
still writes to cad/ and says where the file actually belongs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 12:06:55 +02:00

671 lines
26 KiB
Python

# -*- 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_<Typ>) und an der
Anlagenkoordinate eingefuegt. **Jeder Typ hat seine eigene Form** (Kreis,
Dreieck, Rechteck, Raute ...; siehe TroSymbol in tro_catalog), sodass sich die
Typen auch ohne Farbe unterscheiden. Die Daten stehen als Attribute am Block:
ID TRO03 sichtbar, im Dialog bearbeitbar
TYPE 1Sep sichtbar, im Dialog bearbeitbar
FB_BLOCK FB_ILS_... sichtbar nur mit --fb
ITEMS 1x Separator unsichtbar, reine Daten
CONFIDENCE mittel unsichtbar
SEPARATORS 0010 unsichtbar
Damit sind sie in BricsCAD auswertbar und aenderbar - und ein korrigiertes ID
kann spaeter zurueck in die Generierung laufen. Zum Bearbeiten gibt es den
Dialog cad/tro_edit.dcl mit dem Befehl TROEDIT (cad/tro_edit.lsp); die Liste der
gueltigen Typen dafuer schreibt --emit-lisp aus dem Katalog.
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)
-------------------------
<name>_annotated.dxf Kopie der Zeichnung mit Beschriftung
<name>_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 os
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, TRO_CATALOG, TroSymbol, 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))
def lisp_dir() -> Path:
"""
Zielordner fuer die erzeugte LISP-Typliste.
Bevorzugt DXFM_LISP - dort liegen die SSG_LIB-Module in dxfmakros, die den
Dialog TRO_EDIT bereitstellen. Ohne die Variable landet die Datei in
<SPS_SKEL>/cad, damit der Lauf auch ohne dxfmakros funktioniert.
"""
env = os.environ.get("DXFM_LISP")
if env:
return Path(env)
return Path(__file__).resolve().parent.parent / "cad"
def emit_lisp(cad_dir: Path) -> Path:
"""
Typliste fuer den DCL-Dialog schreiben.
Der Dialog TRO_EDIT (dxfmakros/Lisp/Tro_Edit.lsp) bietet TYPE als
Auswahlliste an. Damit sie nicht von der Typdefinition abweicht, wird sie
hier aus TRO_CATALOG erzeugt statt in der LISP-Datei gepflegt.
"""
cad_dir.mkdir(parents=True, exist_ok=True)
path = cad_dir / "tro_types.lsp"
types = " ".join(f'"{d.get_name()}"' for d in TRO_CATALOG)
path.write_text(
";; Automatisch erzeugt von lib/tro_annotate.py --emit-lisp\n"
";; Quelle: TRO_CATALOG in lib/tro_catalog.py - nicht manuell aendern.\n"
f"(setq *TRO-TYPES* (list {types}))\n"
"(princ)\n",
encoding="utf-8",
)
return path
# ---------------------------------------------------------------------------
# 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:
# Strichfarbe, nicht Fuellfarbe: im CAD ist das Symbol eine Linie.
# Die pastelligen Fuellfarben waeren als Linien kaum zu sehen.
doc.layers.add(name, dxfattribs={
"true_color": ezdxf.rgb2int(hex_to_rgb(style.stroke)),
})
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 _polygon(sides: int, radius: float, start_deg: float) -> list[tuple[float, float]]:
"""Eckpunkte eines regelmaessigen Vielecks um den Ursprung."""
return [
(radius * math.cos(math.radians(start_deg + i * 360 / sides)),
radius * math.sin(math.radians(start_deg + i * 360 / sides)))
for i in range(sides)
]
def draw_shape(blk, symbol: str, r: float) -> None:
"""
Symbolgeometrie in eine Blockdefinition zeichnen.
Farbe 256 = BYLAYER, damit die Farbe des INSERT-Layers (Farbgruppe des Typs)
durchschlaegt. Alle Formen liegen um den Ursprung, damit der Einfuegepunkt
des Blocks die Anlagenkoordinate ist.
"""
attr = {"color": 256}
if symbol == TroSymbol.CIRCLE:
blk.add_circle((0, 0), r, dxfattribs=attr)
elif symbol == TroSymbol.DOUBLE_CIRCLE:
blk.add_circle((0, 0), r, dxfattribs=attr)
blk.add_circle((0, 0), r * 0.6, dxfattribs=attr)
elif symbol == TroSymbol.TRIANGLE:
blk.add_lwpolyline(_polygon(3, r, 90), close=True, dxfattribs=attr)
elif symbol == TroSymbol.TRIANGLE_DOWN:
blk.add_lwpolyline(_polygon(3, r, 270), close=True, dxfattribs=attr)
elif symbol == TroSymbol.SQUARE:
blk.add_lwpolyline(_polygon(4, r, 45), close=True, dxfattribs=attr)
elif symbol == TroSymbol.DIAMOND:
blk.add_lwpolyline(_polygon(4, r, 90), close=True, dxfattribs=attr)
elif symbol == TroSymbol.PENTAGON:
blk.add_lwpolyline(_polygon(5, r, 90), close=True, dxfattribs=attr)
elif symbol == TroSymbol.HEXAGON:
blk.add_lwpolyline(_polygon(6, r, 0), close=True, dxfattribs=attr)
elif symbol == TroSymbol.RECT:
w, h = r * 1.45, r * 0.72
blk.add_lwpolyline([(-w, -h), (w, -h), (w, h), (-w, h)],
close=True, dxfattribs=attr)
elif symbol == TroSymbol.ARROW:
blk.add_lwpolyline(
[(-r * 0.55, -r), (r, 0), (-r * 0.55, r), (-r * 0.55, r * 0.35),
(-r, r * 0.35), (-r, -r * 0.35), (-r * 0.55, -r * 0.35)],
close=True, dxfattribs=attr)
else:
# unbekanntes Symbol: Kreis mit Kreuz, faellt auf
blk.add_circle((0, 0), r, dxfattribs=attr)
blk.add_line((-r, -r), (r, r), dxfattribs=attr)
blk.add_line((-r, r), (r, -r), dxfattribs=attr)
def build_symbol(doc, type_name: str, marker: float, text: float, show_fb: bool) -> str:
"""
Blockdefinition fuer einen TRO-Typ anlegen: Symbol + Attributdefinitionen.
Je Typ eine eigene Form (siehe TroSymbol), damit sich die Typen in der
Zeichnung auch ohne Farbe unterscheiden. Die Attribute ID und TYPE sind
sichtbar und werden vom DCL-Dialog TROEDIT bearbeitet.
"""
name = f"{BLOCK_PREFIX}{type_name}"
if name in doc.blocks:
return name
blk = doc.blocks.new(name=name)
definition = get_tro(type_name)
draw_shape(blk, definition.get_symbol() if definition else "", marker)
gap = marker * 1.5
rows = [("ID", "TRO??", text * 1.35), ("TYPE", type_name, text)]
if show_fb:
rows.append(("FB_BLOCK", "FB", text * 0.85))
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, nicht im Dialog
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 = {
"ID": tro.tro_id,
"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", metavar="NAME",
help="Originalzeichnung als DXF (Pfad oder Name in "
"%%SKEL_DATA%%). Ausser bei --emit-lisp erforderlich.")
parser.add_argument("--out", metavar="NAME",
help="Name der Ausgabedatei in %%SKEL_RESULTS%%. "
"Standard: <csv>_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.")
parser.add_argument("--emit-lisp", action="store_true",
help="cad/tro_types.lsp aus dem Typkatalog neu schreiben "
"(Auswahlliste des Dialogs TROEDIT) und beenden.")
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
warnings: list[str] = []
if args.emit_lisp:
path = emit_lisp(lisp_dir())
print(f"Typliste geschrieben: {path} ({len(TRO_CATALOG)} Typen)")
if not os.environ.get("DXFM_LISP"):
print("HINWEIS: DXFM_LISP nicht gesetzt - die Datei gehoert neben die "
"SSG_LIB-Module (dxfmakros/Lisp), damit TRO_EDIT sie findet.")
return 0
if not args.dxf:
print("FEHLER: --dxf fehlt (nur --emit-lisp geht ohne Zeichnung).",
file=sys.stderr)
return 1
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())