// ============================================================
// rover_spring_arm.scad — SaltyRover Spring Suspension Arm
// Issue: #109  Agent: sl-mechanical  Date: 2026-03-01
// ============================================================
//
// Trailing-arm spring suspension for rough terrain.
// One arm per wheel (print 4×).
//
// Mechanical principle:
//   The arm pivots on an M8 bolt through the pivot bracket
//   (saltyrover_chassis_r2.scad → pivot_bracket).
//   A captured compression spring between the pivot bracket's
//   spring boss and the arm's spring pocket provides restoring
//   force.  When a wheel strikes a bump, the arm swings upward
//   (rotating around the pivot) and compresses the spring.
//
//   Pivot bracket (chassis-fixed)
//        │
//    [M8 pivot]──────────────────[Motor axle dropout]
//        │         [Trailing arm]         │
//        └──[Spring upper seat]           │
//                   │                    │
//               [Spring]                 │
//                   │                    │
//            [Spring pocket in arm]      │
//
// Spring spec (wire form compression, standard size):
//   OD  : 14 mm (slides over bracket's 14 mm guide boss)
//   ID  : ~10 mm
//   Free length : 50 mm
//   Solid height: ~20 mm
//   Travel (max): 25 mm  →  spring compressed to 25 mm
//   Spring rate  : ~5 N/mm (soft — adjust to robot mass)
//   Part no.     : e.g. Lee Spring LCI 014M 05 S (or equivalent)
//
// Wheel travel:
//   Bump  (compression): 25 mm  (spring coils bind before hard stop)
//   Droop (extension)  : 15 mm  (limited by pivot bracket flange)
//   Total travel       : 40 mm
//
// Axle-to-ground height at full compression (worst case bump):
//   AXLE_H − TRAVEL_BUMP = 127 − 25 = 102 mm above ground ✓
//
// Print:
//   Material : PETG or PC (PC recommended for structural rigidity)
//   Settings : 5 perimeters, 60 % gyroid infill
//   Orientation: pivot boss flat face on build plate; no supports needed
//   Qty: 4×
//
// Export commands:
//   STL (main arm × 4):
//     openscad rover_spring_arm.scad -D 'RENDER="arm_stl"' -o rover_spring_arm.stl
//   STL (spring retainer cap × 4):
//     openscad rover_spring_arm.scad -D 'RENDER="retainer_stl"' -o rover_spring_retainer.stl
//   Assembly preview:
//     openscad rover_spring_arm.scad -D 'RENDER="assembly"'
// ============================================================

$fn = 64;
e   = 0.01;

// ── Motor axle (BOM.md caliper-verified) ────────────────────────────────────
AXLE_D       = 16.11;   // axle base section OD (caliper)
AXLE_FLAT    = 13.00;   // D-cut chord width (caliper)
AXLE_D_DCUT  = 15.95;   // D-cut section OD (caliper)
BEARING_OD   = 37.80;   // bearing seat collar OD (caliper)
BEARING_RECESS_H = 8.0; // recess depth for bearing seat on inboard face

// ── Arm geometry ─────────────────────────────────────────────────────────────
// Pivot end is at X=0, Y=0, Z=0 (pivot CL)
// Motor axle end is at X = +ARM_REACH (outboard, positive X = outboard)
ARM_REACH    = 75.0;    // pivot CL to motor axle CL (outboard reach)
ARM_W        = 38.0;    // arm width (fore-aft / Y direction)
ARM_T        = 14.0;    // arm thickness (vertical / Z direction)
ARM_TAPER    =  4.0;    // taper at motor end (arm narrows by this amount)

// Pivot boss
PIV_BOSS_OD  = ARM_W;   // boss is as wide as arm for structural continuity
PIV_BOSS_L   = ARM_T;   // boss length = arm thickness
PIV_D        =  8.5;    // M8 clearance bore through pivot

