Rendering these values. This takes a few seconds.
Download STL
Read this model as text

A hinged travel case for a tiny whoop, its charger and a row of 1S batteries, designed for PETG, with a smooth outside. The base is one shell for every combination; two drop-in trays go inside. The whoop tray has a pocket shaped around the ducts of one whoop, so a new whoop only needs a new tray. The charger tray holds a BetaFPV charger in the front right corner (seen from the front with the lid open behind) with two click jaws: each slides in a dovetail groove in the tray and ratchets against a row of teeth, so you push it against the charger until it clicks and it stays there, and its lip hooks over the top edge of the charger. No nuts or bolts in the charger tray. A slider sits flush in the front wall over the charging window: pull it up to open, push it down to close; it clicks in both positions. The latch sits flush in a recess. The lid holds the batteries standing on their side in snap clips. Two pin hinges at the rear, each on one M3 bolt.

The image shows the solid OpenSCAD renders at part assembly · test_print false · show_contents false · whoop_wheelbase 80 · duct_d 50 · whoop_h 40 · charger_h 19 · port_from_side 23 · port_z 9.5 · side_jaw_clicks 0 · rear_jaw_clicks 0 · battery_class LAVA 480-680 · battery_count 8 · color_case SteelBlue.

MIT

Tiny whoop case with drop-in trays, click jaws and a port slider

mick/whoop-case@2.2.0

Remix this
Report

For a legal or policy problem: a license violation, personal data, somebody else’s work republished as their own. A model that prints badly is a comment, not a report.

Sign in to report

0reused0 remixes · 0 imports0downloads3.6.3latest

Parameters

from source
5580, steps of 0.5
2850, steps of 0.5
2060, steps of 1
1424, steps of 0.5
1040, steps of 0.5

Showing the render for these values.

Description

What is this

A hinged travel case for a tiny whoop, its charger and a row of 1S batteries, designed for PETG, with a smooth outside. The base is one shell for every combination; two drop-in trays go inside. The whoop tray has a pocket shaped around the ducts of one whoop, so a new whoop only needs a new tray. The charger tray holds a BetaFPV charger in the front right corner (seen from the front with the lid open behind) with two click jaws: each slides in a dovetail groove in the tray and ratchets against a row of teeth, so you push it against the charger until it clicks and it stays there, and its lip hooks over the top edge of the charger. No nuts or bolts in the charger tray. A slider sits flush in the front wall over the charging window: pull it up to open, push it down to close; it clicks in both positions. The latch sits flush in a recess. The lid holds the batteries standing on their side in snap clips. Two pin hinges at the rear, each on one M3 bolt.

What you use it for

Carrying a whoop up to the size of a Meteor75 Pro II (80 mm wheelbase, 50 mm ducts) with a BetaFPV charger and spare 1S batteries in one box. The whoop tray takes the wheelbase and duct diameter of the whoop it is for; the shell stays the same. The jaws click in 1 mm steps: the side jaw holds widths from 46.2 mm down to 42.2 mm, the rear jaw lengths from 79.2 mm down to 70.2 mm, each a few tenths over the BetaFPV sizes they are set for. The BetaFPV 6-port charger (79 x 46 x 19 mm) sits at 0 clicks on both jaws, the BetaFPV BT2.0 charger board (75 x 42 x 22 mm) at 4 clicks on both. side_jaw_clicks and rear_jaw_clicks show the jaws in any position, with a ghost charger of the size that position holds. Set charger_h to the real height of the charger so the jaw lips catch its top edge. To fit or take out the charger, lift the post at the back of a jaw and slide the jaw back. The battery clips take the LAVA family: 480, 550, 580 and 680 mAh (15 to 15.5 mm wide, up to 7 mm thick, up to 80 mm long) or 280 and 320 mAh (10 mm wide).

Design notes

