0f02fd83aa
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>
653 lines
25 KiB
Python
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())
|