// ============================================================
// sensor_rail_brackets.scad — Quick-Swap Sensor Brackets
// Issue: #138  Agent: sl-mechanical  Date: 2026-03-01
// ============================================================
//
// Slide-on T-slot brackets for the 2020 sensor rail defined in
// sensor_rail.scad.  All brackets share a common T-nut base that
// clamps to the rail with a single M3 thumbscrew (tool-free, ¼ turn).
//
// Bracket catalogue:
//   Part 1 — universal_tnut_base()   shared base for all brackets
//   Part 2 — rplidar_bracket()       RPLIDAR A1M8 Ø70 mm scanner
//   Part 3 — d435i_bracket()         Intel RealSense D435i (90×25×25 mm)
//   Part 4 — imx219_bracket()        IMX219 CSI camera (32×32 mm PCB)
//   Part 5 — uwb_bracket()           MaUWB ESP32-S3 anchor (~50×25 mm)
//   Part 6 — cable_clip()            Cable management clip
//   Part 7 — assembly_preview()      Full rail + all brackets
//
// Sensor interface dimensions (caliper-verified):
//   RPLIDAR A1M8   : body Ø70 mm, bolt circle Ø58 mm, 4× M3 at 45°
//   D435i          : 90×25×25 mm body, 1/4-20 UNC female bottom port
//   IMX219         : 32×32 mm PCB, M2 holes 24×24 mm (±12 mm grid)
//   UWB ESP32-S3   : 50×25 mm PCB, M3 holes at corners (42×17 mm)
//
// T-nut base interface (matches sensor_rail.scad constants):
//   TNUT_W = 9.8 mm, TNUT_H = 5.5 mm, TNUT_L = 12 mm
//   M3 thumbscrew (M3×16 SHCS + printed thumbwheel)
//
// Coordinate convention (same as sensor_rail.scad):
//   Rail runs along Z (vertical); front sensor face faces +Y.
//   Bracket Z=0 at the bottom of the bracket body.
//
// RENDER options:
//   "assembly"        full preview — rail + all brackets
//   "base_stl"        T-nut base only (universal — print ×N)
//   "rplidar_stl"     RPLIDAR bracket arm + platform
//   "d435i_stl"       D435i bracket arm + plate
//   "imx219_stl"      IMX219 bracket arm + PCB cradle
//   "uwb_stl"         UWB bracket arm + PCB cradle
//   "cable_clip_stl"  cable management clip (print ×6–10)
//
// Export commands:
//   openscad sensor_rail_brackets.scad -D 'RENDER="base_stl"'    -o srb_tnut_base.stl
//   openscad sensor_rail_brackets.scad -D 'RENDER="rplidar_stl"' -o srb_rplidar.stl
//   openscad sensor_rail_brackets.scad -D 'RENDER="d435i_stl"'   -o srb_d435i.stl
//   openscad sensor_rail_brackets.scad -D 'RENDER="imx219_stl"'  -o srb_imx219.stl
//   openscad sensor_rail_brackets.scad -D 'RENDER="uwb_stl"'     -o srb_uwb.stl
//   openscad sensor_rail_brackets.scad -D 'RENDER="cable_clip_stl"' -o srb_cable_clip.stl
// ============================================================

$fn = 64;
e   = 0.01;

// ── Rail geometry constants (must match sensor_rail.scad) ────────────────────
RAIL_W       = 20.0;
SLOT_OPEN    =  6.0;
SLOT_INNER_W = 10.2;
SLOT_INNER_H =  5.8;
SLOT_NECK_H  =  3.2;
INDEX_PITCH  = 25.0;
INDEX_HOLE_D =  5.3;

// ── T-nut constants (must match sensor_rail.scad) ────────────────────────────
TNUT_W         = 9.8;
TNUT_H         = 5.5;
TNUT_L         = 12.0;
TNUT_M3_NUT_AF = 5.5;
TNUT_M3_NUT_H  = 2.5;
TNUT_BOLT_D    = 3.3;

