202 lines
7.4 KiB
OpenSCAD
202 lines
7.4 KiB
OpenSCAD
// =====================================================================
|
|
// Parametric box with clip-in lid
|
|
// =====================================================================
|
|
// PRINT NOTES
|
|
// * Box: print open side UP. Lid: print flat (either face down).
|
|
// * No supports needed - every overhang is <= 45 degrees.
|
|
// * The snap beams flex in the XY plane, i.e. along the layer lines,
|
|
// so they are strong (no bending across weak layer bonds).
|
|
// * Suggested: 0.4 mm nozzle, 0.2 mm layers, 3+ perimeters, PLA/PETG.
|
|
// Walls and beams are multiples of 0.4 mm -> full-width extrusions.
|
|
// * Lid too tight? raise `clearance` (0.1 steps).
|
|
// * Lid rattles? raise `snap_preload` (up to ~= clearance).
|
|
// * Lid pops off too easily? raise `snap_depth` (0.1 steps) or `flex_t`.
|
|
// =====================================================================
|
|
|
|
/* [Box size - outer dimensions in mm] */
|
|
length = 50; // X
|
|
width = 50; // Y
|
|
height = 30; // Z
|
|
|
|
/* [Walls] */
|
|
wall = 2.4; // side wall thickness
|
|
floor_t = 1.6; // floor thickness
|
|
corner_r = 3; // rounded vertical edges (0 = sharp)
|
|
foot_chamfer = 0.4; // bottom edge chamfer, compensates "elephant foot" (0 = off)
|
|
|
|
/* [Lid] */
|
|
lid_t = 3; // lid thickness
|
|
ledge = 0.8; // width of the shelf the lid rests on
|
|
clearance = 0.2; // gap between lid and box
|
|
|
|
/* [Snap fit] */
|
|
snap_depth = 0.5; // how far the ridge sticks out
|
|
snap_flat = 0.6; // flat tip of the ridge
|
|
snap_preload = 0.15; // 0 = loose, for snug fit
|
|
clip_count = 1; // clips per long edge
|
|
clip_len = 30; // length of each flexible beam
|
|
flex_t = 1.6; // beam thickness (thinner = softer clip)
|
|
slot_w = 1.2; // slot width = room for the beam to flex
|
|
|
|
/* [Options] */
|
|
pry_notch = true; // small notch to get a fingernail under the lid
|
|
notch_r = 5;
|
|
part = "print"; // [print, box, lid, assembly]
|
|
cutaway = false;
|
|
|
|
|
|
$fn = 48;
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Derived values
|
|
// ---------------------------------------------------------------------
|
|
R = corner_r;
|
|
rim_wall = wall - ledge; // wall thickness in the lid rebate
|
|
rr = max(R - rim_wall, 0); // rebate corner radius
|
|
rl = length - 2*rim_wall; // rebate size
|
|
rw = width - 2*rim_wall;
|
|
lid_l = rl - 2*clearance; // lid size
|
|
lid_w = rw - 2*clearance;
|
|
lid_r = max(rr - clearance, 0);
|
|
|
|
// The clips go on the two long edges of the lid.
|
|
long_x = length >= width;
|
|
edge_l = long_x ? lid_l : lid_w; // length of the clip edges
|
|
edge_w = long_x ? lid_w : lid_l; // lid size across the clip edges
|
|
|
|
hb = snap_depth + snap_flat/2; // half-height of ridge at its base
|
|
avail = edge_l - 2*lid_r - 4; // straight edge usable for clips
|
|
seg = avail / clip_count;
|
|
clip_l = min(clip_len, seg - 4);
|
|
ridge_l = clip_l - slot_w;
|
|
clip_x = [for (i = [0 : clip_count-1]) -avail/2 + seg*(i + 0.5)];
|
|
|
|
assert(corner_r >= 0 && corner_r <= min(length, width)/2 - 0.1,
|
|
"corner_r is too large for this box");
|
|
assert(height >= floor_t + lid_t + 8,
|
|
"height is too small for floor + lid");
|
|
assert(rim_wall - snap_depth >= 0.8,
|
|
"Rim wall too thin at the groove: reduce ledge/snap_depth or increase wall");
|
|
assert(lid_t/2 - snap_preload - hb >= 0.2,
|
|
"Lid too thin for the snap ridge: increase lid_t or reduce snap_depth/snap_flat");
|
|
assert(edge_w/2 - flex_t - slot_w >= 3,
|
|
"Box too narrow for the flex slots: reduce flex_t/slot_w");
|
|
assert(clip_count >= 1 && clip_l >= 8,
|
|
"Box too short for this many clips: reduce clip_count");
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------
|
|
module rrect(l, w, r) { // centred rounded rectangle (2D)
|
|
if (r > 0.01) offset(r = r) square([l - 2*r, w - 2*r], center = true);
|
|
else square([l, w], center = true);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// BOX
|
|
// ---------------------------------------------------------------------
|
|
module outer_shell() {
|
|
if (foot_chamfer > 0)
|
|
hull() {
|
|
linear_extrude(0.01)
|
|
rrect(length - 2*foot_chamfer, width - 2*foot_chamfer,
|
|
max(R - foot_chamfer, 0));
|
|
translate([0, 0, foot_chamfer])
|
|
linear_extrude(height - foot_chamfer) rrect(length, width, R);
|
|
}
|
|
else
|
|
linear_extrude(height) rrect(length, width, R);
|
|
}
|
|
|
|
// Snap groove running around the rebate wall.
|
|
// Trapezoid profile with 45 degree flanks -> prints without supports.
|
|
module snap_groove() {
|
|
zc = height - lid_t/2 - snap_preload;
|
|
ht = snap_flat/2;
|
|
hull() {
|
|
translate([0, 0, zc - ht]) linear_extrude(2*ht)
|
|
rrect(rl + 2*snap_depth, rw + 2*snap_depth, rr + snap_depth);
|
|
translate([0, 0, zc - hb]) linear_extrude(2*hb)
|
|
rrect(rl, rw, rr);
|
|
}
|
|
}
|
|
|
|
module pry_notch() {
|
|
p = long_x ? [length/2, 0] : [0, width/2]; // on a short wall
|
|
translate([p[0], p[1], height - lid_t - 1])
|
|
cylinder(r = notch_r, h = lid_t + 2);
|
|
}
|
|
|
|
module box() {
|
|
difference() {
|
|
outer_shell();
|
|
// main cavity
|
|
translate([0, 0, floor_t]) linear_extrude(height)
|
|
rrect(length - 2*wall, width - 2*wall, max(R - wall, 0));
|
|
// rebate for the lid (leaves a ledge of width `ledge`)
|
|
translate([0, 0, height - lid_t]) linear_extrude(lid_t + 1)
|
|
rrect(rl, rw, rr);
|
|
snap_groove();
|
|
if (pry_notch) pry_notch();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// LID
|
|
// ---------------------------------------------------------------------
|
|
// Ridge along +Y edge, centred on x = 0, z = 0. 45 degree flanks.
|
|
module ridge(len) {
|
|
e = 0.05;
|
|
ht = snap_flat/2;
|
|
rotate([90, 0, 90]) translate([0, 0, -len/2]) linear_extrude(len)
|
|
polygon([[-e, -hb - e], [snap_depth, -ht],
|
|
[snap_depth, ht], [-e, hb + e]]);
|
|
}
|
|
|
|
// Rounded slot
|
|
module slot(len) {
|
|
hull() for (x = [-1, 1])
|
|
translate([x*(len - slot_w)/2, 0, 0]) cylinder(d = slot_w, h = lid_t + 2);
|
|
}
|
|
|
|
module lid_core(L, W) {
|
|
difference() {
|
|
union() {
|
|
linear_extrude(lid_t) rrect(L, W, lid_r);
|
|
for (s = [-1, 1]) scale([1, s, 1])
|
|
for (cx = clip_x)
|
|
translate([cx, W/2, lid_t/2]) ridge(ridge_l);
|
|
}
|
|
for (s = [-1, 1]) scale([1, s, 1])
|
|
for (cx = clip_x)
|
|
translate([cx, W/2 - flex_t - slot_w/2, -1]) slot(clip_l);
|
|
}
|
|
}
|
|
|
|
module lid() {
|
|
rotate([0, 0, long_x ? 0 : 90]) lid_core(edge_l, edge_w);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// OUTPUT
|
|
// ---------------------------------------------------------------------
|
|
module assembly() {
|
|
big = 10 * max(length, width, height);
|
|
difference() {
|
|
union() {
|
|
box();
|
|
color("orange") translate([0, 0, height - lid_t]) lid();
|
|
}
|
|
if (cutaway) translate([-big/2, 0, -1]) cube([big, big, big]);
|
|
}
|
|
}
|
|
|
|
if (part == "box") box();
|
|
else if (part == "lid") lid();
|
|
else if (part == "assembly") assembly();
|
|
else { // "print": both parts side by side
|
|
lid_ext_y = long_x ? lid_w : lid_l;
|
|
box();
|
|
translate([0, (width + lid_ext_y)/2 + 10, 0]) lid();
|
|
}
|