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, no loose inserts and no hardware. The base is filled up to a deck with two pockets cut to size: one shaped around the ducts of the whoop, one for the charger in the front right corner (seen from the front with the lid open behind). Small crush ribs in both pockets give a press fit, so the whoop and the charger stay in. A slider fills a dovetail channel in the front wall from under the charging window up to the rim, flush with the wall, and works like the cover of the parts box: with the lid closed it cannot move, and with the lid open you slide it up and out. The latch sits flush in a recess. The lid holds the batteries standing on their side in snap clips, and beside them a parts box with a sliding cover for props, screws and small tools. Two hinges at the rear turn on printed snap pins. Fillets, chamfers and a few ribs strengthen the small parts and give the inside a finished look. The case is only as big as its contents need: a smaller whoop, charger, battery row or parts box makes it narrower and shallower.
The image shows the solid OpenSCAD renders at part assembly · test_print false · show_contents false · whoop_wheelbase 80 · duct_d 50 · gap_fill 8 · whoop_h 40 · charger BetaFPV 6-port · custom_l 79 · custom_w 46 · custom_h 19 · port_from_side 23 · port_z 9.5 · battery_class LAVA 480-680 · battery_count 8 · parts_box_w 35 · color_case SteelBlue.
Tiny whoop case sized to its contents, with press-fit pockets, snap-pin hinges and a parts box
mick/whoop-case@3.4.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, its charger and a row of 1S batteries, designed for PETG, with a smooth outside, no loose inserts and no hardware. The base is filled up to a deck with two pockets cut to size: one shaped around the ducts of the whoop, one for the charger in the front right corner (seen from the front with the lid open behind). Small crush ribs in both pockets give a press fit, so the whoop and the charger stay in. A slider fills a dovetail channel in the front wall from under the charging window up to the rim, flush with the wall, and works like the cover of the parts box: with the lid closed it cannot move, and with the lid open you slide it up and out. The latch sits flush in a recess. The lid holds the batteries standing on their side in snap clips, and beside them a parts box with a sliding cover for props, screws and small tools. Two hinges at the rear turn on printed snap pins. Fillets, chamfers and a few ribs strengthen the small parts and give the inside a finished look. The case is only as big as its contents need: a smaller whoop, charger, battery row or parts box makes it narrower and shallower.
What you use it for
Carrying one whoop with its charger, spare 1S batteries and a few spare parts in one box. Set the whoop by its wheelbase, duct diameter and height, pick the charger (BetaFPV 6-port, 79 x 46 x 19 mm, or the BetaFPV BT2.0 charger board, 75 x 42 x 22 mm, or custom sizes), and set where its port sits. The whoop pocket follows the ducts but does not reach deep in between them, where most whoops have frame braces; gap_fill sets how far it is filled in there. The charger presses into its pocket once and stays: the ribs on the rear and inner wall push it into the corner against the front wall, with its port at the window. To charge, open the lid, slide the port slider up and plug the cable in: let the slider rest on the plug, or take it out. Resting on the plug it stands above the rim, so take it out if you want to charge with the lid closed. Unplugged, it slides back down to the bottom of its channel, closes the window and sits flush with the rim. The whoop drops into its pocket with a light click from four ribs on the diagonals and lifts out by hand. 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). The parts box runs the full depth of the lid on the far left as you look at the open case, about 16 mm deep. With the lid open, slide its cover out over the front edge of the lid; with the case closed, a key on the base rim fills the slot it slides through, so the cover cannot move and the outside stays smooth. To fit the lid, line up its knuckles between those of the base and push a snap pin through each hinge from the outer side, head first: the split tail clicks open behind the far knuckle. To take a pin out, squeeze the two barbs together with tweezers and push it back. Print the fit frames first: a thin ring of each pocket with its ribs, to check the fit on your printer in a few minutes before the whole base.
Design notes
Everything prints without supports and the floor is closed. The deck fills the base up to the pockets; slice it with sparse infill. The whoop pocket is the outline of the four ducts and the frame between them, closed with a radius of gap_fill: every inward notch narrower than twice that radius is filled, so the deck between two ducts ends in a round tip well short of the line through their centres. The pockets have 0.3 mm (charger) and 0.5 mm (whoop) clearance; the ribs stand 0.25 and 0.2 mm into that, are upright so they print cleanly, and taper at the top so the charger or the whoop slides onto them. Reinforcement and finish: the top edges of both pockets carry a 0.8 mm chamfer, a lead-in for the whoop and the charger; a 1.5 mm 45 degree fillet runs where the deck meets the walls, where the lid plate meets the lid walls, and along both walls of the parts box, inside and out, stiffening the thin walls; every battery arm has a root fillet, 0.6 mm on its outer side and 0.4 mm on the battery side, where its bending stress peaks; a rib inside the front wall, above the charger and with a 45 degree underside, backs the thinned wall behind the slider channel; the two slits that free the latch tongue end in round holes, so they cannot tear on; the split tail of each snap pin starts at a round relief; the sliding plates have chamfered leading corners so they find their dovetail; and the key on the base rim has chamfered top edges. The fillets are stepped in 0.25 mm, about a layer, so they print as smooth slopes. The snap pins print lying down, so they are strong along their length; a flat underside keeps them on the bed. The tail is split lengthwise in the plane of the bed, so its two halves bend sideways, along their layers, as the barbs squeeze through the 4 mm hole; the barbs then catch in a counterbore in the far knuckle. Head and tail sit 0.2 mm inside counterbores at both ends of each hinge, which are teardrops with a short flat top so they print in a wall. The pin holes are teardrops too. The port slider and the parts box cover share one design: a plate whose edges narrow at 45 degrees, running in a 45 degree dovetail, printed wide face down, with grooves across one end for a fingertip. The slider channel is narrow at the outer face and wide behind, cut straight up through the rim; its floor is the stop at the bottom, and the slider fills it to 0.2 mm under the rim. The walls of the parts box rise from the lid plate and carry the dovetail along their free edge. 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. With a Meteor75 Pro II, the BetaFPV 6-port charger, eight batteries and the parts box the case is about 169 x 116 mm. test_print renders the mechanism coupons: one hinge in both halves with a snap pin, the latch in both halves, the slider channel with the slider, one battery clip, and the front end of the parts box with a piece of its cover.
Parameters
part picks the assembly or one part in print orientation; pins lays out both snap pins, fit_frames a thin ring of each pocket with its ribs. test_print swaps it for the mechanism coupons, 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 whoop pocket, gap_fill how far it is filled in between the ducts (0 follows the ducts all the way in), whoop_h (total height with camera) the height of the case. charger picks a BetaFPV charger or custom; custom_l, custom_w and custom_h are its size when custom. 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. battery_class picks the LAVA size family, battery_count the number of clips. parts_box_w is the inside width of the parts box, 0 for none. 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@3.4.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, slider, box_cover, pins, fit_frames | ||
| test_print | false | |||
| show_contents | false | |||
| whoop_wheelbase | 80 | 55 to 100 step 0.5 | ||
| duct_d | 50 | 28 to 65 step 0.5 | ||
| gap_fill | 8 | 0 to 16 step 0.5 | ||
| whoop_h | 40 | 20 to 60 step 1 | ||
| charger | "BetaFPV 6-port" | BetaFPV 6-port, BetaFPV BT2.0 board, custom | ||
| custom_l | 79 | 40 to 120 step 0.5 | ||
| custom_w | 46 | 25 to 70 step 0.5 | ||
| custom_h | 19 | 8 to 35 step 0.5 | ||
| port_from_side | 23 | 10 to 40 step 0.5 | ||
| port_z | 9.5 | 6 to 20 step 0.5 | ||
| battery_class | "LAVA 480-680" | LAVA 480-680, LAVA 280-320 | ||
| battery_count | 8 | 0 to 12 step 1 | ||
| parts_box_w | 35 | 0 to 60 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@3.4.0 is MIT
Source
main.scad is the entrypointmain.scad
// Tiny whoop case: a whoop, its charger, a row of 1S batteries and a few spare parts
// in one hinged box, printed in PETG, with a smooth outside, no loose inserts and no
// hardware. The base is filled up to a deck with a pocket around the ducts of the
// whoop and a pocket for the charger in the front right corner (seen from the front
// with the lid open behind); crush ribs in both give a press fit. A flush slider
// covers the charging window, from under it up to the rim. The lid holds the
// batteries on their side in snap clips and, beside them, a parts box with a sliding
// cover. The two hinges turn on printed snap pins. Fillets, chamfers and a few ribs
// strengthen the small parts. The case is only as big as its contents need. 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 parts box runs along the high x side of the lid. The lid is modelled
// closed and flipped for printing.
/* [Part] */
// The whole case with the lid open, or one part in print orientation. pins lays out
// both snap pins; fit_frames is a thin ring of each pocket with its ribs, to check
// the fit before the whole base.
part = "assembly"; // [assembly, base, lid, slider, box_cover, pins, fit_frames]
// Print the mechanism coupons instead: one hinge in both halves with a snap pin,
// the latch in both halves, the slider channel with the slider, one battery clip,
// and the front end of the parts box with a piece of its cover.
test_print = false;
// Show the whoop, the charger and the batteries as ghosts in the assembly.
show_contents = false;
/* [Whoop] */
// Motor to motor across the diagonal: 80 for a Meteor75 Pro II.
whoop_wheelbase = 80; // [55:0.5:100]
// Outer diameter of one duct.
duct_d = 50; // [28:0.5:65]
// How far the pocket is filled in between the ducts, as a rounding radius: the
// higher, the shorter the deck reaches in between two ducts. 0 follows the ducts
// all the way in.
gap_fill = 8; // [0:0.5:16]
// Total height of the whoop, camera included: sets the height of the case.
whoop_h = 40; // [20:1:60]
/* [Charger] */
// The charger the pocket is cut for.
charger = "BetaFPV 6-port"; // [BetaFPV 6-port, BetaFPV BT2.0 board, custom]
// Size of a custom charger: length (port end to back), width and height.
custom_l = 79; // [40:0.5:120]
custom_w = 46; // [25:0.5:70]
custom_h = 19; // [8:0.5:35]
// 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]
/* [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]
/* [Parts box] */
// Inside width of the parts box in the lid, beside the batteries, with a sliding
// cover. 0 for no box.
parts_box_w = 35; // [0:1:60]
/* [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;
// fillets: size and number of steps (0.25 mm, about a layer); chamfer on the pocket
// edges
fil = 1.5;
fil_n = 6;
pocket_ch = 0.8;
// deck that fills the base around the pockets, clearance around the whoop and the
// charger, deck left between the pockets and around the whoop
deck_h = 14;
w_clr = 0.5;
c_clr = 0.3;
c_web = 3;
w_web = 1.2;
// crush ribs: how far into the clearance they stand past the device, base width
rib_c = 0.25;
rib_w = 0.2;
rib_b = 1.6;
// the charger
ch_l = charger == "BetaFPV 6-port" ? 79 : charger == "BetaFPV BT2.0 board" ? 75 : custom_l;
ch_w = charger == "BetaFPV 6-port" ? 46 : charger == "BetaFPV BT2.0 board" ? 42 : custom_w;
ch_h = charger == "BetaFPV 6-port" ? 19 : charger == "BetaFPV BT2.0 board" ? 22 : custom_h;
// port slider: width, how far it reaches below the middle of the window, thickness
sl_w = 26;
sl_below = 8;
sl_t = 1.6;
// parts box: wall, cover thickness, gap between the rim and the cover
pb_wall = 3;
pb_t = 1.6;
pb_g = 0.3;
box_on = parts_box_w > 0;
// 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: knuckle radius, axis behind the rear face, segment width and gap, length
// of one hinge (base, lid, base)
rk = 4;
hk = rk + 0.5;
seg_w = 8;
seg_gap = 0.4;
hinge_in = 22;
grp = 3 * seg_w + 2 * seg_gap;
// snap pins: hole, pin, head diameter and thickness, depth of the counterbore for
// the head and for the tail, radius of the tail counterbore, barb reach, height of
// the pin axis above its flat underside
pin_hole = 4;
pin_d = 3.6;
head_d = 5.6;
head_t = 1.4;
head_cb = 1.6;
tail_cb = 1.5;
tail_r = 2.8;
barb_r = 2.4;
pin_c0 = 1.3;
pin_l = grp - 0.4;
// latch
latch_w = 20;
latch_drop = 10;
tongue_t = 1.6;
sq = whoop_wheelbase / sqrt(2);
whoop_span = sq + duct_d;
whoop_zone = whoop_span + 2 * w_clr + 2 * w_web;
charger_zone = ch_w + c_clr + c_web;
bat_pitch = battery_t + 2 * (bat_clr + arm_t) + arm_gap;
lid_need = battery_count * bat_pitch + 8 + (box_on ? parts_box_w + 2 * pb_wall + 2 : 0);
Sx_in = max(charger_zone + whoop_zone, lid_need, 2 * hinge_in + 30);
Sy_in = max(whoop_zone, ch_l + c_clr + c_web, battery_l + 12);
Sx = Sx_in + 2 * wall;
Sy = Sy_in + 2 * wall;
fz = floor_t;
cav_h = max(whoop_h + 2, ch_h + 4, 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 pocket: against the outer and the front wall
yf = Sy - wall;
cx_in = wall + ch_w + c_clr;
cy_r = yf - ch_l - c_clr;
// whoop centre, in the zone right of the charger
dx = wall + charger_zone + (Sx_in - charger_zone) / 2;
dy = Sy / 2;
r_rib = whoop_wheelbase / 2 + duct_d / 2 + w_clr;
// parts box across x: inner faces of its outer wall and of its partition; the free
// edge of its walls just above the rim
bi1 = Sx - wall - pb_wall;
bi0 = bi1 - parts_box_w;
z_e = zc + pb_g;
// the battery row, centred in what the box leaves
xr0 = wall + 4;
xr1 = box_on ? bi0 - pb_wall - 2 : Sx - wall - 4;
bat_cx = (xr0 + xr1) / 2;
// charging window, and the slider from sl_below under its middle up to the rim
px = max(corner_r + sl_w / 2 + sl_t + 1.5, wall + port_from_side);
pz = fz + max(9.5, min(port_z, cav_h - 10));
sl_z0 = pz - sl_below;
sl_h = zc - 0.2 - sl_z0;
// ---- 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);
}
// The same with its tip cut flat at top, a short bridge, so it stays inside a
// knuckle.
module teardrop_flat2d(r, top) {
intersection() {
teardrop2d(r);
translate([-r - 1, -r - 1]) square([2 * r + 2, r + 1 + top]);
}
}
// 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 whoop pocket, about the whoop centre: the outline with its clearance, closed
// with a radius of gap_fill, so the notches between the ducts are filled in and the
// deck there ends in a round tip instead of reaching in between them.
module whoop_pocket2d() {
if (gap_fill > 0)
offset(r = -gap_fill) offset(r = gap_fill) offset(r = w_clr) whoop2d(sq, duct_d);
else
offset(r = w_clr) whoop2d(sq, duct_d);
}
// The charger pocket, open into the outer and the front wall.
module charger_pocket2d() {
translate([wall - 1, cy_r]) offset(r = 1) offset(delta = -1) square([cx_in - wall + 1, yf - cy_r + 1]);
}
// The deck in plan: the inside of the base less the two pockets.
module deck2d() {
difference() {
inset2d(wall - 0.01);
translate([dx, dy]) whoop_pocket2d();
charger_pocket2d();
}
}
// 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]]);
}
// ---- Sliding plate ----
// The port slider and the parts box cover: a plate w wide on its narrow face, l long
// and t thick, whose edges flare at 45 degrees to the wide face, printed wide face
// down. The leading corners at y = 0 are chamfered so the plate finds its dovetail.
// Three grooves across the narrow face near the far end give a fingertip a grip.
// Local frame: x across, y along, z from the wide face to the narrow face.
module slide_plate(w, l, t) {
difference() {
hull() {
translate([-t, 0, 0]) cube([w + 2 * t, l, 0.01]);
translate([0, 0, t - 0.01]) cube([w, l, 0.01]);
}
for (gy = [l - 3, l - 5.5, l - 8]) translate([-t - 1, gy - 0.4, t - 0.6]) cube([w + 2 * t + 2, 0.8, 1]);
for (m = [0, 1]) translate([m ? w : 0, 0, -1]) mirror([m, 0, 0]) linear_extrude(t + 2)
polygon([[-t - 1, -1], [2.2, -1], [-t - 1, t + 2.2]]);
}
}
// ---- Crush rib ----
// An upright rib on a pocket wall at the origin, standing d into the pocket along
// +x, tapered at the top so the device slides onto it.
module rib(h, d) {
hull() {
linear_extrude(h - 1.5) polygon([[-0.3, -rib_b / 2], [d, 0], [-0.3, rib_b / 2]]);
translate([-0.3, -rib_b / 2, h - 0.01]) cube([0.31, rib_b, 0.01]);
}
}
// Two ribs on the inner wall and two on the rear wall of the charger pocket: they
// press the charger into the corner against the front and the outer wall.
module charger_ribs(x0, y0, h) {
for (f = [0.25, 0.75]) {
translate([x0 + ch_w + c_clr, y0 + ch_l * f, 0]) rotate(180) rib(h, c_clr + rib_c);
translate([x0 + ch_w * f, y0, 0]) rotate(90) rib(h, c_clr + rib_c);
}
}
// Four ribs on the diagonals, at the outer point of each duct, pushing toward the
// centre. Drawn about the whoop centre.
module whoop_ribs(h) {
for (sx = [-1, 1], sy = [-1, 1])
translate([sx * r_rib / sqrt(2), sy * r_rib / sqrt(2), 0]) rotate(atan2(-sy, -sx)) rib(h, w_clr + rib_w);
}
// ---- 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; the pin hole is a teardrop.
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(pin_hole / 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 snap pin.
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);
}
// The ends of each hinge, as a transform: children are drawn for the left hinge,
// head end at x = 0 going +x; the right hinge is its mirror, head at its outer end.
module at_hinges() {
translate([hinge_in - grp / 2, 0, 0]) children();
translate([Sx - hinge_in + grp / 2, 0, 0]) mirror([1, 0, 0]) children();
}
// Counterbores in the end faces of the outer base knuckles: one for the pin head,
// one at the far end for the barbs of the tail.
module pin_counterbores() {
at_hinges() {
translate([-0.01, 0, 0]) rotate([90, 0, 90]) linear_extrude(head_cb + 0.01)
translate([-hk, zc]) teardrop_flat2d(head_d / 2 + 0.2, head_d / 2 + 0.8);
translate([grp - tail_cb, 0, 0]) rotate([90, 0, 90]) linear_extrude(tail_cb + 0.01)
translate([-hk, zc]) teardrop_flat2d(tail_r, tail_r + 0.8);
}
}
// ---- Snap pin ----
// Printed lying down, flat underside on the bed, axis pin_c0 above it, head at x = 0.
// The tail is split lengthwise in the plane of the bed, so its halves bend sideways,
// along their layers, as the barbs squeeze through the hole; past it they spring out
// and catch in the counterbore of the far knuckle. The split starts at a round relief
// so it cannot tear on. Head and tail sit 0.2 mm inside their counterbores.
module snap_pin() {
xb = grp - 0.2 - tail_cb + 0.1;
difference() {
intersection() {
union() {
translate([0, 0, pin_c0]) rotate([0, 90, 0]) cylinder(d = head_d, h = head_t, $fn = 32);
translate([0, 0, pin_c0]) rotate([0, 90, 0]) cylinder(d = pin_d, h = pin_l, $fn = 32);
for (s = [-1, 1]) translate([0, 0, pin_c0 - 0.6]) linear_extrude(1.2)
polygon([[xb, s * 0.9], [xb, s * barb_r], [pin_l, s * (pin_d / 2)], [pin_l, s * 0.9]]);
}
translate([-1, -5, 0]) cube([pin_l + 2, 10, 10]);
}
// the split tail, back to the start of the far base knuckle, with a round relief
translate([grp - seg_w, -0.5, -1]) cube([pin_l, 1, 10]);
translate([grp - seg_w, 0, -1]) cylinder(d = 1.6, h = 10, $fn = 16);
}
}
module pins_placed() {
at_hinges() translate([0.2, -hk, zc - pin_c0]) snap_pin();
}
module pins_flat() {
snap_pin();
translate([0, head_d + 3, 0]) snap_pin();
}
// ---- 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);
}
}
// The deck fills the base up to deck_h around the two pockets. Slice it with sparse
// infill.
module deck() {
translate([0, 0, fz - 0.01]) linear_extrude(deck_h + 0.01) deck2d();
}
// A 45 degree fillet where the deck meets the walls, in steps of about a layer.
module deck_wall_fillet() {
st = fil / fil_n;
for (i = [0 : fil_n - 1]) let (w = fil - i * st)
translate([0, 0, fz + deck_h + i * st - 0.01]) linear_extrude(st + 0.02)
intersection() {
difference() { inset2d(wall - 0.01); inset2d(wall + w); }
deck2d();
}
}
// Chamfer on the top edges of both pockets: a lead-in for the whoop and the charger.
module pocket_chamfers() {
st = pocket_ch / 4;
for (i = [0 : 3]) translate([0, 0, fz + deck_h - pocket_ch + i * st]) linear_extrude(st + 0.01)
intersection() {
inset2d(wall + 0.01);
union() {
translate([dx, dy]) offset(r = (i + 1) * st) whoop_pocket2d();
offset(r = (i + 1) * st) charger_pocket2d();
}
}
}
// Key on the front rim, with chamfered top edges: it fills the slot the parts box
// cover slides out through, so the cover stays put and the outside is smooth when
// the case is closed.
module box_key() {
kx = bi0 - pb_t - 0.2;
kw = parts_box_w + 2 * pb_t + 0.4;
kd = wall - seam_ch;
kh = pb_g + pb_t + 0.8;
if (box_on) hull() {
translate([kx, Sy - wall, zc - 0.6]) cube([kw, kd, kh - 0.4]);
translate([kx + 0.4, Sy - wall, zc - 0.6]) cube([kw - 0.8, kd - 0.4, kh]);
}
}
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, its floor the stop at the bottom.
// The slider fills it up to the rim; 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]]);
}
// Rib inside the front wall behind the slider channel, above the charger, with a
// 45 degree underside: it backs the thinned wall.
module channel_rib() {
zb = fz + ch_h + 1.5;
x0 = px - sl_w / 2 - sl_t - 2;
if (zc - zb > 4) hull() {
translate([x0, yf - 2, zb + 2]) cube([sl_w + 2 * sl_t + 4, 2.01, zc - zb - 2]);
translate([x0, yf - 0.01, zb]) cube([sl_w + 2 * sl_t + 4, 0.02, 0.01]);
}
}
// 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);
deck();
deck_wall_fillet();
latch_boss();
channel_rib();
box_key();
}
pin_counterbores();
port_cut();
slider_channel();
latch_cut();
pocket_chamfers();
}
translate([0, 0, fz - 0.01]) charger_ribs(wall, cy_r, deck_h + 0.01);
translate([dx, dy, fz - 0.01]) whoop_ribs(deck_h + 0.01);
}
// ---- Port slider ----
// The sliding plate, wide face against the wall, from the bottom of its channel to
// just under the rim; the grip grooves at its top end.
module slider_flat() {
slide_plate(sl_w, sl_h, sl_t);
}
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();
}
// ---- Fit frames ----
// A thin ring of each pocket with its ribs, 6 mm tall: press the charger and the
// whoop in to check the fit on your printer before printing the whole base.
module fit_frames() {
translate([2, 2, 0]) {
linear_extrude(6) difference() {
offset(r = 2) square([ch_w + c_clr, ch_l + c_clr]);
square([ch_w + c_clr, ch_l + c_clr]);
}
charger_ribs(0, 0, 6);
}
translate([ch_w + 10 + whoop_span / 2 + w_clr + 2, whoop_span / 2 + w_clr + 2, 0]) {
linear_extrude(6) difference() {
offset(r = 2) whoop_pocket2d();
whoop_pocket2d();
}
whoop_ribs(6);
}
}
// ---- 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);
}
}
// A 45 degree fillet under the lid plate along the lid walls, and along both faces
// of the parts box walls, in steps of about a layer.
module lid_fillets() {
st = fil / fil_n;
for (i = [0 : fil_n - 1]) let (w = fil - i * st)
translate([0, 0, pb - (i + 1) * st]) linear_extrude(st + 0.01) {
difference() { inset2d(wall - 0.01); inset2d(wall + w); }
if (box_on) for (x0 = [bi0, bi1 - w, bi0 - pb_wall - w]) translate([x0, wall]) square([w, Sy - 2 * wall]);
}
}
// 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. The slits end in round holes so they
// cannot tear on.
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 + s * (latch_w / 2 + 0.3), Sy - wall - 1, zc + 4]) rotate([-90, 0, 0]) cylinder(d = 1.2, h = wall + 2, $fn = 16);
}
translate([Sx / 2 - latch_w / 2 - 0.01, Sy - wall - 1, zc - 1]) cube([latch_w + 0.02, wall + 1 - tongue_t, 5]);
}
// Parts box: two walls along the full depth of the lid, from the plate to just above
// the rim, the outer one against the lid wall. A dovetail groove runs along the free
// edge of each, 45 degrees, for the cover.
module box_walls() {
if (box_on) difference() {
for (x0 = [bi0 - pb_wall, bi1]) translate([x0, wall - 0.01, z_e]) cube([pb_wall, Sy - 2 * wall + 0.02, pb - z_e + 0.01]);
translate([0, Sy + 1, 0]) rotate([90, 0, 0]) linear_extrude(Sy + 2) {
polygon([[bi1 - 0.01, z_e - 0.01], [bi1, z_e - 0.01], [bi1 + pb_t + 0.2, z_e + pb_t + 0.2], [bi1 - 0.01, z_e + pb_t + 0.2]]);
polygon([[bi0 + 0.01, z_e - 0.01], [bi0, z_e - 0.01], [bi0 - pb_t - 0.2, z_e + pb_t + 0.2], [bi0 + 0.01, z_e + pb_t + 0.2]]);
}
}
}
// Slot in the lid front wall the cover slides out through; the key on the base rim
// fills it when the case is closed.
module box_slot() {
if (box_on) translate([bi0 - pb_t - 0.4, Sy - wall - 1, zc - 0.01]) cube([parts_box_w + 2 * pb_t + 0.8, wall + 2, pb_g + pb_t + 0.4]);
}
function bat_x(i) = bat_cx + (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, and a root fillet on both faces where its bending stress peaks.
// 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);
xo = f + s * arm_t;
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]]);
polygon([[xo - s * 0.01, pb + 0.01], [xo + s * 0.6, pb + 0.01], [xo - s * 0.01, pb - 0.6]]);
polygon([[f + s * 0.01, pb + 0.01], [f - s * 0.4, pb + 0.01], [f + s * 0.01, pb - 0.4]]);
}
}
}
module lid() {
difference() {
union() {
lid_shell();
knuckles(true);
bat_clips();
latch_tongue();
box_walls();
lid_fillets();
}
tongue_relief();
box_slot();
}
}
module lid_print() {
translate([0, Sy, zc + lid_h]) rotate([180, 0, 0]) lid();
}
// ---- Parts box cover ----
// The sliding plate, the grip grooves at its front end.
module box_cover_flat() {
slide_plate(parts_box_w - 0.4, Sy - 2 * wall - 0.4, pb_t);
}
module box_cover_placed() {
multmatrix([[1, 0, 0, bi0 + 0.2], [0, 1, 0, wall + 0.2], [0, 0, -1, z_e + pb_t], [0, 0, 0, 1]]) box_cover_flat();
}
// ---- 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]);
}
}
module ghost_charger() {
color("OrangeRed", 0.5) translate([wall, yf - ch_l, fz]) cube([ch_w, ch_l, ch_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, and a snap pin
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();
translate([2, 44, 0]) snap_pin();
// 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();
// one battery clip
if (battery_count > 0) {
yc = Sy / 2 - clip_y;
bx = bat_x(0);
translate([3 * 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();
}
// front end of the parts box, and a piece of its cover
if (box_on) {
translate([0, 54, 0]) coupon([bi0 - pb_wall - 1, -0.1, 0], [Sx + 0.1, 20, lid_h + 1]) lid_print();
translate([parts_box_w + 2 * pb_wall + 10, 54, 0])
intersection() { box_cover_flat(); translate([-pb_t - 1, 0, -1]) cube([parts_box_w + 2 * pb_t + 2, 20, pb_t + 2]); }
}
}
// ---- Layout ----
module assembly() {
color(color_case) base();
color(color_case) pins_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 (box_on) color(color_case) box_cover_placed();
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 == "slider") color(color_case) slider_flat();
else if (part == "box_cover") color(color_case) box_cover_flat();
else if (part == "pins") color(color_case) pins_flat();
else if (part == "fit_frames") color(color_case) fit_frames();
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.
