// ============================================================
// bumper_frame.scad — Sensor Head Roll Cage  Rev A  2026-03-01
// Agent: sl-mechanical
// ============================================================
// Roll cage for SaltyBot sensor head (25 mm OD stem).
// Protects RPLIDAR A1M8, RealSense D435i, and 4× IMX219 cameras
// when the robot tips over.
//
// Cage posts sit at 45°/135°/225°/315° (between camera arms at
// 0°/90°/180°/270°), minimising camera FOV obstruction.
// Crown ring sits ABOVE the RPLIDAR top — no scan-plane crossing.
// Posts lean outward so they are outside RPLIDAR body radius at
// scan-plane height.
//
// Component overview:
//   cage_collar_half()  — split stem collar, print 2×
//   cage_post()         — inclined rib, print 4×, lay flat
//   cage_crown()        — flat ring above RPLIDAR, print 1×
//   tpu_crown_pad()     — snap-on TPU impact strip, print in TPU
//
// RENDER options:
//   "assembly"          full 3-D view (default)
//   "collar_front"      front collar half for slicing
//   "collar_rear"       rear collar half for slicing
//   "post"              single post for slicing (print 4×)
//   "crown"             crown ring for slicing
//   "tpu_pad"           TPU snap strip for slicing
// ============================================================

RENDER = "assembly";

// ── Stem ─────────────────────────────────────────────────────
STEM_OD   = 25.0;
STEM_BORE = 25.5;            // +0.5 mm clearance (collar slides easily)

// ── Cage collar ──────────────────────────────────────────────
COL_OD    = 52.0;
COL_H     = 24.0;            // height (shorter than sensor-head collar)
COL_BOLT_X = 19.0;           // M4 clamping bolt CL from stem axis (X)
COL_BOLT_D =  4.5;           // M4 clearance
COL_NUT_W  =  7.0;           // M4 hex nut A/F
COL_NUT_H  =  3.4;           // M4 hex nut thickness

// Post attachment boss on collar outer surface
// Boss at 45°/135°/225°/315°, protrudes POST_BOSS_H radially
POST_BOSS_H =  6.0;
POST_BOSS_W = 14.0;          // boss width (tangential)
POST_BOSS_Z =  6.0;          // boss bottom offset from collar base
POST_BOSS_HT = COL_H - POST_BOSS_Z - 4; // boss height on collar

// ── Cage post ────────────────────────────────────────────────
POST_BASE_R = COL_OD/2 + POST_BOSS_H; // radial start of post CL
POST_TIP_R  = 108.0;         // radial end of post at crown height
CROWN_Z     = 148.0;         // height of crown ring (above collar base)
                             // RPLIDAR top is ~120 mm above cage collar
POST_W      = 12.0;          // post width (tangential)
POST_T      =  8.0;          // post thickness (radial)
POST_LEN    = sqrt(pow(POST_TIP_R - POST_BASE_R, 2) + pow(CROWN_Z, 2));
POST_LEAN   = atan((POST_TIP_R - POST_BASE_R) / CROWN_Z); // ~25° from vertical
// At RPLIDAR scan height (~105 mm), post is at radius:
// POST_BASE_R + (POST_TIP_R - POST_BASE_R) * (105/CROWN_Z) ≈ 78 mm
// >> RPLIDAR body radius 35 mm — no scan-plane intersection

POST_BOLT_D =  4.5;          // M4 clearance at each post end

// ── Crown ring ───────────────────────────────────────────────
CROWN_OD    = 230.0;         // crown outer diameter
CROWN_ID    = 206.0;         // crown inner diameter
CROWN_RH    =  12.0;         // crown ring height (thickness)
CROWN_PAD_D =  10.0;         // TPU pad snap channel diameter (semi-circle)
CROWN_PAD_INSET = 5.0;       // inset from crown outer edge

// Post socket: slot in crown ring where post tip inserts
SOCK_W  = POST_W + 0.4;      // slot width (clearance)
SOCK_D  = POST_T + 0.4;      // slot depth
SOCK_H  = CROWN_RH;          // full ring thickness

