Files
plant2dxf/lib/dxf2lib.py
T

450 lines
18 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
import logging
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)
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, INSERT
- 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_x = 0
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", "INSERT"}
filtered_entities = []
for e in entities:
if e.dxftype() in allowed_types:
filtered_entities.append(e)
else:
logger.info(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
# Sicherstellen, dass die im Quell-DXF verwendeten Layer im Zieldokument existieren
try:
used_layer_names = {e.dxf.layer for e in entities if hasattr(e.dxf, "layer")}
for layer_name in used_layer_names:
if layer_name and layer_name not in doc.layers:
try:
src_layer = src_doc.layers.get(layer_name)
doc.layers.add(
name=layer_name,
color=getattr(src_layer.dxf, "color", None),
linetype=getattr(src_layer.dxf, "linetype", None),
lineweight=getattr(src_layer.dxf, "lineweight", None),
)
except Exception:
# Fallback: Layer mit Standardwerten anlegen
doc.layers.add(name=layer_name)
except Exception:
pass
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:
# Sicherstellen, dass referenzierte Blöcke für INSERT verfügbar sind
if e.dxftype() == "INSERT":
handle_insert_entities(doc, src_doc, e)
cp = copy_entity(logger, error_files, filename, e, center)
if cp:
blk.add_entity(cp)
# Platzierung in Reihen und Spalten
# Attribut-Definition (ATTDEF) hinzufügen
blk.add_attdef(
tag="NAME",
insert=(0.2, 0.2), # Position relativ zum Blockursprung
)
# Blockreferenz-Layer bestimmen
blockref_layer = None
try:
#Konfiguration erlaubt expliziten Layernamen
cfg_layer = None
try:
cfg_layer = config.get("dxf2lib", "blockref_layer")
except Exception:
cfg_layer = None
if cfg_layer and cfg_layer.strip():
blockref_layer = cfg_layer.strip()
else:
# Dominanten Ursprungs-Layer verwenden (Mehrheit der Entities)
layer_counts = {}
for e in entities:
ln = getattr(e.dxf, "layer", None)
if not ln:
continue
layer_counts[ln] = layer_counts.get(ln, 0) + 1
if layer_counts:
blockref_layer = max(layer_counts.items(), key=lambda kv: kv[1])[0]
if blockref_layer and blockref_layer not in doc.layers:
# Sicherstellen, dass der Layer existiert
try:
src_layer = src_doc.layers.get(blockref_layer)
doc.layers.add(
name=blockref_layer,
color=getattr(src_layer.dxf, "color", None),
linetype=getattr(src_layer.dxf, "linetype", None),
lineweight=getattr(src_layer.dxf, "lineweight", None),
)
except Exception:
doc.layers.add(name=blockref_layer)
except Exception:
blockref_layer = None
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 + 400
block_spacing_x = ausdehnung_x + extra_block_space_x
max_blockspacing_x = max(max_blockspacing_x, block_spacing_x)
max_blockspacing_y = max(max_blockspacing_y, block_spacing_y)
x_offset += max_blockspacing_x
# Blockreferenz mit optionalem Layer einfügen (Entity-Layer bleiben erhalten)
if blockref_layer:
msp.add_blockref(name, insert=(x_offset, y_offset), dxfattribs={"layer": blockref_layer})
else:
msp.add_blockref(name, insert=(x_offset, y_offset))
# Text mit Blocknamen über dem Block
# Werte aus Config holen (Block: [dxf2lib])
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
# 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
max_blockspacing_x = 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)
def copy_entity(logger, error_files, filename, e, center):
try:
cp = e.copy()
# Geometrie verschieben!
cp.translate(-center.x, -center.y, 0)
# Ursprungs-Layer übernehmen
if hasattr(e.dxf, "layer"):
cp.dxf.layer = e.dxf.layer
if (hasattr(e.dxf, "color")):
cp.dxf.color = e.dxf.color
return 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))
return None
def handle_insert_entities(doc, src_doc, e):
try:
ref_name = e.dxf.name
if ref_name not in doc.blocks and ref_name in src_doc.blocks:
src_blk = src_doc.blocks[ref_name]
dst_blk = doc.blocks.new(name=ref_name)
# Basis­punkt übernehmen, falls vorhanden
try:
dst_blk.block.dxf.base_point = src_blk.block.dxf.base_point
except Exception:
pass
for ent in src_blk:
dst_blk.add_entity(ent.copy())
except Exception:
pass
# 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
log_file = Path(os.environ['PROJECT_LOG']) / 'dxf2lib.log'
file_handler = logging.FileHandler(str(log_file), 'a', 'utf-8')
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}")
logger.addHandler(file_handler)
# 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 ===")