This commit is contained in:
2025-07-23 14:32:43 +02:00
5 changed files with 97917 additions and 214 deletions
-2
View File
@@ -1,2 +0,0 @@
[simple_types]
shape_names = ILS 2.0 Kreisel
+44 -31
View File
@@ -7,11 +7,8 @@ import sys
from pathlib import Path from pathlib import Path
import ezdxf import ezdxf
import ezdxf.filemanagement
from ezdxf.addons import iterdxf from ezdxf.addons import iterdxf
from shapely.geometry import Point from shapely.geometry import Point
# Fix import for DXFStructureError
from ezdxf.lldxf.const import DXFStructureError from ezdxf.lldxf.const import DXFStructureError
@@ -70,6 +67,38 @@ def get_attributes_of_insert(d_insert: dict, d_pos: dict) -> tuple[dict, str, st
ld = d_insert ld = d_insert
typ = 'unknown' typ = 'unknown'
# die neueren Bläcke heissen nicht IO, sondern haben einen Namen
if "IO" in d_insert:
pos = d_pos["IO"]
typ = get_type_of_name_cfg(d_insert["IO"])
# neuer Block: Enthält alles was nötig is
if "ARTINR" in d_insert and "SPS" in d_insert:
id_ = d_insert["IO"]+"@"+d_insert["SPS"]
ld["pos"] = (pos[0], pos[1])
if "REALE_POSITION" in d_insert and d_insert["REALE_POSITION"] == 'x':
pos = d_pos["REALE_POSITION"]
else:
id_ = d_insert["IO"]
# Sensoren werden später gemerged mit den anderen Blöcken des Rahmens mit A,B,C, usw
if "SPS" in d_insert and typ != "Sensor":
id_ = id_+"@"+d_insert["SPS"]
if "REALE_POSITION" in d_insert and d_insert["REALE_POSITION"] == 'x':
pos = d_pos["REALE_POSITION"]
# Hoehe und Breite von "x" addieren, um Mittelpunkt zu finden
breite_marker = config.getfloat("GetPos-Geom-Sensor", "Breite")
hoehe_marker = config.getfloat("GetPos-Geom-Sensor", "Hoehe")
midx = pos[0] + breite_marker * 0.5
midy = pos[1] + hoehe_marker * 0.5
ld["pos"] = (round(midx, 1), round(midy, 1))
else:
ld["pos"] = (pos[0], pos[1])
# die neueren Bläcke heissen nicht IO, sondern haben einen Namen # die neueren Bläcke heissen nicht IO, sondern haben einen Namen
if "NAME" in d_insert: if "NAME" in d_insert:
typ = get_type_of_name_cfg(d_insert["NAME"]) typ = get_type_of_name_cfg(d_insert["NAME"])
@@ -77,28 +106,7 @@ def get_attributes_of_insert(d_insert: dict, d_pos: dict) -> tuple[dict, str, st
pos = d_pos["NAME"] pos = d_pos["NAME"]
ld["pos"] = (pos[0], pos[1]) ld["pos"] = (pos[0], pos[1])
if "IO" in d_insert: if "B" in d_insert and "IO" not in d_insert:
attr_text = d_insert["IO"]
typ = get_type_of_name_cfg(attr_text)
id_ = d_insert["IO"]
# Sensoren werden später gemerged mit den anderen Blöcken des Rahmens mit A,B,C, usw
if "SPS" in d_insert and typ != "Sensor":
id_ = id_+"@"+d_insert["SPS"]
pos = d_pos["IO"]
if "REALE_POSITION" in d_insert and d_insert["REALE_POSITION"] == 'x':
pos = d_pos["REALE_POSITION"]
# Hoehe und Breite von "x" addieren, um Mittelpunkt zu finden
breite_marker = config.getfloat("GetPos-Geom-Sensor", "Breite")
hoehe_marker = config.getfloat("GetPos-Geom-Sensor", "Hoehe")
midx = pos[0] + breite_marker * 0.5
midy = pos[1] + hoehe_marker * 0.5
ld["pos"] = (round(midx, 1), round(midy, 1))
else:
ld["pos"] = (pos[0], pos[1])
if "B" in d_insert:
attr_text = d_insert["B"] attr_text = d_insert["B"]
typ = get_type_of_name_cfg(attr_text) typ = get_type_of_name_cfg(attr_text)
id_ = attr_text id_ = attr_text
@@ -217,11 +225,14 @@ def extract_input_positions(insert_iterable) -> tuple[dict, dict, dict, dict]:
ld, id_, typ = get_attributes_of_insert(insert, pos) ld, id_, typ = get_attributes_of_insert(insert, pos)
if typ == "Sensor": if typ == "Sensor":
# wenn NAME enthalten ist, dann ist das ein eindeutiger Bezeichner # wenn NAME enthalten ist, z.B. bei Erdungslayouts, dann ist das ein eindeutiger Bezeichner
if "NAME" in ld: if "NAME" in ld:
all_sensors[id_] = ld all_sensors[id_] = ld
# neuer Block der alle nötigen Infos enthält
elif "IO" in ld and "ARTINR" in ld :
all_sensors[id_] = ld
# alle anderen Sachen werden dann aus mehreren Rahmen zusammen gesammelt
else: else:
# alle anderen Sachen werden dann aus mehreren Rahmen zusammen gesammelt
wp.add_block(id_, ld) wp.add_block(id_, ld)
elif typ == "Kabel": elif typ == "Kabel":
@@ -651,6 +662,11 @@ def check_environment_var(env_str: str) -> Path:
print(f"Umgebungsvariable {env_str} ist nicht gesetzt oder leer.") print(f"Umgebungsvariable {env_str} ist nicht gesetzt oder leer.")
exit() exit()
def get_input_positions_combined(source: str, use_iter: bool) -> tuple[dict, dict, dict, dict]:
if use_iter:
return get_input_positions_iter(source)
else:
return get_input_positions(source)
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(description='fetches the x/y positions from a dxf file', prog='getpositions') parser = argparse.ArgumentParser(description='fetches the x/y positions from a dxf file', prog='getpositions')
@@ -717,10 +733,7 @@ if __name__ == '__main__':
if args.sensors: if args.sensors:
# Sensoren auslesen # Sensoren auslesen
if use_iter: res_sens, res_schaltschrank_elemente, res_double, missing_attribs = get_input_positions_combined(dxf_path if use_iter else msp, use_iter)
res_sens, res_schaltschrank_elemente, res_double, missing_attribs = get_input_positions_iter(dxf_path)
else:
res_sens, res_schaltschrank_elemente, res_double, missing_attribs = get_input_positions(msp)
if args.errors and len(res_double) > 0: if args.errors and len(res_double) > 0:
print("Duplicate blocks found. Writing errors-file.") print("Duplicate blocks found. Writing errors-file.")
-180
View File
@@ -1,180 +0,0 @@
"""
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 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)
# --------------------------------------------------------- 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 extract_coords(planquadrat: str) -> tuple[float, float]:
"""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(",", "."))
def parse_merkmale(merkmale_str: str) -> dict:
"""Parst Merkmale-JSON-String in dict; bei Fehler → leeres Dict."""
try:
return json.loads(merkmale_str)
except json.JSONDecodeError:
return {}
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())
def build_simple_shape(msp, teileart: str, x: float, y: float,
teile_id: str, merkmale: dict):
"""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
cx1 = x - abstand / 2
cx2 = x + abstand / 2
# 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__":
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)
+30
View File
@@ -14,6 +14,10 @@ import shapely
# Globale Variable, die in main aufgerufen wird und steuert ob Graphen in unittests gezeichnet werden # Globale Variable, die in main aufgerufen wird und steuert ob Graphen in unittests gezeichnet werden
draw = False draw = False
class PointSorter: class PointSorter:
''' Klasse, die Punkte sortiert.
Die Punkte werden in der Liste self.points gespeichert.
Die Punkte werden sortiert nach x- und y-Koordinate.
'''
def __init__(self): def __init__(self):
self.points = [] self.points = []
@@ -34,6 +38,9 @@ def to_json(d, pretty: bool = True) -> str:
return json.dumps(d, indent=2 if pretty else None, default=str) #ensure_ascii false für darstellung von "ue" return json.dumps(d, indent=2 if pretty else None, default=str) #ensure_ascii false für darstellung von "ue"
class NodeIDs(): class NodeIDs():
''' Klasse, die Punkte verwaltet und NodeIDs zu Punkten zuordnet.
Die NodeIDs sind ganze Zahlen, die die Position der Punkte in der Liste self.points repräsentieren.
'''
def __init__(self, points=[]): def __init__(self, points=[]):
self._counter = 0 self._counter = 0
self._cord2id = dict() self._cord2id = dict()
@@ -41,6 +48,8 @@ class NodeIDs():
self.add_points(points) self.add_points(points)
def add_point(self, point:Point): def add_point(self, point:Point):
''' Fügt den Punkt unter einer neuen NodeId hinzu.
'''
if self.point_exists(point): if self.point_exists(point):
return True return True
self._counter += 1 self._counter += 1
@@ -78,6 +87,9 @@ class NodeIDs():
return len(self._cord2id.keys()) return len(self._cord2id.keys())
def get_points(self, nids:list[int]) -> list[Point]: def get_points(self, nids:list[int]) -> list[Point]:
''' Gibt zu einer Liste von NodeIDs die zugehörigen Punkte zurück.
Die Punkte werden in der gleichen Reihenfolge wie die NodeIDs zurückgegeben.
'''
ret = list() ret = list()
for n in nids: for n in nids:
c = self.get_point(n) c = self.get_point(n)
@@ -87,6 +99,9 @@ class NodeIDs():
def show(self): def show(self):
return self._id2cord return self._id2cord
class RackIDs(): class RackIDs():
''' Klasse, die Racks verwaltet und Rack-Racks und Rack-Equipment miteinander verbindet.
Die Racks werden als Linien in den STR-Baum eingefügt.
'''
def __init__(self, tol_snap = 200.0): def __init__(self, tol_snap = 200.0):
self._point2rack = dict() self._point2rack = dict()
self._rack2begend = dict() self._rack2begend = dict()
@@ -96,12 +111,18 @@ class RackIDs():
self._rack_tree = None self._rack_tree = None
def add_rack(self, beg:Point, end:Point, name:str): def add_rack(self, beg:Point, end:Point, name:str):
''' Fügt einen Rack hinzu.
Fügt die Anfangs- und Endpunkte des Racks zu den Racks hinzu.
'''
self.add_point_to_rack(beg, name) self.add_point_to_rack(beg, name)
self.add_point_to_rack(end, name) self.add_point_to_rack(end, name)
self._rack2begend[name] = (beg, end) # Anfangs und Endpunkte zu Rack Namen merken self._rack2begend[name] = (beg, end) # Anfangs und Endpunkte zu Rack Namen merken
def add_racks(self, racks:dict): def add_racks(self, racks:dict):
''' Fügt Racks hinzu.
Fügt die Anfangs- und Endpunkte der Racks zu den Racks hinzu.
'''
for name,v in racks.items(): for name,v in racks.items():
if len(v) == 2: if len(v) == 2:
self.add_rack(v[0], v[1], name) self.add_rack(v[0], v[1], name)
@@ -151,6 +172,9 @@ class RackIDs():
return ret_sorted return ret_sorted
def _build_rack_strtree(self): def _build_rack_strtree(self):
''' Erzeugt einen STR-Baum aus den Racks.
Die Racks werden als Linien in den STR-Baum eingefügt.
'''
self._rack_lines = [] self._rack_lines = []
self._rack_map = {} self._rack_map = {}
for r_name, pts in self.get_racks_borders().items(): for r_name, pts in self.get_racks_borders().items():
@@ -160,6 +184,9 @@ class RackIDs():
self._rack_tree = STRtree(self._rack_lines) self._rack_tree = STRtree(self._rack_lines)
def join_racks_str(self): def join_racks_str(self):
''' Verbindet Racks miteinander.
Die Racks werden als Linien in den STR-Baum eingefügt.
'''
if self._rack_tree is None: if self._rack_tree is None:
self._build_rack_strtree() self._build_rack_strtree()
@@ -193,6 +220,9 @@ class RackIDs():
self.add_rack(pt, snap_point, connrackname) self.add_rack(pt, snap_point, connrackname)
def rack_is_horizontal(self, name): def rack_is_horizontal(self, name):
''' Gibt True zurück, wenn der Rack horizontal ist.
False, wenn der Rack vertikal ist.
'''
[pa, pe] = self._rack2begend[name] [pa, pe] = self._rack2begend[name]
if pa.y == pe.y: if pa.y == pe.y:
return True return True
+97842
View File
File diff suppressed because it is too large Load Diff