new design: parameterized box with clip in lid

This commit is contained in:
2026-09-22 17:37:48 +02:00
parent 07e8105cce
commit 67f0d26978
3 changed files with 296 additions and 4 deletions
+201
View File
@@ -0,0 +1,201 @@
// =====================================================================
// 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();
}