// ============================================================
// sensor_rail.scad — Universal Sensor Mount Rail System
// Issue: #138  Agent: sl-mechanical  Date: 2026-03-01
// ============================================================
//
// T-slot 20×20 mm rail system for quick-swap sensor mounting
// across all SaltyLab robot variants.
//
// Rail profile: compatible with OpenBuilds 2020 / MISUMI HFS5-2020
//   standard aluminium extrusion (off-the-shelf preferred).
//   Printable PETG sections provided for rapid prototyping only.
//
// Height indexing: M5 cross-holes every 25 mm along rail length.
//   Brackets can be positioned at any height (stepless) and locked
//   with a thumbscrew, or indexed to 25 mm grid by aligning to holes.
//
// Bracket retention: M3 thumbscrew (no tools, ¼ turn).
//   Each bracket has a printed T-nut in the rail T-groove.
//   Thumbscrew clamps T-nut against groove walls from outside.
//
// Cross-variant base adapters (this file):
//   • stem_adapter()    — Ø25 mm stem (SaltyLab / SaltyRover mast)
//   • post_adapter()    — square tube (SaltyRover vertical posts)
//   • tank_clamp()      — flat plate clamp (SaltyTank uprights)
//
// Sensor brackets → see sensor_rail_brackets.scad
//
// Coordinate convention:
//   Rail runs along Z (vertical).
//   Rail cross-section in X-Y plane.
//   Front face (sensor side) faces +Y.
//   Z = 0 at rail bottom.
//
// RENDER options:
//   "assembly"         full rail + adapters + bracket ghosts (default)
//   "rail_2d"          DXF — rail profile cross-section (spec for extrusion)
//   "rail_section_stl" STL — printable rail section (prototype only)
//   "stem_adapter_stl" STL — Ø25 mm stem adapter (print 1×)
//   "post_adapter_stl" STL — square-tube post adapter (print 1×)
//   "tank_clamp_stl"   STL — flat-plate tank upright clamp (print 1×)
//   "end_cap_stl"      STL — rail end cap (print 2× per rail)
//   "index_pin_stl"    STL — 25 mm index pin set (print 1 set)
//
// ── Export commands ─────────────────────────────────────────
//  Rail profile DXF (send to extrusion supplier):
//    openscad sensor_rail.scad -D 'RENDER="rail_2d"' -o sensor_rail_profile.dxf
//  Printable rail section STL (200 mm, prototype):
//    openscad sensor_rail.scad -D 'RENDER="rail_section_stl"' -o sensor_rail_200.stl
//  Stem adapter:
//    openscad sensor_rail.scad -D 'RENDER="stem_adapter_stl"' -o sensor_rail_stem_adapter.stl
//  Post adapter:
//    openscad sensor_rail.scad -D 'RENDER="post_adapter_stl"' -o sensor_rail_post_adapter.stl
//  Tank clamp:
//    openscad sensor_rail.scad -D 'RENDER="tank_clamp_stl"' -o sensor_rail_tank_clamp.stl
//  End cap:
//    openscad sensor_rail.scad -D 'RENDER="end_cap_stl"' -o sensor_rail_end_cap.stl
// ============================================================

$fn = 64;
e   = 0.01;

// ── 2020 T-slot profile geometry ─────────────────────────────────────────────
// Matches OpenBuilds V-Slot 2020 / MISUMI HFS5-2020 / standard 2020 T-slot.
// ⚠ Do NOT modify these — they must match the aluminium extrusion you purchase.
RAIL_W       =  20.0;    // outer width/height of profile
SLOT_OPEN    =   6.0;    // T-groove opening width at outer face
SLOT_INNER_W =  10.2;    // T-groove inner width
SLOT_INNER_H =   5.8;    // T-groove inner height (depth)
SLOT_NECK_H  =   3.2;    // distance from outer face to T-groove inner
CENTRAL_BORE =   4.2;    // central M5 bore diameter (tap drill)
CORNER_NOTCH =   1.6;    // corner chamfer radius (standard 2020)

// ── Rail section parameters ───────────────────────────────────────────────────
RAIL_LEN     = 200.0;    // default section length (printable prototype)
// For aluminium: order in 200 / 250 / 300 mm lengths as needed.

