""" Setzt Koordinatensystem-Bloecke (K1, K2, ...) in Omniflo DXF-Dateien. Jeder Block enthaelt drei Linien vom Ursprung: - rot(1) X-Achse, Laenge 1 - gruen(3) Y-Achse, Laenge 2 - blau(5) Z-Achse, Laenge 3 Schalter: --k1set K1 an alle Boegen und Weichen setzen. Boegen: Beginn der obersten horizontalen Linie, x rechts, y oben, z aus Zeichenebene. Weichen: Beginn der untersten vertikalen Linie, x oben, y links, z aus Zeichenebene. --k2set K2 an alle Boegen und Weichen setzen. Boegen: anderes Kettenende, x tangential, y senkrecht, z wie K1. Weichen: noch nicht implementiert (Dummy). --show-omniflo Uebersichts-DXF mit K-Positionen als farbige Kreuze. --test Erzeugt Testdatei mit 4 KOS und verifiziert diese. --number SIVA Nur diese eine 9-stellige Sivasnr verarbeiten. """ import argparse import json import math import os import sys import ezdxf from ezdxf.math import Matrix44 from utils import ( ROW_GROUPS, TEXT_HEIGHT, TEXT_MARGIN, CROSS_SIZE, ROW_LABEL_WIDTH, SWITCH_FILTERS, load_omniflo_data, build_row_layout, import_element_as_block, draw_cross, is_bogen, ) # --------------------------------------------------------------------------- # Block-Definition # --------------------------------------------------------------------------- KS_LINES = [ (1, 1.0, 0.0, 0.0), # rot, X-Achse, Laenge 1 (3, 0.0, 2.0, 0.0), # gruen, Y-Achse, Laenge 2 (5, 0.0, 0.0, 3.0), # blau, Z-Achse, Laenge 3 ] def _ensure_block(doc, block_name): """Erzeugt den Block falls er noch nicht existiert.""" if block_name not in doc.blocks: blk = doc.blocks.new(name=block_name) for color, dx, dy, dz in KS_LINES: blk.add_line( start=(0, 0, 0), end=(dx, dy, dz), dxfattribs={"color": color}, ) return block_name def _ensure_layer(doc, layer_name="_KOORDINATENSYSTEME"): """Stellt sicher, dass der Layer existiert.""" if layer_name not in doc.layers: doc.layers.add(layer_name) # --------------------------------------------------------------------------- # insert / read / verify # --------------------------------------------------------------------------- def insert_ks(msp, name, point, rx=0, ry=0, rz=0): """ Erzeugt ein benanntes Koordinatensystem als Block-Referenz. Args: msp: Modelspace oder Block des Ziel-Dokuments. name: Block-Name (z.B. "K1", "K2", "K3", "K4"). point: Einfuegepunkt als (x, y, z) Tupel. rx: Rotation um X-Achse in Grad. ry: Rotation um Y-Achse in Grad. rz: Rotation um Z-Achse in Grad. Returns: Das erzeugte INSERT-Entity. """ doc = msp.doc _ensure_layer(doc) _ensure_block(doc, name) insert = msp.add_blockref( name, insert=point, dxfattribs={"layer": "_KOORDINATENSYSTEME"}, ) m = Matrix44.chain( Matrix44.translate(-point[0], -point[1], -point[2]), Matrix44.x_rotate(math.radians(rx)), Matrix44.y_rotate(math.radians(ry)), Matrix44.z_rotate(math.radians(rz)), Matrix44.translate(point[0], point[1], point[2]), ) insert.transform(m) return insert def read_ks(doc, name): """ Liest ein Koordinatensystem aus einer DXF-Datei zurueck. Returns: Dict mit 'name', 'point', 'rx', 'ry', 'rz' oder None. """ msp = doc.modelspace() for entity in msp: if entity.dxftype() == "INSERT" and entity.dxf.name == name: lines = [e for e in entity.virtual_entities() if e.dxftype() == "LINE"] if len(lines) != 3: return None point = (lines[0].dxf.start[0], lines[0].dxf.start[1], lines[0].dxf.start[2]) axes = {} for line in lines: end = line.dxf.end dx = end[0] - point[0] dy = end[1] - point[1] dz = end[2] - point[2] length = (dx**2 + dy**2 + dz**2) ** 0.5 axes[line.dxf.color] = (dx / length, dy / length, dz / length) x_axis = axes.get(1, (1, 0, 0)) y_axis = axes.get(3, (0, 1, 0)) z_axis = axes.get(5, (0, 0, 1)) ry = math.asin(max(-1, min(1, -x_axis[2]))) cos_ry = math.cos(ry) if abs(cos_ry) > 1e-6: rx = math.atan2(y_axis[2] / cos_ry, z_axis[2] / cos_ry) rz = math.atan2(x_axis[1] / cos_ry, x_axis[0] / cos_ry) else: rz = 0 rx = math.atan2(-z_axis[0], y_axis[0]) return { "name": name, "point": point, "rx": math.degrees(rx), "ry": math.degrees(ry), "rz": math.degrees(rz), } return None def verify_ks(dxf_path, expected): """Liest KS-Bloecke und vergleicht mit Erwartungswerten (Toleranz 0.01).""" doc = ezdxf.readfile(dxf_path) all_ok = True tol = 0.01 for exp in expected: result = read_ks(doc, exp["name"]) if result is None: print(f"FEHLER: Block '{exp['name']}' nicht gefunden") all_ok = False continue errors = [] for i, axis in enumerate(("x", "y", "z")): if abs(result["point"][i] - exp["point"][i]) > tol: errors.append(f"{axis}={result['point'][i]:.3f} (erwartet {exp['point'][i]:.3f})") for angle in ("rx", "ry", "rz"): diff = abs(result[angle] - exp[angle]) if diff > 360 - tol: diff = abs(diff - 360) if diff > tol: errors.append(f"{angle}={result[angle]:.2f} (erwartet {exp[angle]:.2f})") if errors: print(f"FEHLER {exp['name']}: {', '.join(errors)}") all_ok = False else: print(f"OK {exp['name']}: point=({result['point'][0]:.2f},{result['point'][1]:.2f},{result['point'][2]:.2f}) " f"rx={result['rx']:.2f} ry={result['ry']:.2f} rz={result['rz']:.2f}") return all_ok # --------------------------------------------------------------------------- # K1-Positionsbestimmung # --------------------------------------------------------------------------- def find_topmost_horizontal_line(doc): """Findet die am weitesten oben liegende horizontale Linie.""" best = None best_y = -float('inf') for entity in doc.modelspace(): if entity.dxftype() != "LINE": continue s, e = entity.dxf.start, entity.dxf.end if abs(e[1] - s[1]) > 0.01: continue if s[1] > best_y: best_y = s[1] best = entity return best def find_bottommost_vertical_line(doc): """Findet die am weitesten unten liegende rein vertikale Linie.""" best = None best_min_y = float('inf') for entity in doc.modelspace(): if entity.dxftype() != "LINE": continue s, e = entity.dxf.start, entity.dxf.end if abs(e[0] - s[0]) > 0.01: continue min_y = min(s[1], e[1]) if min_y < best_min_y: best_min_y = min_y best = entity return best def k1_point_bogen(doc): """K1-Position fuer Boegen: Beginn der obersten horizontalen Linie. Rotation: x rechts, y oben, z aus Zeichenebene (Standard, rz=0). """ hline = find_topmost_horizontal_line(doc) if hline is None: return None s, e = hline.dxf.start, hline.dxf.end left_x = min(s[0], e[0]) y = s[1] return (left_x, y, 0), 0, 0, 0 def k1_point_weiche(doc): """K1-Position fuer Weichen: Beginn der untersten vertikalen Linie. Rotation: x oben, y links, z aus Zeichenebene (rz=90). """ vline = find_bottommost_vertical_line(doc) if vline is None: return None s, e = vline.dxf.start, vline.dxf.end x = s[0] bottom_y = min(s[1], e[1]) return (x, bottom_y, 0), 0, 0, 90 # --------------------------------------------------------------------------- # K2-Positionsbestimmung (nur Boegen) # --------------------------------------------------------------------------- def k2_point_bogen(doc): """K2-Position fuer Boegen: anderes Ende der Kette (Gerade-Bogen-Gerade). Sucht den Bogen, bestimmt welches Ende zur K1-Seite (oberste H-Linie) gehoert, findet die laengste Linie am anderen Bogenende und setzt K2 an deren aeusseres Ende. x-Achse tangential zur Linie (Richtung weg vom Bogen), y senkrecht dazu, z aus Zeichenebene (wie K1). Sonderfall 180-Grad-Bogen: x nach rechts (rz=0). """ msp = doc.modelspace() # Bogen finden arc = None for e in msp: if e.dxftype() == "ARC": arc = e break if arc is None: return None cx, cy = arc.dxf.center[0], arc.dxf.center[1] r = arc.dxf.radius sa, ea = arc.dxf.start_angle, arc.dxf.end_angle arc_start = (cx + r * math.cos(math.radians(sa)), cy + r * math.sin(math.radians(sa))) arc_end = (cx + r * math.cos(math.radians(ea)), cy + r * math.sin(math.radians(ea))) # K1-Seite = oberste horizontale Linie hline = find_topmost_horizontal_line(doc) if hline is None: return None h_y = hline.dxf.start[1] # Welches Bogenende liegt an der K1-H-Linie? tol = 1.0 if abs(arc_end[1] - h_y) < tol: k2_junction = arc_start else: k2_junction = arc_end # Laengste Linie am K2-Bogenende suchen best_line_len = 0 k2_pos = None junc_pt = None for e in msp: if e.dxftype() != "LINE": continue s = (e.dxf.start[0], e.dxf.start[1]) end = (e.dxf.end[0], e.dxf.end[1]) ds = ((s[0] - k2_junction[0])**2 + (s[1] - k2_junction[1])**2) ** 0.5 de = ((end[0] - k2_junction[0])**2 + (end[1] - k2_junction[1])**2) ** 0.5 if ds > tol and de > tol: continue ln = ((end[0] - s[0])**2 + (end[1] - s[1])**2) ** 0.5 if ln > best_line_len: best_line_len = ln if ds < tol: k2_pos = end junc_pt = s else: k2_pos = s junc_pt = end if k2_pos is None: return None # Bogenspanne berechnen span = (ea - sa) % 360 # Tangenten-Richtung: von K2 zur Junction (zur Bogenmitte) dx = junc_pt[0] - k2_pos[0] dy = junc_pt[1] - k2_pos[1] rz = math.degrees(math.atan2(dy, dx)) # 180-Grad-Sonderfall: x nach rechts, wie K1 if abs(span - 180) < 1: rz = 0 return (k2_pos[0], k2_pos[1], 0), 0, 0, rz # --------------------------------------------------------------------------- # K2-Positionsbestimmung Weichen (Dummy-Funktionen) # --------------------------------------------------------------------------- def k2_point_weichen45(doc): """K2 fuer Weichen 45 Grad – noch nicht implementiert.""" return None def k2_point_weichen90(doc): """K2 fuer Weichen 90 Grad – noch nicht implementiert.""" return None def k2_point_weichenkoerper(doc): """K2 fuer Weichenkoerper – noch nicht implementiert.""" return None def k2_point_weichen_parallel(doc): """K2 fuer Weichen Parallel – noch nicht implementiert.""" return None def k2_point_sternweiche(doc): """K2 fuer Sternweiche – noch nicht implementiert.""" return None def k2_point_delta(doc): """K2 fuer Deltaweichen – noch nicht implementiert.""" return None def k2_point_dreifachweiche(doc): """K2 fuer Dreifachweichen – noch nicht implementiert.""" return None # --------------------------------------------------------------------------- # K2 Schalter-Zuordnung: SWITCH_FILTERS-Key -> K2-Funktion # --------------------------------------------------------------------------- K2_FUNCS = { "boegen": k2_point_bogen, "weichen45": k2_point_weichen45, "weichen90": k2_point_weichen90, "weichenkoerper": k2_point_weichenkoerper, "weichen_parallel": k2_point_weichen_parallel, "sternweiche": k2_point_sternweiche, "delta": k2_point_delta, "dreifachweiche": k2_point_dreifachweiche, } # --------------------------------------------------------------------------- # --k1set # --------------------------------------------------------------------------- def process_k1set(data_dir, results_dir, number=None): """Setzt K1-Block an alle Boegen und Weichen.""" sources = load_omniflo_data(data_dir) omniflo_dir = os.path.join(data_dir, "omniflo") all_items = [] for b in sources["boegen"]: all_items.append((str(b["Sivasnr"]), b["ProfilTyp"], True)) for w in sources["weichen"]: all_items.append((str(w["Sivasnr"]), w["ProfilTyp"], False)) if number: all_items = [(s, p, ib) for s, p, ib in all_items if s == str(number)] if not all_items: print("Keine Elemente gefunden.") return print("=== K1 setzen ===") for sivasnr, profil, is_bog in all_items: dxf_path = os.path.join(results_dir, f"{sivasnr}.dxf") if not os.path.exists(dxf_path): dxf_path = os.path.join(omniflo_dir, f"{sivasnr}.dxf") if not os.path.exists(dxf_path): continue doc = ezdxf.readfile(dxf_path) # Bestehende K1-Referenzen entfernen msp = doc.modelspace() for entity in list(msp): if entity.dxftype() == "INSERT" and entity.dxf.name == "K1": msp.delete_entity(entity) if is_bog: result = k1_point_bogen(doc) else: result = k1_point_weiche(doc) if result is None: print(f"WARNUNG: K1 in {sivasnr}.dxf nicht bestimmbar, ueberspringe.") continue point, rx, ry, rz = result msp = doc.modelspace() insert_ks(msp, "K1", point, rx, ry, rz) out_path = os.path.join(results_dir, f"{sivasnr}.dxf") doc.saveas(out_path) print(f"{sivasnr}: K1 at ({point[0]:.2f},{point[1]:.2f},{point[2]:.2f}) " f"rx={rx} ry={ry} rz={rz} [{profil}]") print(f"\nErgebnisse in: {results_dir}") # --------------------------------------------------------------------------- # --k2set # --------------------------------------------------------------------------- def process_k2set(data_dir, results_dir, number=None): """Setzt K2-Block an alle Boegen und Weichen (ueber SWITCH_FILTERS).""" omniflo_dir = os.path.join(data_dir, "omniflo") for key, k2_func in K2_FUNCS.items(): label, json_file, filter_func = SWITCH_FILTERS[key] json_path = os.path.join(data_dir, "json", json_file) if not os.path.exists(json_path): continue with open(json_path, "r", encoding="utf-8") as f: all_items = json.load(f) filtered = [item for item in all_items if filter_func(item)] if number: filtered = [item for item in filtered if str(item["Sivasnr"]) == str(number)] if not filtered: continue print(f"=== K2 setzen: {label} ===") for item in filtered: sivasnr = str(item["Sivasnr"]) dxf_path = os.path.join(results_dir, f"{sivasnr}.dxf") if not os.path.exists(dxf_path): dxf_path = os.path.join(omniflo_dir, f"{sivasnr}.dxf") if not os.path.exists(dxf_path): continue doc = ezdxf.readfile(dxf_path) msp = doc.modelspace() # Bestehende K2-Referenzen entfernen for entity in list(msp): if entity.dxftype() == "INSERT" and entity.dxf.name == "K2": msp.delete_entity(entity) result = k2_func(doc) if result is None: print(f" {sivasnr}: K2 nicht bestimmbar (Dummy), ueberspringe.") continue point, rx, ry, rz = result insert_ks(msp, "K2", point, rx, ry, rz) out_path = os.path.join(results_dir, f"{sivasnr}.dxf") doc.saveas(out_path) print(f" {sivasnr}: K2 at ({point[0]:.2f},{point[1]:.2f},{point[2]:.2f}) " f"rx={rx} ry={ry} rz={rz:.1f} [{item['ProfilTyp']}]") print(f"\nErgebnisse in: {results_dir}") # --------------------------------------------------------------------------- # --show-omniflo # --------------------------------------------------------------------------- # Farben: K1=rot(1), K2=orange(30), K3=gelb(2), K4=gruen(3) K_COLORS = {"K1": 1, "K2": 30, "K3": 2, "K4": 3} def show_omniflo(data_dir, results_dir): """Uebersichts-DXF mit K-Positionen als farbige Kreuze.""" sources, rows = build_row_layout(data_dir, results_dir) target = ezdxf.new(dxfversion='R2010') target_msp = target.modelspace() target.layers.add('ANNOTATION', color=7) target.layers.add('K_POINTS', color=1) target.layers.add('ROW_LABEL', color=3) block_counter = 0 for row in rows: label_y = row['cursor_y'] + row['max_height'] / 2 target_msp.add_mtext( row['label'], dxfattribs={ 'layer': 'ROW_LABEL', 'char_height': TEXT_HEIGHT * 1.2, } ).set_location(insert=(-ROW_LABEL_WIDTH, label_y)) for elem in row['elements']: block_name = f"BLK_{block_counter}" block_counter += 1 import_element_as_block(elem['source'], target, block_name) target_msp.add_blockref(block_name, insert=(elem['offset_x'], elem['offset_y'])) # K-Positionen aus dem Quell-DXF auslesen und anzeigen for k_name, k_color in K_COLORS.items(): k_data = read_ks(elem['source'], k_name) if k_data is None: continue kx = k_data['point'][0] + elem['offset_x'] ky = k_data['point'][1] + elem['offset_y'] draw_cross(target_msp, kx, ky, CROSS_SIZE, k_color, 'K_POINTS') text_x = elem['offset_x'] + elem['extmin'][0] text_y = row['cursor_y'] + elem['height'] + TEXT_MARGIN target_msp.add_mtext( elem['sivasnr'], dxfattribs={ 'layer': 'ANNOTATION', 'char_height': TEXT_HEIGHT, } ).set_location(insert=(text_x, text_y)) out_path = os.path.join(results_dir, "koords_uebersicht.dxf") target.saveas(out_path) print(f"Uebersicht gespeichert: {out_path}") print(f" {block_counter} Elemente in {len(rows)} Reihen") # --------------------------------------------------------------------------- # --test # --------------------------------------------------------------------------- def run_test(results_dir): """Erzeugt Testdatei mit 4 KOS und verifiziert Ruecklesen.""" doc = ezdxf.new(dxfversion="R2010") msp = doc.modelspace() test_cases = [ {"name": "K1", "point": (0, 0, 0), "rx": 0, "ry": 0, "rz": 0}, {"name": "K2", "point": (10, 0, 0), "rx": 0, "ry": 0, "rz": 90}, {"name": "K3", "point": (20, 0, 0), "rx": 0, "ry": 45, "rz": 0}, {"name": "K4", "point": (30, 0, 0), "rx": 30, "ry": 45, "rz": 60}, ] for tc in test_cases: insert_ks(msp, tc["name"], tc["point"], tc["rx"], tc["ry"], tc["rz"]) out_path = os.path.join(results_dir, "ks_test.dxf") doc.saveas(out_path) print(f"Testdatei gespeichert: {out_path}\n") print("Verifikation:") ok = verify_ks(out_path, test_cases) print(f"\nErgebnis: {'ALLE OK' if ok else 'FEHLER GEFUNDEN'}") return ok # --------------------------------------------------------------------------- # main # --------------------------------------------------------------------------- def main(): parser = argparse.ArgumentParser( description="Setzt Koordinatensystem-Bloecke in Omniflo DXF-Dateien" ) parser.add_argument("--k1set", action="store_true", help="K1-Block an alle Boegen und Weichen setzen") parser.add_argument("--k2set", action="store_true", help="K2-Block an alle Boegen setzen (anderes Kettenende)") parser.add_argument("--show-omniflo", action="store_true", help="Uebersichts-DXF mit K-Positionen erzeugen") parser.add_argument("--test", action="store_true", help="Testdatei mit 4 KOS erzeugen und verifizieren") parser.add_argument("--number", type=int, help="Nur diese 9-stellige Sivasnr verarbeiten") args = parser.parse_args() if args.number and len(str(args.number)) != 9: print("FEHLER: --number muss eine 9-stellige Ganzzahl sein.") sys.exit(1) if not args.k1set and not args.k2set and not args.show_omniflo and not args.test: parser.print_help() sys.exit(1) data_dir = os.environ.get("DXFM_DATA") results_dir = os.environ.get("DXFM_RESULTS") if not data_dir or not results_dir: if args.test: results_dir = results_dir or "results" os.makedirs(results_dir, exist_ok=True) run_test(results_dir) return print("FEHLER: Umgebungsvariablen DXFM_DATA und DXFM_RESULTS muessen gesetzt sein.") sys.exit(1) os.makedirs(results_dir, exist_ok=True) if args.test: run_test(results_dir) if args.k1set: process_k1set(data_dir, results_dir, args.number) if args.k2set: process_k2set(data_dir, results_dir, args.number) if args.show_omniflo: print("=== Koordinaten Uebersicht ===") show_omniflo(data_dir, results_dir) if __name__ == "__main__": main()