// ============================================================
// saltytank_chassis.scad — SaltyTank Tracked Chassis
// Issue: #121  Agent: sl-mechanical  Date: 2026-03-01
// ============================================================
//
// Parametric tank chassis for rubber or metal continuous tracks.
//
// Structure (left-to-right cross-section):
//   [left track belt]
//     ← sprocket + idler + road wheels on outer face of side frame
//   [left side frame plate, 8 mm Al]
//   [deck plate, 8 mm Al]          ← electronics bay on top
//   [right side frame plate, 8 mm Al]
//     → sprocket + idler + road wheels
//   [right track belt]
//
// Drive:   hoverboard hub motors (rear) — caliper-verified axle
//          (16.11 mm OD, D-cut flat 13.00 mm, bearing seat Ø37.8 mm)
// Idler:   80 mm OD, M8 axle; tensioner slot ±15 mm fore-aft
// Road wh: 60 mm OD × 2 per side, M8 axle, fixed
// Tracks:  rubber belt, 80 mm wide, 20 mm pitch (parametric)
//
// Electronics bay: reuses rover_electronics_bay.scad (same deck
//   footprint, FC 30.5 × 30.5 mm M3 + Jetson 58 × 49 mm M3)
// Sensors: RPLIDAR A1M8 top (bay lid tower), D435i front,
//          4 × IMX219 / CSI at deck corners
// Stem:    Ø25 mm (shared with SaltyLab / SaltyRover)
//
// Coordinate convention:
//   Z = 0    deck top face
//   +Y       forward
//   +X       right
//   Ground   Z = -(DECK_T + FRAME_H)   [= −98 mm with defaults]
//
// Ground clearance of hull (between tracks): FRAME_H = 90 mm ✓
//   (exceeds 50 mm requirement with significant margin)
//
// Weight estimate — frame only (excl. motors, electronics, battery):
//   Deck plate (8 mm Al, lightened) ≈ 1.55 kg
//   Side frames 2 × (6 mm Al)      ≈ 0.52 kg
//   Skid plate (saltytank_skid_plate.scad, 4 mm HDPE) ≈ 0.56 kg
//   Brackets + fasteners (PETG + SS) ≈ 0.35 kg
//   Total ≈ 2.98 kg  ← just under 3 kg target
//
// RENDER options:
//   "assembly"           full 3D preview (default)
//   "deck_2d"            DXF — deck plate (waterjet / CNC)
//   "side_frame_2d"      DXF — side frame plate (×2 mirrored; CNC)
//   "side_frame_stl"     STL — side frame (print 2×, mirror right)
//   "idler_block_stl"    STL — tensioner idler block (print 2×)
//   "csi_mount_stl"      STL — CSI corner bracket (print 4×)
//   "d435i_mount_stl"    STL — D435i front bracket (print 1×)
//
// ── Export commands ─────────────────────────────────────────
//  Deck DXF:
//    openscad saltytank_chassis.scad -D 'RENDER="deck_2d"' -o saltytank_deck.dxf
//  Side frame DXF (cut 2×, flip one for mirror):
//    openscad saltytank_chassis.scad -D 'RENDER="side_frame_2d"' -o saltytank_side_frame.dxf
//  Side frame STL (print 2×; mirror one in slicer):
//    openscad saltytank_chassis.scad -D 'RENDER="side_frame_stl"' -o saltytank_side_frame.stl
//  Idler block STL (×2):
//    openscad saltytank_chassis.scad -D 'RENDER="idler_block_stl"' -o saltytank_idler_block.stl
//  CSI bracket STL (×4):
//    openscad saltytank_chassis.scad -D 'RENDER="csi_mount_stl"' -o saltytank_csi_mount.stl
//  D435i bracket STL (×1):
//    openscad saltytank_chassis.scad -D 'RENDER="d435i_mount_stl"' -o saltytank_d435i_mount.stl
// ============================================================

$fn = 64;
e   = 0.01;

