// ============================================================
// Cable Management Tray — Issue #628
// Agent   : sl-mechanical
// Date    : 2026-03-15
// Part catalogue:
//   1. tray_body     — under-plate tray with snap-in cable channels, Velcro
//                      tie-down slots every 40 mm, pass-through holes, label slots
//   2. tnut_bracket  — 2020 T-nut rail mount bracket (×2, slide under tray)
//   3. channel_clip  — snap-in divider clip separating power / signal / servo zones
//   4. cover_panel   — hinged snap-on lid (living-hinge PETG flexure strip)
//   5. cable_saddle  — individual cable saddle / strain-relief clip (×n)
//
// BOM:
//   4 × M5×10 BHCS + M5 T-nuts    (tnut_bracket × 2 to rail)
//   4 × M3×8  SHCS                (tnut_bracket to tray body)
//   n × 100 mm Velcro tie-down strips (through 6×2 mm slots, every 40 mm)
//
// Cable channel layout (X axis, inside tray):
//   Zone A — Power  (2S–6S LiPo, XT60/XT30): 20 mm wide, 14 mm deep
//   Zone B — Signal (JST-SH, PWM, I2C, UART): 14 mm wide, 10 mm deep
//   Zone C — Servo  (JST-PH, thick servo leads): 14 mm wide, 12 mm deep
//   Divider walls: 2.5 mm thick between zones
//
// Print settings (PETG):
//   tray_body / tnut_bracket / channel_clip : 5 perimeters, 40 % gyroid, no supports
//   cover_panel                             : 3 perimeters, 20 % gyroid, no supports
//                                             (living-hinge — print flat, thin strip flexes)
//   cable_saddle                            : 3 perimeters, 30 % gyroid, no supports
//
// Export commands:
//   openscad -D 'RENDER="tray_body"'     -o tray_body.stl     cable_tray.scad
//   openscad -D 'RENDER="tnut_bracket"'  -o tnut_bracket.stl  cable_tray.scad
//   openscad -D 'RENDER="channel_clip"'  -o channel_clip.stl  cable_tray.scad
//   openscad -D 'RENDER="cover_panel"'   -o cover_panel.stl   cable_tray.scad
//   openscad -D 'RENDER="cable_saddle"'  -o cable_saddle.stl  cable_tray.scad
//   openscad -D 'RENDER="assembly"'      -o assembly.png      cable_tray.scad
// ============================================================

RENDER = "assembly"; // tray_body | tnut_bracket | channel_clip | cover_panel | cable_saddle | assembly

$fn = 48;
EPS = 0.01;

// ── 2020 rail constants ──────────────────────────────────────
RAIL_W      = 20.0;
TNUT_W      =  9.8;
TNUT_H      =  5.5;
TNUT_L      = 12.0;
SLOT_NECK_H =  3.2;
M5_D        =  5.2;
M5_HEAD_D   =  9.5;
M5_HEAD_H   =  4.0;

// ── Tray geometry ────────────────────────────────────────────
TRAY_L      = 280.0;  // length along rail (7 × 40 mm tie-down pitch)
TRAY_W      =  60.0;  // width across rail (covers standard 40 mm rail pair)
TRAY_WALL   =   2.5;  // side / floor wall thickness
TRAY_DEPTH  =  18.0;  // interior depth (tallest zone + wall)

// Cable channel zones (widths must sum to TRAY_W - 2*TRAY_WALL - 2*DIV_T)
DIV_T       =   2.5;  // divider wall thickness
ZONE_A_W    =  20.0;  // Power
ZONE_A_D    =  14.0;
ZONE_B_W    =  14.0;  // Signal
ZONE_B_D    =  10.0;
ZONE_C_W    =  14.0;  // Servo
ZONE_C_D    =  12.0;
// Total inner width used: ZONE_A_W + ZONE_B_W + ZONE_C_W + 2*DIV_T = 55 mm < TRAY_W - 2*TRAY_WALL = 55 mm ✓

// Tie-down slots (Velcro strips)
TIEDOWN_PITCH = 40.0;
TIEDOWN_W   =   6.0;  // slot width (fits 6 mm wide Velcro)
TIEDOWN_T   =   2.2;  // slot through-thickness (floor)
TIEDOWN_CNT = 7;      // 7 positions along tray

// Pass-through holes in floor
PASSTHRU_D  =  12.0;  // circular grommet-compatible pass-through
PASSTHRU_CNT = 3;     // one per zone, at tray mid-length

// Label slots (rear outer wall)
LABEL_W     =  24.0;
LABEL_H     =   8.0;
LABEL_T     =   1.0;  // depth from outer face

// Snap ledge for cover
SNAP_LEDGE_H =  2.5;
SNAP_LEDGE_D =  1.5;