// ── TPU crown pad ────────────────────────────────────────────
PAD_ID   = CROWN_OD - 2 * CROWN_PAD_INSET;
PAD_OD   = CROWN_OD + 2.0;
PAD_H    = CROWN_PAD_D + 2.0;

$fn = 64;
e   = 0.01;

// ─────────────────────────────────────────────────────────────
// cage_collar_half(side)
//   side = "front" (+Y) | "rear" (-Y)
//   Print flat-face-down.
// ─────────────────────────────────────────────────────────────
module cage_collar_half(side = "front") {
    y_front = (side == "front");
    y_sign  = y_front ? 1 : -1;

    difference() {
        union() {
            // D-shaped half body
            intersection() {
                cylinder(d = COL_OD, h = COL_H);
                translate([-COL_OD/2, y_front ? 0 : -COL_OD/2, 0])
                    cube([COL_OD, COL_OD/2, COL_H]);
            }

            // Post attachment bosses at 45° and 135° (front) / 225° and 315° (rear)
            for (a = y_front ? [45, -45] : [135, -135]) {
                rotate([0, 0, a])
                    translate([COL_OD/2, -POST_BOSS_W/2, POST_BOSS_Z])
                        cube([POST_BOSS_H, POST_BOSS_W, POST_BOSS_HT]);
            }
        }

        // Stem bore
        translate([0, 0, -e])
            cylinder(d = STEM_BORE, h = COL_H + 2*e);

        // M4 clamping bolt holes (Y direction through mating face)
        for (bx = [-COL_BOLT_X, COL_BOLT_X]) {
            translate([bx, y_front ? COL_OD/2 : 0, COL_H/2])
                rotate([90, 0, 0])
                    cylinder(d = COL_BOLT_D, h = COL_OD/2 + e);
        }

        // M4 hex nut pockets in rear half
        if (!y_front) {
            for (bx = [-COL_BOLT_X, COL_BOLT_X]) {
                translate([bx, -(COL_OD/4 + e), COL_H/2])
                    rotate([90, 0, 0])
                        cylinder(d = COL_NUT_W / cos(30), h = COL_NUT_H + e, $fn = 6);
            }
        }

        // M4 set screw (front only, outer face → stem bore)
        if (y_front) {
            translate([0, COL_OD/2, COL_H * 0.7])
                rotate([90, 0, 0])
                    cylinder(d = COL_BOLT_D, h = COL_OD/2 - STEM_BORE/2 + e);
        }

        // M4 post attachment hole through each boss (radial direction)
        for (a = y_front ? [45, -45] : [135, -135]) {
            rotate([0, 0, a])
                translate([COL_OD/2 + e, 0, POST_BOSS_Z + POST_BOSS_HT/2])
                    rotate([0, -90, 0])
                        cylinder(d = POST_BOLT_D, h = POST_BOSS_H + COL_OD/8 + e);
        }
    }
}

// ─────────────────────────────────────────────────────────────
// cage_post()
//   For assembly: placed radially at angle a.
//   For slicing ("post" RENDER): printed lying flat on XY plane.
// ─────────────────────────────────────────────────────────────
module cage_post_geometry() {
    // Post spans from base (r = POST_BASE_R, z = 0) to tip (r = POST_TIP_R, z = CROWN_Z)
    // Width POST_W in tangential direction, thickness POST_T in radial direction
    difference() {
        hull() {
            translate([POST_BASE_R, -POST_W/2, 0])
                cube([POST_T, POST_W, 1]);
            translate([POST_TIP_R, -POST_W/2, CROWN_Z - 1])
                cube([POST_T, POST_W, 1]);
        }

        // M4 hole at base end (through POST_T, radially)
        translate([POST_BASE_R + POST_T/2, 0, POST_BOSS_Z + POST_BOSS_HT/2])
            rotate([0, 90, 0])
                cylinder(d = POST_BOLT_D, h = POST_T + 2*e, center = true);

        // M4 hole at tip end
        translate([POST_TIP_R + POST_T/2, 0, CROWN_Z - CROWN_RH/2])
            rotate([0, 90, 0])
                cylinder(d = POST_BOLT_D, h = POST_T + 2*e, center = true);

        // TPU pad snap groove on outer (high-r) face — runs along post length
        // Groove: 5 mm wide × 2.5 mm deep semi-channel at midspan
        translate([POST_TIP_R + POST_T - 0.1, -2.5, CROWN_Z * 0.3])
            cube([3, 5, CROWN_Z * 0.4]);
    }
}