// ── Index holes ───────────────────────────────────────────────────────────────
INDEX_PITCH  =  25.0;    // index hole spacing (mm) — height adjustment grid
INDEX_HOLE_D =   5.3;    // M5 clearance through rail (perpendicular to Z)
// Index holes are on the LEFT and RIGHT faces of the rail (±X faces),
// perpendicular to Z (rail axis).  Bracket T-nut has an M5 registration
// peg that drops into these holes for repeatable indexed positioning.

// ── Printable T-nut (used by sensor_rail_brackets.scad) ──────────────────────
// These dims define the T-nut that slides in the SLOT_INNER T-groove.
TNUT_W       =   SLOT_INNER_W - 0.4;   // 9.8 mm — 0.4 clearance per side
TNUT_H       =   SLOT_INNER_H - 0.3;   // 5.5 mm
TNUT_L       =  12.0;                  // T-nut body length
TNUT_M3_NUT_AF = 5.5;                  // M3 hex nut across-flats (DIN 934)
TNUT_M3_NUT_H  = 2.5;                  // M3 hex nut thickness
TNUT_BOLT_D    = 3.3;                  // M3 clearance bore through T-nut

// ── Thumbscrew (M3 × 16 SHCS + printed thumbwheel) ───────────────────────────
THUMB_D      =  16.0;    // thumbwheel OD
THUMB_H      =   8.0;    // thumbwheel height
THUMB_KNURL  =  12;      // number of knurl ridges on thumbwheel

// ── Stem adapter (Ø25 mm — SaltyLab / SaltyRover mast) ───────────────────────
STEM_OD      =  25.0;    // SaltyLab / SaltyRover stem OD
STEM_BORE    =  25.4;    // collar bore with clearance
STEM_COL_OD  =  46.0;    // collar outer diameter
STEM_COL_H   =  40.0;    // collar height
STEM_BOLT_X  =  17.0;    // M4 clamping bolt CL from stem axis
STEM_RAIL_W  =  60.0;    // rail-mounting flange width
STEM_RAIL_H  =  30.0;    // rail-mounting flange height

// ── Post adapter (square tube, SaltyRover frame posts) ───────────────────────
POST_W       =  20.0;    // square tube OD (20×20 mm aluminium post)
POST_WALL    =   2.0;    // tube wall thickness
POST_CLAMP_T =   4.0;    // clamp plate thickness
POST_CLAMP_W =  50.0;    // clamp plate width
POST_CLAMP_H =  60.0;    // clamp plate height

// ── Tank clamp (flat plate, SaltyTank side frame uprights) ───────────────────
TANK_PLATE_T =   6.0;    // SaltyTank side frame plate thickness (matches FRAME_T)
TANK_CLAMP_W =  50.0;    // clamp body width
TANK_CLAMP_H =  60.0;    // clamp body height
TANK_BOLT_SPC=  30.0;    // M4 bolt spacing for clamp-to-frame attachment

// ── Fasteners ─────────────────────────────────────────────────────────────────
M3_D  = 3.3;
M4_D  = 4.3;
M5_D  = 5.3;

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

if (RENDER == "assembly") {
    assembly();
} else if (RENDER == "rail_2d") {
    projection(cut = true)
        translate([0, 0, -RAIL_W/2])
            rotate([90, 0, 0])
                rail_section(RAIL_W);
} else if (RENDER == "rail_section_stl") {
    rail_section(RAIL_LEN);
} else if (RENDER == "stem_adapter_stl") {
    stem_adapter();
} else if (RENDER == "post_adapter_stl") {
    post_adapter();
} else if (RENDER == "tank_clamp_stl") {
    tank_clamp();
} else if (RENDER == "end_cap_stl") {
    rail_end_cap();
} else if (RENDER == "index_pin_stl") {
    index_pin_set();
}

// ============================================================
// ASSEMBLY PREVIEW
// ============================================================
module assembly() {
    // Rail section (200 mm, vertical)
    color("Silver", 0.85) rail_section(200);

