Files
sps_skel/lib/tro_annotate.py
T
s.ayadi 0f02fd83aa Give each TRO type its own CAD symbol and add a TROEDIT dialog
Replaces the generic marker with one shape per type, so the types are
distinguishable in the drawing without relying on colour. The shape belongs to
the type, so it sits in tro_catalog next to the colour - colour groups (6 groups),
shape identifies (10 types):

  1Sep circle          1Sep1Swi triangle      1Sep2Swi pentagon
  1Sep_SSCC 2 circles  Vario rectangle        Vario_workStation hexagon
  PinStore_Auto square EmptyCarrBuffer diamond
  LoadingBoom arrow    2Sep1Swi triangle down

The visible attributes are now ID and TYPE, as those are what the dialog edits;
FB_BLOCK stays visible under --fb and ITEMS/CONFIDENCE/SEPARATORS stay hidden
data. Note this renames the former TRO_ID/TRO_TYPE tags, so drawings annotated
with an earlier version need regenerating.

Marker layers now take the *stroke* colour instead of the fill. A CAD symbol is
line work, and the pastel fills of the palette were nearly invisible as lines -
LoadingBoom in particular came out almost white on white.

cad/tro_edit.dcl and cad/tro_edit.lsp add the TROEDIT command: pick a TRO_SYM_*
block, edit ID and TYPE, write back on OK. TYPE is a picklist rather than free
text so it cannot drift from the catalogue; the list comes from cad/tro_types.lsp,
which "tro_annotate.py --emit-lisp" generates from TRO_CATALOG. If that file is
missing the dialog degrades to the block's current type instead of failing.

The dialog deliberately changes attributes only. The marker shape belongs to the
block definition of the type and FB_BLOCK is derived from it, so both follow on
the next annotation run - the dialog says so, and TROEDIT prints a reminder when
the type was changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:32:48 +02:00

653 lines
25 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 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 emit_lisp(cad_dir: Path) -> Path:
"""
Typliste fuer den DCL-Dialog schreiben.
Der Dialog TROEDIT 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(Path(__file__).resolve().parent.parent / "cad")
print(f"Typliste geschrieben: {path} ({len(TRO_CATALOG)} Typen)")
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())