// ============================================================
// uwb_tag_enclosure.scad — Wearable UWB Tag  Rev B  2026-03-01
// Agent: sl-mechanical
// Closes issues #57, #61
// ============================================================
// Belt-clip enclosure for "follow-me" UWB tag worn by operator.
//
// Components:
//   MaUWB ESP32-S3 module    ~50 × 25 × 10 mm PCB
//   TP4056 charger board     ~25 × 19 mm,  micro-USB port
//   18650 Li-ion cell        Ø18 × 65 mm
//
// ⚠  VERIFY WITH CALIPERS BEFORE PRINTING:
//    MAWB_L, MAWB_W, MAWB_H  — UWB module
//    TP_L, TP_W               — TP4056 board
//    TP_USB_W, TP_USB_H       — micro-USB receptacle
//    SW_W, SW_H               — power switch body
//
// Key design decisions:
//   • UWB antenna window (open rectangle) in LID — no material
//     within 10 mm of antenna area; antenna faces up
//   • IP44-ish: lid overlaps body 4 mm + 2-turn labyrinth seam;
//     all ports on side faces, NO holes in top face
//   • TPU bumper sleeve snaps around body bottom edge (drop protection)
//   • Micro-USB on short face (X−), power switch on long face (Y−)
//   • Belt clip integrated on back face (Y−)
//
// Internal layout (cross-section view from top):
//
//   Y+  ┌──────────────────────────────────────────┐
//       │  [   MaUWB PCB  50×25mm flat   ]         │
//       │  [TP4056] ──── [  18650 cell  ] ──────   │
//   Y−  └──────────────────────────────────────────┘
//       X−                                         X+
//
// RENDER options:
//   "assembly"    closed box (default)
//   "body"        lower shell for slicing
//   "lid"         snap-fit lid for slicing
//   "tpu_bumper"  TPU edge sleeve for slicing (print in TPU 95A)
//   "body_2d"     bottom projection → DXF
// ============================================================

RENDER = "assembly";

// ── ⚠ Verify-before-print ────────────────────────────────────
// MaUWB ESP32-S3
MAWB_L      = 50.0;   // PCB length (X)
MAWB_W      = 25.0;   // PCB width  (Y)
MAWB_H      = 10.0;   // PCB + tallest component (Z)
MAWB_HOLE_X = 43.0;   // M2 mounting hole spacing X (verify)
MAWB_HOLE_Y = 20.0;   // M2 mounting hole spacing Y (verify)
M2_D        =  2.2;   // M2 clearance
M2_STNDFF   =  3.0;   // standoff height (PCB proud of tray floor)
M2_STNDFF_OD=  4.5;   // standoff post OD

// TP4056 charger
TP_L        = 25.0;   // PCB length (X, at X− end)
TP_W        = 19.0;   // PCB width  (Y)
TP_H        =  5.0;   // PCB + components (Z)
TP_USB_W    =  8.0;   // micro-USB receptacle width
TP_USB_H    =  3.5;   // micro-USB receptacle height

// Power switch (slide or toggle, 13×7 mm body)
SW_W        = 13.0;   // switch body length (X)
SW_H        =  7.0;   // switch body height (Z)
SW_BEZEL_W  =  8.0;   // switch cutout width in wall
SW_BEZEL_H  =  5.0;   // switch cutout height in wall
SW_Y_POS    =  6.0;   // distance from bottom of internal space (Z)

// 18650 cell
BATT_OD     = 18.6;   // diameter + clearance
BATT_L      = 65.5;   // length + clearance

// ── Enclosure shell ──────────────────────────────────────────
WALL        =  2.2;   // shell wall thickness
FLOOR       =  2.5;   // floor thickness (bottom face printed)
LID_H       =  3.0;   // lid panel thickness
LID_OVLP    =  4.0;   // lid rim overlap depth (IP44 seal)
LID_LBYRTH  =  1.0;   // labyrinth ridge height
LID_LBYRTH_W=  1.2;   // labyrinth ridge width

// Snap clips (×4, one per side)
SNAP_W      =  8.0;
SNAP_T      =  1.5;
SNAP_H      =  1.8;

// ── Internal component layout ────────────────────────────────
// Battery runs in X at Y = INT_W/2 − BATT_OD/2 (left/bottom of bay)
// TP4056 at X− end, beside battery in Y
// MaUWB PCB flat above battery and TP4056 (stacked)