// ── Suspension spring parameters ────────────────────────────────────────────
SPG_OD       = 14.0;    // spring OD (matches bracket guide boss OD)
SPG_FREE_L   = 50.0;    // spring free length (see spec above)
SPG_TRAVEL   = 25.0;    // max bump travel / spring compression
SPG_POCKET_D = SPG_OD + 1.5;   // pocket bore (spring slides in with clearance)
SPG_POCKET_H = SPG_TRAVEL + 5; // pocket depth (captures spring bottom)

// Spring pocket CL from pivot (along arm)
SPG_POS_X    = ARM_REACH * 0.45;   // ~45% along arm from pivot

// ── Motor axle dropout slot ──────────────────────────────────────────────────
// Open-end slot at motor end of arm.  Retained by spring_retainer_cap.
DROP_W       = AXLE_D + 1.0;   // slot width (snug but not interference)
DROP_DEPTH   = AXLE_D + 4.0;   // slot depth from arm end inward

// ── Spring retainer cap ──────────────────────────────────────────────────────
// Small cap that closes the open axle slot from below, screwing onto the arm.
// Prevents axle from dropping out; provides second bearing recess face.
RET_T        = 6.0;    // cap thickness
RET_W        = ARM_W + 4.0;  // cap wider than arm for alignment lip
RET_BOLT_D   = M3_D;  // 2× M3 bolts retain the cap

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 == "arm_stl") {
    spring_arm();
} else if (RENDER == "retainer_stl") {
    spring_retainer_cap();
}

// ============================================================
// FULL ASSEMBLY PREVIEW
// ============================================================
module assembly() {
    color("SteelBlue", 0.85) spring_arm();
    color("CornflowerBlue", 0.80)
        translate([ARM_REACH, 0, -(ARM_T/2 + RET_T)])
            spring_retainer_cap();

    // Phantom spring (compressed)
    %color("LimeGreen", 0.5)
        translate([SPG_POS_X, 0, ARM_T/2])
            cylinder(d = SPG_OD, h = SPG_FREE_L - SPG_TRAVEL);

    // Phantom motor axle
    %color("Tomato", 0.3)
        translate([ARM_REACH, 0, 0])
            rotate([0, 90, 0])
                cylinder(d = AXLE_D, h = 120, center = true);

    // Phantom pivot bolt (M8)
    %color("Gray", 0.5)
        rotate([0, 90, 0])
            cylinder(d = 8, h = ARM_T + 20, center = true);

    // Phantom bracket guide boss (from pivot_bracket)
    %color("DarkGray", 0.4)
        translate([SPG_POS_X, 0, ARM_T/2])
            cylinder(d = SPG_OD, h = 20);
}

// ============================================================
// SPRING ARM
// ============================================================
// Pivot CL at (0, 0, 0).  Arm extends toward +X.
// Pivots around Y-axis (M8 bolt runs in Y direction).
// Spring acts in Z (vertical) at SPG_POS_X along arm.
// Motor axle runs in Y direction at (ARM_REACH, 0, 0).
// ============================================================
module spring_arm() {
    w_motor = ARM_W - ARM_TAPER;   // narrower at motor end