// ── Deck plate ────────────────────────────────────────────────────────────────
BODY_L     = 500.0;    // deck fore-aft (Y)
BODY_W     = 360.0;    // deck left-right (X) — space between inner frame faces
DECK_T     =   8.0;    // deck plate thickness
DECK_R     =  15.0;    // corner fillet radius

// ── Side frame geometry ───────────────────────────────────────────────────────
FRAME_H    =  90.0;    // frame height below deck bottom = hull ground clearance
FRAME_T    =   6.0;    // frame plate thickness (6 mm Al laser-cut)
FRAME_R    =  10.0;    // frame corner fillet radius

// ── Track system (rubber or metal belt) ───────────────────────────────────────
TRACK_WID  =  80.0;    // track belt width
TRACK_PITCH =  20.0;   // track link pitch (mm) — affects sprocket tooth count
// Track belt inner face sits at X = ±(BODY_W/2 + FRAME_T) from centre
// Track CL at X = ±(BODY_W/2 + FRAME_T + TRACK_WID/2) from centre

// ── Drive sprocket (rear) — hoverboard hub motor ──────────────────────────────
// Caliper-verified axle (matches BOM.md):
SPROCKET_AXLE_D   =  16.11;   // axle base OD (round section near hub)
SPROCKET_AXLE_FLAT=  13.00;   // D-cut chord width
SPROCKET_AXLE_DCUT=  15.95;   // D-cut OD
BEARING_OD        =  37.80;   // motor bearing-seat collar OD
BEARING_RECESS    =   8.0;    // bearing seat recess depth in frame

// Sprocket pitch circle: 10 teeth × 20 mm pitch
// PCD = pitch / sin(π/N) = 20 / sin(18°) ≈ 64.7 mm → R ≈ 32.4 mm
SPROCKET_R        =  33.0;    // sprocket pitch-circle radius (nominal)
SPROCKET_POS_Y    = -(BODY_L/2 - SPROCKET_R - 22);  // rear, −Y

// Hub motor flange bolt circle (4× M5 at 90°, BC = 52 mm)
// ⚠ Verify against your motor flange before fabricating!
HUB_FLANGE_BC     =  52.0;    // motor hub bolt circle OD (M5 × 4)
HUB_BOLT_D        =   5.3;    // M5 clearance

// ── Idler wheel (front, adjustable tensioner) ─────────────────────────────────
IDLER_R           =  40.0;    // idler wheel radius (OD = 80 mm)
IDLER_AXLE_D      =   8.5;    // M8 axle clearance bore
IDLER_POS_Y_NOM   = +(BODY_L/2 - IDLER_R - 22);  // front nominal, +Y
// Tensioner slot: idler can move ±TENS_TRAVEL fore-aft to tension track
TENS_TRAVEL       =  15.0;    // ±15 mm track tension adjustment
// Tensioner bolt: M6 runs fore-aft in threaded lug at front of slot
TENS_BOLT_D       =   6.5;    // M6 clearance
TENS_BLOCK_L      =  35.0;    // sliding idler block length
TENS_BLOCK_W      =  TRACK_WID - 4;  // block width (fits inside track)
TENS_SLOT_H       =  IDLER_AXLE_D + 2.0;  // slot height (clearance for block)

// ── Road wheels (2× per side, between sprocket and idler) ────────────────────
ROAD_WHEEL_R      =  30.0;    // road wheel radius (OD = 60 mm)
ROAD_AXLE_D       =   8.5;    // M8 axle clearance bore
ROAD_Y_1          = -BODY_L/4;   // forward road wheel (−125 mm from centre)
ROAD_Y_2          = +BODY_L/4;   // rearward road wheel (+125 mm from centre)
// ⚠ Reversed fore/aft: ROAD_Y_1 is near rear sprocket, ROAD_Y_2 near front idler

