import os import ezdxf from ezdxf.math import Vec2, BoundingBox import math import argparse import sys import shutil import configparser from utils import check_environment_var, setup_logger from pathlib import Path def get_bbox(entities): """ Berechnet die Bounding Box für eine Liste von DXF-Entities. Args: entities: Liste von DXF-Entities (ezdxf entities) Returns: Vec2 or None: Zentrum der Bounding Box als Vec2-Objekt oder None, falls keine gültige Geometrie gefunden wurde Note: Unterstützt POLYLINE, LWPOLYLINE und andere Entity-Typen. Fehlerhafte Entities werden übersprungen und protokolliert. """ min_x, min_y = float('inf'), float('inf') max_x, max_y = float('-inf'), float('-inf') for e in entities: try: if e.dxftype() == "POLYLINE": for vertex in e.vertices: if hasattr(vertex.dxf, 'location'): x, y = vertex.dxf.location.x, vertex.dxf.location.y min_x, min_y = min(min_x, x), min(min_y, y) max_x, max_y = max(max_x, x), max(max_y, y) elif e.dxftype() == "LWPOLYLINE": for x, y, *_ in e.get_points("xy"): min_x, min_y = min(min_x, x), min(min_y, y) max_x, max_y = max(max_x, x), max(max_y, y) elif e.dxftype() == "ARC": # Handle ARC entities: consider endpoints and cardinal extrema try: cx, cy = e.dxf.center.x, e.dxf.center.y except Exception: cx, cy = e.dxf.center r = float(e.dxf.radius) start_deg = float(e.dxf.start_angle) end_deg = float(e.dxf.end_angle) start = math.radians(start_deg % 360) end = math.radians(end_deg % 360) def in_sweep(a: float, s: float, e_: float) -> bool: # CCW sweep from s to e_ with wrap handling if e_ >= s: return s <= a <= e_ return a >= s or a <= e_ candidates = [] # Endpoints candidates.append((cx + r * math.cos(start), cy + r * math.sin(start))) candidates.append((cx + r * math.cos(end), cy + r * math.sin(end))) # Cardinal angles 0, 90, 180, 270 deg for ang in (0.0, math.pi/2, math.pi, 3*math.pi/2): if in_sweep(ang, start, end): candidates.append((cx + r * math.cos(ang), cy + r * math.sin(ang))) for px, py in candidates: min_x, min_y = min(min_x, px), min(min_y, py) max_x, max_y = max(max_x, px), max(max_y, py) elif e.dxftype() == "CIRCLE": # Handle CIRCLE entities via center and radius try: cx, cy = e.dsf.center.x, e.dsf.center.y except Exception: cx, cy = e.dxf.center r = float(e.dxf.radius) min_x, min_y = min(min_x, cx - r), min(min_y, cy - r) max_x, max_y = max(max_x, cx + r), max(max_y, cy + r) elif e.dxftype() == "LINE" or e.dxftype()== "Line": # Handle simple line entities by their start/end points try: sx, sy = e.dxf.start.x, e.dxf.start.y ex, ey = e.dxf.end.x, e.dxf.end.y except Exception: # Some ezdxf versions provide tuples (sx, sy), (ex, ey) = e.dxf.start, e.dxf.end min_x, min_y = min(min_x, sx, ex ), min(min_y, sy, ey) max_x, max_y = max(max_x, sx, ex), max(max_y, sy, ey) elif e.dxftype() == "SPLINE": # Approximate spline to compute bounding box points = [] try: points = e.approximate(60) except Exception: try: points = list(e.flattening(1.0)) except Exception: points = [] if points: for pt in points: try: px, py = pt.x, pt.y except Exception: px, py = pt[0], pt[1] min_x, min_y = min(min_x, px), min(min_y, py) max_x, max_y = max(max_x, px), max(max_y, py) else: box = e.bbox() if box: (x1, y1), (x2, y2) = box.extmin, box.extmax min_x, min_y = min(min_x, x1), min(min_y, y1) max_x, max_y = max(max_x, x2), max(max_y, y2) else: # Für andere Entity-Typen: Standard bbox() verwenden box = e.bbox() if box: (x1, y1), (x2, y2) = box.extmin, box.extmax min_x, min_y = min(min_x, x1), min(min_y, y1) max_x, max_y = max(max_x, x2), max(max_y, y2) except Exception as err: print(f" BBox Fehler für {e.dxftype()}: {err}") continue if min_x == float('inf'): return None, (0,0) return Vec2((min_x + max_x) / 2, (min_y + max_y) / 2), (max_x -min_x, max_y -min_y) def create_block_library(input_dir, output_file, config, logger=None): """ Erstellt eine DXF-Block-Bibliothek aus einzelnen DXF-Dateien. Diese Funktion liest alle DXF-Dateien aus einem Verzeichnis und erstellt daraus eine Bibliothek mit Blöcken. Die Blöcke werden in einem Raster angeordnet und mit Beschriftungen versehen. Fehlerhafte Dateien werden protokolliert. Args: input_dir (str): Verzeichnis mit den zu verarbeitenden DXF-Dateien output_file (str): Pfad zur zu erstellenden Bibliotheks-DXF-Datei config: ConfigParser-Objekt mit den Konfigurationswerten logger: Optionaler Logger für Logging-Ausgaben Note: - Unterstützte Entity-Typen: LINE, LWPOLYLINE, POLYLINE, SPLINE, CIRCLE, ARC - Blöcke werden zentriert und in einem 20x20 Raster angeordnet - Erstellt eine Log-Datei mit Zeitstempel für fehlerhafte Dateien - Automatische Erstellung des Output-Verzeichnisses falls nötig """ doc = ezdxf.new() msp = doc.modelspace() x_offset = 0 y_offset = 0 blocks_in_row = 0 error_files = [] processed_files = [] max_blockspacing_y = 0 for filename in os.listdir(input_dir): if not filename.lower().endswith(".dxf"): continue filepath = os.path.join(input_dir, filename) name = os.path.splitext(filename)[0] try: src_doc = ezdxf.readfile(filepath) src_msp = src_doc.modelspace() entities = list(src_msp) allowed_types = {"LINE", "LWPOLYLINE", "POLYLINE", "SPLINE", "CIRCLE", "ARC"} filtered_entities = [] insert = 0 for e in entities: if e.dxftype() in allowed_types: filtered_entities.append(e) else: print(f"{e.dxftype()} is nicht in der erlaubten Liste. Diese befindet sich in {filename}") entities = filtered_entities except Exception as e: error_msg = f"Fehler beim Lesen von {filename}: {e}" if logger: logger.error(error_msg) else: print(error_msg) error_files.append((filename, error_msg)) continue center, ausdehnung = get_bbox(entities) if center is None: error_msg = f"Keine gültige Geometrie in {filename}" if logger: logger.error(error_msg) else: print(error_msg) error_files.append((filename, error_msg)) continue if name in doc.blocks: doc.blocks.delete_block(name) blk = doc.blocks.new(name=name, base_point=(0,0)) for e in entities: try: cp = e.copy() cp.translate(-center.x, -center.y, 0) # Geometrie verschieben! blk.add_entity(cp) except Exception as err: error_msg = f"Fehler beim Verarbeiten von Entity {e.dxftype()} in {filename}: {err}" if logger: logger.error(error_msg) else: print(error_msg) error_files.append((filename, error_msg)) # Platzierung in Reihen und Spalten # Attribut-Definition (ATTDEF) hinzufügen blk.add_attdef( tag="NAME", insert=(0.2, 0.2), # Position relativ zum Blockursprung text="Default Text", ) msp.add_blockref(name, insert=(x_offset, y_offset)) # Text mit Blocknamen über dem Block # Werte aus Config holen (Block: [dxf2lib]) section = "dxf2lib" text_height = get_cfg_value(section, "text_height", DEFAULTS["text_height"]) extra_block_space_x = get_cfg_value(section, "extra_block_space_x", DEFAULTS["extra_block_space_x"]) blocks_per_row = get_cfg_value(section, "blocks_per_row", DEFAULTS["blocks_per_row"]) extra_text_space_y = get_cfg_value(section, "extra_text_space_y", DEFAULTS["extra_text_space_y"]) ausdehnung_x, ausdehung_y = ausdehnung[0], ausdehnung[1] block_spacing_y = ausdehung_y block_spacing_x = ausdehnung_x + extra_block_space_x max_blockspacing_y = max(max_blockspacing_y, block_spacing_y) msp.add_text(name, dxfattribs={'height': text_height, 'insert': (x_offset -ausdehnung_x/2 , y_offset + ausdehung_y/2 + extra_text_space_y)}) processed_files.append(filename) blocks_in_row += 1 x_offset += block_spacing_x # Abstand zwischen Blöcken in einer Reihe if blocks_in_row == blocks_per_row: blocks_in_row = 0 x_offset = 0 y_offset -= max_blockspacing_y # Neue Zeile, nach unten versetzt max_blockspacing_y = 0 if logger: logger.info(f"Bibliotheks-DXF gespeichert: {output_file}") logger.info(f"Verarbeitete Dateien: {len(processed_files)}") logger.info(f"Fehlerhafte Dateien: {len(error_files)}") if error_files: logger.warning(f"{len(error_files)} Dateien konnten nicht verarbeitet werden:") for filename, error_msg in error_files: logger.error(f"{filename}: {error_msg}") else: logger.info("Keine fehlerhaften Dateien gefunden.") else: print(f"Bibliotheks-DXF gespeichert: {output_file}") print(f"Verarbeitete Dateien: {len(processed_files)}") print(f"Fehlerhafte Dateien: {len(error_files)}") if error_files: print(f"Warnung: {len(error_files)} Dateien konnten nicht verarbeitet werden.") output_dir = output_file.parent if not output_dir.exists(): output_dir.mkdir(parents=True, exist_ok=True) doc.saveas(output_file) # Standardwerte (falls nicht in der Config) DEFAULTS = { "text_height": 20, # Schriftgröße des Texts (in DXF-Einheiten) "blocks_per_row": 20, # Anzahl Blöcke pro Zeile im Raster "extra_block_space_x" : 50, # Extra Platz damit sich Blöcke nicht überlappen "extra_text_space_y" : 50 # Abstand der Überschrift über dem Symbol } def get_cfg_value(section, key, fallback): try: return int(config.get(section, key)) except Exception: return fallback if __name__ == "__main__": # Argumentparser für Kommandozeilenoptionen parser = argparse.ArgumentParser(description="SVG/XML zu DXF Konverter") parser.add_argument('-i', '--input', type=str, help='Input-Verzeichnis mit SVG/XML-Dateien') parser.add_argument('-n', '--name', required=False, type=str, help='Name der zu erzeugenden Bibliothek (optional, wird sonst abgefragt)', default="test") if len(sys.argv) == 2 and sys.argv[1] in ("-h", "--help"): parser.print_help() sys.exit(0) args = parser.parse_args() if not args.name: args.name = input("Bitte Namen der zu erzeugenden Bibliothek eingeben: ").strip() if not args.name: print("Fehler: Kein Name angegeben. Beende.") sys.exit(1) # Verzeichnisse über Umgebungsvariablen oder Fallback if args.input: INPUT_DIR = Path(args.input) print(f"Verwende Input-Verzeichnis: {INPUT_DIR} \n") else: INPUT_DIR = check_environment_var("PROJECT_DATA") / "dxf" print(f"Kein Input-Verzeichnis angegeben, verwende Standard: {INPUT_DIR} \n") OUTPUT_FILE = check_environment_var("PROJECT_DATA") / "block_libraries" / f"{args.name}.dxf" # Prüfe und erstelle log-Verzeichnis falls nötig log_dir = check_environment_var("PROJECT_LOG") if not log_dir.exists(): log_dir.mkdir(parents=True, exist_ok=True) print(f"Log-Verzeichnis erstellt: {log_dir}") # Logger Setup logger = setup_logger(log_dir, name='dxf2lib') logger.info("=== DXF2LIB Verarbeitung gestartet ===") logger.info(f"Input-Verzeichnis: {INPUT_DIR}") logger.info(f"Output-Datei: {OUTPUT_FILE}") # Lade Config-Datei config = configparser.ConfigParser() config_path = check_environment_var("PROJECT_CFG") / "allgemein.cfg" config.read(config_path, encoding="utf-8") logger.info(f"Config-Datei geladen: {config_path}") # Erstelle die Block-Bibliothek logger.info(f"Erstelle Block-Bibliothek aus {INPUT_DIR}...") create_block_library(INPUT_DIR, OUTPUT_FILE, config, logger) logger.info("=== DXF2LIB Verarbeitung abgeschlossen ===")