Everything prints without supports. The trays print upright and have no floor under the pockets, so the whoop and the charger stand on the floor of the shell; the rest of each tray is solid, so slice it with sparse infill. The jaw grooves are dovetails with 45 degree lips, and the teeth on their floor are a sawtooth that rises toward the charger. A jaw prints on its side: its slider matches the groove, its lower slab is a pawl arm freed by a thin gap, rooted at the front, with a tooth under its rear end, and a post rises from that end through a slot in the jaw to above its top. Printed on its side, the pawl bends along its layers. Lifting the post lifts the tooth out of the teeth. The tooth comes to rest 0.2 mm past each tooth of the rack, so the clamped positions are 0.2, 1.2, 2.2 mm and so on in from fully open; the largest charger sizes are set 0.2 mm under the open jaw for that. The slider prints back face down, its edges narrowing at 45 degrees into the dovetail of its channel, which runs straight up to the rim so the slider goes in from the top and the closed lid keeps it there. A tongue cut into it, bending along its layers, has a hole at its tip that clicks over a bump in the channel at the open and the closed position; two grooves across its top give the thumb a grip. The window has chamfered top corners, so its top is an 8 mm bridge. The outside has a 6 mm corner radius, a 45 degree chamfer under the base and on top of the lid, and a small chamfer either side of the parting line. The latch tongue is the outer skin of the lid front, freed by two slits and a relief behind it; its hook rides over a 45 degree lead-in and catches in a pocket backed by a boss inside the front wall, with a scoop under it for a fingernail. Battery arms hang from the lid plate and bend across their layers, which PETG takes at their small deflection. The only hardware is the two hinge pins: each hinge is base, lid, base on an M3 x 30 with a nut. The case is about 179 x 117 mm and fits a 180 mm bed. test_print renders a coupon set: one hinge and the latch in both halves, the slider channel with the slider, a strip of charger tray with its groove and the side jaw, and one battery clip.

Parameters

part picks the assembly or one part in print orientation; jaws lays out the side and the rear jaw. test_print swaps it for the coupon set, show_contents shows the whoop, charger and batteries as ghosts. whoop_wheelbase (motor to motor across the diagonal) and duct_d (outer diameter of one duct) set the pocket of the whoop tray, up to 80 and 50; whoop_h (total height with camera) sets the height of the case. charger_h is the height of the charger, where the jaw lips catch. port_from_side is the distance from the outer side of the charger to the middle of its port, port_z the height of the middle of the port above the floor. side_jaw_clicks and rear_jaw_clicks place the jaws in the assembly, counted in clicks from fully open, and size the ghost charger to match; they change nothing that prints. battery_class picks the LAVA size family, battery_count the number of clips. color_case is display only.

Use with your agent
{
  "mcpServers": {
    "remixcad": {
      "type": "http",
      "url": "https://remix-cad.vercel.app/api/mcp"
    }
  }
}
get_model("mick/whoop-case@2.2.0")

Returns the manifest, the source, the import closure and the license obligations. Full MCP setup.

Parameter table

Parsed from the Customizer annotations in main.scad
Parameters of Tiny whoop case with drop-in trays, click jaws and a port slider, with the ranges the source declares
NameDefaultRangeUnitDescription
part"assembly"assembly, base, lid, whoop_tray, charger_tray, jaws, slider
test_printfalse
show_contentsfalse
whoop_wheelbase8055 to 80 step 0.5
duct_d5028 to 50 step 0.5
whoop_h4020 to 60 step 1
charger_h1914 to 24 step 0.5
port_from_side2310 to 40 step 0.5
port_z9.56 to 20 step 0.5
side_jaw_clicks00 to 4 step 1
rear_jaw_clicks00 to 9 step 1
battery_class"LAVA 480-680"LAVA 480-680, LAVA 280-320
battery_count80 to 12 step 1
color_case"SteelBlue"SteelBlue, White, DimGray, Black, FireBrick, Goldenrod, ForestGreen, MediumPurple

Lineage

0 models depend on this one

Only what this model came from and what came from it.

Remix, the code changed Import, used unchanged and pinned The version you are on Withdrawn, still recorded

License

Whoever downloads this takes these obligations along
  • mick/whoop-case@2.2.0 is MIT

Source

main.scad is the entrypoint
main.scad
// Tiny whoop case: a whoop, its BetaFPV charger and a row of 1S batteries in one
// hinged box, printed in PETG, with a smooth outside. One shell for everything and
// two drop-in trays: a whoop tray with a pocket shaped around the ducts of one
// whoop, and a charger tray with two click jaws that ratchet against the charger.
// A flush slider opens and closes the charging window. The lid holds the batteries
// on their side in snap clips. Every part prints without supports.
//
// Coordinates: y = 0 at the rear (hinges), y = Sy at the front (latch). Seen from
// the front, the viewer's right is low x: the charger sits in the corner at low x,
// high y. The lid is modelled closed and flipped for printing.

