// RemoteRig camera node case v4
// Upright appliance-style OpenSCAD prototype for a strap-mounted camera node.
// Units: millimeters. Coordinate system: X=width, Y=depth/front-back, Z=height.
// Front/service lid is on the -Y face. Rear side-feed zip-tie brackets are on the +Y face.
//
// v4 visual direction: tall/upright appliance/control box matching the original
// reference image, replacing the rejected wide, low generic electronics box.
// Nominal body: 56 W x 36 D x 82 H mm; with low rear zip-tie loops ~41 D.
//
// Prototype assumptions to confirm against purchased parts:
// - 1.3 inch OLED module/window opening: 31 x 16 mm visible window, 37 x 22 mm panel recess.
// - Small rocker switch cutout: 13 x 19 mm rectangular snap-in opening.
// - LEDs: two 3 mm panel LEDs (PWR + RGB STAT) with 3.2 mm holes.
// - Boards: ESP32-C3 Super Mini 22.5 x 18 mm, ESP-01S 24.7 x 14.3 x 12 mm.
// - USB-C bottom power inlet and side USB-A passthrough are panel/breakout placeholders;
//   measure purchased connector flanges/bodies before production prints.
// - Left-side IPEX/U.FL antenna pigtail connector/lead hole is a prototype 5.0 mm
//   circular through-wall clearance; measure the final bulkhead/lead before production.

$fn = 56;

// ----- Main enclosure parameters -----
case_w = 56;                 // upright appliance-style external width
case_d = 36;                 // depth for module stack + wiring clearance
case_h = 82;                 // tall vertical appliance-style height
wall = 2.2;
corner_r = 4.0;
front_recess_d = 2.0;        // lid sits in this front pocket, nominally flush
lid_clearance = 0.35;
lid_w = case_w - 8;          // nearly full-height/front-width inset panel
lid_h = case_h - 8;
lid_t = 2.0;
lid_lip_t = 1.2;             // locating lip protrudes inside service opening
service_opening_w = lid_w - 10.0;
service_opening_h = lid_h - 16.0;

// Hardware
screw_d = 2.4;               // M2 self-tapping / pilot; confirm hardware
screw_head_d = 4.6;
boss_d = 6.0;
boss_len = 8.0;

// Front panel components
oled_window_w = 31.0;
oled_window_h = 16.0;
oled_bezel_w = 37.0;         // shallow recessed visual outline around window
oled_bezel_h = 22.0;
oled_z = 53.0;               // upper third, clear of top screw counterbores
led_hole_d = 3.2;            // 3 mm LED clearance
rocker_w = 13.0;             // prototype cutout; measure purchased rocker
rocker_h = 19.0;
front_slot_w = 34.0;         // two long rounded horizontal slots near lower front
front_slot_h = 3.2;

// Rear reusable cloth zip-tie / Velcro pass-through brackets.
// Two visibly vertical external brackets sit left/right of center.
// The strap path is a lateral X-direction tunnel between the sealed rear wall
// and raised bridge face; long side windows stay open for feeding from either side.
rear_loop_x = 13.0;
rear_loop_w = 8.5;            // outside bracket width in X
rear_loop_h = 50.0;           // outside bracket height in Z
rear_loop_z = case_h/2;
rear_loop_gap_y = 3.8;        // usable strap-thickness clearance behind raised bridge
rear_loop_face_t = 1.4;       // low-profile outer bridge skin
rear_loop_y = rear_loop_gap_y + rear_loop_face_t;
rear_loop_anchor_h = 5.0;     // top/bottom weld pads; side window remains long vertically
rear_loop_side_window_h = rear_loop_h - 2*rear_loop_anchor_h;

// USB power connector placeholder cutouts
usb_c_cutout_w = 10.5;        // bottom USB-C female inlet visible opening, X width
usb_c_cutout_d = 4.5;         // bottom USB-C female inlet visible opening, Y/front-back
usb_c_recess_w = 18.0;        // shallow underside panel-mount/breakout recess
usb_c_recess_d = 10.0;
usb_c_y = -7.5;               // close to front/service side but clear of screw bosses/lower slots
usb_a_cutout_d = 16.0;        // side USB-A female opening, Y/front-back dimension
usb_a_cutout_h = 8.0;         // side USB-A female opening, Z height
usb_a_z = 26.0;               // mid/lower right side, clear of front lid screws/strap bridges
usb_a_y = 2.0;