// ── Bracket base geometry ────────────────────────────────────────────────────
BASE_FACE_W  = 30.0;   // width of base body on rail face
BASE_FACE_H  = 20.0;   // height of base body (along rail Z axis)
BASE_FACE_T  = SLOT_NECK_H + 1.5;   // depth: neck + small flange overstand

// ── Bracket arm geometry ─────────────────────────────────────────────────────
ARM_T        =  4.0;   // arm wall thickness
ARM_OUT      = 30.0;   // arm reach out from rail face (+Y)

// ── Sensor interface constants ───────────────────────────────────────────────
// RPLIDAR A1M8
RPL_BODY_D    = 70.0;   // scanner body OD
RPL_BC_D      = 58.0;   // bolt circle diameter
RPL_BOLT_D    =  3.3;   // M3 clearance
RPL_PLAT_T    =  4.0;   // platform plate thickness
RPL_PLAT_D    = 76.0;   // platform OD (6 mm clearance around body)

// D435i RealSense
D4_BODY_W     = 90.0;   // body width (X)
D4_BODY_D     = 25.0;   // body depth (Y)
D4_BODY_H     = 25.0;   // body height (Z)
D4_MOUNT_D    =  6.5;   // 1/4-20 UNC clearance bore (6.35 mm + 0.15)
D4_PLATE_W    = 96.0;   // mounting plate width
D4_PLATE_T    =  3.0;   // mounting plate thickness
D4_TILT_DEG   =  8.0;   // nose-down tilt (matches rover/tank D435i mounts)

// IMX219 CSI camera
IMX_PCB_W     = 32.0;
IMX_PCB_H     = 32.0;
IMX_HOLE_SPC  = 24.0;   // M2 hole pattern (±12 mm, square)
IMX_BOLT_D    =  2.4;   // M2 clearance
IMX_TILT_DEG  = 10.0;   // slight downward tilt for terrain view
IMX_CRADLE_T  =  3.0;   // cradle plate thickness

// UWB anchor (MaUWB ESP32-S3 ~50×25 mm)
UWB_PCB_W     = 50.0;
UWB_PCB_H     = 25.0;
UWB_HOLE_X    = 42.0;   // hole pattern X span (±21 mm)
UWB_HOLE_Y    = 17.0;   // hole pattern Y span (±8.5 mm)
UWB_BOLT_D    =  3.3;   // M3 clearance
UWB_CRADLE_T  =  3.0;

// Cable clip
CLIP_CABLE_D  =  6.5;   // max cable bundle OD (6 mm typ)
CLIP_T        =  2.5;   // clip wall thickness

// Fasteners
M2_D = 2.4;
M3_D = 3.3;
M4_D = 4.3;
M5_D = 5.3;

// ============================================================
// RENDER DISPATCH
// ============================================================
RENDER = "assembly";

if (RENDER == "assembly") {
    assembly_preview();
} else if (RENDER == "base_stl") {
    universal_tnut_base();
} else if (RENDER == "rplidar_stl") {
    rplidar_bracket();
} else if (RENDER == "d435i_stl") {
    d435i_bracket();
} else if (RENDER == "imx219_stl") {
    imx219_bracket();
} else if (RENDER == "uwb_stl") {
    uwb_bracket();
} else if (RENDER == "cable_clip_stl") {
    cable_clip();
}

// ============================================================
// ASSEMBLY PREVIEW
// ============================================================
module assembly_preview() {
    // Ghost rail section
    %color("Silver", 0.35) {
        linear_extrude(250)
            square([RAIL_W, RAIL_W], center = true);
    }

    // RPLIDAR bracket at top (Z=180)
    color("OliveDrab", 0.85)
        translate([0, 0, 180])
            rplidar_bracket();

    // D435i bracket (Z=120)
    color("DarkSlateGray", 0.85)
        translate([0, 0, 120])
            d435i_bracket();

    // IMX219 bracket (Z=75)
    color("Teal", 0.85)
        translate([0, 0, 75])
            imx219_bracket();

    // UWB bracket (Z=30)
    color("SaddleBrown", 0.85)
        translate([0, 0, 30])
            uwb_bracket();

