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:
+476
-66
@@ -35,9 +35,24 @@ Typen auch ohne Farbe unterscheiden. Die Daten stehen als Attribute am Block:
|
||||
TYPE 1Sep sichtbar, im Dialog bearbeitbar
|
||||
FB_BLOCK FB_ILS_... sichtbar nur mit --fb
|
||||
UID 0062 unsichtbar, dauerhafte interne Nummer
|
||||
KEY TRO03 unsichtbar, Schluessel in tro_overrides.ini (ID der
|
||||
ID-Datei; leer bei einem mit TRO_INSERT gesetzten)
|
||||
ITEMS 1x Separator unsichtbar, reine Daten
|
||||
CONFIDENCE mittel unsichtbar
|
||||
SEPARATORS 0010 unsichtbar
|
||||
SLOTS separator1=0010;switch1=0061 unsichtbar, Bauteil je Slot
|
||||
(TeileId; Lagerlinien als lines=0066,0069,...),
|
||||
im Dialog TRO_EDIT bearbeitbar
|
||||
|
||||
Handkorrekturen (--read-tros)
|
||||
-----------------------------
|
||||
Im Dialog TRO_EDIT (dxfmakros) aendern die SPS-Programmierer Typ, Kennung und
|
||||
die Bauteile je Slot; TRO_INSERT setzt neue Marker, geloeschte Marker fallen
|
||||
weg. Fuer "Im Plan waehlen" legt die Beschriftung die Bauteile des Exports
|
||||
(TeileId, Art, Lage in der Zeichnung) als XRECORD SPS_SKEL_PARTS in das
|
||||
Named Object Dictionary. --read-tros vergleicht die Marker mit der Ableitung
|
||||
und schreibt die Abweichungen als Abschnitte edit/edit_add nach
|
||||
cfg/tro_overrides.ini; tro_flow und tro_annotate wenden sie beim naechsten
|
||||
Lauf an (tro_flow.apply_edits).
|
||||
|
||||
Zwei Kennungen, mit Absicht:
|
||||
ID die sprechende Kennung (TRO03). Darf sich aendern - der Dialog TRO_EDIT
|
||||
@@ -122,7 +137,19 @@ from tro_flow import (
|
||||
resolve_ids_file,
|
||||
)
|
||||
from tro_ids import TroIds
|
||||
from model import ANLAGE_SCHEMA, AnlageDocument, JamArea
|
||||
from model import (
|
||||
ANLAGE_SCHEMA,
|
||||
SLOT_PART_KINDS,
|
||||
AnlageDocument,
|
||||
DerivedTro,
|
||||
JamArea,
|
||||
derive_slots,
|
||||
has_lines,
|
||||
slot_kind,
|
||||
slot_names,
|
||||
)
|
||||
from tro_ids import id_problem
|
||||
from tro_overrides import PartRef, TroEdit, load_overrides, override_path, save_edits
|
||||
from scl_context import (
|
||||
JAM_NAMES_SUFFIX,
|
||||
HwSource,
|
||||
@@ -147,6 +174,12 @@ CONNECTION_BLOCK = "CONNECTION_ARROW"
|
||||
# Staubereich (--anlage). Der Name beginnt mit JAM_, damit SSG_BLOCKEDIT ihn
|
||||
# ueber "JAM_*" an den Dialog JAM_EDIT (dxfmakros/Lisp/Jam_Edit.lsp) gibt.
|
||||
JAM_BLOCK = "JAM_AREA"
|
||||
# Bauteile des Exports fuer "Im Plan waehlen" im Dialog TRO_EDIT: XRECORD im
|
||||
# Named Object Dictionary, je Bauteil (1 . TeileId) (3 . Art) (10 . Lage)
|
||||
# (40 . halbe Breite) (41 . halbe Hoehe) in Zeichnungskoordinaten.
|
||||
PARTS_XRECORD = "SPS_SKEL_PARTS"
|
||||
# Unsichtbare Attribute der Marker (build_symbol)
|
||||
HIDDEN_TAGS = ("UID", "KEY", "ITEMS", "CONFIDENCE", "SLOTS")
|
||||
# Anzeige eines Staubereichs ohne Foerderer (Nummer nicht ableitbar)
|
||||
JAM_OFFEN = "Jam????_?"
|
||||
|
||||
@@ -155,7 +188,14 @@ JAM_OFFEN = "Jam????_?"
|
||||
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")
|
||||
JAM_VERSATZ = get_float("tro_annotate", "jam_versatz")
|
||||
JAM_ABSTAND = get_float("tro_annotate", "jam_abstand")
|
||||
# Staubereich als stehendes Rechteck, Masse relativ zur Texthoehe: halbe
|
||||
# Breite / halbe Hoehe, Name senkrecht darin (Jam0054_3 passt hinein).
|
||||
JAM_HALB_BREITE = 0.5
|
||||
JAM_HALB_HOEHE = 2.6
|
||||
JAM_TEXT = 0.6
|
||||
# Fuellung und Rand (ACI 8 = grau) und Schrift (ACI 250 = fast schwarz)
|
||||
JAM_FUELLUNG = "#d0d0d0"
|
||||
|
||||
|
||||
def hex_to_rgb(value: str) -> tuple[int, int, int]:
|
||||
@@ -189,10 +229,26 @@ def emit_lisp(cad_dir: Path) -> Path:
|
||||
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)
|
||||
|
||||
def lisp_list(values) -> str:
|
||||
return " ".join(f'"{v}"' for v in values)
|
||||
|
||||
# Je Typ die Slots in der Reihenfolge von model.derive_slots, dazu die
|
||||
# Vorgabe der Lagerlinien (0 = keine): ("PinStore_Auto" 19 "separator1" ...)
|
||||
slots = "\n".join(
|
||||
" (" + " ".join([f'"{d.get_name()}"', str(has_lines(d.get_name()))]
|
||||
+ [f'"{s}"' for s in slot_names(d.get_name())]) + ")"
|
||||
for d in TRO_CATALOG)
|
||||
kinds = "\n".join(f' ("{k}" {lisp_list(v)})' for k, v in SLOT_PART_KINDS.items())
|
||||
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"
|
||||
";; Quelle: TRO_CATALOG in lib/tro_catalog.py, Slots aus lib/model.py\n"
|
||||
";; (slot_names, SLOT_PART_KINDS) - nicht manuell aendern.\n"
|
||||
f"(setq *TRO-TYPES* (list {types}))\n"
|
||||
";; (Typ Lagerlinien-Vorgabe Slot ...) - Lagerlinien heissen line1..lineN\n"
|
||||
f"(setq *TRO-SLOTS* '(\n{slots}\n))\n"
|
||||
";; (Slot-Art Bauteilart ...) - Pruefung beim Waehlen im Plan\n"
|
||||
f"(setq *TRO-SLOT-KINDS* '(\n{kinds}\n))\n"
|
||||
"(princ)\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
@@ -403,7 +459,7 @@ def build_layers(doc, prefix: str) -> dict[str, str]:
|
||||
("weiche", "#cc8800"),
|
||||
("eingang", STATION_FARBE[BOUNDARY_ENTRY]),
|
||||
("ausgang", STATION_FARBE[BOUNDARY_EXIT]),
|
||||
("jam", "#7030a0"),
|
||||
("jam", "#808080"),
|
||||
("legend", "#808080")):
|
||||
name = f"{prefix}{key.upper()}"
|
||||
names[key] = name
|
||||
@@ -518,6 +574,18 @@ def build_symbol(doc, type_name: str, marker: float, text: float, show_fb: bool)
|
||||
"""
|
||||
name = f"{BLOCK_PREFIX}{type_name}"
|
||||
if name in doc.blocks:
|
||||
# Blockdefinition aus einem frueheren Lauf (bleibt, weil von Hand
|
||||
# gesetzte Marker sie benutzen): fehlende Datenattribute nachtragen,
|
||||
# sonst haetten die neuen Marker z. B. kein KEY/SLOTS.
|
||||
blk = doc.blocks.get(name)
|
||||
vorhanden = {a.dxf.tag.upper() for a in blk.query("ATTDEF")}
|
||||
offset = min((a.dxf.insert.y for a in blk.query("ATTDEF")), default=0.0)
|
||||
for tag_name in HIDDEN_TAGS:
|
||||
if tag_name not in vorhanden:
|
||||
offset -= text
|
||||
blk.add_attdef(tag_name, insert=(marker * 1.5, offset), height=text * 0.7,
|
||||
dxfattribs={"style": "Standard", "color": 256, "text": "",
|
||||
"invisible": 1})
|
||||
return name
|
||||
blk = doc.blocks.new(name=name)
|
||||
|
||||
@@ -542,9 +610,9 @@ def build_symbol(doc, type_name: str, marker: float, text: float, show_fb: bool)
|
||||
)
|
||||
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"):
|
||||
# nicht sichtbare Attribute: reine Daten; KEY und SLOTS liest der Dialog
|
||||
# TRO_EDIT. ID zuerst - gleiche Reihenfolge wie *strecke-attr-front* in dxfmakros.
|
||||
for tag_name in HIDDEN_TAGS:
|
||||
blk.add_attdef(
|
||||
tag_name,
|
||||
insert=(gap, offset),
|
||||
@@ -591,10 +659,11 @@ def place_markers(
|
||||
values = {
|
||||
"ID": tro.tro_id,
|
||||
"UID": internal[tro.tro_id],
|
||||
"KEY": analysis.edit_key(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),
|
||||
"SLOTS": format_slots(*tro_slots(tro)),
|
||||
}
|
||||
if show_fb:
|
||||
values["FB_BLOCK"] = tro.fb_block or "-"
|
||||
@@ -773,7 +842,8 @@ def draw_connections(
|
||||
|
||||
def build_jam_symbol(doc, text: float) -> str:
|
||||
"""
|
||||
Blockdefinition fuer einen Staubereich: Rechteck, der Name mittig darin.
|
||||
Blockdefinition fuer einen Staubereich: stehendes graues Rechteck, der
|
||||
Name senkrecht mittig darin.
|
||||
|
||||
Attribute:
|
||||
NAME sichtbar - Jam0054_3 (Foerderer + laufende Nummer)
|
||||
@@ -787,15 +857,23 @@ def build_jam_symbol(doc, text: float) -> str:
|
||||
TRO + ENTRY ist der stabile Schluessel (scl_context.jam_key).
|
||||
"""
|
||||
if JAM_BLOCK in doc.blocks:
|
||||
# Form aus einem frueheren Lauf (liegendes Rechteck) ersetzen; die
|
||||
# Attributdefinitionen bleiben, damit vorhandene Bloecke gueltig sind.
|
||||
blk = doc.blocks.get(JAM_BLOCK)
|
||||
for e in list(blk):
|
||||
if e.dxftype() != "ATTDEF":
|
||||
blk.delete_entity(e)
|
||||
_jam_form(blk, text)
|
||||
for a in blk.query("ATTDEF"):
|
||||
if a.dxf.tag.upper() == "NAME":
|
||||
_jam_name(a, text)
|
||||
return JAM_BLOCK
|
||||
blk = doc.blocks.new(name=JAM_BLOCK)
|
||||
w, h = text * 4.2, text * 1.2
|
||||
blk.add_lwpolyline([(-w, -h), (w, -h), (w, h), (-w, h)], close=True,
|
||||
dxfattribs={"color": 256})
|
||||
name = blk.add_attdef("NAME", insert=(0, 0), height=text,
|
||||
dxfattribs={"style": "Standard", "color": 256,
|
||||
"text": JAM_OFFEN})
|
||||
name.set_placement((0, 0), align=TextEntityAlignment.MIDDLE_CENTER)
|
||||
_jam_form(blk, text)
|
||||
w, h = text * JAM_HALB_BREITE, text * JAM_HALB_HOEHE
|
||||
name = blk.add_attdef("NAME", insert=(0, 0), height=text * JAM_TEXT,
|
||||
dxfattribs={"style": "Standard", "text": JAM_OFFEN})
|
||||
_jam_name(name, text)
|
||||
offset = -h - text
|
||||
for tag_name in ("CONVEYOR", "NUMBER", "TRO", "ENTRY", "AREA", "AUTO"):
|
||||
blk.add_attdef(tag_name, insert=(-w, offset), height=text * 0.7,
|
||||
@@ -805,36 +883,70 @@ def build_jam_symbol(doc, text: float) -> str:
|
||||
return JAM_BLOCK
|
||||
|
||||
|
||||
def _jam_position(area: JamArea, anlage: AnlageDocument) -> tuple[float, float] | None:
|
||||
"""Lage eines Staubereichs (CSV-Koordinaten): vor seinem Separator.
|
||||
def _jam_form(blk, text: float) -> None:
|
||||
"""Stehendes Rechteck: graue Fuellung, grauer Rand."""
|
||||
w, h = text * JAM_HALB_BREITE, text * JAM_HALB_HOEHE
|
||||
ecken = [(-w, -h), (w, -h), (w, h), (-w, h)]
|
||||
hatch = blk.add_hatch(dxfattribs={"true_color": ezdxf.rgb2int(hex_to_rgb(JAM_FUELLUNG))})
|
||||
hatch.paths.add_polyline_path(ecken, is_closed=True)
|
||||
blk.add_lwpolyline(ecken, close=True, dxfattribs={"color": 8})
|
||||
|
||||
Ausgangspunkt ist der Separator (sonst die TRO-Position); von dort
|
||||
JAM_VERSATZ in Richtung des ersten Vorgaenger-TRO - gegen den Fluss, wo
|
||||
die Traeger warten. Liegt der Vorgaenger naeher, nur die halbe Strecke.
|
||||
|
||||
def _jam_name(attdef, text: float) -> None:
|
||||
"""Name senkrecht (90 Grad) mittig ins Rechteck."""
|
||||
attdef.dxf.height = text * JAM_TEXT
|
||||
attdef.dxf.rotation = 90.0
|
||||
attdef.dxf.color = 250
|
||||
attdef.set_placement((0, 0), align=TextEntityAlignment.MIDDLE_CENTER)
|
||||
|
||||
|
||||
def _jam_position(area: JamArea, anlage: AnlageDocument,
|
||||
marker: float, text: float) -> tuple[float, float] | None:
|
||||
"""Lage eines Staubereichs (CSV-Koordinaten): direkt vor seinem TRO.
|
||||
|
||||
Ausgangspunkt ist der TRO-Marker (die TRO-Position, nicht der Separator:
|
||||
bei Speichern und 2Sep-Typen liegt der weit vom Marker). Das Rechteck
|
||||
sitzt gerade so weit davon, dass es den Marker nicht beruehrt
|
||||
(Markerradius + JAM_ABSTAND + halbe Breite), ein zweiter Eingang direkt
|
||||
daneben - in Richtung des ersten Vorgaenger-TRO, gegen den Fluss, wo die
|
||||
Traeger warten. Ohne Vorgaenger mit Lage (Station) gegen die Richtung zum
|
||||
Nachfolger. Liegt der Nachbar naeher, hoechstens die halbe Strecke. Nur
|
||||
Lage - Verbindungen und Fliessrichtung bleiben unberuehrt.
|
||||
"""
|
||||
layout = anlage.layout
|
||||
part = layout.part(area.separator) if area.separator else None
|
||||
if part is not None and part.position is not None:
|
||||
x, y = part.position[0], part.position[1]
|
||||
tro = layout.by_id(area.tro)
|
||||
if tro is not None and tro.position:
|
||||
x, y = tro.position[0], tro.position[1]
|
||||
elif area.position is not None:
|
||||
x, y = area.position[0], area.position[1]
|
||||
else:
|
||||
return None
|
||||
for vor in area.predecessors:
|
||||
tro = layout.by_id(vor)
|
||||
if tro is None or not tro.position:
|
||||
continue
|
||||
vx, vy = tro.position[0] - x, tro.position[1] - y
|
||||
dist = math.hypot(vx, vy)
|
||||
if dist > 1e-6:
|
||||
schritt = min(JAM_VERSATZ, dist / 2)
|
||||
return x + vx / dist * schritt, y + vy / dist * schritt
|
||||
return x, y
|
||||
breite = 2 * text * JAM_HALB_BREITE
|
||||
abstand = marker + JAM_ABSTAND + breite / 2 + (area.entry - 1) * (breite + JAM_ABSTAND)
|
||||
|
||||
def richtung(ids: list[str], vorzeichen: float):
|
||||
for other_id in ids:
|
||||
other = layout.by_id(other_id)
|
||||
if other is None or not other.position:
|
||||
continue
|
||||
vx, vy = other.position[0] - x, other.position[1] - y
|
||||
dist = math.hypot(vx, vy)
|
||||
if dist > 1e-6:
|
||||
schritt = min(abstand, dist / 2)
|
||||
return (x + vorzeichen * vx / dist * schritt,
|
||||
y + vorzeichen * vy / dist * schritt)
|
||||
return None
|
||||
|
||||
lage = richtung(area.predecessors or (tro.predecessors if tro else []), 1.0)
|
||||
if lage is None and tro is not None:
|
||||
lage = richtung(tro.successors, -1.0)
|
||||
return lage if lage is not None else (x - abstand, y)
|
||||
|
||||
|
||||
def place_jams(
|
||||
msp, doc, anlage: AnlageDocument, overrides: dict[str, tuple[str, int]],
|
||||
transform: Transform, layers: dict[str, str], text: float, warnings: list[str],
|
||||
marker: float = DEFAULT_MARKER,
|
||||
) -> int:
|
||||
"""
|
||||
Je Staubereich der Anlagen-JSON einen JAM_AREA-Block einfuegen.
|
||||
@@ -849,7 +961,7 @@ def place_jams(
|
||||
block = build_jam_symbol(doc, text)
|
||||
gezeichnet = 0
|
||||
for area in layout.jam_areas_or_derived():
|
||||
lage = _jam_position(area, anlage)
|
||||
lage = _jam_position(area, anlage, marker, text)
|
||||
if lage is None:
|
||||
warnings.append(f"Staubereich {area.jam_id} ({area.tro}): keine Lage - "
|
||||
f"nicht gezeichnet")
|
||||
@@ -915,6 +1027,218 @@ def read_jams(dxf_file: Path, ini_file: Path) -> tuple[dict[str, tuple[str, int]
|
||||
return overrides, meldungen
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Handkorrekturen an den TROs (Dialog TRO_EDIT, --read-tros)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def tro_slots(tro: DerivedTro) -> tuple[dict[str, str], list[str]]:
|
||||
"""Bauteile je Slot (ohne Lagerlinien) und die Lagerlinien in Reihenfolge."""
|
||||
slots: dict[str, str] = {}
|
||||
lines: list[str] = []
|
||||
for slot in derive_slots(tro):
|
||||
if slot_kind(slot.slot) == "line":
|
||||
lines.append(slot.layout_id)
|
||||
else:
|
||||
slots[slot.slot] = slot.layout_id
|
||||
return slots, lines
|
||||
|
||||
|
||||
def format_slots(slots: dict[str, str], lines: list[str]) -> str:
|
||||
"""Attribut SLOTS: 'separator1=0010;switch1=;lines=0066,0069'."""
|
||||
text = ";".join(f"{k}={v}" for k, v in slots.items())
|
||||
if lines:
|
||||
text += (";" if text else "") + "lines=" + ",".join(lines)
|
||||
return text
|
||||
|
||||
|
||||
def parse_slots(text: str) -> tuple[dict[str, str], list[str] | None]:
|
||||
"""Gegenstueck zu format_slots; lines ist None, wenn das Attribut keine nennt."""
|
||||
slots: dict[str, str] = {}
|
||||
lines: list[str] | None = None
|
||||
for part in text.split(";"):
|
||||
key, sep, value = (t.strip() for t in part.partition("="))
|
||||
if not sep or not key:
|
||||
continue
|
||||
if key.lower() == "lines":
|
||||
lines = [v.strip() for v in value.split(",") if v.strip()]
|
||||
else:
|
||||
slots[key.lower()] = value
|
||||
return slots, lines
|
||||
|
||||
|
||||
def write_parts(doc, elements, transform: Transform) -> int:
|
||||
"""Bauteile, die in einen Slot passen, als XRECORD in die Zeichnung legen."""
|
||||
kinds = {k for v in SLOT_PART_KINDS.values() for k in v}
|
||||
tags: list[tuple[int, object]] = []
|
||||
for e in elements:
|
||||
if e.kind not in kinds or e.position is None or not e.teile_id:
|
||||
continue
|
||||
x, y = transform.to_dxf(e.x, e.y)
|
||||
w, h = (e.bbox[0] / 2, e.bbox[1] / 2) if e.bbox else (0.0, 0.0)
|
||||
tags += [(1, e.teile_id), (3, e.kind), (10, (x, y, 0.0)), (40, w), (41, h)]
|
||||
root = doc.rootdict
|
||||
if PARTS_XRECORD in root:
|
||||
root.remove(PARTS_XRECORD)
|
||||
xrecord = root.add_xrecord(PARTS_XRECORD)
|
||||
xrecord.reset(tags)
|
||||
return len(tags) // 5
|
||||
|
||||
|
||||
def read_markers(dxf_file: Path) -> list[dict[str, str]]:
|
||||
"""Attribute aller TRO-Marker (Bloecke TRO_SYM_*) der Zeichnung."""
|
||||
doc = ezdxf.readfile(str(dxf_file))
|
||||
markers = []
|
||||
for ref in doc.modelspace().query("INSERT"):
|
||||
if not ref.dxf.name.upper().startswith(BLOCK_PREFIX):
|
||||
continue
|
||||
a = {att.dxf.tag.upper(): (att.dxf.text or "").strip() for att in ref.attribs}
|
||||
a["_BLOCK"] = ref.dxf.name
|
||||
markers.append(a)
|
||||
return markers
|
||||
|
||||
|
||||
def read_tros(markers: list[dict[str, str]], raw: Analysis, elements,
|
||||
csv_name: str, cfg_dir: Path | None = None,
|
||||
write: bool = True) -> tuple[list[TroEdit], list[str]]:
|
||||
"""Marker der Zeichnung mit der Ableitung vergleichen -> Handkorrekturen.
|
||||
|
||||
``raw`` ist die Ableitung OHNE Handkorrekturen (IDs aus der ID-Datei). Je
|
||||
Marker mit KEY eines abgeleiteten TRO entsteht ein edit-Abschnitt, wenn
|
||||
Typ, Kennung oder Bauteile abweichen; ein Marker ohne passenden KEY ist
|
||||
ein neuer TRO (edit_add); ein abgeleiteter TRO mit Lage, zu dem kein
|
||||
Marker mehr da ist, wurde geloescht. Die Abschnitte ersetzen alle
|
||||
bisherigen edit-/edit_add-Abschnitte der CSV in tro_overrides.ini
|
||||
(Begruendungen bleiben). Rueckgabe: (Korrekturen, Meldungen).
|
||||
"""
|
||||
meldungen: list[str] = []
|
||||
by_id = {e.teile_id: e for e in elements}
|
||||
alt = {(e.section_kind, e.key): e
|
||||
for e in load_overrides(csv_name, cfg_dir).edits}
|
||||
|
||||
def ref(teile_id: str) -> PartRef:
|
||||
if not teile_id:
|
||||
return PartRef()
|
||||
element = by_id.get(teile_id.split("-", 1)[0])
|
||||
if element is None or element.position is None:
|
||||
return PartRef(teile_id=teile_id)
|
||||
return PartRef(teile_id=teile_id, x=element.x, y=element.y)
|
||||
|
||||
def pruefen(where: str, type_name: str, slots: dict[str, str],
|
||||
lines: list[str]) -> None:
|
||||
for slot in slot_names(type_name):
|
||||
teile_id = slots.get(slot, "")
|
||||
if not teile_id:
|
||||
if slot.startswith("separator"):
|
||||
meldungen.append(f"{where}: {slot} ist leer")
|
||||
continue
|
||||
element = by_id.get(teile_id.split("-", 1)[0])
|
||||
kinds = SLOT_PART_KINDS[slot_kind(slot)]
|
||||
if element is None:
|
||||
meldungen.append(f"{where}: {slot} = {teile_id} steht nicht im Export")
|
||||
elif element.kind not in kinds:
|
||||
meldungen.append(f"{where}: {slot} = {teile_id} ist ein {element.kind}, "
|
||||
f"erwartet {'/'.join(kinds)}")
|
||||
if has_lines(type_name) and not lines:
|
||||
meldungen.append(f"{where}: Speicher ohne Lagerlinie")
|
||||
|
||||
raw_by_id = {t.tro_id: t for t in raw.tros}
|
||||
edits: list[TroEdit] = []
|
||||
gesehen: set[str] = set()
|
||||
vergeben: dict[str, str] = {}
|
||||
ids: dict[str, str] = {}
|
||||
for m in markers:
|
||||
tro_id, key = m.get("ID", ""), m.get("KEY", "")
|
||||
type_name = m.get("TYPE", "") or m["_BLOCK"][len(BLOCK_PREFIX):]
|
||||
where = f"Marker {tro_id or '?'}"
|
||||
if not tro_id:
|
||||
meldungen.append(f"{where} ({m['_BLOCK']}): ohne ID - uebersprungen")
|
||||
continue
|
||||
if get_tro(type_name) is None:
|
||||
meldungen.append(f"{where}: Typ '{type_name}' steht nicht im Katalog - "
|
||||
f"uebersprungen")
|
||||
continue
|
||||
if problem := id_problem(tro_id):
|
||||
meldungen.append(f"{where}: {problem} - uebersprungen")
|
||||
continue
|
||||
if tro_id in ids:
|
||||
meldungen.append(f"{where}: Kennung doppelt (auch {ids[tro_id]})")
|
||||
ids[tro_id] = key or tro_id
|
||||
slots, lines = parse_slots(m.get("SLOTS", ""))
|
||||
if key in gesehen:
|
||||
# kopierter Marker: gleicher KEY wie ein schon gelesener
|
||||
meldungen.append(f"{where}: KEY {key} doppelt (Marker kopiert?) - als "
|
||||
f"neuer TRO gelesen")
|
||||
key = ""
|
||||
derived = raw_by_id.get(key)
|
||||
if derived is not None:
|
||||
gesehen.add(key)
|
||||
raw_slots, raw_lines = tro_slots(derived)
|
||||
else:
|
||||
raw_slots, raw_lines = {}, []
|
||||
if lines is None:
|
||||
lines = raw_lines if has_lines(type_name) else []
|
||||
pruefen(where, type_name, slots, lines)
|
||||
for teile_id in [v for v in slots.values() if v] + lines:
|
||||
if teile_id in vergeben and vergeben[teile_id] != tro_id:
|
||||
meldungen.append(f"{where}: Bauteil {teile_id} steckt auch in "
|
||||
f"{vergeben[teile_id]}")
|
||||
vergeben[teile_id] = tro_id
|
||||
|
||||
names = slot_names(type_name)
|
||||
if derived is None:
|
||||
edit = TroEdit(key=tro_id, add=True, type_name=type_name,
|
||||
slots={s: ref(slots.get(s, "")) for s in names},
|
||||
lines=[ref(t) for t in lines] if has_lines(type_name) else None)
|
||||
else:
|
||||
edit = TroEdit(key=key)
|
||||
if type_name != derived.type_name:
|
||||
edit.type_name = type_name
|
||||
if tro_id != key:
|
||||
edit.name = tro_id
|
||||
if any(slots.get(s, "") != raw_slots.get(s, "") for s in names):
|
||||
edit.slots = {s: ref(slots.get(s, "")) for s in names}
|
||||
if has_lines(type_name) and lines != raw_lines:
|
||||
edit.lines = [ref(t) for t in lines]
|
||||
if edit.is_empty():
|
||||
continue
|
||||
if (vorher := alt.get((edit.section_kind, edit.key))) is not None:
|
||||
edit.reason = vorher.reason
|
||||
edits.append(edit)
|
||||
meldungen.append(_describe_edit(edit))
|
||||
|
||||
for tro in raw.tros:
|
||||
if tro.tro_id in gesehen or not tro.position:
|
||||
continue
|
||||
edit = TroEdit(key=tro.tro_id, delete=True)
|
||||
if (vorher := alt.get(("edit", tro.tro_id))) is not None:
|
||||
edit.reason = vorher.reason
|
||||
edits.append(edit)
|
||||
meldungen.append(_describe_edit(edit))
|
||||
|
||||
if write:
|
||||
save_edits(csv_name, edits, cfg_dir)
|
||||
return edits, meldungen
|
||||
|
||||
|
||||
def _describe_edit(edit: TroEdit) -> str:
|
||||
if edit.delete:
|
||||
return f"{edit.key}: geloescht"
|
||||
teile = []
|
||||
if edit.add:
|
||||
teile.append(f"neu ({edit.type_name})")
|
||||
elif edit.type_name:
|
||||
teile.append(f"Typ -> {edit.type_name}")
|
||||
if edit.name:
|
||||
teile.append(f"Kennung -> {edit.name}")
|
||||
if edit.slots is not None and not edit.add:
|
||||
teile.append("Bauteile " + ", ".join(
|
||||
f"{k}={r.teile_id or '-'}" for k, r in edit.slots.items()))
|
||||
if edit.lines is not None:
|
||||
teile.append(f"{len(edit.lines)} Lagerlinien")
|
||||
return f"{edit.key}: {'; '.join(teile)}"
|
||||
|
||||
|
||||
def draw_legend(
|
||||
msp, analysis: Analysis, placed: dict[str, tuple[float, float]],
|
||||
layers: dict[str, str], marker: float, text: float, source: str,
|
||||
@@ -1000,7 +1324,12 @@ def report(
|
||||
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"TROs = {len(analysis.tros)}, beschriftet: {counts['marker']}"
|
||||
+ (f" (Handkorrekturen: {len(analysis.renamed)} umbenannt, "
|
||||
f"{len(analysis.added)} neu)" if analysis.renamed or analysis.added else ""))
|
||||
if counts.get("parts"):
|
||||
print(f"Bauteile = {counts['parts']} (XRECORD {PARTS_XRECORD}, fuer "
|
||||
f"TRO_EDIT 'Im Plan waehlen'; uebernehmen mit --read-tros)")
|
||||
print(f"Verbindungen = {counts['flow']}"
|
||||
+ (" (Block CONNECTION_ARROW mit FROM_UID/TO_UID)" if counts["flow"] else ""))
|
||||
if counts.get("jams"):
|
||||
@@ -1039,6 +1368,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
" tro_annotate.bat --file dematic_merge.csv --dxf dematic_merge..dxf --flow "
|
||||
"--anlage dematic_merge_anlage.json\n"
|
||||
" tro_annotate.bat --file dematic_merge.csv --read-jams\n"
|
||||
" tro_annotate.bat --file mubea.csv --read-tros --check\n"
|
||||
"\n"
|
||||
"Exit-Codes:\n"
|
||||
" 0 ok\n"
|
||||
@@ -1085,6 +1415,13 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
"<csv>_annotated.dxf in %%SKEL_RESULTS%%) und nach "
|
||||
"%%SKEL_CFG%%\\<csv>_jam_names.ini schreiben, dann beenden. "
|
||||
"scl_skeleton liest die Datei.")
|
||||
parser.add_argument("--read-tros", nargs="?", const="", default=None, metavar="DXF",
|
||||
help="Im Dialog TRO_EDIT/TRO_INSERT geaenderte, neue und "
|
||||
"geloeschte TRO-Marker aus der beschrifteten Zeichnung "
|
||||
"uebernehmen (Standard: <csv>_annotated.dxf in "
|
||||
"%%SKEL_RESULTS%%) und als Abschnitte edit/edit_add nach "
|
||||
"%%SKEL_CFG%%\\tro_overrides.ini schreiben, dann beenden. "
|
||||
"tro_flow und tro_annotate wenden sie an.")
|
||||
parser.add_argument("--check", action="store_true",
|
||||
help="Nur pruefen und berichten, keine Datei schreiben.")
|
||||
parser.add_argument("--emit-lisp", action="store_true",
|
||||
@@ -1125,6 +1462,96 @@ def _main_read_jams(args, csv_file: Path) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _derive(args, csv_file: Path, edits: list[TroEdit] | None):
|
||||
"""CSV lesen und die TROs ableiten - (Elemente, Analysis) oder None.
|
||||
|
||||
Derselbe Aufruf wie in tro_flow.main (mit der Separator-Gegenprobe aus
|
||||
<name>_raw.json, der ID-Datei und den Handkorrekturen), damit die
|
||||
Zeichnung genau die TROs der TRO-JSON zeigt - sie ist nur Ansicht, keine
|
||||
Datenquelle. --read-tros vergleicht mit der Ableitung ohne Korrekturen
|
||||
(edits=None).
|
||||
"""
|
||||
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 None
|
||||
graph = build_graph(elements, csv_file.name, read_warnings)
|
||||
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 None
|
||||
analysis = analyse_tros(graph, elements, sepliste_counts=sepliste_counts,
|
||||
tro_ids=tro_ids, edits=edits)
|
||||
if not analysis.tros:
|
||||
print(f"FEHLER: keine TROs aus {csv_file} ableitbar.", file=sys.stderr)
|
||||
return None
|
||||
return elements, analysis
|
||||
|
||||
|
||||
def _main_read_tros(args, csv_file: Path) -> int:
|
||||
"""--read-tros: Marker der beschrifteten Zeichnung -> tro_overrides.ini."""
|
||||
results = env_dir("SKEL_RESULTS", "results")
|
||||
cfg_dir = env_dir("SKEL_CFG", "cfg")
|
||||
name = args.read_tros or args.out or f"{csv_file.stem}_annotated.dxf"
|
||||
dxf_file = Path(name)
|
||||
if not dxf_file.is_absolute() and dxf_file.parent == Path("."):
|
||||
dxf_file = results / name
|
||||
if not dxf_file.is_file():
|
||||
print(f"FEHLER: beschriftete Zeichnung nicht gefunden: {dxf_file}", file=sys.stderr)
|
||||
print(" In BricsCAD als DXF speichern (Dateiname wie beim Beschriften).",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
if not csv_file.is_file():
|
||||
print(f"FEHLER: CSV nicht gefunden: {csv_file}", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
markers = read_markers(dxf_file)
|
||||
except (OSError, ezdxf.DXFError) as exc:
|
||||
print(f"FEHLER: {dxf_file} nicht lesbar: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
derived = _derive(args, csv_file, None)
|
||||
if derived is None:
|
||||
return 1
|
||||
elements, raw = derived
|
||||
# Schutz: eine falsche Zeichnung (ohne oder mit fremden Markern) wuerde
|
||||
# sonst alle TROs als geloescht eintragen.
|
||||
keys = {m.get("KEY", "") for m in markers}
|
||||
if not markers or not keys & {t.tro_id for t in raw.tros}:
|
||||
print(f"FEHLER: {dxf_file.name} enthaelt keine Marker zu den TROs aus "
|
||||
f"{csv_file.name} ({len(markers)} Marker) - nichts geschrieben. Zeichnung "
|
||||
f"mit dem aktuellen tro_annotate neu beschriften?", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
edits, meldungen = read_tros(markers, raw, elements, csv_file.name, cfg_dir,
|
||||
write=not args.check)
|
||||
except (OSError, configparser.Error) as exc:
|
||||
print(f"FEHLER: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
print("")
|
||||
print("================================================================")
|
||||
print("TRO-HANDKORREKTUREN UEBERNEHMEN")
|
||||
print("================================================================")
|
||||
print(f"Zeichnung = {dxf_file} ({len(markers)} Marker)")
|
||||
ziel = override_path(cfg_dir)
|
||||
print(f"Korrekturen = {len(edits)} -> "
|
||||
f"{'- (--check, nichts geschrieben)' if args.check else ziel}")
|
||||
for m in meldungen:
|
||||
print(f" {m}")
|
||||
print("Danach: tro_flow --json (und tro_annotate) neu laufen lassen.")
|
||||
print("================================================================")
|
||||
print("")
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
warnings: list[str] = []
|
||||
@@ -1145,10 +1572,12 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
if args.read_jams is not None:
|
||||
return _main_read_jams(args, csv_file)
|
||||
if args.read_tros is not None:
|
||||
return _main_read_tros(args, csv_file)
|
||||
|
||||
if not args.dxf:
|
||||
print("FEHLER: --dxf fehlt (nur --emit-lisp und --read-jams gehen ohne "
|
||||
"Zeichnung).", file=sys.stderr)
|
||||
print("FEHLER: --dxf fehlt (nur --emit-lisp, --read-jams und --read-tros gehen "
|
||||
"ohne Zeichnung).", file=sys.stderr)
|
||||
return 1
|
||||
if not csv_file.is_file():
|
||||
print(f"FEHLER: CSV nicht gefunden: {csv_file}", file=sys.stderr)
|
||||
@@ -1192,33 +1621,13 @@ def main(argv: list[str] | None = None) -> int:
|
||||
f"nicht zu '{csv_file.name}'.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# 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)
|
||||
# TROs aus dem CSV, mit den Handkorrekturen aus tro_overrides.ini
|
||||
overrides = load_overrides(csv_file.name, cfg_dir)
|
||||
warnings += [w for w in overrides.warnings if ".edit" in w]
|
||||
derived = _derive(args, csv_file, overrides.edits)
|
||||
if derived is None:
|
||||
return 1
|
||||
elements, analysis = derived
|
||||
|
||||
# Zeichnung
|
||||
try:
|
||||
@@ -1274,6 +1683,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
counts["marker"] = len(placed)
|
||||
counts["blocks"] = sum(1 for b in doc.blocks if b.name.startswith(BLOCK_PREFIX))
|
||||
counts["first_id"] = next_id
|
||||
counts["parts"] = write_parts(doc, elements, transform)
|
||||
|
||||
|
||||
if args.flow:
|
||||
@@ -1285,7 +1695,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
if anlage is not None:
|
||||
counts["jams"] = place_jams(msp, doc, anlage, jam_overrides, transform, layers,
|
||||
args.text_height, warnings)
|
||||
args.text_height, warnings, marker=args.marker_size)
|
||||
if args.legend:
|
||||
draw_legend(msp, analysis, placed, layers, args.marker_size,
|
||||
args.text_height, csv_file.name)
|
||||
|
||||
Reference in New Issue
Block a user