/* [Part] */
// The whole case with the lid open, or one part in print orientation.
part = "assembly"; // [assembly, base, lid, whoop_tray, charger_tray, jaws, slider]
// Print a coupon set instead: one hinge and the latch in base and lid halves, the
// slider channel with the slider, a strip of charger tray with the side jaw, and
// one battery clip.
test_print = false;
// Show the whoop, the charger and the batteries as ghosts in the assembly.
show_contents = false;

/* [Whoop] */
// The whoop the whoop tray is for. Motor to motor across the diagonal: 80 for a
// Meteor75 Pro II, the largest the case takes.
whoop_wheelbase = 80; // [55:0.5:80]
// Outer diameter of one duct, up to 50.
duct_d = 50; // [28:0.5:50]
// Total height of the whoop, camera included: sets the height of the case.
whoop_h = 40; // [20:1:60]

/* [Charger] */
// Height of the charger: the jaw lips catch its top edge. 19 for the BetaFPV 6-port
// charger, 22 for the BT2.0 charger board.
charger_h = 19; // [14:0.5:24]
// Charging port: distance from the outer side of the charger to the middle of the
// port, and height of the middle of the port above the floor.
port_from_side = 23; // [10:0.5:40]
port_z = 9.5; // [6:0.5:20]

/* [Charger jaws] */
// Display only: the jaws in the assembly, counted in 1 mm clicks from fully open,
// with a ghost charger of the size that position holds. Side jaw: 0 holds 46.2 mm
// wide (BetaFPV 6-port), 4 holds 42.2 mm (BT2.0 charger board).
side_jaw_clicks = 0; // [0:1:4]
// Rear jaw: 0 holds 79.2 mm long (BetaFPV 6-port), 4 holds 75.2 mm (BT2.0 charger
// board), 9 holds 70.2 mm.
rear_jaw_clicks = 0; // [0:1:9]

/* [Batteries] */
// LAVA size family: 480 to 680 mAh (15 mm wide), or 280 and 320 mAh (10 mm wide).
battery_class = "LAVA 480-680"; // [LAVA 480-680, LAVA 280-320]
// Batteries in the lid, standing on their side. 0 for an empty lid.
battery_count = 8; // [0:1:12]

/* [Colour] */
// Display only, the STL carries no colour.
color_case = "SteelBlue"; // [SteelBlue, White, DimGray, Black, FireBrick, Goldenrod, ForestGreen, MediumPurple]

/* [Hidden] */
$fn = 64;
wall = 3.2;
floor_t = 2;
top_t = 2;
corner_r = 6;
bot_ch = 1.5;
top_ch = 2;
seam_ch = 0.6;
// trays: clearance in the shell, whoop tray height, charger tray height, clearance
// around the whoop, the largest whoop the shell takes
t_clr = 0.3;
deck_h = 14;
ct_h = 12;
c_clr = 0.5;
max_wb = 80;
max_dd = 50;
// charger: the footprint with both jaws fully open, the length of the side and the
// rear jaw (their travel is 9 less). The jaws rest 0.2 mm past each tooth, so the
// clamped positions are 0.2, 1.2, 2.2 ... mm in from fully open.
ch_w = 46.4;
ch_l = 79.4;
l_side = 14;
l_rear = 19;
jaw_h = charger_h - ct_h + 3.6;
p_side = side_jaw_clicks + 0.2;
p_rear = rear_jaw_clicks + 0.2;
gh_w = ch_w - p_side;
gh_l = ch_l - p_rear;
// port slider: width, height, thickness, travel, tongue centre from its left edge
sl_w = 26;
sl_h = 16;
sl_t = 1.6;
sl_travel = 13;
tg_x = sl_w / 2 + 9.5;
// lip on the base rim, recess in the lid
lip_h = 2;
lip_t = 1.2;
lip_c = 0.25;
// battery clips
big_bat = battery_class == "LAVA 480-680";
battery_w = big_bat ? 15.5 : 10.5;
battery_t = big_bat ? 7 : 6.5;
battery_l = big_bat ? 80 : 64;
bat_clr = 0.25;
arm_t = 1.4;
arm_w = 6;
arm_gap = 1.6;
bump = 0.7;
clip_y = 17;
// hinges: pin hole, knuckle radius, axis behind the rear face, segment width and gap
bolt_d = 3.4;
rk = 4;
hk = rk + 0.5;
seg_w = 8;
seg_gap = 0.4;
hinge_in = 22;
// latch
latch_w = 20;
latch_drop = 10;
tongue_t = 1.6;