// Z stack:
//   Z=0                    body floor
//   Z=FLOOR                interior floor
//   Z=FLOOR..FLOOR+BATT_OD battery (18 mm dia)
//   Z=FLOOR+BATT_OD+1      PCB tray floor (1 mm above battery)
//   Z=PCB_TRAY+M2_STNDFF   PCB bottom face
//   Z=PCB_TRAY+M2_STNDFF+MAWB_H  PCB top
//   + 2 mm clearance + LID
PCB_TRAY_Z  = FLOOR + BATT_OD + 1.5;
PCB_BOT_Z   = PCB_TRAY_Z + M2_STNDFF;
PCB_TOP_Z   = PCB_BOT_Z + MAWB_H;

// Internal dimensions
INT_L       = BATT_L + 5.0;             // 70.5 mm (battery + wiring room)
INT_W       = BATT_OD + MAWB_W - 10;   // ~33 mm (battery + PCB overlap in Y)
INT_H       = PCB_TOP_Z - FLOOR + 2.5; // ~31 mm

// External dimensions
EXT_L       = INT_L + 2 * WALL;
EXT_W       = INT_W + 2 * WALL;
EXT_H       = INT_H + FLOOR;

// ── Belt clip ─────────────────────────────────────────────────
CLIP_H      = 58.0;   // total clip height
CLIP_GAP    =  5.5;   // belt slot gap (fits 5 mm thick belt)
CLIP_BELT_W = 42.0;   // belt slot width
CLIP_BACK_T =  4.0;   // clip back-plate thickness
CLIP_FLEX_T =  2.0;   // spring arm thickness

// ── TPU bumper sleeve ─────────────────────────────────────────
TPU_T       =  4.0;   // bumper wall thickness
TPU_H       = 12.0;   // bumper height (covers bottom edge)
TPU_CR      =  5.0;   // outer corner radius

$fn = 48;
e   = 0.01;

// ─────────────────────────────────────────────────────────────
module rounded_box(l, w, h, r = 3.5) {
    hull()
        for (x=[r, l-r], y=[r, w-r])
            translate([x, y, 0])
                cylinder(r=r, h=h);
}