    // Cable clips
    for (cz = [50, 100, 150, 200])
        color("DimGray", 0.70)
            translate([RAIL_W/2, 0, cz])
                rotate([0, -90, 0])
                    cable_clip();
}

// ============================================================
// PART 1 — UNIVERSAL T-NUT BASE
// ============================================================
// Common base used by all sensor brackets.
// Slides into the 2020 rail T-groove from the end.
// M3×16 SHCS + thumbwheel clamps the T-nut from outside the rail.
//
// Print: PETG, 5 perims, 60 % infill, flat face (face plate) down.
// Orientation: face plate is the -Y face (against rail), T-nut protrudes +Y.
//
// The face plate provides a flat surface for bracket arm attachment via
// 2× M3 bolts (inset into the bracket arm from the +Y side).
module universal_tnut_base() {
    difference() {
        union() {
            // ── Face plate (sits flush against rail outer face) ──────────
            // Width = BASE_FACE_W, height = BASE_FACE_H, thin (BASE_FACE_T)
            translate([-BASE_FACE_W/2, -BASE_FACE_T, 0])
                cube([BASE_FACE_W, BASE_FACE_T, BASE_FACE_H]);

            // ── T-nut tongue (protrudes into rail T-groove) ──────────────
            // Centred on face plate; sized to TNUT_W × TNUT_H
            translate([-TNUT_W/2, 0, (BASE_FACE_H - TNUT_L)/2])
                cube([TNUT_W, SLOT_NECK_H + e, TNUT_L]);

            // ── T-nut inner body (wider, inside T-groove) ────────────────
            translate([-TNUT_W/2, SLOT_NECK_H - e, (BASE_FACE_H - TNUT_L)/2])
                cube([TNUT_W, TNUT_H - SLOT_NECK_H + e, TNUT_L]);
        }

        // ── M3 thumbscrew bore (centre of T-nut, through face plate) ────
        translate([0, -BASE_FACE_T - e, BASE_FACE_H/2])
            rotate([-90, 0, 0])
                cylinder(d = TNUT_BOLT_D, h = BASE_FACE_T + TNUT_H + 2*e);

        // ── M3 hex nut pocket (inside T-nut body, for thumbscrew) ────────
        translate([0, SLOT_NECK_H + 0.3, BASE_FACE_H/2])
            rotate([-90, 0, 0])
                cylinder(d = TNUT_M3_NUT_AF / cos(30),
                         h = TNUT_M3_NUT_H + 0.3,
                         $fn = 6);

        // ── 2× M3 bolt holes in face plate (bracket arm attachment) ─────
        for (hz = [BASE_FACE_H * 0.28, BASE_FACE_H * 0.72])
            translate([0, -BASE_FACE_T - e, hz])
                rotate([-90, 0, 0])
                    cylinder(d = M3_D, h = BASE_FACE_T + 2*e);

        // ── Index pin pocket (optional 25 mm grid lock) ──────────────────
        // Shallow Ø5 mm pocket on right side — aligns with rail index hole
        translate([BASE_FACE_W/2 - 3, -BASE_FACE_T/2, BASE_FACE_H/2])
            rotate([0, 90, 0])
                cylinder(d = 5.1, h = 3 + e);
    }
}

// ── Internal helper: base with bolt holes for arm attachment ─────────────────
// Used by all sensor brackets — arms bolt to this face plate.
// Returns the face plate at Y=0 (rail face), arm extends in +Y.
module _base_with_arm_holes() {
    universal_tnut_base();
}

// ── Internal helper: arm stem from rail face to sensor platform ───────────────
// arm_len: reach in +Y from rail face
// arm_w  : arm width in X
// arm_h  : arm height in Z (same as BASE_FACE_H unless overridden)
module _arm(arm_len, arm_w, arm_h = BASE_FACE_H) {
    // Chamfered arm block
    hull() {
        translate([-arm_w/2, 0, 0])
            cube([arm_w, ARM_T, arm_h]);
        translate([-arm_w/2, arm_len - ARM_T, (arm_h - arm_h*0.6)/2])
            cube([arm_w, ARM_T, arm_h * 0.6]);
    }
}