sq = whoop_wheelbase / sqrt(2);
whoop_zone = max_wb / sqrt(2) + max_dd + 2 * c_clr + 2.4;
// charger zone across x: charger, side jaw groove, wall behind it
x_pw = wall + ch_w;
x_cz = x_pw + l_side + 0.5 + 1.5;
bat_pitch = battery_t + 2 * (bat_clr + arm_t) + arm_gap;
Sx_in = max(x_cz - wall + 2 * t_clr + whoop_zone, battery_count * bat_pitch + 8);
Sy_in = max(whoop_zone + 2 * t_clr, ch_l + l_rear + 0.5 + 1.5 + t_clr, battery_l + 12);
Sx = Sx_in + 2 * wall;
Sy = Sy_in + 2 * wall;
fz = floor_t;
cav_h = max(whoop_h + 2, charger_h + 6, deck_h + 4);
base_h = fz + cav_h;
zc = base_h;
lid_cav = max(battery_w + 2.5, 9);
lid_h = top_t + lid_cav;
pb = zc + lid_h - top_t;
// charger along y: front face on the front wall, rear jaw behind it
yf = Sy - wall;
y_pr = yf - ch_l;
y_ji = yf - 40;
x_jr = wall + ch_w / 2;
// whoop centre
dx = (x_cz + Sx - wall) / 2;
dy = Sy / 2;
// charging window and slider
px = max(corner_r + sl_w / 2 + sl_t + 1.5, min(wall + port_from_side, x_cz - sl_w / 2 - 3));
pz = fz + max(9.5, min(port_z, cav_h - 23));
sl_z0 = pz - sl_h / 2;

// ---- 2D helpers ----

module outer2d() {
  offset(r = corner_r) offset(delta = -corner_r) square([Sx, Sy]);
}

module inset2d(k) {
  offset(r = -k) outer2d();
}

module ring2d(a, b) {
  difference() { inset2d(a); inset2d(b); }
}

// Horizontal hole that prints without support: the tip points up in 2D y.
module teardrop2d(r) {
  circle(r = r);
  rotate(45) square(r);
}

// Whoop outline: four ducts on a square of side s, and the frame between them.
module whoop2d(s, d) {
  for (sx = [-1, 1], sy = [-1, 1]) translate([sx * s / 2, sy * s / 2]) circle(d = d);
  square(s, center = true);
}

// The charging window: 14 x 9 with chamfered top corners, so the top is an 8 mm
// bridge.
module win2d() {
  polygon([[-7, -4.5], [7, -4.5], [7, 1.5], [4, 4.5], [-4, 4.5], [-7, 1.5]]);
}

// ---- Hinge ----
// Knuckle profile in (y, z) about the hinge axis, which sits hk behind the rear face
// at rim height. Its underside runs into the wall at 45 degrees. The lid mirrors it
// about the rim, so both halves print without support.

module knuckle_profile() {
  difference() {
    hull() {
      translate([-hk, 0]) circle(r = rk);
      translate([-0.01, -hk - 1.42 * rk]) square([1, hk + 1.42 * rk - 2]);
    }
    translate([-hk, 0]) teardrop2d(bolt_d / 2);
  }
}

module knuckle2d(upper) {
  translate([0, zc]) {
    if (upper) mirror([0, 1]) knuckle_profile();
    else knuckle_profile();
  }
}

// Two hinges, each base-lid-base on one M3 x 30 bolt with a nut.
module knuckles(upper) {
  for (gx = [hinge_in, Sx - hinge_in], k = upper ? [0] : [-1, 1])
    translate([gx + k * (seg_w + seg_gap) - seg_w / 2, 0, 0])
      rotate([90, 0, 90]) linear_extrude(seg_w) knuckle2d(upper);
}

// ---- Base shell ----

module base_shell() {
  difference() {
    union() {
      hull() {
        linear_extrude(0.01) inset2d(bot_ch);
        translate([0, 0, bot_ch]) linear_extrude(base_h - bot_ch - seam_ch) outer2d();
        translate([0, 0, base_h - 0.01]) linear_extrude(0.01) inset2d(seam_ch);
      }
      // lip on the inner half of the rim, it locates the lid
      translate([0, 0, base_h - 0.01]) linear_extrude(lip_h + 0.01) ring2d(wall - lip_t, wall);
    }
    translate([0, 0, floor_t]) linear_extrude(base_h + lip_h) inset2d(wall);
  }
}

