import argparse from pathlib import Path from error_collector import ErrorCollector, write_json_file from utils import ( check_environment_var, check_file_in_work, dxf_is_binary, get_dxf_file, extract_insert_attributes_with_doc as extract_block_attributes, draw_symbol_frames ) """ Dieses Programm liest eine dxf Datei und holt sich vom Layer RENAME die Angaben für die Bereiche in der Anlage, welche Motoren, Sensoren und Aktoren welchem Unterverteiler zugeordnet werden müssen. (Attribut KENNZEICHNUNG) Alle Symbol Templates werden entsprechend der Richtungsangaben in DIRECTION nummeriert DIRECTION kann TOP_BOTTOM, BOTTOM_TOP, LEFT_RIGHT oder RIGHT_LEFT sein Ein Renamer Symbol enthält z.B. die folgenden Angaben: für POT-RA, POT-MA und POT-UC: NAME1=POT-RA@@ NAME2=POT-MA@@ NAME3=POT-UC@@ KENNZEICHNUNG=A01+UC… LAYER_NAME1=ILS_POT-RA LAYER_NAME2=ILS_POT-MA LAYER_NAME3=ILS_POT-UC DIRECTION = LEFT_RIGHT für BG, MB: NAME1=BG-1@@@ NAME2=MB-1@@@ NAME3= KENNZEICHNUNG= LAYER_NAME1=ILS_Eingang LAYER_NAME2=ILS_Ausgang LAYER_NAME3= DIRECTION = LEFT_RIGHT oder nur zur Nummerierung der MA Symbole NAME=MA-1@@ KENNZEICHNUNG=A01+UH00 LAYER_NAME=ILS_MOTOR DIRECTION = TOP_BOTTOM/LEFT_RIGHT Diese Symbole auf dem RENAMER Layer sind immer in einem Block mit einer Polylinie oder einen REchteck. Alle Sensoren, Motoren, Aktoren etc. innerhalb der zugehörigen Polylinie nehmen an der Nummerierung teil, falls diese das @ Zeichen enthalten, also noch ein Template sind. """ def read_config_layers(config_path: Path) -> list: """ Liest die enumerate.cfg und gibt die Layer zurück, auf denen nach Renamer-Blöcken gesucht werden soll. Die Konfiguration hat eine einfache Liste unter [Layers]: [Layers] RENAMER OTHER_LAYER """ layers = [] with open(config_path, 'r', encoding='utf-8') as f: in_layers_section = False for line in f: line = line.strip() # Überspringe leere Zeilen und Kommentare if not line or line.startswith('#') or line.startswith(';'): continue # Prüfe ob wir in der [Layers] Sektion sind if line.startswith('['): in_layers_section = line.lower() == '[layers]' continue # Wenn in [Layers], füge Layer hinzu if in_layers_section: layers.append(line) return layers 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) # Suche nach LWPOLYLINE, POLYLINE oder geschlossenen Linien for entity in block_layout: if entity.dxftype() == 'LWPOLYLINE': # Get points and transform them points = list(entity.get_points()) # Prüfe ob geschlossen (entweder Flag gesetzt oder erster == letzter Punkt) is_closed = entity.closed or (entity.dxf.flags & 1) if not is_closed and len(points) > 1: # Prüfe ob erster und letzter Punkt gleich sind first = points[0][:2] last = points[-1][:2] if abs(first[0] - last[0]) < 0.001 and abs(first[1] - last[1]) < 0.001: is_closed = True if is_closed or len(points) >= 4: # Transform points relative to insert position insert_point = insert.dxf.insert transformed_points = [] for p in points: # Einfache Translation (ohne Rotation/Skalierung für ersten Ansatz) transformed_points.append(( insert_point[0] + p[0], insert_point[1] + p[1] )) # 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: points = [(v.dxf.location.x, v.dxf.location.y) for v in entity.vertices] insert_point = insert.dxf.insert transformed_points = [] for p in points: transformed_points.append(( insert_point[0] + p[0], insert_point[1] + p[1] )) # 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): """ 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 find_symbols_in_boundary(doc, msp, boundary, target_layers, attributes, error_collector=None): """ Findet alle Symbole (INSERT-Blöcke) innerhalb des angegebenen Bereichs auf den Ziel-Layern. """ symbols = [] # Bestimme welche Layer durchsucht werden sollen search_layers = [] for i in range(1, 4): # NAME1, NAME2, NAME3 layer_key = f"LAYER_NAME{i}" if layer_key in attributes and attributes[layer_key]: search_layers.append(attributes[layer_key]) # Falls nur NAME/LAYER_NAME vorhanden if "LAYER_NAME" in attributes and attributes["LAYER_NAME"]: search_layers.append(attributes["LAYER_NAME"]) # Durchsuche alle INSERT-Blöcke for entity in msp.query('INSERT'): if entity.dxf.layer not in search_layers: continue # 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, error_collector) if not symbol_attribs: continue # Hole Position des Symbols pos = entity.dxf.insert point = (pos[0], pos[1]) # Prüfe ob innerhalb des Bereichs if point_in_polygon(point, boundary): # Prüfe ob es ein Template ist (enthält @) has_template = False for value in symbol_attribs.values(): if '@' in str(value): has_template = True break if has_template: symbols.append({ 'entity': entity, 'position': point, 'attributes': symbol_attribs, 'layer': entity.dxf.layer }) return symbols def sort_symbols_by_direction(symbols, direction): """ Sortiert die Symbole nach der angegebenen Richtung. 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/LEFT_RIGHT" or direction == "TOP_BOTTOM" or direction == "LEFT_RIGHT": return sorted(symbols, key=lambda s: (-s['position'][1], s['position'][0])) 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])) else: # Fallback: LEFT_RIGHT return sorted(symbols, key=lambda s: (s['position'][0], s['position'][1])) def enumerate_symbols(symbols, attributes): """ Nummeriert die Symbole durch und ersetzt die @@ Platzhalter. """ counter = 1 renamed = [] for symbol in symbols: layer = symbol['layer'] # Finde das passende NAME-Template für diesen Layer name_template = None for i in range(1, 4): layer_key = f"LAYER_NAME{i}" name_key = f"NAME{i}" if layer_key in attributes and attributes[layer_key] == layer: if name_key in attributes: name_template = attributes[name_key] break # Falls nur NAME/LAYER_NAME vorhanden if not name_template and "LAYER_NAME" in attributes and attributes["LAYER_NAME"] == layer: if "NAME" in attributes: name_template = attributes["NAME"] if not name_template: continue # Ersetze @-Zeichen durch Zahlen # Zähle wie viele @ im Template sind at_count = name_template.count('@') number_str = str(counter).zfill(at_count) # Aktualisiere Attribute im Symbol for attrib in symbol['entity'].attribs: old_value = attrib.dxf.text if '@' in old_value: # Ersetze @ durch die Nummer new_value = old_value.replace('@' * at_count, number_str) attrib.dxf.text = new_value renamed.append({ 'position': symbol['position'], 'layer': layer, 'old_value': old_value, 'new_value': new_value }) counter += 1 return renamed def get_layer_names_from_attributes(attributes): """ Extrahiert alle LAYER_NAME Attribute aus einem Renamer-Block. Args: attributes: Dictionary mit Block-Attributen Returns: Liste der Layer-Namen (z.B. ['ILS_Eingang', 'ILS_Ausgang']) """ layer_names = [] if "LAYER_NAME" in attributes and attributes["LAYER_NAME"]: layer_names.append(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(attributes[layer_key]) return layer_names 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 collect_and_group_renamer_blocks(doc, msp, renamer_layers, error_collector): """ Erster Pass: Sammelt und gruppiert alle Renamer-Blöcke nach Konfiguration. Returns: Dictionary mit {config_key: [(insert, boundary, attributes, is_rectangle), ...]} """ 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 (unterstützt zweistufige Blockstruktur) attributes = extract_block_attributes(doc, insert, error_collector) if not attributes: print(f" Block ohne Attribute gefunden an Position {insert.dxf.insert}") continue # Prüfe ob es ein Renamer-Block ist has_name = "NAME" in attributes or "NAME1" in attributes has_direction = "DIRECTION" in attributes if not (has_name and has_direction): continue 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, 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 (Rechteck: {is_rectangle})") print(f" Eckpunkte: {', '.join([f'({p[0]:.2f}, {p[1]:.2f})' for p in boundary])}") # Validiere DIRECTION gegen Geometrie direction = attributes.get("DIRECTION", "LEFT_RIGHT") is_valid, validation_error = validate_direction_with_geometry(direction, is_rectangle) 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}) # 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)) return renamer_groups def find_template_symbols_in_geometry(doc, msp, group_blocks, error_collector): """ Findet alle Symbole mit '@' die geometrisch in den Renamer-Bereichen liegen, unabhängig vom Layer. Args: doc: DXF-Dokument msp: Modelspace group_blocks: Liste von (insert, boundary, attributes, is_rectangle) Tupeln error_collector: ErrorCollector für Fehlerbehandlung Returns: Liste von Dictionaries mit 'layer', 'position', 'io' für jedes gefundene Symbol """ symbols_in_geometry = [] for insert, boundary, attributes, is_rectangle in group_blocks: for entity in msp.query('INSERT'): symbol_attribs = extract_block_attributes(doc, entity, error_collector) if not symbol_attribs: continue # Prüfe ob irgendein Attribut "@" enthält has_template = any('@' in str(value) for value in symbol_attribs.values()) if has_template: pos = entity.dxf.insert point = (pos[0], pos[1]) if point_in_polygon(point, boundary): symbols_in_geometry.append({ 'layer': entity.dxf.layer, 'position': point, 'io': symbol_attribs.get('IO', 'N/A') }) return symbols_in_geometry def create_no_symbols_found_error_message(block_name, direction, layer_names, group_blocks, doc, msp, error_collector): """ Erstellt eine detaillierte Fehlermeldung wenn keine Symbole in einem Renamer-Bereich gefunden wurden. Args: block_name: Name des Renamer-Blocks direction: DIRECTION-Attribut layer_names: Liste der erwarteten Layer-Namen group_blocks: Liste von (insert, boundary, attributes, is_rectangle) Tupeln doc: DXF-Dokument msp: Modelspace error_collector: ErrorCollector für Fehlerbehandlung Returns: Fehlermeldungs-String """ renamer_positions = [f"({insert.dxf.insert[0]:.1f}, {insert.dxf.insert[1]:.1f})" for insert, _, _, _ in group_blocks] # Prüfe ob es Symbole mit @ gibt, die geometrisch im Bereich liegen (egal auf welchem Layer) symbols_in_geometry = find_template_symbols_in_geometry(doc, msp, group_blocks, error_collector) # Erstelle Fehlermeldung basierend auf gefundenen Symbolen if symbols_in_geometry and not layer_names: # Es gibt Symbole im Bereich, aber keine LAYER_NAME Attribute definiert return ( f"Renamer-Block-Gruppe '{block_name}' (DIRECTION: {direction}) " f"hat keine passenden Symbole gefunden. " f"Es wurden {len(symbols_in_geometry)} Symbol(e) mit '@' geometrisch im Bereich gefunden, " f"aber der Renamer-Block hat KEINE LAYER_NAME Attribute (LAYER_NAME1, LAYER_NAME2, LAYER_NAME3) definiert. " f"Ohne diese Attribute können Symbole nicht zugeordnet werden. " f"Renamer-Blöcke an Positionen: {', '.join(renamer_positions)}" ) elif symbols_in_geometry and layer_names: # Es gibt Symbole im Bereich, aber sie sind auf falschen Layern found_layers = set(s['layer'] for s in symbols_in_geometry) return ( f"Renamer-Block-Gruppe '{block_name}' (DIRECTION: {direction}) " f"hat keine passenden Symbole gefunden. " f"Es wurden {len(symbols_in_geometry)} Symbol(e) mit '@' geometrisch im Bereich gefunden, " f"aber diese sind auf Layer: {', '.join(found_layers)}. " f"Erwartet werden Layer: {', '.join(layer_names)}. " f"Prüfen Sie die LAYER_NAME Attribute im Renamer-Block. " f"Renamer-Blöcke an Positionen: {', '.join(renamer_positions)}" ) else: # Keine Symbole im Bereich gefunden (weder geometrisch noch auf richtigen Layern) return ( f"Renamer-Block-Gruppe '{block_name}' (DIRECTION: {direction}) " f"hat keine passenden Symbole gefunden. " f"Gesucht auf Layer: {', '.join(layer_names) if layer_names else 'N/A'}. " f"Renamer-Blöcke an Positionen: {', '.join(renamer_positions)}" ) def collect_symbols_from_group(doc, msp, renamer_layers, group_blocks, block_name, error_collector): """ Sammelt alle Symbole aus allen Boundaries einer Renamer-Gruppe. Prüft auf Duplikate (Symbole in mehreren Bereichen). Args: doc: DXF-Dokument msp: Modelspace renamer_layers: Layer auf denen nach Renamer-Blöcken gesucht wird group_blocks: Liste von (insert, boundary, attributes, is_rectangle) Tupeln block_name: Name des Renamer-Blocks (für Fehlermeldungen) error_collector: ErrorCollector für Fehlerbehandlung Returns: Liste aller eindeutigen Symbole """ 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, error_collector) # 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 - Fehler existing_symbol, existing_positions = seen_entities[entity_id] existing_positions.append(renamer_pos) symbol_pos = symbol['position'] io_value = symbol['attributes'].get('IO', 'N/A') error_msg = ( f"Symbol an Position ({symbol_pos[0]:.1f}, {symbol_pos[1]:.1f}) " f"mit IO='{io_value}' 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])}" ) print(f" FEHLER: {error_msg}") error_collector.add_errors({"symbol_in_multiple_boundaries": error_msg}) return all_symbols def process_renamer_groups(doc, msp, renamer_layers, renamer_groups, error_collector): """ Zweiter Pass: Verarbeitet jede Renamer-Block-Gruppe. Sammelt Symbole, sortiert sie und nummeriert sie. Returns: Liste aller umbenannten Symbole """ all_renamed = [] 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 = collect_symbols_from_group(doc, msp, renamer_layers, group_blocks, block_name, error_collector) print(f" Gesamt {len(all_symbols)} eindeutige Template-Symbole aus {len(group_blocks)} Blöcken") if not all_symbols: # Fehlermeldung: Keine Symbole gefunden layer_names = get_layer_names_from_attributes(reference_attributes) error_msg = create_no_symbols_found_error_message( block_name, direction, layer_names, group_blocks, doc, msp, error_collector ) print(f" FEHLER: {error_msg}") error_collector.add_errors({"no_symbols_found": error_msg}) 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 def find_symbols_with_at_in_io(doc, msp, error_collector): """ Findet alle Symbole mit "@" im IO-Attribut. Args: doc: DXF-Dokument msp: Modelspace error_collector: ErrorCollector für Fehlerbehandlung Returns: Liste von Dictionaries mit Symbol-Informationen """ symbols_with_at = [] for entity in msp.query('INSERT'): symbol_attribs = extract_block_attributes(doc, entity, error_collector) if not symbol_attribs: continue # Prüfe ob IO-Attribut "@" enthält io_value = symbol_attribs.get('IO', '') if '@' in str(io_value): pos = entity.dxf.insert point = (pos[0], pos[1]) symbols_with_at.append({ 'entity': entity, 'position': point, 'attributes': symbol_attribs, 'layer': entity.dxf.layer, 'io': io_value }) return symbols_with_at def check_symbol_in_boundary(symbol, boundary, attributes, renamer_pos): """ Prüft ob ein Symbol geometrisch und vom Layer her in einem Renamer-Bereich liegt. Args: symbol: Dictionary mit Symbol-Informationen boundary: Polygon-Punkte des Renamer-Bereichs attributes: Attribute des Renamer-Blocks renamer_pos: Position des Renamer-Blocks Returns: Tuple (in_boundary, wrong_layer_info): - in_boundary: True wenn Symbol geometrisch und vom Layer her im Bereich liegt - wrong_layer_info: Dictionary mit Fehlerinformationen wenn Layer falsch ist, sonst None """ # Prüfe ob Symbol geometrisch innerhalb des Bereichs liegt if not point_in_polygon(symbol['position'], boundary): return False, None # Symbol liegt geometrisch im Bereich - prüfe Layer search_layers = get_layer_names_from_attributes(attributes) if symbol['layer'] in search_layers: # Layer passt auch - alles OK return True, None else: # Symbol liegt geometrisch im Bereich, aber Layer passt nicht block_name = attributes.get('NAME', attributes.get('NAME1', 'UNKNOWN')) wrong_layer_info = { 'block_name': block_name, 'renamer_pos': renamer_pos, 'expected_layers': search_layers, 'attributes': attributes } return False, wrong_layer_info def create_wrong_layer_warning(symbol, wrong_layer_info): """ Erstellt eine Warnung wenn ein Symbol im Bereich liegt, aber der Layer nicht passt. Args: symbol: Dictionary mit Symbol-Informationen wrong_layer_info: Dictionary mit Fehlerinformationen Returns: Warnmeldungs-String """ if wrong_layer_info['expected_layers']: return ( f"Symbol an Position ({symbol['position'][0]:.1f}, {symbol['position'][1]:.1f}) " f"mit IO='{symbol['io']}' liegt geometrisch im Renamer-Bereich '{wrong_layer_info['block_name']}' " f"(Renamer-Position: ({wrong_layer_info['renamer_pos'][0]:.1f}, {wrong_layer_info['renamer_pos'][1]:.1f})), " f"aber das Symbol ist auf Layer '{symbol['layer']}', erwartet wird: {', '.join(wrong_layer_info['expected_layers'])}. " f"Prüfen Sie die LAYER_NAME Attribute im Renamer-Block." ) else: return ( f"Symbol an Position ({symbol['position'][0]:.1f}, {symbol['position'][1]:.1f}) " f"mit IO='{symbol['io']}' liegt geometrisch im Renamer-Bereich '{wrong_layer_info['block_name']}' " f"(Renamer-Position: ({wrong_layer_info['renamer_pos'][0]:.1f}, {wrong_layer_info['renamer_pos'][1]:.1f})), " f"aber der Renamer-Block hat KEINE LAYER_NAME Attribute definiert. " f"Symbole können nur gefunden werden, wenn LAYER_NAME1/LAYER_NAME2/LAYER_NAME3 Attribute gesetzt sind." ) def check_symbols_in_boundaries(doc, msp, renamer_groups, error_collector): """ Dritter Pass: Prüft ob alle Symbole mit "@" im IO in einem Renamer-Bereich liegen. """ print("\n" + "="*60) print("Prüfe ob alle Symbole mit '@' im IO in einem Renamer-Bereich liegen") print("="*60) # Sammle alle Renamer-Boundaries all_boundaries = [] for group_blocks in renamer_groups.values(): for insert, boundary, attributes, is_rectangle in group_blocks: all_boundaries.append((boundary, attributes, insert.dxf.insert)) # Finde alle Symbole mit "@" im IO-Attribut symbols_with_at = find_symbols_with_at_in_io(doc, msp, error_collector) # Prüfe für jedes Symbol, ob es in mindestens einem Renamer-Bereich liegt symbols_not_in_any_boundary = [] for symbol in symbols_with_at: in_any_boundary = False wrong_layer_info = None for boundary, attributes, renamer_pos in all_boundaries: is_in_boundary, layer_info = check_symbol_in_boundary(symbol, boundary, attributes, renamer_pos) if is_in_boundary: # Layer passt auch - alles OK in_any_boundary = True break elif layer_info is not None: # Symbol liegt geometrisch im Bereich, aber Layer passt nicht wrong_layer_info = layer_info if not in_any_boundary: if wrong_layer_info: # Warnung: Symbol liegt geometrisch im Bereich, aber Layer stimmt nicht warning_msg = create_wrong_layer_warning(symbol, wrong_layer_info) print(f" WARNUNG: {warning_msg}") error_collector.add_warnings({"symbol_wrong_layer": warning_msg}) # NICHT als Fehler hinzufügen - Warnung reicht aus else: # Fehler: Symbol liegt weder geometrisch noch vom Layer her in einem Renamer-Bereich symbols_not_in_any_boundary.append(symbol) # Melde Fehler für Symbole, die in keinem Bereich liegen if symbols_not_in_any_boundary: for symbol in symbols_not_in_any_boundary: error_msg = ( f"Symbol an Position ({symbol['position'][0]:.1f}, {symbol['position'][1]:.1f}) " f"mit IO='{symbol['io']}' auf Layer '{symbol['layer']}' " f"enthält '@' im IO-Attribut, liegt aber in keinem Renamer-Bereich." ) print(f" FEHLER: {error_msg}") error_collector.add_errors({"symbol_with_at_not_in_boundary": error_msg}) else: print(f" Alle {len(symbols_with_at)} Symbole mit '@' im IO liegen in einem Renamer-Bereich.") 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. Returns: Tuple (all_renamed, renamer_groups): - all_renamed: Liste der umbenannten Symbole - renamer_groups: Dictionary der gruppierten Renamer-Blöcke """ # Erster Pass: Sammle und gruppiere alle Renamer-Blöcke renamer_groups = collect_and_group_renamer_blocks(doc, msp, renamer_layers, error_collector) # Zweiter Pass: Verarbeite jede Gruppe all_renamed = process_renamer_groups(doc, msp, renamer_layers, renamer_groups, error_collector) # Dritter Pass: Prüfe ob alle Symbole mit "@" im IO in einem Renamer-Bereich liegen check_symbols_in_boundaries(doc, msp, renamer_groups, error_collector) return all_renamed, renamer_groups def collect_all_symbols_from_groups(doc, msp, renamer_groups, renamer_layers, error_collector): """ Sammelt alle Symbole aus allen Renamer-Gruppen für Visualisierungszwecke. Args: doc: DXF-Dokument msp: Modelspace renamer_groups: Dictionary mit gruppierten Renamer-Blöcken renamer_layers: Liste der Layer mit Renamer-Blöcken error_collector: ErrorCollector für Fehlerbehandlung Returns: Liste aller gefundenen Symbole mit 'entity', 'position', 'attributes', 'layer' """ all_symbols = [] seen_entities = set() # Verhindere Duplikate for config_key, group_blocks in renamer_groups.items(): if not group_blocks: continue reference_attributes = group_blocks[0][2] 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, error_collector) # Füge Symbole hinzu, aber nur wenn das Entity noch nicht gesehen wurde for symbol in symbols: entity_id = id(symbol['entity']) if entity_id not in seen_entities: seen_entities.add(entity_id) all_symbols.append(symbol) return all_symbols if __name__ == '__main__': # Lese Umgebungsvariablen vor argparse, um Config-Info in Hilfe anzuzeigen out_dir = check_environment_var('PROJECT_DATA') work_dir = check_environment_var('PROJECT_WORK') config_dir = check_environment_var("PROJECT_CFG") # Versuche Config zu lesen für --help Ausgabe config_path = Path(config_dir) / "enumerate.cfg" available_layers = [] if config_path.exists(): try: available_layers = read_config_layers(config_path) except Exception: pass # Ignoriere Fehler beim Lesen für --help # Erstelle Epilog mit verfügbaren Layern epilog_text = "\nKonfigurierte Renamer-Layer:\n" if available_layers: epilog_text += " " + ", ".join(available_layers) + "\n" epilog_text += f"\n(aus Konfigurationsdatei: {config_path})" else: epilog_text += " (keine Layer konfiguriert oder Config-Datei nicht gefunden)\n" epilog_text += f" Standard-Pfad: {config_path}" parser = argparse.ArgumentParser( description='Nummeriert Symbole in DXF-Dateien basierend auf Renamer-Blöcken', prog='create_numbers', epilog=epilog_text, formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument('-f', '--filename', action='store', required=True, help='DXF-Datei die verarbeitet werden soll', metavar='myfile.dxf') 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('--show_symbol_frames', action='store_true', help='Zeichne Rahmen um jedes Symbol, um die Grenze des Symbols zu visualisieren') args = parser.parse_args() # Initialisiere ErrorCollector error_collector = ErrorCollector() filename = Path(args.filename) if not filename.suffix == ".dxf": print("Nur für .dxf Dateien verfügbar") exit(1) (dxf_path, dexists) = check_file_in_work(work_dir, filename) if dexists == False: print(f"Datei nicht gefunden: {filename}") parser.print_help() exit(1) # Config-Datei existiert bereits (wurde für --help geprüft) if not config_path.exists(): print(f"Konfigurationsdatei nicht gefunden: {config_path}") exit(1) renamer_layers = read_config_layers(config_path) print(f"Konfigurierte Layer: {renamer_layers}") if not renamer_layers: print("Keine Layer in der Konfiguration gefunden") exit(1) # Lese DXF-Datei if dxf_is_binary(dxf_path): print("DXF-Datei ist binär. Lese komplette Datei. Achten Sie auf RAM-Nutzung.") doc = get_dxf_file(dxf_path) msp = doc.modelspace() else: print("DXF-Datei ist ASCII.") doc = get_dxf_file(dxf_path) msp = doc.modelspace() # Verarbeite Renamer-Blöcke print("\n" + "="*60) print("Starte Verarbeitung der Renamer-Blöcke") print("="*60 + "\n") renamed_symbols, renamer_groups = process_renamer_blocks(doc, msp, renamer_layers, error_collector) print("\n" + "="*60) print(f"Verarbeitung abgeschlossen: {len(renamed_symbols)} Symbole nummeriert") print("="*60 + "\n") # Ausgabe der Ergebnisse if renamed_symbols: print("Nummerierte Symbole:") for item in renamed_symbols: print(f" {item['old_value']} -> {item['new_value']} (Layer: {item['layer']}, Pos: {item['position']})") # Zeichne Symbol-Rahmen wenn gewünscht if args.show_symbol_frames: all_symbols = collect_all_symbols_from_groups(doc, msp, renamer_groups, renamer_layers, error_collector) if all_symbols: draw_symbol_frames(doc, msp, all_symbols) else: print("\nKeine Symbole für Rahmen-Zeichnung gefunden") # 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}") else: print("\nDry-run Modus: DXF-Datei wurde nicht überschrieben") # Schreibe JSON-Ausgabe wenn gewünscht if args.info: output_data = { 'renamed_symbols': renamed_symbols, 'total_count': len(renamed_symbols) } output_path = Path(args.info) write_json_file(output_data, output_path.parent, output_path.name) print(f"Ergebnisse in JSON gespeichert: {args.info}") # 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) # Wenn nur ein Dateiname ohne Pfad angegeben wurde, speichere im work-Ordner if error_path.parent == Path('.'): output_dir = dxf_path.parent output_filename = error_path.name else: # Absoluter oder relativer Pfad mit Verzeichnis angegeben output_dir = error_path.parent output_filename = error_path.name write_json_file(error_data, output_dir, output_filename) print(f"Fehler/Warnungen gespeichert: {output_dir / output_filename}") 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(): print("\n!!! Es sind Fehler aufgetreten !!!") exit(1) elif error_collector.has_errors_or_warnings(): print("\n(Warnungen vorhanden, aber keine kritischen Fehler)") exit(0) else: exit(0)