diff --git a/lib/create_example.py b/lib/create_example.py new file mode 100644 index 0000000..3191b22 --- /dev/null +++ b/lib/create_example.py @@ -0,0 +1,663 @@ +#!/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 +from pathlib import Path +import ezdxf +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')) + + +class TestDataGenerator: + """Generiert Test-DXF-Dateien für verschiedene Szenarien""" + + def __init__(self, filename, test_type): + self.filename = filename + self.test_type = test_type + self.doc = None + self.msp = 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': '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): + """ + 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', ...]) + + Returns: + Block-Definition + """ + if block_name in self.doc.blocks: + return self.doc.blocks[block_name] + + attrib_block = self.doc.blocks.new(block_name) + + # Definiere Standard-Positionen und Werte für Attribute + # Font-Größe auf ein Viertel reduziert (85.0 → 21.25) + attrib_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 attrib_definitions: + attdef = attrib_definitions[tag] + attrib_block.add_attdef( + tag=tag, + insert=attdef['insert'], + text=attdef['default'], + dxfattribs={ + 'height': attdef['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'): + """ + 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) + self.create_renamer_attrib_block(attrib_block_name, attrib_tags) + + # 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 - würde später für BG/MG/POT verwendet + # Placeholder für Multi-Layer Frame + pass + + # Füge Block-Referenz im Modelspace ein + blockref = self.msp.add_blockref( + outer_block_name, + insert=(x, y), + dxfattribs={'layer': 'ILS_RENAMER'} + ) + + def generate_nummerierung(self): + """ + 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) + """ + print("Generiere Nummerierung-Testdaten...") + + # 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 + + # 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@@') + + # Abstand zwischen Symbolen (20% der Breite als Puffer) + spacing = ma1_width * 1.2 + + # Definiere Positionen + # Rechts oben: 3x MA-1@@ nebeneinander + top_right_y = 1000 + top_right_x_start = 1500 + + for i in range(3): + x = top_right_x_start + i * spacing + blockref = self.msp.add_blockref( + 'io', + insert=(x, top_right_y), + dxfattribs={'layer': 'ILS_MOTOR'} + ) + # Erzeuge Attribut-Instanzen aus ATTDEFs mit Werten + blockref.add_auto_attribs({ + 'SPS': '1', + 'IO': 'MA-1@@', + 'VERW': 'Motor', + 'BEZEICHNUNG': 'Motor MA-1@@', + 'ARTIKELNR': '790902001', + 'ARTIKELBEZEICHN': 'E-Teile für SEW Motor ASE1-HAN10ES-BG', + 'KENNZEICHNUNG': '=A01+UH00', + 'TEXT-D': '', + 'TEXT-E': '', + 'TEXT-ES': '', + 'TEXT-F': '', + 'TEXT-I': '', + 'ID': '', + 'REALE_POSITION': 'x', + }) + + # Links unten: 1x MA-1@@ + left_bottom_x = 500 + left_bottom_y = 200 + blockref = self.msp.add_blockref( + 'io', + insert=(left_bottom_x, left_bottom_y), + dxfattribs={'layer': 'ILS_MOTOR'} + ) + blockref.add_auto_attribs({ + 'SPS': '1', + 'IO': 'MA-1@@', + 'VERW': 'Motor', + 'BEZEICHNUNG': 'Motor MA-1@@', + 'ARTIKELNR': '790902001', + 'ARTIKELBEZEICHN': 'E-Teile für SEW Motor ASE1-HAN10ES-BG', + 'KENNZEICHNUNG': '=A01+UH00', + 'TEXT-D': '', + 'TEXT-E': '', + 'TEXT-F': '', + 'TEXT-ES': '', + 'ID': '', + 'REALE_POSITION': 'x' + }) + + # Rechts unten: 3x MA-2@@ leicht versetzt + bottom_right_y_start = 400 + bottom_right_x_start = top_right_x_start + y_offsets = [0, -50, 50] # Leichte vertikale Versetzung (angepasst an Symbol-Höhe) + + for i in range(3): + x = bottom_right_x_start + i * spacing + y = bottom_right_y_start + y_offsets[i] + blockref = self.msp.add_blockref( + 'io', + insert=(x, y), + dxfattribs={'layer': 'ILS_MOTOR'} + ) + blockref.add_auto_attribs({ + 'SPS': '1', + 'IO': 'MA-2@@', + 'VERW': 'Motor', + 'BEZEICHNUNG': 'Motor MA-2@@', + 'ARTIKELNR': '790902001', + 'ARTIKELBEZEICHN': 'E-Teile für SEW Motor ASE1-HAN10ES-BG', + 'KENNZEICHNUNG': '=A01+UH00', + 'TEXT-D': '', + 'TEXT-E': '', + 'TEXT-ES': '', + 'TEXT-F': '', + 'ID': '', + 'REALE_POSITION': 'x' + }) + + # Renamer-Rahmen erstellen (mit angepassten Größen basierend auf Symbol-Ausdehnung) + # Puffer für Rahmen + frame_padding = 100 + + # Rahmen 1: Rechts oben (Rechteck) - umschließt die 3 MA-1@@ + frame1_width = 3 * spacing + ma1_width * 0.2 + frame1_height = ma1_height + 2 * frame_padding + self.create_renamer_frame( + insert_point=(top_right_x_start - frame_padding, top_right_y + frame_padding), + width=frame1_width, + height=frame1_height, + name='MA-1@@', + kennzeichnung='A01+UH00', + layer_name='ILS_MOTOR', + direction='LEFT_RIGHT', + use_polyline=False + ) + + # Rahmen 2: Links unten (Polylinie) - umschließt das einzelne MA-1@@ + # Schmaler machen - nur 15% breiter als das Symbol + frame2_padding = 50 # Kleinerer Puffer für schmalen Rahmen + frame2_width = ma1_width * 1.15 + frame2_height = ma1_height + 2 * frame2_padding + self.create_renamer_frame( + insert_point=(left_bottom_x - frame2_padding, left_bottom_y - frame2_padding), + width=frame2_width, + height=frame2_height, + name='MA-1@@', + kennzeichnung='A01+UH00', + layer_name='ILS_MOTOR', + direction='TOP_BOTTOM/LEFT_RIGHT', + use_polyline=True + ) + + # Rahmen 3: Rechts unten (Rechteck) - umschließt die 3 MA-2@@ + # Berücksichtige die vertikalen Offsets (max offset = 50) + frame3_width = 3 * spacing + ma2_width * 0.2 + frame3_height = ma2_height + 2 * frame_padding + 100 # Extra für y_offsets + self.create_renamer_frame( + insert_point=(bottom_right_x_start - frame_padding, bottom_right_y_start - frame_padding - 50), + width=frame3_width, + height=frame3_height, + name='MA-2@@', + kennzeichnung='A01+UH00', + layer_name='ILS_MOTOR', + direction='TOP_BOTTOM', + use_polyline=False + ) + + def generate(self): + """Generiert die Testdaten basierend auf dem Test-Typ""" + self.create_document() + + if self.test_type == 'nummerierung': + self.generate_nummerierung() + elif self.test_type == 'sps': + print("SPS-Testdaten noch nicht implementiert") + elif self.test_type == 'todraw': + print("ToDraw-Testdaten noch nicht implementiert") + elif self.test_type == 'erdung': + print("Erdung-Testdaten noch nicht implementiert") + elif self.test_type == 'translation': + print("Translation-Testdaten noch nicht implementiert") + elif self.test_type == 'hoehe': + print("Höhe-Testdaten noch nicht implementiert") + else: + print(f"Unbekannter Test-Typ: {self.test_type}") + return False + + return True + + +def main(): + """Hauptfunktion mit Argparse""" + parser = argparse.ArgumentParser( + description='Erzeugt Test-DXF-Dateien für verschiedene Testszenarien' + ) + + parser.add_argument( + '--filename', + type=str, + required=True, + help='Name der zu erzeugenden Testdatei (z.B. Nummerierung1.dxf)' + ) + + parser.add_argument( + '--type', + type=str, + required=True, + choices=['nummerierung', 'sps', 'todraw', 'erdung', 'translation', 'hoehe'], + help='Typ des Tests' + ) + + parser.add_argument( + '--output-dir', + type=str, + default=None, + help='Ausgabeverzeichnis (Standard: testdata)' + ) + + args = parser.parse_args() + + # Stelle sicher, dass Dateiname .dxf Endung hat + if not args.filename.endswith('.dxf'): + args.filename += '.dxf' + + # Generiere Testdaten + generator = TestDataGenerator(args.filename, args.type) + + 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())