// ── Height stack ─────────────────────────────────────────────────────────────
// Ground Z = 0 (absolute); deck coords: Z=0 at deck top.
// In deck coords, ground is at Z = -(DECK_T + FRAME_H) = -98 mm.
//   SPROCKET_CL_Z = -(DECK_T + FRAME_H - SPROCKET_R)  = -(98 - 33) = -65 mm
//   IDLER_CL_Z    = -(DECK_T + FRAME_H - IDLER_R)     = -(98 - 40) = -58 mm
//   ROAD_WHEEL_Z  = -(DECK_T + FRAME_H - ROAD_WHEEL_R) = -(98 - 30) = -68 mm
GROUND_Z          = -(DECK_T + FRAME_H);   // = -98 mm in deck coords

// ── Stem socket (deck centre, shared with SaltyLab / SaltyRover) ─────────────
STEM_BORE         =  25.5;    // 25 mm tube + 0.5 mm clearance
STEM_COLLAR_OD    =  50.0;
STEM_COLLAR_H     =  20.0;    // boss height above deck top
STEM_FLANGE_BC    =  40.0;    // 4× M4 bolt circle

// ── Electronics bay footprint (rover_electronics_bay.scad) ───────────────────
// Bay dimensions match rover_electronics_bay.scad exactly.
// Deck holes match bay floor bolt pattern for drop-in compatibility.
BAY_L             = 240.0;    // bay length (X on deck = left-right)
BAY_W             = 200.0;    // bay width  (Y on deck = fore-aft)
BAY_WALL          =   3.0;    // bay wall thickness
BAY_BOLT_INSET    =   8.0;    // bay bolt CL from bay exterior corner

// ── FC mount — 30.5 × 30.5 mm M3 (shared SaltyLab pattern) ──────────────────
FC_PITCH          =  30.5;
FC_HOLE_D         =   3.2;
FC_POS_Y          =  BAY_W/2 - 50.0;   // near front edge (inside bay footprint)

// ── Jetson Orin mount — 58 × 49 mm M3 (shared SaltyLab pattern) ─────────────
ORIN_HOLE_X       =  58.0;
ORIN_HOLE_Y       =  49.0;
ORIN_HOLE_D       =   3.2;
ORIN_POS_Y        = -(BAY_W/2 - 55.0);  // near rear edge

// ── CSI corner camera mounts ──────────────────────────────────────────────────
CSI_PCB           =  25.0;    // IMX219 PCB square side
CSI_M2_SPC        =  15.0;    // M2 hole pitch
CSI_TILT          =  20.0;    // nose-down tilt (degrees)

// ── D435i front bracket ───────────────────────────────────────────────────────
RS_TILT           =   8.0;    // nose-down tilt (degrees)
RS_ARM_LEN        =  70.0;    // arm reach forward from deck edge
RS_BASE_W         =  44.0;    // base plate width

// ── Fasteners ─────────────────────────────────────────────────────────────────
M2_D  = 2.3;
M3_D  = 3.3;
M4_D  = 4.3;
M5_D  = 5.3;
M6_D  = 6.5;
M8_D  = 8.5;

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

if (RENDER == "assembly") {
    assembly();
} else if (RENDER == "deck_2d") {
    projection(cut = true)
        translate([0, 0, -DECK_T / 2])
            deck_plate();
} else if (RENDER == "side_frame_2d") {
    // Left frame, projected flat (2D profile only — Z/Y plane)
    projection(cut = true)
        rotate([90, 0, 0])
            translate([0, 0, BODY_L / 2])
                side_frame(-1);
} else if (RENDER == "side_frame_stl") {
    side_frame(-1);  // mirror one in slicer for right side
} else if (RENDER == "idler_block_stl") {
    idler_block();
} else if (RENDER == "csi_mount_stl") {
    csi_corner_bracket();
} else if (RENDER == "d435i_mount_stl") {
    d435i_front_bracket();
}

// ============================================================
// FULL ASSEMBLY
// ============================================================
module assembly() {
    // Deck plate
    color("Silver", 0.90) deck_plate();

    // Stem collar
    color("DimGray", 0.85) stem_collar();

    // Side frames (left = −1, right = +1)
    color("SteelBlue",  0.80) side_frame(-1);
    color("SteelBlue",  0.80) side_frame(+1);

