TRO-Typen an Kreisel-Beruehrpunkten (lib/kreisel_junctions.py, derive_tros Schritt 3c): jeder Kreisel als Stadion-Form mit Laufrichtung; jeder gleichlaufende Beruehrpunkt ist eine Weiche (exportierte ILS Weiche oder abgeleitet W<a>_<b>), Beruehrpunkte derselben Umlenkung bilden einen Knoten. Ein Separator steuert ihn, wenn er in Laufrichtung unmittelbar davor liegt (keine andere Weiche dazwischen). Typ = Separatoren x Weichen. export.csv: 1Sep1Swi (9/1, 9/8, 8/7), 2Sep1Swi (7/2), 2Sep2Swi (2/3/4), 3Sep2Swi (4/5/6). Neuer Katalogtyp 3Sep2Swi (nicht aus SCL belegt). Gegenlaeufige Beruehrpunkte (Mubea, Dematic) bleiben bei der alten Regel; Mubea/HundM05/Dematic unveraendert, Showroom 0018+0019 jetzt 2Sep1Swi. material_flow --track: Mouseover je Knoten mit Zugehoerigkeit zu Kreisel/VF/GF (Zuordnung, TrackIds-Kette, Lage laut Geometrie), Weiche mit beiden Kreiseln, Be-/Entladung mit ihrem Kreisel; Widersprueche mit "!". Zahlenwerte (Toleranzen, Schwellen, Massstaebe, Groessen) aus allen lib-Modulen nach cfg/parameter.ini (lib/parameter.py, Abschnitt je Modul, Ueberschreiben je Anlage ueber %SKEL_CFG%/parameter.ini). Ausgaben aller Anlagen bytegleich. tro_flow.py: Docstrings mit Parametern/Rueckgabe, fehlende Typangaben. Geprueft: 90 Tests gruen; tro_flow/material_flow/scl_skeleton auf 5 CSVs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1009 lines
41 KiB
Python
1009 lines
41 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 Zeichnung ist eine **Ansicht** zur Sichtkontrolle, keine Datenquelle: die
|
|
TRO-JSON fuer scl_skeleton.py schreibt tro_flow.py --json direkt aus dem CSV.
|
|
tro_annotate leitet die TROs mit genau demselben Aufruf ab wie tro_flow, damit
|
|
Zeichnung und JSON uebereinstimmen. (tro_extract.py, der Rueckweg Zeichnung ->
|
|
JSON, ist geparkt.)
|
|
|
|
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
|
|
UID 0062 unsichtbar, dauerhafte interne Nummer
|
|
ITEMS 1x Separator unsichtbar, reine Daten
|
|
CONFIDENCE mittel unsichtbar
|
|
SEPARATORS 0010 unsichtbar
|
|
|
|
Zwei Kennungen, mit Absicht:
|
|
ID die sprechende Kennung (TRO03). Darf sich aendern - der Dialog TRO_EDIT
|
|
und TRO_INSERT in dxfmakros arbeiten damit.
|
|
UID die eindeutige, dauerhafte interne Nummer. Vierstellig wie die TeileId
|
|
des CSV-Exports und wie das ID-Attribut von SSG_LIB
|
|
(dxfmakros/Lisp/ssg_id.lsp), damit sie im selben Nummernraum liegt und
|
|
nicht mit bestehenden Nummern kollidiert; vergeben wird oberhalb der
|
|
hoechsten schon belegten Nummer aus Zeichnung *und* CSV.
|
|
Bewusst ein eigenes Tag und nicht ID: in dieser Zeichnung steht in ID
|
|
schon die sprechende Kennung, und IDSCHECK wuerde sie beim Pruefen auf
|
|
Dubletten ueberschreiben.
|
|
|
|
Verbindungen
|
|
------------
|
|
Mit --flow wird je Kante zusaetzlich ein Block CONNECTION_ARROW in die Mitte des
|
|
Pfeils gesetzt. Er haelt die Beziehung ueber die **UIDs** fest (FROM_UID/TO_UID,
|
|
dazu FROM_TRO/TO_TRO als Klartext), sodass sie im DXF-Export eindeutig lesbar
|
|
bleibt, auch wenn ein Marker verschoben oder seine sprechende Kennung geaendert
|
|
wird.
|
|
|
|
Damit sind die Daten in BricsCAD auswertbar und aenderbar. Zum Bearbeiten gibt es
|
|
die Dialoge in dxfmakros: TRO_EDIT (dcl/tro_edit.dcl) und CONNECTION_EDIT
|
|
(dcl/connection_edit.dcl); die Liste der gueltigen Typen dafuer schreibt
|
|
--emit-lisp aus dem Katalog nach DXFM_LISP.
|
|
|
|
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 fnmatch
|
|
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 model import Analysis
|
|
from parameter import get_float
|
|
from tro_flow import (
|
|
BOUNDARY_ENTRY,
|
|
BOUNDARY_EXIT,
|
|
STATION_FARBE,
|
|
_station_positionen,
|
|
analyse_tros,
|
|
find_sibling_raw_json,
|
|
load_sepliste_counts,
|
|
resolve_ids_file,
|
|
)
|
|
from tro_ids import TroIds
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Konstanten
|
|
# ---------------------------------------------------------------------------
|
|
|
|
APPID = "SPS_SKEL_TRO"
|
|
LAYER_PREFIX = "TRO_"
|
|
BLOCK_PREFIX = "TRO_SYM_"
|
|
# Verbindungsblock. Der Name beginnt mit CONNECTION_, damit der Dispatcher
|
|
# SSG_BLOCKEDIT in dxfmakros ihn ueber "CONNECTION_*" erkennt.
|
|
CONNECTION_BLOCK = "CONNECTION_ARROW"
|
|
|
|
# Zeichnungsgroessen und Restfehler-Grenze des Lagebezugs aus cfg/parameter.ini,
|
|
# Abschnitt [tro_annotate] (lib/parameter.py)
|
|
DEFAULT_MARKER = get_float("tro_annotate", "default_marker")
|
|
DEFAULT_TEXT = get_float("tro_annotate", "default_text")
|
|
DEFAULT_MAX_RESIDUAL = get_float("tro_annotate", "default_max_residual")
|
|
|
|
|
|
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.
|
|
|
|
ANCHOR_BLOCKS enthaelt Namensmuster, keine festen Namen - die von den
|
|
Makros erzeugten Zeichnungen haengen an jeden Blocknamen ein `_2D`/`_3D`
|
|
an (Separator_SP_3D statt S-SP).
|
|
"""
|
|
points: dict[str, list[tuple[float, float]]] = collections.defaultdict(list)
|
|
for ref in msp.query("INSERT"):
|
|
name = ref.dxf.name.upper()
|
|
for role, patterns in ANCHOR_BLOCKS.items():
|
|
if any(fnmatch.fnmatchcase(name, p.upper()) for p in patterns):
|
|
points[role].append((ref.dxf.insert.x, ref.dxf.insert.y))
|
|
break
|
|
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 format_internal_id(number: int) -> str:
|
|
"""Vierstellige interne Nummer, wie (ssg-id-format n) in dxfmakros."""
|
|
return f"{number:04d}"
|
|
|
|
|
|
def next_internal_id(msp, elements=None) -> int:
|
|
"""
|
|
Naechste freie interne ID.
|
|
|
|
SSG_LIB vergibt jedem eingefuegten Baustein im Attribut ID eine eindeutige
|
|
vierstellige Nummer (Lisp/ssg_id.lsp); dieselbe Nummer erscheint im
|
|
CSV-Export als TeileId. Beides ist **derselbe** ID-Raum.
|
|
|
|
Deshalb wird das Maximum aus zwei Quellen gebildet:
|
|
* ID-Attribute der Bloecke in der Zeichnung
|
|
* TeileId aller Objekte des CSV-Exports
|
|
|
|
Die zweite Quelle ist der wichtigere Fall: eine mechanische Zeichnung, die
|
|
nicht mit SSG_LIB gebaut wurde, hat gar keine ID-Attribute (im Mubea-Plan
|
|
tragen nur 10 von 669 Bloecken ueberhaupt Attribute). Ohne die TeileIds
|
|
wuerde die Beschriftung bei 0001 anfangen und damit Nummern belegen, die im
|
|
CSV schon Bauteile bezeichnen - IDSCHECK saehe spaeter Dubletten.
|
|
"""
|
|
highest = 0
|
|
for ref in msp.query("INSERT"):
|
|
try:
|
|
attribs = ref.attribs
|
|
except AttributeError:
|
|
continue
|
|
for attrib in attribs:
|
|
if attrib.dxf.tag.upper() != "ID":
|
|
continue
|
|
text = (attrib.dxf.text or "").strip()
|
|
if text.isdigit():
|
|
highest = max(highest, int(text))
|
|
|
|
for element in elements or ():
|
|
teile_id = (element.teile_id or "").strip()
|
|
if teile_id.isdigit():
|
|
highest = max(highest, int(teile_id))
|
|
|
|
return highest + 1
|
|
|
|
|
|
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)),
|
|
})
|
|
# Kantenfarben wie im TRO-Fluss-SVG (tro_flow.render_dot): Uebergabe blau,
|
|
# Weiche orange, Materialeingang gruen, Materialausgang rot.
|
|
for key, colour in (("label", "#404040"), ("flow", "#2f5597"),
|
|
("weiche", "#cc8800"),
|
|
("eingang", STATION_FARBE[BOUNDARY_ENTRY]),
|
|
("ausgang", STATION_FARBE[BOUNDARY_EXIT]),
|
|
("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 shape_outline(symbol: str, r: float) -> list[tuple[float, float]]:
|
|
"""Aeussere Kontur eines Symbols als Polygon - Rand der Fuellflaeche
|
|
(Kreise als 48-Eck angenaehert)."""
|
|
if symbol in (TroSymbol.CIRCLE, TroSymbol.DOUBLE_CIRCLE):
|
|
return _polygon(48, r, 0)
|
|
if symbol == TroSymbol.TRIANGLE:
|
|
return _polygon(3, r, 90)
|
|
if symbol in (TroSymbol.TRIANGLE_DOWN, TroSymbol.DOUBLE_TRIANGLE_DOWN):
|
|
return _polygon(3, r, 270)
|
|
if symbol == TroSymbol.SQUARE:
|
|
return _polygon(4, r, 45)
|
|
if symbol == TroSymbol.DIAMOND:
|
|
return _polygon(4, r, 90)
|
|
if symbol == TroSymbol.PENTAGON:
|
|
return _polygon(5, r, 90)
|
|
if symbol == TroSymbol.HEXAGON:
|
|
return _polygon(6, r, 0)
|
|
if symbol == TroSymbol.RECT:
|
|
w, h = r * 1.45, r * 0.72
|
|
return [(-w, -h), (w, -h), (w, h), (-w, h)]
|
|
if symbol == TroSymbol.ARROW:
|
|
return [(-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)]
|
|
return _polygon(48, r, 0)
|
|
|
|
|
|
def draw_fill(blk, symbol: str, r: float, fill_hex: str) -> None:
|
|
"""Typ-eigene Fuellfarbe (wie im TRO-Fluss-SVG) als Solid-Hatch unter der
|
|
Kontur. Feste Farbe statt BYLAYER: der Layer traegt die Randfarbe der
|
|
Gruppe, die Fuellung unterscheidet den einzelnen Typ."""
|
|
hatch = blk.add_hatch(dxfattribs={"true_color": ezdxf.rgb2int(hex_to_rgb(fill_hex))})
|
|
hatch.paths.add_polyline_path(shape_outline(symbol, r), is_closed=True)
|
|
|
|
|
|
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.DOUBLE_TRIANGLE_DOWN:
|
|
blk.add_lwpolyline(_polygon(3, r, 270), close=True, dxfattribs=attr)
|
|
blk.add_lwpolyline(_polygon(3, r * 0.55, 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.
|
|
|
|
Attribute (Tags nach der Konvention von dxfmakros/Lisp/ssg_id.lsp):
|
|
ID unsichtbar - die eindeutige, dauerhafte interne Nummer im
|
|
ID-Raum von SSG_LIB (vierstellig, wie die TeileId im CSV).
|
|
IDSCHECK prueft und korrigiert sie zeichnungsweit.
|
|
TRO_ID sichtbar - der sprechende Name (TRO01), vom Dialog aenderbar
|
|
TYPE sichtbar - TRO-Typ, vom Dialog aenderbar
|
|
"""
|
|
name = f"{BLOCK_PREFIX}{type_name}"
|
|
if name in doc.blocks:
|
|
return name
|
|
blk = doc.blocks.new(name=name)
|
|
|
|
definition = get_tro(type_name)
|
|
symbol = definition.get_symbol() if definition else ""
|
|
if definition:
|
|
draw_fill(blk, symbol, marker, definition.get_fill())
|
|
draw_shape(blk, symbol, 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.
|
|
# ID zuerst - gleiche Reihenfolge wie *strecke-attr-front* in dxfmakros.
|
|
for tag_name in ("UID", "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],
|
|
next_id: int = 1,
|
|
) -> tuple[dict[str, tuple[float, float]], dict[str, str]]:
|
|
"""
|
|
Je TRO einen Blockmarker mit Attributen einfuegen.
|
|
|
|
Rueckgabe: (Position je TRO, interne ID je TRO). Die interne ID wird ab
|
|
`next_id` fortlaufend vergeben, damit sie mit den bereits in der Zeichnung
|
|
vorhandenen IDs nicht kollidiert.
|
|
"""
|
|
placed: dict[str, tuple[float, float]] = {}
|
|
internal: dict[str, str] = {}
|
|
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)
|
|
internal[tro.tro_id] = format_internal_id(next_id)
|
|
next_id += 1
|
|
|
|
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,
|
|
"UID": internal[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, internal
|
|
|
|
|
|
def place_stations(
|
|
msp, analysis: Analysis, placed: dict[str, tuple[float, float]],
|
|
layers: dict[str, str], text: float,
|
|
) -> dict[str, tuple[float, float]]:
|
|
"""Be-/Entladestationen wie im TRO-Fluss-SVG: kein Symbol, nur der Name
|
|
in Gruen (Eingang) bzw. Rot (Ausgang) am Ende des Zu-/Ablaufpfeils.
|
|
Lage wie dort (tro_flow._station_positionen): feste Pfeillaenge ab dem
|
|
angehaengten TRO, Richtung von dessen Kanten weg."""
|
|
positionen = _station_positionen(analysis, placed)
|
|
for station in analysis.boundaries:
|
|
if station.tro_id not in positionen:
|
|
continue
|
|
x, y = positionen[station.tro_id]
|
|
layer = layers["eingang" if station.type_name == BOUNDARY_ENTRY else "ausgang"]
|
|
label = msp.add_text(station.label, height=text,
|
|
dxfattribs={"layer": layer, "style": "Standard"})
|
|
label.set_placement((x, y), align=TextEntityAlignment.MIDDLE_CENTER)
|
|
tag(label, station.tro_id)
|
|
return positionen
|
|
|
|
|
|
def build_connection_symbol(doc, marker: float, text: float) -> str:
|
|
"""
|
|
Blockdefinition fuer eine Verbindung anlegen.
|
|
|
|
Der Block sitzt in der Mitte des Pfeils und traegt die Beziehung als
|
|
Attribute; Schaft und Spitze sind eigene Geometrie auf dem Fluss-Layer.
|
|
Grund fuer diese Trennung: ein Block mit fester Geometrie kann nicht zwei
|
|
beliebige Punkte verbinden - ihn in X zu skalieren wuerde die Pfeilspitze
|
|
verzerren. Der Mittelpunkt-Marker ist anklickbar (Doppelklick -> Dialog)
|
|
und bleibt beim Zoomen gleich gross.
|
|
|
|
Attribute:
|
|
UID unsichtbar - eindeutige interne Nummer, dauerhaft
|
|
FROM_UID unsichtbar - interne UID des Quell-TRO
|
|
TO_UID unsichtbar - interne UID des Ziel-TRO
|
|
FROM_TRO unsichtbar - sprechender Name der Quelle (TRO01)
|
|
TO_TRO unsichtbar - sprechender Name des Ziels (TRO09)
|
|
KIND unsichtbar - flow | weiche | colocated
|
|
"""
|
|
name = CONNECTION_BLOCK
|
|
if name in doc.blocks:
|
|
return name
|
|
blk = doc.blocks.new(name=name)
|
|
|
|
# Kleiner Doppelpfeil-Winkel als Griff, entlang +X ausgerichtet
|
|
size = marker * 0.55
|
|
blk.add_lwpolyline([(-size, size * 0.7), (size, 0), (-size, -size * 0.7)],
|
|
dxfattribs={"color": 256})
|
|
blk.add_circle((0, 0), size * 0.12, dxfattribs={"color": 256})
|
|
|
|
# FROM_TRO/TO_TRO unsichtbar: an jeder Kante sichtbar machten sie die
|
|
# Zeichnung unruhig, im TRO-Fluss-SVG stehen sie auch nicht (Review
|
|
# 2026-10-02). Die Daten bleiben am Block, CONNECTION_EDIT zeigt sie.
|
|
offset = size * 1.4
|
|
for tag_name in ("FROM_TRO", "TO_TRO"):
|
|
blk.add_attdef(
|
|
tag_name,
|
|
insert=(size * 1.3, offset),
|
|
height=text * 0.8,
|
|
dxfattribs={"style": "Standard", "color": 256, "text": "",
|
|
"invisible": 1},
|
|
)
|
|
offset -= text * 1.2
|
|
|
|
for tag_name in ("UID", "FROM_UID", "TO_UID", "KIND"):
|
|
blk.add_attdef(
|
|
tag_name,
|
|
insert=(size * 1.3, offset),
|
|
height=text * 0.7,
|
|
dxfattribs={"style": "Standard", "color": 256, "text": "",
|
|
"invisible": 1},
|
|
)
|
|
offset -= text
|
|
return name
|
|
|
|
|
|
def draw_connections(
|
|
msp, doc, analysis: Analysis, placed: dict[str, tuple[float, float]],
|
|
internal: dict[str, str], layers: dict[str, str], marker: float, text: float,
|
|
next_id: int = 1,
|
|
) -> int:
|
|
"""
|
|
Verbindungen zwischen den TROs zeichnen.
|
|
|
|
Je Verbindung: Schaft und Pfeilspitze als Geometrie plus ein
|
|
CONNECTION_ARROW-Block in der Mitte, der Quelle und Ziel ueber die
|
|
**internen IDs** festhaelt. Damit bleibt die Beziehung im DXF-Export
|
|
lesbar, auch wenn ein Marker verschoben oder umbenannt wird.
|
|
"""
|
|
head = marker * 0.9
|
|
block = build_connection_symbol(doc, marker, text)
|
|
stationen = {b.tro_id for b in analysis.boundaries}
|
|
drawn = 0
|
|
for src, dst in analysis.edges:
|
|
if src not in placed or dst not in placed:
|
|
continue
|
|
# Farbe/Layer wie im TRO-Fluss-SVG
|
|
if src in stationen:
|
|
layer, kind = layers["eingang"], "eingang"
|
|
elif dst in stationen:
|
|
layer, kind = layers["ausgang"], "ausgang"
|
|
elif analysis.edge_kinds.get((src, dst)) == "weiche":
|
|
layer, kind = layers["weiche"], "weiche"
|
|
else:
|
|
layer, kind = layers["flow"], "flow"
|
|
x1, y1 = placed[src]
|
|
x2, y2 = placed[dst]
|
|
length = math.dist((x1, y1), (x2, y2))
|
|
if length < 1e-6:
|
|
continue
|
|
ux, uy = (x2 - x1) / length, (y2 - y1) / length
|
|
# An beiden Enden um den Marker kuerzen, damit der Pfeil ihn nicht
|
|
# deckt; das Stationsende ist nur eine Beschriftung, dort weniger.
|
|
kurz_a = marker * (0.3 if src in stationen else 1.1)
|
|
kurz_b = marker * (0.3 if dst in stationen else 1.1)
|
|
# Zu kurz fuer einen gekuerzten Pfeil (Marker liegen dicht beieinander,
|
|
# z.B. HundM05 TRO03->TRO04 mit 500 mm): NICHT weglassen - sonst fehlt
|
|
# die Verbindung in tro_extract.py und damit in der SCL. Stattdessen
|
|
# Mitte-zu-Mitte ohne Kuerzung zeichnen.
|
|
kurz = length <= kurz_a + kurz_b + head
|
|
if kurz:
|
|
kurz_a = kurz_b = 0.0
|
|
sx, sy = x1 + ux * kurz_a, y1 + uy * kurz_a
|
|
ex, ey = x2 - ux * kurz_b, y2 - uy * kurz_b
|
|
shaft = msp.add_lwpolyline([(sx, sy), (ex, ey)],
|
|
dxfattribs={"layer": layer})
|
|
tag(shaft, f"{src}->{dst}")
|
|
# Pfeilspitze als gefuellte Flaeche (bei kurzer Verbindung in der Mitte,
|
|
# damit sie nicht im Zielmarker verschwindet)
|
|
hx, hy = ((x1 + x2) / 2 + ux * head / 2, (y1 + y2) / 2 + uy * head / 2) \
|
|
if kurz else (ex, ey)
|
|
bx, by = hx - ux * head, hy - uy * head
|
|
px, py = -uy * head * 0.38, ux * head * 0.38
|
|
point = msp.add_solid(
|
|
[(hx, hy), (bx + px, by + py), (bx - px, by - py)],
|
|
dxfattribs={"layer": layer},
|
|
)
|
|
tag(point, f"{src}->{dst}")
|
|
if kind in ("eingang", "ausgang"):
|
|
# Stationspfeile ohne Verbindungsblock: keine TRO-TRO-Beziehung
|
|
drawn += 1
|
|
continue
|
|
|
|
# Traeger der Beziehung: Block in der Pfeilmitte, entlang des Pfeils
|
|
ref = msp.add_blockref(
|
|
block, ((sx + ex) / 2, (sy + ey) / 2),
|
|
dxfattribs={"layer": layers["flow"],
|
|
"rotation": math.degrees(math.atan2(uy, ux))},
|
|
)
|
|
ref.add_auto_attribs({
|
|
"UID": format_internal_id(next_id),
|
|
"FROM_UID": internal.get(src, ""),
|
|
"TO_UID": internal.get(dst, ""),
|
|
"FROM_TRO": src,
|
|
"TO_TRO": dst,
|
|
"KIND": kind,
|
|
})
|
|
tag(ref, f"{src}->{dst}")
|
|
next_id += 1
|
|
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)
|
|
|
|
# Wie die Legende im TRO-Fluss-SVG: je vorkommendem Typ Fuellfarbe, Rand
|
|
# der Gruppe und Form, danach die Kantenfarben.
|
|
used = {t.type_name for t in analysis.tros}
|
|
zeilen: list[tuple[str, str | None, str | None]] = []
|
|
for definition in TRO_CATALOG:
|
|
if definition.get_name() in used:
|
|
zeilen.append((f"{definition.get_name()} - {definition.describe_symbol()}",
|
|
definition.get_fill(),
|
|
layers.get(definition.get_group(), layers["ext"])))
|
|
pfeile = [("eingang", "Materialeingang (Beladung / Eingang)"),
|
|
("ausgang", "Materialausgang (Entladung / Ausgang)"),
|
|
("flow", "Uebergabe / Ein-Ausschleusung"),
|
|
("weiche", "Weiche (Kreisel-Uebergang)")]
|
|
|
|
y = y0
|
|
for beschriftung, fill, rand_layer in zeilen:
|
|
quadrat = [(x0, y), (x0 + text, y), (x0 + text, y - text), (x0, y - text)]
|
|
box = msp.add_hatch(dxfattribs={"layer": layers["legend"],
|
|
"true_color": ezdxf.rgb2int(hex_to_rgb(fill))})
|
|
box.paths.add_polyline_path(quadrat, is_closed=True)
|
|
tag(box)
|
|
rand = msp.add_lwpolyline(quadrat, close=True, dxfattribs={"layer": rand_layer})
|
|
tag(rand)
|
|
label = msp.add_text(beschriftung, height=text,
|
|
dxfattribs={"layer": layers["legend"], "style": "Standard"})
|
|
label.set_placement((x0 + text * 1.8, y - text))
|
|
tag(label)
|
|
y -= row
|
|
for key, beschriftung in pfeile:
|
|
mitte = y - text / 2
|
|
linie = msp.add_lwpolyline([(x0 - text * 0.5, mitte), (x0 + text * 0.9, mitte)],
|
|
dxfattribs={"layer": layers[key]})
|
|
tag(linie)
|
|
spitze = msp.add_solid([(x0 + text * 1.4, mitte), (x0 + text * 0.9, mitte + text * 0.3),
|
|
(x0 + text * 0.9, mitte - text * 0.3)],
|
|
dxfattribs={"layer": layers[key]})
|
|
tag(spitze)
|
|
label = msp.add_text(beschriftung, height=text,
|
|
dxfattribs={"layer": layers[key], "style": "Standard"})
|
|
label.set_placement((x0 + text * 1.8, y - text))
|
|
tag(label)
|
|
y -= row
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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"Verbindungen = {counts['flow']}"
|
|
+ (" (Block CONNECTION_ARROW mit FROM_UID/TO_UID)" if counts["flow"] else ""))
|
|
if counts.get("first_id"):
|
|
print(f"Interne IDs ab = {format_internal_id(counts['first_id'])} "
|
|
f"(ID-Raum von SSG_LIB, siehe IDSCHECK)")
|
|
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("--ids", metavar="DATEI",
|
|
help="ID-Datei der TROs wie bei tro_flow (nur gelesen). "
|
|
"Standard: <csv>_tro_ids.ini in %%SKEL_CFG%%.")
|
|
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("--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 csv_file.suffix.lower() != ".csv":
|
|
print(f"FEHLER: --file erwartet eine CSV-Datei, keine '{csv_file.suffix}'-Datei: "
|
|
f"{csv_file}", file=sys.stderr)
|
|
return 1
|
|
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)
|
|
# Derselbe Aufruf wie in tro_flow.main (ohne cfg/tro_overrides.ini, mit
|
|
# der Separator-Gegenprobe aus <name>_raw.json und der ID-Datei), damit die Zeichnung genau
|
|
# die TROs der TRO-JSON zeigt - sie ist nur Ansicht, keine Datenquelle.
|
|
raw_json_file = find_sibling_raw_json(csv_file)
|
|
sepliste_counts = (load_sepliste_counts(raw_json_file, read_warnings)
|
|
if raw_json_file is not None else {})
|
|
# Dieselbe ID-Datei wie tro_flow, aber nur lesen: neue TROs bekommen
|
|
# dieselbe vorlaeufige ID, die tro_flow anhaengen wuerde.
|
|
ids_file = resolve_ids_file(args.ids, csv_file)
|
|
try:
|
|
tro_ids = TroIds.load(ids_file) if ids_file.is_file() else None
|
|
except (OSError, ValueError) as exc:
|
|
print(f"FEHLER: ID-Datei nicht lesbar: {exc}", file=sys.stderr)
|
|
return 1
|
|
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts,
|
|
tro_ids=tro_ids)
|
|
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 > DEFAULT_MAX_RESIDUAL:
|
|
print(f"FEHLER: Restfehler des Lagebezugs {transform.residual_max:.1f} mm "
|
|
f"ueber der Grenze von {DEFAULT_MAX_RESIDUAL:.1f} mm.", file=sys.stderr)
|
|
print(f" {transform.describe()}", file=sys.stderr)
|
|
print(" Mit --offset von Hand setzen.", file=sys.stderr)
|
|
return 4
|
|
|
|
counts = {"marker": 0, "flow": 0, "removed": 0, "layers": 0, "blocks": 0,
|
|
"first_id": 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, LAYER_PREFIX)
|
|
counts["layers"] = len({v for v in layers.values()})
|
|
|
|
# Interne IDs erst NACH remove_previous suchen: die Marker des letzten Laufs
|
|
# sind dann weg und ihre Nummern wieder frei.
|
|
next_id = next_internal_id(msp, elements)
|
|
placed, internal = place_markers(
|
|
msp, doc, analysis, transform, layers, args.marker_size,
|
|
args.text_height, args.fb, warnings, next_id=next_id,
|
|
)
|
|
counts["marker"] = len(placed)
|
|
counts["blocks"] = sum(1 for b in doc.blocks if b.name.startswith(BLOCK_PREFIX))
|
|
counts["first_id"] = next_id
|
|
|
|
|
|
if args.flow:
|
|
# Stationen nur mit --flow: sie bestehen aus ihrem Zu-/Ablaufpfeil
|
|
stationen = place_stations(msp, analysis, placed, layers, args.text_height)
|
|
counts["flow"] = draw_connections(
|
|
msp, doc, analysis, {**placed, **stationen}, internal, layers,
|
|
args.marker_size, args.text_height, next_id=next_id + len(placed),
|
|
)
|
|
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())
|