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