// ============================================================
// saltyrover_chassis_r2.scad — SaltyRover 4-Wheel Chassis Rev 2
// Issue: #109  Agent: sl-mechanical  Date: 2026-03-01
// ============================================================
//
// Complete parametric chassis assembly for the SaltyRover 4-wheel
// rough-terrain variant.  Designed to be printed (PETG), laser-cut
// (6 mm 5052-H32 Al), or CNC-routed.
//
// NEW vs Rev 1 (issue #73 / saltyrover_chassis.scad):
//   • 4× trailing-arm spring-suspension corners
//   • Enclosed electronics bay (rover_electronics_bay.scad)
//   • M3-slot-adjustable pivot brackets (replaces fixed M5 flanges)
//   • CSI camera corner brackets (4×, 45° outward tilt)
//   • RPLIDAR tower stub on electronics bay lid
//   • D435i front bracket arm
//   • Weight target: <2 kg frame (excl. motors/electronics)
//
// Shared SaltyLab patterns (swappable electronics):
//   FC  : 30.5 × 30.5 mm M3  (ESP32-S3 BALANCE / Pixhawk)
//   Jetson: 58 × 49 mm M3    (Orin NX / Nano carrier board)
//   Stem  : Ø25 mm bore      (sensor head unchanged)
//
// Coordinate convention (all modules):
//   Z = 0   deck top face
//   +Y      forward
//   +X      right
//   Ground at Z ≈ −(GND_CLR + BATT_PACK_H + BATT_FLOOR_T + DECK_T)
//
// RENDER options:
//   "assembly"        full 3D preview (default)
//   "deck_2d"         DXF — deck plate for waterjet / CNC
//   "pivot_bracket_2d" DXF — pivot bracket for CNC / laser (×4)
//   "pivot_bracket_stl" STL — pivot bracket (print 4×)
//   "csi_mount_stl"   STL — CSI corner bracket (print 4×)
//   "d435i_mount_stl" STL — D435i front bracket (print 1×)
//
// ── Export commands ─────────────────────────────────────────
//  Deck DXF:
//    openscad saltyrover_chassis_r2.scad -D 'RENDER="deck_2d"' -o saltyrover_r2_deck.dxf
//  Pivot bracket DXF (×4):
//    openscad saltyrover_chassis_r2.scad -D 'RENDER="pivot_bracket_2d"' -o rover_pivot_bracket.dxf
//  Pivot bracket STL (×4):
//    openscad saltyrover_chassis_r2.scad -D 'RENDER="pivot_bracket_stl"' -o rover_pivot_bracket.stl
//  CSI mount STL (×4):
//    openscad saltyrover_chassis_r2.scad -D 'RENDER="csi_mount_stl"' -o rover_csi_mount.stl
//  D435i mount STL (×1):
//    openscad saltyrover_chassis_r2.scad -D 'RENDER="d435i_mount_stl"' -o rover_d435i_mount.stl
// ============================================================

$fn = 64;
e   = 0.01;

// ── Deck footprint ────────────────────────────────────────────────────────────
ROVER_L    = 500.0;   // deck fore-aft (Y)
ROVER_W    = 480.0;   // deck left-right (X)
DECK_T     =   6.0;   // deck plate thickness (6 mm Al — weight-optimised)
DECK_R     =  15.0;   // corner fillet radius

// ── Drive geometry ────────────────────────────────────────────────────────────
// Hoverboard hub motors — caliper-verified (matches BOM.md / rover_motor_mount.scad)
TRACK_W    = 540.0;   // motor axle CL to CL, left-right (X)
AXLE_BASE  = 340.0;   // motor axle CL to CL, fore-aft   (Y)
AXLE_H     = 127.0;   // axle CL above ground (10×2.125" tire, r=127mm)
AXLE_D     =  16.11;  // axle base section OD (caliper)
AXLE_FLAT  =  13.00;  // D-cut chord width (caliper)
BEARING_OD =  37.80;  // bearing seat collar OD (caliper)

// ── Height stack ─────────────────────────────────────────────────────────────
GND_CLR      =  55.0;  // min ground clearance (at static suspension sag)
BATT_FLOOR_T =   3.0;  // battery tray floor thickness
BATT_PACK_H  =  56.0;  // battery pack height (420×88×56mm, laid flat)
DECK_BOT_H   = GND_CLR + BATT_FLOOR_T + BATT_PACK_H;  // 114 mm
DECK_TOP_H   = DECK_BOT_H + DECK_T;                    // 120 mm
// Axle above deck top (in chassis SCAD coords = positive Z):
//   AXLE_H - DECK_TOP_H = 127 - 120 = +7 mm  ← axle is 7 mm above deck top

