diff --git a/.vscode/launch.json b/.vscode/launch.json index dfda9f3..5f52d75 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -24,6 +24,16 @@ "program": "${file}", "console": "integratedTerminal" }, + { + "name": "create_numbers with Nummerierung1.dxf", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "args": [ + "--filename", + "Nummerierung1.dxf" ] + }, { "name": "getpositions with easy.dxf", "type": "debugpy", diff --git a/lib/create_example.py b/lib/create_example.py index 8b7d1b5..fb6601b 100644 --- a/lib/create_example.py +++ b/lib/create_example.py @@ -13,7 +13,6 @@ import os import json from pathlib import Path import ezdxf -from ezdxf.math import Vec3 # Umgebungsvariablen PROJECT = os.getenv('PROJECT', Path(__file__).parent.parent.absolute()) diff --git a/lib/create_numbers.py b/lib/create_numbers.py index 384ecf0..3960923 100644 --- a/lib/create_numbers.py +++ b/lib/create_numbers.py @@ -1,14 +1,6 @@ import argparse -import configparser -import json -import os -import re -import sys from pathlib import Path -import ezdxf -from ezdxf.addons import iterdxf -from ezdxf.lldxf.const import DXFStructureError from error_collector import ErrorCollector, write_json_file from utils import check_environment_var, check_file_in_work, dxf_is_binary, get_dxf_file @@ -87,27 +79,90 @@ def read_config_layers(config_path: Path) -> list: return layers -def extract_block_attributes(insert) -> dict: +def extract_block_attributes(doc, insert) -> dict: """ Extrahiert alle Attribute aus einem INSERT-Block. + Unterstützt zweistufige Blockstruktur: + - Äußerer Block enthält LWPOLYLINE und INSERT zu Attribut-Block + - Innerer Attribut-Block enthält die eigentlichen Attribute + + Args: + doc: DXF-Dokument + insert: INSERT-Entity des äußeren Blocks + + Returns: + Dictionary mit allen gefundenen Attributen """ attributes = {} if insert.dxftype() != 'INSERT': return attributes - if insert.has_attrib: - for attrib in insert.attribs: + # Prüfe zuerst ob der Block direkt Attribute hat (alte Struktur) + # Sammle alle direkten Attribute + direct_attribs = list(insert.attribs) + if direct_attribs: + for attrib in direct_attribs: tag = attrib.dxf.tag value = attrib.dxf.text attributes[tag] = value + return attributes + + # Neue zweistufige Struktur: Suche im Block nach einem INSERT mit Attributen + try: + block_layout = doc.blocks.get(insert.dxf.name) + for entity in block_layout: + if entity.dxftype() == 'INSERT': + inner_attribs = list(entity.attribs) + if inner_attribs: + # Gefunden: innerer Block mit Attributen + for attrib in inner_attribs: + tag = attrib.dxf.tag + value = attrib.dxf.text + attributes[tag] = value + break # Nur das erste INSERT mit Attributen verwenden + except Exception as e: + print(f" Warnung: Fehler beim Extrahieren der Attribute aus Block: {e}") return attributes +def is_rectangle_polyline(points): + """ + Prüft, ob eine Liste von Punkten ein Rechteck darstellt. + Ein Rechteck hat genau 4 Eckpunkte (oder 5 wenn geschlossen mit identischem Start/End). + + Args: + points: Liste von Punkten (Tupel mit mindestens x, y Koordinaten) + + Returns: + True wenn es ein Rechteck ist, False sonst + """ + if len(points) < 4: + return False + + # Prüfe ob erster und letzter Punkt identisch sind (geschlossene Polylinie) + first = (points[0][0], points[0][1]) + last = (points[-1][0], points[-1][1]) + + if abs(first[0] - last[0]) < 0.001 and abs(first[1] - last[1]) < 0.001: + # Geschlossene Polylinie - sollte 5 Punkte haben (4 Ecken + 1 Schließung) + if len(points) == 5: + return True + # Wenn mehr als 5 Punkte, ist es kein einfaches Rechteck + return False + else: + # Offene Polylinie - sollte genau 4 Punkte haben + return len(points) == 4 + + def get_boundary_geometry(doc, insert): """ Sucht im Block nach einem Rechteck oder einer geschlossenen Polylinie. + Unterstützt zweistufige Blockstruktur (äußerer Block mit LWPOLYLINE + INSERT zu Attribut-Block). Gibt die Eckpunkte zurück. + + Returns: + Tupel (transformed_points, is_rectangle) oder (None, None) wenn nichts gefunden """ block_layout = doc.blocks.get(insert.dxf.name) @@ -126,7 +181,7 @@ def get_boundary_geometry(doc, insert): if abs(first[0] - last[0]) < 0.001 and abs(first[1] - last[1]) < 0.001: is_closed = True - if is_closed: + if is_closed or len(points) >= 4: # Transform points relative to insert position insert_point = insert.dxf.insert transformed_points = [] @@ -136,7 +191,10 @@ def get_boundary_geometry(doc, insert): insert_point[0] + p[0], insert_point[1] + p[1] )) - return transformed_points + + # Prüfe ob es ein Rechteck ist + is_rect = is_rectangle_polyline(points) + return transformed_points, is_rect elif entity.dxftype() == 'POLYLINE': if entity.is_closed: @@ -148,9 +206,12 @@ def get_boundary_geometry(doc, insert): insert_point[0] + p[0], insert_point[1] + p[1] )) - return transformed_points - return None + # Prüfe ob es ein Rechteck ist + is_rect = is_rectangle_polyline(points) + return transformed_points, is_rect + + return None, None def point_in_polygon(point, polygon): @@ -198,8 +259,11 @@ def find_symbols_in_boundary(doc, msp, boundary, target_layers, attributes): if entity.dxf.layer not in search_layers: continue - # Prüfe ob der Block Attribute hat - if not entity.has_attrib: + # Prüfe ob der Block Attribute hat (direkt oder in zweistufiger Struktur) + # Verwende extract_block_attributes um beides zu unterstützen + symbol_attribs = extract_block_attributes(doc, entity) + + if not symbol_attribs: continue # Hole Position des Symbols @@ -208,9 +272,6 @@ def find_symbols_in_boundary(doc, msp, boundary, target_layers, attributes): # Prüfe ob innerhalb des Bereichs if point_in_polygon(point, boundary): - # Extrahiere Attribute des Symbols - symbol_attribs = extract_block_attributes(entity) - # Prüfe ob es ein Template ist (enthält @) has_template = False for value in symbol_attribs.values(): @@ -232,19 +293,25 @@ def find_symbols_in_boundary(doc, msp, boundary, target_layers, attributes): def sort_symbols_by_direction(symbols, direction): """ Sortiert die Symbole nach der angegebenen Richtung. - TOP_BOTTOM: nach Y absteigend, dann X - BOTTOM_TOP: nach Y aufsteigend, dann X - LEFT_RIGHT: nach X aufsteigend, dann Y - RIGHT_LEFT: nach X absteigend, dann Y + TOP_BOTTOM/LEFT_RIGHT: nach Y absteigend, dann X aufsteigend + TOP_BOTTOM/RIGHT_LEFT: nach Y absteigend, dann X absteigend + BOTTOM_TOP/RIGHT_LEFT: nach Y aufsteigend, dann X absteigend + BOTTOM_TOP/LEFT_RIGHT: nach Y aufsteigend, dann X aufsteigend + + default ist von links nach rechts wenn keine Angabe gemacht wird. Oder von oben nach unten + TOP_BOTTOM: TOP_BOTTOM/LEFT_RIGHT + BOTTOM_TOP: BOTTOM_TOP/LEFT_RIGHT + LEFT_RIGHT: TOP_BOTTOM/LEFT_RIGHT + RIGHT_LEFT: TOP_BOTTOM/RIGHT_LEFT """ - if direction == "TOP_BOTTOM": + if direction == "TOP_BOTTOM/LEFT_RIGHT" or direction == "TOP_BOTTOM" or direction == "LEFT_RIGHT": return sorted(symbols, key=lambda s: (-s['position'][1], s['position'][0])) - elif direction == "BOTTOM_TOP": + elif direction == "TOP_BOTTOM/RIGHT_LEFT" or direction == "RIGHT_LEFT": + return sorted(symbols, key=lambda s: (-s['position'][1], s['position'][0])) + elif direction == "BOTTOM_TOP/RIGHT_LEFT": + return sorted(symbols, key=lambda s: (s['position'][1], s['position'][0])) + elif direction == "BOTTOM_TOP/LEFT_RIGHT"or direction == "BOTTOM_TOP": return sorted(symbols, key=lambda s: (s['position'][1], s['position'][0])) - elif direction == "LEFT_RIGHT": - return sorted(symbols, key=lambda s: (s['position'][0], s['position'][1])) - elif direction == "RIGHT_LEFT": - return sorted(symbols, key=lambda s: (-s['position'][0], s['position'][1])) else: # Fallback: LEFT_RIGHT return sorted(symbols, key=lambda s: (s['position'][0], s['position'][1])) @@ -258,7 +325,6 @@ def enumerate_symbols(symbols, attributes): renamed = [] for symbol in symbols: - symbol_attribs = symbol['attributes'] layer = symbol['layer'] # Finde das passende NAME-Template für diesen Layer @@ -283,7 +349,6 @@ def enumerate_symbols(symbols, attributes): # Zähle wie viele @ im Template sind at_count = name_template.count('@') number_str = str(counter).zfill(at_count) - new_name = name_template.replace('@' * at_count, number_str) # Aktualisiere Attribute im Symbol for attrib in symbol['entity'].attribs: @@ -305,19 +370,100 @@ def enumerate_symbols(symbols, attributes): return renamed +def get_renamer_config_key(attributes): + """ + Erstellt einen eindeutigen Schlüssel für die Renamer-Block-Konfiguration. + Blöcke mit demselben Schlüssel werden gruppiert und gemeinsam nummeriert. + + Args: + attributes: Dictionary mit Block-Attributen + + Returns: + Tuple mit (NAME-Patterns, LAYER_NAMEs, DIRECTION, KENNZEICHNUNG) + """ + # Sammle NAME-Patterns (normalisiert: NAME oder NAME1-3) + name_patterns = [] + if "NAME" in attributes and attributes["NAME"]: + name_patterns.append(("NAME", attributes["NAME"])) + for i in range(1, 4): + name_key = f"NAME{i}" + if name_key in attributes and attributes[name_key]: + name_patterns.append((name_key, attributes[name_key])) + + # Sammle LAYER_NAMEs (normalisiert: LAYER_NAME oder LAYER_NAME1-3) + layer_names = [] + if "LAYER_NAME" in attributes and attributes["LAYER_NAME"]: + layer_names.append(("LAYER_NAME", attributes["LAYER_NAME"])) + for i in range(1, 4): + layer_key = f"LAYER_NAME{i}" + if layer_key in attributes and attributes[layer_key]: + layer_names.append((layer_key, attributes[layer_key])) + + # DIRECTION und KENNZEICHNUNG + direction = attributes.get("DIRECTION", "") + kennzeichnung = attributes.get("KENNZEICHNUNG", "") + + # Erstelle einen eindeutigen Schlüssel + # Sortiere die Listen für konsistente Keys + name_patterns.sort() + layer_names.sort() + + return (tuple(name_patterns), tuple(layer_names), direction, kennzeichnung) + + +def validate_direction_with_geometry(direction, is_rectangle): + """ + Validiert die DIRECTION-Angabe gegen die tatsächliche Geometrie. + + Args: + direction: DIRECTION-Attribut (z.B. "LEFT_RIGHT", "POLYLINE_PATH") + is_rectangle: True wenn die Polylinie ein Rechteck ist, False sonst + + Returns: + Tupel (is_valid, error_message): + - is_valid: True wenn Kombination gültig ist + - error_message: Fehlermeldung wenn ungültig, sonst None + """ + # POLYLINE_PATH-Richtungen erwarten komplexe Polylinien (keine Rechtecke) + polyline_path_directions = ["POLYLINE_PATH"] + + # Rechteck/Standard-Richtungen erwarten einfache Rechtecke + rectangle_directions = ["TOP_BOTTOM", "BOTTOM_TOP", "LEFT_RIGHT", "RIGHT_LEFT", + "TOP_BOTTOM/LEFT_RIGHT", "TOP_BOTTOM/RIGHT_LEFT", "BOTTOM_TOP/RIGHT_LEFT", "BOTTOM_TOP/LEFT_RIGHT"] + + if direction in polyline_path_directions: + if is_rectangle: + return False, f"DIRECTION '{direction}' erfordert eine komplexe Polylinie, aber ein Rechteck wurde gefunden" + return True, None + + elif direction in rectangle_directions: + if not is_rectangle: + return False, f"DIRECTION '{direction}' erfordert ein Rechteck, aber eine komplexe Polylinie wurde gefunden" + return True, None + + else: + # Unbekannte DIRECTION - gebe Warnung, aber breche nicht ab + return True, f"Unbekannte DIRECTION '{direction}' - Verarbeitung wird fortgesetzt" + + def process_renamer_blocks(doc, msp, renamer_layers, error_collector): """ Verarbeitet alle Renamer-Blöcke auf den angegebenen Layern. + Gruppiert Blöcke mit identischer Konfiguration und nummeriert alle Symbole + aus allen Blöcken einer Gruppe gemeinsam. """ all_renamed = [] + # Erste Pass: Sammle und gruppiere alle Renamer-Blöcke + renamer_groups = {} # {config_key: [(insert, boundary, attributes, is_rectangle), ...]} + for layer in renamer_layers: print(f"Durchsuche Layer: {layer}") # Finde alle INSERT-Blöcke auf diesem Layer for insert in msp.query(f'INSERT[layer=="{layer}"]'): - # Extrahiere Attribute - attributes = extract_block_attributes(insert) + # Extrahiere Attribute (unterstützt zweistufige Blockstruktur) + attributes = extract_block_attributes(doc, insert) if not attributes: print(f" Block ohne Attribute gefunden an Position {insert.dxf.insert}") @@ -330,36 +476,95 @@ def process_renamer_blocks(doc, msp, renamer_layers, error_collector): if not (has_name and has_direction): continue - print(f" Renamer-Block gefunden: {attributes.get('NAME', attributes.get('NAME1', 'UNKNOWN'))}") + block_name = attributes.get('NAME', attributes.get('NAME1', 'UNKNOWN')) + print(f" Renamer-Block gefunden: {block_name}") print(f" Direction: {attributes.get('DIRECTION', 'UNKNOWN')}") print(f" Kennzeichnung: {attributes.get('KENNZEICHNUNG', 'N/A')}") # Finde Boundary (Rechteck oder Polylinie) - boundary = get_boundary_geometry(doc, insert) - if not boundary: - error_msg = f"Keine Polylinie/Rechteck im Renamer-Block an Position {insert.dxf.insert} gefunden" - print(f" WARNUNG: {error_msg}") - error_collector.add_warnings({"missing_boundary": error_msg}) + boundary, is_rectangle = get_boundary_geometry(doc, insert) + if boundary is None: + error_msg = f"Keine Polylinie/Rechteck im Renamer-Block '{block_name}' an Position {insert.dxf.insert} gefunden" + print(f" FEHLER: {error_msg}") + error_collector.add_errors({"missing_boundary": error_msg}) continue - print(f" Boundary gefunden mit {len(boundary)} Punkten") + print(f" Boundary gefunden mit {len(boundary)} Punkten (Rechteck: {is_rectangle})") - # Finde Symbole innerhalb des Bereichs - symbols = find_symbols_in_boundary(doc, msp, boundary, renamer_layers, attributes) - print(f" {len(symbols)} Template-Symbole gefunden") - - if not symbols: - continue - - # Sortiere nach Richtung + # Validiere DIRECTION gegen Geometrie direction = attributes.get("DIRECTION", "LEFT_RIGHT") - sorted_symbols = sort_symbols_by_direction(symbols, direction) + is_valid, validation_error = validate_direction_with_geometry(direction, is_rectangle) - # Nummeriere durch - renamed = enumerate_symbols(sorted_symbols, attributes) - all_renamed.extend(renamed) + if not is_valid: + error_msg = f"Renamer-Block '{block_name}' an Position {insert.dxf.insert}: {validation_error}" + print(f" FEHLER: {error_msg}") + error_collector.add_errors({"direction_geometry_mismatch": error_msg}) + continue + elif validation_error: + # Warnung bei unbekannter DIRECTION, aber fortfahren + print(f" WARNUNG: {validation_error}") + error_collector.add_warnings({"unknown_direction": validation_error}) - print(f" {len(renamed)} Symbole nummeriert") + # Erstelle Konfigurationsschlüssel und gruppiere + config_key = get_renamer_config_key(attributes) + if config_key not in renamer_groups: + renamer_groups[config_key] = [] + renamer_groups[config_key].append((insert, boundary, attributes, is_rectangle)) + + # Zweite Pass: Verarbeite jede Gruppe + for config_key, group_blocks in renamer_groups.items(): + if not group_blocks: + continue + + # Verwende die Attribute des ersten Blocks als Referenz (alle in der Gruppe haben identische Attribute) + reference_attributes = group_blocks[0][2] + direction = reference_attributes.get("DIRECTION", "LEFT_RIGHT") + block_name = reference_attributes.get('NAME', reference_attributes.get('NAME1', 'UNKNOWN')) + + print(f"\n Verarbeite Gruppe: {block_name} ({len(group_blocks)} Renamer-Blöcke)") + + # Sammle Symbole aus ALLEN Boundaries dieser Gruppe + all_symbols = [] + seen_entities = {} # {entity_id: (symbol, [list of renamer block positions])} + + for insert, boundary, attributes, is_rectangle in group_blocks: + # Finde Symbole innerhalb dieses Bereichs + symbols = find_symbols_in_boundary(doc, msp, boundary, renamer_layers, attributes) + + # Füge Symbole hinzu, aber nur wenn das Entity noch nicht gesehen wurde + for symbol in symbols: + entity_id = id(symbol['entity']) # Eindeutige ID des Entity-Objekts + renamer_pos = insert.dxf.insert + + if entity_id not in seen_entities: + seen_entities[entity_id] = (symbol, [renamer_pos]) + all_symbols.append(symbol) + else: + # Symbol wurde bereits in einem anderen Bereich gefunden - Warnung + existing_symbol, existing_positions = seen_entities[entity_id] + existing_positions.append(renamer_pos) + symbol_pos = symbol['position'] + warning_msg = ( + f"Symbol an Position ({symbol_pos[0]:.1f}, {symbol_pos[1]:.1f}) " + f"liegt in mehreren Renamer-Bereichen der Gruppe '{block_name}'. " + f"Gefunden in Renamer-Blöcken an Positionen: " + f"{', '.join([f'({p[0]:.1f}, {p[1]:.1f})' for p in existing_positions])}" + ) + error_collector.add_warnings({"symbol_in_multiple_boundaries": warning_msg}) + + print(f" Gesamt {len(all_symbols)} eindeutige Template-Symbole aus {len(group_blocks)} Blöcken") + + if not all_symbols: + continue + + # Sortiere alle Symbole zusammen nach Richtung + sorted_symbols = sort_symbols_by_direction(all_symbols, direction) + + # Nummeriere alle Symbole als eine Sequenz + renamed = enumerate_symbols(sorted_symbols, reference_attributes) + all_renamed.extend(renamed) + + print(f" {len(renamed)} Symbole nummeriert") return all_renamed @@ -449,12 +654,21 @@ if __name__ == '__main__': write_json_file(output_data, output_path.parent, output_path.name) print(f"Ergebnisse in JSON gespeichert: {args.write}") - # Schreibe Fehler-Datei - if args.errorfile: - error_data = error_collector.get_all_issues() - error_path = Path(args.errorfile) - write_json_file(error_data, error_path.parent, error_path.name) - print(f"Fehler/Warnungen gespeichert: {args.errorfile}") + # Schreibe Fehler-Datei nur wenn Fehler oder Warnungen vorhanden sind + if error_collector.has_errors_or_warnings(): + if args.errorfile: + error_data = error_collector.get_all_issues() + error_path = Path(args.errorfile) + write_json_file(error_data, error_path.parent, error_path.name) + print(f"Fehler/Warnungen gespeichert: {args.errorfile}") + else: + # Keine explizite Fehlerdatei angegeben, generiere eine basierend auf Input-Datei + error_filename = f"{dxf_path.stem}_errors.json" + error_data = error_collector.get_all_issues() + write_json_file(error_data, dxf_path.parent, error_filename) + print(f"Fehler/Warnungen gespeichert: {dxf_path.parent / error_filename}") + else: + print("\nKeine Fehler oder Warnungen - keine Fehlerdatei wird geschrieben") # Exit-Code basierend auf Fehlern if error_collector.has_errors(): @@ -464,5 +678,4 @@ if __name__ == '__main__': print("\n(Warnungen vorhanden, aber keine kritischen Fehler)") exit(0) else: - print("\nKeine Fehler oder Warnungen") exit(0) diff --git a/lib/translate.py b/lib/translate.py index 5a8ade4..c6b1339 100644 --- a/lib/translate.py +++ b/lib/translate.py @@ -1,6 +1,5 @@ import argparse import json -import os import re import sys from pathlib import Path