Mehrere Regionen können über das Blatt verteilt sein. Sammle alle zusammengehörigen Items in Gruppen

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