FreeCAD/DXF to .rd conversion, named upload to controller memory with idle check and read-back verification, read-only status/file listing, and a fake controller for dry runs. Co-Authored-By: Claude Opus 5.5 <[email protected]>
88 lines
3.2 KiB
Python
Executable File
88 lines
3.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""dxf2rd.py — DXF (mm, LINE entities) -> Ruida .rd job FILE. Offline only.
|
|
|
|
Writes bytes to disk. Contains NO networking: it never talks to the laser.
|
|
A person loads/runs jobs at the laser (see SKILL.md, "Safety").
|
|
|
|
usage: dxf2rd.py in.dxf out.rd --speed 25 --max 65 --min 55 --passes 2 [--air]
|
|
"""
|
|
import argparse, sys
|
|
from meerk40t.ruida.rdjob import RDJob, swizzles_lut
|
|
|
|
UM = 1000 # Ruida native units are micrometres
|
|
|
|
|
|
def read_lines(path):
|
|
t = open(path).read().split("\n")
|
|
out = []
|
|
for i, v in enumerate(t):
|
|
if v.strip() == "LINE":
|
|
g = dict(zip([s.strip() for s in t[i + 1:i + 17:2]], [s.strip() for s in t[i + 2:i + 18:2]]))
|
|
out.append(((float(g["10"]), float(g["20"])), (float(g["11"]), float(g["21"]))))
|
|
return out
|
|
|
|
|
|
def chain(segs, tol=1e-3):
|
|
key = lambda p: (round(p[0] / tol), round(p[1] / tol))
|
|
segs, loops = segs[:], []
|
|
while segs:
|
|
a, b = segs.pop(0)
|
|
path = [a, b]
|
|
while key(path[-1]) != key(path[0]):
|
|
for k, (p, q) in enumerate(segs):
|
|
if key(p) == key(path[-1]): path.append(q); segs.pop(k); break
|
|
if key(q) == key(path[-1]): path.append(p); segs.pop(k); break
|
|
else:
|
|
break # open path: cut as-is
|
|
loops.append(path)
|
|
return loops
|
|
|
|
|
|
class Box:
|
|
def __init__(self, pts):
|
|
xs, ys = [p[0] for p in pts], [p[1] for p in pts]
|
|
self._b = (min(xs), min(ys), max(xs), max(ys))
|
|
def left(self): return self._b[0]
|
|
def upper(self): return self._b[1]
|
|
def right(self): return self._b[2]
|
|
def lower(self): return self._b[3]
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("dxf"); ap.add_argument("rd")
|
|
ap.add_argument("--speed", type=float, required=True, help="mm/s")
|
|
ap.add_argument("--max", type=float, required=True, help="max power %%")
|
|
ap.add_argument("--min", type=float, required=True, help="min power %%")
|
|
ap.add_argument("--passes", type=int, default=1)
|
|
ap.add_argument("--air", action="store_true")
|
|
ap.add_argument("--offset", type=float, nargs=2, default=(10, 10), help="mm from origin")
|
|
a = ap.parse_args()
|
|
assert 10 <= a.min <= a.max <= 80, "power outside 10-80% (shop rule)"
|
|
assert a.speed >= 5, "speed below 5 mm/s is a fire risk on wood"
|
|
|
|
loops = chain(read_lines(a.dxf))
|
|
ox, oy = a.offset
|
|
loops = [[(int(round((x + ox) * UM)), int(round((y + oy) * UM))) for x, y in lp] for lp in loops]
|
|
job = RDJob(magic=0x88)
|
|
allpts = [p for lp in loops for p in lp]
|
|
job.write_header([Box(allpts)])
|
|
job.write_settings({"speed": a.speed, "power": a.max * 10, "coolant": 1 if a.air else 2})
|
|
job.min_power_1(a.min); job.min_power_2(a.min) # Ruida min power: fires during accel/decel
|
|
cx = cy = None
|
|
for _ in range(a.passes):
|
|
for lp in loops:
|
|
x0, y0 = lp[0]
|
|
job.move_abs_xy(x0, y0); cx, cy = x0, y0
|
|
for x, y in lp[1:]:
|
|
job.mark(x, y, x - cx, y - cy); cx, cy = x, y
|
|
job.write_tail()
|
|
data = job.get_contents()
|
|
sw, _ = swizzles_lut(0x88)
|
|
open(a.rd, "wb").write(bytes(sw[b] for b in data))
|
|
print(f"{a.rd}: {len(loops)} paths x {a.passes} passes, {len(data)} bytes")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|