    // Stem adapter at base
    color("SteelBlue", 0.80)
        translate([0, 0, -STEM_COL_H - 10])
            stem_adapter();

    // End cap at top
    color("DimGray", 0.80)
        translate([0, 0, 200])
            rail_end_cap();

    // Ghost sensor brackets (from sensor_rail_brackets.scad)
    // RPLIDAR at top
    %color("OliveDrab", 0.3)
        translate([0, RAIL_W/2 + 5, 170])
            cube([90, 20, 60], center = true);

    // D435i at middle
    %color("DarkSlateGray", 0.3)
        translate([0, RAIL_W/2 + 5, 100])
            cube([95, 20, 30], center = true);

    // IMX219 at lower
    %color("Teal", 0.3)
        translate([0, RAIL_W/2 + 5, 50])
            cube([40, 20, 40], center = true);

    // Index hole markers
    for (z = [0 : INDEX_PITCH : 200])
        %color("Red", 0.4)
            translate([RAIL_W/2 + 2, 0, z])
                rotate([0, 90, 0])
                    cylinder(d = 3, h = 4);
}

// ============================================================
// RAIL SECTION  (Part A — aluminium extrusion or PETG print)
// ============================================================
// Standard 2020 T-slot profile extruded along Z axis.
// This OpenSCAD module is primarily for:
//   1. DXF export to spec the extrusion cross-section for a supplier
//   2. Printable prototype sections (print in PETG, 5 perims, 60% infill)
//
// For production: purchase OpenBuilds V-Slot 2020 in desired length.
//   Search: "2020 V-slot aluminium extrusion" or "2020 T-slot rail"
//   Cut to length with a mitre saw. Tap central M5 bore at ends for
//   end-mounting and rail-to-adapter connections.
//
// Index holes: drilled/tapped M5 on LEFT and RIGHT faces (±X)
//   at 25 mm pitch. Specify when ordering pre-drilled extrusion,
//   or drill manually after cutting.
module rail_section(length = RAIL_LEN) {
    difference() {
        // ── Extrude the 2020 profile ─────────────────────────────────
        linear_extrude(length)
            tslot_profile_2d();

        // ── Central bore (M5 tap drill, both ends) ────────────────────
        translate([0, 0, -e])
            cylinder(d = CENTRAL_BORE, h = length + 2*e);

        // ── Index holes (M5 clearance, ±X faces, every 25 mm) ─────────
        for (z = [INDEX_PITCH/2 : INDEX_PITCH : length - INDEX_PITCH/2])
            translate([-RAIL_W/2 - e, 0, z])
                rotate([0, 90, 0])
                    cylinder(d = INDEX_HOLE_D, h = RAIL_W + 2*e);
    }
}

// ── 2020 T-slot cross-section (2D profile for linear_extrude) ─────────────────
// Matches OpenBuilds V-Slot 2020 profile.
module tslot_profile_2d() {
    difference() {
        // Outer square with corner notches
        difference() {
            square([RAIL_W, RAIL_W], center = true);
            // Corner notches (standard 2020 chamfer)
            for (cx = [-1, 1])
            for (cy = [-1, 1])
                translate([cx * (RAIL_W/2 - CORNER_NOTCH/2),
                           cy * (RAIL_W/2 - CORNER_NOTCH/2)])
                    rotate([0, 0, 45])
                        square([CORNER_NOTCH * 1.41, CORNER_NOTCH * 1.41],
                                center = true);
        }

        // Central lightening bore
        circle(d = CENTRAL_BORE);

        // Internal corner channels (weight reduction, standard in 2020)
        for (cx = [-1, 1])
        for (cy = [-1, 1])
            translate([cx * (RAIL_W/4 + 0.5), cy * (RAIL_W/4 + 0.5)])
                circle(d = 3.2);

        // 4× T-grooves (one per face)
        for (rot = [0, 90, 180, 270])
            rotate([0, 0, rot])
                tslot_groove_2d(face_dist = RAIL_W/2);
    }
}

// ── Single T-groove profile (2D, centred on face at face_dist from origin) ────
module tslot_groove_2d(face_dist) {
    // Outer slot opening (tapered/chamfered entry)
    translate([0, face_dist - SLOT_NECK_H])
        square([SLOT_OPEN, SLOT_NECK_H + e], center = true);