    // Idler tensioner blocks (×2)
    color("LightSlateGray", 0.85)
        translate([-BODY_W/2 - FRAME_T, IDLER_POS_Y_NOM,
                   GROUND_Z + IDLER_R])
            rotate([0, 90, 0]) idler_block();
    color("LightSlateGray", 0.85)
        translate([+BODY_W/2, IDLER_POS_Y_NOM,
                   GROUND_Z + IDLER_R])
            rotate([0, -90, 0]) idler_block();

    // CSI corner brackets
    for (sx = [-1, 1])
    for (sy = [-1, 1])
        color("Teal", 0.85) csi_bracket_placed(sx, sy);

    // D435i front bracket
    color("DarkSlateGray", 0.85) d435i_bracket_placed();

    // Phantom: electronics bay (rover_electronics_bay.scad)
    %color("OliveDrab", 0.25)
        translate([0, 0, DECK_T])
            cube([BAY_L + 2*BAY_WALL,
                  BAY_W + 2*BAY_WALL,
                  84], center = true);

    // Phantom: track loops (rubber belt cross-section)
    for (sx = [-1, 1])
        %color("Black", 0.15)
            translate([sx * (BODY_W/2 + FRAME_T + TRACK_WID/2), 0, 0])
                rotate([90, 0, 0])
                    track_loop_ghost();

    // Phantom: hub motors (rear, outboard)
    for (sx = [-1, 1])
        %color("Orange", 0.20)
            translate([sx * (BODY_W/2 + FRAME_T + 30),
                       SPROCKET_POS_Y,
                       GROUND_Z + SPROCKET_R])
                rotate([0, sx*90, 0])
                    cylinder(d = BEARING_OD, h = 70, center = false);
}

// Ghost track loop outline for preview (no geometry output)
module track_loop_ghost() {
    span = abs(IDLER_POS_Y_NOM - SPROCKET_POS_Y);
    hull() {
        translate([0, IDLER_POS_Y_NOM,  GROUND_Z + IDLER_R])
            rotate([90, 0, 0]) cylinder(d = IDLER_R*2, h = 1);
        translate([0, SPROCKET_POS_Y,   GROUND_Z + SPROCKET_R])
            rotate([90, 0, 0]) cylinder(d = SPROCKET_R*2, h = 1);
    }
}