// ── Battery packs (under-deck, laid flat) ────────────────────────────────────
BATT_X_DIM  = 420.0;  // pack long side (left-right)
BATT_Y_DIM  =  88.0;  // pack fore-aft per pack
BATT_N      =    2;   // number of packs fore-aft (2 = 176 mm; 4 = 352 mm)
TRAY_MARGIN =   5.0;  // opening margin each side

// ── Stem socket (deck centre) ─────────────────────────────────────────────────
STEM_BORE      = 25.5;   // 25 mm tube + 0.5 mm FDM clearance
STEM_COLLAR_OD = 50.0;
STEM_COLLAR_H  = 20.0;   // raised boss height above deck top
STEM_FLANGE_BC = 40.0;   // 4× M4 bolt circle for stem adapter

// ── FC mount — ESP32-S3 BALANCE / Pixhawk (30.5 × 30.5 mm M3) ────────────────────
// Shared with SaltyLab — swappable electronics
FC_PITCH    = 30.5;
FC_HOLE_D   =  3.2;
FC_POS_Y    = ROVER_L/2 - 65.0;   // near front edge

// ── Jetson Orin NX / Nano mount (58 × 49 mm M3) ──────────────────────────────
// Shared with SaltyLab — swappable electronics
ORIN_HOLE_X  = 58.0;
ORIN_HOLE_Y  = 49.0;
ORIN_HOLE_D  =  3.2;
ORIN_POS_Y   = -(ROVER_L/2 - 60.0);  // near rear edge

// ── Pivot bracket (motor corner mount, adjustable) ───────────────────────────
// Each corner: one pivot bracket bolted to deck top at motor CL fore-aft.
// M3 slotted holes allow ±15 mm fore-aft and ±10 mm lateral adjustment.
PBK_L        =  80.0;   // bracket plate length (fore-aft / Y)
PBK_W        =  55.0;   // bracket plate width  (lateral / X from deck edge)
PBK_T        =   8.0;   // bracket plate thickness
PBK_FLANGE_H =  20.0;   // vertical flange height below deck bottom face

// M3 adjustment slots (4× per bracket on the deck-top flange)
ADJ_SLOT_L   =  25.0;   // slot length (allows ±12 mm adjustment)
ADJ_M3_D     =   3.3;   // M3 clearance
ADJ_INSET_X  =  12.0;   // slot CL from lateral edge of bracket
ADJ_INSET_Y  =  16.0;   // slot CL from fore/aft ends

// Pivot pin (M8 through-bolt; arm swings around this)
PIV_D        =   8.5;   // M8 clearance bore
PIV_POS_X   = ROVER_W/2 + 5.0;    // pivot CL from deck centre (just at edge)
// Pivot fore-aft at each motor corner (±AXLE_BASE/2)

// Spring guide boss on bracket underside
SPG_GUIDE_OD =  14.0;   // spring guide boss OD (spring slides over this)
SPG_GUIDE_H  =  15.0;   // guide boss height below bracket bottom

// ── CSI camera corner brackets ────────────────────────────────────────────────
CSI_PCB      =  25.0;   // IMX219 / CSI module PCB width (square)
CSI_M2_SPC   =  15.0;   // M2 hole pitch (±7.5 mm from centre)
CSI_TILT     =  20.0;   // downward tilt (degrees) for terrain view
CSI_ANGLE    =  45.0;   // outward rotation at each corner

// ── D435i front bracket ───────────────────────────────────────────────────────
RS_TILT      =   8.0;   // nose-down tilt (degrees)
RS_ARM_LEN   =  65.0;   // arm length from deck front edge to camera CL
RS_BASE_W    =  40.0;   // base width (left-right)

// ── Fasteners ─────────────────────────────────────────────────────────────────
M2_D  = 2.3;
M3_D  = 3.3;
M4_D  = 4.3;
M5_D  = 5.3;
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 == "pivot_bracket_2d") {
    projection(cut = true)
        translate([0, 0, -PBK_T / 2])
            pivot_bracket_flat();
} else if (RENDER == "pivot_bracket_stl") {
    pivot_bracket();
} else if (RENDER == "csi_mount_stl") {
    csi_corner_bracket();
} else if (RENDER == "d435i_mount_stl") {
    d435i_front_bracket();
}