    // Inner T-groove
    translate([0, face_dist - SLOT_NECK_H - SLOT_INNER_H + e])
        square([SLOT_INNER_W, SLOT_INNER_H + e], center = true);
}

// ============================================================
// PRINTABLE T-NUT  (Part B — print ×N as needed in PETG)
// ============================================================
// Slides into the T-groove of the 2020 rail.
// Captured M3 hex nut allows a thumbscrew to clamp from outside.
// The T-nut has a registration peg that drops into index holes.
//
// Print: PETG, flat face down, 5 perims, 60% infill.
// Standard M3 hex nut pressed in from top after printing.
module printable_tnut() {
    difference() {
        union() {
            // Main body (fits inside T-groove)
            cube([TNUT_W, TNUT_L, TNUT_H], center = true);

            // Wings that sit behind slot opening (wider than SLOT_OPEN)
            // These wings bear against the T-groove inner walls
            translate([0, 0, TNUT_H/2 - 0.8])
                cube([TNUT_W, TNUT_L, 1.6], center = true);
        }

        // M3 hex nut pocket (press-fit from top)
        translate([0, 0, TNUT_H/2 - TNUT_M3_NUT_H - 0.3])
            cylinder(d = TNUT_M3_NUT_AF / cos(30),
                     h = TNUT_M3_NUT_H + 0.4,
                     $fn = 6);

        // M3 clearance bore (through T-nut, for thumbscrew shank)
        cylinder(d = TNUT_BOLT_D, h = TNUT_H + 2*e, center = true);
    }
}

// ============================================================
// THUMBSCREW WHEEL  (Part C — print ×N as needed in PETG)
// ============================================================
// Press-fit onto M3×16 SHCS head. Provides finger grip for
// tool-free tightening. One per bracket.
// Press-fit bore: 5.6 mm (M3 SHCS head hex socket OD ≈ 5.5 mm)
module thumbscrew_wheel() {
    difference() {
        union() {
            cylinder(d = THUMB_D, h = THUMB_H);
            // Knurl ridges (cosmetic grooves — helps grip)
            for (i = [0 : THUMB_KNURL - 1])
                rotate([0, 0, i * 360 / THUMB_KNURL])
                    translate([THUMB_D/2 - 1, 0, 0])
                        cylinder(d = 2.0, h = THUMB_H);
        }
        // M3 SHCS head hex socket bore (press-fit)
        translate([0, 0, -e])
            cylinder(d = 5.7, h = 4);
        // M3 shank clearance bore (bolt passes through)
        cylinder(d = M3_D, h = THUMB_H + 2*e);
    }
}

// ============================================================
// RAIL END CAP  (Part D — print 2× per rail in PETG)
// ============================================================
// Safety end cap — prevents T-nuts from sliding off end.
// Snap-friction fit (no fasteners needed for prototyping).
// Or: drill M5 through cap + rail end, use M5×10 set screw.
module rail_end_cap() {
    cap_h = 8;
    cap_plug_h = 6;   // depth inserted into rail end

    difference() {
        union() {
            // Outer flange (≥ rail OD, prevents over-insertion)
            cylinder(d = RAIL_W + 6, h = cap_h);

            // Inner plug (fits inside rail with 0.3 mm clearance per side)
            translate([0, 0, cap_h - e])
                cube([RAIL_W - 0.6, RAIL_W - 0.6, cap_plug_h], center = true);
        }

        // Central M5 bore (for optional end-bolt)
        cylinder(d = M5_D, h = cap_h + cap_plug_h + e);

        // Lightening cutout
        translate([0, 0, 2])
            cube([RAIL_W - 4, RAIL_W - 4, cap_h + cap_plug_h], center = true);
    }
}

// ============================================================
// INDEX PIN SET  (Part E — print 1 set in PETG)
// ============================================================
// Small pins that insert through M5 index holes on the rail.
// When a bracket T-nut has a matching recess, the pin provides
// positive indexed positioning (true 25 mm grid lock).
// For non-indexed positioning: leave pins out, use thumbscrew only.
module index_pin_set() {
    // Print 4 pins on a carrier plate for easy identification
    for (i = [0:3])
        translate([i * 20, 0, 0])
            index_pin();
}