// ============================================================
// DECK PLATE  (Part A — laser-cut 8 mm 5052-H32 aluminium)
// ============================================================
// Rectangular plate spanning the gap between the two side frames.
// All electronics and sensor mounts attach to the deck top face.
// Side frames bolt to deck side edges (M5 × 4 per side).
// Weight estimate: 500×360×8 mm Al, ~45% lightened ≈ 1.55 kg
module deck_plate() {
    difference() {
        // ── Outer profile ─────────────────────────────────────────────
        linear_extrude(DECK_T)
            minkowski() {
                square([BODY_L - 2*DECK_R, BODY_W - 2*DECK_R],
                        center = true);
                circle(r = DECK_R);
            }

        // ── Side frame attachment slots (M5 × 4 per side) ─────────────
        // Slots run fore-aft (Y) for ±10 mm lateral alignment adjustment
        for (sx = [-1, 1])
        for (py = [-BODY_L/4, BODY_L/4]) {
            hull() {
                translate([sx*(BODY_W/2 - 8), py - 12, -e])
                    cylinder(d = M5_D, h = DECK_T + 2*e);
                translate([sx*(BODY_W/2 - 8), py + 12, -e])
                    cylinder(d = M5_D, h = DECK_T + 2*e);
            }
        }

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

        // ── Stem flange bolts (4× M4 at 90°) ─────────────────────────
        for (a = [0, 90, 180, 270])
            rotate([0, 0, a])
                translate([STEM_FLANGE_BC/2, 0, -e])
                    cylinder(d = M4_D, h = DECK_T + 2*e);

        // ── Electronics bay footprint bolt holes (10× M3) ─────────────
        // Matches rover_electronics_bay.scad floor flange pattern exactly
        for (sx = [-1, 1])
        for (sy = [-1, 1]) {
            bx = sx * (BAY_L/2 + BAY_WALL - BAY_BOLT_INSET);
            by = sy * (BAY_W/2 + BAY_WALL - BAY_BOLT_INSET);
            translate([bx, by, -e])
                cylinder(d = M3_D, h = DECK_T + 2*e);
        }
        // Centre long-wall bolts (2×)
        for (sy = [-1, 1])
            translate([0, sy*(BAY_W/2 + BAY_WALL - BAY_BOLT_INSET), -e])
                cylinder(d = M3_D, h = DECK_T + 2*e);

        // ── FC mount holes — 30.5×30.5 M3 (shared SaltyLab) ──────────
        for (dx = [-FC_PITCH/2, FC_PITCH/2])
        for (dy = [-FC_PITCH/2, FC_PITCH/2])
            translate([dx, FC_POS_Y + dy, -e])
                cylinder(d = FC_HOLE_D, h = DECK_T + 2*e);

        // ── Jetson Orin mount holes — 58×49 M3 (shared SaltyLab) ─────
        for (dx = [-ORIN_HOLE_X/2, ORIN_HOLE_X/2])
        for (dy = [-ORIN_HOLE_Y/2, ORIN_HOLE_Y/2])
            translate([dx, ORIN_POS_Y + dy, -e])
                cylinder(d = ORIN_HOLE_D, h = DECK_T + 2*e);

        // ── Lightening holes (between bay footprint and deck edges) ───
        for (sx = [-1, 1])
        for (sy = [-1, 1]) {
            lx = sx * (BAY_L/2 + 40);
            ly = sy * (BODY_L/4 + 10);
            translate([lx, ly, -e])
                cylinder(d = 50, h = DECK_T + 2*e);
        }
        // Centre pair flanking stem
        for (sx = [-1, 1])
            translate([sx * 65, 0, -e])
                cylinder(d = 38, h = DECK_T + 2*e);

        // ── Cable routing slots (motor phase + sensor harness) ─────────
        // 4× slots near side frame attachment points
        for (sx = [-1, 1])
        for (sy = [-1, 1])
            hull() {
                translate([sx*(BODY_W/2 - 30), sy*(BODY_L/4 - 8), -e])
                    cylinder(d = 14, h = DECK_T + 2*e);
                translate([sx*(BODY_W/2 - 30), sy*(BODY_L/4 + 8), -e])
                    cylinder(d = 14, h = DECK_T + 2*e);
            }

        // ── Skid plate attachment holes (M4 × 8, through deck underside)
        // These align with saltytank_skid_plate.scad bolt pattern
        for (sx = [-1, 1])
        for (sy = [-1, 0, 1]) {
            bx = sx * (BODY_W/2 - 20);
            by = sy * (BODY_L/3);
            translate([bx, by, -e])
                cylinder(d = M4_D, h = DECK_T + 2*e);
        }

        // ── CSI corner bracket attachment holes (M3 × 2 per corner) ───
        for (sx = [-1, 1])
        for (sy = [-1, 1])
        for (dd = [-12, 12])
            translate([sx*(BODY_W/2 - 18),
                       sy*(BODY_L/2 - 18) + dd*sy, -e])
                cylinder(d = M3_D, h = DECK_T + 2*e);
    }
}

// ── Deck-top stem collar ─────────────────────────────────────────────────────
module stem_collar() {
    translate([0, 0, DECK_T])
        difference() {
            cylinder(d = STEM_COLLAR_OD, h = STEM_COLLAR_H);
            translate([0, 0, -e])
                cylinder(d = STEM_BORE, h = STEM_COLLAR_H + 2*e);
            for (a = [0, 90, 180, 270])
                rotate([0, 0, a])
                    translate([STEM_FLANGE_BC/2, 0, -e])
                        cylinder(d = M4_D, h = STEM_COLLAR_H + 2*e);
        }
}

