attradd.py und add_attr.bat in set_attributs.py/.bat umbenannt
lib/attradd.py -> lib/set_attributs.py: Name besser passend zum Skript-Zweck (setzt Attribute, analog set_einfuegepkt / set_koords). bin/add_attr.bat -> bin/set_attributs.bat entsprechend angepasst. cfg/attradd.cfg: Kommentar auf neuen Skriptnamen aktualisiert. HOEHE/DREHUNG-Eintraege aus [bogen] und [weiche] entfernt, da diese Attribute jetzt dynamisch via ssg-cfg-or aus component_defaults.json bezogen werden (nicht mehr als statische ATTDEF-Defaults noetig). Ebenso RADIUS aus [weiche] entfernt (war redundant zu KurvenWinkel). doc/tools.md und CLAUDE.md: Alle Verweise auf attradd.py auf set_attributs.py aktualisiert. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
set_attributs.py - Fuegt Attribute (ATTDEF) zu allen Omniflo DXF-Dateien hinzu.
|
||||
|
||||
Liest die Attribut-Konfiguration aus cfg/attradd.cfg und die Element-Daten
|
||||
aus data/json/omniflo_boegen.json und omniflo_weichen.json.
|
||||
Die ergaenzten DXF-Dateien werden in results/omniflo/ gespeichert.
|
||||
|
||||
Umgebungsvariablen:
|
||||
DXFM_DATA - Pfad zum data/-Verzeichnis
|
||||
DXFM_CFG - Pfad zum cfg/-Verzeichnis
|
||||
DXFM_RESULTS - Pfad zum results/-Verzeichnis
|
||||
|
||||
Aufruf:
|
||||
python lib/set_attributs.py [--number SIVASNR] [--dry-run]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import configparser
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import ezdxf
|
||||
from ezdxf.enums import TextEntityAlignment
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Konfiguration lesen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def load_config(cfg_path):
|
||||
"""
|
||||
Liest attradd.cfg und gibt ein Dict zurueck:
|
||||
{
|
||||
"bogen": [("BESCHR", "ProfilTyp"), ("ARTINR", "Sivasnr"), ...],
|
||||
"weiche": [("BESCHR", "ProfilTyp"), ...],
|
||||
"layer": {"bogen": "OF Boegen A-2", "weiche_90": "Weiche 90 Grad A-2", ...},
|
||||
}
|
||||
"""
|
||||
config = configparser.ConfigParser()
|
||||
config.optionxform = str # Case-sensitive Keys
|
||||
config.read(cfg_path, encoding="utf-8")
|
||||
|
||||
result = {}
|
||||
for section in config.sections():
|
||||
if section == "layer":
|
||||
# Layer-Sektion als einfaches Dict speichern
|
||||
layer_map = {}
|
||||
for key, val in config.items(section):
|
||||
val = val.strip()
|
||||
if val.startswith('"') and val.endswith('"'):
|
||||
val = val[1:-1]
|
||||
layer_map[key] = val
|
||||
result["layer"] = layer_map
|
||||
else:
|
||||
attrs = []
|
||||
for tag, source in config.items(section):
|
||||
attrs.append((tag, source.strip()))
|
||||
result[section] = attrs
|
||||
|
||||
return result
|
||||
|
||||
|
||||
KOMBI_TYPEN = {"Deltaweiche", "Sternweiche", "Dreifachweiche"}
|
||||
|
||||
|
||||
def get_layer_for_entry(typ, json_entry, layer_map):
|
||||
"""
|
||||
Bestimmt den Layer-Namen anhand von Elementtyp und JSON-Daten.
|
||||
Gibt den Layer-String zurueck oder "" wenn kein Mapping vorhanden.
|
||||
"""
|
||||
if not layer_map:
|
||||
return ""
|
||||
|
||||
if typ == "bogen":
|
||||
return layer_map.get("bogen", "")
|
||||
|
||||
# Weiche: Kategorie anhand KurvenWinkel und WeichenTyp bestimmen
|
||||
winkel = json_entry.get("KurvenWinkel", 0)
|
||||
weichentyp = json_entry.get("WeichenTyp", "")
|
||||
|
||||
# Weichenkombinationen (Deltaweiche, Sternweiche, Dreifachweiche)
|
||||
if weichentyp in KOMBI_TYPEN:
|
||||
return layer_map.get("weichenkombination", "")
|
||||
|
||||
# Weichenkoerper (KurvenWinkel = 22.5)
|
||||
if winkel == 22.5:
|
||||
return layer_map.get("weichenkoerper", "")
|
||||
|
||||
# Weiche 90 Grad
|
||||
if winkel == 90:
|
||||
return layer_map.get("weiche_90", "")
|
||||
|
||||
# Weiche 45 Grad
|
||||
if winkel == 45:
|
||||
return layer_map.get("weiche_45", "")
|
||||
|
||||
# Weichenbatterien / Parallel (KurvenWinkel = 0)
|
||||
if winkel == 0:
|
||||
return layer_map.get("weichenbatterie", "")
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def resolve_value(source, json_entry, sivasnr):
|
||||
"""
|
||||
Loest einen Quell-Ausdruck aus der Config auf:
|
||||
- "fester Text" -> fester Text
|
||||
- {sivasnr} -> SivasNr-Wert
|
||||
- Feldname -> Wert aus json_entry
|
||||
"""
|
||||
source = source.strip()
|
||||
|
||||
# Fester Wert in Anfuehrungszeichen
|
||||
if source.startswith('"') and source.endswith('"'):
|
||||
return source[1:-1]
|
||||
|
||||
# Platzhalter
|
||||
if source == "{sivasnr}":
|
||||
return str(sivasnr)
|
||||
|
||||
# JSON-Feld
|
||||
val = json_entry.get(source)
|
||||
if val is None:
|
||||
return ""
|
||||
return str(val)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# JSON-Daten laden
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def load_json(path):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def build_lookup(boegen, weichen):
|
||||
"""Dict: SivasNr (String) -> (typ, eintrag)"""
|
||||
lookup = {}
|
||||
for b in boegen:
|
||||
lookup[str(b["Sivasnr"])] = ("bogen", b)
|
||||
for w in weichen:
|
||||
lookup[str(w["Sivasnr"])] = ("weiche", w)
|
||||
return lookup
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ATTDEF zu DXF hinzufuegen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ATTDEF_START_Y = -50.0
|
||||
ATTDEF_SPACING = -30.0
|
||||
ATTDEF_HEIGHT = 5.0
|
||||
ATTDEF_LAYER = "ATTRIB"
|
||||
|
||||
|
||||
def wrap_entities_in_block(doc, block_name):
|
||||
"""
|
||||
Verschiebt alle Entities aus dem Modelspace in eine Blockdefinition
|
||||
mit dem Namen block_name und fuegt eine Blockreferenz bei (0, 0) ein.
|
||||
Vorhandene Blockdefinition mit gleichem Namen wird ersetzt (fuer Re-Runs).
|
||||
"""
|
||||
msp = doc.modelspace()
|
||||
|
||||
# Vorhandene Blockdefinition entfernen
|
||||
if block_name in doc.blocks:
|
||||
doc.blocks.delete_block(block_name, safe=False)
|
||||
|
||||
blk = doc.blocks.new(block_name)
|
||||
|
||||
for entity in list(msp):
|
||||
msp.move_to_layout(entity, blk)
|
||||
|
||||
msp.add_blockref(block_name, (0, 0))
|
||||
|
||||
|
||||
def add_attdefs_to_dxf(dxf_path, output_path, attr_defs, json_entry, sivasnr,
|
||||
layer_value=""):
|
||||
"""
|
||||
Oeffnet eine DXF-Datei, fuegt ATTDEF-Entities in den Modelspace ein,
|
||||
fasst alle Entities anschliessend in einen Block mit dem Namen sivasnr
|
||||
zusammen und speichert das Ergebnis.
|
||||
|
||||
attr_defs: Liste von (TAG, source_expression) aus der Config
|
||||
json_entry: Dict mit den JSON-Daten fuer dieses Element
|
||||
layer_value: Layer-Name aus [layer]-Sektion (wird als LAYER-ATTDEF gesetzt)
|
||||
"""
|
||||
doc = ezdxf.readfile(dxf_path)
|
||||
msp = doc.modelspace()
|
||||
|
||||
# ATTRIB-Layer anlegen falls nicht vorhanden
|
||||
if ATTDEF_LAYER not in doc.layers:
|
||||
doc.layers.add(ATTDEF_LAYER, color=7)
|
||||
|
||||
# Bestehende ATTDEFs entfernen (fuer Re-Runs)
|
||||
existing = [e for e in msp if e.dxftype() == "ATTDEF"]
|
||||
for e in existing:
|
||||
msp.delete_entity(e)
|
||||
|
||||
# Alle ATTDEFs sammeln: Config-Attribute + LAYER aus [layer]-Sektion
|
||||
all_defs = list(attr_defs)
|
||||
if layer_value:
|
||||
all_defs.append(("LAYER", f'"{layer_value}"'))
|
||||
|
||||
# ATTDEFs hinzufuegen
|
||||
y_pos = ATTDEF_START_Y
|
||||
count = 0
|
||||
for tag, source in all_defs:
|
||||
default_value = resolve_value(source, json_entry, sivasnr)
|
||||
prompt = tag
|
||||
|
||||
msp.add_attdef(
|
||||
tag=tag,
|
||||
text=default_value,
|
||||
dxfattribs={
|
||||
"layer": ATTDEF_LAYER,
|
||||
"height": ATTDEF_HEIGHT,
|
||||
"insert": (0, y_pos),
|
||||
"prompt": prompt,
|
||||
"flags": 0, # sichtbar
|
||||
},
|
||||
)
|
||||
y_pos += ATTDEF_SPACING
|
||||
count += 1
|
||||
|
||||
# Alle Entities (Geometrie + ATTDEFs) in Block mit Sivasnr-Namen fassen
|
||||
wrap_entities_in_block(doc, sivasnr)
|
||||
|
||||
doc.saveas(output_path)
|
||||
return count
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hauptprogramm
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def process_all(data_dir, cfg_path, results_dir, single_nr=None, dry_run=False):
|
||||
"""Verarbeitet alle Omniflo DXF-Dateien."""
|
||||
|
||||
# Config laden
|
||||
attr_config = load_config(cfg_path)
|
||||
layer_map = attr_config.pop("layer", {})
|
||||
print(f"[set_attributs] Config geladen: {list(attr_config.keys())}")
|
||||
if layer_map:
|
||||
print(f" [layer] {len(layer_map)} Zuordnungen: "
|
||||
f"{', '.join(f'{k}={v}' for k, v in layer_map.items())}")
|
||||
for section, attrs in attr_config.items():
|
||||
tags = [a[0] for a in attrs]
|
||||
print(f" [{section}] Attribute: {', '.join(tags)}")
|
||||
|
||||
# JSON-Daten laden
|
||||
boegen = load_json(os.path.join(data_dir, "json", "omniflo_boegen.json"))
|
||||
weichen = load_json(os.path.join(data_dir, "json", "omniflo_weichen.json"))
|
||||
lookup = build_lookup(boegen, weichen)
|
||||
print(f"[set_attributs] {len(boegen)} Boegen, {len(weichen)} Weichen geladen.")
|
||||
|
||||
# Ausgabeverzeichnis
|
||||
out_dir = os.path.join(results_dir, "omniflo")
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
omniflo_dir = os.path.join(data_dir, "omniflo")
|
||||
processed = 0
|
||||
skipped = 0
|
||||
errors = 0
|
||||
|
||||
# Alle bekannten SivasNr verarbeiten
|
||||
for sivasnr, (typ, entry) in sorted(lookup.items()):
|
||||
if single_nr and sivasnr != single_nr:
|
||||
continue
|
||||
|
||||
# Quelldatei finden
|
||||
dxf_name = f"{sivasnr}.dxf"
|
||||
dxf_path = os.path.join(omniflo_dir, dxf_name)
|
||||
if not os.path.exists(dxf_path):
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# Attribute fuer diesen Typ
|
||||
if typ not in attr_config:
|
||||
print(f" WARNUNG: Kein Config-Abschnitt fuer Typ '{typ}', "
|
||||
f"ueberspringe {sivasnr}")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
attr_defs = attr_config[typ]
|
||||
layer_value = get_layer_for_entry(typ, entry, layer_map)
|
||||
output_path = os.path.join(out_dir, dxf_name)
|
||||
|
||||
if dry_run:
|
||||
values = {tag: resolve_value(src, entry, sivasnr)
|
||||
for tag, src in attr_defs}
|
||||
if layer_value:
|
||||
values["LAYER"] = layer_value
|
||||
print(f" [DRY] {sivasnr} ({typ}): {values}")
|
||||
processed += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
count = add_attdefs_to_dxf(dxf_path, output_path, attr_defs,
|
||||
entry, sivasnr,
|
||||
layer_value=layer_value)
|
||||
processed += 1
|
||||
print(f" OK: {sivasnr} ({typ}) -> {count} Attribute -> {output_path}")
|
||||
except Exception as e:
|
||||
errors += 1
|
||||
print(f" FEHLER: {sivasnr}: {e}")
|
||||
|
||||
print(f"\n[set_attributs] Fertig: {processed} verarbeitet, "
|
||||
f"{skipped} uebersprungen, {errors} Fehler.")
|
||||
if not dry_run:
|
||||
print(f"[set_attributs] Ausgabe in: {out_dir}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Fuegt Attribute (ATTDEF) zu Omniflo DXF-Dateien hinzu.")
|
||||
parser.add_argument("--number", "-n", type=str, default=None,
|
||||
help="Nur diese SivasNr verarbeiten")
|
||||
parser.add_argument("--dry-run", "-d", action="store_true",
|
||||
help="Nur anzeigen, nicht schreiben")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Umgebungsvariablen
|
||||
data_dir = os.environ.get("DXFM_DATA")
|
||||
cfg_dir = os.environ.get("DXFM_CFG")
|
||||
results_dir = os.environ.get("DXFM_RESULTS")
|
||||
|
||||
# Fallback: relative Pfade vom Projektverzeichnis
|
||||
if not data_dir:
|
||||
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
data_dir = os.path.join(base, "data")
|
||||
if not cfg_dir:
|
||||
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
cfg_dir = os.path.join(base, "cfg")
|
||||
if not results_dir:
|
||||
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
results_dir = os.path.join(base, "results")
|
||||
|
||||
cfg_path = os.path.join(cfg_dir, "attradd.cfg")
|
||||
if not os.path.exists(cfg_path):
|
||||
print(f"[set_attributs] FEHLER: Config nicht gefunden: {cfg_path}")
|
||||
sys.exit(1)
|
||||
|
||||
process_all(data_dir, cfg_path, results_dir,
|
||||
single_nr=args.number, dry_run=args.dry_run)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user