module index_pin() {
    pin_od   = 4.9;    // M5 hole clearance (5.3 - 0.4 mm)
    pin_len  = RAIL_W + 2;   // spans full rail width + 1 mm each side
    head_od  = 8.0;    // knurled head OD (finger pull)
    head_h   = 5.0;

    union() {
        // Pin shaft
        cylinder(d = pin_od, h = pin_len);
        // Knurled head (at one end)
        translate([0, 0, pin_len])
            difference() {
                cylinder(d = head_od, h = head_h);
                for (j = [0:7])
                    rotate([0, 0, j*45])
                        translate([head_od/2 - 1.2, 0, 0])
                            cylinder(d = 2, h = head_h + e);
            }
    }
}

// ============================================================
// STEM ADAPTER  (Part F — SaltyLab / SaltyRover Ø25 mm mast)
// ============================================================
// Split collar clamps to the robot's Ø25 mm vertical stem.
// Two rail-mounting flanges (front + rear) allow one or two
// sensor rails to be mounted at 180° apart.
//
// Print 2× halves (front + rear), join with 2× M4×30 SHCS.
// Each half is printed flat-face-down with no supports needed.
module stem_adapter() {
    // Front half only — print 2× and assemble around stem
    stem_adapter_half("front");
}

module stem_adapter_half(side = "front") {
    sy = (side == "front") ? 1 : -1;

    difference() {
        union() {
            // ── Collar half (semicircle) ─────────────────────────────
            rotate_extrude(angle = 180)
                translate([STEM_COL_OD/2 - (STEM_COL_OD - STEM_BORE)/2, 0, 0])
                    square([(STEM_COL_OD - STEM_BORE)/2,
                            STEM_COL_H], center = false);

            // ── Rail mounting flange (extends forward from collar) ────
            // Single flange on front face, centred left-right
            translate([-STEM_RAIL_W/2,
                        sy * (STEM_COL_OD/2 - 2),
                        STEM_COL_H/2 - STEM_RAIL_H/2])
                cube([STEM_RAIL_W, STEM_COL_OD/4 + 5, STEM_RAIL_H]);
        }

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

        // ── M4 clamping bolt holes (through collar flanges) ───────────
        for (sx = [-1, 1])
            translate([sx * STEM_BOLT_X, 0, STEM_COL_H / 2])
                rotate([90, 0, 0])
                    cylinder(d = M4_D, h = STEM_COL_OD + 2*e, center = true);

        // ── M4 nut pocket (rear half) ─────────────────────────────────
        if (side == "rear") {
            for (sx = [-1, 1])
                translate([sx * STEM_BOLT_X, -STEM_COL_OD/2 + 4,
                           STEM_COL_H/2])
                    rotate([90, 0, 0]) {
                        cylinder(d = 7.5/cos(30), h = 4, $fn = 6); // M4 nut
                    }
        }

        // ── Rail attachment slots in flange (M5 × 2) ─────────────────
        // Slots allow ±5 mm front-back fine adjustment of rail position
        for (rz = [-STEM_RAIL_H/4, STEM_RAIL_H/4])
            translate([0,
                       sy * (STEM_COL_OD/2 - 2 + STEM_COL_OD/8 + 5/2),
                       STEM_COL_H/2 + rz])
                rotate([90, 0, 0])
                    hull() {
                        translate([-6, 0, 0])
                            cylinder(d = M5_D, h = 10, center = true);
                        translate([+6, 0, 0])
                            cylinder(d = M5_D, h = 10, center = true);
                    }
    }
}

