// Wardrowbe mirror: fixed mount for the Arducam 16MP USB stick camera.
// One printed part. Three pads screw straight onto the existing M2 standoffs.
// Camera points straight down through the round opening. Filleted web frame, no hinge.
//
// Render:  openscad -D 'part="print"' -o camera_mount.stl camera_mount.scad
// part = "print" (upside down, frame on the bed, no supports) | "preview" (in place)
part = "preview";
show_board = true;     // preview only: draw the camera board in place

// ---- Mirror side. Origin = standoff A (the lone one), -Y toward the other two.
standoff_pitch = 33;        // A to B (v1 fit test: 35 was 2 mm too long)
third_dx       = 26.5;      // C sideways from B, level with B
standoff_h     = 14.3;      // standoff top above the black bracket plate
screw_d        = 2.2;       // M2 screws into the 3.54 mm standoffs
cam_pos        = [22.5, -11.5];  // lens centre; opening centre measured at 24, -11.7. Kept left of the two spare standoffs at x~54.
cradle_gap     = 1.0;       // channel floor above the bracket; lens pokes 3 mm into the bowl

// ---- Camera board (your calipers; mic end at -X) --------------------------
cam_len = 62.2;  cam_w = 9.0;  cam_t = 1.57;
hs_t    = 3.74 - cam_t;                    // heatsink on the back, 3..38 mm from the mic end
lens_y  = 30.1;  lens_sz = 8.5;            // lens centre from mic end
rib_from_mic = [38, 42];                   // front-face gap behind the lens housing: floor rib goes here

// ---- Looks and print tuning ----------------------------------------------
wall = 1.6;  ledge = 1.2;  fit = 0.4;  slack = 0.6;  lip_t = 1.2;  plate_t = 3;
pad_d = 10;  pad_c_d = 12;  strut_w = 6.5;  strut_tip = 5;   // C gets a bigger pad: its slot is longer
r_out = 2.5;  r_in = 3.5;  r_plan = 2.5;  pocket_r = 2.0;
iso_pitch = 6.0;  iso_web = 1.5;  bar_w = 2.2;    // isogrid web + zigzag bars
tri_web = 1.2;                                      // web between the triangular wall pockets
finger_w = 4;  finger_slit = 0.8;                   // vertical spring fingers hang from the top rail
hook_lip = 0.5;
$fn = 64;

// ---- Derived --------------------------------------------------------------
cav_w = cam_w + fit;  cav_len = cam_len + slack + lip_t;  crad_w = cav_w + 2*wall;
top_z = standoff_h + plate_t;
H     = top_z - cradle_gap;                // channel height, floor to frame top
rail  = ledge + cam_t + hs_t + 1.3;        // low rail top, just above the heatsink
A = [0, 0];  B = [0, -standoff_pitch];  C = [third_dx, -standoff_pitch];
x0 = -lens_y - wall;  x1 = x0 + wall + cav_len;      // channel ends, local X (lens at 0)
mic = x0 + wall;                                     // local X of the board's mic end
far_y = cam_pos[1] - crad_w/2;                       // far wall outer face, world
tri_x = [-19, 17];                                   // triangle pockets run here (local X); mic block before, finger section after
finger_x = 22;                                       // local X of each spring finger centre
land1 = cam_pos[0] + 4.5;   land2 = cam_pos[0] + 15.5;   // Y-strut landings on the far wall, world X

module ycyl(d, h) rotate([90,0,0]) cylinder(d=d, h=h, center=true);
module rrect_y(xa, xb, za, zb, r, t) hull() for (x=[xa+r, xb-r], z=[za+r, zb-r]) translate([x, 0, z]) ycyl(2*r, t);
module closing(r) offset(r=-r) offset(r=r) children();
module opening(r) offset(r=r) offset(r=-r) children();
module seg(p, q, wp, wq) hull() { translate(p) circle(d=wp); translate(q) circle(d=wq); }

module channel_plan() translate([cam_pos[0], cam_pos[1]]) opening(r_plan) translate([x0, -crad_w/2]) square([x1 - x0, crad_w]);
module trough_plan() translate([cam_pos[0], cam_pos[1]]) translate([mic, -cav_w/2]) square([cav_len + 5, cav_w]);