// Left-side antenna lead / IPEX-U.FL pigtail connector placeholder.
// Opposite the right-side USB-A port and cut fully through the left wall into the cavity.
ipex_hole_d = 5.0;            // prototype circular clearance; measure final pigtail/bulkhead
ipex_recess_d = 8.5;          // shallow exterior visual/seat recess, not retention geometry
ipex_recess_depth = 0.9;
ipex_z = usb_a_z;
ipex_y = usb_a_y;

// ----- Utility geometry -----
module rounded_box(size=[10,10,10], r=2, center_xy=true) {
    // Rounded in XY, straight in Z.
    linear_extrude(height=size[2])
        offset(r=r)
            square([size[0]-2*r, size[1]-2*r], center=center_xy);
}

module xz_rounded_prism(w, d, h, r=2) {
    // Rounded rectangle in the visible X/Z plane, extruded through Y.
    rotate([-90,0,0])
        linear_extrude(height=d, center=true)
            offset(r=r)
                square([w-2*r, h-2*r], center=true);
}

module yz_rounded_prism(d, x, h, r=2) {
    // Rounded rectangle in the visible Y/Z plane, extruded through X.
    // First argument maps to global Y, third argument maps to global Z.
    rotate([0,90,0])
        linear_extrude(height=x, center=true)
            offset(r=r)
                square([h-2*r, d-2*r], center=true);
}

module y_cylinder(d, h, center=true) {
    rotate([90,0,0]) cylinder(d=d, h=h, center=center);
}

module x_cylinder(d, h, center=true) {
    rotate([0,90,0]) cylinder(d=d, h=h, center=center);
}

module screw_boss(x, z) {
    translate([x, -case_d/2 + front_recess_d + boss_len/2, z])
        difference() {
            y_cylinder(d=boss_d, h=boss_len);
            y_cylinder(d=screw_d, h=boss_len + 0.8);
        }
}

module rear_zip_tie_loop(xc) {
    // Vertical external belt-loop bracket for reusable cloth zip ties/Velcro.
    // The bracket silhouette remains vertical, but the real strap tunnel runs
    // laterally in X through the long side windows, behind the raised bridge face.
    // Top/bottom pads weld the bridge to the shell; no cut reaches the rear wall.
    loop_overlap_y = 0.75;
    pad_r = 1.15;
    bridge_y_center = case_d/2 + rear_loop_gap_y + rear_loop_face_t/2;
    pad_y_center = case_d/2 + rear_loop_y/2 - loop_overlap_y;
    pad_z_offset = rear_loop_h/2 - rear_loop_anchor_h/2;

    union() {
        // Raised vertical bridge face: visually preserves the requested vertical
        // rear brackets while spanning the side-feed tunnel externally.
        translate([xc, bridge_y_center, rear_loop_z])
            xz_rounded_prism(rear_loop_w, rear_loop_face_t, rear_loop_h, r=1.6);

        // Top and bottom anchor pads close the old top-to-bottom feed direction
        // and tie the raised face back into the rear wall without opening the case.
        for (zoff = [-pad_z_offset, pad_z_offset])
            translate([xc, pad_y_center, rear_loop_z + zoff])
                xz_rounded_prism(rear_loop_w, rear_loop_y, rear_loop_anchor_h, r=pad_r);
    }
}