// ─────────────────────────────────────────────────────────────
// enclosure_body()   Print: floor on build plate, no supports.
// ─────────────────────────────────────────────────────────────
module enclosure_body() {
    difference() {
        union() {
            // ── Outer shell ──────────────────────────────────
            rounded_box(EXT_L, EXT_W, EXT_H, r = 3.5);

            // ── IP44 labyrinth ridge on top rim ──────────────
            // Inner ridge that mates with lid groove
            translate([WALL - LID_LBYRTH_W, WALL - LID_LBYRTH_W,
                       EXT_H - LID_LBYRTH])
                difference() {
                    rounded_box(INT_L + 2*LID_LBYRTH_W,
                                INT_W + 2*LID_LBYRTH_W,
                                LID_LBYRTH + e, r = 2.5);
                    translate([LID_LBYRTH_W, LID_LBYRTH_W, -e])
                        cube([INT_L, INT_W, LID_LBYRTH + 2*e]);
                }

            // ── M2 standoffs for UWB PCB ─────────────────────
            bx = WALL + (INT_L - MAWB_L)/2;
            by = WALL + (INT_W - MAWB_W)/2;
            for (sx=[0, MAWB_HOLE_X], sy=[0, MAWB_HOLE_Y])
                translate([bx + (MAWB_L - MAWB_HOLE_X)/2 + sx,
                           by + (MAWB_W - MAWB_HOLE_Y)/2 + sy,
                           0])
                    cylinder(d=M2_STNDFF_OD, h=PCB_TRAY_Z + M2_STNDFF);
        }

        // ── Interior cavity ───────────────────────────────────
        translate([WALL, WALL, FLOOR])
            cube([INT_L, INT_W, INT_H + e]);

        // ── 18650 battery cradle ──────────────────────────────
        // Semi-cylindrical channel in floor, Y− side of interior
        hull() {
            translate([WALL + 3, WALL + BATT_OD/2, FLOOR])
                rotate([-90, 0, 90])
                    cylinder(d=BATT_OD, h=BATT_L - 3);
        }
        // Positive end spring hole
        translate([WALL + INT_L - 2, WALL + BATT_OD/2, FLOOR])
            cylinder(d=8, h=FLOOR + e);

        // ── Micro-USB cutout (X− face) ────────────────────────
        translate([-e, WALL + (INT_W - TP_USB_W)/2,
                   FLOOR + BATT_OD/2 - TP_USB_H/2 + 1])
            cube([WALL + 2*e, TP_USB_W, TP_USB_H]);

        // ── Power switch cutout (Y− face) ─────────────────────
        translate([WALL + (INT_L - SW_W)/2, -e,
                   FLOOR + SW_Y_POS])
            cube([SW_BEZEL_W, WALL + 2*e, SW_BEZEL_H]);

        // ── Status LED window (Y+ face, small hole) ───────────
        // TP4056 charge LED and ESP32 LED visible from side
        translate([WALL + TP_L / 2, EXT_W - WALL - e,
                   FLOOR + BATT_OD + 2])
            rotate([90, 0, 0])
                cylinder(d=2.5, h=WALL + 2*e);

        // ── M2 standoff bores ─────────────────────────────────
        bx = WALL + (INT_L - MAWB_L)/2;
        by = WALL + (INT_W - MAWB_W)/2;
        for (sx=[0, MAWB_HOLE_X], sy=[0, MAWB_HOLE_Y])
            translate([bx + (MAWB_L - MAWB_HOLE_X)/2 + sx,
                       by + (MAWB_W - MAWB_HOLE_Y)/2 + sy,
                       -e])
                cylinder(d=M2_D, h=PCB_TRAY_Z + M2_STNDFF + e);

        // ── Lid snap recesses on body exterior rim ────────────
        // Front and rear pair
        for (ys=[WALL/2, EXT_W - WALL/2 - SNAP_T]) {
            translate([EXT_L/2 - SNAP_W/2, ys,
                       EXT_H - SNAP_H - 0.2])
                cube([SNAP_W, SNAP_T + e, SNAP_H + 0.2]);
        }
        // Left and right pair
        for (xs=[WALL/2, EXT_L - WALL/2 - SNAP_T]) {
            translate([xs, EXT_W/2 - SNAP_W/2,
                       EXT_H - SNAP_H - 0.2])
                cube([SNAP_T + e, SNAP_W, SNAP_H + 0.2]);
        }
    }
}

// ─────────────────────────────────────────────────────────────
// enclosure_lid()   Print: inside face down.
// ─────────────────────────────────────────────────────────────
module enclosure_lid() {
    tol = 0.3;
    lid_l = EXT_L + tol;
    lid_w = EXT_W + tol;

    difference() {
        union() {
            // Lid panel
            rounded_box(lid_l, lid_w, LID_H, r = 3.5);

            // Rim that drops over body (IP44 labyrinth)
            translate([0, 0, -LID_OVLP])
                difference() {
                    rounded_box(lid_l, lid_w, LID_OVLP, r = 3.5);
                    // Hollow inside rim
                    translate([WALL + tol/2, WALL + tol/2, -e])
                        cube([INT_L - tol, INT_W - tol, LID_OVLP + 2*e]);
                }

            // Labyrinth groove ridge (mates with body ridge)
            translate([WALL - LID_LBYRTH_W + tol/2,
                       WALL - LID_LBYRTH_W + tol/2,
                       -LID_OVLP + LID_LBYRTH_H/2])
                difference() {
                    rounded_box(INT_L + 2*LID_LBYRTH_W - tol,
                                INT_W + 2*LID_LBYRTH_W - tol,
                                LID_LBYRTH + 0.5, r = 2.5);
                    translate([LID_LBYRTH_W, LID_LBYRTH_W, -e])
                        cube([INT_L - tol, INT_W - tol,
                              LID_LBYRTH + 0.7]);
                }
        }

        // ── UWB antenna window (NO lid material over antenna) ─
        // PCB centred in enclosure → antenna window centred in lid
        bx = (lid_l - MAWB_L + 4) / 2;
        by = (lid_w - MAWB_W + 4) / 2;
        translate([bx, by, -e])
            rounded_box(MAWB_L - 4, MAWB_W - 4,
                        LID_H + 2*e, r = 2);

        // ── Snap tabs on inside of lid rim ────────────────────
        for (ys=[WALL/2, lid_w - WALL/2 - SNAP_T]) {
            translate([lid_l/2 - SNAP_W/2, ys,
                       -LID_OVLP + (LID_OVLP - SNAP_H)/2])
                cube([SNAP_W, SNAP_T + e, SNAP_H]);
        }
        for (xs=[WALL/2, lid_l - WALL/2 - SNAP_T]) {
            translate([xs, lid_w/2 - SNAP_W/2,
                       -LID_OVLP + (LID_OVLP - SNAP_H)/2])
                cube([SNAP_T + e, SNAP_W, SNAP_H]);
        }
    }
}

