186 lines
8.8 KiB
OpenSCAD
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);
|
|
}
|
|
}
|