// ============================================================
// PART 2 — RPLIDAR A1M8 BRACKET
// ============================================================
// Circular platform that holds the RPLIDAR A1M8 scanner on top
// of the sensor rail.  The scanner sits with its scan plane
// perpendicular to the rail (horizontal plane).
//
// RPLIDAR bolt pattern: 4× M3 on Ø58 mm BC at 45°/135°/225°/315°.
// Motor connector exits through a slot in the rear of the platform.
//
// Print: PETG, 4 perims, 40 % infill.  Arm + platform in one piece.
module rplidar_bracket() {
    union() {
        // T-nut base
        _base_with_arm_holes();

        // Vertical arm rising above rail centre
        translate([0, 0, 0])
            difference() {
                // Arm + top platform merge via hull
                union() {
                    // Arm
                    translate([-BASE_FACE_W/2, 0, 0])
                        cube([BASE_FACE_W, ARM_OUT, ARM_T]);

                    // Platform disc at arm end
                    translate([0, ARM_OUT, BASE_FACE_H/2 + 5])
                        cylinder(d = RPL_PLAT_D, h = RPL_PLAT_T);
                }

                // ── Central bore (RPLIDAR body clears through) ───────────
                // Not needed — scanner sits ON platform, not through it.
                // Slot for motor/USB connector exit at rear
                translate([0, ARM_OUT - RPL_PLAT_D/2 - e,
                           BASE_FACE_H/2 + 5 - e])
                    cube([20, 15, RPL_PLAT_T + 2*e], center = true);
            }

        // ── Bolt holes subtracted from platform (separate difference) ────
        translate([0, ARM_OUT, BASE_FACE_H/2 + 5])
            difference() {
                cylinder(d = RPL_PLAT_D, h = RPL_PLAT_T);

                // 4× M3 bolt holes on Ø58 mm bolt circle at 45°
                for (a = [45, 135, 225, 315])
                    translate([RPL_BC_D/2 * cos(a),
                               RPL_BC_D/2 * sin(a), -e])
                        cylinder(d = RPL_BOLT_D, h = RPL_PLAT_T + 2*e);

                // Lightening pockets (reduces print time/weight)
                for (a = [0, 90, 180, 270])
                    translate([RPL_PLAT_D/2 * 0.5 * cos(a),
                               RPL_PLAT_D/2 * 0.5 * sin(a), 1])
                        cylinder(d = 14, h = RPL_PLAT_T + e);

                // Connector slot
                translate([0, -RPL_PLAT_D/2 + 5, -e])
                    cube([20, 15, RPL_PLAT_T + 2*e], center = true);
            }
    }
}

// ============================================================
// PART 3 — D435i REALSENSE BRACKET
// ============================================================
// Angled mounting plate for the Intel RealSense D435i.
// Camera attaches via captured 1/4-20 UNC hex nut (standard tripod).
// 8° nose-down tilt for forward terrain view (matches rover/tank mounts).
//
// Print: PETG, 4 perims, 40 % infill.
module d435i_bracket() {
    union() {
        // T-nut base
        _base_with_arm_holes();

        // Horizontal arm + tilted mounting plate
        difference() {
            union() {
                // Arm from rail face to camera plate
                translate([-D4_PLATE_W/2, 0, 0])
                    cube([D4_PLATE_W, ARM_OUT, ARM_T]);

                // Camera mounting plate (tilted 8° nose-down)
                translate([0, ARM_OUT, BASE_FACE_H/2])
                    rotate([D4_TILT_DEG, 0, 0])
                        translate([-D4_PLATE_W/2, 0, -D4_PLATE_T/2])
                            cube([D4_PLATE_W, D4_PLATE_T, BASE_FACE_H]);
            }

            // 1/4-20 UNC clearance bore (6.5 mm through mounting plate)
            translate([0, ARM_OUT + D4_PLATE_T/2, BASE_FACE_H/2])
                rotate([D4_TILT_DEG + 90, 0, 0])
                    cylinder(d = D4_MOUNT_D, h = D4_PLATE_T + 2*e,
                             center = true);

            // 1/4-20 hex nut pocket (rear of plate — 11.1 mm AF UNC)
            translate([0, ARM_OUT - D4_PLATE_T/2 - 3, BASE_FACE_H/2])
                rotate([D4_TILT_DEG + 90, 0, 0])
                    cylinder(d = 11.4 / cos(30), h = 5,
                             $fn = 6, center = true);

            // Cable routing notch at plate edge
            translate([D4_PLATE_W/2 - 8, ARM_OUT,
                       BASE_FACE_H/2 - BASE_FACE_H * 0.3])
                cube([10, ARM_T + 2*e, 8], center = true);
        }
    }
}

