utils enthält jetzt generelle Routinen der verschiedenen Programme. create_example refactored. utils wird jetzt entsprechend überall importiert

This commit is contained in:
2026-01-30 15:39:35 +01:00
parent 09ffb5d7b2
commit 222a2efbf6
6 changed files with 832 additions and 204 deletions
+58 -20
View File
@@ -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', []):
+306 -135
View File
@@ -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"
+1 -30
View File
@@ -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()
+381
View File
@@ -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
# ============================================================================