diff --git a/cfg/create_tests.json b/cfg/create_tests.json index 074cb1b..e7a326a 100644 --- a/cfg/create_tests.json +++ b/cfg/create_tests.json @@ -25,7 +25,7 @@ "name": "MA-2@@_bottom", "count": 3, "layout_type": "horizontal_offset", - "base_x": 1800, + "base_x": 1850, "base_y": 400, "spacing": 1450 } @@ -51,7 +51,7 @@ "direction": "LEFT_RIGHT", "x": 400, "y": 75, - "width": 1400, + "width": 1150, "height": 480, "use_polyline": true }, @@ -149,11 +149,11 @@ "path_points": [ {"x": 400, "y": 2800}, {"x": 4800, "y": 2800}, - {"x": 6400, "y": 2400}, - {"x": 6400, "y": 1400}, + {"x": 6500, "y": 2400}, + {"x": 6500, "y": 1400}, {"x": 6080, "y": 800}, {"x": 2480, "y": 800}, - {"x": 3400, "y": 1800}, + {"x": 3500, "y": 1800}, {"x": 1200, "y": 1800}, {"x": 400, "y": 2800} ] @@ -322,7 +322,7 @@ "kennzeichnung": "", "layer_name1": "ILS_Eingang", "direction": "LEFT_RIGHT", - "x": 6000, + "x": 6500, "y": 800, "width": 3210, "height": 600, diff --git a/lib/create_example.py b/lib/create_example.py index 5a40896..6377c3e 100644 --- a/lib/create_example.py +++ b/lib/create_example.py @@ -883,7 +883,7 @@ class TestDataGenerator: # Generiere Renamer-Rahmen aus Config for frame_config in scene.get('renaming_frames', []): - frame_type = frame_config.get('type', 'rectangle') + frame_type = frame_config.get('type', 'rectangle') if frame_type == 'polyline_path': # Polyline-Pfad Frame @@ -933,6 +933,245 @@ class TestDataGenerator: # Prüfe ob alle generierten Symbole in einem Renamer-Frame liegen self._validate_symbols_in_frames(scene) + # Prüfe ob sich Renamer-Rechtecke überlappen (außer POLYLINE_PATH) + self._check_overlapping_frames(scene) + + def _rectangles_overlap(self, rect1, rect2): + """ + Prüft ob sich zwei Rechtecke überlappen. + + Args: + rect1: Dictionary mit 'x', 'y', 'width', 'height' + rect2: Dictionary mit 'x', 'y', 'width', 'height' + + Returns: + True wenn sich die Rechtecke überlappen + """ + # Rechteck 1 + r1_left = rect1['x'] + r1_right = rect1['x'] + rect1['width'] + r1_bottom = rect1['y'] + r1_top = rect1['y'] + rect1['height'] + + # Rechteck 2 + r2_left = rect2['x'] + r2_right = rect2['x'] + rect2['width'] + r2_bottom = rect2['y'] + r2_top = rect2['y'] + rect2['height'] + + # Prüfe ob keine Überlappung (dann ist das Gegenteil eine Überlappung) + no_overlap = (r1_right <= r2_left or # R1 ist links von R2 + r2_right <= r1_left or # R2 ist links von R1 + r1_top <= r2_bottom or # R1 ist unter R2 + r2_top <= r1_bottom) # R2 ist unter R1 + + return not no_overlap + + def _calculate_overlap_area(self, rect1, rect2): + """ + Berechnet die Überlappungsfläche zweier Rechtecke. + + Args: + rect1: Dictionary mit 'x', 'y', 'width', 'height' + rect2: Dictionary mit 'x', 'y', 'width', 'height' + + Returns: + Überlappungsfläche in Quadrateinheiten + """ + # Rechteck 1 + r1_left = rect1['x'] + r1_right = rect1['x'] + rect1['width'] + r1_bottom = rect1['y'] + r1_top = rect1['y'] + rect1['height'] + + # Rechteck 2 + r2_left = rect2['x'] + r2_right = rect2['x'] + rect2['width'] + r2_bottom = rect2['y'] + r2_top = rect2['y'] + rect2['height'] + + # Berechne überlappenden Bereich + overlap_left = max(r1_left, r2_left) + overlap_right = min(r1_right, r2_right) + overlap_bottom = max(r1_bottom, r2_bottom) + overlap_top = min(r1_top, r2_top) + + # Wenn keine Überlappung, gib 0 zurück + if overlap_right <= overlap_left or overlap_top <= overlap_bottom: + return 0 + + # Berechne Fläche + overlap_width = overlap_right - overlap_left + overlap_height = overlap_top - overlap_bottom + return overlap_width * overlap_height + + def _calculate_overlap_recommendations(self, frame1, frame2): + """ + Berechnet Empfehlungen zur Behebung einer Überlappung zwischen zwei Frames. + + Args: + frame1: Dictionary mit 'x', 'y', 'width', 'height', 'name' + frame2: Dictionary mit 'x', 'y', 'width', 'height', 'name' + + Returns: + Liste von Empfehlungs-Strings + """ + recommendations = [] + + # Berechne Kanten + f1_left = frame1['x'] + f1_right = frame1['x'] + frame1['width'] + f1_bottom = frame1['y'] + f1_top = frame1['y'] + frame1['height'] + + f2_left = frame2['x'] + f2_right = frame2['x'] + frame2['width'] + f2_bottom = frame2['y'] + f2_top = frame2['y'] + frame2['height'] + + # Berechne Überlappungsbereiche + overlap_left = max(f1_left, f2_left) + overlap_right = min(f1_right, f2_right) + overlap_bottom = max(f1_bottom, f2_bottom) + overlap_top = min(f1_top, f2_top) + + overlap_width = overlap_right - overlap_left + overlap_height = overlap_top - overlap_bottom + + # Horizontale Überlappung + if overlap_width > 0: + # Frame 1 ist links von Frame 2 + if f1_left < f2_left: + # Option 1: Frame 1 width reduzieren + new_width1 = f1_right - f1_left - overlap_width - 1 + recommendations.append( + f"Reduziere '{frame1['name']}' width von {frame1['width']} auf {new_width1:.0f} " + f"(um {overlap_width + 1:.0f} Einheiten)" + ) + # Option 2: Frame 2 nach rechts verschieben + new_x2 = f2_left + overlap_width + 1 + recommendations.append( + f"Verschiebe '{frame2['name']}' x von {frame2['x']} auf {new_x2:.0f} " + f"(um {overlap_width + 1:.0f} Einheiten nach rechts)" + ) + # Frame 2 ist links von Frame 1 + else: + # Option 1: Frame 2 width reduzieren + new_width2 = f2_right - f2_left - overlap_width - 1 + recommendations.append( + f"Reduziere '{frame2['name']}' width von {frame2['width']} auf {new_width2:.0f} " + f"(um {overlap_width + 1:.0f} Einheiten)" + ) + # Option 2: Frame 1 nach rechts verschieben + new_x1 = f1_left + overlap_width + 1 + recommendations.append( + f"Verschiebe '{frame1['name']}' x von {frame1['x']} auf {new_x1:.0f} " + f"(um {overlap_width + 1:.0f} Einheiten nach rechts)" + ) + + # Vertikale Überlappung + if overlap_height > 0: + # Frame 1 ist unter Frame 2 + if f1_bottom < f2_bottom: + # Option 1: Frame 1 height reduzieren + new_height1 = f1_top - f1_bottom - overlap_height - 1 + recommendations.append( + f"Reduziere '{frame1['name']}' height von {frame1['height']} auf {new_height1:.0f} " + f"(um {overlap_height + 1:.0f} Einheiten)" + ) + # Option 2: Frame 2 nach oben verschieben + new_y2 = f2_bottom + overlap_height + 1 + recommendations.append( + f"Verschiebe '{frame2['name']}' y von {frame2['y']} auf {new_y2:.0f} " + f"(um {overlap_height + 1:.0f} Einheiten nach oben)" + ) + # Frame 2 ist unter Frame 1 + else: + # Option 1: Frame 2 height reduzieren + new_height2 = f2_top - f2_bottom - overlap_height - 1 + recommendations.append( + f"Reduziere '{frame2['name']}' height von {frame2['height']} auf {new_height2:.0f} " + f"(um {overlap_height + 1:.0f} Einheiten)" + ) + # Option 2: Frame 1 nach oben verschieben + new_y1 = f1_bottom + overlap_height + 1 + recommendations.append( + f"Verschiebe '{frame1['name']}' y von {frame1['y']} auf {new_y1:.0f} " + f"(um {overlap_height + 1:.0f} Einheiten nach oben)" + ) + + return recommendations + + def _check_overlapping_frames(self, scene): + """ + Prüft ob sich Renamer-Rechtecke überlappen. + Polyline-Pfade (DIRECTION=POLYLINE_PATH) werden nicht geprüft. + + Args: + scene: Scene-Configuration Dictionary + """ + print("\n" + "="*60) + print("Prüfe auf überlappende Renamer-Rechtecke") + print("="*60) + + # Sammle alle Rechteck-Frames (ohne POLYLINE_PATH) + rectangle_frames = [] + for frame_config in scene.get('renaming_frames', []): + frame_type = frame_config.get('type', 'rectangle') + direction = frame_config.get('direction', '') + + # Überspringe POLYLINE_PATH Frames + if frame_type == 'polyline_path' or direction == 'POLYLINE_PATH': + continue + + # Überspringe Frames ohne Positionsangaben + if 'x' not in frame_config or 'y' not in frame_config: + continue + + rectangle_frames.append({ + 'name': frame_config.get('name', 'UNKNOWN'), + 'x': frame_config['x'], + 'y': frame_config['y'], + 'width': frame_config.get('width', 0), + 'height': frame_config.get('height', 0), + 'direction': direction + }) + + # Prüfe alle Paare von Rechtecken auf Überlappung + overlaps_found = [] + for i in range(len(rectangle_frames)): + for j in range(i + 1, len(rectangle_frames)): + frame1 = rectangle_frames[i] + frame2 = rectangle_frames[j] + + if self._rectangles_overlap(frame1, frame2): + overlap_area = self._calculate_overlap_area(frame1, frame2) + recommendations = self._calculate_overlap_recommendations(frame1, frame2) + overlaps_found.append({ + 'frame1': frame1, + 'frame2': frame2, + 'overlap_area': overlap_area, + 'recommendations': recommendations + }) + + # Gib Warnungen aus + if overlaps_found: + print(f"\nWARNUNG: {len(overlaps_found)} Überlappung(en) zwischen Renamer-Rechtecken gefunden:") + for overlap in overlaps_found: + f1 = overlap['frame1'] + f2 = overlap['frame2'] + area = overlap['overlap_area'] + print(f" Frame '{f1['name']}' (Position: {f1['x']}, {f1['y']}, " + f"Größe: {f1['width']}x{f1['height']}) " + f"überlappt mit Frame '{f2['name']}' " + f"(Position: {f2['x']}, {f2['y']}, Größe: {f2['width']}x{f2['height']})") + print(f" Überlappungsfläche: {area:.1f} Quadrateinheiten") + print(f" Empfohlene Änderungen (wähle eine):") + for rec in overlap['recommendations']: + print(f" - {rec}") + else: + print(f"\nOK: Keine Überlappungen zwischen {len(rectangle_frames)} Renamer-Rechtecken gefunden") + def _point_in_rectangle(self, point, rect_x, rect_y, rect_width, rect_height): """ Prüft ob ein Punkt innerhalb eines Rechtecks liegt. @@ -1347,15 +1586,21 @@ def main(): parser.add_argument( '--scene', type=str, - required=True, + default=None, help='Name der Testszene (z.B. Nummerierung1, Nummerierung2). Muss in der Config existieren.' ) + parser.add_argument( + '--all', + action='store_true', + help='Erzeugt alle konfigurierten Testszenen' + ) + 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.' + help='Name der zu erzeugenden Testdatei (z.B. Nummerierung1.dxf). Falls nicht angegeben, wird scene + .dxf verwendet. Wird bei --all ignoriert.' ) parser.add_argument( @@ -1380,29 +1625,83 @@ def main(): args = parser.parse_args() - # Bestimme Filename: entweder angegeben oder aus scene ableiten - if args.filename: - filename = args.filename + # Validierung: entweder --scene oder --all muss angegeben sein + if not args.scene and not args.all: + parser.error("Entweder --scene oder --all muss angegeben werden") + + if args.scene and args.all: + parser.error("--scene und --all können nicht zusammen verwendet werden") + + # Bestimme welche Szenen zu erzeugen sind + if args.all: + # Lade Config, um alle Szenen zu bekommen + config_path = Path(PROJECT_CFG) / args.config + if not config_path.exists(): + print(f"FEHLER: Config-Datei nicht gefunden: {config_path}") + return 1 + + try: + with open(config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + except json.JSONDecodeError as e: + print(f"FEHLER beim Parsen der Config-Datei: {e}") + return 1 + + if 'test_scenes' not in config: + print("FEHLER: Config enthält keinen 'test_scenes' Abschnitt") + return 1 + + scenes_to_generate = list(config['test_scenes'].keys()) + print(f"Erzeuge alle {len(scenes_to_generate)} konfigurierten Testszenen:") + for scene in scenes_to_generate: + print(f" - {scene}") + print() else: - filename = args.scene + '.dxf' + # Einzelne Szene + scenes_to_generate = [args.scene] - # Stelle sicher, dass Dateiname .dxf Endung hat - if not filename.endswith('.dxf'): - filename += '.dxf' + # Generiere alle Szenen + success_count = 0 + error_count = 0 - # Generiere Testdaten - generator = TestDataGenerator(filename, args.scene, config_file=args.config) + for scene_name in scenes_to_generate: + # Bestimme Filename: entweder angegeben (nur bei einzelner Szene) oder aus scene ableiten + if args.filename and not args.all: + filename = args.filename + else: + filename = scene_name + '.dxf' - if generator.generate(): - # Zeichne Symbol-Rahmen wenn gewünscht - if args.show_symbol_frames: - generator.draw_frames_for_generated_symbols() + # Stelle sicher, dass Dateiname .dxf Endung hat + if not filename.endswith('.dxf'): + filename += '.dxf' - generator.save(args.output_dir) - return 0 - else: - print("Fehler bei der Generierung der Testdaten") - return 1 + print(f"\n{'='*60}") + print(f"Erzeuge Testszene: {scene_name}") + print(f"Datei: {filename}") + print(f"{'='*60}") + + # Generiere Testdaten + generator = TestDataGenerator(filename, scene_name, 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) + success_count += 1 + else: + print(f"Fehler bei der Generierung von {scene_name}") + error_count += 1 + + # Zusammenfassung + print(f"\n{'='*60}") + print(f"Zusammenfassung:") + print(f" Erfolgreich: {success_count}") + print(f" Fehler: {error_count}") + print(f"{'='*60}") + + return 0 if error_count == 0 else 1 if __name__ == '__main__': diff --git a/lib/create_numbers.py b/lib/create_numbers.py index da9322c..7a01915 100644 --- a/lib/create_numbers.py +++ b/lib/create_numbers.py @@ -881,6 +881,98 @@ def check_symbols_in_boundaries(doc, msp, renamer_groups, error_collector): print(f" Alle {len(symbols_with_at)} Symbole mit '@' im IO liegen in einem Renamer-Bereich.") +def rectangles_overlap(boundary1, boundary2): + """ + Prüft ob sich zwei Rechteck-Boundaries überlappen. + + Args: + boundary1: Liste von (x, y) Punkten (mindestens 4 Eckpunkte) + boundary2: Liste von (x, y) Punkten (mindestens 4 Eckpunkte) + + Returns: + True wenn sich die Rechtecke überlappen + """ + # Extrahiere Min/Max für beide Rechtecke + b1_xs = [p[0] for p in boundary1] + b1_ys = [p[1] for p in boundary1] + b1_left, b1_right = min(b1_xs), max(b1_xs) + b1_bottom, b1_top = min(b1_ys), max(b1_ys) + + b2_xs = [p[0] for p in boundary2] + b2_ys = [p[1] for p in boundary2] + b2_left, b2_right = min(b2_xs), max(b2_xs) + b2_bottom, b2_top = min(b2_ys), max(b2_ys) + + # Prüfe ob keine Überlappung + no_overlap = (b1_right <= b2_left or # B1 ist links von B2 + b2_right <= b1_left or # B2 ist links von B1 + b1_top <= b2_bottom or # B1 ist unter B2 + b2_top <= b1_bottom) # B2 ist unter B1 + + return not no_overlap + + +def check_overlapping_renamer_blocks(renamer_groups, error_collector): + """ + Prüft ob sich Renamer-Rechtecke überlappen. + Polyline-Pfade (DIRECTION=POLYLINE_PATH) werden nicht geprüft. + + Args: + renamer_groups: Dictionary mit gruppierten Renamer-Blöcken + error_collector: ErrorCollector für Fehlerbehandlung + """ + print("\n" + "="*60) + print("Prüfe auf überlappende Renamer-Bereiche") + print("="*60) + + # Sammle alle Rechteck-Renamer (ohne POLYLINE_PATH) + rectangle_renamers = [] + for config_key, group_blocks in renamer_groups.items(): + for insert, boundary, attributes, is_rectangle in group_blocks: + direction = attributes.get("DIRECTION", "") + block_name = attributes.get('NAME', attributes.get('NAME1', 'UNKNOWN')) + + # Überspringe POLYLINE_PATH + if direction == 'POLYLINE_PATH': + continue + + # Nur Rechtecke prüfen + if is_rectangle: + rectangle_renamers.append({ + 'name': block_name, + 'position': insert.dxf.insert, + 'boundary': boundary, + 'direction': direction, + 'is_rectangle': is_rectangle + }) + + # Prüfe alle Paare auf Überlappung + overlaps_found = [] + for i in range(len(rectangle_renamers)): + for j in range(i + 1, len(rectangle_renamers)): + r1 = rectangle_renamers[i] + r2 = rectangle_renamers[j] + + if rectangles_overlap(r1['boundary'], r2['boundary']): + overlaps_found.append((r1, r2)) + + # Melde Fehler + if overlaps_found: + print(f"\nFEHLER: {len(overlaps_found)} Überlappung(en) zwischen Renamer-Bereichen gefunden:") + for r1, r2 in overlaps_found: + error_msg = ( + f"Renamer-Bereich '{r1['name']}' an Position " + f"({r1['position'][0]:.1f}, {r1['position'][1]:.1f}) " + f"überlappt mit Renamer-Bereich '{r2['name']}' an Position " + f"({r2['position'][0]:.1f}, {r2['position'][1]:.1f}). " + f"Symbole könnten mehrfach nummeriert werden." + ) + print(f" {error_msg}") + error_collector.add_errors({"overlapping_renamer_blocks": error_msg}) + else: + print(f"\nOK: Keine Überlappungen zwischen {len(rectangle_renamers)} Renamer-Rechtecken gefunden") + + def process_renamer_blocks(doc, msp, renamer_layers, error_collector): """ Verarbeitet alle Renamer-Blöcke auf den angegebenen Layern. @@ -895,6 +987,9 @@ def process_renamer_blocks(doc, msp, renamer_layers, error_collector): # Erster Pass: Sammle und gruppiere alle Renamer-Blöcke renamer_groups = collect_and_group_renamer_blocks(doc, msp, renamer_layers, error_collector) + # Prüfe auf überlappende Renamer-Bereiche + check_overlapping_renamer_blocks(renamer_groups, error_collector) + # Zweiter Pass: Verarbeite jede Gruppe all_renamed = process_renamer_groups(doc, msp, renamer_layers, renamer_groups, error_collector) @@ -975,10 +1070,15 @@ if __name__ == '__main__': parser.add_argument('-e', '--errorfile', action='store', required=False, help='JSON-Datei für Fehler und Warnungen', metavar='errors.json') parser.add_argument('-i', '--info', action='store', help='Schreibe Ergebnisse der Nummerierung in eine JSON-Datei') parser.add_argument('-d', '--dryrun', action='store_true', help='Symbole nicht in der DXF-Datei überschreiben, nur Ausgabe auf Konsole') + parser.add_argument('--overwrite', action='store_true', help='Überschreibt die Originaldatei anstatt eine neue Datei mit Suffix _numbered zu erstellen') 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() + # Validierung: --overwrite und --dryrun können nicht zusammen verwendet werden + if args.overwrite and args.dryrun: + parser.error("--overwrite und --dryrun können nicht zusammen verwendet werden") + # Initialisiere ErrorCollector error_collector = ErrorCollector() @@ -1042,9 +1142,16 @@ if __name__ == '__main__': # Speichere DXF-Datei wenn nicht dry-run if not args.dryrun: - output_path = dxf_path.parent / f"{dxf_path.stem}_numbered{dxf_path.suffix}" - doc.saveas(output_path) - print(f"\nNummerierte DXF-Datei gespeichert: {output_path}") + if args.overwrite: + # Überschreibe die Originaldatei + output_path = dxf_path + doc.saveas(output_path) + print(f"\nOriginaldatei überschrieben: {output_path}") + else: + # Erstelle neue Datei mit _numbered Suffix + output_path = dxf_path.parent / f"{dxf_path.stem}_numbered{dxf_path.suffix}" + doc.saveas(output_path) + print(f"\nNummerierte DXF-Datei gespeichert: {output_path}") else: print("\nDry-run Modus: DXF-Datei wurde nicht überschrieben")