# =====================================================================
#  RefrigDNA SenseNode — Clamp Body  ·  FreeCAD macro (builds + exports STL)
#  Rev A1 / canonical spec "Rev C"  ·  Mar Organica  ·  TRL 4 prototype
# ---------------------------------------------------------------------
#  HOW TO RUN (you already have FreeCAD):
#    1. FreeCAD > Macro > Macros... > create "RDNA_clamp" > paste this in.
#    2. Edit OUT_DIR below to a real folder on your PC.
#    3. Press the green Run arrow.  It builds the body + jaw and writes
#       RDNA-CLAMP-BODY.stl and RDNA-CLAMP-JAW.stl ready to slice & print.
#  Units = mm.  Geometry mirrors the OpenSCAD model (simplified booleans).
#  Lines marked  # TODO-CAD  are assumptions to confirm vs your internals.
# =====================================================================
import os, FreeCAD as App, Part, Mesh

OUT_DIR = os.path.expanduser("~/Desktop")   # <-- EDIT to where you want the STLs

# ---- Rev C dimensions (mm) ----
encl_l, encl_w, encl_h = 116.0, 68.0, 42.5
wall, corner_r = 3.0, 6.0
pipe_d, pad_t  = 22.23, 1.5      # 22.23 = 7/8"; set 6.35..28.6
jaw_l, fit     = 48.0, 0.3
screw_d, pin_d, knuckle_r = 4.3, 4.0, 5.0

bore_r = pipe_d/2.0 + pad_t
jaw_h  = bore_r + wall
up_h   = encl_h - jaw_h
# pipe axis = line y=0, z=0 along X; upper body z>0, jaw z<0

def box(l,w,h,x=0,y=0,z=0):
    b = Part.makeBox(l,w,h); b.translate(App.Vector(x-l/2.0, y-w/2.0, z)); return b

def rbox(l,w,h,z=0):
    # box with rounded vertical edges via fillet
    b = Part.makeBox(l,w,h, App.Vector(-l/2.0,-w/2.0,z))
    edges = [e for e in b.Edges if abs(e.Vertexes[0].Z - e.Vertexes[1].Z) > h-0.01]
    try:    b = b.makeFillet(corner_r, edges)
    except: pass
    return b

def pipe_cyl(r):
    c = Part.makeCylinder(r, encl_l+4)
    c.rotate(App.Vector(0,0,0), App.Vector(0,1,0), 90)   # axis -> X
    c.translate(App.Vector(-(encl_l+4)/2.0,0,0)); return c

def knuckles(which):
    seg = jaw_l/3.0; solids=[]
    for i in range(3):
        keep = (i!=1) if which=="upper" else (i==1)
        if not keep: continue
        x0 = -jaw_l/2.0 + i*seg
        k = Part.makeCylinder(knuckle_r, seg-0.6)
        k.rotate(App.Vector(0,0,0), App.Vector(0,1,0), 90)
        k.translate(App.Vector(x0, -encl_w/2.0, 0))
        hole = Part.makeCylinder(pin_d/2.0 + (fit if which=="lower" else 0), seg+2)
        hole.rotate(App.Vector(0,0,0), App.Vector(0,1,0), 90)
        hole.translate(App.Vector(x0-1, -encl_w/2.0, 0))
        solids.append(k.cut(hole))
    s = solids[0]
    for x in solids[1:]: s = s.fuse(x)
    return s

def latch_lug(zmin,zmax):
    lug = Part.makeBox(18,8,zmax-zmin, App.Vector(-9, encl_w/2.0-1, zmin))
    hole= Part.makeCylinder(screw_d/2.0, (zmax-zmin)+2, App.Vector(0, encl_w/2.0+3, zmin-1))
    return lug.cut(hole)

def body():
    s = rbox(encl_l, encl_w, up_h, 0)
    s = s.fuse(knuckles("upper")).fuse(latch_lug(0,10))
    # service boss
    boss = Part.makeCylinder(6,8, App.Vector(encl_l/2.0-16,0,up_h-0.2))
    bh   = Part.makeCylinder(3.2,12, App.Vector(encl_l/2.0-16,0,up_h-1))   # TODO-CAD
    s = s.fuse(boss.cut(bh))
    s = s.cut(pipe_cyl(bore_r))                              # top half of bore
    cav = rbox(encl_l-2*wall, encl_w-2*wall, max(1,up_h-(bore_r+2)-wall), bore_r+2)
    s = s.cut(cav)                                           # electronics cavity
    usb = Part.makeBox(8,12,8, App.Vector(-encl_l/2.0-2,-6,up_h-9)); s = s.cut(usb)  # TODO-CAD
    pin = pipe_cyl(pin_d/2.0); pin.translate(App.Vector(0,-encl_w/2.0,0)); s = s.cut(pin)
    return s

def jaw():
    s = rbox(jaw_l, encl_w, jaw_h, -jaw_h)
    s = s.fuse(knuckles("lower")).fuse(latch_lug(-10,0))
    s = s.cut(pipe_cyl(bore_r+fit))
    pin = pipe_cyl(pin_d/2.0); pin.translate(App.Vector(0,-encl_w/2.0,0)); s = s.cut(pin)
    return s

if not os.path.isdir(OUT_DIR): OUT_DIR = os.path.expanduser("~")
for name, shp in [("RDNA-CLAMP-BODY", body()), ("RDNA-CLAMP-JAW", jaw())]:
    Part.show(shp, name)
    m = Mesh.Mesh(); m.addFacets(shp.tessellate(0.2))
    out = os.path.join(OUT_DIR, name+".stl"); m.write(out)
    App.Console.PrintMessage("Wrote %s\n" % out)
App.ActiveDocument.recompute()
App.Console.PrintMessage("Done. STLs are in %s\n" % OUT_DIR)