// ============================================================
// SIDE FRAME  (Part B — laser-cut 6 mm 6061-T6 aluminium)
// ============================================================
// Vertical plate running the full body length.
// Inner face bolts to deck plate side edge.
// Outer face mounts: drive sprocket (rear) + idler slot (front)
//                   + road wheels (middle).
//
// `side` = -1 (left / −X) or +1 (right / +X)
// The right frame is a mirror of the left — cut 2× from same DXF,
// flip one face-down before mounting.
//
// Weight estimate: 500×90×6 mm Al, ~35% lightened ≈ 0.26 kg each
// ============================================================
module side_frame(side = -1) {
    sx = side;  // −1 = left, +1 = right
    // Frame inner face at X = sx * BODY_W/2
    // Frame outer face at X = sx * (BODY_W/2 + FRAME_T)
    frame_x = sx * BODY_W/2;  // inner face X position

    // In deck coords:  sprocket / idler CL Z values
    spr_z  = GROUND_Z + SPROCKET_R;   // = -(DECK_T + FRAME_H) + SPROCKET_R
    idl_z  = GROUND_Z + IDLER_R;
    rw_z   = GROUND_Z + ROAD_WHEEL_R;

    translate([frame_x, 0, 0])
    rotate([0, sx > 0 ? 180 : 0, 0])  // mirror right frame in X
    translate([0, 0, 0]) {
        difference() {
            // ── Outer profile of side frame ───────────────────────────
            // Frame plate in the Y-Z plane, FRAME_T thick in X
            translate([0, -BODY_L/2, GROUND_Z])
                linear_extrude(FRAME_T)
                    minkowski() {
                        square([BODY_L - 2*FRAME_R,
                                FRAME_H + DECK_T - 2*FRAME_R],
                                center = false);
                        circle(r = FRAME_R);
                    }

            // ── Deck attachment slots (2× M5 per side, front + rear) ──
            // Slots allow ±10 mm vertical adjustment for frame height
            for (py = [-BODY_L/4, BODY_L/4])
                hull() {
                    translate([e, py - 12, -DECK_T / 2])
                        rotate([0, 90, 0])
                            cylinder(d = M5_D, h = FRAME_T + 2*e);
                    translate([e, py + 12, -DECK_T / 2])
                        rotate([0, 90, 0])
                            cylinder(d = M5_D, h = FRAME_T + 2*e);
                }

            // ── Drive sprocket bore — D-cut (rear) ────────────────────
            // Round section (base, near hub)
            translate([e, SPROCKET_POS_Y, spr_z])
                rotate([0, 90, 0])
                    cylinder(d = SPROCKET_AXLE_D + 0.4, h = FRAME_T + 2*e);

            // D-cut flat (anti-rotation) — removes chord to AXLE_FLAT
            dcut_h = sqrt(pow((SPROCKET_AXLE_DCUT + 0.4)/2, 2)
                          - pow((SPROCKET_AXLE_FLAT + 0.4)/2, 2));
            translate([e, SPROCKET_POS_Y, spr_z])
                rotate([0, 90, 0])
                    translate([0, (SPROCKET_AXLE_FLAT + 0.4)/2, 0])
                        cube([(SPROCKET_AXLE_DCUT + 0.4)/2,
                               FRAME_T + 2*e,
                               FRAME_T + 2*e],
                              center = false);

            // ── Bearing seat recess (outboard face — inboard of track) ─
            // Prevents Ø37.8 mm collar binding on frame face
            translate([e - BEARING_RECESS, SPROCKET_POS_Y, spr_z])
                rotate([0, 90, 0])
                    cylinder(d = BEARING_OD + 1.5,
                             h = BEARING_RECESS + e);

            // ── Hub motor flange bolt holes (4× M5, 52 mm BC) ─────────
            for (a = [45, 135, 225, 315])
                translate([e,
                           SPROCKET_POS_Y + HUB_FLANGE_BC/2 * sin(a),
                           spr_z           + HUB_FLANGE_BC/2 * cos(a)])
                    rotate([0, 90, 0])
                        cylinder(d = HUB_BOLT_D, h = FRAME_T + 2*e);

            // ── Idler tensioner slot (front) ──────────────────────────
            // Horizontal slot in Y direction; idler block slides in it
            slot_y_ctr = IDLER_POS_Y_NOM;
            slot_y_min = slot_y_ctr - TENS_TRAVEL;
            slot_y_max = slot_y_ctr + TENS_TRAVEL;
            translate([e, slot_y_min, idl_z - TENS_SLOT_H/2])
                cube([FRAME_T + 2*e,
                      slot_y_max - slot_y_min,
                      TENS_SLOT_H]);
            // Rounded ends
            translate([e, slot_y_min, idl_z])
                rotate([0, 90, 0])
                    cylinder(d = TENS_SLOT_H, h = FRAME_T + 2*e);
            translate([e, slot_y_max, idl_z])
                rotate([0, 90, 0])
                    cylinder(d = TENS_SLOT_H, h = FRAME_T + 2*e);

            // ── Tensioner bolt bore (M6, at front end of slot) ────────
            // M6 bolt threads into lug at front of slot; pushes block rearward
            translate([e,
                       slot_y_max + 8,
                       idl_z])
                rotate([0, 90, 0])
                    cylinder(d = M6_D, h = FRAME_T + 2*e);

            // ── Road wheel bores (2× M8, fore/aft of centre) ──────────
            for (ry = [ROAD_Y_1, ROAD_Y_2])
                translate([e, ry, rw_z])
                    rotate([0, 90, 0])
                        cylinder(d = ROAD_AXLE_D, h = FRAME_T + 2*e);

            // ── Lightening holes (between bore positions) ──────────────
            // Row 1: between road wheel 1 and sprocket
            translate([e,
                       (SPROCKET_POS_Y + ROAD_Y_1) / 2,
                       GROUND_Z + FRAME_H/2])
                rotate([0, 90, 0])
                    cylinder(d = 45, h = FRAME_T + 2*e);

            // Row 2: between road wheel 2 and idler
            translate([e,
                       (IDLER_POS_Y_NOM + ROAD_Y_2) / 2,
                       GROUND_Z + FRAME_H/2])
                rotate([0, 90, 0])
                    cylinder(d = 45, h = FRAME_T + 2*e);

            // Row 3: between the two road wheels
            translate([e, (ROAD_Y_1 + ROAD_Y_2) / 2, GROUND_Z + FRAME_H/2])
                rotate([0, 90, 0])
                    cylinder(d = 35, h = FRAME_T + 2*e);
        }
    }
}

