3d-printing/lib/skadis-board/skadis-board.scad
2026-06-10 09:24:49 +02:00

186 lines
8.8 KiB
OpenSCAD

// Generates a Skadis-compatible pegboard panel: a flat board with the IKEA
// Skadis through-slot pattern, so off-the-shelf Skadis accessories hang on it.
//
// Provides:
// skadis_board(width, height, thickness, density, pitch, min_margin,
// slot_w, slot_h, edge_chamfer, corner_r, perimeter_fillet, fn)
// A rounded-rectangle board of the given OUTER width x height with a
// centered lattice of vertical pill-shaped through-slots. The whole
// perimeter (both front- and back-face edges) is softened by a small
// perimeter_fillet.
// skadis_cols(width, pitch, min_margin) / skadis_rows(height, ...)
// Number of standard 40mm columns / rows that fit in a given size.
// skadis_slot_positions(width, height, pitch, density, min_margin)
// List of [x, z] slot centers for the chosen density.
//
// Hole pattern (the rule that keeps it accessory-compatible):
// A 40mm x 40mm grid PLUS a second 40mm grid offset +20mm in both X and Z.
// Equivalently, a slot at every 20mm-grid point (i, j) where (i + j) is even
// ("standard" checkerboard). "dense" mode also fills the (i + j) odd points
// for a full 20mm grid — a strict superset, so standard accessories still
// register. There is no free hole-spacing parameter, so the 40/20 ratio
// cannot be broken.
//
// Relationship to lib/skadis-t-clip: that library makes the *accessory's*
// mounting cutout (a slot + T-clip cap recess). This library makes the *board
// itself* (plain through-slots). They are complementary, not dependent.
//
// Coordinate system:
// Origin (0, 0, 0) = bottom-left-back corner of the board.
// X = horizontal (width), Z = vertical (height)
// Y = board thickness; back face at Y=0, front face at Y=thickness.
// Slots are vertical ovals (long axis along Z), like a real Skadis board.
// Standard Skadis grid pitch (mm) — keep at 40 for accessory compatibility.
SKADIS_PITCH = 40;
// Number of standard 40mm columns / rows that fit in a given outer size,
// leaving at least min_margin from the outer slots to the board edge.
function skadis_cols(width, pitch = 40, min_margin = 14) =
max(1, floor((width - 2 * min_margin) / pitch) + 1);
function skadis_rows(height, pitch = 40, min_margin = 14) =
max(1, floor((height - 2 * min_margin) / pitch) + 1);
// [x, z] centers of every slot in the lattice, centered within the board.
function skadis_slot_positions(width, height, pitch = 40,
density = "standard", min_margin = 14) =
let (
half = pitch / 2,
cols = skadis_cols(width, pitch, min_margin),
rows = skadis_rows(height, pitch, min_margin),
imax = 2 * (cols - 1), // half-pitch index span (X)
jmax = 2 * (rows - 1), // half-pitch index span (Z)
ox = (width - imax * half) / 2, // centered origin offset (X)
oz = (height - jmax * half) / 2 // centered origin offset (Z)
)
[ for (i = [0 : imax], j = [0 : jmax])
if (density == "dense" || (i + j) % 2 == 0)
[ox + i * half, oz + j * half] ];
module skadis_board(
width,
height,
thickness = 5.0,
density = "standard",
pitch = 40,
min_margin = undef, // defaults to 14 ("standard") / 10 ("dense")
slot_w = 5.0,
slot_h = 15.0,
edge_chamfer = 1.0,
corner_r = 4,
perimeter_fillet = 1.0,
fn = 64
) {
assert(width > 0 && height > 0, "width and height must be positive");
assert(thickness > 0, "thickness must be positive");
assert(pitch == 40,
"pitch must be 40 to stay compatible with real Skadis accessories");
assert(slot_h >= slot_w, "slot_h must be >= slot_w");
assert(density == "standard" || density == "dense",
"density must be \"standard\" or \"dense\"");
// The dense grid packs slots tighter, so it can tolerate a smaller edge
// margin; fall back to that when the caller doesn't set min_margin.
margin = is_undef(min_margin) ? (density == "dense" ? 10 : 14) : min_margin;
assert(edge_chamfer >= 0 && 2 * edge_chamfer < thickness,
"edge_chamfer must be >= 0 and fit within the thickness");
assert(corner_r >= 0, "corner_r must be non-negative");
assert(perimeter_fillet >= 0, "perimeter_fillet must be non-negative");
assert(2 * perimeter_fillet < thickness,
"perimeter_fillet must fit within the thickness (2 * fillet < thickness)");
assert(corner_r == 0 || perimeter_fillet <= corner_r,
"perimeter_fillet must be <= corner_r when corner_r > 0");
assert(len(skadis_slot_positions(width, height, pitch, density, margin)) > 0,
"board is too small to hold any slot — increase width/height or reduce min_margin");
difference() {
_board_blank(width, height, thickness, corner_r, perimeter_fillet, fn);
for (p = skadis_slot_positions(width, height, pitch, density, margin))
translate([p[0], 0, p[1]])
_skadis_board_slot(thickness, slot_w, slot_h, edge_chamfer, fn);
}
}
// ============================================
// COMPONENT MODULES
// ============================================
// Solid board blank: a rounded rectangle (corner_r) spanning X=[0,width],
// Z=[0,height], with thickness along Y=[0,thickness]. Built by hulling four
// Y-axis posts whose radial-axial profile rounds both the front- and back-
// face perimeter edges by perimeter_fillet, so the whole outline is softened.
// (With perimeter_fillet = 0 the posts are plain cylinders, matching the
// rounded-back-plate idiom used elsewhere in the repo.)
module _board_blank(width, height, thickness, corner_r, perimeter_fillet, fn) {
if (corner_r > 0)
hull()
for (xc = [corner_r, width - corner_r])
for (zc = [corner_r, height - corner_r])
translate([xc, 0, zc])
rotate([-90, 0, 0])
rotate_extrude($fn = fn)
polygon(_board_post_profile(
R = corner_r,
H = thickness,
F = perimeter_fillet));
else
cube([width, thickness, height]); // X=width, Y=thickness, Z=height
}
// Radial-axial profile of a corner post: a rectangle (R wide X H tall) with
// BOTH outer corners rounded by F. After rotate_extrude (axis along Z) and
// rotate([-90,0,0]) (Z -> Y), the profile's outer edge becomes the post's
// side wall and its two outer corners become the front- and back-face
// perimeter edges — so hulling four posts fillets the whole perimeter.
function _board_post_profile(R, H, F, n_arc = 16) =
(F > 0) ? concat(
[[0, 0], [R - F, 0]],
// Back-face fillet: [R-F, 0] curving up the side wall to [R, F].
[for (i = [0 : n_arc])
let (a = -90 + 90 * i / n_arc)
[(R - F) + F * cos(a), F + F * sin(a)]],
// Front-face fillet: [R, H-F] curving in to [R-F, H].
[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]];
// One vertical pill-shaped through-slot centered at the origin, running
// through Y from the back face to the front face, with a chamfered (counter-
// sunk) mouth on both faces. Pill = hull of two Y-axis cylinders at z=+/-e,
// matching the slot idiom in lib/skadis-t-clip.
module _skadis_board_slot(thickness, slot_w, slot_h, chamfer, fn) {
pad = 1;
e = (slot_h - slot_w) / 2;
union() {
hull()
for (zc = [-e, e])
translate([0, -pad, zc])
rotate([-90, 0, 0])
cylinder(h = thickness + 2 * pad, d = slot_w, $fn = fn);
if (chamfer > 0) {
// Back-face countersink: wide at Y=0, narrows to the slot at Y=chamfer.
_slot_mouth_chamfer(e, slot_w / 2, chamfer,
y_wide = -0.01, y_narrow = chamfer, fn = fn);
// Front-face countersink: wide at Y=thickness, narrows at Y=thickness-chamfer.
_slot_mouth_chamfer(e, slot_w / 2, chamfer,
y_wide = thickness, y_narrow = thickness - chamfer,
fn = fn);
}
}
}
// A 45-ish degree flare at a slot mouth: hull of a wide pill ring at y_wide
// and a slot-sized pill ring at y_narrow. Subtracted as part of the slot.
module _slot_mouth_chamfer(e, r_slot, chamfer, y_wide, y_narrow, fn) {
eps = 0.01;
hull()
for (zc = [-e, e]) {
translate([0, y_wide, zc])
rotate([-90, 0, 0])
cylinder(h = eps, r = r_slot + chamfer, $fn = fn);
translate([0, y_narrow, zc])
rotate([-90, 0, 0])
cylinder(h = eps, r = r_slot, $fn = fn);
}
}