module port_cut() {
  translate([0, Sy + 1, 0]) rotate([90, 0, 0]) linear_extrude(wall + 2) translate([px, pz]) win2d();
}

// Dovetail channel in the outer face of the front wall, from under the window up
// through the rim: narrow at the face, wide behind. The slider sits flush in it and
// goes in from the top; the closed lid keeps it in.
module slider_channel() {
  a = sl_w / 2 + 0.2;
  b = sl_w / 2 + sl_t + 0.2;
  translate([px, 0, sl_z0]) linear_extrude(zc + lip_h + 1 - sl_z0)
    polygon([[-a, Sy + 0.01], [a, Sy + 0.01], [b, Sy - sl_t], [b, Sy - sl_t - 0.3], [-b, Sy - sl_t - 0.3], [-b, Sy - sl_t]]);
}

// Two bumps on the channel floor: the hole in the slider tongue clicks over the
// lower one when closed and over the upper one when open.
module slider_bumps() {
  intersection() {
    for (zb = [sl_z0 + 4, sl_z0 + 4 + sl_travel]) translate([px - sl_w / 2 + tg_x, Sy - sl_t - 0.6, zb]) sphere(r = 0.9, $fn = 24);
    translate([px, Sy - sl_t - 0.31, sl_z0]) cube([sl_w, 2, sl_h + sl_travel + 4]);
  }
}

// Boss inside the front wall behind the latch pocket, with a 45 degree underside.
module latch_boss() {
  hull() {
    translate([Sx / 2 - 14, yf - 3, zc - latch_drop - 3]) cube([28, 3.01, latch_drop + 3]);
    translate([Sx / 2 - 14, yf - 0.01, zc - latch_drop - 6]) cube([28, 0.02, 0.01]);
  }
}

// Recess for the flush tongue, a 45 degree lead-in over the wall top, the pocket the
// hook catches in, a scoop for a fingernail under the tongue, and a notch in the
// rim lip where the tongue passes.
module latch_cut() {
  zt = zc - latch_drop;
  translate([Sx / 2 - latch_w / 2 - 0.5, 0, 0]) rotate([90, 0, 90]) linear_extrude(latch_w + 1) {
    translate([Sy - 1.9, zt - 0.5]) square([3, latch_drop + 2]);
    polygon([[Sy - 1.89, zc - 1], [Sy - 1.89, zc + 1], [Sy - 2.9, zc + 1], [Sy - 2.9, zc]]);
    polygon([[Sy - 1.89, zt - 0.2], [Sy - 2.9, zt + 0.81], [Sy - 2.9, zt + 1.59], [Sy - 1.89, zt + 2.6]]);
    polygon([[Sy - 1.9, zt - 0.49], [Sy + 1, zt - 0.49], [Sy + 1, zt - 3.4]]);
  }
  translate([Sx / 2 - latch_w / 2 - 0.6, Sy - wall - 0.2, zc - 0.01]) cube([latch_w + 1.2, wall + 1.2, lip_h + 1]);
}

module base() {
  difference() {
    union() {
      base_shell();
      knuckles(false);
      latch_boss();
    }
    port_cut();
    slider_channel();
    latch_cut();
  }
  slider_bumps();
}

// ---- Whoop tray ----
// Fills the whoop side of the shell up to deck_h, with a pocket around the ducts of
// one whoop. No floor under the pocket: the whoop stands on the floor of the shell.

module whoop_tray() {
  translate([0, 0, fz]) linear_extrude(deck_h) difference() {
    intersection() {
      inset2d(wall + t_clr);
      translate([x_cz + t_clr, 0]) square([Sx, Sy]);
    }
    translate([dx, dy]) offset(r = c_clr) whoop2d(sq, duct_d);
  }
}

// ---- Charger tray and click jaws ----
// The tray fills the charger side up to ct_h, with a pocket in the front right corner
// open to the front and the outer wall, so the charger meets both. Behind the pocket
// run two dovetail grooves, one for each jaw, with a row of sawtooth teeth on their
// floor that rise toward the charger. Local frame of a groove or a jaw: u toward the
// charger, w across, z from the top of the tray; the pocket face at u = 0.