// ─────────────────────────────────────────────────────────────
// cage_crown()
//   Flat ring. 4 post sockets at 45/135/225/315°.
//   TPU snap channel on outer rim.
// ─────────────────────────────────────────────────────────────
module cage_crown() {
    difference() {
        cylinder(d = CROWN_OD, h = CROWN_RH, $fn = 8);

        // Hollow centre
        translate([0, 0, -e])
            cylinder(d = CROWN_ID, h = CROWN_RH + 2*e);

        // 4 post-tip sockets at 45/135/225/315°
        for (a = [45, 135, 225, 315]) {
            rotate([0, 0, a])
                translate([CROWN_ID/2 + (CROWN_OD-CROWN_ID)/4 - SOCK_D/2,
                           -SOCK_W/2, -e])
                    cube([SOCK_D + e, SOCK_W, SOCK_H + 2*e]);
        }

        // 2× M4 bolts per post socket (through ring top to post)
        for (a = [45, 135, 225, 315]) {
            rotate([0, 0, a])
                for (dw = [-POST_W/4, POST_W/4])
                    translate([CROWN_ID/2 + (CROWN_OD-CROWN_ID)/4,
                               dw, -e])
                        cylinder(d = POST_BOLT_D, h = CROWN_RH + 2*e);
        }

        // TPU snap channel — semicircular groove on outer rim top face
        translate([0, 0, CROWN_RH - CROWN_PAD_D/2])
            rotate_extrude()
                translate([CROWN_OD/2 - CROWN_PAD_INSET, 0])
                    circle(d = CROWN_PAD_D);
    }
}

// ─────────────────────────────────────────────────────────────
// tpu_crown_pad()  — print in TPU, snaps into crown channel
// ─────────────────────────────────────────────────────────────
module tpu_crown_pad() {
    // Ring with round cross-section slightly larger than channel
    rotate_extrude()
        translate([PAD_ID/2 + (PAD_OD - PAD_ID)/2 - CROWN_PAD_INSET, 0])
            circle(d = CROWN_PAD_D * 1.15);  // slight interference for snap
}

// ─────────────────────────────────────────────────────────────
// Render selector
// ─────────────────────────────────────────────────────────────
if (RENDER == "assembly") {
    color("SkyBlue", 0.9)     cage_collar_half("front");
    color("DodgerBlue", 0.9)  mirror([0,1,0]) cage_collar_half("rear");
    for (a = [45, 135, 225, 315])
        color("LightGray", 0.85)
            rotate([0, 0, a]) cage_post_geometry();
    color("Silver", 0.9)      translate([0, 0, CROWN_Z]) cage_crown();
    color("OrangeRed", 0.7)   translate([0, 0, CROWN_Z + CROWN_RH - CROWN_PAD_D/2])
                                  tpu_crown_pad();

} else if (RENDER == "collar_front") {
    cage_collar_half("front");

} else if (RENDER == "collar_rear") {
    cage_collar_half("rear");

} else if (RENDER == "post") {
    // Orient flat for slicing: long axis along X, flat face on Z=0
    rotate([0, 0, -45])             // un-rotate from assembly angle
        rotate([-90, 0, 0])         // lay flat
            cage_post_geometry();

} else if (RENDER == "crown") {
    cage_crown();

} else if (RENDER == "tpu_pad") {
    tpu_crown_pad();
}
