utils.py mit eingecheckt. Beispieldatei aus Config erzeugen.

This commit is contained in:
2026-01-23 16:05:22 +01:00
parent d55d71d623
commit 84f8751eff
4 changed files with 343 additions and 8 deletions
+146 -8
View File
@@ -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)
+191
View File
@@ -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