Files
kabellaengen/lib/create_example.py
T

1312 lines
51 KiB
Python

#!/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
from symbol_frames import draw_symbol_frames
# Umgebungsvariablen
PROJECT = os.getenv('PROJECT', Path(__file__).parent.parent.absolute())
PROJECT_WORK = os.getenv('PROJECT_WORK', os.path.join(PROJECT, 'work'))
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_WORK
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 draw_frames_for_generated_symbols(self):
"""
Zeichnet Rahmen um alle generierten Symbole zur Visualisierung.
Konvertiert das interne Format in das von draw_symbol_frames erwartete Format.
"""
if not self.generated_symbols:
print("Keine generierten Symbole zum Zeichnen von Rahmen gefunden")
return 0
# Konvertiere das interne Format in das erwartete Format
symbols_for_frames = []
for sym in self.generated_symbols:
# generated_symbols enthält 'x', 'y' (Einfügepunkt = linke untere Ecke bei left_bottom_corner)
# draw_symbol_frames erwartet Position = linke untere Ecke des Symbols
# Wir können die gespeicherten Koordinaten direkt verwenden
symbols_for_frames.append({
'position': (sym['x'], sym['y']), # Position = linke untere Ecke
'attributes': {'IO': sym['io']}, # IO-Wert für Breitenberechnung
'layer': sym.get('layer', 'ILS_MOTOR')
})
# Zeichne die Rahmen
return draw_symbol_frames(self.doc, self.msp, symbols_for_frames)
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_symbol_group(self, group, symbol_defaults, symbol_type='MA', spacing_factor=1.2, y_offsets_list=[0, -50, 50], insert_point='left_bottom_corner'):
"""
Generiert eine Gruppe von Symbolen (MA, BG, MB, POT, etc.)
Args:
group: Dict mit Gruppen-Config (name, count, layout_type, base_x, base_y, spacing)
symbol_defaults: Dict mit Symbol-Default-Attributen (ma_defaults, bg_defaults, etc.)
symbol_type: Typ des Symbols ('MA', 'BG', 'MB', 'POT') für BEZEICHNUNG
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_bottom_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]
# Hole Dimensionen aus defaults
dimensions = symbol_defaults.get('dimensions', {})
symbol_width = dimensions.get('width', 1210)
symbol_height = dimensions.get('height', 381)
# Berechne Spacing
if layout_type in ['horizontal', 'horizontal_offset']:
actual_spacing = symbol_width * spacing_factor
else:
actual_spacing = 0
# Hole Attribute und Block-Info aus defaults
attributes = symbol_defaults.get('attributes', {})
layer = symbol_defaults.get('layer', 'ILS_MOTOR')
block_name = symbol_defaults.get('block_name', 'io')
# Berechne Offset basierend auf Einfügepunkt
offset_x, offset_y = self._calculate_insert_offset(insert_point, symbol_width, symbol_height)
# Bestimme BEZEICHNUNG basierend auf Symbol-Typ
bezeichnung_prefix = {
'MA': 'Motor',
'BG': 'Eingang',
'MB': 'Ausgang',
'POT': 'Erdung'
}.get(symbol_type, 'Symbol')
# 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(
block_name,
insert=(left_top_x, left_top_y - symbol_height),
dxfattribs={'layer': layer}
)
# Setze Attribute (dynamisch IO und BEZEICHNUNG)
attrib_values = attributes.copy()
attrib_values['IO'] = io_pattern
# Setze BEZEICHNUNG nur wenn im Template vorhanden
if 'BEZEICHNUNG' in attrib_values:
attrib_values['BEZEICHNUNG'] = f"{bezeichnung_prefix} {io_pattern}"
blockref.add_auto_attribs(attrib_values)
# Speichere Symbol-Position für spätere Validierung
# Speichere den tatsächlichen Einfügepunkt (linke untere Ecke bei left_bottom_corner)
actual_insert_x = left_top_x
actual_insert_y = left_top_y - symbol_height
self.generated_symbols.append({
'x': actual_insert_x,
'y': actual_insert_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]
general = self.config.get('general', {})
# 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_bottom_corner)
ma_frame = general.get('ma_frame', {})
insert_point = ma_frame.get('insert_point', 'left_bottom_corner')
# Generiere MA-Gruppen aus Config
ma_defaults = self.config.get('ma_defaults', {})
for group in scene.get('ma_groups', []):
self._generate_symbol_group(group, ma_defaults, symbol_type='MA',
spacing_factor=spacing_factor,
y_offsets_list=y_offsets_list,
insert_point=insert_point)
# Generiere BG-Gruppen aus Config (Eingänge)
bg_defaults = self.config.get('bg_defaults', {})
for group in scene.get('bg_groups', []):
self._generate_symbol_group(group, bg_defaults, symbol_type='BG',
spacing_factor=spacing_factor,
y_offsets_list=y_offsets_list,
insert_point=insert_point)
# Generiere MB-Gruppen aus Config (Ausgänge)
mb_defaults = self.config.get('mb_defaults', {})
for group in scene.get('mb_groups', []):
self._generate_symbol_group(group, mb_defaults, symbol_type='MB',
spacing_factor=spacing_factor,
y_offsets_list=y_offsets_list,
insert_point=insert_point)
# Generiere POT-Gruppen aus Config (Erdungspunkte)
pot_defaults = self.config.get('pot_defaults', {})
for group in scene.get('pot_groups', []):
self._generate_symbol_group(group, pot_defaults, symbol_type='POT',
spacing_factor=spacing_factor,
y_offsets_list=y_offsets_list,
insert_point=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 (feste Werte)
dimensions = self.config.get('ma_defaults', {}).get('dimensions', {})
ma_symbol_width = dimensions.get('width', 1210)
ma_symbol_height = dimensions.get('height', 381)
# Hole Multi-Frame Dimensionen
multi_dimensions = self.config.get('general', {}).get('multi_frame', {}).get('dimensions', {})
multi_symbol_width = multi_dimensions.get('width', 1410)
multi_symbol_height = multi_dimensions.get('height', 381)
# 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 basierend auf IO-Text Länge
io_text = symbol['io']
char_count = len(io_text)
if char_count >= 7:
symbol_width = multi_symbol_width # Multi-Frame (BG/MB/POT)
symbol_height = multi_symbol_height
else:
symbol_width = ma_symbol_width # MA-Frame
symbol_height = ma_symbol_height
# Berechne Mittelpunkt des Symbols
# symbol['x'], symbol['y'] ist die linke untere 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_name'] = closest_frame.get('name', 'UNKNOWN') if closest_frame else 'UNKNOWN'
symbol_with_type['closest_frame_dict'] = closest_frame # Speichere das gesamte Dictionary
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_name = symbol.get('closest_frame_name', 'UNKNOWN')
if shift_x != 0 or shift_y != 0:
print(f"{base_msg}: "
f"Symbol liegt nicht innerhalb des Renamer-Rahmens '{closest_frame_name}'. "
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
# Berechne Symbol-Zentrum (symbol['x'], symbol['y'] = linke untere Ecke)
io_text = symbol['io']
char_count = len(io_text)
if char_count >= 7:
sym_width = multi_symbol_width
sym_height = multi_symbol_height
else:
sym_width = ma_symbol_width
sym_height = ma_symbol_height
symbol_center_x = symbol['x'] + sym_width / 2
symbol_center_y = symbol['y'] + sym_height / 2
self._draw_error_circle(symbol_center_x, symbol_center_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})'
)
parser.add_argument(
'--show_symbol_frames',
action='store_true',
help='Zeichne Rahmen um jedes Symbol, um die Grenze des Symbols zu visualisieren'
)
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():
# Zeichne Symbol-Rahmen wenn gewünscht
if args.show_symbol_frames:
generator.draw_frames_for_generated_symbols()
generator.save(args.output_dir)
return 0
else:
print("Fehler bei der Generierung der Testdaten")
return 1
if __name__ == '__main__':
sys.exit(main())