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