#!/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())