// ============================================================
// PART 4 — IMX219 CSI CAMERA BRACKET
// ============================================================
// Small cradle for 32×32 mm IMX219 CSI camera PCB.
// 10° downward tilt for terrain view.
// PCB attaches with 4× M2 bolts (24×24 mm pattern, countersunk).
// FFC cable exits through open bottom of cradle.
//
// Print: PETG, 4 perims, 40 % infill.
module imx219_bracket() {
    pcb_h = IMX_PCB_H + 4;   // cradle height (PCB + rim)
    pcb_w = IMX_PCB_W + 4;   // cradle width

    union() {
        // T-nut base
        _base_with_arm_holes();

        difference() {
            union() {
                // Short arm (IMX219 is compact, less reach needed)
                translate([-pcb_w/2, 0, 0])
                    cube([pcb_w, ARM_OUT * 0.7, ARM_T]);

                // Tilted PCB cradle
                translate([0, ARM_OUT * 0.7, BASE_FACE_H/2])
                    rotate([IMX_TILT_DEG, 0, 0])
                        translate([-pcb_w/2, 0, -pcb_h/2])
                            cube([pcb_w, IMX_CRADLE_T + 2, pcb_h]);
            }

            // 4× M2 clearance bores (24×24 mm pattern)
            for (hx = [-IMX_HOLE_SPC/2, IMX_HOLE_SPC/2])
            for (hz = [-IMX_HOLE_SPC/2, IMX_HOLE_SPC/2])
                translate([hx,
                           ARM_OUT * 0.7 + IMX_CRADLE_T + 2 + e,
                           BASE_FACE_H/2 + hz])
                    rotate([90 - IMX_TILT_DEG, 0, 0])
                        cylinder(d = IMX_BOLT_D,
                                 h = IMX_CRADLE_T + 4, center = true);

            // FFC cable exit slot (bottom of cradle)
            translate([0, ARM_OUT * 0.7 + (IMX_CRADLE_T + 2)/2,
                       BASE_FACE_H/2 - pcb_h/2 - e])
                rotate([IMX_TILT_DEG, 0, 0])
                    cube([12, IMX_CRADLE_T + 4, 8], center = true);

            // Lens window (open centre of cradle face)
            translate([0, ARM_OUT * 0.7 - e, BASE_FACE_H/2])
                rotate([IMX_TILT_DEG, 0, 0])
                    cube([20, IMX_CRADLE_T + 4 + 2*e, 18], center = true);
        }
    }
}

// ============================================================
// PART 5 — UWB ANCHOR BRACKET
// ============================================================
// Open cradle for MaUWB ESP32-S3 PCB (~50×25 mm).
// Antenna patch faces forward (+Y); PCB attaches with 4× M3 bolts.
// Bracket angled 0° (vertical face) for panoramic UWB coverage.
//
// Print: PETG, 4 perims, 40 % infill.
module uwb_bracket() {
    rim = 3.0;   // cradle rim thickness
    crd_w = UWB_PCB_W + 2*rim;
    crd_h = UWB_PCB_H + 2*rim;

    union() {
        // T-nut base
        _base_with_arm_holes();