// ── T-nut bracket ────────────────────────────────────────────
BKT_L       =  60.0;
BKT_W       =  TRAY_W;
BKT_T       =   6.0;
BOLT_PITCH  =  40.0;
M3_D        =   3.2;
M3_HEAD_D   =   6.0;
M3_HEAD_H   =   3.0;
M3_NUT_W    =   5.5;
M3_NUT_H    =   2.4;

// ── Cover panel ──────────────────────────────────────────────
CVR_T       =   1.8;  // panel thickness
HINGE_T     =   0.6;  // living-hinge strip thickness (printed in PETG)
HINGE_W     =   3.0;  // hinge strip width (flexes easily)
SNAP_HOOK_H =   3.5;  // snap hook height
SNAP_HOOK_T =   2.2;

// ── Cable saddle ─────────────────────────────────────────────
SAD_W       =  12.0;
SAD_H       =   8.0;
SAD_T       =   2.5;
SAD_BORE_D  =   7.0;  // cable bundle bore
SAD_CLIP_T  =   1.6;  // snap arm thickness

// ── Utilities ────────────────────────────────────────────────
module chamfer_cube(size, ch=1.0) {
    hull() {
        translate([ch, ch, 0])   cube([size[0]-2*ch, size[1]-2*ch, EPS]);
        translate([0, 0, ch])    cube(size - [0, 0, ch]);
    }
}

module hex_pocket(af, depth) {
    cylinder(d=af/cos(30), h=depth, $fn=6);
}

// ── Part 1: tray_body ───────────────────────────────────────
module tray_body() {
    difference() {
        // Outer shell
        union() {
            chamfer_cube([TRAY_L, TRAY_W, TRAY_DEPTH + TRAY_WALL], ch=1.5);

            // Snap ledge along top of both long walls (for cover_panel)
            for (y = [-SNAP_LEDGE_D, TRAY_W])
                translate([0, y, TRAY_DEPTH])
                    cube([TRAY_L, TRAY_WALL + SNAP_LEDGE_D, SNAP_LEDGE_H]);
        }

        // Interior cavity
        translate([TRAY_WALL, TRAY_WALL, TRAY_WALL])
            cube([TRAY_L - 2*TRAY_WALL, TRAY_W - 2*TRAY_WALL,
                  TRAY_DEPTH + EPS]);

        // ── Zone dividers (subtract from solid to leave walls) ──
        // Zone A (Power) inner floor cut — full depth A
        translate([TRAY_WALL, TRAY_WALL, TRAY_WALL + (TRAY_DEPTH - ZONE_A_D)])
            cube([TRAY_L - 2*TRAY_WALL, ZONE_A_W, ZONE_A_D + EPS]);

        // Zone B (Signal) inner floor cut
        translate([TRAY_WALL, TRAY_WALL + ZONE_A_W + DIV_T,
                   TRAY_WALL + (TRAY_DEPTH - ZONE_B_D)])
            cube([TRAY_L - 2*TRAY_WALL, ZONE_B_W, ZONE_B_D + EPS]);

        // Zone C (Servo) inner floor cut
        translate([TRAY_WALL, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W + DIV_T,
                   TRAY_WALL + (TRAY_DEPTH - ZONE_C_D)])
            cube([TRAY_L - 2*TRAY_WALL, ZONE_C_W, ZONE_C_D + EPS]);

        // ── Velcro tie-down slots (floor, every 40 mm) ──────────
        for (i = [0:TIEDOWN_CNT-1]) {
            x = TRAY_WALL + 20 + i * TIEDOWN_PITCH - TIEDOWN_W/2;
            // Zone A slot
            translate([x, TRAY_WALL + 2, -EPS])
                cube([TIEDOWN_W, ZONE_A_W - 4, TRAY_WALL + 2*EPS]);
            // Zone B slot
            translate([x, TRAY_WALL + ZONE_A_W + DIV_T + 2, -EPS])
                cube([TIEDOWN_W, ZONE_B_W - 4, TRAY_WALL + 2*EPS]);
            // Zone C slot
            translate([x, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W + DIV_T + 2, -EPS])
                cube([TIEDOWN_W, ZONE_C_W - 4, TRAY_WALL + 2*EPS]);
        }

        // ── Pass-through holes in floor (centre of each zone at mid-length) ──
        mid_x = TRAY_L / 2;
        // Zone A
        translate([mid_x, TRAY_WALL + ZONE_A_W/2, -EPS])
            cylinder(d=PASSTHRU_D, h=TRAY_WALL + 2*EPS);
        // Zone B
        translate([mid_x, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W/2, -EPS])
            cylinder(d=PASSTHRU_D, h=TRAY_WALL + 2*EPS);
        // Zone C
        translate([mid_x, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W + DIV_T + ZONE_C_W/2, -EPS])
            cylinder(d=PASSTHRU_D, h=TRAY_WALL + 2*EPS);

        // ── Label slots on front wall (y = 0) — one per zone ────
        zone_ctrs = [TRAY_WALL + ZONE_A_W/2,
                     TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W/2,
                     TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W + DIV_T + ZONE_C_W/2];
        label_z = TRAY_WALL + 2;
        for (yc = zone_ctrs)
            translate([TRAY_L/2 - LABEL_W/2, -EPS, label_z])
                cube([LABEL_W, LABEL_T + EPS, LABEL_H]);

        // ── M3 bracket bolt holes in floor (4 corners) ──────────
        for (x = [20, TRAY_L - 20])
            for (y = [TRAY_W/4, 3*TRAY_W/4])
                translate([x, y, -EPS])
                    cylinder(d=M3_D, h=TRAY_WALL + 2*EPS);

        // ── Channel clip snap sockets (top of each divider, every 80 mm) ──
        for (i = [0:2]) {
            cx = 40 + i * 80;
            for (dy = [ZONE_A_W, ZONE_A_W + DIV_T + ZONE_B_W])
                translate([cx - 3, TRAY_WALL + dy - 1, TRAY_DEPTH - 4])
                    cube([6, DIV_T + 2, 4 + EPS]);
        }
    }

