This commit is contained in:
2025-07-23 11:26:11 +02:00
3 changed files with 173 additions and 17 deletions
+2 -3
View File
@@ -2,7 +2,6 @@
Erzeugt aus einer csv Datei mit Angaben über alle in einer Anlage enthaltenen Elemente eine dxf der Anlage Erzeugt aus einer csv Datei mit Angaben über alle in einer Anlage enthaltenen Elemente eine dxf der Anlage
``csv ``csv
Elementnummer;TeileArt;TeileId;NachbarIds;Bezeichnung;Planquadrat;Merkmale Elementnummer;TeileArt;TeileId;NachbarIds;Bezeichnung;Planquadrat;Merkmale
@@ -25,6 +24,6 @@ Erzeugt aus einer csv Datei mit Angaben über alle in einer Anlage enthaltenen E
17;"ILS 2.0 Gefällestrecke";"shape_8b856d97-b18b-6263-7c98-79d8b4ca679a";"shape_f81e5c4b-a976-a3c0-3304-d2b30da1ab29";"";"X:13674.15 Y:8821.64";{"Anzahl der Zusatzseparatoren":"1","Länge in Meter":"8.2"} 17;"ILS 2.0 Gefällestrecke";"shape_8b856d97-b18b-6263-7c98-79d8b4ca679a";"shape_f81e5c4b-a976-a3c0-3304-d2b30da1ab29";"";"X:13674.15 Y:8821.64";{"Anzahl der Zusatzseparatoren":"1","Länge in Meter":"8.2"}
18;"ILS 2.0 Gefällestrecke";"shape_67f8d19e-8857-1f5d-7d90-3a2a0a9da157";"shape_f81e5c4b-a976-a3c0-3304-d2b30da1ab29";"";"X:13674.15 Y:8021.59";{"Anzahl der Zusatzseparatoren":"1","Länge in Meter":"8.2"} 18;"ILS 2.0 Gefällestrecke";"shape_67f8d19e-8857-1f5d-7d90-3a2a0a9da157";"shape_f81e5c4b-a976-a3c0-3304-d2b30da1ab29";"";"X:13674.15 Y:8021.59";{"Anzahl der Zusatzseparatoren":"1","Länge in Meter":"8.2"}
19;"ILS 2.0 Gefällestrecke";"shape_80ca3488-cc41-4caa-00cd-b3de9f5428e6";"shape_f81e5c4b-a976-a3c0-3304-d2b30da1ab29";"";"X:13674.15 Y:9621.69";{"Anzahl der Zusatzseparatoren":"1","Länge in Meter":"8.2"} 19;"ILS 2.0 Gefällestrecke";"shape_80ca3488-cc41-4caa-00cd-b3de9f5428e6";"shape_f81e5c4b-a976-a3c0-3304-d2b30da1ab29";"";"X:13674.15 Y:9621.69";{"Anzahl der Zusatzseparatoren":"1","Länge in Meter":"8.2"}
``
![Mubea Anlage](doc/images/mubea.png) ![Mubea Anlage](doc/images/mubea.png)
+2
View File
@@ -0,0 +1,2 @@
[simple_types]
shape_names = ILS 2.0 Kreisel
+169 -14
View File
@@ -1,25 +1,180 @@
"""
placeblocks.py
Erzeugt DXF-Elemente aus einer RuleDesigner-CSV.
Einfache Formen (z.B. "ILS 2.0 Kreisel") werden direkt konstruiert,
komplexe per Blockreferenz aus einer DXF-Bibliothek eingefügt.
"""
import os import os
import unittest import sys
import csv
import json
import re
import argparse
import configparser
import ezdxf
from pathlib import Path
# --------------------------------------------------------- Konstante Parameter
ATTR_TAG = "TeileId" # Attributtag im Block
RADIUS = 400 # Radius der Kreiselkreise (mm)
class TestPlant(unittest.TestCase): # --------------------------------------------------------- Hilfsfunktionen
def check_environment_var(env_str: str) -> Path:
"""Liefert Path aus Umgebungsvariable oder beendet mit Fehlermeldung."""
out_path = os.environ.get(env_str)
if out_path:
return Path(out_path)
print(f"Umgebungsvariable {env_str} ist nicht gesetzt oder leer.")
sys.exit(1)
def test_duplicate_points(self): def extract_coords(planquadrat: str) -> tuple[float, float]:
''' Testet das Nicht-Hinzufügen von doppelten Punkten''' """Extrahiert X/Y Koordinaten aus PlanquadratString."""
m = re.search(r"X:(\d+[\.,]?\d*)\s+Y:(\d+[\.,]?\d*)", planquadrat)
if not m:
raise ValueError(f"Koordinaten nicht gefunden in: '{planquadrat}'")
x, y = m.groups()
return float(x.replace(",", ".")), float(y.replace(",", "."))
# nodeids.add_point(Point(1,1)) def parse_merkmale(merkmale_str: str) -> dict:
# nodeids.add_point(Point(1,1)) """Parst Merkmale-JSON-String in dict; bei Fehler → leeres Dict."""
try:
return json.loads(merkmale_str)
except json.JSONDecodeError:
return {}
self.assertEqual(1, 1) def import_block(block_name: str, from_doc, to_doc) -> None:
"""Importiert Blockdefinition block_name von from_doc nach to_doc."""
if block_name in to_doc.blocks:
return
if block_name not in from_doc.blocks:
raise ValueError(f"Block '{block_name}' nicht in Bibliothek gefunden.")
src = from_doc.blocks[block_name]
tgt = to_doc.blocks.new(name=block_name, dxfattribs=src.dxf.attribs())
for ent in src:
tgt.add_entity(ent.copy())
if __name__ == '__main__': def build_simple_shape(msp, teileart: str, x: float, y: float,
# Plot Ausgabe in Unittests steuern teile_id: str, merkmale: dict):
draw = False """Erzeugt einfache Geometrien direkt im Modelspace."""
if teileart == "ILS 2.0 Kreisel":
abstand_m = merkmale.get(
"Abstand (Kreiselachse A - Kreiselachse) in Meter", "20"
).replace(",", ".")
try:
abstand = float(abstand_m) * 1000 # Meter → mm
except ValueError:
abstand = 20000 # Fallback 20m
suite = unittest.TestSuite() cx1 = x - abstand / 2
suite.addTest(TestPlant('test_duplicate_points')) cx2 = x + abstand / 2
runner = unittest.TextTestRunner()
runner.run(suite) # Zwei Kreise + tangentiale Verbindungs­linien
msp.add_circle((cx1, y), RADIUS)
msp.add_circle((cx2, y), RADIUS)
msp.add_line((cx1, y - RADIUS), (cx2, y - RADIUS))
msp.add_line((cx1, y + RADIUS), (cx2, y + RADIUS))
else:
raise NotImplementedError(f"Einfache Form '{teileart}' nicht implementiert.")
def load_simple_types(cfg_path: Path) -> set[str]:
"""Lädt die Liste der einfachen TeileArtNamen aus .cfg."""
cfg = configparser.ConfigParser()
cfg.read(cfg_path)
names = cfg.get("simple_types", "shape_names", fallback="")
return {n.strip() for n in names.split(",") if n.strip()}
# --------------------------------------------------------- Hauptfunktion
def main(csv_path: Path, lib_path: Path, cfg_path: Path,
output_path: Path, verbose=False):
simple_types = load_simple_types(cfg_path)
# Bibliothek nur laden, wenn Datei existiert
lib_doc = None
if lib_path.exists():
try:
lib_doc = ezdxf.readfile(lib_path)
if verbose:
print(f"[INFO] Bibliothek geladen: {lib_path}")
except Exception as e:
sys.exit(f"Fehler beim Lesen der Bibliothek '{lib_path}': {e}")
else:
print(f"[INFO] Keine Bibliothek gefunden unter {lib_path}. "
"Komplexe Formen werden übersprungen.")
# Neue Ziel­zeichnung (DXF R2018)
doc = ezdxf.new(dxfversion="R2018")
msp = doc.modelspace()
# CSV einlesen
with csv_path.open(newline="", encoding="utf-8") as fh:
reader = csv.DictReader(fh, delimiter=';')
for row in reader:
teileart = row["TeileArt"].strip()
teileid = row["TeileId"].strip()
planquadrat = row["Planquadrat"]
merkmale = parse_merkmale(row.get("Merkmale", ""))
try:
x, y = extract_coords(planquadrat)
except Exception as e:
print(f"[WARN] {teileid}: {e}")
continue
# Einfache Form
if teileart in simple_types:
try:
build_simple_shape(msp, teileart, x, y, teileid, merkmale)
if verbose:
print(f"[INFO] Simple '{teileart}'{teileid} "
f"({x:.1f}, {y:.1f})")
except Exception as e:
print(f"[ERROR] Simple '{teileart}' ({teileid}): {e}")
continue
# Komplexe Form (Block)
if not lib_doc:
print(f"[WARN] '{teileart}' benötigt Bibliothek, wird übersprungen.")
continue
try:
import_block(teileart, lib_doc, doc)
bref = msp.add_blockref(teileart, (x, y))
bref.add_auto_attribs({ATTR_TAG: teileid})
if verbose:
print(f"[INFO] Block '{teileart}'{teileid} "
f"({x:.1f}, {y:.1f})")
except Exception as e:
print(f"[ERROR] Block '{teileart}' ({teileid}): {e}")
# DXF speichern
doc.saveas(output_path)
print(f"[DONE] DXF gespeichert unter: {output_path}")
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Plaziert Anlagenkomponenten aus RuleDesigner CSV.")
parser.add_argument("-f", "--file", required=True, help="CSV-Datei (Name oder Pfad)", metavar="input.csv")
parser.add_argument("-c", "--config", help="CFG mit einfachen Formen", metavar="shapes.cfg")
parser.add_argument("-l", "--lib", help="DXF-Bibliothek mit Blöcken", metavar="bibliothek.dxf")
parser.add_argument("-o", "--output", help="Ziel-DXF (Standard: PROJECT_WORK/anlage.dxf)", metavar="anlage.dxf")
parser.add_argument("-v", "--verbose", action="store_true", help="mehr Ausgaben anzeigen")
args = parser.parse_args()
# Verzeichnisse aus Umgebungs­variablen
data_dir = check_environment_var("PROJECT_DATA")
work_dir = check_environment_var("PROJECT_WORK")
config_dir = check_environment_var("PROJECT_CFG")
# CSVPfad: nur Dateiname → im WORKOrdner suchen
if os.sep not in args.file and "/" not in args.file:
csv_path = work_dir / args.file
else:
csv_path = Path(args.file)
cfg_path = Path(args.config) if args.config else config_dir / "shapes.cfg"
lib_path = Path(args.lib) if args.lib else data_dir / "bibliothek.dxf"
output_path = Path(args.output) if args.output else (work_dir / f"{csv_path.stem}.dxf")
main(csv_path, lib_path, cfg_path, output_path, verbose=args.verbose)