Ausserhalb der Bereiche liegende Symbole werden im dxf von einem Kreis umgeben, um neben der Consoleausgabe auch ein graphisches Feedback zu bekommen. Angabe des Einfügepunktes am Symbol in der Config möglich. Der MA Block kann im Zentrum, an der linken oberen Ecke etc. positioniert werden. Nummerierung2 wurde vergrössert, damit die Symbole sich nicht überschneiden.

This commit is contained in:
2026-01-26 19:09:02 +01:00
parent b82c751ba9
commit e214e9d21e
3 changed files with 365 additions and 77 deletions
+307 -44
View File
@@ -88,6 +88,7 @@ class TestDataGenerator:
{'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},
@@ -573,7 +574,31 @@ class TestDataGenerator:
else:
print("Fehler in Config ... terminating")
def _generate_ma_group(self, group, ma_defaults, width_per_char, fixed_height, spacing_factor, y_offsets_list):
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
@@ -584,6 +609,7 @@ class TestDataGenerator:
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']
@@ -607,6 +633,9 @@ class TestDataGenerator:
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':
@@ -622,12 +651,15 @@ class TestDataGenerator:
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=(block_insert_x, block_insert_y),
insert=(x, y),
insert=(left_top_x, left_top_y - fixed_height),
dxfattribs={'layer': layer}
)
@@ -637,11 +669,12 @@ class TestDataGenerator:
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': x,
'y': y,
'x': left_top_x,
'y': left_top_y,
'io': io_pattern,
'group_name': name,
'layer': layer
@@ -668,9 +701,13 @@ class TestDataGenerator:
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)
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', []):
@@ -704,16 +741,46 @@ class TestDataGenerator:
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
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).
@@ -736,34 +803,109 @@ class TestDataGenerator:
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.
Gibt Warnungen aus für Symbole, die außerhalb 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':
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:
polygon = [(p['x'], p['y']) for p in path_points]
polyline = [(p['x'], p['y']) for p in path_points]
frame_boundaries.append({
'type': 'polygon',
'boundary': polygon,
'validation_type': 'polyline_path',
'polyline': polyline,
'name': frame_config.get('name', 'UNKNOWN'),
'layer_name': layer_name
'layer_name': layer_name,
'direction': direction
})
else:
# Rechteck: berechne Eckpunkte
@@ -771,7 +913,7 @@ class TestDataGenerator:
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),
@@ -780,29 +922,60 @@ class TestDataGenerator:
(rect_x, rect_y + rect_height)
]
frame_boundaries.append({
'type': 'rectangle',
'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
'layer_name': layer_name,
'direction': direction
})
# Prüfe jedes generierte Symbol
symbols_outside = []
for symbol in self.generated_symbols:
symbol_point = (symbol['x'], symbol['y'])
# 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
# Prüfe ob Symbol innerhalb des Frames liegt
if frame['type'] == 'rectangle':
# 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'],
@@ -812,23 +985,79 @@ class TestDataGenerator:
):
in_any_frame = True
break
else: # polygon
if self._point_in_polygon(symbol_point, frame['boundary']):
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:
symbols_outside.append(symbol)
# Gib Warnungen aus
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) liegen außerhalb aller Renamer-Frames:")
print(f"\nWARNUNG: {len(symbols_outside)} Symbol(e) erfüllen nicht die Frame-Kriterien:")
for symbol in symbols_outside:
print(f" Symbol IO='{symbol['io']}' aus Gruppe '{symbol['group_name']}' "
f"an Position ({symbol['x']:.1f}, {symbol['y']:.1f}) "
f"auf Layer '{symbol['layer']}' liegt in keinem Renamer-Frame")
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"\n Alle {len(self.generated_symbols)} generierten Symbole liegen in Renamer-Frames")
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
}
)
@@ -858,8 +1087,42 @@ class TestDataGenerator:
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'
description='Erzeugt Test-DXF-Dateien für verschiedene Testszenarien',
epilog=epilog_text,
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
@@ -886,8 +1149,8 @@ def main():
parser.add_argument(
'--config',
type=str,
default='create_tests.json',
help='JSON-Konfigurationsdatei (Standard: create_tests.json)'
default=default_config,
help=f'JSON-Konfigurationsdatei (Standard: {default_config})'
)
args = parser.parse_args()