    // ── Divider walls (positive geometry) ───────────────────
    // Wall between Zone A and Zone B
    translate([TRAY_WALL, TRAY_WALL + ZONE_A_W, TRAY_WALL])
        cube([TRAY_L - 2*TRAY_WALL, DIV_T,
              TRAY_DEPTH - ZONE_A_D]);  // partial height — lower in A zone

    // Wall between Zone B and Zone C
    translate([TRAY_WALL, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W, TRAY_WALL])
        cube([TRAY_L - 2*TRAY_WALL, DIV_T,
              TRAY_DEPTH - ZONE_B_D]);
}

// ── Part 2: tnut_bracket ────────────────────────────────────
module tnut_bracket() {
    difference() {
        chamfer_cube([BKT_L, BKT_W, BKT_T], ch=1.5);

        // M5 T-nut holes (2 per bracket, on rail centreline)
        for (x = [BKT_L/2 - BOLT_PITCH/2, BKT_L/2 + BOLT_PITCH/2]) {
            translate([x, BKT_W/2, -EPS]) {
                cylinder(d=M5_D, h=BKT_T + 2*EPS);
                cylinder(d=M5_HEAD_D, h=M5_HEAD_H + EPS);
            }
            translate([x - TNUT_L/2, BKT_W/2 - TNUT_W/2, BKT_T - TNUT_H])
                cube([TNUT_L, TNUT_W, TNUT_H + EPS]);
        }

        // M3 tray-attachment holes (4 corners)
        for (x = [10, BKT_L - 10])
            for (y = [10, BKT_W - 10]) {
                translate([x, y, -EPS])
                    cylinder(d=M3_D, h=BKT_T + 2*EPS);
                // M3 hex nut captured pocket (from top)
                translate([x, y, BKT_T - M3_NUT_H - 0.2])
                    hex_pocket(M3_NUT_W + 0.3, M3_NUT_H + 0.3);
            }

        // Weight relief
        translate([15, 8, -EPS])
            cube([BKT_L - 30, BKT_W - 16, BKT_T/2]);
    }
}

// ── Part 3: channel_clip ────────────────────────────────────
// Snap-in clip that locks into divider-wall snap sockets;
// holds individual bundles in their zone and acts as colour-coded zone marker.
module channel_clip() {
    clip_body_w = 6.0;
    clip_body_h = DIV_T + 4.0;
    clip_body_t = 8.0;
    tab_h = 3.5;
    tab_w = 2.5;

    difference() {
        union() {
            // Body spanning divider
            cube([clip_body_t, clip_body_w, clip_body_h]);

            // Snap tabs (bottom, straddle divider)
            for (s = [0, clip_body_w - tab_w])
                translate([clip_body_t/2 - 1, s, -tab_h])
                    cube([2, tab_w, tab_h + 1]);
        }

        // Cable radius slot on each face
        translate([-EPS, clip_body_w/2, clip_body_h * 0.6])
            rotate([0, 90, 0])
                cylinder(d=5.0, h=clip_body_t + 2*EPS);

        // Snap tab undercut for flex
        for (s = [0, clip_body_w - tab_w])
            translate([clip_body_t/2 - 2, s - EPS, -tab_h + 1.5])
                cube([4, tab_w + 2*EPS, 1.5]);
    }
}

// ── Part 4: cover_panel ─────────────────────────────────────
// Flat snap-on lid with living-hinge along one long edge.
// Print flat; PETG living hinge flexes ~90° to snap onto tray.
module cover_panel() {
    total_w = TRAY_W + 2 * SNAP_HOOK_T;

