import os import ezdxf from ezdxf.math import Vec2 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) 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 return Vec2((min_x + max_x) / 2, (min_y + max_y) / 2) 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 = [] 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 = [e for e in entities if e.dxftype() in allowed_types] 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 = 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 msp.add_blockref(name, insert=(x_offset, y_offset)) # Text mit Blocknamen über dem Block # Werte aus Config holen (Block: [dxf2lib]) section = "dxf2lib" text_y = y_offset + get_cfg_value(section, "text_offset_y", DEFAULTS["text_offset_y"]) text_offset_x = get_cfg_value(section, "text_offset_x", DEFAULTS["text_offset_x"]) max_len = get_cfg_value(section, "max_len", DEFAULTS["max_len"]) text_height = get_cfg_value(section, "text_height", DEFAULTS["text_height"]) line_spacing = get_cfg_value(section, "line_spacing", DEFAULTS["line_spacing"]) block_spacing_x = get_cfg_value(section, "block_spacing_x", DEFAULTS["block_spacing_x"]) blocks_per_row = get_cfg_value(section, "blocks_per_row", DEFAULTS["blocks_per_row"]) block_spacing_y = get_cfg_value(section, "block_spacing_y", DEFAULTS["block_spacing_y"]) if len(name) > max_len: first_line = name[:max_len] second_line = name[max_len:] # obere Zeile msp.add_text(first_line, dxfattribs={'height': text_height, 'insert': (x_offset + text_offset_x, text_y)}) # untere Zeile, etwas niedriger (y kleiner) msp.add_text(second_line, dxfattribs={'height': text_height, 'insert': (x_offset + text_offset_x, text_y - line_spacing)}) else: msp.add_text(name, dxfattribs={'height': text_height, 'insert': (x_offset + text_offset_x, text_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 -= block_spacing_y # Neue Zeile, nach unten versetzt 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_offset_y": 175, # Vertikaler Abstand des Texts über dem Block (in DXF-Einheiten) "text_offset_x": -150, # Horizontaler Abstand des Texts links vom Block (in DXF-Einheiten) "max_len": 9, # Maximale Zeichenlänge pro Textzeile (bei Überschreitung wird umgebrochen) "text_height": 20, # Schriftgröße des Texts (in DXF-Einheiten) "line_spacing": 22, # Abstand zwischen zwei Textzeilen bei Umbrüchen (in DXF-Einheiten) "block_spacing_x": 350, # Horizontaler Abstand zwischen Blöcken in einer Reihe (in DXF-Einheiten) "blocks_per_row": 20, # Anzahl Blöcke pro Zeile im Raster "block_spacing_y": 500, # Vertikaler Abstand zwischen Zeilen im Raster (in DXF-Einheiten) } 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)') parser.add_argument('-k', '--keep', action="store_true", help='Behalte converted_dxf Ordner. Sonst Löschen nach erfolgreicher Bibliothekserstellung') 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_WORK") / "converted_dxfs" print(f"Kein Input-Verzeichnis angegeben, verwende Standard: {INPUT_DIR} \n") OUTPUT_FILE = check_environment_var("PROJECT_DATA") / "block_libaries" / 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) if not args.keep: logger.info(f"Keep-Argument nicht gesetzt, lösche Input-Verzeichnis: {INPUT_DIR}") try: if INPUT_DIR.exists(): shutil.rmtree(INPUT_DIR) logger.info(f"Verzeichnis '{INPUT_DIR}' wurde gelöscht.") except Exception as err: logger.warning(f"Konnte '{INPUT_DIR}' nicht löschen: {err}") logger.info("=== DXF2LIB Verarbeitung abgeschlossen ===")