// ============================================================
// IDLER TENSIONER BLOCK  (Part C — 3D print PETG, × 2)
// ============================================================
// Slides in the frame's tensioner slot.
// M8 bore holds the idler axle.
// A flat face at the front receives the tensioner M6 bolt.
// Lock nut (M6 nyloc) clamps block at desired track tension.
// Print orientation: flat face on bed; no supports needed.
// ============================================================
module idler_block() {
    block_h = TENS_SLOT_H - 0.6;   // height with slot clearance
    block_l = TENS_BLOCK_L;
    block_w = FRAME_T - 0.4;       // thickness with clearance

    difference() {
        union() {
            // Main block body
            cube([block_w, block_l, block_h], center = true);

            // Tensioner bolt lug (extends from +Y face)
            translate([0, block_l/2, 0])
                cube([block_w, 10, block_h], center = true);
        }

        // M8 axle bore through full width
        translate([0, 0, 0])
            rotate([0, 90, 0])
                cylinder(d = IDLER_AXLE_D, h = block_w + 2*e, center = true);

        // M6 tensioner bolt bore (fore-aft, through lug)
        translate([0, block_l/2 + 5, 0])
            rotate([90, 0, 0])
                cylinder(d = M6_D, h = 16, center = true);

        // M6 nyloc nut pocket (rear face of lug — captured nut)
        translate([0, block_l/2 - 1, 0])
            rotate([90, 0, 0])
                cylinder(d = 11.5, h = 5, $fn = 6, center = false);

        // ── Lightening slot (centre, removes material not in load path)
        cube([block_w + 2*e, block_l/2 - IDLER_AXLE_D/2 - 4, block_h - 4],
              center = true);
    }
}