module struts2d() opening(r_out) union() {
    for (p=[A, B]) translate(p) circle(d=pad_d);
    translate(C) circle(d=pad_c_d);
    seg(A, B, strut_w, strut_w);  seg(B, C, strut_w, strut_w);
    intersection() {
        union() { seg(C, [land1, far_y - 0.5], strut_w, strut_tip); seg(C, [land2, far_y - 0.5], strut_w, strut_tip); }
        translate([-50, -100]) square([200, 100 + far_y + wall]);
    }
}
module web2d() closing(r_in) union() { struts2d(); difference() { channel_plan(); trough_plan(); } }

// isogrid (triangular lattice) filling the web interior; struts sit on top of it
module lines3() translate([13, -25]) for (a=[0, 60, 120]) rotate(a) for (k=[-12:12]) translate([-100, k*iso_pitch - iso_web/2]) square([200, iso_web]);
module web_interior() polygon([[0, far_y], [0, -standoff_pitch], [C[0], -standoff_pitch], [land2, far_y]]);
module isogrid2d() union() { intersection() { web_interior(); lines3(); } difference() { web_interior(); offset(r=-1.5) web_interior(); } }
// zigzag truss across the open top of the channel, node to node on the two rail centrelines
yn = cam_pos[1] + crad_w/2 - wall/2;  yf = cam_pos[1] - crad_w/2 + wall/2;
nodes = [[cam_pos[0] - 19, yn], [cam_pos[0] - 8.4, yf], [cam_pos[0] + 3.5, yn], [land2, yf], [cam_pos[0] + 26, yn], [cam_pos[0] + 32.5, yf]];
module zigzag2d() for (i=[0:len(nodes)-2]) seg(nodes[i], nodes[i+1], bar_w, bar_w);

module frame() translate([0, 0, standoff_h]) {
    linear_extrude(plate_t - 1) web2d();
    linear_extrude(plate_t) difference() { channel_plan(); trough_plan(); }            // wall tops stay flat
    for (i=[0:4]) translate([0, 0, plate_t - 1 + 0.2*i]) linear_extrude(0.2) offset(r=0.25) offset(r=-0.2*(i + 1) - 0.25) web2d();   // no slivers under 0.5 mm in the chamfer steps
    linear_extrude(plate_t) { isogrid2d(); zigzag2d(); }
}

// alternating triangles between the rails, rounded, webs of tri_web between them
module tri_row() let(h = H - plate_t - rail, side = 2*h/sqrt(3), n = floor((tri_x[1] - tri_x[0]) / (side/2)))
    for (i=[0:n-1]) let(xc = tri_x[0] + side/2 + i*side/2)
        offset(r=0.5) offset(delta=-0.5 - tri_web/2)
            polygon(i%2==0 ? [[xc - side/2, rail], [xc + side/2, rail], [xc, rail + h]] : [[xc - side/2, rail + h], [xc + side/2, rail + h], [xc, rail]]);

