From 222a2efbf6a98340c769b08eeb07c8e43c1292ac Mon Sep 17 00:00:00 2001 From: mistangl Date: Fri, 30 Jan 2026 15:39:35 +0100 Subject: [PATCH] =?UTF-8?q?utils=20enth=C3=A4lt=20jetzt=20generelle=20Rout?= =?UTF-8?q?inen=20der=20verschiedenen=20Programme.=20create=5Fexample=20re?= =?UTF-8?q?factored.=20utils=20wird=20jetzt=20entsprechend=20=C3=BCberall?= =?UTF-8?q?=20importiert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/launch.json | 12 ++ cfg/create_tests.json | 93 +++++++-- lib/create_example.py | 78 ++++++-- lib/create_numbers.py | 441 +++++++++++++++++++++++++++++------------- lib/getpositions.py | 31 +-- lib/utils.py | 381 ++++++++++++++++++++++++++++++++++++ 6 files changed, 832 insertions(+), 204 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index a82ba1c..ce92a78 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -370,5 +370,17 @@ "--write", "BGMG_symbols.json" ] }, + { + "name": "create_numbers.py with BGMG-UndefSymbols.dxf", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "args": [ + "--filename", "BGMG-UndefSymbols.dxf", + "--errorfile", "BGMG-UndefSymbols_errors.json", + "--write", "BGMG-UndefSymbols_symbols.json" + ] + } ] } diff --git a/cfg/create_tests.json b/cfg/create_tests.json index f669b86..96dc210 100644 --- a/cfg/create_tests.json +++ b/cfg/create_tests.json @@ -163,7 +163,7 @@ "BGMG": { "description": "Test-Szene mit drei Rechtecken für BG-1@@@ und BG-2@@@ Symbole mit mehreren Layern", - "ma_groups": [ + "bg_groups": [ { "name": "BG-1@@_frame1", "count": 1, @@ -248,8 +248,8 @@ "renaming_frames": [ { "comment": "Rahmen 1: Links unten - 1x BG-1@@@ (Polylinie) - Angepasst für left_bottom_corner", - "name": "BG-1@@@", - "name2": "MB-1@@@", + "name": "BG-1@@", + "name2": "MB-1@@", "kennzeichnung": "", "layer_name1": "ILS_Eingang", "layer_name2": "ILS_Ausgang", @@ -293,9 +293,9 @@ "BGMG-UndefSymbols": { "description": "Test-Szene wie BGMB, aber mit nur 2x BG-1@@ Symbolen, um Undefiniert-Symbole zu testen", - "ma_groups": [ + "bg_groups": [ { - "name": "BG-1@@_frame1", + "name": "BG-1@@@_frame1", "count": 5, "layout_type": "horizontal", "base_x": 1000, @@ -305,10 +305,10 @@ ], "renaming_frames": [ { - "comment": "Rahmen: 2x BG-1@@@ (Polylinie) - Angepasst für linke Seite - Akzeptiert alle Layer", + "comment": "Rahmen: 2x BG-1@@@ (Polylinie) - Angepasst für linke Seite", "name": "BG-1@@@", "kennzeichnung": "", - "layer_name": "", + "layer_name1": "ILS_Eingang", "direction": "LEFT_RIGHT", "x": 800, "y": 800, @@ -317,12 +317,12 @@ "use_polyline": true }, { - "comment": "Rahmen: 2x BG-1@@@ (Polylinie) - Angepasst für rechte Seite - Akzeptiert alle Layer", + "comment": "Rahmen: 2x BG-1@@@ (Polylinie) - Angepasst für rechte Seite", "name": "BG-1@@@", "kennzeichnung": "", - "layer_name": "", + "layer_name1": "ILS_Eingang", "direction": "LEFT_RIGHT", - "x": 5000, + "x": 6000, "y": 800, "width": 3210, "height": 600, @@ -338,7 +338,9 @@ "layer": "ILS_MOTOR", "attributes": { "SPS": "1", - "VERW": "Motor", + "IO": "MA0000", + "VERW": "ILSMotor", + "BEZEICHNUNG": "Motor MA0000", "ARTIKELNR": "790902001", "ARTIKELBEZEICHN": "E-Teile für SEW Motor ASE1-HAN10ES-BG", "KENNZEICHNUNG": "=A01+UH00", @@ -348,7 +350,8 @@ "TEXT-ES": "", "TEXT-F": "", "TEXT-I": "", - "ID": "" + "ID": "", + "REALE_POSITION": "x" }, "dimensions": { "_comment": "Symbol-Dimensionen für Layout-Berechnung. Feste Werte basierend auf IO-Feld Analyse (6 Zeichen MA-1@@: 1210 × 381)", @@ -357,18 +360,70 @@ } }, "pot_defaults": { - "_description": "Standard-Werte für POT-Symbole", - "block_name": "Erdungspunkt", - "layer": "POT-Erdung", + "_description": "Standard-Werte für POT-Symbole. Basiert auf Analyse von ILS_POT-Erdung aus Nummerierung_IO.dxf", + "block_name": "ILS_POT-Erdung", + "layer": "ILS_POT-Erdung", "attributes": { "REALE_POSITION": "x", "KENNZEICHNUNG": "=A01+UH00-X01", - "NAME": "POT-FS-01-02" + "NAME": "POT-1@@@" }, "dimensions": { - "_comment": "Symbol-Dimensionen für Layout-Berechnung.", - "width": 1210, - "height": 300 + "_comment": "Symbol-Dimensionen für POT-Erdung. Basiert auf Block-Analyse: 816 × 618", + "width": 820, + "height": 620 + } + }, + "bg_defaults": { + "_description": "Standard-Werte für BG/MB-Symbole (Eingänge/Ausgänge). Basiert auf Analyse von BG3240 aus easy.dxf", + "block_name": "io", + "layer": "ILS_Eingang", + "attributes": { + "SPS": "1", + "IO": "BG0000", + "VERW": "Sensor", + "BEZEICHNUNG": "", + "ARTIKELNR": "", + "ARTIKELBEZEICHN": "", + "KENNZEICHNUNG": "=A01+UC0101", + "TEXT-D": "", + "TEXT-E": "", + "TEXT-ES": "", + "TEXT-F": "", + "TEXT-I": "", + "ID": "", + "REALE_POSITION": "x" + }, + "dimensions": { + "_comment": "Symbol-Dimensionen für BG/MB-Symbole. 7 Zeichen (BG-1@@@): 1410 × 381", + "width": 1410, + "height": 381 + } + }, + "mb_defaults": { + "_description": "Standard-Werte für MB-Symbole (Ausgänge). Verwendet dieselbe Struktur wie BG, nur anderer Layer", + "block_name": "io", + "layer": "ILS_Ausgang", + "attributes": { + "SPS": "1", + "IO": "MB0000", + "VERW": "Aktor", + "BEZEICHNUNG": "", + "ARTIKELNR": "", + "ARTIKELBEZEICHN": "", + "KENNZEICHNUNG": "=A01+UC0101", + "TEXT-D": "", + "TEXT-E": "", + "TEXT-ES": "", + "TEXT-F": "", + "TEXT-I": "", + "ID": "", + "REALE_POSITION": "x" + }, + "dimensions": { + "_comment": "Symbol-Dimensionen für MB-Symbole. 7 Zeichen (MB-1@@@): 1410 × 381", + "width": 1410, + "height": 381 } }, "general": { diff --git a/lib/create_example.py b/lib/create_example.py index 8c2df1f..58df837 100644 --- a/lib/create_example.py +++ b/lib/create_example.py @@ -635,18 +635,17 @@ class TestDataGenerator: return offset_map.get(insert_point, (0, 0)) # Default: left_top_corner - def _generate_ma_group(self, group, ma_defaults, symbol_width, symbol_height, spacing_factor, y_offsets_list, insert_point='left_top_corner'): + def _generate_symbol_group(self, group, symbol_defaults, symbol_type='MA', spacing_factor=1.2, y_offsets_list=[0, -50, 50], insert_point='left_bottom_corner'): """ - Generiert eine Gruppe von MA-Symbolen + Generiert eine Gruppe von Symbolen (MA, BG, MB, POT, etc.) Args: group: Dict mit Gruppen-Config (name, count, layout_type, base_x, base_y, spacing) - ma_defaults: Dict mit MA-Default-Attributen - symbol_width: Feste Breite des Symbols - symbol_height: Feste Höhe des Symbols + symbol_defaults: Dict mit Symbol-Default-Attributen (ma_defaults, bg_defaults, etc.) + symbol_type: Typ des Symbols ('MA', 'BG', 'MB', 'POT') für BEZEICHNUNG 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") + insert_point: Einfügepunkt des Blocks (z.B. "left_bottom_corner", "center") """ name = group['name'] count = group['count'] @@ -657,19 +656,33 @@ class TestDataGenerator: # Extrahiere IO-Pattern aus Name (z.B. "MA-1@@_top" -> "MA-1@@") io_pattern = name.split('_')[0] + # Hole Dimensionen aus defaults + dimensions = symbol_defaults.get('dimensions', {}) + symbol_width = dimensions.get('width', 1210) + symbol_height = dimensions.get('height', 381) + # Berechne Spacing if layout_type in ['horizontal', 'horizontal_offset']: actual_spacing = symbol_width * spacing_factor else: actual_spacing = 0 - # Hole Attribute aus ma_defaults - attributes = ma_defaults.get('attributes', {}) - layer = ma_defaults.get('layer', 'ILS_MOTOR') + # Hole Attribute und Block-Info aus defaults + attributes = symbol_defaults.get('attributes', {}) + layer = symbol_defaults.get('layer', 'ILS_MOTOR') + block_name = symbol_defaults.get('block_name', 'io') # Berechne Offset basierend auf Einfügepunkt offset_x, offset_y = self._calculate_insert_offset(insert_point, symbol_width, symbol_height) + # Bestimme BEZEICHNUNG basierend auf Symbol-Typ + bezeichnung_prefix = { + 'MA': 'Motor', + 'BG': 'Eingang', + 'MB': 'Ausgang', + 'POT': 'Erdung' + }.get(symbol_type, 'Symbol') + # Generiere Symbole basierend auf Layout-Typ for i in range(count): if layout_type == 'single': @@ -692,7 +705,7 @@ class TestDataGenerator: # 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', + block_name, insert=(left_top_x, left_top_y - symbol_height), dxfattribs={'layer': layer} ) @@ -700,7 +713,10 @@ class TestDataGenerator: # Setze Attribute (dynamisch IO und BEZEICHNUNG) attrib_values = attributes.copy() attrib_values['IO'] = io_pattern - attrib_values['BEZEICHNUNG'] = f"Motor {io_pattern}" + + # Setze BEZEICHNUNG nur wenn im Template vorhanden + if 'BEZEICHNUNG' in attrib_values: + attrib_values['BEZEICHNUNG'] = f"{bezeichnung_prefix} {io_pattern}" blockref.add_auto_attribs(attrib_values) @@ -724,26 +740,48 @@ class TestDataGenerator: scene_name: Name der Testszene in der Config """ scene = self.config['test_scenes'][scene_name] - ma_defaults = self.config.get('ma_defaults', {}) general = self.config.get('general', {}) - # Hole Dimensions aus Config (feste Werte) - dimensions = ma_defaults.get('dimensions', {}) - symbol_width = dimensions.get('width', 1210) - symbol_height = dimensions.get('height', 381) - # Hole Layout-Einstellungen layout = general.get('layout', {}) 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) + # Hole Einfügepunkt aus Config (default: left_bottom_corner) ma_frame = general.get('ma_frame', {}) - insert_point = ma_frame.get('insert_point', 'left_top_corner') + insert_point = ma_frame.get('insert_point', 'left_bottom_corner') # Generiere MA-Gruppen aus Config + ma_defaults = self.config.get('ma_defaults', {}) for group in scene.get('ma_groups', []): - self._generate_ma_group(group, ma_defaults, symbol_width, symbol_height, spacing_factor, y_offsets_list, insert_point) + self._generate_symbol_group(group, ma_defaults, symbol_type='MA', + spacing_factor=spacing_factor, + y_offsets_list=y_offsets_list, + insert_point=insert_point) + + # Generiere BG-Gruppen aus Config (Eingänge) + bg_defaults = self.config.get('bg_defaults', {}) + for group in scene.get('bg_groups', []): + self._generate_symbol_group(group, bg_defaults, symbol_type='BG', + spacing_factor=spacing_factor, + y_offsets_list=y_offsets_list, + insert_point=insert_point) + + # Generiere MB-Gruppen aus Config (Ausgänge) + mb_defaults = self.config.get('mb_defaults', {}) + for group in scene.get('mb_groups', []): + self._generate_symbol_group(group, mb_defaults, symbol_type='MB', + spacing_factor=spacing_factor, + y_offsets_list=y_offsets_list, + insert_point=insert_point) + + # Generiere POT-Gruppen aus Config (Erdungspunkte) + pot_defaults = self.config.get('pot_defaults', {}) + for group in scene.get('pot_groups', []): + self._generate_symbol_group(group, pot_defaults, symbol_type='POT', + spacing_factor=spacing_factor, + y_offsets_list=y_offsets_list, + insert_point=insert_point) # Generiere Renamer-Rahmen aus Config for frame_config in scene.get('renaming_frames', []): diff --git a/lib/create_numbers.py b/lib/create_numbers.py index 944cb94..0f5b12c 100644 --- a/lib/create_numbers.py +++ b/lib/create_numbers.py @@ -3,7 +3,10 @@ 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 +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 +) from symbol_frames import draw_symbol_frames @@ -80,56 +83,6 @@ def read_config_layers(config_path: Path) -> list: return layers -def extract_block_attributes(doc, insert, error_collector=None) -> 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 - error_collector: Optional ErrorCollector für Warnungen - - Returns: - Dictionary mit allen gefundenen Attributen - """ - attributes = {} - if insert.dxftype() != 'INSERT': - return attributes - - # 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: - error_msg = f"Fehler beim Extrahieren der Attribute aus Block '{insert.dxf.name}': {e}" - if error_collector: - error_collector.add_warnings({"attribute_extraction_error": error_msg}) - - return attributes - - def is_rectangle_polyline(points): """ Prüft, ob eine Liste von Punkten ein Rechteck darstellt. @@ -374,6 +327,26 @@ def enumerate_symbols(symbols, attributes): 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. @@ -516,11 +489,153 @@ def collect_and_group_renamer_blocks(doc, msp, renamer_layers, error_collector): 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 """ @@ -538,55 +653,15 @@ def process_renamer_groups(doc, msp, renamer_layers, renamer_groups, error_colle 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, 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}) + 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 = [] - for i in range(1, 4): - layer_key = f"LAYER_NAME{i}" - if layer_key in reference_attributes and reference_attributes[layer_key]: - layer_names.append(reference_attributes[layer_key]) - if "LAYER_NAME" in reference_attributes and reference_attributes["LAYER_NAME"]: - layer_names.append(reference_attributes["LAYER_NAME"]) - - renamer_positions = [f"({insert.dxf.insert[0]:.1f}, {insert.dxf.insert[1]:.1f})" - for insert, _, _, _ in group_blocks] - error_msg = ( - 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)}" + 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}) @@ -603,27 +678,25 @@ def process_renamer_groups(doc, msp, renamer_layers, renamer_groups, error_colle return all_renamed -def check_symbols_in_boundaries(doc, msp, renamer_groups, error_collector): + +def find_symbols_with_at_in_io(doc, msp, error_collector): """ - Dritter Pass: Prüft ob alle Symbole mit "@" im IO in einem Renamer-Bereich liegen. + 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 """ - 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)) - - # Finde alle Symbole mit "@" im IO-Attribut 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): @@ -636,32 +709,120 @@ def check_symbols_in_boundaries(doc, msp, renamer_groups, error_collector): '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 - for boundary, attributes in all_boundaries: - # Prüfe ob Symbol auf dem richtigen Layer ist - search_layers = [] - for i in range(1, 4): - layer_key = f"LAYER_NAME{i}" - if layer_key in attributes and attributes[layer_key]: - search_layers.append(attributes[layer_key]) - if "LAYER_NAME" in attributes and attributes["LAYER_NAME"]: - search_layers.append(attributes["LAYER_NAME"]) - - if symbol['layer'] not in search_layers: - continue - - # Prüfe ob Symbol innerhalb des Bereichs liegt - if point_in_polygon(symbol['position'], boundary): + 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: - symbols_not_in_any_boundary.append(symbol) - + 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: @@ -768,7 +929,7 @@ if __name__ == '__main__': ) 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('-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') @@ -844,22 +1005,32 @@ if __name__ == '__main__': print("\nDry-run Modus: DXF-Datei wurde nicht überschrieben") # Schreibe JSON-Ausgabe wenn gewünscht - if args.write: + if args.info: output_data = { 'renamed_symbols': renamed_symbols, 'total_count': len(renamed_symbols) } - output_path = Path(args.write) + output_path = Path(args.info) write_json_file(output_data, output_path.parent, output_path.name) - print(f"Ergebnisse in JSON gespeichert: {args.write}") + 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) - write_json_file(error_data, error_path.parent, error_path.name) - print(f"Fehler/Warnungen gespeichert: {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" diff --git a/lib/getpositions.py b/lib/getpositions.py index 1644b8c..e81f895 100644 --- a/lib/getpositions.py +++ b/lib/getpositions.py @@ -20,6 +20,7 @@ from utils import ( merge_two_dicts, to_json, write_results, + extract_attributes_with_positions as attribs_to_dicts, ) @@ -371,36 +372,6 @@ def create_new_id(id_: str, dict1: dict, dict2: dict, error_collector: ErrorColl return f"{id_}@UNKNOWN" # Fallback für fehlenden SPS-Präfix return f"{id_}@{sps_praefix}" -def attribs_to_dicts(insert_iterable) -> tuple[list, list]: - """ - Wandelt eine Iterable von INSERT-Objekten in zwei Listen um: - - all_inserts: Liste von Dictionaries mit Attribut-Tags und deren Textwerten - - all_positions: Liste von Dictionaries mit Attribut-Tags und deren (x, y, z)-Positionen (jeweils gerundet auf eine Nachkommastelle) - - Jeder Eintrag in den Listen entspricht einem INSERT-Block mit Attributen. - Blöcke ohne Attribute werden übersprungen. - """ - all_inserts = list() - all_positions = list() - for insert in insert_iterable: - if insert.dxftype() != 'INSERT': - continue - itemdata = dict() - positions = dict() - typ = 'unknown' - for attrib in insert.attribs: - if len(insert.attribs) == 0: - continue # Überspringe Blöcke ohne Attribute - attr_tag = attrib.dxf.tag - attr_text = attrib.dxf.text - pos = attrib.dxf.insert - itemdata[attr_tag] = attr_text - positions[attr_tag] = (round(pos.x, 1), round(pos.y, 1), round(pos.z, 1)) - if len(itemdata) > 0: - all_inserts.append(itemdata) - all_positions.append(positions) - return all_inserts, all_positions - def create_mappings(positions: dict) -> tuple[dict, dict]: unterverteiler_pfad = "" dnamen = dict() diff --git a/lib/utils.py b/lib/utils.py index 22cd5cc..7f614d7 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -171,6 +171,387 @@ def get_dxf_file(filepath: Path): return doc +# ============================================================================ +# ezdxf Operations (High-Level DXF Helpers) +# ============================================================================ + +def extract_insert_attributes(insert, error_collector=None) -> 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: + insert: INSERT-Entity des Blocks + error_collector: Optional ErrorCollector für Warnungen + + Returns: + Dictionary mit allen gefundenen Attributen {tag: text} + """ + attributes = {} + if insert.dxftype() != 'INSERT': + return attributes + + # Prüfe zuerst ob der Block direkt Attribute hat (alte Struktur) + 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 + # Dies erfordert das doc-Objekt, daher nur wenn verfügbar + if hasattr(insert, 'doc') and insert.doc: + try: + block_layout = insert.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: + if error_collector: + error_msg = f"Fehler beim Extrahieren der Attribute aus Block '{insert.dxf.name}': {e}" + if hasattr(error_collector, 'add_warnings'): + error_collector.add_warnings({"attribute_extraction_error": error_msg}) + + return attributes + + +def extract_insert_attributes_with_doc(doc, insert, error_collector=None) -> dict: + """ + Extrahiert alle Attribute aus einem INSERT-Block mit explizitem doc-Parameter. + Empfohlen für neue Code, da es zweistufige Blockstrukturen besser unterstützt. + + Args: + doc: DXF-Dokument + insert: INSERT-Entity des Blocks + error_collector: Optional ErrorCollector für Warnungen + + Returns: + Dictionary mit allen gefundenen Attributen {tag: text} + """ + attributes = {} + if insert.dxftype() != 'INSERT': + return attributes + + # Prüfe zuerst ob der Block direkt Attribute hat (alte Struktur) + 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: + if error_collector: + error_msg = f"Fehler beim Extrahieren der Attribute aus Block '{insert.dxf.name}': {e}" + if hasattr(error_collector, 'add_warnings'): + error_collector.add_warnings({"attribute_extraction_error": error_msg}) + + return attributes + + +def ensure_layer_exists(doc, layer_name: str, color: int = None, linetype: str = None, + lineweight: int = None, description: str = None): + """ + Stellt sicher, dass ein Layer existiert und erstellt ihn falls nötig. + + Args: + doc: DXF-Dokument + layer_name: Name des Layers + color: ACI Color Index (1-255), optional + linetype: Name des Linientyps (z.B. 'CONTINUOUS', 'DASHED'), optional + lineweight: Linienbreite in mm*100 (z.B. 25 für 0.25mm), optional + description: Layer-Beschreibung, optional + + Returns: + Layer-Objekt (existierend oder neu erstellt) + """ + if layer_name in doc.layers: + layer = doc.layers.get(layer_name) + else: + layer = doc.layers.add(layer_name) + + # Setze optionale Attribute + if color is not None: + layer.color = color + if linetype is not None: + layer.dxf.linetype = linetype + if lineweight is not None: + layer.dxf.lineweight = lineweight + if description is not None: + layer.dxf.description = description + + return layer + + +def add_rectangle_lwpolyline(msp, x: float, y: float, width: float, height: float, + layer: str = '0', color: int = None, rgb: tuple = None, + closed: bool = True): + """ + Fügt eine rechteckige LWPOLYLINE hinzu. + + Args: + msp: Modelspace des DXF-Dokuments + x: X-Koordinate der unteren linken Ecke + y: Y-Koordinate der unteren linken Ecke + width: Breite des Rechtecks + height: Höhe des Rechtecks + layer: Layer-Name (default: '0') + color: ACI Color Index (1-255), optional + rgb: RGB-Tuple (r, g, b) mit Werten 0-255, optional + closed: Ob die Polylinie geschlossen sein soll (default: True) + + Returns: + LWPOLYLINE-Entity + """ + points = [ + (x, y), + (x + width, y), + (x + width, y + height), + (x, y + height) + ] + + polyline = msp.add_lwpolyline(points, close=closed) + polyline.dxf.layer = layer + + if color is not None: + polyline.dxf.color = color + if rgb is not None: + polyline.rgb = rgb + + return polyline + + +def add_text_with_alignment(msp, text: str, x: float, y: float, + height: float = 50, layer: str = '0', + color: int = None, halign: int = 0, valign: int = 0, + rotation: float = 0.0, style: str = 'Standard'): + """ + Fügt einen Text mit Ausrichtung hinzu. + + Args: + msp: Modelspace des DXF-Dokuments + text: Textinhalt + x: X-Koordinate + y: Y-Koordinate + height: Texthöhe (default: 50) + layer: Layer-Name (default: '0') + color: ACI Color Index (1-255), optional + halign: Horizontale Ausrichtung (0=links, 1=mitte, 2=rechts) + valign: Vertikale Ausrichtung (0=basis, 1=unten, 2=mitte, 3=oben) + rotation: Rotation in Grad (default: 0.0) + style: Textstil-Name (default: 'Standard') + + Returns: + TEXT-Entity + """ + text_entity = msp.add_text(text, height=height) + text_entity.dxf.layer = layer + text_entity.dxf.style = style + text_entity.dxf.rotation = rotation + + if color is not None: + text_entity.dxf.color = color + + # Setze Ausrichtung + text_entity.set_placement((x, y), align=f"{'LEFT' if halign == 0 else 'CENTER' if halign == 1 else 'RIGHT'}_{'BASELINE' if valign == 0 else 'BOTTOM' if valign == 1 else 'MIDDLE' if valign == 2 else 'TOP'}") + + return text_entity + + +def extract_attributes_with_positions(insert_iterable, round_decimals: int = 1): + """ + Wandelt eine Iterable von INSERT-Objekten in zwei Listen um. + Kompatibilität mit getpositions.py attribs_to_dicts(). + + Args: + insert_iterable: Iterable von INSERT-Entities (z.B. msp oder iterdxf.modelspace()) + round_decimals: Anzahl Nachkommastellen für Positions-Rundung (default: 1) + + Returns: + Tuple von (all_inserts, all_positions): + - all_inserts: Liste von Dicts mit Attribut-Tags und deren Textwerten + - all_positions: Liste von Dicts mit Attribut-Tags und deren (x, y, z)-Positionen + + Note: + Blöcke ohne Attribute werden übersprungen. + Diese Funktion ist kompatibel mit der alten attribs_to_dicts() aus getpositions.py + """ + all_inserts = [] + all_positions = [] + + for insert in insert_iterable: + if insert.dxftype() != 'INSERT': + continue + + itemdata = {} + positions = {} + + # Direkte Attribute (alte Struktur) + for attrib in insert.attribs: + if len(insert.attribs) == 0: + continue # Überspringe Blöcke ohne Attribute + attr_tag = attrib.dxf.tag + attr_text = attrib.dxf.text + pos = attrib.dxf.insert + itemdata[attr_tag] = attr_text + positions[attr_tag] = ( + round(pos.x, round_decimals), + round(pos.y, round_decimals), + round(pos.z, round_decimals) + ) + + if len(itemdata) > 0: + all_inserts.append(itemdata) + all_positions.append(positions) + + return all_inserts, all_positions + + +def add_blockref_with_attributes(msp, block_name: str, insert_point: tuple, + attributes: dict = None, layer: str = '0', + scale: float = 1.0, rotation: float = 0.0): + """ + Fügt eine Block-Referenz (INSERT) mit Attributen hinzu. + + Args: + msp: Modelspace des DXF-Dokuments + block_name: Name des Blocks + insert_point: Einfügepunkt als (x, y) oder (x, y, z) Tuple + attributes: Dictionary mit Attribut-Namen und -Werten (optional) + layer: Layer-Name (default: '0') + scale: Skalierungsfaktor (default: 1.0) + rotation: Rotation in Grad (default: 0.0) + + Returns: + INSERT-Entity mit gesetzten Attributen + """ + # Füge Block-Referenz hinzu + blockref = msp.add_blockref(block_name, insert_point) + blockref.dxf.layer = layer + + if scale != 1.0: + blockref.dxf.xscale = scale + blockref.dxf.yscale = scale + blockref.dxf.zscale = scale + + if rotation != 0.0: + blockref.dxf.rotation = rotation + + # Füge Attribute hinzu falls vorhanden + if attributes: + blockref.add_auto_attribs(attributes) + + return blockref + + +def query_entities_by_layer(msp, entity_type: str = None, layers: list = None, + exclude_layers: list = None): + """ + Filtert Entities nach Typ und Layer. + + Args: + msp: Modelspace des DXF-Dokuments + entity_type: Entity-Typ (z.B. 'INSERT', 'LINE', 'TEXT'), None für alle + layers: Liste der zu inkludierenden Layer (None = alle außer exclude_layers) + exclude_layers: Liste der zu exkludierenden Layer (None = keine Exklusion) + + Returns: + Liste der gefilterten Entities + + Examples: + # Alle INSERTs auf Layer 'ILS_MOTOR' + query_entities_by_layer(msp, 'INSERT', layers=['ILS_MOTOR']) + + # Alle Entities außer auf Layer '0' + query_entities_by_layer(msp, exclude_layers=['0']) + + # Alle LINEs auf ILS_* Layern (manuelles Filtern nötig) + all_lines = query_entities_by_layer(msp, 'LINE') + ils_lines = [e for e in all_lines if e.dxf.layer.startswith('ILS_')] + """ + # Baue Query-String + if entity_type: + entities = msp.query(entity_type) + else: + entities = msp + + # Filtere nach Layern + result = [] + for entity in entities: + layer = entity.dxf.layer + + # Prüfe Exklusion + if exclude_layers and layer in exclude_layers: + continue + + # Prüfe Inklusion + if layers is None or layer in layers: + result.append(entity) + + return result + + +def create_layers_from_config(doc, layer_definitions: dict): + """ + Erstellt mehrere Layer aus einer Konfiguration. + + Args: + doc: DXF-Dokument + layer_definitions: Dictionary mit Layer-Definitionen + Format: { + 'layer_name': { + 'color': int (optional), + 'linetype': str (optional), + 'lineweight': int (optional), + 'description': str (optional) + }, + ... + } + + Example: + layer_config = { + 'ILS_MOTOR': {'color': 1, 'description': 'Motor layer'}, + 'ILS_SENSOR': {'color': 3, 'lineweight': 25}, + 'ILS_CABLE': {'color': 5, 'linetype': 'DASHED'} + } + create_layers_from_config(doc, layer_config) + """ + for layer_name, props in layer_definitions.items(): + ensure_layer_exists( + doc, + layer_name, + color=props.get('color'), + linetype=props.get('linetype'), + lineweight=props.get('lineweight'), + description=props.get('description') + ) + + # ============================================================================ # Dictionary Operations # ============================================================================