"""SPLIT v2 fixed side into two independently printable pieces (Cameron 2026-07-09):
- gripper_v2_base_v3.stl : gripper base (holder/riser/plate/finger/cradle) + 4 mounting pins on top
- gripper_v2_umimod_v3.stl : the UMI module (foot plate + camera bracket + pin mount + marker slab)
Drop-on pins (Ø3, CLEAR=0.35 — the coupon-validated fit) + 2 optional M2.5 lock screws.
Reprint a new box or camera mount WITHOUT touching the base."""
import numpy as np, trimesh, trimesh.repair
from gen_curvy import _rrect, _stitch

SPLIT_Y = -28.7          # foot bottom SEATS FLUSH on the riser top (-28.7); holder 0.3 below, servo 0.5 clear
PIN_R, CLEAR, PIN_PROUD = 1.5, 0.35, 3.5
def LR(f):
    m = trimesh.load(f, force="mesh")
    if not m.is_volume:
        m.merge_vertices(merge_tex=True, merge_norm=True)
        m.update_faces(m.nondegenerate_faces()); m.update_faces(m.unique_faces())
        trimesh.repair.fill_holes(m); m.fix_normals()
    return m
def U(parts):
    m = trimesh.boolean.union(parts, engine="manifold")
    keep = [b for b in m.split(only_watertight=False) if b.volume > 100.0]
    return keep[0] if len(keep) == 1 else trimesh.util.concatenate(keep)
def D(a, b): return trimesh.boolean.difference([a, b], engine="manifold")
def cylY(x, z, r, y0, y1):
    c = trimesh.creation.cylinder(radius=r, height=abs(y1 - y0), sections=40)
    c.apply_transform(trimesh.transformations.rotation_matrix(np.pi / 2, [1, 0, 0]))
    c.apply_translation([x, (y0 + y1) / 2, z]); return c
def prism(lo, hi, r=5.0):
    L = hi - lo; k = int(np.argmax(L)); lat = [i for i in range(3) if i != k]
    e1, e2 = np.eye(3)[lat[0]], np.eye(3)[lat[1]]
    c = (lo + hi) / 2; rings = []
    for i in range(12):
        t = i / 11.0
        q = _rrect(L[lat[0]] / 2, L[lat[1]] / 2, min(r, L[lat[0]] / 2 - 0.4, L[lat[1]] / 2 - 0.4))
        p = c.copy(); p[k] = lo[k] + L[k] * t
        rings.append(p + np.outer(q[:, 0], e1) + np.outer(q[:, 1], e2))
    return _stitch(rings)

holder = LR("gnode_v2_holder.stl"); riser = LR("gnode_v2_fixed_riser.stl")
# pins: 2 on the holder top (y=-28.4 face), 2 on the riser top (y=-28.7)
pins = [(-1.0, -8.0, -28.3), (-1.0, 8.0, -28.3), (14.0, 20.0, -28.6), (14.0, 33.0, -28.6)]
pin_meshes = []
for (px, pz, ytop) in pins:
    body = cylY(px, pz, PIN_R, ytop + 2.0, SPLIT_Y - PIN_PROUD + PIN_R)
    dome = trimesh.creation.icosphere(radius=PIN_R, subdivisions=3)
    dome.apply_translation([px, SPLIT_Y - PIN_PROUD + PIN_R, pz])
    pin_meshes += [body, dome]
base = U([LR("gripper_v2_print_fixed.stl")] + pin_meshes)
base.export("gripper_v2_base_v3.stl")
print(f"base_v3: wt={base.is_watertight} bodies={len(base.split(only_watertight=False))} vol={base.volume/1000:.1f}cm3")

# module: foot plate + cut bracket + mount + slab + bridge
slab = LR("gnode_v2_aruco_cube.stl")
br = LR("gnode_v2_cam_conn.stl"); mount = LR("gnode_v2_camera_mount.stl")
foot = prism(np.array([-13.0, SPLIT_Y - 6.0, -16.0]), np.array([25.0, SPLIT_Y, 39.0]), r=6.0)   # stops 2mm short of the yoke (x max -15)
plane_o, plane_n = [0, SPLIT_Y, 0], [0, -1, 0]                    # keep y <= SPLIT_Y
br_cut = trimesh.intersections.slice_mesh_plane(br, plane_normal=plane_n, plane_origin=plane_o, cap=True)
slo, shi = slab.bounds
bridge = prism(np.array([18.0, SPLIT_Y - 8.0, max(slo[2], -16.0) + 2]),
               np.array([min(slo[0] + 4.0, shi[0] - 2.0), SPLIT_Y, min(shi[2], 39.0) - 2]), r=4.0)   # never through the marker face
mod = U([foot, br_cut, mount, slab, bridge])
# sockets + optional M2.5 locks + USB corridor + camera seat relief
for (px, pz, ytop) in pins:
    mod = D(mod, cylY(px, pz, PIN_R + CLEAR, SPLIT_Y - PIN_PROUD - 0.4, SPLIT_Y + 0.1))
for (sx, sz) in [(-1.0, 0.0), (14.0, 26.5)]:
    mod = D(mod, cylY(sx, sz, 1.45, SPLIT_Y - 6.5, SPLIT_Y + 0.1))   # M2.5 clearance through the foot
T = np.load("/tmp/T_auth_to_v2.npy")
cor = trimesh.creation.box(extents=[16.6, 12.8, 50.0])
cor.apply_translation([-7.0, -13.7, -28.0]); cor.apply_transform(T)
mod = D(mod, cor)
body = LR("gnode_v2_camera_body.stl")
blob = trimesh.boolean.intersection([body, mod], engine="manifold")
if blob.volume > 1.0:
    V = blob.convex_hull.vertices
    offs = [d * s for d in np.eye(3) for s in (1.5, -1.5)]
    mod = D(mod, trimesh.PointCloud(np.vstack([V + o for o in offs])).convex_hull)
keep = [b for b in mod.split(only_watertight=False) if b.volume > 100.0]
mod = keep[0] if len(keep) == 1 else trimesh.util.concatenate(keep)
mod.export("gripper_v2_umimod_v3.stl")
nb = len(mod.split(only_watertight=False))
print(f"umimod_v3: wt={mod.is_watertight} bodies={nb} vol={mod.volume/1000:.1f}cm3{' <-- DISJOINT' if nb>1 else ''}")
chk = trimesh.boolean.intersection([body, mod], engine="manifold").volume
print(f"camera body vs module: {chk:.1f}mm3")
