Files
dxfmakros/lib/set_koords.py
T

669 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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()