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:
+229
-2
@@ -36,6 +36,31 @@ mehrere Anlagen in einer Datei stehen koennen):
|
||||
|
||||
Offene Punkte aendern die TRO-Struktur ausdruecklich **nicht**.
|
||||
|
||||
Handkorrekturen aus BricsCAD (seit 2026-10-26)
|
||||
-----------------------------------------------
|
||||
Die Abschnitte oben wendet tro_flow nicht mehr an (siehe unten). Die Arten
|
||||
`edit` und `edit_add` dagegen schon: sie schreibt `tro_annotate --read-tros`
|
||||
aus den im Dialog TRO_EDIT/TRO_INSERT (dxfmakros) geaenderten Markern, und
|
||||
`tro_flow.apply_edits` wendet sie nach der Ableitung an. Sie haengen an der
|
||||
ID aus `<csv>_tro_ids.ini`, die ueber die Lage des Separators stabil bleibt -
|
||||
anders als die laufenden Nummern, an denen die alten Abschnitte veraltet sind.
|
||||
|
||||
[<csv>.edit.<ID>] Korrektur an einem abgeleiteten TRO
|
||||
type = 1Sep1Swi neuer Katalogtyp (fehlt = wie abgeleitet)
|
||||
name = Einschleus_1 neue Kennung (fehlt = <ID> bleibt)
|
||||
delete = true TRO entfaellt
|
||||
separator1 = 0010 @ 1234.5 -567.8 Bauteil je Slot: TeileId @ x y (mm,
|
||||
switch1 = - CSV); "-" = Slot bewusst leer
|
||||
lines = 0066 @ 958 12300, 0069 @ 958 12600 Lagerlinien in Reihenfolge
|
||||
reason = ... Begruendung (bleibt beim Neuschreiben erhalten)
|
||||
|
||||
[<csv>.edit_add.<ID>] neuer TRO (TRO_INSERT), Schluessel wie oben
|
||||
|
||||
Stehen Slots im Abschnitt, gelten sie vollstaendig (fehlende Slots des Typs
|
||||
bleiben leer); stehen keine, bleiben die Bauteile wie abgeleitet. Die Lage
|
||||
hinter der TeileId macht den Eintrag exportfest: passt die TeileId im neuen
|
||||
Export nicht mehr zur Lage, gilt das naechste Bauteil derselben Art dort.
|
||||
|
||||
Die Felder heissen wie in lib/model.py (`type_name`, `separators`, `hosts`,
|
||||
`reason`, `confidence`); Verbindungen und offene Punkte sind direkt
|
||||
`model.Connection` bzw. `model.TroCandidate`.
|
||||
@@ -45,7 +70,9 @@ from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from model import Connection, TroCandidate
|
||||
@@ -122,9 +149,84 @@ class MergeSpec:
|
||||
confidence: str = DEFAULT_CONFIDENCE
|
||||
|
||||
|
||||
@dataclass
|
||||
class PartRef:
|
||||
"""Bauteil in einem Slot: TeileId plus Lage (mm, CSV) als Rueckfallebene.
|
||||
|
||||
Die TeileId wechselt zwischen zwei Exporten derselben Anlage, die Lage
|
||||
nicht (wie bei der ID-Datei, lib/tro_ids.py). ``teile_id == ""`` ist ein
|
||||
bewusst leerer Slot.
|
||||
"""
|
||||
|
||||
teile_id: str = ""
|
||||
x: float | None = None
|
||||
y: float | None = None
|
||||
|
||||
def render(self) -> str:
|
||||
"""'0010 @ 1234.5 -567.8', '0010' ohne Lage, '-' fuer leer."""
|
||||
if not self.teile_id:
|
||||
return "-"
|
||||
if self.x is None or self.y is None:
|
||||
return self.teile_id
|
||||
return f"{self.teile_id} @ {self.x:.1f} {self.y:.1f}"
|
||||
|
||||
@classmethod
|
||||
def parse(cls, raw: str) -> PartRef:
|
||||
"""Gegenstueck zu :meth:`render` (ValueError bei falscher Form)."""
|
||||
text = raw.strip()
|
||||
if text in ("", "-"):
|
||||
return cls()
|
||||
teile_id, at, pos = (t.strip() for t in text.partition("@"))
|
||||
if not teile_id or " " in teile_id:
|
||||
raise ValueError(f"'{raw}' ist kein Bauteil '<TeileId> @ x y'")
|
||||
if not at:
|
||||
return cls(teile_id=teile_id)
|
||||
werte = pos.replace(",", " ").split()
|
||||
if len(werte) != 2:
|
||||
raise ValueError(f"'{raw}': Lage hinter @ braucht x und y")
|
||||
return cls(teile_id=teile_id, x=float(werte[0]), y=float(werte[1]))
|
||||
|
||||
|
||||
# Slot-Schluessel in einem edit-Abschnitt (lines steht fuer alle Lagerlinien)
|
||||
_SLOT_KEY = re.compile(r"^(separator|switch|scanner)\d+$|^drive$")
|
||||
_EDIT_KEYS = ("type", "name", "delete", "reason", "lines")
|
||||
|
||||
|
||||
@dataclass
|
||||
class TroEdit:
|
||||
"""Handkorrektur aus BricsCAD (Abschnitt edit bzw. edit_add).
|
||||
|
||||
``key`` ist die ID aus ``<csv>_tro_ids.ini`` (edit) bzw. die Kennung des
|
||||
neuen TRO (edit_add). ``slots``/``lines`` sind None, wenn die Bauteile wie
|
||||
abgeleitet bleiben.
|
||||
"""
|
||||
|
||||
key: str
|
||||
add: bool = False
|
||||
delete: bool = False
|
||||
name: str = ""
|
||||
type_name: str = ""
|
||||
slots: dict[str, PartRef] | None = None
|
||||
lines: list[PartRef] | None = None
|
||||
reason: str = ""
|
||||
|
||||
@property
|
||||
def section_kind(self) -> str:
|
||||
return "edit_add" if self.add else "edit"
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
"""Nichts zu tun (ein edit-Abschnitt ohne Aenderung entfaellt)."""
|
||||
return not (self.add or self.delete or self.name or self.type_name
|
||||
or self.slots is not None or self.lines is not None)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Overrides:
|
||||
"""Alle Korrekturen zu einer CSV-Datei."""
|
||||
"""Alle Korrekturen zu einer CSV-Datei.
|
||||
|
||||
``edits`` (Handkorrekturen aus BricsCAD) zaehlen fuer ``bool()`` nicht mit:
|
||||
material_flow --connect liest nur die uebrigen Arten.
|
||||
"""
|
||||
|
||||
source: Path | None = None
|
||||
tros: dict[str, TroOverride] = field(default_factory=dict)
|
||||
@@ -133,6 +235,8 @@ class Overrides:
|
||||
# Gesetzte Topologie: ersetzt die abgeleiteten Kanten vollstaendig
|
||||
connections: list[Connection] = field(default_factory=list)
|
||||
candidates: list[TroCandidate] = field(default_factory=list)
|
||||
# Handkorrekturen aus BricsCAD in Dateireihenfolge (tro_flow.apply_edits)
|
||||
edits: list[TroEdit] = field(default_factory=list)
|
||||
# Protokoll der tatsaechlich angewandten Korrekturen (fuer die Doku)
|
||||
applied: list[str] = field(default_factory=list)
|
||||
warnings: list[str] = field(default_factory=list)
|
||||
@@ -294,11 +398,134 @@ def load_overrides(csv_name: str, cfg_dir: Path | None = None) -> Overrides:
|
||||
note=values.get("note", "").strip(),
|
||||
)
|
||||
)
|
||||
elif kind in ("edit", "edit_add"):
|
||||
edit = _parse_edit(section, name, kind == "edit_add", values,
|
||||
result.warnings)
|
||||
if edit is not None:
|
||||
result.edits.append(edit)
|
||||
else:
|
||||
result.warnings.append(
|
||||
f"[{section}]: unbekannte Art '{kind}' - erwartet 'tro', 'split', "
|
||||
f"'merge' oder 'openpoint'"
|
||||
f"'merge', 'openpoint', 'edit' oder 'edit_add'"
|
||||
)
|
||||
|
||||
result.candidates.sort(key=lambda p: p.name)
|
||||
return result
|
||||
|
||||
|
||||
def _parse_edit(section: str, name: str, add: bool, values,
|
||||
warnings: list[str]) -> TroEdit | None:
|
||||
"""Einen edit-/edit_add-Abschnitt lesen (None bei einem Fehler)."""
|
||||
type_name = values.get("type", "").strip()
|
||||
if type_name and get_tro(type_name) is None:
|
||||
warnings.append(f"[{section}]: Typ '{type_name}' steht nicht im TRO-Katalog "
|
||||
f"- Korrektur ignoriert")
|
||||
return None
|
||||
if add and not type_name:
|
||||
warnings.append(f"[{section}]: neuer TRO ohne 'type' - nicht angelegt")
|
||||
return None
|
||||
edit = TroEdit(
|
||||
key=name,
|
||||
add=add,
|
||||
delete=values.get("delete", "").strip().lower() in ("true", "ja", "1", "yes"),
|
||||
name=values.get("name", "").strip(),
|
||||
type_name=type_name,
|
||||
reason=values.get("reason", "").strip(),
|
||||
)
|
||||
try:
|
||||
slots = {key: PartRef.parse(raw) for key, raw in values.items()
|
||||
if key not in _EDIT_KEYS}
|
||||
if "lines" in values:
|
||||
edit.lines = [PartRef.parse(t) for t in values["lines"].split(",") if t.strip()]
|
||||
except ValueError as exc:
|
||||
warnings.append(f"[{section}]: {exc} - Korrektur ignoriert")
|
||||
return None
|
||||
falsch = sorted(k for k in slots if not _SLOT_KEY.match(k))
|
||||
if falsch:
|
||||
warnings.append(f"[{section}]: unbekannte Schluessel {', '.join(falsch)} - "
|
||||
f"erwartet type, name, delete, reason, lines oder Slots "
|
||||
f"(separator1, switch1, scanner1, drive)")
|
||||
return None
|
||||
if slots:
|
||||
edit.slots = slots
|
||||
return edit
|
||||
|
||||
|
||||
# Kopfzeile des von --read-tros geschriebenen Blocks; beim Neuschreiben
|
||||
# wird sie samt aller edit-/edit_add-Abschnitte der CSV ersetzt.
|
||||
_EDIT_HEAD = "; --- Handkorrekturen aus BricsCAD zu {csv}"
|
||||
_HEADER = re.compile(r"^\s*\[(.+)\]\s*$")
|
||||
|
||||
|
||||
def render_edit(csv_name: str, edit: TroEdit) -> str:
|
||||
"""Einen Abschnitt als INI-Text."""
|
||||
lines = [f"[{csv_name}.{edit.section_kind}.{edit.key}]"]
|
||||
if edit.delete:
|
||||
lines.append("delete = true")
|
||||
if edit.type_name:
|
||||
lines.append(f"type = {edit.type_name}")
|
||||
if edit.name:
|
||||
lines.append(f"name = {edit.name}")
|
||||
for slot, ref in (edit.slots or {}).items():
|
||||
lines.append(f"{slot} = {ref.render()}")
|
||||
if edit.lines is not None:
|
||||
lines.append(f"lines = {', '.join(r.render() for r in edit.lines)}")
|
||||
if edit.reason:
|
||||
lines.append(f"reason = {edit.reason}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def save_edits(csv_name: str, edits: list[TroEdit], cfg_dir: Path | None = None) -> Path:
|
||||
"""Alle edit-/edit_add-Abschnitte zu `csv_name` durch `edits` ersetzen.
|
||||
|
||||
Der Rest der Datei (von Hand gepflegt, mit Kommentaren) bleibt Zeile fuer
|
||||
Zeile erhalten; der neue Block steht am Ende. Kommentarzeilen direkt vor
|
||||
dem naechsten Abschnitt gehoeren zu diesem und bleiben stehen.
|
||||
"""
|
||||
path = override_path(cfg_dir)
|
||||
text = path.read_text(encoding="utf-8") if path.is_file() else ""
|
||||
prefix = csv_name.lower()
|
||||
head = _EDIT_HEAD.format(csv=csv_name).lower()
|
||||
|
||||
def ist_edit(section: str) -> bool:
|
||||
parts = section.split(".")
|
||||
return (len(parts) >= 3 and parts[-2].lower() in ("edit", "edit_add")
|
||||
and ".".join(parts[:-2]).lower() == prefix)
|
||||
|
||||
def ist_rand(line: str) -> bool:
|
||||
return not line.strip() or line.lstrip().startswith(";")
|
||||
|
||||
keep: list[str] = []
|
||||
skipped: list[str] = []
|
||||
skipping = False
|
||||
for line in text.splitlines():
|
||||
if line.strip().lower().startswith(head):
|
||||
continue
|
||||
if m := _HEADER.match(line):
|
||||
if skipping:
|
||||
# Kommentare/Leerzeilen am Ende gehoeren zum naechsten Abschnitt
|
||||
tail: list[str] = []
|
||||
while skipped and ist_rand(skipped[-1]):
|
||||
tail.insert(0, skipped.pop())
|
||||
keep.extend(tail)
|
||||
skipping = ist_edit(m.group(1).strip())
|
||||
skipped = []
|
||||
if skipping:
|
||||
continue
|
||||
if skipping:
|
||||
skipped.append(line)
|
||||
else:
|
||||
keep.append(line)
|
||||
|
||||
while keep and not keep[-1].strip():
|
||||
keep.pop()
|
||||
if edits:
|
||||
if keep:
|
||||
keep += ["", ""]
|
||||
keep.append(_EDIT_HEAD.format(csv=csv_name)
|
||||
+ f" - tro_annotate --read-tros, {datetime.now():%Y-%m-%d}")
|
||||
for edit in edits:
|
||||
keep += [render_edit(csv_name, edit), ""]
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("\n".join(keep).rstrip("\n") + "\n", encoding="utf-8")
|
||||
return path
|
||||
|
||||
Reference in New Issue
Block a user