module jaw_groove(lc) {
  translate([-lc, 0, 0]) rotate([90, 0, 90]) linear_extrude(lc + 1)
    polygon([[-4.0, 0.01], [4.0, 0.01], [5.5, -1.5], [5.5, -5.3], [-5.5, -5.3], [-5.5, -1.5]]);
}

module jaw_teeth(lc) {
  translate([0, 5.5, 0]) rotate([90, 0, 0]) linear_extrude(11)
    for (i = [0 : floor(lc) - 1]) polygon([[-lc + i, -5.31], [-lc + i + 1, -4.5], [-lc + i + 1, -5.31]]);
}

module at_side_groove() {
  translate([x_pw, y_ji, fz + ct_h]) rotate(180) children();
}

module at_rear_groove() {
  translate([x_jr, y_pr, fz + ct_h]) rotate(90) children();
}

module charger_tray() {
  difference() {
    translate([0, 0, fz]) linear_extrude(ct_h) difference() {
      intersection() {
        inset2d(wall + t_clr);
        square([x_cz - t_clr, Sy]);
      }
      translate([-1, y_pr]) square([x_pw + 1, Sy]);
    }
    at_side_groove() jaw_groove(l_side + 0.5);
    at_rear_groove() jaw_groove(l_rear + 0.5);
  }
  at_side_groove() jaw_teeth(l_side + 0.5);
  at_rear_groove() jaw_teeth(l_rear + 0.5);
}

// A click jaw, length len. The body rides on the tray, the slider below it runs in
// the dovetail. The lower slab of the slider is a pawl arm, freed by a thin gap and
// rooted at the front, with a tooth under its rear end that drops into the teeth of
// the groove: the jaw slides toward the charger and clicks, but not back. A post on
// the rear end of the arm rises through a slot to above the jaw: lift it to slide
// the jaw back. The lip on the face has a 45 degree underside and hooks over the
// charger.
module jaw(len) {
  zl = charger_h - ct_h + 0.3;
  zb = -4.2;
  difference() {
    union() {
      translate([-len, -5.3, 0]) cube([len, 10.6, jaw_h]);
      translate([-len, 0, 0]) rotate([90, 0, 90]) linear_extrude(len)
        polygon([[-3.8, 0.01], [3.8, 0.01], [5.3, -1.5], [5.3, zb], [-5.3, zb], [-5.3, -1.5]]);
      translate([0, 5.3, 0]) rotate([90, 0, 0]) linear_extrude(10.6) {
        polygon([[-0.01, zl], [1.2, zl + 1.2], [1.2, zl + 1.6], [-0.01, zl + 2.8]]);
        polygon([[-len + 0.3, zb + 0.01], [-len + 0.3, zb - 0.8], [-len + 1.3, zb + 0.01]]);
      }
    }
    // the gap over the pawl arm, open at the rear
    translate([-len - 1, -6, zb + 1.2]) cube([len - 1.5, 12, 0.7]);
    // the slot the release post rises through, open at the rear
    translate([-len - 1, -1.9, zb + 1.2]) cube([3.3, 3.8, jaw_h + 6]);
  }
  // the release post, standing on the rear end of the pawl arm
  translate([-len + 0.3, -1.5, zb + 1.19]) cube([1.6, 3, jaw_h + 2 - zb - 1.19]);
}

// The jaws in the assembly, side_jaw_clicks and rear_jaw_clicks in from fully open:
// the side jaw pushes toward the outer wall, the rear jaw toward the front wall.
module jaws_placed() {
  translate([x_pw - p_side, y_ji, fz + ct_h]) rotate(180) jaw(l_side);
  translate([x_jr, y_pr + p_rear, fz + ct_h]) rotate(90) jaw(l_rear);
}

// Printed on its side, so the pawl arm bends along its layers.
module jaw_flat(len) {
  translate([len, 0, 5.3]) rotate([90, 0, 0]) jaw(len);
}

module jaws_flat() {
  jaw_flat(l_side);
  translate([0, -(jaw_h + 14), 0]) jaw_flat(l_rear);
}

// ---- Port slider ----
// Prints back face down; its edges narrow at 45 degrees toward the front, into the
// dovetail. A tongue cut into it beside the window bends along its layers; the hole
// at its tip clicks over the bumps in the channel. Two grooves across the top of the
// front give the thumb a grip. Local frame: x across, y up the wall, z from the back
// face to the front.