// ─────────────────────────────────────────────────────────────
// belt_clip()   Print: back face down.  Use PETG for flex.
// ─────────────────────────────────────────────────────────────
module belt_clip() {
    difference() {
        union() {
            // Back plate (mounts flush to Y− face of enclosure)
            cube([EXT_L, CLIP_BACK_T, CLIP_H]);

            // Outer spine
            translate([0, CLIP_BACK_T, 0])
                cube([EXT_L, CLIP_FLEX_T + CLIP_GAP, CLIP_H]);

            // Top bridge
            translate([0, CLIP_BACK_T, CLIP_H - CLIP_FLEX_T])
                cube([EXT_L, CLIP_GAP + CLIP_FLEX_T + CLIP_BACK_T,
                      CLIP_FLEX_T]);

            // Flex spring arm (tapers for compliance)
            translate([EXT_L * 0.1, CLIP_BACK_T + CLIP_GAP, 0])
                hull() {
                    cube([EXT_L * 0.8, CLIP_FLEX_T, 1]);
                    translate([EXT_L * 0.05, 1, CLIP_H - CLIP_FLEX_T - 4])
                        cube([EXT_L * 0.7, CLIP_FLEX_T + 1, 1]);
                }
        }

        // Belt slot through outer spine
        translate([(EXT_L - CLIP_BELT_W) / 2, CLIP_BACK_T - e,
                   CLIP_FLEX_T + 3])
            cube([CLIP_BELT_W, CLIP_GAP + CLIP_FLEX_T + 2*e,
                  CLIP_H - 2*CLIP_FLEX_T - 6]);

        // 2× M3 screws (clip → enclosure back face)
        for (x = [EXT_L*0.25, EXT_L*0.75])
            translate([x, -e, CLIP_H/2])
                rotate([-90, 0, 0])
                    cylinder(d=3.3, h=CLIP_BACK_T + 2*e);
    }
}

// ─────────────────────────────────────────────────────────────
// tpu_bumper()   Print in TPU 95A.  Snaps around body bottom.
// ─────────────────────────────────────────────────────────────
module tpu_bumper() {
    // Outer frame; inner cutout = body exterior + 0.5 clearance
    cl = 0.5;
    difference() {
        // Outer rounded rectangle
        translate([-TPU_T, -TPU_T, 0])
            rounded_box(EXT_L + 2*TPU_T, EXT_W + 2*TPU_T,
                        TPU_H, r = TPU_CR);

        // Inner cutout (fits over body)
        translate([-cl/2, -cl/2, -e])
            rounded_box(EXT_L + cl, EXT_W + cl,
                        TPU_H + 2*e, r = 3.5);
    }
}

// ─────────────────────────────────────────────────────────────
// Render selector
// ─────────────────────────────────────────────────────────────
if (RENDER == "assembly") {
    color("SlateGray",  0.92) enclosure_body();
    color("DimGray",    0.75) translate([0, 0, EXT_H])
                                  enclosure_lid();
    color("SteelBlue",  0.85) translate([0, -CLIP_BACK_T, 0])
                                  belt_clip();
    color("OrangeRed",  0.65) translate([0, 0, -TPU_H - 0.5])
                                  tpu_bumper();

} else if (RENDER == "body") {
    enclosure_body();

} else if (RENDER == "lid") {
    enclosure_lid();

} else if (RENDER == "tpu_bumper") {
    tpu_bumper();

} else if (RENDER == "body_2d") {
    projection(cut = true)
        translate([0, 0, -FLOOR/2])
            enclosure_body();
}
