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 and no loose inserts. 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 pin hinges at the rear, each on one M3 bolt. 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 · 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.

MIT

Tiny whoop case sized to its contents, with press-fit pockets, a port slider and a parts box

mick/whoop-case@3.2.1

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
55100, steps of 0.5
2865, steps of 0.5
2060, steps of 1
40120, 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 and no loose inserts. 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 pin hinges at the rear, each on one M3 bolt. 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 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. 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 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. 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. The only hardware is the two hinge pins: each hinge is base, lid, base on an M3 x 30 with a nut. 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 and 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; fit_frames lays out 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, 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.2.1")

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 sized to its contents, with press-fit pockets, a port slider and a parts box, with the ranges the source declares
NameDefaultRangeUnitDescription
part"assembly"assembly, base, lid, slider, box_cover, fit_frames
test_printfalse
show_contentsfalse
whoop_wheelbase8055 to 100 step 0.5
duct_d5028 to 65 step 0.5
whoop_h4020 to 60 step 1
charger"BetaFPV 6-port"BetaFPV 6-port, BetaFPV BT2.0 board, custom
custom_l7940 to 120 step 0.5
custom_w4625 to 70 step 0.5
custom_h198 to 35 step 0.5
port_from_side2310 to 40 step 0.5
port_z9.56 to 20 step 0.5
battery_class"LAVA 480-680"LAVA 480-680, LAVA 280-320
battery_count80 to 12 step 1
parts_box_w350 to 60 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@3.2.1 is MIT

Source

main.scad is the entrypoint
main.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 and no loose inserts.
// 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 slider
// and the cover share one design: a plate with 45 degree edges in a dovetail,
// captive while the lid is shut. 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. 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, fit_frames]
// Print the mechanism coupons instead: one hinge and the latch in base and lid
// 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]
// 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;
// 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: 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_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);
}

// 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]]);
}

// ---- 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. 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]);
  }
}

// ---- 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.

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);
  }
}

// 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) difference() {
    inset2d(wall - 0.01);
    translate([dx, dy]) offset(r = w_clr) whoop2d(sq, duct_d);
    translate([wall - 1, cy_r]) offset(r = 1) offset(delta = -1) square([cx_in - wall + 1, yf - cy_r + 1]);
  }
}

// Key on the front rim: 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() {
  if (box_on) translate([bi0 - pb_t - 0.2, Sy - wall, zc - 0.6]) cube([parts_box_w + 2 * pb_t + 0.4, wall - seam_ch, pb_g + pb_t + 0.8]);
}

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]]);
}

// 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();
      latch_boss();
      box_key();
    }
    port_cut();
    slider_channel();
    latch_cut();
  }
  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 = w_clr + 2) whoop2d(sq, duct_d);
      offset(r = w_clr) whoop2d(sq, duct_d);
    }
    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);
  }
}

// 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]);
}

// 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. 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();
      box_walls();
    }
    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
  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();
  // 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, 50, 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, 50, 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) 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 == "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.

Sign in to comment