From 84f8751eff87fcbf71abdc418a776fc3c6ed2897 Mon Sep 17 00:00:00 2001 From: mistangl Date: Fri, 23 Jan 2026 16:05:22 +0100 Subject: [PATCH] utils.py mit eingecheckt. Beispieldatei aus Config erzeugen. --- bin/create_example.bat | 4 + cfg/enumerate.cfg | 2 + lib/create_example.py | 154 +++++++++++++++++++++++++++++++-- lib/utils.py | 191 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 343 insertions(+), 8 deletions(-) create mode 100644 bin/create_example.bat create mode 100644 cfg/enumerate.cfg create mode 100644 lib/utils.py diff --git a/bin/create_example.bat b/bin/create_example.bat new file mode 100644 index 0000000..e446f0d --- /dev/null +++ b/bin/create_example.bat @@ -0,0 +1,4 @@ +@echo off +CALL manage_interpreter.bat activate_interpreter +python %PROJECT_LIB%\create_example.py %* +CALL manage_interpreter.bat deactivate_interpreter diff --git a/cfg/enumerate.cfg b/cfg/enumerate.cfg new file mode 100644 index 0000000..094fd51 --- /dev/null +++ b/cfg/enumerate.cfg @@ -0,0 +1,2 @@ +[Layers] +ILS_RENAMER diff --git a/lib/create_example.py b/lib/create_example.py index 3191b22..5aea6f2 100644 --- a/lib/create_example.py +++ b/lib/create_example.py @@ -10,6 +10,7 @@ für verschiedene Testfälle (Nummerierung, SPS, Erdung, etc.). import argparse import sys import os +import json from pathlib import Path import ezdxf from ezdxf.math import Vec3 @@ -17,16 +18,44 @@ from ezdxf.math import Vec3 # Umgebungsvariablen PROJECT = os.getenv('PROJECT', Path(__file__).parent.parent.absolute()) PROJECT_TEST = os.getenv('PROJECT_TEST', os.path.join(PROJECT, 'testdata')) +PROJECT_CFG = os.getenv('PROJECT_CFG', os.path.join(PROJECT, 'cfg')) class TestDataGenerator: """Generiert Test-DXF-Dateien für verschiedene Szenarien""" - def __init__(self, filename, test_type): + def __init__(self, filename, test_type, config_file=None): self.filename = filename self.test_type = test_type self.doc = None self.msp = None + self.config = None + + # Lade Config-Datei falls angegeben + if config_file: + self.load_config(config_file) + + def load_config(self, config_file): + """ + Lädt die JSON-Konfigurationsdatei + + Args: + config_file: Pfad zur JSON-Config-Datei + """ + config_path = Path(config_file) + if not config_path.is_absolute(): + config_path = Path(PROJECT_CFG) / config_file + + try: + with open(config_path, 'r', encoding='utf-8') as f: + self.config = json.load(f) + print(f"Config geladen: {config_path}") + except FileNotFoundError: + print(f"Warnung: Config-Datei nicht gefunden: {config_path}") + self.config = None + except json.JSONDecodeError as e: + print(f"Fehler beim Parsen der Config-Datei: {e}") + self.config = None def create_document(self): """Erstellt ein neues DXF-Dokument""" @@ -437,11 +466,7 @@ class TestDataGenerator: """ Generiert Testdaten für Nummerierung-Szenario - Layout: - - 3x MA-1@@ rechts oben nebeneinander - - 1x MA-1@@ links unten - - 3x MA-2@@ rechts unten (leicht versetzt) - - 3 Renamer-Rahmen (2x Rechteck, 1x Polylinie) + Verwendet entweder die JSON-Config oder Hard-coded Defaults """ print("Generiere Nummerierung-Testdaten...") @@ -452,6 +477,113 @@ class TestDataGenerator: print("Fehler: io-Block konnte nicht erstellt werden") return + # Wenn Config geladen ist, verwende sie, ansonsten Hard-coded Defaults + if self.config and 'test_scenes' in self.config and 'Nummerierung1' in self.config['test_scenes']: + print("Verwende JSON-Config für Nummerierung1...") + self._generate_nummerierung_from_config() + else: + print("Verwende Hard-coded Defaults für Nummerierung...") + self._generate_nummerierung_hardcoded() + + def _generate_ma_group(self, group, ma_defaults, width_per_char, fixed_height, spacing_factor, y_offsets_list): + """ + Generiert eine Gruppe von MA-Symbolen + + Args: + group: Dict mit Gruppen-Config (name, count, layout_type, base_x, base_y, spacing) + ma_defaults: Dict mit MA-Default-Attributen + width_per_char: Breite pro Zeichen + fixed_height: Fixe Symbol-Höhe + spacing_factor: Spacing-Faktor zwischen Symbolen + y_offsets_list: Liste von Y-Offsets für horizontal_offset Layout + """ + name = group['name'] + count = group['count'] + layout_type = group['layout_type'] + base_x = group['base_x'] + base_y = group['base_y'] + + # Extrahiere IO-Pattern aus Name (z.B. "MA-1@@_top" -> "MA-1@@") + io_pattern = name.split('_')[0] + + # Berechne Symbol-Ausdehnung + symbol_width = len(io_pattern) * width_per_char + + # Berechne Spacing + if layout_type in ['horizontal', 'horizontal_offset']: + actual_spacing = symbol_width * spacing_factor + else: + actual_spacing = 0 + + # Hole Attribute aus ma_defaults + attributes = ma_defaults.get('attributes', {}) + layer = ma_defaults.get('layer', 'ILS_MOTOR') + + # Generiere Symbole basierend auf Layout-Typ + for i in range(count): + if layout_type == 'single': + x, y = base_x, base_y + elif layout_type == 'horizontal': + x = base_x + i * actual_spacing + y = base_y + elif layout_type == 'horizontal_offset': + x = base_x + i * actual_spacing + y_offset = y_offsets_list[i % len(y_offsets_list)] + y = base_y + y_offset + else: + print(f"Unbekannter Layout-Typ: {layout_type}") + continue + + # Erstelle Block-Referenz + blockref = self.msp.add_blockref( + 'io', + insert=(x, y), + dxfattribs={'layer': layer} + ) + + # Setze Attribute (dynamisch IO und BEZEICHNUNG) + attrib_values = attributes.copy() + attrib_values['IO'] = io_pattern + attrib_values['BEZEICHNUNG'] = f"Motor {io_pattern}" + + blockref.add_auto_attribs(attrib_values) + + def _generate_nummerierung_from_config(self): + """Generiert Nummerierung-Testdaten aus JSON-Config""" + scene = self.config['test_scenes']['Nummerierung1'] + ma_defaults = self.config.get('ma_defaults', {}) + general = self.config.get('general', {}) + + # Hole Dimensions aus Config + dimensions = ma_defaults.get('dimensions', {}) + width_per_char = dimensions.get('width_per_char', 201.49) + fixed_height = dimensions.get('fixed_height', 380.94) + + # Hole Layout-Einstellungen + layout = general.get('layout', {}) + spacing_factor = layout.get('symbol_spacing_factor', 1.2) + y_offsets_list = layout.get('horizontal_offset_y_offsets', [0, -50, 50]) + + # Generiere MA-Gruppen aus Config + for group in scene.get('ma_groups', []): + self._generate_ma_group(group, ma_defaults, width_per_char, fixed_height, spacing_factor, y_offsets_list) + + # Generiere Renamer-Rahmen aus Config + for frame_config in scene.get('renaming_frames', []): + self.create_renamer_frame( + insert_point=(frame_config['x'], frame_config['y']), + width=frame_config['width'], + height=frame_config['height'], + name=frame_config['name'], + kennzeichnung=frame_config['kennzeichnung'], + layer_name=frame_config['layer_name'], + direction=frame_config['direction'], + use_polyline=frame_config.get('use_polyline', False) + ) + + def _generate_nummerierung_hardcoded(self): + """Generiert Nummerierung-Testdaten mit Hard-coded Werten (Fallback)""" + # Berechne Symbol-Ausdehnung für korrekte Positionierung ma1_width, ma1_height = self.calculate_symbol_extent('MA-1@@') ma2_width, ma2_height = self.calculate_symbol_extent('MA-2@@') @@ -535,7 +667,6 @@ class TestDataGenerator: 'ARTIKELBEZEICHN': 'E-Teile für SEW Motor ASE1-HAN10ES-BG', 'KENNZEICHNUNG': '=A01+UH00', 'TEXT-D': '', - 'TEXT-E': '', 'TEXT-ES': '', 'TEXT-F': '', 'ID': '', @@ -642,6 +773,13 @@ def main(): help='Ausgabeverzeichnis (Standard: testdata)' ) + parser.add_argument( + '--config', + type=str, + default='create_tests.json', + help='JSON-Konfigurationsdatei (Standard: create_tests.json)' + ) + args = parser.parse_args() # Stelle sicher, dass Dateiname .dxf Endung hat @@ -649,7 +787,7 @@ def main(): args.filename += '.dxf' # Generiere Testdaten - generator = TestDataGenerator(args.filename, args.type) + generator = TestDataGenerator(args.filename, args.type, config_file=args.config) if generator.generate(): generator.save(args.output_dir) diff --git a/lib/utils.py b/lib/utils.py new file mode 100644 index 0000000..22cd5cc --- /dev/null +++ b/lib/utils.py @@ -0,0 +1,191 @@ +""" +Common utility functions for the cable routing system. + +This module contains frequently used helper functions for: +- File and path operations +- Environment variable handling +- JSON operations +- DXF file handling +- Dictionary operations +""" + +import json +import os +import sys +from pathlib import Path +from typing import Any, Tuple + +import ezdxf +from ezdxf.lldxf.const import DXFStructureError + + +# ============================================================================ +# File Operations +# ============================================================================ + +def check_file_in_work(work_dir, filename) -> Tuple[Path, bool]: + """ + Check if a file exists, either at the given path or in the work directory. + + Args: + work_dir: Working directory to check if file not found at filename path (str or Path) + filename: Path to the file to check (str or Path) + + Returns: + Tuple of (resolved_path, exists_flag) + - resolved_path: Path where file was found (or would be) + - exists_flag: True if file exists, False otherwise + """ + work_dir = Path(work_dir) + filename = Path(filename) + + fexists = True + if not filename.exists(): + mypath = work_dir.joinpath(filename) + if not mypath.exists(): + fexists = False + else: + mypath = filename + return mypath, fexists + + +# ============================================================================ +# Environment Variable Operations +# ============================================================================ + +def check_environment_var(env_str: str) -> Path: + """ + Get and validate an environment variable as a Path. + + Args: + env_str: Name of the environment variable + + Returns: + Path object from the environment variable + + Exits: + Exits the program if the environment variable is not set or empty + """ + out_path = os.environ.get(env_str) + if out_path: + return Path(out_path) + else: + print(f"Umgebungsvariable {env_str} ist nicht gesetzt oder leer.") + exit() + + +# ============================================================================ +# JSON Operations +# ============================================================================ + +def to_json(d: Any, pretty: bool = True) -> str: + """ + Convert a Python object to a JSON string. + + Args: + d: Object to convert to JSON + pretty: If True, format with indentation for readability + + Returns: + JSON string representation of the object + + Note: + Uses ensure_ascii=False to properly display German umlauts (ä, ö, ü) + """ + return json.dumps(d, indent=2 if pretty else None, ensure_ascii=False, default=str) + + +def load_json(jsonfilename: str) -> dict: + """ + Load JSON data from a file. + + Args: + jsonfilename: Path to the JSON file + + Returns: + Parsed JSON data as a dictionary + """ + with open(jsonfilename, encoding='utf-8') as fh: + return json.load(fh) + + +def write_results(jsn_results: str, out_dir: Path, filename: str) -> None: + """ + Write JSON results to a file. + + Args: + jsn_results: JSON string to write + out_dir: Output directory path + filename: Name of the output file + """ + print("writing results to a json file ...") + outfile = Path(out_dir) / filename + with open(outfile, 'w', encoding='utf-8') as fh: + fh.write(jsn_results) + print("done") + + +# ============================================================================ +# DXF File Operations +# ============================================================================ + +def dxf_is_binary(dxf_path: Path) -> bool: + """ + Check if a DXF file is in binary format. + + Args: + dxf_path: Path to the DXF file + + Returns: + True if the file is a binary DXF, False otherwise + """ + with open(dxf_path, 'rb') as f: + header = f.read(22) + return b'AutoCAD Binary DXF' in header + + +def get_dxf_file(filepath: Path): + """ + Load a DXF file using ezdxf. + + Args: + filepath: Path to the DXF file + + Returns: + ezdxf Drawing object + + Exits: + Exits with code 1 for I/O errors + Exits with code 2 for invalid/corrupted DXF files + """ + try: + print("reading file ..", end='') + doc = ezdxf.filemanagement.readfile(filepath) + print("done") + except IOError: + print("Not a DXF file or a generic I/O error.") + sys.exit(1) + except DXFStructureError: + print("Invalid or corrupted DXF file.") + sys.exit(2) + return doc + + +# ============================================================================ +# Dictionary Operations +# ============================================================================ + +def merge_two_dicts(x: dict, y: dict) -> dict: + """ + Merge two dictionaries, with values from y overwriting those in x. + + Args: + x: First dictionary (base) + y: Second dictionary (override values) + + Returns: + New dictionary with merged values + """ + z = x.copy() + z.update(y) + return z