    difference() {
        union() {
            // ── Main arm body (tapered hull) ──────────────────────────
            hull() {
                // Pivot end — full-width rectangular section
                translate([0, -ARM_W/2, -ARM_T/2])
                    cube([e, ARM_W, ARM_T]);
                // Motor end — slightly narrower
                translate([ARM_REACH - DROP_DEPTH, -w_motor/2, -ARM_T/2])
                    cube([e, w_motor, ARM_T]);
            }

            // ── Pivot boss (cylindrical for hinge strength) ───────────
            // Cylindrical boss sits at pivot CL; flange provides washer seat.
            rotate([90, 0, 0]) {
                difference() {
                    cylinder(d = PIV_BOSS_OD, h = PIV_BOSS_L, center = true);
                }
            }

            // ── Spring pocket boss (raised above arm top face) ─────────
            // Boss rises to meet the bracket's spring guide boss.
            // The compression spring is captured between the two bosses.
            translate([SPG_POS_X, 0, ARM_T/2])
                cylinder(d = SPG_OD + 8, h = 8);

            // ── Axle retention lug at motor end (prevents side loading) ─
            translate([ARM_REACH - ARM_T, -w_motor/2, -ARM_T/2])
                cube([ARM_T, w_motor, ARM_T - BEARING_RECESS_H]);
        }

        // ── M8 pivot bore (through pivot boss in Y direction) ─────────
        rotate([90, 0, 0])
            cylinder(d = PIV_D, h = PIV_BOSS_L + 2*e, center = true);

        // ── Spring pocket bore (from top, captures spring bottom) ─────
        // Bore is slightly larger than spring OD for easy insertion.
        translate([SPG_POS_X, 0, ARM_T/2 - e])
            cylinder(d = SPG_POCKET_D, h = SPG_POCKET_H + e);

        // ── Spring pocket access slot (allows spring preload assembly) ─
        // Lateral slot lets spring be pressed in from side during assembly.
        translate([SPG_POS_X - SPG_OD/2, -SPG_OD/2 - 0.5, ARM_T/2 - e])
            cube([SPG_OD, SPG_OD + 1, SPG_POCKET_H + e]);

        // ── Motor axle dropout slot (open at arm tip end, +X) ─────────
        // Slot width = axle OD + 1 mm; depth = DROP_DEPTH inward.
        // Motor axle slides in from the open end.
        translate([ARM_REACH - DROP_DEPTH, -DROP_W/2, -ARM_T/2 - e])
            cube([DROP_DEPTH + e, DROP_W, ARM_T + 2*e]);

        // Rounded bore at inner end of dropout slot (distributes load)
        translate([ARM_REACH - DROP_DEPTH, 0, -ARM_T/2 - e])
            cylinder(d = DROP_W, h = ARM_T + 2*e);

        // ── Bearing seat recess (inboard face of axle slot) ──────────
        // Prevents bearing collar (Ø37.8) from clashing with arm face.
        translate([ARM_REACH - DROP_DEPTH - BEARING_RECESS_H, 0, -ARM_T/2 - e])
            cylinder(d = BEARING_OD + 1.5, h = BEARING_RECESS_H + e);

        // ── Retainer cap M3 bolt holes (2×, for spring_retainer_cap) ──
        for (dy = [-ARM_W/4, ARM_W/4])
            translate([ARM_REACH - DROP_DEPTH/2, dy, -ARM_T/2 - e])
                cylinder(d = M3_D - 0.3, h = ARM_T/2 + e);
                // Slightly tight bore — M3 self-taps into PETG at 3.0 mm

        // ── Lightening slot (mid-arm, between pivot boss and spring) ──
        lx1 = PIV_BOSS_OD/2 + 5;
        lx2 = SPG_POS_X - SPG_OD/2 - 5;
        translate([lx1, -(ARM_W/4), -ARM_T/2 - e])
            cube([max(lx2 - lx1, 1), ARM_W/2, ARM_T + 2*e]);
    }
}

// ============================================================
// SPRING RETAINER CAP
// ============================================================
// Clips onto the open axle slot at the arm tip.
// Prevents motor axle from falling out of the dropout slot.
// 2× M3 bolts thread into the arm's self-tap holes.
// Also provides the outboard bearing-seat face.
// ============================================================
module spring_retainer_cap() {
    w_cap = ARM_W - ARM_TAPER + 2;

    difference() {
        union() {
            cube([DROP_DEPTH, w_cap, RET_T], center = true);

            // Alignment lips (engage the arm slot edges)
            for (dy = [-1, 1])
                translate([0, dy * (w_cap/2 + 1), 0])
                    cube([DROP_DEPTH - 2, 2, RET_T + 4], center = true);
        }

        // Axle bore (clearance) — round section
        cylinder(d = AXLE_D + 0.8, h = RET_T + 2*e, center = true);

        // Bearing seat recess (outboard face)
        translate([0, 0, RET_T/2 - BEARING_RECESS_H/2])
            cylinder(d = BEARING_OD + 1.5, h = BEARING_RECESS_H + e, center = true);

        // 2× M3 bolt clearance holes
        for (dy = [-ARM_W/4, ARM_W/4])
            translate([0, dy, 0])
                cylinder(d = M3_D, h = RET_T + 2*e, center = true);
    }
}