// channel: floor at z 0, lens at origin, board along X, open at +X end
module channel() {
    nw = -crad_w/2;  fw = crad_w/2;
    difference() {
        union() {
            translate([-cam_pos[0], -cam_pos[1], 0.8]) linear_extrude(H - 0.8) channel_plan();
            for (i=[0:3]) translate([-cam_pos[0], -cam_pos[1], 0.2*i]) linear_extrude(0.2) offset(r=-0.8 + 0.2*i) channel_plan();
        }
        translate([mic, nw + wall, ledge]) cube([cav_len + 1, cav_w, H]);                              // trough
        translate([mic, nw + wall + ledge, -1]) cube([cav_len + 1, cav_w - 2*ledge, ledge + 2]);       // front window
        // lens window runs from the lens back to the floor rib: the lens rides the ledges on the way in and must drop
        // in before the board can get under the mic-end lip (v6 jammed: heatsink hit the lip with the board still lifted)
        translate([-(lens_sz + 1.5)/2, nw + wall, -1]) cube([(lens_sz + 1.5)/2 + mic + rib_from_mic[0], cav_w, ledge + 2]);
        for (yy=[nw + wall/2, fw - wall/2]) translate([0, yy, 0]) rotate([90, 0, 0]) linear_extrude(wall + 1, center=true) tri_row();
        // spring fingers: vertical strips of wall hanging from the top rail, hook at the free lower end.
        // Print upside down they stand on the bed, so no overhang anywhere.
        for (yw=[nw, fw - wall]) {
            for (sx=[-1, 1]) translate([finger_x + sx*(finger_w/2 + finger_slit/2) - finger_slit/2, yw - 0.01, ledge + 0.6]) cube([finger_slit, wall + 0.02, H - plate_t - ledge - 0.6]);
            translate([finger_x - finger_w/2 - finger_slit, yw - 0.01, ledge + 0.6]) cube([finger_w + 2*finger_slit, wall + 0.02, 0.6]);
        }
    }
    // floor rib between the ledges, in the component-free zone behind the lens
    translate([mic + rib_from_mic[0], -crad_w/2, 0]) cube([rib_from_mic[1] - rib_from_mic[0], crad_w, ledge]);   // 9.4 mm bridge in the print; slicer calls it a cantilever, it is fine
    // mic-end lip: short shelf over the bare PCB end (heatsink starts 3 mm in). Board slides under it flat,
    // 0.3 mm over the PCB; sloped top so it prints upside down without support.
    hull() {
        translate([mic, -cav_w/2, ledge + cam_t + 0.3]) cube([0.01, cav_w, 2.5]);
        translate([mic + 1.5, -cav_w/2, ledge + cam_t + 0.3]) cube([0.01, cav_w, 0.01]);
    }
    // hooks on both tongues: flat face holds the board, ramp on top
    for (s=[-1, 1]) translate([finger_x, s*cav_w/2, ledge + cam_t + 0.15]) rotate([0, 0, s > 0 ? -90 : 90]) hull() {
        translate([0, -1.5, 0]) cube([hook_lip, 3, 0.01]);
        translate([0, -1.5, 0]) cube([0.01, 3, 1.6]);
    }
    // end lips beyond the board: corners sit behind them, cable passes between
    for (s=[-1,1]) translate([x1 - lip_t, s*cav_w/2 - (s>0 ? 1.5 : 0), ledge]) cube([lip_t, 1.5, cam_t]);
}

module screw_slots() {
    for (p=[A, B]) translate([p[0], p[1], standoff_h - 1]) hull() for (s=[-1,1]) translate([0, s*2, 0]) cylinder(d=screw_d + 0.4, h=10);
    translate([C[0], C[1], standoff_h - 1]) hull() for (sx=[-1,1], sy=[-1,1]) translate([sx*2.5, sy*1.0, 0]) cylinder(d=screw_d + 0.4, h=10);
}
module part_() difference() {
    union() { translate([cam_pos[0], cam_pos[1], cradle_gap]) channel(); frame(); }
    screw_slots();
}

if (part == "print") translate([0, 0, top_z]) rotate([180, 0, 0]) part_();
else {
    part_();
    %translate([-20, -60, -1]) cube([100, 80, 1]);
    %translate([24, -11.7, -1.01]) cylinder(d=24, h=1.02);
    %for (p=[A, B, C, [54.5, 0], [54, -28.6]]) translate(p) cylinder(d=3.54, h=standoff_h);
    if (show_board) translate([cam_pos[0], cam_pos[1], cradle_gap + ledge]) {
        color("DarkGreen") translate([-lens_y, -cam_w/2, 0]) cube([cam_len, cam_w, cam_t]);
        color("Silver") translate([-lens_y + 3, -cam_w/2 + 0.2, cam_t]) cube([35, cam_w - 0.4, hs_t]);
        color("Black") translate([-lens_sz/2, -lens_sz/2, -4]) cube([lens_sz, lens_sz, 4]);
        color("White") translate([-lens_y + 57, -3, -2.5]) cube([5, 6, 2.5]);
    }
}
