From fc431351440c4e044f47c6515d73c3fb6639b37d Mon Sep 17 00:00:00 2001 From: sl-mechanical Date: Sat, 7 Mar 2026 09:49:10 -0500 Subject: [PATCH] feat: Spring-loaded phone mount bracket for T-slot rail (Issue #535) Parametric OpenSCAD design for 2020 T-slot rail phone mount bracket. Adjustable width 60-85mm, spring-loaded cam quick-release lever, vibration-dampening flexure rib grip pads. PETG 3D-printable, no supports. Co-Authored-By: Claude Sonnet 4.6 --- chassis/phone_mount_bracket.scad | 504 +++++++++++++++++++++++++++++++ 1 file changed, 504 insertions(+) create mode 100644 chassis/phone_mount_bracket.scad diff --git a/chassis/phone_mount_bracket.scad b/chassis/phone_mount_bracket.scad new file mode 100644 index 0000000..f351cd4 --- /dev/null +++ b/chassis/phone_mount_bracket.scad @@ -0,0 +1,504 @@ +// ============================================================ +// phone_mount_bracket.scad — Spring-Loaded Phone Mount for T-Slot Rail +// Issue: #535 Agent: sl-mechanical Date: 2026-03-07 +// ============================================================ +// +// Parametric spring-loaded phone mount that clamps to the 2020 aluminium +// T-slot sensor rail. Adjustable phone width 60–85 mm. Quick-release +// cam lever for tool-free phone swap. Vibration-damping flexure ribs +// on grip pads absorb motor/terrain vibration (PETG compliance). +// +// Design overview: +// - Fixed jaw + sliding jaw on a 40 mm guide rail (M4 rod) +// - Coil spring (Ø8 × 30 mm) compressed between jaw and end-stop — +// spring pre-load keeps phone clamped at any width in range +// - Cam lever (printed PETG) rotates 90° to release / lock spring +// - Anti-vibration flexure ribs on both grip pad faces +// - Landscape or portrait orientation: bracket rotates on T-nut base +// +// Parts (STL exports): +// Part 1 — tnut_base() Rail attachment base (universal) +// Part 2 — fixed_jaw() Fixed bottom jaw + guide rail bosses +// Part 3 — sliding_jaw() Spring-loaded sliding jaw +// Part 4 — cam_lever() Quick-release cam lever +// Part 5 — grip_pad() Flexure grip pad (print ×2, TPU optional) +// Part 6 — assembly_preview() Full assembly +// +// Hardware BOM (per mount): +// 1× M4 × 60 mm SHCS guide rod + spring bolt +// 1× M4 hex nut end-stop on sliding jaw +// 1× Ø8 × 30 mm coil spring ~0.5 N/mm rate (spring clamping) +// 2× M3 × 16 mm SHCS T-nut base thumbscrew + arm bolts +// 1× M3 hex nut thumbscrew nut in T-nut +// 4× M2 × 8 mm SHCS grip pad retention bolts (optional) +// +// Dimensions: +// Phone width range : PHONE_W_MIN–PHONE_W_MAX (60–85 mm) parametric +// Phone thickness : up to PHONE_THICK_MAX (12 mm) — open-front jaw +// Phone height held : GRIP_SPAN (22 mm each jaw) — portrait/landscape +// Overall bracket H : ~110 mm W: ~90 mm D: ~55 mm +// +// Print settings: +// Material : PETG (tnut_base, fixed_jaw, sliding_jaw, cam_lever) +// TPU 95A optional for grip_pad (or PETG for rigidity) +// Perimeters: 5 (structural parts), 3 (grip_pad) +// Infill : 40 % gyroid (jaws), 20 % (grip_pad) +// Supports : none needed (designed for FDM orientation) +// Layer ht : 0.2 mm +// +// Export commands: +// openscad phone_mount_bracket.scad -D 'RENDER="tnut_base_stl"' -o pm_tnut_base.stl +// openscad phone_mount_bracket.scad -D 'RENDER="fixed_jaw_stl"' -o pm_fixed_jaw.stl +// openscad phone_mount_bracket.scad -D 'RENDER="sliding_jaw_stl"' -o pm_sliding_jaw.stl +// openscad phone_mount_bracket.scad -D 'RENDER="cam_lever_stl"' -o pm_cam_lever.stl +// openscad phone_mount_bracket.scad -D 'RENDER="grip_pad_stl"' -o pm_grip_pad.stl +// ============================================================ + +$fn = 64; +e = 0.01; // epsilon for boolean clearance + +// ── Phone parameters (adjust to target device) ─────────────────────────────── +PHONE_W_MIN = 60.0; // narrowest phone width supported (mm) +PHONE_W_MAX = 85.0; // widest phone width supported (mm) +PHONE_THICK_MAX = 12.0; // max phone body thickness incl. case (mm) + +// ── Rail geometry (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; + +// ── T-nut constants ─────────────────────────────────────────────────────────── +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; // M3 clearance + +// ── Base plate geometry ─────────────────────────────────────────────────────── +BASE_FACE_W = 30.0; +BASE_FACE_H = 25.0; +BASE_FACE_T = SLOT_NECK_H + 1.5; + +// ── Jaw geometry ───────────────────────────────────────────────────────────── +JAW_BODY_W = 88.0; // jaw outer width (> PHONE_W_MAX for rim) +JAW_BODY_H = 28.0; // jaw height (Z) — phone grip span +JAW_BODY_T = 14.0; // jaw depth (Y) — phone cradled this deep +JAW_WALL_T = 4.0; // jaw side wall thickness +JAW_LIP_T = 3.0; // front retaining lip thickness +JAW_LIP_H = 5.0; // front lip height (retains phone) +PHONE_POCKET_D = PHONE_THICK_MAX + 0.5; // pocket depth for phone + +// ── Guide rod / spring system ───────────────────────────────────────────────── +GUIDE_ROD_D = 4.3; // M4 clearance bore in sliding jaw +GUIDE_BOSS_D = 10.0; // boss OD around guide bore +GUIDE_BOSS_T = 6.0; // boss length +SPRING_OD = 8.5; // coil spring OD pocket (spring is Ø8) +SPRING_L = 32.0; // spring pocket length (spring compressed ~22 mm) +SPRING_SEAT_T = 3.0; // spring seat wall at end-stop boss +JAW_TRAVEL = PHONE_W_MAX - PHONE_W_MIN + 4.0; // max jaw travel (mm) +ARM_SPAN = PHONE_W_MAX + 2 * JAW_WALL_T + 8; // fixed jaw total width + +// ── Cam lever geometry ──────────────────────────────────────────────────────── +CAM_R_MIN = 5.0; // cam small radius (engaged / clamped) +CAM_R_MAX = 9.0; // cam large radius (released, spring compressed) +CAM_THICK = 8.0; // cam disc thickness +CAM_HANDLE_L = 45.0; // lever arm length +CAM_HANDLE_W = 8.0; // lever handle width +CAM_HANDLE_T = 5.0; // lever handle thickness +CAM_BORE_D = 4.3; // M4 pivot bore +CAM_DETENT_D = 3.0; // detent ball pocket (3 mm bearing) + +// ── Grip pad geometry (vibration dampening flexure ribs) ───────────────────── +PAD_W = JAW_BODY_W - 2*JAW_WALL_T - 2; // pad width +PAD_H = JAW_BODY_H - 2; // pad height +PAD_T = 2.5; // pad body thickness +RIB_H = 1.5; // flexure rib height above pad face +RIB_W = 1.2; // rib width +RIB_PITCH = 5.0; // rib pitch (centre-to-centre) +RIB_COUNT = floor(PAD_W / RIB_PITCH) - 1; + +// ── Arm geometry (base to jaw body) ────────────────────────────────────────── +ARM_REACH = 38.0; // distance from rail face to jaw centreline (+Y) +ARM_T = 4.0; // arm thickness +ARM_H = BASE_FACE_H; + +// ── Fasteners ───────────────────────────────────────────────────────────────── +M2_D = 2.4; +M3_D = 3.3; +M4_D = 4.3; +M4_NUT_AF = 7.0; // M4 hex nut across-flats +M4_NUT_H = 3.2; // M4 hex nut height + +// ============================================================ +// RENDER DISPATCH +// ============================================================ +RENDER = "assembly"; + +if (RENDER == "assembly") assembly_preview(); +else if (RENDER == "tnut_base_stl") tnut_base(); +else if (RENDER == "fixed_jaw_stl") fixed_jaw(); +else if (RENDER == "sliding_jaw_stl") sliding_jaw(); +else if (RENDER == "cam_lever_stl") cam_lever(); +else if (RENDER == "grip_pad_stl") grip_pad(); + +// ============================================================ +// ASSEMBLY PREVIEW +// ============================================================ +module assembly_preview() { + // Ghost rail section (20 × 20 × 200) + %color("Silver", 0.30) + linear_extrude(200) + square([RAIL_W, RAIL_W], center = true); + + // T-nut base at Z=80 on rail + color("OliveDrab", 0.85) + translate([0, 0, 80]) + tnut_base(); + + // Fixed jaw assembly (centred, extending +Y from base) + color("DarkSlateGray", 0.85) + translate([0, SLOT_NECK_H + BASE_FACE_T + ARM_REACH, 80]) + fixed_jaw(); + + // Sliding jaw — shown at mid-travel (phone ~72 mm wide) + color("SteelBlue", 0.85) + translate([PHONE_W_MIN + (PHONE_W_MAX - PHONE_W_MIN)/2, + SLOT_NECK_H + BASE_FACE_T + ARM_REACH, 80]) + sliding_jaw(); + + // Grip pads on both jaws + color("DimGray", 0.85) { + translate([0, SLOT_NECK_H + BASE_FACE_T + ARM_REACH, 80]) + translate([JAW_WALL_T, JAW_BODY_T, JAW_BODY_H/2]) + rotate([90, 0, 0]) + grip_pad(); + translate([PHONE_W_MIN + (PHONE_W_MAX - PHONE_W_MIN)/2, + SLOT_NECK_H + BASE_FACE_T + ARM_REACH, 80]) + translate([-JAW_WALL_T - PAD_T, JAW_BODY_T, JAW_BODY_H/2]) + rotate([90, 0, 180]) + grip_pad(); + } + + // Cam lever — shown in locked (clamped) position + color("OrangeRed", 0.85) + translate([ARM_SPAN/2 + 6, + SLOT_NECK_H + BASE_FACE_T + ARM_REACH + GUIDE_BOSS_D/2, + 80 + JAW_BODY_H/2]) + rotate([0, 0, 0]) + cam_lever(); +} + +// ============================================================ +// PART 1 — T-NUT BASE +// ============================================================ +// Standard 2020 T-slot rail attachment base. +// Identical interface to sensor_rail_brackets.scad universal_tnut_base(). +// Arm extends in +Y; rail clamp bolt in -Y face. +// +// Print flat (face plate down), PETG, 5 perims, 60 % infill. +module tnut_base() { + difference() { + union() { + // Face plate (flush against rail outer face) + translate([-BASE_FACE_W/2, -BASE_FACE_T, 0]) + cube([BASE_FACE_W, BASE_FACE_T, BASE_FACE_H]); + + // T-nut neck (enters rail slot) + translate([-TNUT_W/2, 0, (BASE_FACE_H - TNUT_L)/2]) + cube([TNUT_W, SLOT_NECK_H + e, TNUT_L]); + + // T-nut 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]); + + // Arm stub (face plate → jaw) + translate([-BASE_FACE_W/2, -BASE_FACE_T, 0]) + cube([BASE_FACE_W, BASE_FACE_T + ARM_REACH, ARM_T]); + } + + // M3 rail clamp bolt 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) + 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 for arm-to-jaw bolting + for (bx = [-10, 10]) + translate([bx, ARM_REACH - BASE_FACE_T - e, ARM_T/2]) + rotate([-90, 0, 0]) + cylinder(d = M3_D, h = 8 + 2*e); + + // Lightening slot in arm + translate([0, -BASE_FACE_T/2 + ARM_REACH/2, ARM_T/2]) + cube([BASE_FACE_W - 12, ARM_REACH - 16, ARM_T + 2*e], + center = true); + } +} + +// ============================================================ +// PART 2 — FIXED JAW +// ============================================================ +// Fixed lower jaw of the clamping system. Phone sits in the pocket +// formed by the fixed jaw (bottom) and sliding jaw (top). +// Two guide bosses on the right wall carry the M4 guide rod + spring. +// The cam lever pivot boss is on the outer right face. +// +// Coordinate origin: centre-bottom of jaw body. +// Phone entry face: +Y (open front), phone pocket opens toward +Y. +// Fixed jaw left edge is at X = -JAW_BODY_W/2. +// +// Print jaw-pocket-face down, PETG, 5 perims, 40 % infill. +module fixed_jaw() { + difference() { + union() { + // ── Main jaw body ──────────────────────────────────────────── + translate([-JAW_BODY_W/2, -JAW_BODY_T/2, 0]) + cube([JAW_BODY_W, JAW_BODY_T, JAW_BODY_H]); + + // ── Front retaining lip (keeps phone from falling forward) ─── + translate([-JAW_BODY_W/2, JAW_BODY_T/2 - JAW_LIP_T, 0]) + cube([JAW_BODY_W, JAW_LIP_T, JAW_LIP_H]); + + // ── Guide boss right (outer, carries spring + end-stop) ────── + translate([JAW_BODY_W/2, 0, JAW_BODY_H/2]) + rotate([0, 90, 0]) + cylinder(d = GUIDE_BOSS_D, h = GUIDE_BOSS_T); + + // ── Cam lever pivot boss (right face, above guide boss) ────── + translate([JAW_BODY_W/2, 0, JAW_BODY_H/2 + GUIDE_BOSS_D + 4]) + rotate([0, 90, 0]) + cylinder(d = CAM_THICK + 4, h = 6); + + // ── Arm attachment bosses (left side, connect to tnut_base) ── + for (bx = [-10, 10]) + translate([bx, -JAW_BODY_T/2 - 8, ARM_T/2]) + cylinder(d = 8, h = 8); + } + + // ── Phone pocket (open-top U channel centred in jaw) ──────────── + // Pocket opens toward +Y (front), phone drops in from above. + translate([0, -JAW_BODY_T/2 - e, + JAW_LIP_H]) + cube([JAW_BODY_W - 2*JAW_WALL_T, + PHONE_POCKET_D + JAW_WALL_T, + JAW_BODY_H - JAW_LIP_H + e], + center = [true, false, false]); + + // ── Guide rod bore (M4 clearance, through both guide bosses) ──── + translate([-JAW_BODY_W/2 - e, 0, JAW_BODY_H/2]) + rotate([0, 90, 0]) + cylinder(d = GUIDE_ROD_D, + h = JAW_BODY_W + GUIDE_BOSS_T + 2*e); + + // ── Spring pocket (coaxial with guide rod, in right boss) ──────── + translate([JAW_BODY_W/2 + e, 0, JAW_BODY_H/2]) + rotate([0, -90, 0]) + cylinder(d = SPRING_OD, h = SPRING_L); + + // ── M4 hex nut pocket in spring-seat wall (end-stop nut) ──────── + translate([JAW_BODY_W/2 + GUIDE_BOSS_T + e, 0, JAW_BODY_H/2]) + rotate([0, -90, 0]) + cylinder(d = M4_NUT_AF / cos(30), h = M4_NUT_H + 0.5, + $fn = 6); + + // ── Cam pivot bore (M4 pivot, through pivot boss) ──────────────── + translate([JAW_BODY_W/2 - e, 0, JAW_BODY_H/2 + GUIDE_BOSS_D + 4]) + rotate([0, 90, 0]) + cylinder(d = CAM_BORE_D, h = 6 + 2*e); + + // ── Arm attachment bolt holes (M3, to tnut_base arm stubs) ────── + for (bx = [-10, 10]) + translate([bx, -JAW_BODY_T/2 - 8 - e, ARM_T/2]) + rotate([-90, 0, 0]) + cylinder(d = M3_D, h = 12 + 2*e); + + // ── Grip pad seats (recessed Ø1.5 mm, 2 mm deep, optional) ────── + for (pz = [JAW_BODY_H * 0.3, JAW_BODY_H * 0.7]) + for (px = [-PAD_W/4, PAD_W/4]) + translate([px, -JAW_BODY_T/2 + PHONE_POCKET_D + 1, pz]) + rotate([-90, 0, 0]) + cylinder(d = M2_D, h = 10); + + // ── Lightening pockets (non-structural core removal) ───────────── + translate([0, 0, JAW_BODY_H/2]) + cube([JAW_BODY_W - 2*JAW_WALL_T - 4, + JAW_BODY_T - 2*JAW_WALL_T, + JAW_BODY_H - JAW_LIP_H - 4], + center = true); + } +} + +// ============================================================ +// PART 3 — SLIDING JAW +// ============================================================ +// Upper clamping jaw. Slides along the M4 guide rod. +// Spring pushes this jaw toward the phone (inward). +// M4 hex nut on the guide rod limits maximum travel (full open). +// Cam lever pushes on this jaw face to compress spring (release). +// +// Coordinate origin same convention as fixed_jaw() for assembly. +// Jaw slides in +X direction (away from fixed jaw left wall). +// +// Print jaw-pocket-face down, PETG, 5 perims, 40 % infill. +module sliding_jaw() { + difference() { + union() { + // ── Main jaw body ──────────────────────────────────────────── + translate([-JAW_WALL_T, -JAW_BODY_T/2, 0]) + cube([JAW_BODY_W/2 + JAW_WALL_T, JAW_BODY_T, JAW_BODY_H]); + + // ── Front retaining lip ────────────────────────────────────── + translate([-JAW_WALL_T, JAW_BODY_T/2 - JAW_LIP_T, 0]) + cube([JAW_BODY_W/2 + JAW_WALL_T, JAW_LIP_T, JAW_LIP_H]); + + // ── Guide boss (carries guide rod, spring butts against face) ─ + translate([-JAW_WALL_T - GUIDE_BOSS_T, 0, JAW_BODY_H/2]) + rotate([0, 90, 0]) + cylinder(d = GUIDE_BOSS_D, h = GUIDE_BOSS_T); + + // ── Cam follower ear (contacts cam lever) ──────────────────── + translate([-JAW_WALL_T - 2, 0, + JAW_BODY_H/2 + GUIDE_BOSS_D + 4]) + cube([4, CAM_THICK + 2, CAM_THICK + 2], center = true); + } + + // ── Phone pocket (inner face, contacts phone side) ─────────────── + translate([-JAW_WALL_T - e, -JAW_BODY_T/2 - e, JAW_LIP_H]) + cube([JAW_BODY_W/2 - JAW_WALL_T + e, + PHONE_POCKET_D + JAW_WALL_T + 2*e, + JAW_BODY_H - JAW_LIP_H + e]); + + // ── Guide rod bore (M4 clearance through boss + jaw wall) ──────── + translate([-JAW_WALL_T - GUIDE_BOSS_T - e, 0, JAW_BODY_H/2]) + rotate([0, 90, 0]) + cylinder(d = GUIDE_ROD_D, + h = GUIDE_BOSS_T + JAW_WALL_T + 2*e); + + // ── M4 nut pocket (end-stop nut, rear of guide boss) ──────────── + translate([-JAW_WALL_T - GUIDE_BOSS_T - e, 0, JAW_BODY_H/2]) + rotate([0, 90, 0]) + cylinder(d = M4_NUT_AF / cos(30), h = M4_NUT_H + 1, + $fn = 6); + + // ── Cam follower bore (M4 pivot passes through ear) ───────────── + translate([-JAW_WALL_T - 2 - e, 0, + JAW_BODY_H/2 + GUIDE_BOSS_D + 4]) + rotate([0, 90, 0]) + cylinder(d = CAM_BORE_D, h = 6 + 2*e); + + // ── Grip pad seats ─────────────────────────────────────────────── + for (pz = [JAW_BODY_H * 0.3, JAW_BODY_H * 0.7]) + for (px = [JAW_BODY_W/8]) + translate([px, -JAW_BODY_T/2 + PHONE_POCKET_D + 1, pz]) + rotate([-90, 0, 0]) + cylinder(d = M2_D, h = 10); + } +} + +// ============================================================ +// PART 4 — CAM LEVER (QUICK-RELEASE) +// ============================================================ +// Eccentric cam disc + integral handle lever. +// Rotates 90° on M4 pivot pin between CLAMPED and RELEASED states: +// CLAMPED : cam small radius (CAM_R_MIN) toward jaw → spring pushes jaw +// RELEASED : cam large radius (CAM_R_MAX) toward jaw → compresses spring +// by (CAM_R_MAX - CAM_R_MIN) = 4 mm, opening jaw +// +// Detent ball pocket (Ø3 mm) snaps into rail-dimple for each position. +// Handle points rearward (-Y) in clamped state for low profile. +// +// Print standing on cam edge (cam disc vertical), PETG, 5 perims, 40%. +module cam_lever() { + cam_offset = (CAM_R_MAX - CAM_R_MIN) / 2; // 2 mm eccentricity + union() { + difference() { + union() { + // ── Eccentric cam disc ─────────────────────────────────── + // Offset so pivot bore is eccentric to disc profile + translate([cam_offset, 0, 0]) + cylinder(r = CAM_R_MAX, h = CAM_THICK, center = true); + + // ── Lever handle arm ───────────────────────────────────── + hull() { + translate([cam_offset, 0, 0]) + cylinder(r = CAM_R_MAX, h = CAM_THICK, center = true); + translate([cam_offset + CAM_HANDLE_L, 0, 0]) + cylinder(r = CAM_HANDLE_W/2, + h = CAM_HANDLE_T, center = true); + } + } + + // ── M4 pivot bore (through cam centre) ─────────────────────── + cylinder(d = CAM_BORE_D, h = CAM_THICK + 2*e, center = true); + + // ── Detent pockets (2× Ø3 mm, at 0° and 90°) ──────────────── + // Pocket at 0° → clamped detent + translate([CAM_R_MAX - 2, 0, CAM_THICK/2 - 1.5]) + cylinder(d = CAM_DETENT_D + 0.2, h = 2); + // Pocket at 90° → released detent + translate([0, CAM_R_MAX - 2, CAM_THICK/2 - 1.5]) + cylinder(d = CAM_DETENT_D + 0.2, h = 2); + + // ── Lightening recesses on cam disc face ───────────────────── + for (a = [0, 60, 120, 180, 240, 300]) + translate([cam_offset + (CAM_R_MAX - 4) * cos(a), + (CAM_R_MAX - 4) * sin(a), 0]) + cylinder(d = 4, h = CAM_THICK + 2*e, center = true); + + // ── Handle grip grooves ────────────────────────────────────── + for (i = [0:4]) + translate([cam_offset + 20 + i * 5, 0, 0]) + rotate([90, 0, 0]) + cylinder(d = 2.5, h = CAM_HANDLE_W + 2*e, + center = true); + } + } +} + +// ============================================================ +// PART 5 — GRIP PAD (VIBRATION DAMPENING) +// ============================================================ +// Flat pad with transverse flexure ribs that press against phone side. +// The rib profile (thin PETG fins) provides compliance in Z (vertical) +// absorbing vibration transmitted through the bracket. +// Optional: print in TPU 95A for superior damping. +// M2 bolts or adhesive-backed foam tape attach pad to jaw pocket face. +// +// Pad face (+Y) contacts phone. Mounting face (-Y) bonds to jaw. +// Ribs run parallel to Z axis (vertical). +// +// Print flat (mounting face down), PETG or TPU 95A, 3 perims, 20%. +module grip_pad() { + union() { + // ── Base plate ─────────────────────────────────────────────────── + translate([-PAD_W/2, -PAD_T, -PAD_H/2]) + cube([PAD_W, PAD_T, PAD_H]); + + // ── Flexure ribs (transverse, dampening in Z) ──────────────────── + // RIB_COUNT ribs spaced RIB_PITCH apart, centred on pad + for (i = [0 : RIB_COUNT - 1]) { + rx = -PAD_W/2 + RIB_PITCH/2 + i * RIB_PITCH; + if (abs(rx) <= PAD_W/2 - RIB_W/2) // stay within pad + translate([rx, 0, 0]) + cube([RIB_W, RIB_H, PAD_H - 4], center = true); + } + + // ── Corner retention nubs (M2 boss for optional bolt-through) ──── + for (px = [-PAD_W/2 + 5, PAD_W/2 - 5]) + for (pz = [-PAD_H/2 + 5, PAD_H/2 - 5]) + translate([px, -PAD_T/2, pz]) + difference() { + cylinder(d = 5, h = PAD_T, center = true); + cylinder(d = M2_D, h = PAD_T + 2*e, center = true); + } + } +}