    difference() {
        union() {
            // Main panel
            cube([TRAY_L, TRAY_W, CVR_T]);

            // Living hinge strip along back edge (thin, flexes)
            translate([0, TRAY_W - EPS, 0])
                cube([TRAY_L, HINGE_W, HINGE_T]);

            // Snap hooks along front edge (clips under tray snap ledge)
            for (x = [20, TRAY_L/2 - 20, TRAY_L/2 + 20, TRAY_L - 20])
                translate([x - SNAP_HOOK_T/2, -SNAP_HOOK_H + EPS, 0])
                    difference() {
                        cube([SNAP_HOOK_T, SNAP_HOOK_H, CVR_T + 1.5]);
                        // Hook nose chamfer
                        translate([-EPS, -EPS, CVR_T])
                            rotate([0, 0, 0])
                                cube([SNAP_HOOK_T + 2*EPS, 1.5, 1.5]);
                    }
        }

        // Ventilation slots (3 rows × 6 slots)
        for (row = [0:2])
            for (col = [0:5]) {
                sx = 20 + col * 40 + row * 10;
                sy = 10 + row * 12;
                if (sx + 25 < TRAY_L && sy + 6 < TRAY_W)
                    translate([sx, sy, -EPS])
                        cube([25, 6, CVR_T + 2*EPS]);
            }

        // Zone label windows (align with tray label slots)
        zone_ctrs = [TRAY_WALL + ZONE_A_W/2,
                     TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W/2,
                     TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W + DIV_T + ZONE_C_W/2];
        for (yc = zone_ctrs)
            translate([TRAY_L/2 - LABEL_W/2, yc - LABEL_H/2, -EPS])
                cube([LABEL_W, LABEL_H, CVR_T + 2*EPS]);
    }
}

// ── Part 5: cable_saddle ────────────────────────────────────
// Snap-in cable saddle / strain-relief clip; press-fits onto tray top edge.
module cable_saddle() {
    arm_gap = TRAY_WALL + 0.4;  // fits over tray wall
    arm_len = 8.0;

    difference() {
        union() {
            // Body
            chamfer_cube([SAD_W, SAD_T * 2 + arm_gap, SAD_H], ch=1.0);

            // Cable retaining arch
            translate([SAD_W/2, SAD_T + arm_gap/2, SAD_H])
                scale([1, 0.6, 1])
                    difference() {
                        cylinder(d=SAD_BORE_D + SAD_CLIP_T * 2, h=SAD_T);
                        translate([0, 0, -EPS])
                            cylinder(d=SAD_BORE_D, h=SAD_T + 2*EPS);
                        translate([-SAD_BORE_D, 0, -EPS])
                            cube([SAD_BORE_D * 2, SAD_BORE_D, SAD_T + 2*EPS]);
                    }
        }

        // Slot for tray wall (negative)
        translate([0, SAD_T, -EPS])
            cube([SAD_W, arm_gap, arm_len + EPS]);

        // M3 tie-down hole
        translate([SAD_W/2, SAD_T + arm_gap/2, -EPS])
            cylinder(d=M3_D, h=SAD_H + 2*EPS);
    }
}

// ── Assembly ────────────────────────────────────────────────
module assembly() {
    // Tray body (open face up for visibility)
    color("SteelBlue")
        tray_body();

    // Two T-nut brackets underneath at 1/4 and 3/4 length
    for (bx = [TRAY_L/4 - BKT_L/2, 3*TRAY_L/4 - BKT_L/2])
        color("DodgerBlue")
            translate([bx, 0, -BKT_T])
                tnut_bracket();

    // Channel clips (3 per divider position, every 80 mm)
    for (i = [0:2]) {
        cx = 40 + i * 80;
        // Divider A/B
        color("Tomato", 0.8)
            translate([cx - 4, TRAY_WALL + ZONE_A_W - 2, TRAY_DEPTH - 3])
                channel_clip();
        // Divider B/C
        color("Orange", 0.8)
            translate([cx - 4,
                       TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W - 2,
                       TRAY_DEPTH - 3])
                channel_clip();
    }

    // Cover panel (raised above tray to show interior)
    color("LightSteelBlue", 0.5)
        translate([0, 0, TRAY_DEPTH + SNAP_LEDGE_H + 4])
            cover_panel();

    // Cable saddles along front tray edge
    for (x = [40, 120, 200])
        color("SlateGray")
            translate([x - SAD_W/2, -SAD_T * 2 - TRAY_WALL, 0])
                cable_saddle();
}

// ── Dispatch ────────────────────────────────────────────────
if      (RENDER == "tray_body")    tray_body();
else if (RENDER == "tnut_bracket") tnut_bracket();
else if (RENDER == "channel_clip") channel_clip();
else if (RENDER == "cover_panel")  cover_panel();
else if (RENDER == "cable_saddle") cable_saddle();
else                               assembly();
