407 lines
15 KiB
Python
407 lines
15 KiB
Python
"""
|
|
DXF Schnittpunkt-Analysator
|
|
Analysiert Linienschnittpunkte in DXF-Dateien und klassifiziert diese nach Typ
|
|
"""
|
|
|
|
import ezdxf
|
|
from ezdxf.math import Vec3
|
|
from collections import defaultdict
|
|
import sys
|
|
from pathlib import Path
|
|
from math import sin, cos, pi
|
|
from datetime import datetime
|
|
|
|
class IntersectionAnalyzer:
|
|
def __init__(self, dxf_path, layer_names, tolerance=0.1, mark_radius=50):
|
|
"""
|
|
Initialisiert den Analysator
|
|
|
|
Args:
|
|
dxf_path: Pfad zur DXF-Datei
|
|
layer_names: Liste der zu analysierenden Layer
|
|
tolerance: Toleranz für Punktvergleiche in mm (Standard: 0.01)
|
|
"""
|
|
self.dxf_path = dxf_path
|
|
self.layer_names = layer_names
|
|
self.tolerance = tolerance
|
|
self.mark_radius = mark_radius
|
|
self.doc = None
|
|
self.lines = []
|
|
self.intersections = {
|
|
'Kreuzung': [],
|
|
'T-Verbindung': [],
|
|
'Eckverbindung': []
|
|
}
|
|
self.debug_info = []
|
|
|
|
def load_dxf(self):
|
|
"""Lädt die DXF-Datei"""
|
|
try:
|
|
self.doc = ezdxf.readfile(self.dxf_path)
|
|
print(f"DXF-Datei erfolgreich geladen: {self.dxf_path}")
|
|
return True
|
|
except Exception as e:
|
|
print(f"Fehler beim Laden der DXF-Datei: {e}")
|
|
return False
|
|
|
|
def extract_lines(self):
|
|
"""Extrahiert alle Linien aus den angegebenen Layern"""
|
|
msp = self.doc.modelspace()
|
|
|
|
for entity in msp:
|
|
if entity.dxftype() == 'LINE' and entity.dxf.layer in self.layer_names:
|
|
start = Vec3(entity.dxf.start.x, entity.dxf.start.y, 0) # Z ignorieren
|
|
end = Vec3(entity.dxf.end.x, entity.dxf.end.y, 0)
|
|
|
|
# Debug: Überprüfe auf extrem kurze Linien (Punkte)
|
|
length = start.distance(end)
|
|
if length < 1e-10:
|
|
print(f"WARNUNG: Punkt statt Linie gefunden bei ({start.x}, {start.y})")
|
|
continue
|
|
|
|
self.lines.append((start, end, entity))
|
|
|
|
print(f"Anzahl gefundener Linien: {len(self.lines)}")
|
|
print(f"Verwendete Toleranz: {self.tolerance} mm")
|
|
|
|
# Debug: Zeige alle Linien mit ihren Koordinaten
|
|
#print("\n=== LINIEN-DETAILS ===")
|
|
#for i, (start, end, entity) in enumerate(self.lines):
|
|
#print(f"Linie {i}: Start=({start.x:.3f}, {start.y:.3f}) End=({end.x:.3f}, {end.y:.3f}) Länge={start.distance(end):.3f}")
|
|
|
|
def calculate_intersection(self, line1, line2):
|
|
p1, p2 = line1[0], line1[1]
|
|
p3, p4 = line2[0], line2[1]
|
|
|
|
# Richtungsvektoren
|
|
d1 = p2 - p1
|
|
d2 = p4 - p3
|
|
|
|
# Kreuzprodukt zur Prüfung auf Parallelität/Kollinearität
|
|
cross = d1.x * d2.y - d1.y * d2.x
|
|
|
|
# Kollinear oder parallel
|
|
if abs(cross) < 1e-12:
|
|
for pt in (p1, p2):
|
|
if self.point_on_segment(pt, (p3, p4)):
|
|
return pt
|
|
for pt in (p3, p4):
|
|
if self.point_on_segment(pt, (p1, p2)):
|
|
return pt
|
|
return None
|
|
|
|
# Parametrisierte Schnittpunktberechnung
|
|
denom = (p1.x - p2.x)*(p3.y - p4.y) - (p1.y - p2.y)*(p3.x - p4.x)
|
|
t = ((p1.x - p3.x)*(p3.y - p4.y) - (p1.y - p3.y)*(p3.x - p4.x)) / denom
|
|
u = -((p1.x - p2.x)*(p1.y - p3.y) - (p1.y - p2.y)*(p1.x - p3.x)) / denom
|
|
|
|
# Schnittpunkt auf den Segmenten ± Toleranz
|
|
if -self.tolerance <= t <= 1 + self.tolerance and -self.tolerance <= u <= 1 + self.tolerance:
|
|
x = p1.x + t * d1.x
|
|
y = p1.y + t * d1.y
|
|
intersection = Vec3(x, y, 0)
|
|
|
|
dist1 = self.distance_point_to_segment(intersection, (p1, p2))
|
|
dist2 = self.distance_point_to_segment(intersection, (p3, p4))
|
|
if dist1 <= self.tolerance and dist2 <= self.tolerance:
|
|
return intersection
|
|
|
|
# Prüfen auf T-Verbindung: Endpunkt von line1 nahe line2
|
|
for pt in (p1, p2):
|
|
dist = self.distance_point_to_segment(pt, (p3, p4))
|
|
if 0 < dist <= self.tolerance:
|
|
return pt
|
|
|
|
# Prüfen auf T-Verbindung: Endpunkt von line2 nahe line1
|
|
for pt in (p3, p4):
|
|
dist = self.distance_point_to_segment(pt, (p1, p2))
|
|
if 0 < dist <= self.tolerance:
|
|
return pt
|
|
|
|
# Keine Kreuzung oder T-Verbindung
|
|
return None
|
|
|
|
def distance_point_to_segment(self, pt, segment):
|
|
"""Abstand eines Punktes zu einem Liniensegment"""
|
|
start, end = segment
|
|
seg_vec = end - start
|
|
seg_len_sq = seg_vec.x**2 + seg_vec.y**2
|
|
if seg_len_sq < 1e-12:
|
|
return pt.distance(start)
|
|
t = max(0, min(1, ((pt - start).dot(seg_vec)) / seg_len_sq))
|
|
proj = start + seg_vec * t
|
|
return pt.distance(proj)
|
|
|
|
|
|
def point_on_segment(self, pt, segment):
|
|
ax, ay = segment[0].x, segment[0].y
|
|
bx, by = segment[1].x, segment[1].y
|
|
px, py = pt.x, pt.y
|
|
|
|
# Richtungsvektoren
|
|
vx, vy = bx - ax, by - ay
|
|
wx, wy = px - ax, py - ay
|
|
|
|
# Kreuzprodukt zur Kollinearitätsprüfung
|
|
cross = vx * wy - vy * wx
|
|
if abs(cross) > self.tolerance:
|
|
return False
|
|
|
|
# Skalarprodukt für Projektion
|
|
dot = wx * vx + wy * vy
|
|
if dot < -self.tolerance:
|
|
return False
|
|
|
|
# Länge des Segments im Quadrat
|
|
seg_len_sq = vx * vx + vy * vy
|
|
|
|
if dot > seg_len_sq + self.tolerance:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def is_endpoint(self, point, line):
|
|
start, end = line[0], line[1]
|
|
return (start.distance(point) <= self.tolerance or end.distance(point) <= self.tolerance)
|
|
|
|
|
|
def classify_intersection(self, point, line1, line2):
|
|
"""
|
|
Klassifiziert den Schnittpunkt
|
|
|
|
Returns:
|
|
'Kreuzung', 'T-Verbindung' oder 'Eckverbindung'
|
|
"""
|
|
line1_ends = self.is_endpoint(point, line1)
|
|
line2_ends = self.is_endpoint(point, line2)
|
|
|
|
if line1_ends and line2_ends:
|
|
return 'Eckverbindung'
|
|
elif line1_ends or line2_ends:
|
|
return 'T-Verbindung'
|
|
else:
|
|
return 'Kreuzung'
|
|
|
|
def find_intersections(self):
|
|
"""Findet und klassifiziert alle Schnittpunkte"""
|
|
print("\n=== ANALYSIERE SCHNITTPUNKTE ===\n")
|
|
|
|
processed_pairs = set()
|
|
|
|
for i, line1 in enumerate(self.lines):
|
|
for j, line2 in enumerate(self.lines):
|
|
if i >= j:
|
|
continue
|
|
|
|
pair = (i, j)
|
|
if pair in processed_pairs:
|
|
continue
|
|
|
|
# Prüfe ob Endpunkte übereinstimmen (mit Toleranz)
|
|
l1_start, l1_end = line1[0], line1[1]
|
|
l2_start, l2_end = line2[0], line2[1]
|
|
|
|
# Finde übereinstimmende Endpunkte
|
|
matches = []
|
|
dist_ss = l1_start.distance(l2_start)
|
|
dist_se = l1_start.distance(l2_end)
|
|
dist_es = l1_end.distance(l2_start)
|
|
dist_ee = l1_end.distance(l2_end)
|
|
|
|
if dist_ss < self.tolerance:
|
|
matches.append(('start', 'start', l1_start))
|
|
if dist_se < self.tolerance:
|
|
matches.append(('start', 'end', l1_start))
|
|
if dist_es < self.tolerance:
|
|
matches.append(('end', 'start', l1_end))
|
|
if dist_ee < self.tolerance:
|
|
matches.append(('end', 'end', l1_end))
|
|
|
|
# Debug: Zeige nahe Punkte, die knapp außerhalb der Toleranz liegen
|
|
min_dist = min(dist_ss, dist_se, dist_es, dist_ee)
|
|
#if self.tolerance <= min_dist < self.tolerance * 10:
|
|
#print(f"FAST-MATCH Linie {i}-{j}: min_dist={min_dist:.6f} (Toleranz={self.tolerance})")
|
|
|
|
# Wenn Endpunkte übereinstimmen, ist es eine Eckverbindung
|
|
if matches:
|
|
for match_type, _, point in matches:
|
|
self.intersections['Eckverbindung'].append(point)
|
|
processed_pairs.add(pair)
|
|
#print(f"DEBUG: Eckverbindung Linie {i}-{j} bei ({point.x:.3f}, {point.y:.3f})")
|
|
continue
|
|
|
|
# Berechne geometrischen Schnittpunkt
|
|
point = self.calculate_intersection(line1, line2)
|
|
|
|
if point is not None:
|
|
intersection_type = self.classify_intersection(point, line1, line2)
|
|
self.intersections[intersection_type].append(point)
|
|
processed_pairs.add(pair)
|
|
|
|
# Erweiterte Debug-Info
|
|
l1_ends = self.is_endpoint(point, line1)
|
|
l2_ends = self.is_endpoint(point, line2)
|
|
#print(f"DEBUG: {intersection_type} Linie {i}-{j} bei ({point.x:.3f}, {point.y:.3f}) [L1_end={l1_ends}, L2_end={l2_ends}]")
|
|
|
|
total = sum(len(v) for v in self.intersections.values())
|
|
print(f"\n=== ZUSAMMENFASSUNG ===")
|
|
print(f"Gefundene Schnittpunkte: {total}")
|
|
for itype, points in self.intersections.items():
|
|
print(f" {itype}: {len(points)}")
|
|
|
|
def create_marking_layers(self):
|
|
"""Erstellt Layer für die Markierungen"""
|
|
layer_names = {
|
|
'Kreuzung': 'D-KREUZUNG',
|
|
'T-Verbindung': 'D-T_VERBINDUNG',
|
|
'Eckverbindung': 'D-ECKVERBINDUNG'
|
|
}
|
|
|
|
colors = {
|
|
'Kreuzung': 1, # Rot
|
|
'T-Verbindung': 3, # Grün
|
|
'Eckverbindung': 5 # Blau
|
|
}
|
|
|
|
for itype, layer_name in layer_names.items():
|
|
if layer_name not in self.doc.layers:
|
|
self.doc.layers.add(
|
|
layer_name,
|
|
color=colors[itype]
|
|
)
|
|
|
|
return layer_names
|
|
|
|
|
|
|
|
def add_marking_circles(self, layer_names):
|
|
"""Fügt Markierungskreise an allen Schnittpunkten hinzu"""
|
|
msp = self.doc.modelspace()
|
|
radius = self.mark_radius
|
|
|
|
for itype, points in self.intersections.items():
|
|
layer_name = layer_names[itype]
|
|
|
|
for point in points:
|
|
msp.add_circle(
|
|
center=(point.x, point.y),
|
|
radius=radius,
|
|
dxfattribs={'layer': layer_name}
|
|
)
|
|
|
|
print("\nMarkierungskreise hinzugefügt")
|
|
|
|
def save_results(self, output_dxf, output_txt):
|
|
"""Speichert die Ergebnisse mit Fehlerhandling, falls die Dateien blockiert sind."""
|
|
from pathlib import Path
|
|
print(f"\nSpeichern in {output_dxf}...")
|
|
# DXF speichern
|
|
try:
|
|
self.doc.saveas(output_dxf)
|
|
print(f"\nMarkierte DXF-Datei gespeichert: {output_dxf}")
|
|
except PermissionError:
|
|
print("\nFEHLER: Die DXF-Datei konnte nicht gespeichert werden.")
|
|
print(f"Grund: '{Path(output_dxf).name}' ist vermutlich noch geöffnet.")
|
|
print("Bitte schließen und speichern erneut ausführen.\n")
|
|
return False
|
|
except Exception as e:
|
|
print("\nUnerwarteter Fehler beim Speichern der DXF:")
|
|
print(e)
|
|
return False
|
|
|
|
# TXT speichern
|
|
try:
|
|
dt = datetime.now().strftime("%d.%m.%Y %H:%M:%S")
|
|
with open(output_txt, 'w', encoding='utf-8') as f:
|
|
f.write("=" * 60 + "\n")
|
|
f.write(f"DXF SCHNITTPUNKT-ANALYSE ERGEBNIS - {dt} \n")
|
|
f.write("=" * 60 + "\n\n")
|
|
|
|
f.write(f"Eingabedatei: {self.dxf_path}\n")
|
|
f.write(f"Analysierte Layer: {', '.join(self.layer_names)}\n")
|
|
f.write(f"Anzahl analysierter Linien: {len(self.lines)}\n\n")
|
|
|
|
f.write("-" * 60 + "\n")
|
|
f.write("SCHNITTPUNKT-STATISTIK\n")
|
|
f.write("-" * 60 + "\n\n")
|
|
|
|
total = sum(len(v) for v in self.intersections.values())
|
|
|
|
for itype in ['Kreuzung', 'T-Verbindung', 'Eckverbindung']:
|
|
count = len(self.intersections[itype])
|
|
percentage = (count / total * 100) if total > 0 else 0
|
|
f.write(f"{itype:20s}: {count:5d} ({percentage:5.1f}%)\n")
|
|
|
|
f.write(f"\n{'GESAMT':20s}: {total:5d}\n\n")
|
|
|
|
f.write("-" * 60 + "\n")
|
|
f.write("DETAILS DER SCHNITTPUNKTE\n")
|
|
f.write("-" * 60 + "\n\n")
|
|
|
|
for itype in ['Kreuzung', 'T-Verbindung', 'Eckverbindung']:
|
|
points = self.intersections[itype]
|
|
f.write(f"\n{itype} ({len(points)} Stück):\n")
|
|
f.write("-" * 40 + "\n")
|
|
|
|
for idx, point in enumerate(points, 1):
|
|
f.write(f" {idx:3d}. X={point.x:10.3f}, Y={point.y:10.3f}\n")
|
|
|
|
print(f"Ergebnisdatei gespeichert: {output_txt}")
|
|
|
|
except PermissionError:
|
|
print("\nFEHLER: Die Analyse-Textdatei konnte nicht gespeichert werden.")
|
|
print(f"Grund: '{Path(output_txt).name}' ist vermutlich noch geöffnet.")
|
|
print("Bitte schließen und speichern erneut ausführen.\n")
|
|
return False
|
|
except Exception as e:
|
|
print("\nUnerwarteter Fehler beim Speichern der Textdatei:")
|
|
print(e)
|
|
return False
|
|
|
|
return True
|
|
|
|
def analyze(self, output_dxf=None, output_txt=None):
|
|
"""Führt die komplette Analyse durch"""
|
|
print("\n" + "=" * 60)
|
|
print("DXF SCHNITTPUNKT-ANALYSE")
|
|
print("=" * 60 + "\n")
|
|
|
|
if not self.load_dxf():
|
|
return False
|
|
|
|
self.extract_lines()
|
|
|
|
if len(self.lines) == 0:
|
|
print("Keine Linien auf den angegebenen Layern gefunden!")
|
|
return False
|
|
|
|
self.find_intersections()
|
|
|
|
layer_names = self.create_marking_layers()
|
|
self.add_marking_circles(layer_names)
|
|
|
|
# Standardausgabedateinamen generieren
|
|
if output_dxf is None:
|
|
base = Path(self.dxf_path).stem
|
|
output_dxf = f"{base}_markiert.dxf"
|
|
|
|
if output_txt is None:
|
|
base = Path(self.dxf_path).stem
|
|
output_txt = f"{base}_analyse.txt"
|
|
|
|
# Speichern und Fehler überprüfen
|
|
ok = self.save_results(output_dxf, output_txt)
|
|
if not ok:
|
|
print("\nAnalyse abgebrochen wegen Speicherfehler.\n")
|
|
return False
|
|
|
|
print("\n" + "=" * 60)
|
|
print("ANALYSE ERFOLGREICH ABGESCHLOSSEN")
|
|
print("=" * 60 + "\n")
|
|
|
|
return True
|
|
|
|
|
|
|