        difference() {
            union() {
                // Arm
                translate([-crd_w/2, 0, 0])
                    cube([crd_w, ARM_OUT, ARM_T]);

                // Vertical cradle plate at arm end
                translate([-crd_w/2, ARM_OUT, 0])
                    cube([crd_w, UWB_CRADLE_T, crd_h]);
            }

            // 4× M3 clearance bores (UWB_HOLE_X × UWB_HOLE_Y pattern)
            for (hx = [-UWB_HOLE_X/2, UWB_HOLE_X/2])
            for (hz = [-UWB_HOLE_Y/2 + crd_h/2, UWB_HOLE_Y/2 + crd_h/2])
                translate([hx, ARM_OUT - e, hz])
                    rotate([-90, 0, 0])
                        cylinder(d = UWB_BOLT_D,
                                 h = UWB_CRADLE_T + 2*e);

            // M3 nut pockets (rear of cradle)
            for (hx = [-UWB_HOLE_X/2, UWB_HOLE_X/2])
            for (hz = [-UWB_HOLE_Y/2 + crd_h/2, UWB_HOLE_Y/2 + crd_h/2])
                translate([hx, ARM_OUT + UWB_CRADLE_T - 3, hz])
                    rotate([-90, 0, 0])
                        cylinder(d = 6.4 / cos(30), h = 3 + e, $fn = 6);

            // Open centre window (antenna / component clearance)
            translate([0, ARM_OUT - e,
                       crd_h/2])
                cube([UWB_PCB_W - 2*rim,
                      UWB_CRADLE_T + 2*e,
                      UWB_PCB_H - 2*rim], center = true);

            // USB / wire exit slot at bottom
            translate([0, ARM_OUT + UWB_CRADLE_T/2, rim/2])
                cube([12, UWB_CRADLE_T + 2*e, rim + 2*e], center = true);
        }
    }
}

// ============================================================
// PART 6 — CABLE CLIP
// ============================================================
// Tool-free push-in cable retainer that snaps into the 2020 rail
// T-groove.  Accepts a single cable bundle up to CLIP_CABLE_D mm.
// Snap-fit tongue grips the T-groove without fasteners.
// Print in PETG for flexibility (snap-fit requires some elasticity).
//
// Print: PETG, 3 perims, 20 % infill (flexibility matters).
// Orientation: flat face (channel face) down.
module cable_clip() {
    snap_t   = 1.4;    // snap tongue thickness (springy)
    snap_h   = SLOT_INNER_H - 0.3;
    snap_w   = SLOT_OPEN - 0.4;   // narrow enough to enter slot
    body_w   = 18.0;
    body_h   = 14.0;
    ch_d     = CLIP_CABLE_D;

    difference() {
        union() {
            // ── Body plate (sits on rail face) ──────────────────────────
            translate([-body_w/2, 0, 0])
                cube([body_w, CLIP_T, body_h]);

            // ── Snap tongue (inserts into T-groove) ─────────────────────
            // Centred, protrudes into rail slot
            translate([-snap_w/2, CLIP_T - e, (body_h - TNUT_L)/2])
                cube([snap_w, SLOT_NECK_H + e, TNUT_L]);

            // Barb: slightly wider than slot opening — snaps into T-groove
            translate([-TNUT_W/2 + 0.4, CLIP_T + SLOT_NECK_H - e,
                       (body_h - TNUT_L)/2])
                cube([TNUT_W - 0.8, snap_h + e, TNUT_L]);

            // ── Cable channel (C-clip shape) ─────────────────────────────
            translate([0, CLIP_T + SLOT_NECK_H + snap_h + 2, body_h/2])
                rotate([0, 90, 0])
                    difference() {
                        cylinder(d = ch_d + 2*CLIP_T, h = body_w,
                                 center = true);
                        cylinder(d = ch_d, h = body_w + 2*e,
                                 center = true);
                        // Open front for push-in insertion
                        translate([0, -(ch_d/2 + CLIP_T + e), 0])
                            cube([ch_d * 0.8,
                                  ch_d + 2*CLIP_T + 2*e,
                                  body_w + 2*e], center = true);
                    }
        }

        // ── Lightening slot in body plate ───────────────────────────────
        translate([0, -e, body_h/2])
            cube([body_w - 8, CLIP_T + 2*e, body_h - 8], center = true);
    }
}
