3d-printing/skadis-tamper-holder/holder.scad
Felipe M. 590228388c
Fillet back-plate front-face perimeter
Replace the corner-cylinder hull in back_plate() with a hull of
rotate_extruded posts whose top-right 2D corner is filleted, so the
back plate's front-face perimeter rounds smoothly into the side walls
while the back face stays flat against the pegboard. Adds the
back_front_fillet_r parameter (default 1.0 mm).
2026-05-08 15:57:34 +02:00

291 lines
12 KiB
OpenSCAD

// ============================================
// Skadis Coffee Tamper Holder
// ============================================
// IKEA Skadis pegboard holder for a coffee tamper that ships in a
// tamper cup (e.g. Normcore 58mm tamper inside its 64mm cup).
//
// The cup drops open-end-up into the holder's pocket; the tamper rests
// on top of the cup with its tamping face protected.
//
// Mounts to the Skadis pegboard via the T-Clip system from
// https://www.printables.com/model/256896-skadis-t-clip-system
// (clips are printed separately). The slot + recess geometry comes from
// the shared lib/skadis-t-clip helper.
//
// Coordinate system:
// Origin (0, 0, 0) = back-bottom-center of the back plate
// (rear face touches the pegboard).
// X = left/right (width)
// Y = pegboard outward (depth, +Y away from board)
// Z = up (height)
use <../lib/skadis-t-clip/skadis-t-clip.scad>;
// ============================================
// CUSTOMIZABLE PARAMETERS
// ============================================
/* [Tamper Cup] */
// Outer diameter of the tamper cup (mm)
cup_diameter = 64; // [40:0.5:120]
// Pocket depth — how far the cup sits in the holder (mm)
cup_pocket_depth = 10; // [5:1:60]
// Radial clearance around the cup, per side (mm)
cup_tolerance = 0.4; // [0:0.1:2]
// Pocket floor thickness; 0 = open bottom (mm)
cup_floor = 4; // [0:0.5:8]
// Drainage hole through the floor; 0 = solid floor (mm)
cup_drain_diameter = 50; // [0:1:60]
// Front-cut window across the pocket so the cup is graspable; 0 = closed cylinder (mm chord)
cup_front_cut_width = 30; // [0:1:120]
/* [Holder Body] */
// Wall thickness around the cup pocket (mm)
pocket_wall = 4; // [1.5:0.5:6]
// Overlap between the cup pocket and the back plate, ensuring a solid union (mm)
merge_overlap = 3; // [1:0.1:6]
// Fillet radius applied to the cup pocket's outer top + bottom edges and
// to the inside-bottom corner where the cavity wall meets the floor (mm)
cup_fillet_r = 1.5; // [0:0.1:5]
/* [Skadis Mount] */
// Number of T-Clip mounts (1 = compact, 2 prevents rotation)
mount_count = 1; // [1, 2]
// Vertical pitch between slots (Skadis standard = 40mm)
mount_pitch = 40; // [40:1:120]
// Back-plate thickness (mm). The recess depth is subtracted from this,
// so the material remaining at the recess bottom = back_thickness - clip_recess_depth.
back_thickness = 5.4; // [2.5:0.1:8]
// Margin around the slot recess on the back plate (mm)
back_margin = 6; // [3:0.5:15]
// Outer corner radius of the back plate (mm)
back_corner_r = 4; // [0:0.5:10]
// Fillet on the back plate's front-face perimeter edges (mm) — aesthetic only
back_front_fillet_r = 1.0; // [0:0.1:5]
/* [Skadis Slot] */
// Skadis nominal slot is 5 x 15mm. Adjust if your printed T-Clip
// stem needs a different fit.
skadis_slot_width = 5.0; // [4:0.1:8]
skadis_slot_height = 15.0; // [10:0.1:25]
// Cylindrical recess on the front face that seats the T-Clip's cap.
clip_recess_diameter = 22.2; // [15:0.1:35]
clip_recess_depth = 2.9; // [0.1:0.1:5]
/* [Resolution] */
$fn = 96;
// ============================================
// DERIVED DIMENSIONS
// ============================================
cup_inner = cup_diameter + 2 * cup_tolerance;
cup_outer = cup_inner + 2 * pocket_wall;
// Back plate is only as wide as needed for the slot recess + margins
back_w = clip_recess_diameter + 2 * back_margin;
back_h = cup_pocket_depth + 2 * back_margin + skadis_slot_height
+ (mount_count - 1) * mount_pitch;
// Cup pocket sits in front of the back plate, with merge_overlap mm of
// the cylinder pushed into the plate so the union is a solid volume.
cup_pocket_y = back_thickness + cup_outer / 2 - merge_overlap;
// Bottom slot Z; subsequent rows space upward by mount_pitch
bottom_mount_z = cup_pocket_depth + back_margin + skadis_slot_height / 2;
// ============================================
// PARAMETER VALIDATION
// ============================================
assert(cup_diameter > 0, "cup_diameter must be positive");
assert(cup_pocket_depth > 0, "cup_pocket_depth must be positive");
assert(cup_floor >= 0, "cup_floor must be non-negative");
assert(cup_drain_diameter >= 0, "cup_drain_diameter must be non-negative");
assert(cup_drain_diameter < cup_inner,
"cup_drain_diameter must be smaller than the cup interior");
assert(pocket_wall >= 1.5, "pocket_wall must be at least 1.5mm");
assert(merge_overlap > 0 && merge_overlap < cup_outer / 2,
"merge_overlap must be between 0 and cup_outer/2");
assert(mount_count == 1 || mount_count == 2, "mount_count must be 1 or 2");
assert(back_thickness >= 2.5, "back_thickness must be at least 2.5mm for strength");
assert(back_front_fillet_r >= 0, "back_front_fillet_r must be non-negative");
assert(back_front_fillet_r < back_thickness,
"back_front_fillet_r must be smaller than back_thickness");
assert(back_corner_r == 0 || back_front_fillet_r < back_corner_r,
"back_front_fillet_r must be smaller than back_corner_r when back_corner_r > 0");
assert(skadis_slot_height >= skadis_slot_width,
"skadis_slot_height must be at least skadis_slot_width");
assert(clip_recess_diameter > skadis_slot_height,
"clip_recess_diameter must exceed skadis_slot_height so the recess surrounds the slot");
assert(clip_recess_depth < back_thickness,
"clip_recess_depth must be less than back_thickness");
assert(cup_front_cut_width <= cup_outer,
"cup_front_cut_width cannot exceed cup outer diameter");
assert(cup_fillet_r >= 0, "cup_fillet_r must be non-negative");
assert(cup_fillet_r < pocket_wall,
"cup_fillet_r must be smaller than pocket_wall");
assert(2 * cup_fillet_r < cup_pocket_depth,
"2 * cup_fillet_r must be smaller than cup_pocket_depth");
assert(cup_fillet_r <= cup_pocket_depth - cup_floor,
"cup_fillet_r must not exceed (cup_pocket_depth - cup_floor)");
assert(cup_drain_diameter == 0 || 2 * cup_fillet_r <= cup_floor,
"2 * cup_fillet_r must not exceed cup_floor when there is a drain hole (so the drain-top and drain-bottom fillets fit on the drain wall)");
assert(cup_drain_diameter == 0
|| 2 * cup_fillet_r < (cup_inner - cup_drain_diameter) / 2,
"2 * cup_fillet_r must be smaller than (R_inner - R_drain) so the inside-bottom and drain-top fillets fit on the floor top");
// ============================================
// MAIN ASSEMBLY
// ============================================
holder();
module holder() {
difference() {
union() {
back_plate();
cup_pocket();
}
cup_front_cutout();
clip_cutouts();
}
}
// ============================================
// COMPONENT MODULES
// ============================================
// Narrow vertical plate that carries the Skadis mount. Rests flat
// against the pegboard. The front-face perimeter is filleted by
// back_front_fillet_r for a softer look; the back face stays flat.
module back_plate() {
if (back_corner_r > 0) {
hull()
for (xc = [-back_w / 2 + back_corner_r,
back_w / 2 - back_corner_r])
for (zc = [back_corner_r, back_h - back_corner_r])
translate([xc, 0, zc])
rotate([-90, 0, 0])
rotate_extrude($fn = 96)
polygon(_back_plate_post_profile(
R = back_corner_r,
H = back_thickness,
F = back_front_fillet_r));
} else {
translate([-back_w / 2, 0, 0])
cube([back_w, back_thickness, back_h]);
}
}
// Radial-axial profile of a back-plate corner post: a rectangle (R wide
// X H tall) with the top-right corner rounded by F. After rotate_extrude
// (axis along Z) and rotate([-90,0,0]) (Z -> Y), the top-right of this
// 2D profile becomes the post's front edge — so hulling four of these
// posts gives the back plate a filleted front-face perimeter.
function _back_plate_post_profile(R, H, F, n_arc = 16) =
(F > 0) ? concat(
[[0, 0], [R, 0], [R, H - F]],
[for (i = [0 : n_arc])
let (a = 90 * i / n_arc)
[(R - F) + F * cos(a), (H - F) + F * sin(a)]
],
[[0, H]]
) : [[0, 0], [R, 0], [R, H], [0, H]];
// Cup pocket: outer cylinder, cavity, floor, and drain hole built as a
// single rotate_extrude of a 2D radial profile. Five fillets give the
// piece a smoother appearance: bottom-outer + top-outer (outer rim
// joints), inside-bottom (cavity wall meeting the floor), drain-top
// (floor meeting the drain hole, visible from inside the cup), and
// drain-bottom (drain hole meeting the bottom face). The cavity rim
// (top-inside) stays sharp so the cup seats cleanly.
module cup_pocket() {
translate([0, cup_pocket_y, 0])
rotate_extrude($fn = 96)
polygon(_cup_pocket_profile(
R_outer = cup_outer / 2,
R_inner = cup_inner / 2,
R_drain = (cup_drain_diameter > 0) ? cup_drain_diameter / 2 : 0,
depth = cup_pocket_depth,
floor = cup_floor,
f = cup_fillet_r));
}
// Radial-axial 2D profile of the cup pocket's solid material. X is the
// radial direction; Y is the vertical direction (Y=0 at the bottom face,
// Y=depth at the rim). CCW traversal: drain-bottom fillet (if drain) ->
// bottom face -> bottom-outer fillet -> outer wall -> top-outer fillet
// -> top face -> cavity rim -> cavity wall -> inside-bottom fillet ->
// floor top -> drain-top fillet (if drain) -> drain wall -> close.
function _cup_pocket_profile(R_outer, R_inner, R_drain, depth, floor, f,
n_arc = 16) =
let (
// Drain-bottom (convex, plate corner): rounds the bottom face
// meeting the drain hole. Skipped when R_drain == 0.
drain_bottom = (R_drain > 0) ? [
for (i = [0 : n_arc])
let (a = 180 + 90 * i / n_arc)
[(R_drain + f) + f * cos(a), f + f * sin(a)]
] : [[R_drain, 0]],
// Bottom-outer (convex, plate corner): rounds the outer rim at Y=0.
arc_B = [
for (i = [0 : n_arc])
let (a = 270 + 90 * i / n_arc)
[(R_outer - f) + f * cos(a), f + f * sin(a)]
],
// Top-outer (convex, plate corner): rounds the outer rim at Y=depth.
arc_C = [
for (i = [0 : n_arc])
let (a = 0 + 90 * i / n_arc)
[(R_outer - f) + f * cos(a), (depth - f) + f * sin(a)]
],
// Inside-bottom (concave, cavity corner): rounds the cavity wall
// meeting the floor for a smooth interior.
arc_E = [
for (i = [0 : n_arc])
let (a = 0 - 90 * i / n_arc)
[(R_inner - f) + f * cos(a), (floor + f) + f * sin(a)]
],
// Drain-top (convex, plate corner): rounds the floor meeting the
// drain hole, visible from inside the cup. Skipped when there is
// no drain hole (R_drain == 0); the fallback is the sharp F vertex.
drain_top = (R_drain > 0) ? [
for (i = [0 : n_arc])
let (a = 90 + 90 * i / n_arc)
[(R_drain + f) + f * cos(a), (floor - f) + f * sin(a)]
] : [[R_drain, floor]]
)
concat(
drain_bottom,
arc_B,
arc_C,
[[R_inner, depth]],
arc_E,
drain_top
);
// Front window in the pocket wall so the cup can be grabbed from the front.
module cup_front_cutout() {
if (cup_front_cut_width > 0) {
rim_top = cup_pocket_depth + 1; // overshoot the rim
rim_bot = min(cup_floor + 3, cup_pocket_depth - 4);
notch_h = max(rim_top - rim_bot, 1);
translate([-cup_front_cut_width / 2,
cup_pocket_y,
rim_bot])
cube([cup_front_cut_width,
cup_outer / 2 + 2,
notch_h]);
}
}
// One T-Clip cutout per mount, stacked at mount_pitch. Geometry comes
// from lib/skadis-t-clip — see that library for the parameter contract.
module clip_cutouts() {
for (i = [0 : mount_count - 1])
translate([0, 0, bottom_mount_z + i * mount_pitch])
skadis_t_clip_cutout(
plate_thickness = back_thickness,
slot_w = skadis_slot_width,
slot_h = skadis_slot_height,
recess_d = clip_recess_diameter,
recess_depth = clip_recess_depth);
}