// ============================================================
// FULL ASSEMBLY
// ============================================================
module assembly() {
    color("Silver",      0.90) deck_plate();
    color("DimGray",     0.85) stem_collar();

    // 4× pivot brackets at motor corners
    for (sx = [-1, 1])
    for (sy = [-1, 1])
        color("SteelBlue", 0.85)
            translate([sx * TRACK_W/2, sy * AXLE_BASE/2, 0])
                rotate([0, 0, sx > 0 ? 0 : 180])
                    pivot_bracket();

    // 4× 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();

    // Electronics bay reference ghost (from rover_electronics_bay.scad)
    %color("OliveDrab", 0.30)
        translate([0, 0, DECK_T + 0.5])
            cube([240, 200, 80], center = true);

    // Ghost motor axle positions
    for (sx = [-1, 1])
    for (sy = [-1, 1])
        %color("Tomato", 0.25)
            translate([sx * TRACK_W/2, sy * AXLE_BASE/2,
                       AXLE_H - DECK_TOP_H])
                rotate([90, 0, 0])
                    cylinder(d = AXLE_D, h = 80, center = true);

    // Ghost tyre outlines
    for (sx = [-1, 1])
    for (sy = [-1, 1])
        %color("Black", 0.15)
            translate([sx * TRACK_W/2, sy * AXLE_BASE/2,
                       -(DECK_TOP_H - AXLE_H)])
                rotate([90, 0, 0])
                    cylinder(d = 254, h = 55, center = true);
}

// ============================================================
// DECK PLATE  (Part A — laser-cut 6 mm 5052-H32 aluminium)
// ============================================================
// Weight estimate: 480×500×6 mm Al, ~50% lightened ≈ 1.35 kg
module deck_plate() {
    difference() {
        // ── Outer profile — rounded rectangle ─────────────────────────
        linear_extrude(DECK_T)
            minkowski() {
                square([ROVER_L - 2*DECK_R, ROVER_W - 2*DECK_R], center = true);
                circle(r = DECK_R);
            }

        // ── Battery tray opening (under-deck, centred) ─────────────────
        batt_open_x = BATT_X_DIM + 2*TRAY_MARGIN;
        batt_open_y = BATT_Y_DIM * BATT_N + 2*TRAY_MARGIN;
        translate([0, 0, -e])
            cube([batt_open_x, batt_open_y, DECK_T + 2*e], center = true);

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

        // ── Stem collar bolt circle (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);

        // ── FC mount holes — 30.5×30.5 M3 (shared SaltyLab pattern) ──
        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 ───────────────────────
        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);

        // ── Pivot bracket M3 attachment slots (4× corners) ────────────
        // Two slotted holes per corner at the deck attachment flange
        for (sx = [-1, 1])
        for (sy = [-1, 1]) {
            bx = sx * (ROVER_W/2 - ADJ_INSET_X);
            by = sy * AXLE_BASE/2;
            for (offset = [-ADJ_INSET_Y, ADJ_INSET_Y])
                hull() {
                    translate([bx, by + offset - ADJ_SLOT_L/2, -e])
                        cylinder(d = ADJ_M3_D, h = DECK_T + 2*e);
                    translate([bx, by + offset + ADJ_SLOT_L/2, -e])
                        cylinder(d = ADJ_M3_D, h = DECK_T + 2*e);
                }
        }

        // ── Lightening holes — 55 mm dia, in structural corridors ─────
        // Row between battery opening and pivot brackets
        for (sx = [-1, 1])
        for (sy = [-1, 1]) {
            lx = sx * (ROVER_W/4 + 20);
            ly = sy * (ROVER_L/4 + 15);
            translate([lx, ly, -e])
                cylinder(d = 55, h = DECK_T + 2*e);
        }
        // Additional pair flanking stem
        for (sx = [-1, 1])
            translate([sx * 65, 0, -e])
                cylinder(d = 40, h = DECK_T + 2*e);

