tro_annotate --read-tros: TRO-Handkorrekturen aus BricsCAD, Staubereiche vor dem TRO

- Marker tragen KEY (ID der ID-Datei) und SLOTS (Bauteil je Slot), die
  Zeichnung die Bauteiltabelle SPS_SKEL_PARTS (XRECORD) fuer "Im Plan
  waehlen" im Dialog TRO_EDIT (dxfmakros)
- --read-tros schreibt geaenderte, neue und geloeschte Marker als
  Abschnitte edit/edit_add nach cfg/tro_overrides.ini; tro_flow.apply_edits
  wendet nur diese Arten an (Bauteile mit Lage, exportfest)
- --emit-lisp schreibt dazu *TRO-SLOTS* und *TRO-SLOT-KINDS*
- model: Lagerlinien zaehlen je TRO (Katalog nur Vorgabe), Mubea-Speicher
  jetzt 20 Linien (Golden-Datei)
- Staubereich als stehendes graues Rechteck direkt vor seinem TRO-Marker
  (jam_abstand statt jam_versatz); Marker 150 mm, Text 120 mm

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 14:35:04 +01:00
parent 9ec1022fb1
commit 6c8c235287
12 changed files with 1441 additions and 125 deletions
+223 -5
View File
@@ -115,7 +115,10 @@ ausschliesslich derive_tros(), Schritt fuer Schritt:
Alle anderen Funktionen (link_tros, locate_tros, derive_boundaries, ...)
aendern den Typ nicht mehr. cfg/tro_overrides.ini wird hier seit
2026-10-05 nicht mehr angewendet (siehe tro_overrides.py) - ein falsch
erkannter Typ wird in derive_tros() korrigiert, nicht per INI.
erkannter Typ wird in derive_tros() korrigiert, nicht per INI. Ausnahme seit
2026-10-26: die Handkorrekturen aus BricsCAD (Abschnitte edit/edit_add, von
tro_annotate --read-tros geschrieben) wendet apply_edits nach derive_tros an;
sie haengen an den stabilen IDs der ID-Datei.
Nicht abgeleitet werden bisher: 1Sep_SSCC (nur Hinweis, Schritt 6),
EmptyCarrBuffer und LoadingBoom. Schritt 3c braucht eine stimmige
@@ -181,12 +184,23 @@ from material_flow import (
resolve_input,
)
from kreisel_junctions import Junction, find_junctions, separators_before
from model import Analysis, DerivedTro, LayoutPart, TroDocument
from model import (
SLOT_PART_KINDS,
Analysis,
DerivedTro,
LayoutPart,
TroDocument,
has_lines,
slot_kind,
slot_names,
storage_lines,
)
from parameter import get_float, get_int
from tro_ids import (
DEFAULT_TOLERANCE,
TroIds,
assign_ids,
id_problem,
ids_path,
insert_entries,
other_sections,
@@ -203,6 +217,7 @@ from tro_catalog import (
get_tro,
mermaid_classdefs,
)
from tro_overrides import PartRef, TroEdit, load_overrides
# ---------------------------------------------------------------------------
# Konstanten
@@ -2229,12 +2244,197 @@ def locate_tros(tros: list[DerivedTro], elements: list[Element], graph: Graph) -
tro.position_from = source if tro.position else "keine Position"
def _resolve_part(ref: PartRef, slot: str, elements: list[Element],
by_id: dict[str, Element], where: str, tolerance: float,
findings: list[str]) -> str:
"""TeileId fuer einen Slot der Handkorrektur im aktuellen Export.
Erst die TeileId selbst (passende Art, Lage innerhalb der Toleranz), sonst
das naechste Bauteil derselben Art an der Lage - die TeileId wechselt
zwischen zwei Exporten, die Lage nicht. "" wenn nichts passt.
"""
if not ref.teile_id:
return ""
kinds = SLOT_PART_KINDS.get(slot_kind(slot), ())
base = ref.teile_id.split("-", 1)[0]
element = by_id.get(base)
if element is not None and element.kind in kinds:
if ref.x is None or element.position is None or math.hypot(
element.x - ref.x, element.y - ref.y) <= tolerance:
return ref.teile_id
if ref.x is not None and ref.y is not None:
naechste = min(
((math.hypot(e.x - ref.x, e.y - ref.y), e) for e in elements
if e.kind in kinds and e.position is not None),
default=None, key=lambda p: p[0])
if naechste is not None and naechste[0] <= tolerance:
findings.append(
f"{where}: {slot} {ref.teile_id} steht im Export nicht mehr dort - "
f"{naechste[1].kind} {naechste[1].teile_id} an derselben Lage "
f"uebernommen ({naechste[0]:.0f} mm)")
return naechste[1].teile_id
findings.append(f"{where}: {slot} {ref.render()} ist kein "
f"{'/'.join(kinds) or 'Bauteil'} des Exports - Slot bleibt leer")
return ""
def _host_of(sep_id: str, by_id: dict[str, Element], graph: Graph) -> str:
"""Flussknoten, auf dem ein Separator sitzt (CSV-Spalte 'Zuordnung')."""
element = by_id.get(sep_id)
host = element.assignment if element is not None else ""
return host if host and host != UNASSIGNED and host in graph.nodes else ""
def _set_parts(tro: DerivedTro, edit: TroEdit, elements: list[Element],
by_id: dict[str, Element], graph: Graph, tolerance: float,
findings: list[str]) -> None:
"""Bauteile eines TRO aus den Slots der Handkorrektur setzen.
Umkehrung von model.derive_slots: separator<i> -> separators, line<n> ->
die Lagerlinien am Ende von separators (je Linie ein Host), switch<i> ->
switches, scanner<i> -> scanners, drive -> hosts[0].
"""
where = f"Handkorrektur {edit.key}"
slots = edit.slots or {}
names = slot_names(tro.type_name)
def teil(slot: str) -> str:
ref = slots.get(slot)
return _resolve_part(ref, slot, elements, by_id, where, tolerance,
findings) if ref is not None else ""
alte_seps = list(tro.separators)
if edit.slots is not None:
seps = [teil(n) for n in names if n.startswith("separator")]
while seps and not seps[-1]:
seps.pop()
tro.switches = [teil(n) for n in names if n.startswith("switch")]
tro.scanners = [s for s in (teil(n) for n in names if n.startswith("scanner")) if s]
drive = teil("drive") if "drive" in names else ""
else:
seps = alte_seps[:len(alte_seps) - len(storage_lines(tro))] \
if has_lines(tro.type_name) else alte_seps
drive = ""
if has_lines(tro.type_name):
if edit.lines is not None:
lines = [s for s in (
_resolve_part(r, f"line{n}", elements, by_id, where, tolerance, findings)
for n, r in enumerate(edit.lines, start=1)) if s]
else:
lines = storage_lines(tro)
seps = [s for s in seps if s and s not in lines]
tro.separators = seps + lines
# Je Linie ein Host - danach zaehlt model.storage_lines die Linien
tro.hosts = [_host_of(s, by_id, graph) or s for s in lines]
else:
tro.separators = seps
if set(seps) != set(alte_seps):
hosts = list(dict.fromkeys(h for s in seps if (h := _host_of(s, by_id, graph))))
if hosts:
tro.hosts = hosts
if drive:
tro.hosts = [drive] + [h for h in tro.hosts if h != drive]
def apply_edits(tros: list[DerivedTro], edits: list[TroEdit], elements: list[Element],
graph: Graph, findings: list[str], analysis: Analysis,
tolerance: float = DEFAULT_TOLERANCE) -> list[DerivedTro]:
"""Handkorrekturen aus BricsCAD (tro_overrides.ini, edit/edit_add) anwenden.
Laeuft nach derive_tros (die IDs aus der ID-Datei stehen fest) und vor
Verortung und Verknuepfung, damit Koordinaten und Kanten den korrigierten
TRO zeigen. Ein Bauteil, das ein korrigierter TRO bekommt, verliert sein
bisheriger TRO; bleibt der ohne Separator, entfaellt er (Befund).
Umbenennungen kommen zuletzt, die Schluessel bleiben bis dahin die IDs der
ID-Datei. Ergebnis in ``analysis.renamed``/``analysis.added``.
"""
by_id = {e.teile_id: e for e in elements}
tros = list(tros)
renames: list[tuple[DerivedTro, TroEdit]] = []
# Erst loeschen, dann aendern, zuletzt neu anlegen: ein neuer TRO darf die
# Bauteile eines geloeschten uebernehmen, ohne dass der als "verdraengt" gilt.
for edit in sorted(edits, key=lambda e: 0 if e.delete else 2 if e.add else 1):
where = f"Handkorrektur {edit.key}"
tro = next((t for t in tros if t.tro_id == edit.key), None)
if edit.add:
if tro is not None:
findings.append(f"{where}: neuer TRO, aber {edit.key} gibt es schon - "
f"ignoriert")
continue
tro = DerivedTro(tro_id=edit.key, type_name=edit.type_name,
label=f"von Hand angelegt ({edit.key})", hosts=[],
scanners=[], reason="von Hand in BricsCAD angelegt",
confidence="bestaetigt")
tros.append(tro)
analysis.added.append(edit.key)
elif tro is None:
findings.append(f"{where}: TRO {edit.key} wird nicht (mehr) abgeleitet - "
f"Korrektur ignoriert")
continue
if edit.delete:
tros.remove(tro)
findings.append(f"{where}: TRO {edit.key} ({tro.type_name}) von Hand "
f"entfernt" + (f" - {edit.reason}" if edit.reason else ""))
continue
if edit.type_name and edit.type_name != tro.type_name:
findings.append(f"{where}: Typ {tro.type_name} -> {edit.type_name}")
tro.type_name = edit.type_name
if edit.slots is not None or edit.lines is not None or edit.add:
_set_parts(tro, edit, elements, by_id, graph, tolerance, findings)
if not edit.add:
tro.reason = f"{tro.reason}; Handkorrektur BricsCAD"
if edit.reason:
tro.reason = f"{tro.reason} ({edit.reason})"
tro.confidence = "bestaetigt"
# Bauteile, die dieser TRO jetzt hat, den anderen TROs nehmen
eigene = {p for p in tro.separators + tro.scanners + tro.switches if p}
for other in list(tros):
if other is tro:
continue
weg = eigene & set(other.separators + other.scanners + other.switches)
if not weg:
continue
other.separators = [s for s in other.separators if s not in weg]
other.scanners = [s for s in other.scanners if s not in weg]
other.switches = ["" if s in weg else s for s in other.switches]
if other.separators or not other.items.get(TroItem.SEPARATOR):
findings.append(f"{where}: {', '.join(sorted(weg))} von {other.tro_id} "
f"zu {tro.tro_id} verschoben")
else:
tros.remove(other)
findings.append(f"{where}: {other.tro_id} hat keinen Separator mehr "
f"({', '.join(sorted(weg))} an {tro.tro_id}) - entfaellt")
if edit.name and edit.name != tro.tro_id:
renames.append((tro, edit))
for tro, edit in renames:
if tro not in tros:
continue
problem = id_problem(edit.name)
if not problem and any(t.tro_id == edit.name for t in tros):
problem = f"'{edit.name}' ist schon vergeben"
if problem:
findings.append(f"Handkorrektur {edit.key}: Umbenennung nicht moeglich - "
f"{problem}")
continue
if tro.tro_id in analysis.added:
analysis.added[analysis.added.index(tro.tro_id)] = edit.name
else:
analysis.renamed[edit.name] = tro.tro_id
tro.tro_id = edit.name
return tros
def analyse_tros(
graph: Graph,
elements: list[Element],
*,
sepliste_counts: dict[str, int] | None = None,
tro_ids: TroIds | None = None,
edits: list[TroEdit] | None = None,
) -> Analysis:
"""TROs ermitteln, verknuepfen, verorten und gegen die Typdefinition pruefen.
@@ -2250,6 +2450,8 @@ def analyse_tros(
Roh-JSON (load_sepliste_counts) - nur Gegenprobe.
tro_ids: ID-Datei der Anlage (siehe derive_tros); None = neu
nummerieren.
edits: Handkorrekturen aus BricsCAD (tro_overrides.load_overrides
(...).edits), angewandt direkt nach derive_tros (apply_edits).
Rueckgabe:
Analysis mit tros, boundaries, edges/edge_kinds, unknown_types
@@ -2259,6 +2461,11 @@ def analyse_tros(
analysis = Analysis()
analysis.tros = derive_tros(graph, elements, analysis.findings,
sepliste_counts=sepliste_counts, tro_ids=tro_ids)
if edits:
analysis.tros = apply_edits(analysis.tros, edits, elements, graph,
analysis.findings, analysis,
tolerance=tro_ids.tolerance if tro_ids
else DEFAULT_TOLERANCE)
analysis.boundaries = derive_boundaries(elements, graph)
# Fuer Verortung und Verknuepfung zaehlen TROs und Systemgrenzen zusammen:
# link_tros() registriert eine Kante nur zu einem Knoten, der Host eines
@@ -3281,16 +3488,20 @@ def write_ids_file(tro_ids: TroIds, analysis: Analysis, elements: list[Element],
if result is None:
return "unveraendert"
anchors = separator_anchors(elements)
# Die ID-Datei kennt die TROs unter ihrer abgeleiteten ID: Umbenennungen
# und neue TROs aus BricsCAD stehen in tro_overrides.ini, nicht hier.
tros = [t.model_copy(update={"tro_id": analysis.edit_key(t.tro_id)})
for t in analysis.tros if t.tro_id not in analysis.added]
if result.renumbered:
tro_ids.path.parent.mkdir(parents=True, exist_ok=True)
rest = other_sections(tro_ids.path)
text = render_ids(analysis.tros, anchors, source)
text = render_ids(tros, anchors, source)
tro_ids.path.write_text(text + (f"\n{rest}\n" if rest else ""),
encoding="utf-8")
return "neu geschrieben" if tro_ids.exists else "angelegt"
if result.new:
insert_entries(tro_ids.path,
render_new_entries(analysis.tros, anchors, result.new, source))
render_new_entries(tros, anchors, result.new, source))
return f"{len(result.new)} neue(r) TRO(s) angehaengt"
return "unveraendert"
@@ -3379,8 +3590,15 @@ def main(argv: list[str] | None = None) -> int:
print(f"FEHLER: ID-Datei nicht lesbar: {exc}", file=sys.stderr)
return 1
# Handkorrekturen aus BricsCAD (nur die Arten edit/edit_add, siehe
# tro_overrides.py); die uebrigen Abschnitte der Datei gelten hier nicht.
overrides = load_overrides(csv_file.name, env_dir("SKEL_CFG", "cfg"))
warnings += [w for w in overrides.warnings if ".edit" in w]
if overrides.edits and args.renumber:
warnings.append(f"--renumber: {len(overrides.edits)} Handkorrektur(en) in "
f"{overrides.source} haengen an den alten IDs - pruefen")
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts,
tro_ids=tro_ids)
tro_ids=tro_ids, edits=overrides.edits)
if not analysis.tros:
print(f"FEHLER: {csv_file} enthaelt keine Separatoren - keine TROs "
f"ableitbar.", file=sys.stderr)