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 """ 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 extract_block_attributes(insert) -> dict: """ Extrahiert alle Attribute aus einem INSERT-Block. """ attributes = {} if insert.dxftype() != 'INSERT': return attributes if insert.has_attrib: for attrib in insert.attribs: tag = attrib.dxf.tag value = attrib.dxf.text attributes[tag] = value return attributes def get_boundary_geometry(doc, insert): """ Sucht im Block nach einem Rechteck oder einer geschlossenen Polylinie. Gibt die Eckpunkte zurück. """ 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: # 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] )) return transformed_points 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] )) return transformed_points return 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): """ 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 if not entity.has_attrib: 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): # 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(): 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: 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 """ if direction == "TOP_BOTTOM": return sorted(symbols, key=lambda s: (-s['position'][1], s['position'][0])) elif 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])) def enumerate_symbols(symbols, attributes): """ Nummeriert die Symbole durch und ersetzt die @@ Platzhalter. """ counter = 1 renamed = [] for symbol in symbols: symbol_attribs = symbol['attributes'] 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) new_name = name_template.replace('@' * at_count, number_str) # 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 process_renamer_blocks(doc, msp, renamer_layers, error_collector): """ Verarbeitet alle Renamer-Blöcke auf den angegebenen Layern. """ all_renamed = [] 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) 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 print(f" Renamer-Block gefunden: {attributes.get('NAME', attributes.get('NAME1', 'UNKNOWN'))}") 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}) continue print(f" Boundary gefunden mit {len(boundary)} Punkten") # 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 direction = attributes.get("DIRECTION", "LEFT_RIGHT") sorted_symbols = sort_symbols_by_direction(symbols, direction) # Nummeriere durch renamed = enumerate_symbols(sorted_symbols, attributes) all_renamed.extend(renamed) print(f" {len(renamed)} Symbole nummeriert") return all_renamed if __name__ == '__main__': parser = argparse.ArgumentParser(description='Nummeriert Symbole in DXF-Dateien basierend auf Renamer-Blöcken', prog='create_numbers') 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('-w', '--write', 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') args = parser.parse_args() out_dir = check_environment_var('PROJECT_DATA') work_dir = check_environment_var('PROJECT_WORK') config_dir = check_environment_var("PROJECT_CFG") # 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) # Lese Konfiguration config_path = Path(config_dir) / "enumerate.cfg" 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 = 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']})") # 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.write: output_data = { 'renamed_symbols': renamed_symbols, 'total_count': len(renamed_symbols) } output_path = Path(args.write) 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}") # 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: print("\nKeine Fehler oder Warnungen") exit(0)