mehrere oft verwendete Routinen in eigene utils.py eingebaut

This commit is contained in:
2026-01-23 10:23:18 +01:00
parent 37208f61b1
commit 3deebb81f3
9 changed files with 453 additions and 175 deletions
+388 -31
View File
@@ -8,10 +8,10 @@ from pathlib import Path
import ezdxf
from ezdxf.addons import iterdxf
from shapely.geometry import Point
from ezdxf.lldxf.const import DXFStructureError
from error_collector import ErrorCollector, write_json_file
from utils import check_environment_var, check_file_in_work, dxf_is_binary, get_dxf_file
"""
@@ -56,56 +56,413 @@ Alle Sensoren, Motoren, Aktoren etc. innerhalb der zugehörigen Polylinie nehmen
"""
def check_file_in_work(work_dir: Path, filename: Path) -> tuple[Path, bool]:
fexists = True
if not filename.exists():
mypath = work_dir.joinpath(filename)
ex = mypath.exists()
if not mypath.exists():
fexists = False
else:
mypath = filename
return mypath, fexists
def read_config_layers(config_path: Path) -> list:
"""
Liest die enumerate.cfg und gibt die Layer zurück, auf denen nach Renamer-Blöcken gesucht werden soll.
Die Konfiguration hat eine einfache Liste unter [Layers]:
[Layers]
RENAMER
OTHER_LAYER
"""
layers = []
def check_environment_var(env_str: str) -> Path:
out_path = os.environ.get(env_str)
if out_path:
return Path(out_path)
with open(config_path, 'r', encoding='utf-8') as f:
in_layers_section = False
for line in f:
line = line.strip()
# Überspringe leere Zeilen und Kommentare
if not line or line.startswith('#') or line.startswith(';'):
continue
# Prüfe ob wir in der [Layers] Sektion sind
if line.startswith('['):
in_layers_section = line.lower() == '[layers]'
continue
# Wenn in [Layers], füge Layer hinzu
if in_layers_section:
layers.append(line)
return layers
def extract_block_attributes(insert) -> dict:
"""
Extrahiert alle Attribute aus einem INSERT-Block.
"""
attributes = {}
if insert.dxftype() != 'INSERT':
return attributes
if insert.has_attrib:
for attrib in insert.attribs:
tag = attrib.dxf.tag
value = attrib.dxf.text
attributes[tag] = value
return attributes
def get_boundary_geometry(doc, insert):
"""
Sucht im Block nach einem Rechteck oder einer geschlossenen Polylinie.
Gibt die Eckpunkte zurück.
"""
block_layout = doc.blocks.get(insert.dxf.name)
# Suche nach LWPOLYLINE, POLYLINE oder geschlossenen Linien
for entity in block_layout:
if entity.dxftype() == 'LWPOLYLINE':
# Get points and transform them
points = list(entity.get_points())
# Prüfe ob geschlossen (entweder Flag gesetzt oder erster == letzter Punkt)
is_closed = entity.closed or (entity.dxf.flags & 1)
if not is_closed and len(points) > 1:
# Prüfe ob erster und letzter Punkt gleich sind
first = points[0][:2]
last = points[-1][:2]
if abs(first[0] - last[0]) < 0.001 and abs(first[1] - last[1]) < 0.001:
is_closed = True
if is_closed:
# Transform points relative to insert position
insert_point = insert.dxf.insert
transformed_points = []
for p in points:
# Einfache Translation (ohne Rotation/Skalierung für ersten Ansatz)
transformed_points.append((
insert_point[0] + p[0],
insert_point[1] + p[1]
))
return transformed_points
elif entity.dxftype() == 'POLYLINE':
if entity.is_closed:
points = [(v.dxf.location.x, v.dxf.location.y) for v in entity.vertices]
insert_point = insert.dxf.insert
transformed_points = []
for p in points:
transformed_points.append((
insert_point[0] + p[0],
insert_point[1] + p[1]
))
return transformed_points
return None
def point_in_polygon(point, polygon):
"""
Prüft, ob ein Punkt innerhalb eines Polygons liegt (Ray-casting Algorithmus).
"""
x, y = point
n = len(polygon)
inside = False
p1x, p1y = polygon[0]
for i in range(1, n + 1):
p2x, p2y = polygon[i % n]
if y > min(p1y, p2y):
if y <= max(p1y, p2y):
if x <= max(p1x, p2x):
if p1y != p2y:
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
if p1x == p2x or x <= xinters:
inside = not inside
p1x, p1y = p2x, p2y
return inside
def find_symbols_in_boundary(doc, msp, boundary, target_layers, attributes):
"""
Findet alle Symbole (INSERT-Blöcke) innerhalb des angegebenen Bereichs auf den Ziel-Layern.
"""
symbols = []
# Bestimme welche Layer durchsucht werden sollen
search_layers = []
for i in range(1, 4): # NAME1, NAME2, NAME3
layer_key = f"LAYER_NAME{i}"
if layer_key in attributes and attributes[layer_key]:
search_layers.append(attributes[layer_key])
# Falls nur NAME/LAYER_NAME vorhanden
if "LAYER_NAME" in attributes and attributes["LAYER_NAME"]:
search_layers.append(attributes["LAYER_NAME"])
# Durchsuche alle INSERT-Blöcke
for entity in msp.query('INSERT'):
if entity.dxf.layer not in search_layers:
continue
# Prüfe ob der Block Attribute hat
if not entity.has_attrib:
continue
# Hole Position des Symbols
pos = entity.dxf.insert
point = (pos[0], pos[1])
# Prüfe ob innerhalb des Bereichs
if point_in_polygon(point, boundary):
# Extrahiere Attribute des Symbols
symbol_attribs = extract_block_attributes(entity)
# Prüfe ob es ein Template ist (enthält @)
has_template = False
for value in symbol_attribs.values():
if '@' in str(value):
has_template = True
break
if has_template:
symbols.append({
'entity': entity,
'position': point,
'attributes': symbol_attribs,
'layer': entity.dxf.layer
})
return symbols
def sort_symbols_by_direction(symbols, direction):
"""
Sortiert die Symbole nach der angegebenen Richtung.
TOP_BOTTOM: nach Y absteigend, dann X
BOTTOM_TOP: nach Y aufsteigend, dann X
LEFT_RIGHT: nach X aufsteigend, dann Y
RIGHT_LEFT: nach X absteigend, dann Y
"""
if direction == "TOP_BOTTOM":
return sorted(symbols, key=lambda s: (-s['position'][1], s['position'][0]))
elif direction == "BOTTOM_TOP":
return sorted(symbols, key=lambda s: (s['position'][1], s['position'][0]))
elif direction == "LEFT_RIGHT":
return sorted(symbols, key=lambda s: (s['position'][0], s['position'][1]))
elif direction == "RIGHT_LEFT":
return sorted(symbols, key=lambda s: (-s['position'][0], s['position'][1]))
else:
print(f"Umgebungsvariable {env_str} ist nicht gesetzt oder leer.")
exit()
# Fallback: LEFT_RIGHT
return sorted(symbols, key=lambda s: (s['position'][0], s['position'][1]))
def enumerate_symbols(symbols, attributes):
"""
Nummeriert die Symbole durch und ersetzt die @@ Platzhalter.
"""
counter = 1
renamed = []
for symbol in symbols:
symbol_attribs = symbol['attributes']
layer = symbol['layer']
# Finde das passende NAME-Template für diesen Layer
name_template = None
for i in range(1, 4):
layer_key = f"LAYER_NAME{i}"
name_key = f"NAME{i}"
if layer_key in attributes and attributes[layer_key] == layer:
if name_key in attributes:
name_template = attributes[name_key]
break
# Falls nur NAME/LAYER_NAME vorhanden
if not name_template and "LAYER_NAME" in attributes and attributes["LAYER_NAME"] == layer:
if "NAME" in attributes:
name_template = attributes["NAME"]
if not name_template:
continue
# Ersetze @-Zeichen durch Zahlen
# Zähle wie viele @ im Template sind
at_count = name_template.count('@')
number_str = str(counter).zfill(at_count)
new_name = name_template.replace('@' * at_count, number_str)
# Aktualisiere Attribute im Symbol
for attrib in symbol['entity'].attribs:
old_value = attrib.dxf.text
if '@' in old_value:
# Ersetze @ durch die Nummer
new_value = old_value.replace('@' * at_count, number_str)
attrib.dxf.text = new_value
renamed.append({
'position': symbol['position'],
'layer': layer,
'old_value': old_value,
'new_value': new_value
})
counter += 1
return renamed
def process_renamer_blocks(doc, msp, renamer_layers, error_collector):
"""
Verarbeitet alle Renamer-Blöcke auf den angegebenen Layern.
"""
all_renamed = []
for layer in renamer_layers:
print(f"Durchsuche Layer: {layer}")
# Finde alle INSERT-Blöcke auf diesem Layer
for insert in msp.query(f'INSERT[layer=="{layer}"]'):
# Extrahiere Attribute
attributes = extract_block_attributes(insert)
if not attributes:
print(f" Block ohne Attribute gefunden an Position {insert.dxf.insert}")
continue
# Prüfe ob es ein Renamer-Block ist
has_name = "NAME" in attributes or "NAME1" in attributes
has_direction = "DIRECTION" in attributes
if not (has_name and has_direction):
continue
print(f" Renamer-Block gefunden: {attributes.get('NAME', attributes.get('NAME1', 'UNKNOWN'))}")
print(f" Direction: {attributes.get('DIRECTION', 'UNKNOWN')}")
print(f" Kennzeichnung: {attributes.get('KENNZEICHNUNG', 'N/A')}")
# Finde Boundary (Rechteck oder Polylinie)
boundary = get_boundary_geometry(doc, insert)
if not boundary:
error_msg = f"Keine Polylinie/Rechteck im Renamer-Block an Position {insert.dxf.insert} gefunden"
print(f" WARNUNG: {error_msg}")
error_collector.add_warnings({"missing_boundary": error_msg})
continue
print(f" Boundary gefunden mit {len(boundary)} Punkten")
# Finde Symbole innerhalb des Bereichs
symbols = find_symbols_in_boundary(doc, msp, boundary, renamer_layers, attributes)
print(f" {len(symbols)} Template-Symbole gefunden")
if not symbols:
continue
# Sortiere nach Richtung
direction = attributes.get("DIRECTION", "LEFT_RIGHT")
sorted_symbols = sort_symbols_by_direction(symbols, direction)
# Nummeriere durch
renamed = enumerate_symbols(sorted_symbols, attributes)
all_renamed.extend(renamed)
print(f" {len(renamed)} Symbole nummeriert")
return all_renamed
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='fetches the x/y positions from a dxf file', prog='getpositions')
parser.add_argument('-f', '--filename', action='store', required=True, default="ST_6300_Steuerungstestlayout1_neueBloecke.dwg", help='which file should be fetched', metavar='myfile.dxf')
parser.add_argument('-w', '--write', action='store', help='write results into a json file')
parser = argparse.ArgumentParser(description='Nummeriert Symbole in DXF-Dateien basierend auf Renamer-Blöcken', prog='create_numbers')
parser.add_argument('-f', '--filename', action='store', required=True, help='DXF-Datei die verarbeitet werden soll', metavar='myfile.dxf')
parser.add_argument('-e', '--errorfile', action='store', required=False, help='JSON-Datei für Fehler und Warnungen', metavar='errors.json')
parser.add_argument('-w', '--write', action='store', help='Schreibe Ergebnisse der Nummerierung in eine JSON-Datei')
parser.add_argument('-d', '--dryrun', action='store_true', help='Symbole nicht in der DXF-Datei überschreiben, nur Ausgabe auf Konsole')
args = parser.parse_args()
out_dir = check_environment_var('PROJECT_DATA')
work_dir = check_environment_var('PROJECT_WORK')
config_dir = check_environment_var("PROJECT_CFG")
# Initialisiere ErrorCollector
error_collector = ErrorCollector()
filename = Path(args.filename)
if not filename.suffix == ".dxf":
print("only available for .dxf files")
exit()
print("Nur für .dxf Dateien verfügbar")
exit(1)
(dxf_path, dexists) = check_file_in_work(work_dir, filename)
if dexists == False:
print("no such file ")
print(f"Datei nicht gefunden: {filename}")
parser.print_help()
exit()
exit(1)
if dxf_is_binary(dxf_path): # Wenn dxf eine binary ist, dann komplett parsen und modelspace anlegen
print("Given .dxf-file is binary dxf. Proceeding to read file. Watch RAM-usage.")
# Lese Konfiguration
config_path = Path(config_dir) / "enumerate.cfg"
if not config_path.exists():
print(f"Konfigurationsdatei nicht gefunden: {config_path}")
exit(1)
renamer_layers = read_config_layers(config_path)
print(f"Konfigurierte Layer: {renamer_layers}")
if not renamer_layers:
print("Keine Layer in der Konfiguration gefunden")
exit(1)
# Lese DXF-Datei
if dxf_is_binary(dxf_path):
print("DXF-Datei ist binär. Lese komplette Datei. Achten Sie auf RAM-Nutzung.")
doc = get_dxf_file(dxf_path)
msp = doc.modelspace()
use_iter = False
else:
print("Given .dxf-file is ASCII-dxf. Proceeding to use iterative functions. Process may take longer.")
use_iter = True
print("DXF-Datei ist ASCII.")
doc = get_dxf_file(dxf_path)
msp = doc.modelspace()
# Verarbeite Renamer-Blöcke
print("\n" + "="*60)
print("Starte Verarbeitung der Renamer-Blöcke")
print("="*60 + "\n")
renamed_symbols = process_renamer_blocks(doc, msp, renamer_layers, error_collector)
print("\n" + "="*60)
print(f"Verarbeitung abgeschlossen: {len(renamed_symbols)} Symbole nummeriert")
print("="*60 + "\n")
# Ausgabe der Ergebnisse
if renamed_symbols:
print("Nummerierte Symbole:")
for item in renamed_symbols:
print(f" {item['old_value']} -> {item['new_value']} (Layer: {item['layer']}, Pos: {item['position']})")
# Speichere DXF-Datei wenn nicht dry-run
if not args.dryrun:
output_path = dxf_path.parent / f"{dxf_path.stem}_numbered{dxf_path.suffix}"
doc.saveas(output_path)
print(f"\nNummerierte DXF-Datei gespeichert: {output_path}")
else:
parser.print_help()
print("\nDry-run Modus: DXF-Datei wurde nicht überschrieben")
# Schreibe JSON-Ausgabe wenn gewünscht
if args.write:
output_data = {
'renamed_symbols': renamed_symbols,
'total_count': len(renamed_symbols)
}
output_path = Path(args.write)
write_json_file(output_data, output_path.parent, output_path.name)
print(f"Ergebnisse in JSON gespeichert: {args.write}")
# Schreibe Fehler-Datei
if args.errorfile:
error_data = error_collector.get_all_issues()
error_path = Path(args.errorfile)
write_json_file(error_data, error_path.parent, error_path.name)
print(f"Fehler/Warnungen gespeichert: {args.errorfile}")
# Exit-Code basierend auf Fehlern
if error_collector.has_errors():
print("\n!!! Es sind Fehler aufgetreten !!!")
exit(1)
elif error_collector.has_errors_or_warnings():
print("\n(Warnungen vorhanden, aber keine kritischen Fehler)")
exit(0)
else:
print("\nKeine Fehler oder Warnungen")
exit(0)