From fcf3fa2640ffa5b24d2bdd6295f80984317bbb9d Mon Sep 17 00:00:00 2001 From: RWB5510 H1 <107571028+rwb5510@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:08:36 -0400 Subject: [PATCH 1/2] add options for multi device, keystone jack, preset device sizes, angled supports Add multi-section support, keystone jacks, device presets, and angled gussets Merge and extend the original single-switch mount with three new capabilities: multi-section faceplates (up to 4 openings), per-section keystone jack cutouts, and optional angled support gussets for heavy devices. Also adds a device preset table so common gear can be selected by name instead of entering dimensions by hand. - Generalize the dual-sleeve branch (PR #20, @cjolivier01) to N sections with a section_count parameter and per-section enable flags - Merge the keystone jack feature from PR #21, extended to be per-section with side selection and automatic clearance around front wire holes and rack-rail slot columns - Add angled triangular gussets that tie each sleeve to the faceplate, with per-side width auto-clamped to available gutter space - Add a device preset dropdown per section with ~25 preloaded sizes sourced from the MakerWorld 10-inch Mini Rack Generator project page; Custom preserves the original manual-dimension workflow - Emit echo warnings when a preset exceeds rack_height, when keystones collide with rail columns, or when a gusset is clamped below the minimum renderable width Co-authored-by: cjolivier01 Preset dimensions: https://makerworld.com/en/models/1765102-10-inch-mini-rack-generator --- 10InchRackGenerator.scad | 604 ++++++++++++++++++++++++--------------- 1 file changed, 367 insertions(+), 237 deletions(-) diff --git a/10InchRackGenerator.scad b/10InchRackGenerator.scad index 56acdef..e0e2b8c 100644 --- a/10InchRackGenerator.scad +++ b/10InchRackGenerator.scad @@ -1,32 +1,148 @@ -rack_width = 254.0; // [ 254.0:10 inch, 152.4:6 inch] -rack_height = 1.0; // [0.5:0.5:5] +rack_width = 254.0; // [ 254.0:10 inch, 152.4:6 inch] +rack_height = 1.0; // [0.5:0.5:5] half_height_holes = true; // [true:Show partial holes at edges, false:Hide partial holes] -switch_width = 135.0; -switch_depth = 135.0; -switch_height = 28.30; +section_count = 2; // [1:4] -case_thickness = 6; // Thickness of case walls -wire_diameter = 7; // Diameter of power wire holes +/* [Section 1] */ +section1_device = "Custom"; // [Custom, Firewalla Gold, Firewalla Purple - wifi, Firewalla Purple - Ethernet, Firewalla Purple SE, UniFi Security Gateway, UniFi Cloud Key G2+, UniFi Flex Mini, UniFi Flex Mini 2.5G, UniFi Flex 2.5, UniFi Lite 8 POE, UniFi Lite 16 POE, UniFi Express, UniFi Cloud Gateway Ultra-Max, IBM M70q Gen 5, IBM M70q Gen 4, IBM M90q Gen 5, Dell OptiPlex 7020, HP Elite Mini 800, M4 Mac Mini, BeeLink ME Mini, Xyber Hydra, Synology DS223j, Synology DS223, Synology DS124] +section1_width = 69.75; +section1_height = 28; +section1_depth = 92.0; +section1_x_offset = -56.0; +section1_y_offset = 0.0; +section1_keystone = false; // [true:Add keystone jacks, false:None] +section1_keystone_count = 1; // [1:6] +section1_keystone_side = 1; // [0:Left of opening, 1:Right of opening] +section1_support = false; // [true:Add angled supports, false:None] -front_wire_holes = false; // [true:Show front wire holes, false:Hide front wire holes] -air_holes = true; // [true:Show air holes, false:Hide air holes] -print_orientation = true; // [true: Place on printbed, false: Facing forward] +/* [Section 2] */ +section2_device = "Custom"; // [Custom, Firewalla Gold, Firewalla Purple - wifi, Firewalla Purple - Ethernet, Firewalla Purple SE, UniFi Security Gateway, UniFi Cloud Key G2+, UniFi Flex Mini, UniFi Flex Mini 2.5G, UniFi Flex 2.5, UniFi Lite 8 POE, UniFi Lite 16 POE, UniFi Express, UniFi Cloud Gateway Ultra-Max, IBM M70q Gen 5, IBM M70q Gen 4, IBM M90q Gen 5, Dell OptiPlex 7020, HP Elite Mini 800, M4 Mac Mini, BeeLink ME Mini, Xyber Hydra, Synology DS223j, Synology DS223, Synology DS124] +section2_width = 69.75; +section2_height = 28; +section2_depth = 92.0; +section2_x_offset = 56.0; +section2_y_offset = 0.0; +section2_keystone = false; +section2_keystone_count = 1; // [1:6] +section2_keystone_side = 0; // [0:Left of opening, 1:Right of opening] +section2_support = false; + +/* [Section 3] */ +section3_device = "Custom"; // [Custom, Firewalla Gold, Firewalla Purple - wifi, Firewalla Purple - Ethernet, Firewalla Purple SE, UniFi Security Gateway, UniFi Cloud Key G2+, UniFi Flex Mini, UniFi Flex Mini 2.5G, UniFi Flex 2.5, UniFi Lite 8 POE, UniFi Lite 16 POE, UniFi Express, UniFi Cloud Gateway Ultra-Max, IBM M70q Gen 5, IBM M70q Gen 4, IBM M90q Gen 5, Dell OptiPlex 7020, HP Elite Mini 800, M4 Mac Mini, BeeLink ME Mini, Xyber Hydra, Synology DS223j, Synology DS223, Synology DS124] +section3_width = 70.0; +section3_height = 28; +section3_depth = 90.0; +section3_x_offset = 0.0; +section3_y_offset = 0.0; +section3_keystone = false; +section3_keystone_count = 1; // [1:6] +section3_keystone_side = 1; // [0:Left of opening, 1:Right of opening] +section3_support = false; + +/* [Section 4] */ +section4_device = "Custom"; // [Custom, Firewalla Gold, Firewalla Purple - wifi, Firewalla Purple - Ethernet, Firewalla Purple SE, UniFi Security Gateway, UniFi Cloud Key G2+, UniFi Flex Mini, UniFi Flex Mini 2.5G, UniFi Flex 2.5, UniFi Lite 8 POE, UniFi Lite 16 POE, UniFi Express, UniFi Cloud Gateway Ultra-Max, IBM M70q Gen 5, IBM M70q Gen 4, IBM M90q Gen 5, Dell OptiPlex 7020, HP Elite Mini 800, M4 Mac Mini, BeeLink ME Mini, Xyber Hydra, Synology DS223j, Synology DS223, Synology DS124] +section4_width = 70.0; +section4_height = 28; +section4_depth = 90.0; +section4_x_offset = 0.0; +section4_y_offset = 0.0; +section4_keystone = false; +section4_keystone_count = 1; // [1:6] +section4_keystone_side = 1; // [0:Left of opening, 1:Right of opening] +section4_support = false; + +/* [Shared] */ +case_thickness = 6; // Thickness of case walls +wire_diameter = 7; // Diameter of power wire holes +front_wire_holes = false; // [true:Show front wire holes, false:Hide] +air_holes = true; // [true:Show air holes, false:Hide] +print_orientation = true; // [true:Place on printbed, false:Facing forward] tolerance = 0.42; +/* [Keystone Jacks] */ +keystone_width = 14.94; // Standard keystone cutout width +keystone_height = 16.51; // Standard keystone cutout height +keystone_spacing = 3.0; // Gap between adjacent jacks +keystone_gap_from_section = 3.0; // Gap between jack and opening edge + +/* [Angled Supports] */ +support_depth_requested = 18.0; // How far back along sleeve the gusset runs +support_width_requested = 10.0; // How far along faceplate the gusset runs +support_rail_margin = 1.5; // Clearance between support and rack rail slots +support_min_width = 3.0; // Don't render support thinner than this + /* [Hidden] */ height = 44.45 * rack_height; +// Device preset table: [name, width, height, depth] (width = across rack, height = up, depth = into rack) +device_presets = [ + ["Custom", 0, 0, 0 ], + ["Firewalla Gold", 130, 34, 110 ], + ["Firewalla Purple - wifi", 90, 30, 60 ], + ["Firewalla Purple - Ethernet", 130, 34, 110 ], + ["Firewalla Purple SE", 90, 30, 60 ], + ["UniFi Security Gateway", 135, 28.3, 135 ], + ["UniFi Cloud Key G2+", 131.2, 134.2, 27.1 ], + ["UniFi Flex Mini", 107, 21, 70 ], + ["UniFi Flex Mini 2.5G", 117.1, 21.2, 90 ], + ["UniFi Flex 2.5", 212.9, 33.5, 76 ], + ["UniFi Lite 8 POE", 99.6, 31.7, 163.7], + ["UniFi Lite 16 POE", 192, 44, 185 ], + ["UniFi Express", 98, 30, 98 ], + ["UniFi Cloud Gateway Ultra-Max", 141.8, 30, 127.6], + ["IBM M70q Gen 5", 179, 36.5, 182.9], + ["IBM M70q Gen 4", 179, 34.5, 183 ], + ["IBM M90q Gen 5", 179, 36.5, 182.9], + ["Dell OptiPlex 7020", 182, 36, 178 ], + ["HP Elite Mini 800", 177.5, 34.3, 175.2], + ["M4 Mac Mini", 127, 50, 127 ], + ["BeeLink ME Mini", 99, 99, 99 ], + ["Xyber Hydra", 140, 34.5, 98.5 ], + ["Synology DS223j", 165, 225.5, 100 ], + ["Synology DS223", 165, 232.7, 108 ], + ["Synology DS124", 166, 224, 71 ] +]; + +function _preset_lookup(name) = + let (m = [ for (p = device_presets) if (p[0] == name) [p[1], p[2], p[3]] ]) + (len(m) > 0) ? m[0] : undef; + +function _effective_dims(device, w, h, d) = + (device == "Custom") + ? [w, h, d] + : let (p = _preset_lookup(device)) (p == undef) ? [w, h, d] : p; + +// Per-section data: [width, height, depth, x_off, y_off, +// keystone_on, keystone_count, keystone_side, support_on] +_s1_dims = _effective_dims(section1_device, section1_width, section1_height, section1_depth); +_s2_dims = _effective_dims(section2_device, section2_width, section2_height, section2_depth); +_s3_dims = _effective_dims(section3_device, section3_width, section3_height, section3_depth); +_s4_dims = _effective_dims(section4_device, section4_width, section4_height, section4_depth); -// The main module containing all internal variables -module switch_mount(switch_width, switch_height, switch_depth) { - //6 inch racks (mounts=152.4mm; rails=15.875mm; usable space=120.65mm) - //10 inch racks (mounts=254.0mm; rails=15.875mm; usable space=221.5mm) - chassis_width = min(switch_width + (2 * case_thickness), (rack_width == 152.4) ? 120.65 : 221.5); +_sections = [ + [_s1_dims[0], _s1_dims[1], _s1_dims[2], section1_x_offset, section1_y_offset, + section1_keystone, section1_keystone_count, section1_keystone_side, section1_support], + [_s2_dims[0], _s2_dims[1], _s2_dims[2], section2_x_offset, section2_y_offset, + section2_keystone, section2_keystone_count, section2_keystone_side, section2_support], + [_s3_dims[0], _s3_dims[1], _s3_dims[2], section3_x_offset, section3_y_offset, + section3_keystone, section3_keystone_count, section3_keystone_side, section3_support], + [_s4_dims[0], _s4_dims[1], _s4_dims[2], section4_x_offset, section4_y_offset, + section4_keystone, section4_keystone_count, section4_keystone_side, section4_support] +]; + +// Warn about sections that won't fit in the chosen rack_height +for (i = [0:section_count - 1]) { + if (_sections[i][1] > height) { + echo(str("WARNING: section ", i + 1, " height (", _sections[i][1], + "mm) exceeds rack_height (", height, "mm). Increase rack_height.")); + } +} + +module switch_mount_multi() { front_thickness = 3.0; corner_radius = 4.0; chassis_edge_radius = 2.0; - tolerance = 0.42; zip_tie_hole_count = 8; zip_tie_hole_width = 1.5; @@ -34,294 +150,308 @@ module switch_mount(switch_width, switch_height, switch_depth) { zip_tie_indent_depth = 2; zip_tie_cutout_depth = 7; - chassis_depth_main = switch_depth + zip_tie_cutout_depth; - chassis_depth_indented = chassis_depth_main - zip_tie_indent_depth; + usable_width = (rack_width == 152.4) ? 120.65 : 221.5; + max_depth = max([ for (i = [0:section_count - 1]) _sections[i][2] ]); + max_chassis_depth = max_depth + zip_tie_cutout_depth; - hole_total_width = zip_tie_hole_count * zip_tie_hole_width; - space_between_holes = (rack_width - hole_total_width) / (zip_tie_hole_count + 1); + hole_spacing_x = (rack_width == 152.4) ? 136.526 : 236.525; + slot_len = (rack_width == 152.4) ? 6.5 : 10.0; + rail_left_edge = (rack_width - hole_spacing_x) / 2 + slot_len / 2 + support_rail_margin; + rail_right_edge = (rack_width + hole_spacing_x) / 2 - slot_len / 2 - support_rail_margin; $fn = 64; - // Calculated dimensions - cutout_w = switch_width + (2 * tolerance); - cutout_h = switch_height + (2 * tolerance); - cutout_x = (rack_width - cutout_w) / 2; - cutout_y = (height - cutout_h) / 2; - - // Helper modules + function section_center_x(i) = rack_width / 2 + _sections[i][3]; + function section_center_y(i) = height / 2 + _sections[i][4]; + function section_chassis_width(sw) = min(sw + (2 * case_thickness), usable_width); + function section_chassis_height(sh) = sh + (2 * case_thickness); + function section_chassis_depth(sd) = sd + zip_tie_cutout_depth; + function chassis_left(i) = section_center_x(i) - section_chassis_width(_sections[i][0]) / 2; + function chassis_right(i) = section_center_x(i) + section_chassis_width(_sections[i][0]) / 2; + module capsule_slot_2d(L, H) { hull() { - translate([-L/2 + H/2, 0]) circle(r=H/2); - translate([L/2 - H/2, 0]) circle(r=H/2); + translate([-L / 2 + H / 2, 0]) circle(r = H / 2); + translate([ L / 2 - H / 2, 0]) circle(r = H / 2); } } - module rounded_rect_2d(w, h, r) { hull() { - translate([r, r]) circle(r=r); - translate([w-r, r]) circle(r=r); - translate([w-r, h-r]) circle(r=r); - translate([r, h-r]) circle(r=r); + translate([r, r]) circle(r = r); + translate([w - r, r]) circle(r = r); + translate([w - r, h - r]) circle(r = r); + translate([r, h - r]) circle(r = r); } } - - module rounded_chassis_profile(width, height, radius, depth) { + module rounded_chassis_profile(w, h, r, d) { hull() { - translate([radius, radius, 0]) cylinder(h = depth, r = radius); - translate([width - radius, radius, 0]) cylinder(h = depth, r = radius); - translate([radius, height - radius, 0]) cylinder(h = depth, r = radius); - translate([width - radius, height - radius, 0]) cylinder(h = depth, r = radius); + translate([r, r, 0]) cylinder(h = d, r = r); + translate([w - r, r, 0]) cylinder(h = d, r = r); + translate([r, h - r, 0]) cylinder(h = d, r = r); + translate([w - r, h - r, 0]) cylinder(h = d, r = r); + } + } + + module section_sleeve(i) { + sw = _sections[i][0]; sh = _sections[i][1]; sd = _sections[i][2]; + cw = section_chassis_width(sw); + ch = section_chassis_height(sh); + cd = section_chassis_depth(sd); + cx = section_center_x(i); + cy = section_center_y(i); + translate([cx - cw / 2, cy - ch / 2, front_thickness]) { + rounded_chassis_profile(cw, ch, chassis_edge_radius, cd - front_thickness); } } - - // Create the main body as a separate module + + function nearest_obstacle_x(i, side) = + let (own_edge = (side == -1) ? chassis_left(i) : chassis_right(i)) + let (rail_limit = (side == -1) ? rail_left_edge : rail_right_edge) + let (neighbor_edges = [ + for (j = [0:section_count - 1]) + if (j != i) + let (nl = chassis_left(j), nr = chassis_right(j)) + (side == -1) + ? (nr <= own_edge ? nr : -1e9) + : (nl >= own_edge ? nl : 1e9) + ]) + (side == -1) + ? max(concat([rail_limit], neighbor_edges)) + : min(concat([rail_limit], neighbor_edges)); + + function available_space(i, side) = + let (own_edge = (side == -1) ? chassis_left(i) : chassis_right(i)) + (side == -1) ? own_edge - nearest_obstacle_x(i, -1) + : nearest_obstacle_x(i, +1) - own_edge; + + module side_support_gusset(i, side) { + sh = _sections[i][1]; + ch = section_chassis_height(sh); + cy = section_center_y(i); + y_lo = cy - ch / 2; + y_hi = cy + ch / 2; + sx_attach = (side == -1) ? chassis_left(i) : chassis_right(i); + + avail = available_space(i, side) - 1.0; + sup_w = min(support_width_requested, avail); + + if (sup_w >= support_min_width) { + translate([0, y_hi, 0]) + rotate([90, 0, 0]) + linear_extrude(height = y_hi - y_lo) + polygon([ + [sx_attach, 0], + [sx_attach, front_thickness + support_depth_requested], + [sx_attach + side * sup_w, 0] + ]); + } else { + echo(str("WARNING: support skipped on section ", i + 1, + " side ", side, " (available=", avail, ")")); + } + } + module main_body() { - side_margin = (rack_width - chassis_width) / 2; - chassis_height = switch_height + (2 * case_thickness); union() { - // Front panel linear_extrude(height = front_thickness) { rounded_rect_2d(rack_width, height, corner_radius); } - // Chassis body - translate([side_margin, (height - chassis_height) / 2, front_thickness]) { - rounded_chassis_profile(chassis_width, chassis_height, chassis_edge_radius, chassis_depth_main - front_thickness); - } + for (i = [0:section_count - 1]) section_sleeve(i); + for (i = [0:section_count - 1]) + if (_sections[i][8]) + for (side = [-1, 1]) side_support_gusset(i, side); } } - - // Create switch cutout with proper lip - module switch_cutout() { + + module section_switch_cutout(i) { + sw = _sections[i][0]; sh = _sections[i][1]; sd = _sections[i][2]; + cx = section_center_x(i); cy = section_center_y(i); + cd = section_chassis_depth(sd); lip_thickness = 1.2; lip_depth = 0.60; - // Main cutout minus lip (centered) + cutout_w = sw + 2 * tolerance; + cutout_h = sh + 2 * tolerance; + translate([ - (rack_width - (cutout_w - 2*lip_thickness)) / 2, - (height - (cutout_h - 2*lip_thickness)) / 2, + cx - (cutout_w - 2 * lip_thickness) / 2, + cy - (cutout_h - 2 * lip_thickness) / 2, -tolerance - ]) { - cube([cutout_w - 2*lip_thickness, cutout_h - 2*lip_thickness, chassis_depth_main]); - } + ]) cube([cutout_w - 2 * lip_thickness, cutout_h - 2 * lip_thickness, cd]); - // Switch cutout above the lip (centered) - translate([ - (rack_width - cutout_w) / 2, - (height - cutout_h) / 2, - lip_depth - ]) { - cube([cutout_w, cutout_h, chassis_depth_main]); - } + translate([cx - cutout_w / 2, cy - cutout_h / 2, lip_depth]) + cube([cutout_w, cutout_h, cd]); } - - // Create all rack holes + module all_rack_holes() { - // Rack standard: 3 holes per U, with specific positioning - // Each U is 44.45mm, holes are at specific positions within each U - hole_spacing_x = (rack_width == 152.4) ? 136.526 : 236.525; // 6 inch : 10 inch rack - hole_left_x = (rack_width - hole_spacing_x) / 2; + hole_left_x = (rack_width - hole_spacing_x) / 2; hole_right_x = (rack_width + hole_spacing_x) / 2; - - // 10 inch rack = 10x7mm oval - // 6 inchr rack = 3.25 x 6.5mm oval - slot_len = (rack_width == 152.4) ? 6.5 : 10.0; slot_height = (rack_width == 152.4) ? 3.25 : 7.0; + u_hole_positions = [6.35, 22.225, 38.1]; + max_u = ceil(rack_height); - // Standard rack hole positions within each 1U (44.45mm) unit: - // First hole: 6.35mm from top of U - // Second hole: 22.225mm from top of U (middle) - // Third hole: 38.1mm from top of U (6.35mm from bottom) - u_hole_positions = [6.35, 22.225, 38.1]; // positions within each U - - // Calculate how many full and partial U units we need to consider - max_u = ceil(rack_height); // Include partial U units - for (side_x = [hole_left_x, hole_right_x]) { - for (u = [0:max_u-1]) { + for (u = [0:max_u - 1]) { for (hole_pos = u_hole_positions) { - // Calculate hole position from top of entire rack hole_y = height - (u * 44.45 + hole_pos); - // Always show holes that are at least partially within the rack height - // Always show holes fully inside the rack - fully_inside = (hole_y >= slot_height/2 && hole_y <= height - slot_height/2); - // Show partial holes at edge only if half_height_holes is true - partially_inside = (hole_y + slot_height/2 > 0 && hole_y - slot_height/2 < height); + fully_inside = (hole_y >= slot_height / 2 && hole_y <= height - slot_height / 2); + partially_inside = (hole_y + slot_height / 2 > 0 && hole_y - slot_height / 2 < height); show_hole = fully_inside || (half_height_holes && partially_inside && !fully_inside); if (show_hole) { - translate([side_x, hole_y, 0]) { - linear_extrude(height = chassis_depth_main) { + translate([side_x, hole_y, 0]) + linear_extrude(height = max_chassis_depth) capsule_slot_2d(slot_len, slot_height); - } - } } } } } } - // Power wire cutouts: configurable diameter holes at top and bottom rack hole positions - module power_wire_cutouts() { - hole_spacing_x = switch_width; // match rack holes - hole_left_x = (rack_width - hole_spacing_x) / 2 - (wire_diameter /5); - hole_right_x = (rack_width + hole_spacing_x) / 2 + (wire_diameter /5); - // Midplane of switch opening - mid_y = (height - switch_height) / 2 + switch_height / 2; - for (side_x = [hole_left_x, hole_right_x]) { - translate([side_x, mid_y, 0]) { - linear_extrude(height = chassis_depth_main) { - circle(d=wire_diameter); - } - } + module section_power_wire_cutouts(i) { + sw = _sections[i][0]; sd = _sections[i][2]; + cx = section_center_x(i); cy = section_center_y(i); + cd = section_chassis_depth(sd); + hx_left = cx - sw / 2 - wire_diameter / 5; + hx_right = cx + sw / 2 + wire_diameter / 5; + for (sxp = [hx_left, hx_right]) { + translate([sxp, cy, 0]) + linear_extrude(height = cd) + circle(d = wire_diameter); } } - - // Create zip tie holes and indents - module zip_tie_features() { - // Zip tie holes - for (i = [0:zip_tie_hole_count-1]) { - x_pos = (rack_width - switch_width)/2 + (switch_width/(zip_tie_hole_count+1)) * (i+1); - translate([x_pos, 0, switch_depth]) { - cube([zip_tie_hole_width, height, zip_tie_hole_length]); - } - } - - // Zip tie indents (top and bottom) - x_pos = (rack_width - switch_width)/2; - chassis_height = switch_height + (2 * case_thickness); - // Bottom indent - translate([x_pos, (height - chassis_height)/2, switch_depth]) { - cube([switch_width, zip_tie_indent_depth, zip_tie_cutout_depth]); - } - // Top indent - translate([x_pos, (height + chassis_height)/2 - zip_tie_indent_depth, switch_depth]) { - cube([switch_width, zip_tie_indent_depth, zip_tie_cutout_depth]); + + module section_zip_tie_features(i) { + sw = _sections[i][0]; sh = _sections[i][1]; sd = _sections[i][2]; + cx = section_center_x(i); cy = section_center_y(i); + ch = section_chassis_height(sh); + y_start = cy - ch / 2; + x_start = cx - sw / 2; + + for (k = [0:zip_tie_hole_count - 1]) { + x_pos = x_start + (sw / (zip_tie_hole_count + 1)) * (k + 1); + translate([x_pos, y_start, sd]) + cube([zip_tie_hole_width, ch, zip_tie_hole_length]); } + translate([x_start, y_start, sd]) + cube([sw, zip_tie_indent_depth, zip_tie_cutout_depth]); + translate([x_start, y_start + ch - zip_tie_indent_depth, sd]) + cube([sw, zip_tie_indent_depth, zip_tie_cutout_depth]); } - // Simplified air holes with staggered honeycomb pattern on all faces - module air_holes() { - hole_d = 16; - spacing_x = 15; // Horizontal spacing (X and Y directions) - spacing_z = 17; // Vertical spacing (Z direction) - tighter to match visual density - margin = 3; // Keep holes away from edges - - // BACK FACE HOLES (Y-axis through back) - // Calculate available space for holes within switch dimensions - available_width = switch_width - (2 * margin); - available_depth = switch_depth - (2 * margin); - - // Calculate number of holes that fit - x_cols = floor(available_width / spacing_x); - z_rows = floor(available_depth / spacing_z); - - // Calculate actual grid size for centering - actual_grid_width = (x_cols - 1) * spacing_x; - actual_grid_depth = (z_rows - 1) * spacing_z; - - // Center the grid within the switch cutout area - cutout_center_x = rack_width / 2; - cutout_center_z = front_thickness + switch_depth / 2; - - x_start = cutout_center_x - actual_grid_width / 2; - z_start = cutout_center_z - actual_grid_depth / 2; - - // Create back face holes with VERTICAL staggered pattern + module section_air_holes(i) { + sw = _sections[i][0]; sh = _sections[i][1]; sd = _sections[i][2]; + cx = section_center_x(i); cy = section_center_y(i); + cw = section_chassis_width(sw); + ch = section_chassis_height(sh); + center_z = front_thickness + sd / 2; + + hole_d = 16; spacing_x = 15; spacing_z = 17; margin = 3; + + avail_w = sw - 2 * margin; + avail_d = sd - 2 * margin; + x_cols = floor(avail_w / spacing_x); + z_rows = floor(avail_d / spacing_z); + grid_w = (x_cols - 1) * spacing_x; + grid_d = (z_rows - 1) * spacing_z; + x0 = cx - grid_w / 2; + z0 = center_z - grid_d / 2; + y_end = cy + ch / 2; if (x_cols > 0 && z_rows > 0) { - for (i = [0:x_cols-1]) { - for (j = [0:z_rows-1]) { - // Stagger every other COLUMN (i) instead of row (j) for vertical honeycomb pattern - z_offset = (i % 2 == 1) ? spacing_z/2 : 0; - x_pos = x_start + i * spacing_x; - z_pos = z_start + j * spacing_z + z_offset; - - // Only place hole if it fits within bounds after staggering - if (z_pos + hole_d/2 <= cutout_center_z + switch_depth/2 - margin && - z_pos - hole_d/2 >= cutout_center_z - switch_depth/2 + margin) { - translate([x_pos, height, z_pos]) { - rotate([90, 0, 0]) { - cylinder(h = height, d = hole_d, $fn = 6); - } - } - } + for (ii = [0:x_cols - 1]) for (jj = [0:z_rows - 1]) { + z_off = (ii % 2 == 1) ? spacing_z / 2 : 0; + xp = x0 + ii * spacing_x; + zp = z0 + jj * spacing_z + z_off; + if (zp + hole_d / 2 <= center_z + sd / 2 - margin && + zp - hole_d / 2 >= center_z - sd / 2 + margin) { + translate([xp, y_end, zp]) + rotate([90, 0, 0]) + cylinder(h = ch, d = hole_d, $fn = 6); } } } - - // SIDE FACE HOLES (X-axis through left and right sides) - // Calculate chassis dimensions - chassis_width = min(switch_width + (2 * case_thickness), (rack_width == 152.4) ? 120.65 : 221.5); - side_margin = (rack_width - chassis_width) / 2; - - // Calculate available space within switch height - available_height = switch_height - (2 * margin); - available_side_depth = switch_depth - (2 * margin); - - // Calculate number of holes that fit on sides - y_cols = floor(available_height / spacing_x); // Use spacing_x for Y direction - z_rows_side = floor(available_side_depth / spacing_z); - - // Calculate actual grid size for sides - actual_grid_height = (y_cols - 1) * spacing_x; - actual_grid_depth_side = (z_rows_side - 1) * spacing_z; - - // Center the grid within the switch cutout area (Y and Z) - cutout_center_y = height / 2; // Center of the 1U height - - y_start = cutout_center_y - actual_grid_height / 2; - z_start_side = cutout_center_z - actual_grid_depth_side / 2; - - // Create holes on both left and right sides with VERTICAL staggered pattern - if (y_cols > 0 && z_rows_side > 0) { - for (side = [0, 1]) { // 0 = left side, 1 = right side - side_x = side == 0 ? side_margin : rack_width - side_margin; - - for (i = [0:y_cols-1]) { - for (j = [0:z_rows_side-1]) { - // Stagger every other COLUMN (i) instead of row (j) for vertical honeycomb pattern - z_offset = (i % 2 == 1) ? spacing_z/2 : 0; - y_pos = y_start + i * spacing_x; - z_pos = z_start_side + j * spacing_z + z_offset; - - // Only place hole if it fits within bounds after staggering - if (z_pos + hole_d/2 <= cutout_center_z + switch_depth/2 - margin && - z_pos - hole_d/2 >= cutout_center_z - switch_depth/2 + margin) { - translate([side_x, y_pos, z_pos]) { - rotate([0, 90, 0]) { - rotate([0, 0, 90]) { // Rotate hexagon 90 degrees to match front/back orientation - cylinder(h = chassis_width, d = hole_d, $fn = 6); - } - } - } - } - } + + avail_h = sh - 2 * margin; + avail_sd = sd - 2 * margin; + y_cols = floor(avail_h / spacing_x); + z_rows_s = floor(avail_sd / spacing_z); + grid_h = (y_cols - 1) * spacing_x; + grid_ds = (z_rows_s - 1) * spacing_z; + y0 = cy - grid_h / 2; + z0s = center_z - grid_ds / 2; + x_left = cx - cw / 2; + x_right = cx + cw / 2; + if (y_cols > 0 && z_rows_s > 0) { + for (ii = [0:y_cols - 1]) for (jj = [0:z_rows_s - 1]) { + z_off = (ii % 2 == 1) ? spacing_z / 2 : 0; + yp = y0 + ii * spacing_x; + zp = z0s + jj * spacing_z + z_off; + if (zp + hole_d / 2 <= center_z + sd / 2 - margin && + zp - hole_d / 2 >= center_z - sd / 2 + margin) { + translate([x_left, yp, zp]) rotate([0, 90, 0]) rotate([0, 0, 90]) + cylinder(h = cw, d = hole_d, $fn = 6); + translate([x_right, yp, zp]) rotate([0, -90, 0]) rotate([0, 0, 90]) + cylinder(h = cw, d = hole_d, $fn = 6); } } } } - // Main assembly - cleaner boolean structure - translate([-rack_width/2, -height/2, 0]) { + module section_keystone_cutouts(i) { + sw = _sections[i][0]; sh = _sections[i][1]; + ks_count = _sections[i][6]; + ks_side = _sections[i][7]; + + kw = keystone_width + 2 * tolerance; + kh = keystone_height + 2 * tolerance; + pitch = kw + keystone_spacing; + total_w = ks_count * kw + (ks_count - 1) * keystone_spacing; + + cx = section_center_x(i); cy = section_center_y(i); + sec_left = cx - sw / 2; + sec_right = cx + sw / 2; + + wire_bump = front_wire_holes ? (wire_diameter / 5 + wire_diameter / 2) : 0; + + row_y = cy - kh / 2; + cut_depth = front_thickness + 2 * tolerance; + + eff_left = sec_left - wire_bump - keystone_gap_from_section; + eff_right = sec_right + wire_bump + keystone_gap_from_section; + start_x = (ks_side == 0) ? eff_left - total_w : eff_right; + + for (k = [0:ks_count - 1]) { + jx = start_x + k * pitch; + clears_rails = (jx >= rail_left_edge) && (jx + kw <= rail_right_edge); + if (clears_rails) { + translate([jx, row_y, -tolerance]) + cube([kw, kh, cut_depth]); + } else { + echo(str("WARNING: keystone skipped on section ", i + 1, + " index=", k, " x=", jx)); + } + } + } + + translate([-rack_width / 2, -height / 2, 0]) { difference() { main_body(); union() { - switch_cutout(); - all_rack_holes(); - zip_tie_features(); - if (front_wire_holes) { - power_wire_cutouts(); - } - if (air_holes) { - air_holes(); + for (i = [0:section_count - 1]) { + section_switch_cutout(i); + section_zip_tie_features(i); + if (front_wire_holes) section_power_wire_cutouts(i); + if (air_holes) section_air_holes(i); + if (_sections[i][5]) section_keystone_cutouts(i); } + all_rack_holes(); } } } } -// Call the module if (print_orientation) { - switch_mount(switch_width, switch_height, switch_depth); + switch_mount_multi(); } else { - rotate([-90,0,0]) - translate([0, -height/2, -switch_depth/2]) - switch_mount(switch_width, switch_height, switch_depth); + rotate([-90, 0, 0]) + translate([0, -height / 2, + -(max([ for (i = [0:section_count - 1]) _sections[i][2] ]) + 7) / 2]) + switch_mount_multi(); } From bfc1194f20ad911aa8042428bb7f262b8309cc16 Mon Sep 17 00:00:00 2001 From: RWB5510 H1 <107571028+rwb5510@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:12:11 -0400 Subject: [PATCH 2/2] Create usage guide for 10-inch rack mount generator Add usage guide for 10-inch rack mount generator with detailed instructions and parameters. --- Instructions.md | 211 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 Instructions.md diff --git a/Instructions.md b/Instructions.md new file mode 100644 index 0000000..0cb3d59 --- /dev/null +++ b/Instructions.md @@ -0,0 +1,211 @@ +# 10-inch Rack Mount Generator — Usage Guide + +Parametric OpenSCAD generator for 10-inch (and 6-inch) mini-rack faceplates +holding one to four devices per unit. Supports keystone jacks, angled load +supports, and a library of preset device sizes. + +## Requirements + +- OpenSCAD 2021.01 or newer (customizer with dropdowns). +- A slicer capable of handling `.stl` output. + +## Quick start + +1. Open the `.scad` file in OpenSCAD. +2. Open the Customizer panel (`Window → Customizer`). +3. Pick a rack width (10-inch / 6-inch) and height (in U). +4. Set `section_count` to the number of devices on this faceplate. +5. For each section, either pick a device from `sectionN_device` or leave + it on `Custom` and enter `width / height / depth` yourself. +6. Adjust `sectionN_x_offset` (and `y_offset` for tall racks) to position + each opening. +7. Render (F6) and export STL. + +## Parameters + +### Rack + +| Name | Values | Notes | +|---|---|---| +| `rack_width` | `254.0` (10") · `152.4` (6") | Determines usable width, mounting-slot spacing, and slot size. | +| `rack_height` | `0.5` – `5` in `0.5` steps | U count. Most single-device layouts fit 1U; Mac Mini, NAS, and tower-oriented devices need 2U+. | +| `half_height_holes` | `true` / `false` | When `true`, partial rack-rail slots at the edges of fractional-U mounts are rendered. | + +### Section count + +| Name | Values | Notes | +|---|---|---| +| `section_count` | `1` – `4` | Number of openings on the faceplate. Only the first N sections are rendered. | + +### Per-section (Section 1 shown; Sections 2–4 follow the same pattern) + +| Name | Values | Notes | +|---|---|---| +| `section1_device` | Dropdown | `Custom` to use manual `width/height/depth`; any other value pulls dimensions from the preset table (see below). | +| `section1_width` | mm | Device width. Ignored when a preset is selected. | +| `section1_height` | mm | Device height. Ignored when a preset is selected. | +| `section1_depth` | mm | Device depth (front to back). Ignored when a preset is selected. | +| `section1_x_offset` | mm | Horizontal offset of the opening from the **center** of the faceplate. Negative = left. | +| `section1_y_offset` | mm | Vertical offset from the center. Only useful in 2U+ layouts. | +| `section1_keystone` | `true` / `false` | Cut keystone jack holes in the faceplate next to this opening. | +| `section1_keystone_count` | `1` – `6` | Number of jacks in the row. | +| `section1_keystone_side` | `0` / `1` | `0` = left of opening, `1` = right of opening. | +| `section1_support` | `true` / `false` | Add angled support gussets on both outer sides of the sleeve. | + +### Shared + +| Name | Values | Notes | +|---|---|---| +| `case_thickness` | mm | Wall thickness of each sleeve. Default `6`. | +| `wire_diameter` | mm | Diameter of the optional front-panel wire pass-through holes. | +| `front_wire_holes` | `true` / `false` | If `true`, adds round wire-pass holes at the mid-height of each opening, flanking the device. | +| `air_holes` | `true` / `false` | Staggered hex vents on the top, bottom, and sides of each sleeve. | +| `print_orientation` | `true` / `false` | `true` = laid flat on the print bed (back-down). `false` = rotated face-forward for display or preview. | +| `tolerance` | mm | Extra space around cutouts for printer fitment. Default `0.42` suits most FDM printers. | + +### Keystone jacks (globals) + +| Name | Values | Notes | +|---|---|---| +| `keystone_width` | mm | Cutout width. Default `14.94` is the industry-standard snap-in size. | +| `keystone_height` | mm | Cutout height. Default `16.51`. | +| `keystone_spacing` | mm | Gap between adjacent jacks in a row. | +| `keystone_gap_from_section` | mm | Gap between the first jack and the opening edge. | + +Behavior: +- Cutouts only go through the faceplate, not the sleeve behind it. +- If `front_wire_holes` is enabled, jacks are pushed outward by the wire-hole + perimeter so bezels don't clip the circles. +- Any jack that would collide with the rack-rail slot column is silently + dropped and an `echo` warning is printed to the console. + +### Angled supports (globals) + +| Name | Values | Notes | +|---|---|---| +| `support_depth_requested` | mm | How far back along the sleeve the gusset ramps. Longer = stiffer. | +| `support_width_requested` | mm | How far along the faceplate the gusset extends. Actual width is clamped per section to the available gutter. | +| `support_rail_margin` | mm | Safety clearance between a gusset and the nearest rack-rail slot. | +| `support_min_width` | mm | Minimum renderable gusset width. Below this, the support is skipped with an `echo` warning. | + +Behavior: +- Gussets render on both outer walls of any section where + `sectionN_support = true`. +- Each side is independently clamped: the requested width is reduced to + fit between the chassis wall and the nearest obstacle (adjacent section's + chassis or the rack-rail column, minus the rail margin). + +## Device presets + +Selecting any value other than `Custom` in `sectionN_device` overrides that +section's manual `width / height / depth`. Dimensions are listed as +**W × H × D** (width across the rack, height up, depth into the rack). + +### Network gear + +| Device | W × H × D (mm) | Fits in | +|---|---|---| +| Firewalla Gold | 130 × 34 × 110 | 1U | +| Firewalla Purple (wifi) | 90 × 30 × 60 | 1U | +| Firewalla Purple (Ethernet) | 130 × 34 × 110 | 1U | +| Firewalla Purple SE | 90 × 30 × 60 | 1U | +| UniFi Security Gateway | 135 × 28.3 × 135 | 1U | +| UniFi Cloud Key G2+ | 131.2 × 134.2 × 27.1 | 4U (tall orientation) | +| UniFi Flex Mini | 107 × 21 × 70 | 1U | +| UniFi Flex Mini 2.5G | 117.1 × 21.2 × 90 | 1U | +| UniFi Flex 2.5 | 212.9 × 33.5 × 76 | 1U | +| UniFi Lite 8 PoE | 99.6 × 31.7 × 163.7 | 1U | +| UniFi Lite 16 PoE | 192 × 44 × 185 | 1U | +| UniFi Express | 98 × 30 × 98 | 1U | +| UniFi Cloud Gateway Ultra/Max | 141.8 × 30 × 127.6 | 1U | + +### Compute + +| Device | W × H × D (mm) | Fits in | +|---|---|---| +| IBM M70q Gen 5 | 179 × 36.5 × 182.9 | 1U | +| IBM M70q Gen 4 | 179 × 34.5 × 183 | 1U | +| IBM M90q Gen 5 | 179 × 36.5 × 182.9 | 1U | +| Dell OptiPlex 7020 | 182 × 36 × 178 | 1U | +| HP Elite Mini 800 | 177.5 × 34.3 × 175.2 | 1U | +| M4 Mac Mini | 127 × 50 × 127 | 2U | +| BeeLink ME Mini | 99 × 99 × 99 | 3U | +| Xyber Hydra | 140 × 34.5 × 98.5 | 1U | + +### Storage + +| Device | W × H × D (mm) | Fits in | +|---|---|---| +| Synology DS223j | 165 × 225.5 × 100 | 6U | +| Synology DS223 | 165 × 232.7 × 108 | 6U | +| Synology DS124 | 166 × 224 × 71 | 6U | + +> Preset dimensions marked "theoretical/untested" on the MakerWorld source +> are reproduced as-is. Measure your actual device before committing to a +> print for tight-fitting gear. + +If a selected preset is taller than the current `rack_height`, OpenSCAD +prints a warning in the console. Increase `rack_height` or choose a preset +that fits. + +## Recipes + +### Single 1U switch (original behavior) +rack_height = 1.0 +section_count = 1 +section1_device = UniFi Security Gateway +section1_x_offset = 0 + +### Two small devices side-by-side (dual-sleeve) +section_count = 2 +section1_device = Firewalla Purple SE ; section1_x_offset = -56 +section2_device = UniFi Flex Mini ; section2_x_offset = 56 + +### Two devices with keystone jacks in the center gutter +section_count = 2 +section1_keystone = true ; section1_keystone_side = 1 (right of §1) +section2_keystone = true ; section2_keystone_side = 0 (left of §2) + +### Heavy single device with supports +rack_height = 2.0 +section_count = 1 +section1_device = M4 Mac Mini +section1_support = true +support_depth_requested = 25 +support_width_requested = 12 + +### Three small devices across a 1U +section_count = 3 +section1_device = Firewalla Purple SE ; section1_x_offset = -85 +section2_device = UniFi Express ; section2_x_offset = 0 +section3_device = Firewalla Purple SE ; section3_x_offset = 85 + +## Console warnings +The model emits `echo` messages during preview/render when it skips or +clamps geometry. Check the OpenSCAD console if something didn't render as +expected. +| Message | Meaning | Action | +|---|---|---| +| `section N height (X) exceeds rack_height (Y)` | Preset is too tall for current U count. | Increase `rack_height`. | +| `keystone skipped on section N index=K` | A jack would have cut into the rack-rail slot column. | Reduce `sectionN_keystone_count` or move the section with `x_offset`. | +| `support skipped on section N side S` | Not enough free gutter to render a gusset on that side. | Reduce `support_width_requested`, move the adjacent section, or disable the support. | +## Printing notes +- **Material**: PETG or ASA preferred for heavy loads (Mac Mini, NAS, + micro-PCs). PLA works for light devices but creeps under sustained load. +- **Perimeters**: 4+ for heavy devices. +- **Infill**: 40%+ for heavy devices, 20% fine for network gear. +- **Orientation**: Print with `print_orientation = true` (default). + The faceplate sits flat on the bed and the sleeves stand up — no + supports needed in the slicer. +- **Tolerance**: Default `tolerance = 0.42` suits a well-tuned FDM printer. + If devices slide in too loosely, drop to `0.30`; too tight, raise to `0.50`. +## Credits +- Original generator: base single-switch design. +- Dual-sleeve feature: [cjolivier01](https://github.com/cjolivier01), + PR #20 "Added dual sleeved version". +- Keystone jack feature: PR #21. +- Device preset dimensions: compiled from the + [MakerWorld 10-inch Mini Rack Generator project page](https://makerworld.com/en/models/1765102-10-inch-mini-rack-generator). +- Multi-section generalization, angled supports, collision clamping, + preset dropdown: this revision. +