        // ── Cable routing slots (4× around electronics bay footprint) ──
        for (sy = [-1, 1])
            hull() {
                translate([-20, sy * 105, -e]) cylinder(d = 14, h = DECK_T+2*e);
                translate([ 20, sy * 105, -e]) cylinder(d = 14, h = DECK_T+2*e);
            }
        for (sx = [-1, 1])
            hull() {
                translate([sx * 125, -18, -e]) cylinder(d = 14, h = DECK_T+2*e);
                translate([sx * 125,  18, -e]) cylinder(d = 14, h = DECK_T+2*e);
            }

        // ── Motor phase cable pass-throughs at each corner ────────────
        for (sx = [-1, 1])
        for (sy = [-1, 1])
            translate([sx * (ROVER_W/2 - 25),
                       sy * (ROVER_L/2 - 25), -e])
                cylinder(d = 18, h = DECK_T + 2*e);
    }
}

// ── Deck-top stem collar (raised boss, 25 mm bore) ─────────────────────────
module stem_collar() {
    translate([0, 0, DECK_T])
        difference() {
            cylinder(d = STEM_COLLAR_OD, h = STEM_COLLAR_H);
            // Bore
            translate([0, 0, -e])
                cylinder(d = STEM_BORE, h = STEM_COLLAR_H + 2*e);
            // Flange bolt holes
            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);
        }
}

// ============================================================
// PIVOT BRACKET  (Part B — M3-adjustable motor corner mount)
// ============================================================
// One per corner (×4).  Mounts to deck top face via 2× M3 SHCS
// through slotted deck holes (allows ±12 mm fore/aft adjustment).
// Provides:
//   • M8 pivot pin bore for suspension trailing arm
//   • Spring upper seat (captured spring guide boss, 14 mm OD)
//   • Vertical flange to deck edge for lateral stiffness
//
// Print: PETG 5 perims 60% infill, flat base down.
// Alt  : CNC 8 mm 6061-T6 Al from pivot_bracket_2d DXF.
//
// Coordinate: bracket centred at motor corner (sx*TRACK_W/2, sy*AXLE_BASE/2)
// The deck-edge flange is on the -X side (inner face toward deck centre).
// ============================================================
module pivot_bracket() {
    // Deck-top flat base plate
    translate([-PBK_W/2, -PBK_L/2, 0])
        difference() {
            cube([PBK_W, PBK_L, PBK_T]);

            // 2× M3 adjustment slots (fore-aft direction)
            for (s = [-1, 1])
                hull() {
                    translate([ADJ_INSET_X,
                               PBK_L/2 + s*ADJ_INSET_Y - ADJ_SLOT_L/2,
                               -e])
                        cylinder(d = ADJ_M3_D, h = PBK_T + 2*e);
                    translate([ADJ_INSET_X,
                               PBK_L/2 + s*ADJ_INSET_Y + ADJ_SLOT_L/2,
                               -e])
                        cylinder(d = ADJ_M3_D, h = PBK_T + 2*e);
                }

            // Lightening slot (centre of bracket base)
            translate([ADJ_INSET_X + 8, PBK_L/2 - 18, -e])
                cube([PBK_W - ADJ_INSET_X - 14, 36, PBK_T + 2*e]);
        }

    // Outer vertical flange (at +X edge — outboard side, toward motor)
    // This flange drops below the deck to form the suspension pivot clevis.
    translate([PBK_W/2 - PBK_T, -PBK_L/2, -(PBK_FLANGE_H)])
        difference() {
            cube([PBK_T, PBK_L, PBK_FLANGE_H + PBK_T]);

            // M8 pivot pin bore — at mid-height of flange, centred fore-aft
            // The trailing arm will pivot on an M8 bolt through this hole.
            pivot_z = PBK_FLANGE_H / 2;
            translate([-e, PBK_L/2, pivot_z])
                rotate([0, 90, 0])
                    cylinder(d = PIV_D, h = PBK_T + 2*e);
        }

    // Spring upper seat boss (below bracket base, outboard side)
    // Compression spring (Ø14 OD) slides over this guide boss.
    // When arm swings up (bump), spring is compressed between this boss
    // and the matching pocket in the trailing arm.
    translate([PBK_W/2 - PBK_T/2, 0, -e])
        cylinder(d = SPG_GUIDE_OD, h = SPG_GUIDE_H + e);
}

