Read this model as text
A hinged travel case for a tiny whoop, a BetaFPV charger and a row of LAVA 1S batteries, designed for PETG. The case itself is the same for every setup; what changes is two drop-in trays that each bolt to the floor. The whoop tray has a pocket shaped around the whoop you pick, from a Meteor65 Pro II up to a Meteor75 Pro II, with four small catches that click over the duct rims so the whoop stays put when the case is opened upside down. The charger tray fits any BetaFPV charger from the 2-port tester to the 6-port charger: two jaws on slotted channels push it into the front corner and are bolted once; every wall around the charger leans over it by 10 degrees, so it is wedged under all four and cannot lift out. A dial in the front wall turns to open or close the window to the charging port. The lid holds the batteries standing on their side in snap clips. Two pin hinges at the rear, a flush snap latch at the front, a smooth outside.
The image shows the solid OpenSCAD renders at part assembly · test_print false · show_contents false · whoop Meteor75 Pro II · custom_wheelbase 75 · custom_duct_d 48 · custom_frame_h 16 · whoop_h 40 · charger BetaFPV 6-port · custom_port_x 20 · custom_port_z 9 · battery LAVA II 480 580 680 · battery_count 8 · color_case SteelBlue.
Tiny whoop case with drop-in whoop and charger trays, port dial and LAVA battery clips
mick/whoop-case@2.0.0
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.
Parameters
from sourceDescription
What is this
A hinged travel case for a tiny whoop, a BetaFPV charger and a row of LAVA 1S batteries, designed for PETG. The case itself is the same for every setup; what changes is two drop-in trays that each bolt to the floor. The whoop tray has a pocket shaped around the whoop you pick, from a Meteor65 Pro II up to a Meteor75 Pro II, with four small catches that click over the duct rims so the whoop stays put when the case is opened upside down. The charger tray fits any BetaFPV charger from the 2-port tester to the 6-port charger: two jaws on slotted channels push it into the front corner and are bolted once; every wall around the charger leans over it by 10 degrees, so it is wedged under all four and cannot lift out. A dial in the front wall turns to open or close the window to the charging port. The lid holds the batteries standing on their side in snap clips. Two pin hinges at the rear, a flush snap latch at the front, a smooth outside.
What you use it for
Carrying a whoop with its charger and spare batteries in one box, and selling it in variants: print the same case and swap the whoop tray per model. Pick the whoop, the charger and the battery size; the case stays the same size. To fit the charger, drop an M3 nut into each channel from its open end, set the charger in the front outer corner, push the rear jaw and the inner jaw against it and tighten their M3 x 8 bolts. To charge in the case, turn the dial half a turn so its opening lines up with the port. The catches in the whoop tray hold the ducts at their top rim, so push the whoop straight down until it clicks, and lift it by the canopy to take it out.
Design notes
Everything prints without supports. The trays print upright, the jaws upright, the dial front face down, the base upright and the lid upside down. The leaning walls and jaw faces overhang by 10 degrees, which prints cleanly and turns them into a wedge: a charger of any height up to 22 mm touches them with its top edges. Each jaw is a leaning plate with a tab on its side carrying the bolt, so it adds little depth behind the charger; the bolt runs into an M3 nut in a T channel in the tray floor, which keeps the nut from turning and lets the jaw slide over the whole range of BetaFPV chargers. The whoop catches are 1 mm bumps with 45 degree faces at the four diagonal outer points of the ducts; the polypropylene ducts give that much when you push the whoop in. The pocket leaves room for the braces between neighbouring ducts. Both trays bolt down with one M3 into a nut trapped in a boss on the case floor, from above through a counterbore, so they stay in when the case is upside down. The dial sits flush in a round recess, turns on a countersunk M3 x 8 into a nut in a hex pocket in the charger tray, and has notches round its rim for grip; its opening is offset from the pivot, so half a turn opens or closes the port. The windows have chamfered top corners, so their tops are 8 mm bridges. The battery clips fit the LAVA II 480, 580 and 680 (15.5 mm tall, up to 7 mm thick, 61 to 80 mm long) or the 280 and 320 (10 mm tall); the clips sit 20 mm either side of the middle, so every length of the family spans them. Each hinge is base, lid, base on an M3 x 30 with a nut. The case is about 178 x 120 mm and fits a 180 mm bed. test_print renders a coupon set: one hinge and the latch in both halves, the dial in its piece of wall with the dial itself, a jaw with a piece of its channel, one whoop catch, and one battery clip.
Parameters
part picks the assembly or one part in print orientation; jaws lays out both jaws. test_print swaps it for the coupon set, show_contents shows the whoop, charger and batteries as ghosts. whoop picks the whoop tray pocket; custom uses custom_wheelbase (motor to motor across the diagonal), custom_duct_d (outer diameter of one duct) and custom_frame_h (height of the duct rim). whoop_h is the total height of the whoop with its camera and sets the case height. charger places the window at the port of a BetaFPV 6-port or 2-port V2 charger; custom uses custom_port_x (port centre from the outer side of the charger) and custom_port_z (port centre above its bottom). battery picks the LAVA II size family, battery_count the number of clips. color_case is display only. Preset sizes come from the BetaFPV frame specifications; check them against your own whoop.
Use with your agent
{
"mcpServers": {
"remixcad": {
"type": "http",
"url": "https://remix-cad.vercel.app/api/mcp"
}
}
}get_model("mick/whoop-case@2.0.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| Name | Default | Range | Unit | Description |
|---|---|---|---|---|
| part | "assembly" | assembly, base, lid, whoop_tray, charger_tray, jaws, port_dial | ||
| test_print | false | |||
| show_contents | false | |||
| whoop | "Meteor75 Pro II" | Meteor75 Pro II, Meteor75 Pro, Air75 or Meteor75, Meteor65 Pro II, custom | ||
| custom_wheelbase | 75 | 50 to 81 step 0.5 | ||
| custom_duct_d | 48 | 30 to 53 step 0.5 | ||
| custom_frame_h | 16 | 10 to 16.5 step 0.5 | ||
| whoop_h | 40 | 25 to 60 step 1 | ||
| charger | "BetaFPV 6-port" | BetaFPV 6-port, BetaFPV 2-port V2, custom | ||
| custom_port_x | 20 | 8 to 40 step 0.5 | ||
| custom_port_z | 9 | 4 to 16 step 0.5 | ||
| battery | "LAVA II 480 580 680" | LAVA II 480 580 680, LAVA II 280 320 | ||
| battery_count | 8 | 0 to 12 step 1 | ||
| color_case | "SteelBlue" | SteelBlue, White, DimGray, Black, FireBrick, Goldenrod, ForestGreen, MediumPurple |
Lineage
0 models depend on this oneOnly what this model came from and what came from it.
License
Whoever downloads this takes these obligations along- mick/whoop-case@2.0.0 is MIT
Source
main.scad is the entrypointmain.scad
// Tiny whoop case with drop-in trays: the same hinged case for every setup, a whoop
// tray with a pocket per whoop model, and a charger tray that fits any BetaFPV
// charger with two bolted jaws and walls that lean over it. A dial in the front wall
// opens the window to the charging port. The lid holds LAVA 1S batteries standing
// on their side. Printed in PETG, every part without supports.
//
// Coordinates: y = 0 at the rear (hinges), y = Sy at the front (latch). Seen from
// the front with the lid open behind, the viewer's right is low x: the charger tray
// sits there, the whoop tray beside it. 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, port_dial]
// Print a coupon set instead: one hinge and the latch in base and lid halves, the
// dial in its piece of wall, a jaw with a piece of its channel, one whoop catch 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 tray pocket is shaped for.
whoop = "Meteor75 Pro II"; // [Meteor75 Pro II, Meteor75 Pro, Air75 or Meteor75, Meteor65 Pro II, custom]
// For custom: motor to motor across the diagonal, outer diameter of one duct, and
// height of the duct rim above the bottom of the whoop.
custom_wheelbase = 75; // [50:0.5:81]
custom_duct_d = 48; // [30:0.5:53]
custom_frame_h = 16; // [10:0.5:16.5]
// Total height of the whoop with its camera; sets the case height.
whoop_h = 40; // [25:1:60]
/* [Charger] */
// The charger the window lines up with. The charger tray itself fits all of them.
charger = "BetaFPV 6-port"; // [BetaFPV 6-port, BetaFPV 2-port V2, custom]
// For custom: port centre from the outer side of the charger, and above its bottom.
custom_port_x = 20; // [8:0.5:40]
custom_port_z = 9; // [4:0.5:16]
/* [Batteries] */
// LAVA II size family: 480, 580 and 680 are 15.5 mm tall as they stand, 280 and
// 320 are 10 mm.
battery = "LAVA II 480 580 680"; // [LAVA II 480 580 680, LAVA II 280 320]
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 = 48;
wall = 3.2;
floor_t = 2;
top_t = 2;
corner_r = 6;
bot_ch = 1.5;
top_ch = 2;
seam_ch = 0.6;
// lip on the base rim, recess in the lid
lip_h = 2;
lip_t = 1.2;
lip_c = 0.25;
// clearance between a tray and the case
t_clr = 0.3;
// whoop tray: pocket floor, clearance round the whoop, room for the braces between
// ducts, catch bump and width, largest whoop footprint, tray size and height
w_floor = 1.2;
c_clr = 0.5;
brace = 5;
nub = 1;
nub_w = 8;
max_span = 110;
wt_w = max_span + 2 * c_clr + 2.4;
wt_h = w_floor + 16.5 + 2.6;
// charger tray: floor with the channels, wall height and top thickness, lean,
// rims, largest charger width, jaw plate and tab
ct_floor = 5.4;
ct_wh = 22;
ct_wt = 2;
lean = 10;
rim_h = 8;
rim_t = 1.6;
max_cw = 46;
jw_t = 3;
jw_w = 20;
jw_tab = 9;
tl = tan(lean);
ct_w = ct_wt + max_cw + jw_t + ct_wh * tl + 1 + rim_t;
ct_h = ct_floor + ct_wh;
// T channel: bolt slot, nut cavity width and height
ch_slot = 3.6;
ch_nut = 6.2;
ch_nut_h = 2.8;
// M3 hardware and the floor bosses the trays bolt to
bolt_d = 3.4;
nut_af = 5.7;
nut_ac = nut_af / cos(30);
nut_h = 2.6;
bs = 9;
boss_h = 5.7;
// port dial: diameter, thickness, offset of its opening from the pivot
dial_d = 39;
dial_t = 2;
dial_rw = 10;
// battery clips
bat_clr = 0.2;
arm_t = 1.4;
arm_w = 6;
arm_gap = 1.6;
bump = 0.7;
// hinges: knuckle radius, axis behind the rear face, segment width and gap
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;
// whoop presets: wheelbase, duct outer diameter, rim height
sel_w = whoop == "Meteor75 Pro II" ? [80, 52, 16.2]
: whoop == "Meteor75 Pro" ? [80.8, 52, 16.5]
: whoop == "Air75 or Meteor75" ? [75, 47.5, 16.5]
: whoop == "Meteor65 Pro II" ? [70, 42, 15.4]
: [custom_wheelbase, custom_duct_d, custom_frame_h];
wb = sel_w[0];
dd = sel_w[1];
fh = sel_w[2];
sq_s = wb / sqrt(2);
r_n = wb / 2 + dd / 2 + c_clr;
// charger presets: length, width, height, port from the outer side, port height
sel_c = charger == "BetaFPV 2-port V2" ? [52.7, 23.2, 14.2, 11.6, 7.1]
: charger == "custom" ? [79, 46, 19, custom_port_x, custom_port_z]
: [79, 46, 19, 23, 9.5];
cl = sel_c[0];
cw = sel_c[1];
chh = sel_c[2];
// battery family: longest length, height as it stands, thickness
bat = battery == "LAVA II 280 320" ? [64, 10, 6] : [80, 15.5, 7];
battery_l = bat[0];
battery_w = bat[1];
battery_t = bat[2];
bat_pitch = battery_t + 2 * (bat_clr + arm_t) + arm_gap;
Sx_in = ct_w + wt_w + 3 * t_clr;
D = max(wt_w, battery_l + 12);
Sy_in = D + 2 * t_clr;
Sx = Sx_in + 2 * wall;
Sy = Sy_in + 2 * wall;
fz = floor_t;
cav_h = max(w_floor + whoop_h + 2, ct_h + 1, wt_h + 1);
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;
yf = Sy - wall;
// trays
ct_x0 = wall + t_clr;
ct_y0 = wall + t_clr;
wt_x0 = ct_x0 + ct_w + t_clr;
wt_y0 = ct_y0;
wcx = wt_x0 + wt_w / 2;
wcy = wt_y0 + D / 2;
// the charger in its tray, tray coordinates: it touches the leaning walls with its
// top edges
x_o = ct_wt + chh * tl;
y_fc = D - ct_wt - chh * tl;
// window and dial: the opening sits a quarter turn of offset below and outboard of
// the pivot, kept clear of the corner and the bottom chamfer
da = dial_rw / sqrt(2);
px = max(ct_x0 + x_o + sel_c[3], corner_r + 0.5 + dial_d / 2 + 0.2 - da);
pz = max(fz + ct_floor + sel_c[4], bot_ch + 0.5 + dial_d / 2 + 0.2 - da);
pvx = px + da;
pvz = pz + da;
win = [[-7, -4.5], [7, -4.5], [7, 1.5], [4, 4.5], [-4, 4.5], [-7, 1.5]];
// tray bolts
wb_x = wcx;
wb_y = wcy - 45;
cb_x = ct_x0 + 12;
cb_y = ct_y0 + 9;
// ---- 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);
}
// ---- 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 ----
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);
}
}
// Boss on the floor under a tray: an M3 nut slides into a slot open toward +x, under
// 1.2 mm of material, and the tray bolts down into it from above.
module boss_at(x, y) {
translate([x, y, 0]) difference() {
translate([-bs / 2, -bs / 2, fz - 0.01]) cube([bs, bs, boss_h + 0.01]);
translate([0, 0, fz - 0.5]) cylinder(d = bolt_d, h = boss_h + 1);
translate([-nut_ac / 2, -nut_af / 2, fz + boss_h - 1.2 - nut_h]) cube([bs / 2 + nut_ac / 2 + 0.1, nut_af, nut_h]);
}
}
// Window to the charging port. The top corners are chamfered at 45 degrees, so the
// top is an 8 mm bridge.
module port_window(depth) {
translate([0, Sy + 1, 0]) rotate([90, 0, 0]) linear_extrude(depth + 1)
translate([px, pz]) polygon(win);
}
// Round recess the dial sits flush in, and the hole for its pivot bolt.
module dial_recess() {
translate([pvx, Sy + 1, pvz]) rotate([90, 0, 0]) {
cylinder(d = dial_d + 0.4, h = dial_t + 1, $fn = 96);
cylinder(d = bolt_d, h = wall + 2);
}
}
// 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();
boss_at(wb_x, wb_y);
boss_at(cb_x, cb_y);
}
port_window(wall + 1);
dial_recess();
latch_cut();
}
}
// Counterbored M3 hole through a tray, and the recess underneath for the floor boss.
module tray_bolt(x, y, ztop) {
translate([x, y, 0]) {
translate([0, 0, ztop - 3.5]) cylinder(d = 6.5, h = 4);
translate([0, 0, fz - 1]) cylinder(d = bolt_d, h = ztop - fz + 2);
translate([-5.2, -5.2, fz - 0.01]) cube([10.4, 10.4, boss_h + 0.3]);
}
}
// ---- Whoop tray ----
// A block with a pocket shaped round the whoop, with room for the braces between
// neighbouring ducts. Four catches with 45 degree faces stand 1 mm out of the pocket
// wall just above the duct rims, at the outer point of each duct; the whoop clicks
// in under them.
module pocket2d() {
offset(r = c_clr) {
whoop2d(sq_s, dd);
for (a = [0, 90, 180, 270]) rotate(a) translate([-10, sq_s / 2 - 1]) square([20, brace + 1]);
}
}
module whoop_tray() {
zn = fz + w_floor + fh + 0.2;
difference() {
translate([wt_x0, wt_y0, fz]) linear_extrude(wt_h) offset(r = 2) offset(delta = -2) square([wt_w, D]);
translate([wcx, wcy, fz + w_floor]) linear_extrude(wt_h) pocket2d();
tray_bolt(wb_x, wb_y, fz + wt_h);
}
for (sx = [-1, 1], sy = [-1, 1])
translate([wcx + sx * r_n / sqrt(2), wcy + sy * r_n / sqrt(2), 0]) rotate(atan2(-sy, -sx))
translate([0, nub_w / 2, 0]) rotate([90, 0, 0]) linear_extrude(nub_w)
polygon([[-0.8, zn], [0, zn], [nub, zn + nub], [nub, zn + nub + 0.3], [0, zn + 2 * nub + 0.3], [-0.8, zn + 2 * nub + 0.3]]);
}
// ---- Charger tray ----
// A floor with two T channels, a front wall and an outer wall that lean over the
// pocket by lean degrees, low rims at the rear and the inner side, and a block at
// the rear corner for the bolt. Two jaws slide on the channels and push the charger
// into the front outer corner; every face it meets leans over it, so a charger of
// any height up to ct_wh is wedged in by its top edges.
// T channel in the tray floor from a to b, tray coordinates: a slot right through
// for the bolt, and a cavity for an M3 nut that cannot turn in it.
module t_channel(a, b) {
hull() for (p = [a, b]) translate([p[0], p[1], -1]) cylinder(d = ch_slot, h = ct_floor + 2);
hull() for (p = [a, b]) translate([p[0] - ch_nut / 2, p[1] - ch_nut / 2, 1]) cube([ch_nut, ch_nut, ch_nut_h]);
}
module charger_tray() {
hb = ct_wh * tl;
difference() {
translate([ct_x0, ct_y0, fz]) union() {
cube([ct_w, D, ct_floor]);
rotate([90, 0, 90]) linear_extrude(ct_w)
polygon([[D, ct_floor - 0.01], [D - ct_wt, ct_floor - 0.01], [D - ct_wt - hb, ct_h], [D, ct_h]]);
translate([0, D, 0]) rotate([90, 0, 0]) linear_extrude(D)
polygon([[0, ct_floor - 0.01], [ct_wt, ct_floor - 0.01], [ct_wt + hb, ct_h], [0, ct_h]]);
cube([ct_w, rim_t, ct_floor + rim_h]);
translate([ct_w - rim_t, 0, 0]) cube([rim_t, D, ct_floor + rim_h]);
cube([24, 18, ct_floor + 8]);
}
translate([ct_x0, ct_y0, fz]) {
t_channel([30.5, -4], [30.5, 56]);
t_channel([27, D - ct_wt - 25.5], [ct_w + 4, D - ct_wt - 25.5]);
}
port_window(wall + t_clr + 12);
// hex pocket for the nut of the dial bolt, a point up so it prints clean
translate([pvx, ct_y0 + D + 0.01, pvz]) rotate([90, 0, 0]) {
rotate([0, 0, 90]) cylinder(d = nut_ac + 0.3, h = nut_h + 0.3, $fn = 6);
cylinder(d = bolt_d, h = 7);
}
tray_bolt(cb_x, cb_y, fz + ct_floor + 8);
}
}
// ---- Jaw ----
// A plate leaning lean degrees toward the charger with a tab on its side for the
// bolt. Local frame: d toward the charger (the face top at d = 0), w along the jaw,
// z up from the tray floor. Prints upright.
module jaw() {
hb = ct_wh * tl;
difference() {
union() {
translate([0, jw_w / 2, 0]) rotate([90, 0, 0]) linear_extrude(jw_w)
polygon([[-hb, 0], [0, ct_wh], [-jw_t, ct_wh], [-jw_t - hb, 0]]);
translate([-jw_t - hb, jw_w / 2 - 0.01, 0]) cube([jw_t, jw_tab + 0.01, 3]);
}
translate([-jw_t / 2 - hb, jw_w / 2 + jw_tab / 2, -1]) cylinder(d = bolt_d, h = 5);
}
}
// The jaws placed against the chosen charger: the rear one pushes it forward, the
// inner one pushes it outward. Each face touches the charger at its top edge.
module jaws_placed() {
y0r = (y_fc - cl) + (ct_wh - chh) * tl;
x0i = (x_o + cw) - (ct_wh - chh) * tl;
multmatrix([[0, 1, 0, ct_x0 + 16], [1, 0, 0, ct_y0 + y0r], [0, 0, 1, fz + ct_floor], [0, 0, 0, 1]]) jaw();
multmatrix([[-1, 0, 0, ct_x0 + x0i], [0, 1, 0, ct_y0 + D - ct_wt - 40], [0, 0, 1, fz + ct_floor], [0, 0, 0, 1]]) jaw();
}
module jaws_flat() {
hb = ct_wh * tl;
for (k = [0, 1]) translate([jw_t + hb + 2, k * (jw_w + jw_tab + 4) + jw_w / 2, 0]) jaw();
}
// ---- Port dial ----
// A disc flush in the front wall, turning on a countersunk M3 x 8 into the nut in the
// charger tray. Its opening sits dial_rw off the pivot, so half a turn opens or
// closes the port. Notches round the rim give grip. Local frame: the front face at
// z = 0, so it prints front face down.
module dial(open) {
difference() {
cylinder(d = dial_d, h = dial_t, $fn = 96);
translate([0, 0, -0.01]) cylinder(d1 = 6.4, d2 = 3.4, h = 1.5);
translate([0, 0, -1]) cylinder(d = bolt_d, h = dial_t + 2);
rotate(open ? 0 : 180) translate([-da, -da, -1]) linear_extrude(dial_t + 2) polygon(win);
for (i = [0 : 11]) rotate(i * 30) translate([dial_d / 2 + 0.6, 0, -1]) cylinder(d = 3, h = dial_t + 2);
}
}
module dial_placed() {
multmatrix([[1, 0, 0, pvx], [0, 0, -1, Sy], [0, 1, 0, pvz], [0, 0, 0, 1]]) dial(false);
}
// ---- 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;
// Batteries stand on their side, thickness across x, plug end along y. Two pairs of
// arms per battery, 20 mm either side of the middle so every length of the family
// spans them, each with a 45 degree bump that snaps over its free edge.
module bat_clips() {
if (battery_count > 0)
for (i = [0 : battery_count - 1], yc = [Sy / 2 - 20, Sy / 2 + 20], 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([wcx, wcy, fz + w_floor]) {
for (sx = [-1, 1], sy = [-1, 1]) translate([sx * sq_s / 2, sy * sq_s / 2, 0])
difference() { cylinder(d = dd, h = fh); translate([0, 0, -1]) cylinder(d = dd - 3, h = fh + 2); }
translate([-12, -16, 0]) cube([24, 32, whoop_h]);
}
}
module ghost_charger() {
color("OrangeRed", 0.5) translate([ct_x0 + x_o, ct_y0 + y_fc - cl, fz + ct_floor]) cube([cw, cl, chh]);
}
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 = 50;
cx = wcx + r_n / sqrt(2);
cy = wcy + r_n / sqrt(2);
// 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();
// the dial in its piece of front wall, and the dial
translate([2 * g, 0, 0]) coupon([pvx - 21, Sy - 4, max(0, pvz - 21)], [pvx + 21, Sy + 0.1, pvz + 21]) base();
translate([2 * g + 21, 30, 0]) dial(false);
// a jaw and a piece of the inner channel with the rim
translate([3 * g, 0, 0])
coupon([ct_x0 + 22, ct_y0 + D - 40, fz], [ct_x0 + ct_w + 0.1, ct_y0 + D - 14, fz + ct_floor + 1]) charger_tray();
translate([3 * g + 8, 34, 0]) jaw();
// one whoop catch with a piece of pocket wall
translate([4 * g, 0, 0]) coupon([cx - 10, cy - 10, fz], [cx + 10, cy + 10, fz + wt_h]) whoop_tray();
// one battery clip
if (battery_count > 0) {
yc = Sy / 2 - 20;
bx = bat_x(0);
translate([4 * g, 26, 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) dial_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([-wt_x0, -wt_y0, -fz]) whoop_tray();
else if (part == "charger_tray") color(color_case) translate([-ct_x0, -ct_y0, -fz]) charger_tray();
else if (part == "jaws") color(color_case) jaws_flat();
else if (part == "port_dial") color(color_case) dial(false);
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.
