#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ create_example.py - Erzeugt Test-DXF-Dateien für verschiedene Testszenarien Dieses Skript generiert synthetische DXF-Testdateien mit definierten Layouts für verschiedene Testfälle (Nummerierung, SPS, Erdung, etc.). """ import argparse import sys import os import json from pathlib import Path import ezdxf # 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, scene_name, config_file=None): self.filename = filename self.scene_name = scene_name self.doc = None self.msp = None self.config = None self.generated_symbols = [] # Liste von (x, y, io_pattern, group_name) für Validierung # Lade Config-Datei (muss immer angegeben werden) 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"FEHLER: 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""" self.doc = ezdxf.new('R2010') self.msp = self.doc.modelspace() # Erstelle textstyle3 für die Attribute self.create_textstyle() # Erstelle Layer mit korrekten Farben self.create_layers() return self.doc def create_textstyle(self): """Erstellt den textstyle3 für Attribute""" if 'textstyle3' not in self.doc.styles: self.doc.styles.new('textstyle3', dxfattribs={ 'font': 'arial.ttf', 'width': 1.0 }) def create_layers(self): """ Erstellt alle benötigten Layer mit korrekten Farben Basiert auf Analyse von easy.dxf """ # Layer-Definitionen mit Farben layer_defs = [ {'name': '0', 'color': 7}, # Standard-Layer {'name': 'ILS_MOTOR', 'color': 7}, {'name': 'ILS_RENAMER', 'color': 1}, # Rot {'name': 'VALIDATION_ERROR', 'color': 1}, # Rot für Fehlerkreise {'name': 'TEXT-D', 'color': -7}, # Ausgeschaltet (negativ) {'name': 'TEXT-E', 'color': 7}, {'name': 'TEXT-ES', 'color': 7}, {'name': 'TEXT-F', 'color': 7}, {'name': 'TEXT-I', 'color': 7} ] for layer_def in layer_defs: if layer_def['name'] not in self.doc.layers: self.doc.layers.new(layer_def['name'], dxfattribs={ 'color': layer_def['color'] }) def calculate_symbol_extent(self, io_text): """ Berechnet die ungefähre Ausdehnung eines io-Symbols basierend auf der IO-Länge Basierend auf Analyse von IO=MA0062 (6 Zeichen): - Bounding Box: 1208.94 x 380.94 Einheiten - ~201.49 Einheiten Breite pro Zeichen - ~380.94 Einheiten Höhe (konstant) Args: io_text: Text des IO-Attributs (z.B. "MA-1@@") Returns: (width, height) in DXF-Einheiten """ char_count = len(io_text) width_per_char = 201.49 fixed_height = 380.94 width = char_count * width_per_char height = fixed_height return (width, height) def create_io_block(self): """ Erstellt den io-Block mit allen Attribut-Definitionen Basiert auf dem io-Block aus easy.dxf (IO=MA0062) Returns: Block-Definition """ if 'io' in self.doc.blocks: return self.doc.blocks['io'] # Erstelle neuen Block io_block = self.doc.blocks.new('io') # Attribut-Definitionen basierend auf ILS_Motor-790902001 aus Nummerierung_IO.dxf # invisible=True: ID, BEZEICHNUNG, KENNZEICHNUNG, ARTIKELNR, ARTIKELBEZEICHN, TEXT-I # invisible=False: IO, VERW, SPS, REALE_POSITION, TEXT-D, TEXT-E, TEXT-ES, TEXT-F attrib_defs = [ { 'tag': 'REALE_POSITION', 'insert': (0.0, 230.0, 0.0), 'height': 50.0, 'default': 'x', 'layer': 'ILS_MOTOR', 'invisible': False }, { 'tag': 'SPS', 'insert': (0.0, 130.37, 0.0), 'height': 125.0, 'default': '1', 'layer': 'ILS_MOTOR', 'invisible': False }, { 'tag': 'IO', 'insert': (103.88, 130.37, 0.0), 'height': 125.0, 'default': 'MA0000', 'layer': 'ILS_MOTOR', 'invisible': False }, { 'tag': 'TEXT-D', 'insert': (772.87, 110.44, 0.0), 'height': 63.75, 'default': '', 'layer': 'TEXT-D', 'invisible': False }, { 'tag': 'TEXT-E', 'insert': (772.87, 110.44, 0.0), 'height': 63.75, 'default': '', 'layer': 'TEXT-E', 'invisible': False }, { 'tag': 'TEXT-ES', 'insert': (772.87, 110.44, 0.0), 'height': 63.75, 'default': '', 'layer': 'TEXT-ES', 'invisible': False }, { 'tag': 'TEXT-F', 'insert': (772.87, 110.44, 0.0), 'height': 63.75, 'default': '', 'layer': 'TEXT-F', 'invisible': False }, { 'tag': 'TEXT-I', 'insert': (772.87, 110.44, 0.0), 'height': 63.75, 'default': '', 'layer': 'TEXT-I', 'invisible': True # Unsichtbar }, { 'tag': 'VERW', 'insert': (6.92, 20.78, 0.0), 'height': 80.0, 'default': 'ILS-M0000', 'layer': 'ILS_MOTOR', 'invisible': False }, { 'tag': 'ID', 'insert': (0.0, 0.73, 0.0), 'height': 120.0, 'default': '', 'layer': 'ILS_MOTOR', 'invisible': True # Unsichtbar }, { 'tag': 'BEZEICHNUNG', 'insert': (0.0, -67.05, 0.0), 'height': 63.75, 'default': 'Motor MA0000', 'layer': 'ILS_MOTOR', 'invisible': True # Unsichtbar }, { 'tag': 'KENNZEICHNUNG', 'insert': (0.0, -155.33, 0.0), 'height': 63.75, 'default': '=A01+UH00', 'layer': 'ILS_MOTOR', 'invisible': True # Unsichtbar }, { 'tag': 'ARTIKELNR', 'insert': (0.0, -248.90, 0.0), 'height': 63.75, 'default': '790902001', 'layer': 'ILS_MOTOR', 'invisible': True # Unsichtbar }, { 'tag': 'ARTIKELBEZEICHN', 'insert': (0.0, -332.68, 0.0), 'height': 63.75, 'default': 'E-Teile fuer SEW Motor ASE1 - HAN10ES - BG', 'layer': 'ILS_MOTOR', 'invisible': True # Unsichtbar } ] # Füge alle Attribut-Definitionen hinzu for attrib_def in attrib_defs: # Berechne flags: Bit 0 = invisible flags = 1 if attrib_def['invisible'] else 0 io_block.add_attdef( tag=attrib_def['tag'], insert=attrib_def['insert'], text=attrib_def['default'], dxfattribs={ 'height': attrib_def['height'], 'layer': attrib_def['layer'], 'color': 256, # BYLAYER 'style': 'textstyle3', 'rotation': 0, 'halign': 0, 'valign': 0, 'flags': flags } ) return io_block def save(self, output_dir=None): """Speichert das DXF-Dokument""" if output_dir is None: output_dir = PROJECT_TEST output_path = os.path.join(output_dir, self.filename) self.doc.saveas(output_path) print(f"Test-DXF erstellt: {output_path}") return output_path def load_block_from_file(self, source_file, block_name): """ Lädt einen Block aus einer anderen DXF-Datei Args: source_file: Pfad zur Quell-DXF-Datei block_name: Name des zu ladenden Blocks Returns: Block-Definition oder None """ try: source_doc = ezdxf.readfile(source_file) if block_name in source_doc.blocks: # Block in das Zieldokument kopieren source_block = source_doc.blocks.get(block_name) if block_name not in self.doc.blocks: new_block = self.doc.blocks.new(block_name) # Kopiere alle Entities aus dem Quellblock (inkl. ATTDEF) for entity in source_block: try: new_block.add_foreign_entity(entity, copy=True) except Exception as e: print(f"Warnung: Konnte Entity nicht kopieren: {e}") # Versuche direktes Klonen try: cloned = entity.copy() new_block.add_entity(cloned) except: pass return self.doc.blocks.get(block_name) else: print(f"Warnung: Block '{block_name}' nicht in {source_file} gefunden") return None except Exception as e: print(f"Fehler beim Laden von Block '{block_name}': {e}") import traceback traceback.print_exc() return None def create_renamer_attrib_block(self, block_name, attrib_tags, frame_type='MA'): """ Erstellt einen Block mit ATTDEF-Attributen für Renamer-Frames Args: block_name: Name des Attribut-Blocks (z.B. 'RENAMER_ATTRIB_MA') attrib_tags: Liste der Attribut-Tags (z.B. ['NAME', 'KENNZEICHNUNG', ...]) frame_type: 'MA' oder 'MULTI' für Frame-Typ Returns: Block-Definition """ if block_name in self.doc.blocks: return self.doc.blocks[block_name] attrib_block = self.doc.blocks.new(block_name) # Hole Positionen aus Config, falls vorhanden if self.config and 'general' in self.config: general = self.config['general'] if frame_type == 'MA' and 'ma_frame' in general: config_positions = general['ma_frame'].get('attdef_positions', {}) elif frame_type == 'MULTI' and 'multi_frame' in general: config_positions = general['multi_frame'].get('attdef_positions', {}) else: config_positions = {} else: config_positions = {} # Definiere Standard-Positionen und Werte für Attribute (Fallback) default_definitions = { 'NAME': {'insert': (10, 50), 'default': 'MA@@', 'height': 21.25}, 'NAME1': {'insert': (10, 80), 'default': 'BG1@@@', 'height': 21.25}, 'NAME2': {'insert': (10, 60), 'default': 'MB1@@@', 'height': 21.25}, 'NAME3': {'insert': (10, 40), 'default': '', 'height': 21.25}, 'KENNZEICHNUNG': {'insert': (10, 20), 'default': '=A01+UC0101', 'height': 21.25}, 'LAYER_NAME': {'insert': (10, 0), 'default': 'ILS_MOTOR', 'height': 21.25}, 'LAYER_NAME1': {'insert': (10, -20), 'default': 'ILS_Eingang', 'height': 21.25}, 'LAYER_NAME2': {'insert': (10, -40), 'default': 'ILS_Ausgang', 'height': 21.25}, 'LAYER_NAME3': {'insert': (10, -60), 'default': '', 'height': 21.25}, 'DIRECTION': {'insert': (10, -80), 'default': 'LEFT_RIGHT', 'height': 21.25}, 'ID': {'insert': (10, -100), 'default': '', 'height': 21.25} } # Füge nur die angeforderten Attribute hinzu for tag in attrib_tags: if tag in default_definitions: default_def = default_definitions[tag] # Verwende Config-Werte, falls vorhanden, sonst Defaults if tag in config_positions and not tag.startswith('_'): config_pos = config_positions[tag] x = config_pos.get('x', default_def['insert'][0]) y = config_pos.get('y', default_def['insert'][1]) height = config_pos.get('height', default_def['height']) else: x, y = default_def['insert'] height = default_def['height'] attrib_block.add_attdef( tag=tag, insert=(x, y), text=default_def['default'], dxfattribs={ 'height': height, 'layer': 'ILS_RENAMER', 'color': 1, 'style': 'textstyle3' } ) return attrib_block def create_renamer_frame(self, insert_point, width, height, name, kennzeichnung, layer_name, direction, use_polyline=False, frame_type='MA', name2='', name3='', layer_name2='', layer_name3='', id_value=''): """ Erstellt einen Renamer-Rahmen mit zweistufiger Block-Struktur auf Layer ILS_RENAMER Struktur: - Äußerer Block (RENAMER_xxx): 1. LWPOLYLINE (umschließende Polylinie) 2. Block-Referenz mit Attributen (RENAMER_ATTRIB_xxx) Args: insert_point: (x, y) Position des Rahmens width: Breite des Rahmens height: Höhe des Rahmens name: NAME-Attribut (oder NAME1 für Multi-Layer) kennzeichnung: KENNZEICHNUNG-Attribut layer_name: LAYER_NAME-Attribut (oder LAYER_NAME1 für Multi-Layer) direction: DIRECTION-Attribut use_polyline: Wird ignoriert (für Kompatibilität) frame_type: 'MA' für einfachen Frame, 'MULTI' für Multi-Layer Frame """ x, y = insert_point # Bestimme Attribut-Tags basierend auf Frame-Typ if frame_type == 'MA': attrib_tags = ['NAME', 'KENNZEICHNUNG', 'LAYER_NAME', 'DIRECTION'] attrib_block_name = 'RENAMER_ATTRIB_MA' else: # MULTI attrib_tags = ['NAME1', 'NAME2', 'NAME3', 'KENNZEICHNUNG', 'LAYER_NAME1', 'LAYER_NAME2', 'LAYER_NAME3', 'DIRECTION', 'ID'] attrib_block_name = 'RENAMER_ATTRIB_MULTI' # Erstelle Attribut-Block (wiederverwendbar) mit Config-Unterstützung self.create_renamer_attrib_block(attrib_block_name, attrib_tags, frame_type=frame_type) # Erstelle äußeren Renamer-Block mit eindeutigem Namen (inkl. Position und Größe) # Jeder Rahmen bekommt einen eindeutigen Namen basierend auf Position outer_block_name = f"RENAMER_{name.replace('@', '').replace('-', '_')}_{int(x)}_{int(y)}" if outer_block_name not in self.doc.blocks: outer_block = self.doc.blocks.new(outer_block_name) # 1. Füge LWPOLYLINE hinzu (umschließende Polylinie) points = [ (0, 0), (width, 0), (width, height), (0, height), (0, 0) ] outer_block.add_lwpolyline(points, dxfattribs={ 'layer': 'ILS_RENAMER', 'color': 1 }) # 2. Füge Block-Referenz mit Attributen hinzu # Position an der linken oberen Ecke des Rahmens attrib_insert_pos = (10, height - 30) # Linke obere Ecke mit kleinem Abstand attrib_blockref = outer_block.add_blockref( attrib_block_name, insert=attrib_insert_pos, dxfattribs={'layer': 'ILS_RENAMER'} ) # Setze Attribut-Werte auf der inneren Block-Referenz if frame_type == 'MA': attrib_blockref.add_auto_attribs({ 'NAME': name, 'KENNZEICHNUNG': kennzeichnung, 'LAYER_NAME': layer_name, 'DIRECTION': direction }) else: # MULTI - für BG/MG/POT mit mehreren Layern attrib_blockref.add_auto_attribs({ 'NAME1': name, 'NAME2': name2, 'NAME3': name3, 'KENNZEICHNUNG': kennzeichnung, 'LAYER_NAME1': layer_name, 'LAYER_NAME2': layer_name2, 'LAYER_NAME3': layer_name3, 'DIRECTION': direction, 'ID': id_value }) # Füge Block-Referenz im Modelspace ein blockref = self.msp.add_blockref( outer_block_name, insert=(x, y), dxfattribs={'layer': 'ILS_RENAMER'} ) def create_polyline_renamer_frame(self, path_points, name, kennzeichnung, layer_name, direction): """ Erstellt einen Renamer-Rahmen mit Polyline-Pfad Struktur: - Block (RENAMER_POLYLINE_xxx): 1. LWPOLYLINE (Pfad durch die Punkte) 2. Block-Referenz mit Attributen (RENAMER_ATTRIB_MA) Args: path_points: Liste von Dictionaries mit 'x' und 'y' Koordinaten name: NAME-Attribut kennzeichnung: KENNZEICHNUNG-Attribut layer_name: LAYER_NAME-Attribut direction: DIRECTION-Attribut (z.B. 'POLYLINE_PATH') """ # Erstelle Attribut-Block (wiederverwendbar) attrib_tags = ['NAME', 'KENNZEICHNUNG', 'LAYER_NAME', 'DIRECTION'] attrib_block_name = 'RENAMER_ATTRIB_MA' self.create_renamer_attrib_block(attrib_block_name, attrib_tags, frame_type='MA') # Erstelle eindeutigen Block-Namen basierend auf erstem Punkt first_point = path_points[0] if path_points else {'x': 0, 'y': 0} block_name = f"RENAMER_POLYLINE_{name.replace('@', '').replace('-', '_')}_{int(first_point['x'])}_{int(first_point['y'])}" if block_name not in self.doc.blocks: renamer_block = self.doc.blocks.new(block_name) # 1. Füge LWPOLYLINE hinzu (Pfad) # Konvertiere Punkte von Dict zu Tupeln points = [(p['x'], p['y']) for p in path_points] renamer_block.add_lwpolyline(points, dxfattribs={ 'layer': 'ILS_RENAMER', 'color': 1 }) # 2. Füge Block-Referenz mit Attributen hinzu # Position neben dem ersten Punkt des Pfades (mit kleinem Offset) first_x = points[0][0] first_y = points[0][1] attrib_insert_pos = (first_x + 10, first_y + 30) # Offset vom ersten Punkt attrib_blockref = renamer_block.add_blockref( attrib_block_name, insert=attrib_insert_pos, dxfattribs={'layer': 'ILS_RENAMER'} ) # Setze Attribut-Werte attrib_blockref.add_auto_attribs({ 'NAME': name, 'KENNZEICHNUNG': kennzeichnung, 'LAYER_NAME': layer_name, 'DIRECTION': direction }) # Füge Block-Referenz im Modelspace ein (am Ursprung, da Pfad absolute Koordinaten hat) blockref = self.msp.add_blockref( block_name, insert=(0, 0), dxfattribs={'layer': 'ILS_RENAMER'} ) def generate_nummerierung(self, scene_name='Nummerierung1'): """ Generiert Testdaten für Nummerierung-Szenario Verwendet entweder die JSON-Config oder Hard-coded Defaults Args: scene_name: Name der Testszene (z.B. 'Nummerierung1', 'Nummerierung2') """ print(f"Generiere Nummerierung-Testdaten ({scene_name})...") # Erstelle io-Block direkt (ohne Referenzdatei) io_block = self.create_io_block() if io_block is None: 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 scene_name in self.config['test_scenes']: print(f"Verwende JSON-Config für {scene_name}...") self._generate_nummerierung_from_config(scene_name) else: print("Fehler in Config ... terminating") def _calculate_insert_offset(self, insert_point, symbol_width, symbol_height): """ Berechnet den Offset vom Einfügepunkt zur linken oberen Ecke Args: insert_point: String mit dem Einfügepunkt (z.B. "left_top_corner", "center") symbol_width: Breite des Symbols symbol_height: Höhe des Symbols Returns: (offset_x, offset_y) - Offset vom Einfügepunkt zur linken oberen Ecke """ offset_map = { 'left_top_corner': (0, 0), 'left_bottom_corner': (0, symbol_height), 'right_top_corner': (-symbol_width, 0), 'right_bottom_corner': (-symbol_width, symbol_height), 'center': (-symbol_width / 2, symbol_height / 2), 'top_center': (-symbol_width / 2, 0), 'bottom_center': (-symbol_width / 2, symbol_height) } return offset_map.get(insert_point, (0, 0)) # Default: left_top_corner def _generate_ma_group(self, group, ma_defaults, width_per_char, fixed_height, spacing_factor, y_offsets_list, insert_point='left_top_corner'): """ 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 insert_point: Einfügepunkt des Blocks (z.B. "left_top_corner", "center") """ 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') # Berechne Offset basierend auf Einfügepunkt offset_x, offset_y = self._calculate_insert_offset(insert_point, symbol_width, fixed_height) # 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 # Berechne die linke obere Ecke basierend auf dem Einfügepunkt left_top_x = x + offset_x left_top_y = y + offset_y # DXF-Block wird an der linken unteren Ecke eingefügt # (da der io-Block bei (0,0) startet und nach oben geht) blockref = self.msp.add_blockref( 'io', insert=(left_top_x, left_top_y - fixed_height), 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) # Speichere Symbol-Position für spätere Validierung # Speichere immer die linke obere Ecke self.generated_symbols.append({ 'x': left_top_x, 'y': left_top_y, 'io': io_pattern, 'group_name': name, 'layer': layer }) def _generate_nummerierung_from_config(self, scene_name='Nummerierung1'): """ Generiert Nummerierung-Testdaten aus JSON-Config Args: scene_name: Name der Testszene in der Config """ scene = self.config['test_scenes'][scene_name] 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]) # Hole Einfügepunkt aus Config (default: left_top_corner) ma_frame = general.get('ma_frame', {}) insert_point = ma_frame.get('insert_point', 'left_top_corner') # 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, insert_point) # Generiere Renamer-Rahmen aus Config for frame_config in scene.get('renaming_frames', []): frame_type = frame_config.get('type', 'rectangle') if frame_type == 'polyline_path': # Polyline-Pfad Frame self.create_polyline_renamer_frame( path_points=frame_config['path_points'], name=frame_config['name'], kennzeichnung=frame_config['kennzeichnung'], layer_name=frame_config['layer_name'], direction=frame_config['direction'] ) else: # Standard Rectangle Frame # Prüfe ob es ein MULTI-Frame ist (NAME1/NAME2 statt NAME) is_multi_frame = 'name2' in frame_config or 'layer_name1' in frame_config if is_multi_frame: # MULTI-Frame (BG, MG, POT mit mehreren Layern) self.create_renamer_frame( insert_point=(frame_config['x'], frame_config['y']), width=frame_config['width'], height=frame_config['height'], name=frame_config.get('name', ''), kennzeichnung=frame_config.get('kennzeichnung', ''), layer_name=frame_config.get('layer_name1', ''), direction=frame_config.get('direction', ''), use_polyline=frame_config.get('use_polyline', False), frame_type='MULTI', name2=frame_config.get('name2', ''), name3=frame_config.get('name3', ''), layer_name2=frame_config.get('layer_name2', ''), layer_name3=frame_config.get('layer_name3', ''), id_value=frame_config.get('id', '') ) else: # MA-Frame (einfacher Frame) 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) ) # Prüfe ob alle generierten Symbole in einem Renamer-Frame liegen self._validate_symbols_in_frames(scene) def _point_in_rectangle(self, point, rect_x, rect_y, rect_width, rect_height): """ Prüft ob ein Punkt innerhalb eines Rechtecks liegt. Args: point: (x, y) Tupel rect_x, rect_y: Linke obere Ecke des Rechtecks rect_width, rect_height: Breite und Höhe des Rechtecks """ x, y = point return (rect_x <= x <= rect_x + rect_width and rect_y <= y <= rect_y + rect_height) def _calculate_shift_to_rectangle(self, point, rect_x, rect_y, rect_width, rect_height): """ Berechnet den minimalen Verschiebungsvektor, um einen Punkt in ein Rechteck zu verschieben. Args: point: (x, y) Tupel rect_x, rect_y: Linke untere Ecke des Rechtecks rect_width, rect_height: Breite und Höhe des Rechtecks Returns: (delta_x, delta_y) - Verschiebungsvektor """ x, y = point delta_x = 0 delta_y = 0 # Berechne Verschiebung in x-Richtung if x < rect_x: delta_x = rect_x - x elif x > rect_x + rect_width: delta_x = (rect_x + rect_width) - x # Berechne Verschiebung in y-Richtung if y < rect_y: delta_y = rect_y - y elif y > rect_y + rect_height: delta_y = (rect_y + rect_height) - y return (delta_x, delta_y) def _point_in_polygon(self, point, polygon): """ Prüft, ob ein Punkt innerhalb eines Polygons liegt (Ray-casting Algorithmus). """ x, y = point n = len(polygon) inside = False p1x, p1y = polygon[0] for i in range(1, n + 1): p2x, p2y = polygon[i % n] if y > min(p1y, p2y): if y <= max(p1y, p2y): if x <= max(p1x, p2x): if p1y != p2y: xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x if p1x == p2x or x <= xinters: inside = not inside p1x, p1y = p2x, p2y return inside def _circle_intersects_polyline(self, center, radius, polyline): """ Prüft ob ein Kreis mit Mittelpunkt 'center' und Radius 'radius' irgendein Segment der Polylinie schneidet. Args: center: (x, y) Mittelpunkt des Kreises radius: Radius des Kreises polyline: Liste von (x, y) Punkten der Polylinie Returns: True wenn der Kreis die Polylinie schneidet """ cx, cy = center # Prüfe jedes Segment der Polylinie for i in range(len(polyline) - 1): p1 = polyline[i] p2 = polyline[i + 1] # Berechne den kürzesten Abstand vom Kreis-Mittelpunkt zum Linien-Segment dist = self._distance_point_to_segment(center, p1, p2) # Wenn Abstand <= Radius, dann gibt es eine Schnittmenge if dist <= radius: return True return False def _distance_point_to_segment(self, point, seg_start, seg_end): """ Berechnet den kürzesten Abstand von einem Punkt zu einem Linien-Segment. Args: point: (x, y) Punkt seg_start: (x, y) Start des Segments seg_end: (x, y) Ende des Segments Returns: Kürzester Abstand """ px, py = point x1, y1 = seg_start x2, y2 = seg_end # Vektor von seg_start zu seg_end dx = x2 - x1 dy = y2 - y1 # Spezialfall: Segment ist ein Punkt if dx == 0 and dy == 0: return ((px - x1)**2 + (py - y1)**2)**0.5 # Parameter t für die Projektion des Punktes auf die Linie # t = 0 bedeutet seg_start, t = 1 bedeutet seg_end t = max(0, min(1, ((px - x1) * dx + (py - y1) * dy) / (dx * dx + dy * dy))) # Nächster Punkt auf dem Segment closest_x = x1 + t * dx closest_y = y1 + t * dy # Abstand vom Punkt zum nächsten Punkt auf dem Segment return ((px - closest_x)**2 + (py - closest_y)**2)**0.5 def _validate_symbols_in_frames(self, scene): """ Prüft ob alle generierten Symbole aus ma_groups in mindestens einem Renamer-Frame liegen. Für POLYLINE_PATH Frames: Prüft ob die Polylinie einen Kreis um das Symbol schneidet Für andere Frames: Prüft ob das Symbol innerhalb des Frames liegt Gibt Warnungen aus für Symbole, die nicht die Kriterien erfüllen. """ if not self.generated_symbols: return print("\n" + "="*60) print("Prüfe ob alle generierten Symbole in Renamer-Frames liegen") print("="*60) # Hole Symbol-Dimensionen aus Config dimensions = self.config.get('ma_defaults', {}).get('dimensions', {}) width_per_char = dimensions.get('width_per_char', 201.49) fixed_height = dimensions.get('fixed_height', 380.94) # Sammle alle Frame-Boundaries frame_boundaries = [] for frame_config in scene.get('renaming_frames', []): frame_type = frame_config.get('type', 'rectangle') direction = frame_config.get('direction', '') layer_name = frame_config.get('layer_name', '') if frame_type == 'polyline_path' or direction == 'POLYLINE_PATH': # Polyline-Pfad: verwende path_points direkt path_points = frame_config.get('path_points', []) if path_points: polyline = [(p['x'], p['y']) for p in path_points] frame_boundaries.append({ 'validation_type': 'polyline_path', 'polyline': polyline, 'name': frame_config.get('name', 'UNKNOWN'), 'layer_name': layer_name, 'direction': direction }) else: # Rechteck: berechne Eckpunkte rect_x = frame_config.get('x', 0) rect_y = frame_config.get('y', 0) rect_width = frame_config.get('width', 0) rect_height = frame_config.get('height', 0) # Erstelle Rechteck als Polygon (4 Eckpunkte) polygon = [ (rect_x, rect_y), (rect_x + rect_width, rect_y), (rect_x + rect_width, rect_y + rect_height), (rect_x, rect_y + rect_height) ] frame_boundaries.append({ 'validation_type': 'containment', 'boundary': polygon, 'rect_x': rect_x, 'rect_y': rect_y, 'rect_width': rect_width, 'rect_height': rect_height, 'name': frame_config.get('name', 'UNKNOWN'), 'layer_name': layer_name, 'direction': direction }) # Prüfe jedes generierte Symbol symbols_outside = [] for symbol in self.generated_symbols: # Berechne Symbol-Dimensionen io_text = symbol['io'] symbol_width = len(io_text) * width_per_char symbol_height = fixed_height # Berechne Mittelpunkt des Symbols # symbol['x'], symbol['y'] ist die linke obere Ecke symbol_center_x = symbol['x'] + symbol_width / 2 symbol_center_y = symbol['y'] - symbol_height / 2 symbol_point = (symbol_center_x, symbol_center_y) # Radius für Kreis-Test symbol_radius = max(symbol_width, symbol_height) / 2 in_any_frame = False validation_type_used = None closest_frame = None min_shift_distance = float('inf') best_shift = (0, 0) for frame in frame_boundaries: # Prüfe ob Symbol auf dem richtigen Layer ist if frame['layer_name'] and symbol['layer'] != frame['layer_name']: continue # Speichere den Validierungstyp für Fehlermeldung if validation_type_used is None: validation_type_used = frame['validation_type'] # Wähle Validierungsmethode basierend auf Frame-Typ if frame['validation_type'] == 'polyline_path': # POLYLINE_PATH: Prüfe ob Polylinie den Kreis um das Symbol schneidet if self._circle_intersects_polyline(symbol_point, symbol_radius, frame['polyline']): in_any_frame = True break # Für Polyline-Pfade: Berechne minimalen Abstand zur Polylinie # (vereinfacht: nicht implementiert, da komplex) else: # Containment: Prüfe ob Symbol innerhalb des Frames liegt if self._point_in_rectangle( symbol_point, frame['rect_x'], frame['rect_y'], frame['rect_width'], frame['rect_height'] ): in_any_frame = True break # Berechne Verschiebung für diesen Frame shift_x, shift_y = self._calculate_shift_to_rectangle( symbol_point, frame['rect_x'], frame['rect_y'], frame['rect_width'], frame['rect_height'] ) shift_distance = (shift_x**2 + shift_y**2)**0.5 # Speichere den nächsten Frame if shift_distance < min_shift_distance: min_shift_distance = shift_distance best_shift = (shift_x, shift_y) closest_frame = frame if not in_any_frame: symbol_with_type = symbol.copy() symbol_with_type['validation_type'] = validation_type_used symbol_with_type['symbol_radius'] = symbol_radius symbol_with_type['shift_x'] = best_shift[0] symbol_with_type['shift_y'] = best_shift[1] symbol_with_type['closest_frame'] = closest_frame.get('name', 'UNKNOWN') if closest_frame else 'UNKNOWN' symbols_outside.append(symbol_with_type) # Gib Warnungen aus und zeichne Fehlerkreise if symbols_outside: print(f"\nWARNUNG: {len(symbols_outside)} Symbol(e) erfüllen nicht die Frame-Kriterien:") for symbol in symbols_outside: base_msg = (f" Symbol IO='{symbol['io']}' aus Gruppe '{symbol['group_name']}' " f"an Position ({symbol['x']:.1f}, {symbol['y']:.1f}) " f"auf Layer '{symbol['layer']}'") if symbol.get('validation_type') == 'polyline_path': print(f"{base_msg}: " f"Die Polylinie schneidet nicht den Umkreis des Symbols " f"(Radius: {symbol['symbol_radius']:.1f})") else: shift_x = symbol.get('shift_x', 0) shift_y = symbol.get('shift_y', 0) closest_frame = symbol.get('closest_frame', 'UNKNOWN') if shift_x != 0 or shift_y != 0: print(f"{base_msg}: " f"Symbol liegt nicht innerhalb des Renamer-Rahmens '{closest_frame}'. " f"Verschieben Sie um dx={shift_x:.1f}, dy={shift_y:.1f}") else: print(f"{base_msg}: " f"Symbol liegt nicht innerhalb des Renamer-Rahmens") # Zeichne Fehlerkreis um das Symbol self._draw_error_circle(symbol['x'], symbol['y'], symbol['symbol_radius']) else: print(f"\nOK: Alle {len(self.generated_symbols)} generierten Symbole erfüllen die Frame-Kriterien") def _draw_error_circle(self, center_x, center_y, radius): """ Zeichnet einen roten Kreis um ein fehlerhaftes Symbol in die DXF-Datei. Args: center_x: X-Koordinate des Kreismittelpunkts center_y: Y-Koordinate des Kreismittelpunkts radius: Radius des Kreises """ self.msp.add_circle( center=(center_x, center_y), radius=radius, dxfattribs={ 'layer': 'VALIDATION_ERROR', 'color': 1 # Rot } ) def generate(self): """Generiert die Testdaten basierend auf dem Test-Typ""" self.create_document() # Prüfe ob Scene in Config existiert if not self.config: print("FEHLER: Keine Config-Datei geladen") return False if 'test_scenes' not in self.config: print("FEHLER: Config enthält keinen 'test_scenes' Abschnitt") return False if self.scene_name not in self.config['test_scenes']: print(f"FEHLER: Scene '{self.scene_name}' nicht in Config gefunden") print(f"Verfügbare Scenes: {', '.join(self.config['test_scenes'].keys())}") return False # Generiere nur Nummerierung (andere Test-Typen werden später unterstützt) self.generate_nummerierung(self.scene_name) return True def main(): """Hauptfunktion mit Argparse""" # Versuche Config zu lesen für --help Ausgabe default_config = 'create_tests.json' config_path = Path(PROJECT_CFG) / default_config available_scenes = [] scene_descriptions = {} if config_path.exists(): try: with open(config_path, 'r', encoding='utf-8') as f: config = json.load(f) if 'test_scenes' in config: for scene_name, scene_data in config['test_scenes'].items(): available_scenes.append(scene_name) if 'description' in scene_data: scene_descriptions[scene_name] = scene_data['description'] except Exception: pass # Ignoriere Fehler beim Lesen für --help # Erstelle Epilog mit verfügbaren Szenen epilog_text = "\nVerfügbare Test-Szenen:\n" if available_scenes: for scene in sorted(available_scenes): desc = scene_descriptions.get(scene, '') if desc: epilog_text += f" {scene}: {desc}\n" else: epilog_text += f" {scene}\n" epilog_text += f"\n(aus Konfigurationsdatei: {config_path})" else: epilog_text += " (keine Szenen konfiguriert oder Config-Datei nicht gefunden)\n" epilog_text += f" Standard-Pfad: {config_path}" parser = argparse.ArgumentParser( description='Erzeugt Test-DXF-Dateien für verschiedene Testszenarien', epilog=epilog_text, formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument( '--scene', type=str, required=True, help='Name der Testszene (z.B. Nummerierung1, Nummerierung2). Muss in der Config existieren.' ) parser.add_argument( '--filename', type=str, default=None, help='Name der zu erzeugenden Testdatei (z.B. Nummerierung1.dxf). Falls nicht angegeben, wird scene + .dxf verwendet.' ) parser.add_argument( '--output-dir', type=str, default=None, help='Ausgabeverzeichnis (Standard: testdata)' ) parser.add_argument( '--config', type=str, default=default_config, help=f'JSON-Konfigurationsdatei (Standard: {default_config})' ) args = parser.parse_args() # Bestimme Filename: entweder angegeben oder aus scene ableiten if args.filename: filename = args.filename else: filename = args.scene + '.dxf' # Stelle sicher, dass Dateiname .dxf Endung hat if not filename.endswith('.dxf'): filename += '.dxf' # Generiere Testdaten generator = TestDataGenerator(filename, args.scene, config_file=args.config) if generator.generate(): generator.save(args.output_dir) return 0 else: print("Fehler bei der Generierung der Testdaten") return 1 if __name__ == '__main__': sys.exit(main())