// Flat (2D projection source) version of pivot bracket — same profile
module pivot_bracket_flat() {
    difference() {
        cube([PBK_W, PBK_L, PBK_T], center = true);
        // M3 slots
        for (s = [-1, 1])
            hull() {
                translate([0, s*ADJ_INSET_Y - ADJ_SLOT_L/2, 0])
                    cylinder(d = ADJ_M3_D, h = PBK_T + 2*e, center = true);
                translate([0, s*ADJ_INSET_Y + ADJ_SLOT_L/2, 0])
                    cylinder(d = ADJ_M3_D, h = PBK_T + 2*e, center = true);
            }
        // M8 pivot bore
        cylinder(d = PIV_D, h = PBK_T + 2*e, center = true);
    }
}

// ── Place pivot brackets at correct corners ───────────────────────────────────
// Called from assembly() with (sx, sy) = (±1, ±1)

// ============================================================
// CSI CAMERA CORNER BRACKET  (Part C — 4× corners)
// ============================================================
// Mounts an IMX219 / Arducam CSI module at each deck corner,
// angled 45° outward and CSI_TILT° downward for terrain coverage.
// 2× M2 bolts hold camera PCB (15 mm square hole pattern).
// 2× M3 bolts mount bracket to deck top.
//
// Print: PETG 4 perims 30% infill, flat base down.
// ============================================================
module csi_corner_bracket() {
    base_l = 40;
    base_w = 30;
    base_t = 5;
    arm_l  = 30;

    difference() {
        union() {
            // Deck-top base plate
            cube([base_l, base_w, base_t]);

            // Angled arm + camera face plate
            translate([base_l / 2, base_w / 2, base_t])
                rotate([0, CSI_TILT, 0])
                    translate([-CSI_PCB/2, -CSI_PCB/2, 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 camera M2 mounting 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 cable slot (3 mm wide, 12 mm long, centred)
        translate([base_l/2 - 6, base_w/2 - 1.5, -e])
            cube([12, 3, base_t + 2*e]);
    }
}

module csi_bracket_placed(sx, sy) {
    // Corner position
    cx = sx * (ROVER_W/2 - 25);
    cy = sy * (ROVER_L/2 - 25);
    // Rotate so camera faces outward from corner
    rot = atan2(sy, sx) * 180 / 3.14159 - 45;
    translate([cx, cy, DECK_T])
        rotate([0, 0, rot])
            translate([-20, -15, 0])
                csi_corner_bracket();
}

// ============================================================
// D435i FRONT BRACKET  (Part D — 1× front mount)
// ============================================================
// Arm extends forward from deck front edge.
// Camera face tilted RS_TILT° nose-down.
// 1/4-20 UNC captured hex nut for D435i tripod socket.
// 2× M4 bolts mount base to deck front face.
//
// Print: PETG 5 perims 40% infill, arm flat on bed.
// ============================================================
module d435i_front_bracket() {
    base_d  = 22;   // base depth (Y direction, into deck)
    base_h  =  8;   // base/arm thickness
    arm_len = RS_ARM_LEN;

    // 1/4-20 UNC geometry
    nut14_af  = 11.1;   // across-flats
    nut14_h   =  5.6;   // nut thickness
    nut14_cl  =  6.5;   // bolt clearance bore

    difference() {
        union() {
            // Rear base plate (bolts to deck front face)
            translate([-RS_BASE_W/2, 0, 0])
                cube([RS_BASE_W, base_d, base_h]);

            // Forward arm (+ direction is forward / +Y)
            translate([-12, base_d, 0])
                cube([24, arm_len, base_h]);

            // Camera face plate (tilted RS_TILT° downward)
            translate([0, base_d + arm_len, base_h / 2])
                rotate([0, RS_TILT, 0])
                    translate([-15, 0, -base_h / 2])
                        cube([30, 14, base_h]);
        }

        // 2× M4 base attachment holes
        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 captured nut pocket in face plate
        translate([0, base_d + arm_len + 12, base_h / 2])
            rotate([0, 90, 0]) {
                // Hex nut pocket (from back)
                translate([0, 0, -nut14_h - 1])
                    cylinder(d = nut14_af / cos(30), h = nut14_h + 1, $fn = 6);
                // Camera bolt clearance bore
                cylinder(d = nut14_cl, h = 20);
            }
    }
}

module d435i_bracket_placed() {
    // Mount to deck front edge, centred left-right, at deck level
    translate([0, ROVER_L/2 + 10, DECK_T])
        rotate([0, 0, 180])
            d435i_front_bracket();
}
