utils.py mit eingecheckt. Beispieldatei aus Config erzeugen.
This commit is contained in:
+146
-8
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user