module slider_flat() {
  difference() {
    hull() {
      translate([-sl_t, 0, 0]) cube([sl_w + 2 * sl_t, sl_h, 0.01]);
      translate([0, 0, sl_t - 0.01]) cube([sl_w, sl_h, 0.01]);
    }
    difference() {
      translate([tg_x - 2.1, 1.4, -1]) cube([4.2, 10.6, sl_t + 2]);
      translate([tg_x - 1.5, 2, -2]) cube([3, 11, sl_t + 4]);
    }
    translate([tg_x, 4, -1]) cylinder(d = 1.8, h = sl_t + 2, $fn = 24);
    for (gy = [14.2, 15.4]) translate([-sl_t - 1, gy - 0.4, sl_t - 0.6]) cube([sl_w + 2 * sl_t + 2, 0.8, 1]);
  }
}

module slider_placed() {
  multmatrix([[1, 0, 0, px - sl_w / 2], [0, 0, 1, Sy - sl_t], [0, 1, 0, sl_z0], [0, 0, 0, 1]]) slider_flat();
}

// ---- Lid ----

module lid_shell() {
  difference() {
    translate([0, 0, zc]) hull() {
      linear_extrude(0.01) inset2d(seam_ch);
      translate([0, 0, seam_ch]) linear_extrude(lid_h - seam_ch - top_ch) outer2d();
      translate([0, 0, lid_h - 0.01]) linear_extrude(0.01) inset2d(top_ch);
    }
    translate([0, 0, zc - 1]) linear_extrude(lid_cav + 1) inset2d(wall);
    // recess for the lip of the base
    translate([0, 0, zc - 1]) linear_extrude(lip_h + 0.3 + 1) ring2d(wall - lip_t - lip_c, wall + 0.01);
  }
}

// The tongue is the outer skin of the lid front, carried on down past the rim into
// the recess of the base. Its hook rides over the lead-in and catches in the pocket.
module latch_tongue() {
  zt = zc - latch_drop;
  translate([Sx / 2 - latch_w / 2, 0, 0]) rotate([90, 0, 90]) linear_extrude(latch_w) {
    translate([Sy - tongue_t, zt]) square([tongue_t, latch_drop + 4]);
    polygon([[Sy - tongue_t + 0.01, zt], [Sy - 2.7, zt + 1.1], [Sy - 2.7, zt + 1.3], [Sy - tongue_t + 0.01, zt + 2.4]]);
  }
}

// Two slits and a relief behind the tongue free its upper 4 mm from the lid wall,
// so it flexes over latch_drop plus 4 mm.
module tongue_relief() {
  for (s = [-1, 1]) translate([Sx / 2 + s * (latch_w / 2 + 0.3) - 0.3, Sy - wall - 1, zc - 1]) cube([0.6, wall + 2, 5]);
  translate([Sx / 2 - latch_w / 2 - 0.01, Sy - wall - 1, zc - 1]) cube([latch_w + 0.02, wall + 1 - tongue_t, 5]);
}

function bat_x(i) = Sx / 2 + (i - (battery_count - 1) / 2) * bat_pitch;

// 1S batteries stand on their side, thickness across x, plug end along y. Two pairs
// of arms per battery, clip_y either side of the middle, so short and long batteries
// of the family both sit across them; each arm has a 45 degree bump that snaps over
// the free edge. arm_gap between neighbouring arms leaves room for an arm to give
// while the next slot is full.
module bat_clips() {
  if (battery_count > 0)
    for (i = [0 : battery_count - 1], yc = [Sy / 2 - clip_y, Sy / 2 + clip_y], s = [-1, 1]) {
      f = bat_x(i) + s * (battery_t / 2 + bat_clr);
      zb = pb - battery_w - 0.15;
      translate([0, yc + arm_w / 2, 0]) rotate([90, 0, 0]) linear_extrude(arm_w) {
        translate([s < 0 ? f - arm_t : f, pb - battery_w - 1.6]) square([arm_t, battery_w + 1.6 + 0.01]);
        polygon([[f + s * 0.01, zb], [f - s * bump, zb - bump], [f + s * 0.01, zb - 2 * bump]]);
      }
    }
}

module lid() {
  difference() {
    union() {
      lid_shell();
      knuckles(true);
      bat_clips();
      latch_tongue();
    }
    tongue_relief();
  }
}

