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>
This commit is contained in:
2026-07-31 13:32:48 +02:00
parent 977b5c826e
commit 0f02fd83aa
6 changed files with 380 additions and 55 deletions
+120 -28
View File
@@ -21,16 +21,21 @@ denselben Layern unangetastet bleiben.
Marker als Block mit Attributen
-------------------------------
Je TRO-Typ wird eine Blockdefinition angelegt (TRO_SYM_<Typ>) und an der
Anlagenkoordinate eingefuegt. Die Daten stehen als Attribute am Block:
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:
TRO_ID TRO03
TRO_TYPE 1Sep
FB_BLOCK FB_ILS_MTRO_1Sep
ITEMS 1x Separator
CONFIDENCE mittel
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
TRO_ID kann spaeter zurueck in die Generierung laufen.
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
---------
@@ -72,7 +77,7 @@ from dxf_registration import (
verify_transform,
)
from material_flow import build_graph, env_dir, read_elements, resolve_input
from tro_catalog import STYLES, get_tro
from tro_catalog import STYLES, TRO_CATALOG, TroSymbol, get_tro
from tro_flow import Analysis, analyse_tros
# ---------------------------------------------------------------------------
@@ -96,6 +101,27 @@ def hex_to_rgb(value: str) -> tuple[int, int, int]:
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
# ---------------------------------------------------------------------------
@@ -190,8 +216,10 @@ def build_layers(doc, prefix: str) -> dict[str, str]:
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.fill)),
"true_color": ezdxf.rgb2int(hex_to_rgb(style.stroke)),
})
for key, colour in (("label", "#404040"), ("flow", "#2f5597"),
("legend", "#808080")):
@@ -204,29 +232,80 @@ def build_layers(doc, prefix: str) -> dict[str, str]:
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 (Marker + Attributdefinitionen)."""
"""
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)
# Marker: Ring mit Mittelpunkt, Farbe kommt vom Layer des INSERT
blk.add_circle((0, 0), marker, dxfattribs={"color": 256})
blk.add_circle((0, 0), marker * 0.18, dxfattribs={"color": 256})
blk.add_line((-marker, 0), (marker, 0), dxfattribs={"color": 256})
blk.add_line((0, -marker), (0, marker), dxfattribs={"color": 256})
definition = get_tro(type_name)
draw_shape(blk, definition.get_symbol() if definition else "", marker)
gap = marker * 1.35
rows = [
("TRO_ID", type_name and "TRO??", text * 1.35, 0),
("TRO_TYPE", type_name, text, 0),
]
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, 0))
rows.append(("FB_BLOCK", "FB", text * 0.85))
offset = 0.0
for tag_name, default, height, _ in rows:
for tag_name, default, height in rows:
blk.add_attdef(
tag_name,
insert=(gap, offset),
@@ -235,7 +314,7 @@ def build_symbol(doc, type_name: str, marker: float, text: float, show_fb: bool)
)
offset -= height * 1.55
# nicht sichtbare Attribute: reine Daten
# nicht sichtbare Attribute: reine Daten, nicht im Dialog
for tag_name in ("ITEMS", "CONFIDENCE", "SEPARATORS"):
blk.add_attdef(
tag_name,
@@ -271,8 +350,8 @@ def place_markers(
block = build_symbol(doc, tro.type_name, marker, text, show_fb)
values = {
"TRO_ID": tro.tro_id,
"TRO_TYPE": tro.type_name,
"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),
@@ -425,9 +504,9 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser.add_argument("--file", default="export.csv", metavar="NAME",
help="CSV-Export in %%SKEL_DATA%% oder ein Pfad. "
"Standard: %(default)s")
parser.add_argument("--dxf", required=True, metavar="NAME",
parser.add_argument("--dxf", metavar="NAME",
help="Originalzeichnung als DXF (Pfad oder Name in "
"%%SKEL_DATA%%).")
"%%SKEL_DATA%%). Ausser bei --emit-lisp erforderlich.")
parser.add_argument("--out", metavar="NAME",
help="Name der Ausgabedatei in %%SKEL_RESULTS%%. "
"Standard: <csv>_annotated.dxf")
@@ -456,6 +535,9 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"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)
@@ -463,6 +545,16 @@ 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)