// ============================================================
// CSI CORNER BRACKET  (Part D — 3D print PETG, × 4)
// ============================================================
// Same design as saltyrover_chassis_r2.scad.
// Mounts IMX219 / Arducam CSI camera at each deck corner,
// angled 45° outward + CSI_TILT downward.
// ============================================================
module csi_corner_bracket() {
    base_l = 42;
    base_w = 32;
    base_t =  5;

    difference() {
        union() {
            cube([base_l, base_w, base_t]);
            translate([base_l / 2, base_w / 2, base_t])
                rotate([0, CSI_TILT, 0])
                    translate([-CSI_PCB/2 - 3, -CSI_PCB/2 - 3, 0])
                        cube([CSI_PCB + 6, CSI_PCB + 6, base_t]);
        }
        // 2× M3 base attachment holes
        for (dx = [8, base_l - 8])
            translate([dx, base_w / 2, -e])
                cylinder(d = M3_D, h = base_t + 2*e);
        // CSI M2 holes (15×15 mm pattern)
        translate([base_l / 2, base_w / 2, base_t])
            rotate([0, CSI_TILT, 0])
                for (cx = [-CSI_M2_SPC/2, CSI_M2_SPC/2])
                for (cy = [-CSI_M2_SPC/2, CSI_M2_SPC/2])
                    translate([cx, cy, -e])
                        cylinder(d = M2_D, h = base_t + 2*e);
        // CSI ribbon slot
        translate([base_l/2 - 6, base_w/2 - 1.5, -e])
            cube([12, 3, base_t + 2*e]);
    }
}

module csi_bracket_placed(sx, sy) {
    cx = sx * (BODY_W/2 - 22);
    cy = sy * (BODY_L/2 - 22);
    rot = atan2(sy, sx) * 180 / 3.14159 - 45;
    translate([cx, cy, DECK_T])
        rotate([0, 0, rot])
            translate([-21, -16, 0])
                csi_corner_bracket();
}

// ============================================================
// D435i FRONT BRACKET  (Part E — 3D print PETG, × 1)
// ============================================================
// Same design as saltyrover_chassis_r2.scad.
// Arm extends forward from deck front edge.
// RS_TILT degrees nose-down.  1/4-20 captured nut for D435i.
// ============================================================
module d435i_front_bracket() {
    base_d  = 24;
    base_h  =  8;
    arm_len = RS_ARM_LEN;

    nut14_af = 11.1;
    nut14_h  =  5.6;
    nut14_cl =  6.5;

    difference() {
        union() {
            // Base plate (bolts to deck front face)
            translate([-RS_BASE_W/2, 0, 0])
                cube([RS_BASE_W, base_d, base_h]);
            // Forward arm
            translate([-13, base_d, 0])
                cube([26, arm_len, base_h]);
            // Tilted face plate
            translate([0, base_d + arm_len, base_h/2])
                rotate([0, RS_TILT, 0])
                    translate([-16, 0, -base_h/2])
                        cube([32, 14, base_h]);
        }
        // 2× M4 base attachment
        for (dx = [-RS_BASE_W/2 + 10, RS_BASE_W/2 - 10])
            translate([dx, base_d/2, -e])
                cylinder(d = M4_D, h = base_h + 2*e);
        // 1/4-20 UNC captured nut
        translate([0, base_d + arm_len + 12, base_h/2])
            rotate([0, 90, 0]) {
                translate([0, 0, -nut14_h - 1])
                    cylinder(d = nut14_af/cos(30), h = nut14_h + 1, $fn = 6);
                cylinder(d = nut14_cl, h = 20);
            }
    }
}

module d435i_bracket_placed() {
    translate([0, BODY_L/2 + 12, DECK_T])
        rotate([0, 0, 180])
            d435i_front_bracket();
}