module lid_print() {
  translate([0, Sy, zc + lid_h]) rotate([180, 0, 0]) lid();
}

// ---- Ghost contents ----

module ghost_whoop() {
  color("Gainsboro", 0.5) translate([dx, dy, fz]) {
    for (sx = [-1, 1], sy = [-1, 1]) translate([sx * sq / 2, sy * sq / 2, 0])
      difference() { cylinder(d = duct_d, h = 16); translate([0, 0, -1]) cylinder(d = duct_d - 2, h = 18); }
    translate([-12, -16, 0]) cube([24, 32, whoop_h]);
  }
}

// The charger the current jaw positions hold.
module ghost_charger() {
  color("OrangeRed", 0.5) translate([wall, yf - gh_l, fz]) cube([gh_w, gh_l, charger_h]);
}

module ghost_batteries() {
  if (battery_count > 0)
    color("DimGray", 0.6) for (i = [0 : battery_count - 1])
      translate([bat_x(i) - battery_t / 2, Sy / 2 - battery_l / 2, pb - battery_w]) cube([battery_t, battery_l, battery_w]);
}

// ---- Test coupons ----

module coupon(lo, hi) {
  translate(-lo) intersection() { children(); translate(lo) cube(hi - lo); }
}

module test_layout() {
  g = 46;
  // hinge, base and lid half
  coupon([hinge_in - 16, -hk - rk - 1, zc - 14], [hinge_in + 16, 6, zc + lip_h + 1]) base();
  translate([0, 22, 0]) coupon([hinge_in - 16, Sy - 6, 0], [hinge_in + 16, Sy + hk + rk + 1, lid_h + 12]) lid_print();
  // latch, base and lid half
  translate([g, 0, 0]) coupon([Sx / 2 - 16, Sy - 7, zc - latch_drop - 8], [Sx / 2 + 16, Sy + 0.1, zc + lip_h + 1]) base();
  translate([g, 22, 0]) coupon([Sx / 2 - 16, -0.1, 0], [Sx / 2 + 16, 7, lid_h + latch_drop + 1]) lid_print();
  // slider channel with the window, and the slider
  translate([2 * g, 0, 0]) coupon([px - 17, Sy - 5, max(0, sl_z0 - 3)], [px + 17, Sy + 0.1, zc + 0.5]) base();
  translate([2 * g + 4, 12, 0]) slider_flat();
  // a strip of charger tray with the side groove and its teeth, and the side jaw
  translate([3 * g, 0, 0]) coupon([x_pw - 2, y_ji - 9, fz], [x_cz - t_clr + 0.1, y_ji + 9, fz + ct_h]) charger_tray();
  translate([3 * g, jaw_h + 26, 0]) jaw_flat(l_side);
  // one battery clip
  if (battery_count > 0) {
    yc = Sy / 2 - clip_y;
    bx = bat_x(0);
    translate([4 * g, 0, 0])
      coupon([bx - bat_pitch / 2 - 1, Sy - yc - arm_w / 2 - 4, 0], [bx + bat_pitch / 2 + 1, Sy - yc + arm_w / 2 + 4, lid_h + 1]) lid_print();
  }
}

// ---- Layout ----

module assembly() {
  color(color_case) base();
  color(color_case) whoop_tray();
  color(color_case) charger_tray();
  color(color_case) jaws_placed();
  color(color_case) slider_placed();
  if (show_contents) { ghost_whoop(); ghost_charger(); }
  // lid swung open flat behind the base
  translate([0, -hk, zc]) rotate([180, 0, 0]) translate([0, hk, -zc]) {
    color(color_case) lid();
    if (show_contents) ghost_batteries();
  }
}

if (test_print) color(color_case) test_layout();
else if (part == "base") color(color_case) base();
else if (part == "lid") color(color_case) lid_print();
else if (part == "whoop_tray") color(color_case) translate([0, 0, -fz]) whoop_tray();
else if (part == "charger_tray") color(color_case) translate([0, 0, -fz]) charger_tray();
else if (part == "jaws") color(color_case) jaws_flat();
else if (part == "slider") color(color_case) slider_flat();
else assembly();

Comments

Context, not a score. Comments never move a model up or down the results.

No comments yet

Printed it? A note about your material and the clearance you landed on saves the next person a failed print.

Sign in to comment