// ----- Printable body -----
module camera_node_body_v4() {
    difference() {
        union() {
            difference() {
                union() {
                    // Upright outer shell with softened appliance-like corners.
                    rounded_box([case_w, case_d, case_h], r=corner_r);

                    // Rear cloth zip-tie / Velcro side-feed brackets kept flat/quiet.
                    rear_zip_tie_loop(-rear_loop_x);
                    rear_zip_tie_loop( rear_loop_x);
                }

                // Full-height front recessed lid pocket, like the green reference panel.
                translate([0, -case_d/2 + front_recess_d/2, case_h/2])
                    cube([lid_w + lid_clearance, front_recess_d + 0.4, lid_h + lid_clearance], center=true);

                // Through service opening behind the lid, leaving a strong inset frame.
                service_depth = front_recess_d + wall + 2.0;
                translate([0, -case_d/2 + service_depth/2, case_h/2])
                    xz_rounded_prism(service_opening_w, service_depth + 0.4, service_opening_h, r=2.0);

                // Interior electronics cavity: ESP32-C3 Super Mini + ESP-01S plus wiring/service clearance.
                cavity_d = case_d - front_recess_d - 2*wall;
                translate([0, -case_d/2 + front_recess_d + wall + cavity_d/2, case_h/2])
                    cube([case_w - 2*wall, cavity_d, case_h - 2*wall], center=true);

                // Bottom USB-C female power inlet: shallow underside recess plus
                // rounded through-slot for a flush/panel-mount breakout placeholder.
                translate([0, usb_c_y, -0.35])
                    rounded_box([usb_c_recess_w, usb_c_recess_d, 0.9], r=1.5);
                translate([0, usb_c_y, -0.2])
                    rounded_box([usb_c_cutout_w, usb_c_cutout_d, wall + 1.2], r=1.6);

                // Right-side USB-A female passthrough power port for the GoPro.
                translate([case_w/2 - 0.10, usb_a_y, usb_a_z])
                    yz_rounded_prism(usb_a_cutout_d, wall + 2.8, usb_a_cutout_h, r=0.9);

                // Left-side IPEX/U.FL antenna pigtail connector/lead clearance.
                // Through-hole intentionally extends past the inner wall so it opens to the cavity.
                translate([-case_w/2 - 0.10, ipex_y, ipex_z])
                    x_cylinder(d=ipex_hole_d, h=wall + 3.0);
                // Shallow exterior circular recess marks/relieves the antenna exit area.
                translate([-case_w/2 + ipex_recess_depth/2 - 0.05, ipex_y, ipex_z])
                    x_cylinder(d=ipex_recess_d, h=ipex_recess_depth + 0.2);
            }

            // Four protected screw bosses are added after shell hollowing so the
            // electronics cavity cannot cut away the receiving material.
            screw_x = lid_w/2 - 5.0;
            screw_z_low = (case_h - lid_h)/2 + 5.0;
            screw_z_high = case_h - screw_z_low;
            screw_boss(-screw_x, screw_z_low);
            screw_boss( screw_x, screw_z_low);
            screw_boss(-screw_x, screw_z_high);
            screw_boss( screw_x, screw_z_high);
        }

        // Final body-level pilot holes cut through the front frame into the protected bosses.
        screw_x = lid_w/2 - 5.0;
        screw_z_low = (case_h - lid_h)/2 + 5.0;
        screw_z_high = case_h - screw_z_low;
        for (x=[-screw_x, screw_x], z=[screw_z_low, screw_z_high])
            translate([x, -case_d/2 + front_recess_d + boss_len/2, z])
                y_cylinder(d=screw_d, h=boss_len + front_recess_d + 4.0);
    }
}

// ----- Printable front service lid / status panel -----
module camera_node_lid_v4() {
    panel_through_d = lid_t + lid_lip_t + 2.4;
    panel_through_y = 0.25;

