Files
RuidaSkill/ruida-laser/scripts/fc2dxf.py
T
wrickertandClaude Opus 5.5 4e957509cc Ruida laser skill: prepare and load jobs, never run them
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]>
2026-10-04 10:24:05 -05:00

50 lines
2.6 KiB
Python
Executable File

# fc2dxf.py: export a flat FreeCAD body (sheet part) to a LINE-only DXF for dxf2rd.py.
# Takes the largest planar face normal to the body's thinnest axis, writes its outline and holes
# with inner loops first (so the part doesn't drop before its holes are cut), origin at the
# bottom-left corner, mm. Curves are flattened to lines within 0.02 mm. Opens the .FCStd
# read-only and never saves it.
#
# usage (FreeCAD's headless interpreter):
# freecadcmd -c "import sys; sys.argv=['x','MODEL.FCStd','BodyLabel','OUT.dxf']; exec(open('fc2dxf.py').read())"
# Prints the part thickness: check it matches your stock. A blind pocket (not through) can't be
# cut, only engraved, so the script refuses bodies with planar faces between the two sides.
import FreeCAD, sys
MODEL, NAME, OUT = sys.argv[-3], sys.argv[-2], sys.argv[-1]
d = FreeCAD.openDocument(MODEL)
objs = d.getObjectsByLabel(NAME)
assert objs, f"no object labelled {NAME!r}"
s = objs[0].Shape; b = s.BoundBox
dims = [b.XLength, b.YLength, b.ZLength]; t = dims.index(min(dims))
ax = [i for i in range(3) if i != t]
mn = [b.XMin, b.YMin, b.ZMin]
flat = [f for f in s.Faces if f.Surface.__class__.__name__ == 'Plane'
and abs(abs(f.normalAt(0, 0)[t]) - 1) < 1e-6]
levels = sorted({round(f.CenterOfMass[t], 3) for f in flat})
assert len(levels) == 2, f"faces at {levels}: blind pocket or step, not a through-cut sheet part"
f = max(flat, key=lambda f: f.Area)
loops = []
for w in f.Wires:
segs = []
for e in w.OrderedEdges:
pts = ([e.Vertexes[0].Point, e.Vertexes[-1].Point] if e.Curve.__class__.__name__ == 'Line'
else e.discretize(Deflection=0.02))
pts = [(p[ax[0]] - mn[ax[0]], p[ax[1]] - mn[ax[1]]) for p in pts]
segs += list(zip(pts, pts[1:]))
xs = [p[0] for sg in segs for p in sg]; ys = [p[1] for sg in segs for p in sg]
loops.append(((max(xs) - min(xs)) * (max(ys) - min(ys)), segs))
loops.sort(key=lambda l: l[0]) # inner cuts first, outline last
out = ["0", "SECTION", "2", "HEADER", "9", "$INSUNITS", "70", "4", "0", "ENDSEC",
"0", "SECTION", "2", "ENTITIES"]
n = 0
for _, segs in loops:
for (x1, y1), (x2, y2) in segs:
if abs(x1 - x2) + abs(y1 - y2) < 1e-6:
continue
out += ["0", "LINE", "8", "CUT", "10", "%.4f" % x1, "20", "%.4f" % y1, "30", "0",
"11", "%.4f" % x2, "21", "%.4f" % y2, "31", "0"]; n += 1
out += ["0", "ENDSEC", "0", "EOF"]
open(OUT, 'w').write("\n".join(out) + "\n")
print(f"{NAME} -> {OUT}: {n} lines, {len(loops)} loops, {dims[ax[0]]:.2f} x {dims[ax[1]]:.2f} mm, "
f"thickness {dims[t]:.2f} mm")
FreeCAD.closeDocument(d.Name)