#!/usr/bin/env python
"""C-10 — strap-style mount for a Ø29 inrunner (RC airplane).

Two parts, trimesh + manifold3d CSG, all dimensions mm, motor axis = X, Z up.
  cradle : base plate + two ribs with a half-round trough; carries the
           airframe hole pattern (4 × Ø3 on 21.5 × 37.0, counterbored from
           the trough side) and two M3 clamp posts per rib.
  strap  : half-ring cap with two flanges, printed twice (one per rib).
Clamp travel comes from GAP: the strap's flange faces float GAP above the
cradle's top when the bore is nominal, so tightening closes on the motor.
Pattern dimensions measured from printables.com/model/698239 (appelm,
CC BY 4.0) — see NOTES.md.  Regenerate with other values by editing the
PARAMS block; every feature derives from them.
"""
import sys, numpy as np, trimesh
from trimesh.creation import box, cylinder

# ---------------- PARAMS ----------------
MOTOR_D   = 29.0     # motor can diameter (nominal bore)
WALL      = 3.1      # ring / rib wall around the bore
GAP       = 1.0      # clamp travel: strap flange floats this far above cradle top
RIB_W     = 8.0      # rib and strap width along the motor axis (X)
RIB_X     = 18.5     # rib centre ±X  (= airframe pattern 37.0 / 2)
AF_DX, AF_DY = 37.0, 21.5   # airframe hole pattern (X along axis, Y across)
AF_HOLE   = 3.2      # M3 clearance
AF_CBORE_D, AF_CBORE_FLOOR = 6.2, 3.5   # counterbore Ø, floor left under the head
CLAMP_Y   = 21.5     # clamp screw centres ±Y
CLAMP_HOLE_STRAP = 3.4        # clearance in the strap flange
INSERT_D, INSERT_DEPTH = 4.0, 6.0   # M3 heat-set pocket at the rib top
PILOT_D   = 2.7      # self-tap pilot below the pocket (fallback without inserts)
BASE_T    = 4.0      # base plate thickness
FLANGE_T  = 3.5      # strap flange thickness
EXTRA_W   = 3.5      # material outboard of the clamp screw
# ----------------------------------------
R  = MOTOR_D / 2
W  = 2 * (CLAMP_Y + EXTRA_W)          # overall width (Y)
L  = 2 * RIB_X + RIB_W + 2 * 2.0      # overall length (X)
HC = R + WALL                         # bore centre height above base bottom
SEG = 128

def cyl_x(r, length, cx=0, cy=0, cz=0):
    c = cylinder(radius=r, height=length, sections=SEG)
    c.apply_transform(trimesh.transformations.rotation_matrix(np.pi/2, [0,1,0]))
    c.apply_translation([cx, cy, cz]); return c
def cyl_z(r, z0, z1, cx=0, cy=0):
    c = cylinder(radius=r, height=z1-z0, sections=64)
    c.apply_translation([cx, cy, (z0+z1)/2]); return c
def bx(x0,x1,y0,y1,z0,z1):
    b = box(extents=[x1-x0, y1-y0, z1-z0]); b.apply_translation([(x0+x1)/2,(y0+y1)/2,(z0+z1)/2]); return b
def union(ms): return trimesh.boolean.union(ms, engine="manifold")
def diff(a, ms): return trimesh.boolean.difference([a]+ms, engine="manifold")

# ---- cradle ----
plate = bx(-L/2, L/2, -W/2, W/2, 0, BASE_T)
ribs  = [bx(sx*RIB_X-RIB_W/2, sx*RIB_X+RIB_W/2, -W/2, W/2, 0, HC) for sx in (-1,1)]
cradle = union([plate]+ribs)
cuts = [cyl_x(R, L+10, cz=HC)]                                   # trough
for sx in (-1,1):
    for sy in (-1,1):
        x, y = sx*AF_DX/2, sy*AF_DY/2
        cuts.append(cyl_z(AF_HOLE/2, -1, HC+1, x, y))            # airframe through-hole
        cuts.append(cyl_z(AF_CBORE_D/2, AF_CBORE_FLOOR, HC+1, x, y))   # counterbore up to trough
        cx, cy = sx*RIB_X, sy*CLAMP_Y
        cuts.append(cyl_z(INSERT_D/2, HC-INSERT_DEPTH, HC+1, cx, cy))  # insert pocket
        cuts.append(cyl_z(PILOT_D/2, BASE_T+1.0, HC+1, cx, cy))        # self-tap pilot, stops 1 mm above the plate top (plate stays solid)
cradle = diff(cradle, cuts)

# ---- strap (built with ring centre at origin, Z up; printed lying on its X face) ----
ring = diff(cyl_x(R+WALL, RIB_W), [cyl_x(R, RIB_W+2)])
ring = diff(ring, [bx(-RIB_W, RIB_W, -W, W, -R-WALL-1, GAP)])   # keep Z >= GAP only
flange = bx(-RIB_W/2, RIB_W/2, -W/2, W/2, GAP, GAP+FLANGE_T)
strap = union([ring, flange])
strap = diff(strap, [cyl_x(R, RIB_W+2)] + [cyl_z(CLAMP_HOLE_STRAP/2, -1, GAP+FLANGE_T+1, 0, sy*CLAMP_Y) for sy in (-1,1)])

# Print orientation for the strap: lie it on its X face so the D-shaped
# profile is flat on the bed (no arch to bridge, no support). Rotate 90° about
# Y: the 8 mm width becomes the part height. The cradle prints base-down as built.
strap_print = strap.copy()
strap_print.apply_transform(trimesh.transformations.rotation_matrix(np.pi/2, [0,1,0]))

out = sys.argv[1] if len(sys.argv) > 1 else "."
for name, m in (("c10_cradle", cradle), ("c10_strap", strap_print)):
    m.merge_vertices(); m.fix_normals()
    ext = m.extents
    print(f"{name}: watertight={m.is_watertight} volume={m.volume:.0f} mm3 extents={np.round(ext,2).tolist()} faces={len(m.faces)}")
    m.export(f"{out}/{name}.stl")
print(f"params: bore Ø{MOTOR_D} wall {WALL} gap {GAP} cradle {L:.1f}x{W:.1f}x{HC:.1f} rib_x ±{RIB_X} af {AF_DX}x{AF_DY} clamp_y ±{CLAMP_Y}")