    difference() {
        union() {
            // Visible full-height flush panel; restrained and not a busy slab.
            rounded_box([lid_w, lid_t, lid_h], r=0.65);

            // Rear locating lip fits inside the large service opening.
            translate([0, lid_t/2 + lid_lip_t/2 - 0.2, lid_h/2])
                xz_rounded_prism(service_opening_w - 0.8, lid_lip_t, service_opening_h - 0.8, r=1.5);
        }

        // OLED window and shallow black-bezel-style recess near the top.
        translate([0, -lid_t/2 + 0.35, oled_z])
            xz_rounded_prism(oled_bezel_w, 0.9, oled_bezel_h, r=1.3);
        translate([0, panel_through_y, oled_z])
            xz_rounded_prism(oled_window_w, panel_through_d, oled_window_h, r=0.5);

        // Subtle secondary indicators flanking the rocker, below the OLED bezel.
        translate([-15.0, panel_through_y, 33.0]) y_cylinder(d=led_hole_d, h=panel_through_d);
        translate([ 15.0, panel_through_y, 33.0]) y_cylinder(d=led_hole_d, h=panel_through_d);

        // Small rocker lower on the panel, offset away from the OLED, screws, and slots.
        translate([0, panel_through_y, 33.0])
            xz_rounded_prism(rocker_w, panel_through_d, rocker_h, r=0.8);

        // Two long rounded horizontal slots near the lower front, matching the reference.
        translate([0, panel_through_y, 17.0])
            xz_rounded_prism(front_slot_w, panel_through_d, front_slot_h, r=front_slot_h/2 - 0.15);
        translate([0, panel_through_y, 11.0])
            xz_rounded_prism(front_slot_w, panel_through_d, front_slot_h, r=front_slot_h/2 - 0.15);

        // Screw clearance/counterbore holes.
        screw_x = lid_w/2 - 5.0;
        screw_z_low = 5.0;
        screw_z_high = lid_h - screw_z_low;
        for (x=[-screw_x, screw_x], z=[screw_z_low, screw_z_high]) {
            translate([x, panel_through_y, z]) y_cylinder(d=screw_d + 0.4, h=panel_through_d);
            translate([x, -lid_t/2 + 0.55, z]) y_cylinder(d=screw_head_d, h=1.3);
        }
    }
}

// ----- Non-print preview assembly -----
module camera_node_preview_v4(show_lid=true) {
    color("lightgray") camera_node_body_v4();
    if (show_lid)
        translate([0, -case_d/2 + lid_t/2 + 0.03, (case_h - lid_h)/2])
            color("gainsboro") camera_node_lid_v4();

    // Dark OLED bezel/window cue for visual review only (not part of exported lid STL when rendering lid).
    if (show_lid)
        translate([0, -case_d/2 - 0.08, (case_h - lid_h)/2 + oled_z])
            color("black") xz_rounded_prism(oled_bezel_w, 0.6, oled_bezel_h, r=1.3);

    // Internal board/connector volume guides (not printed): ESP modules and USB connector envelopes.
    color([0,0.45,0,0.35]) translate([-9, -1, 26]) cube([22.5, 18, 4], center=true);
    color([0,0.2,0.8,0.35]) translate([9, -1, 45]) cube([24.7, 14.3, 12], center=true);
    color([0.1,0.1,0.1,0.35]) translate([0, usb_c_y, 3.8]) cube([16, 9, 5], center=true);
    color([0.1,0.1,0.1,0.35]) translate([case_w/2 - 5.5, usb_a_y, usb_a_z]) cube([11, usb_a_cutout_d + 2, usb_a_cutout_h + 2], center=true);
    color([0.9,0.7,0.1,0.45]) translate([-case_w/2 - 1.8, ipex_y, ipex_z]) x_cylinder(d=ipex_hole_d, h=8.0);
}

// Non-print review layout: separates the body and front lid while keeping both
// front faces oriented toward -Y. Use this STL when checking that the screen,
// LED, rocker, USB connector, and lower-slot cutouts are visible in a slicer.
module camera_node_front_review_v4() {
    translate([-34, 0, 0]) rotate([0,0,-18]) color("lightgray") camera_node_body_v4();
    translate([34, -case_d/2 + lid_t/2 + 0.03, (case_h - lid_h)/2])
        color("gainsboro") camera_node_lid_v4();
}

// Select part to render from OpenSCAD CLI with: -D 'part="body"'
part = "preview"; // "body", "lid", "preview", or "front_review"

if (part == "body") {
    camera_node_body_v4();
} else if (part == "lid") {
    camera_node_lid_v4();
} else if (part == "front_review") {
    camera_node_front_review_v4();
} else {
    camera_node_preview_v4();
}