// ============================================================
// POST ADAPTER  (Part G — SaltyRover 20×20 mm square post)
// ============================================================
// C-clamp style bracket that wraps around a 20×20 mm aluminium
// post (common in rover extrusion frames).
// M4 × 2 clamping bolts through the clamp flanges.
// Rail mounts forward via 2× M5 slots in the front face.
module post_adapter() {
    // Outer clamp body
    difference() {
        union() {
            // Main C-clamp body
            translate([-POST_CLAMP_W/2, -POST_W/2 - POST_CLAMP_T,
                       0])
                cube([POST_CLAMP_W,
                      POST_W + 2*POST_CLAMP_T + POST_CLAMP_W/3,
                      POST_CLAMP_H]);
        }

        // Post cavity (POST_W × POST_W + clearance)
        translate([0, 0, -e])
            cube([POST_W + 0.4, POST_W + 0.4, POST_CLAMP_H + 2*e],
                  center = true);

        // C-clamp opening (rear gap — allows clamping around post)
        translate([-POST_W/2 - POST_CLAMP_T/2 - e, -POST_W/2 - e, -e])
            cube([POST_CLAMP_T + 2*e,
                  POST_W + 0.2,
                  POST_CLAMP_H + 2*e]);

        // M4 clamping bolt holes
        for (cz = [POST_CLAMP_H/4, 3*POST_CLAMP_H/4])
            translate([-POST_CLAMP_W/2 - e, 0, cz])
                rotate([0, 90, 0])
                    cylinder(d = M4_D, h = POST_CLAMP_W + 2*e);

        // M4 nut pockets (right flange)
        for (cz = [POST_CLAMP_H/4, 3*POST_CLAMP_H/4])
            translate([POST_CLAMP_W/2 - 5, 0, cz])
                rotate([0, 90, 0])
                    cylinder(d = 7.5/cos(30), h = 5, $fn = 6);

        // Rail mounting slots (M5 × 2, front face)
        for (cz = [POST_CLAMP_H/3, 2*POST_CLAMP_H/3])
            translate([0,
                       POST_W/2 + POST_CLAMP_T + POST_CLAMP_W/3 - e,
                       cz])
                rotate([90, 0, 0])
                    hull() {
                        translate([-5, 0, 0])
                            cylinder(d = M5_D,
                                     h = POST_CLAMP_T + 2*e);
                        translate([+5, 0, 0])
                            cylinder(d = M5_D,
                                     h = POST_CLAMP_T + 2*e);
                    }
    }
}

// ============================================================
// TANK CLAMP  (Part H — SaltyTank side frame upright)
// ============================================================
// Flat plate bracket bolts directly to SaltyTank side frame
// (6 mm Al plate) via 4× M4 SHCS.
// Rail attaches to the front face via 2× M5 slots.
module tank_clamp() {
    difference() {
        union() {
            // Main back plate (bolts to tank frame)
            translate([-TANK_CLAMP_W/2, -TANK_PLATE_T - 2, 0])
                cube([TANK_CLAMP_W,
                      TANK_PLATE_T + RAIL_W + 10,
                      TANK_CLAMP_H]);
        }

        // Tank frame plate slot (open at rear, receives frame edge)
        translate([-TANK_CLAMP_W/2 + 5, -TANK_PLATE_T - e, -e])
            cube([TANK_CLAMP_W - 10,
                  TANK_PLATE_T + 0.5,
                  TANK_CLAMP_H + 2*e]);

        // 4× M4 frame attachment bolts
        for (bx = [-TANK_BOLT_SPC/2, TANK_BOLT_SPC/2])
        for (bz = [TANK_CLAMP_H/4, 3*TANK_CLAMP_H/4])
            translate([bx, -TANK_PLATE_T/2, bz])
                rotate([90, 0, 0])
                    cylinder(d = M4_D, h = TANK_PLATE_T + 4, center = true);

        // Rail mounting slots (M5 × 2, front face)
        for (bz = [TANK_CLAMP_H/3, 2*TANK_CLAMP_H/3])
            translate([0, RAIL_W + 9 - e, bz])
                rotate([90, 0, 0])
                    hull() {
                        translate([-6, 0, 0])
                            cylinder(d = M5_D, h = 10);
                        translate([+6, 0, 0])
                            cylinder(d = M5_D, h = 10);
                    }

        // Weight-reduction pockets (back plate)
        translate([0, -TANK_PLATE_T/2 - 1, TANK_CLAMP_H/2])
            cube([TANK_CLAMP_W - 20,
                  4, TANK_CLAMP_H - 30],
                  center = true);
    }
}
