Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
4121c9fb7This file is pinned to the selected release. Download the original to open it in your design tool.
// HomeRacker - Bracket
//
// This file is part of HomeRacker implementation by KellerLab.
// It contains the bracket module — a clamp that wraps a device from above or below
// and hangs it in a HomeRacker frame via lock pins.
//
// The part has two halves: a shell that grips the device (top plate with a window,
// plus side walls reaching down as far as you want) and a pair of mount wings on
// the left and right that cap the vertical supports. The wings widen by whatever
// the device width misses on the 15mm grid, so the lock pin holes always land on
// a support no matter how odd the device measures.
//
// Use cases: hanging network gear, mini PCs, power bricks or any box-shaped device
// in a rack, and as the device-holding half of larger mounts.
//
// MIT License
// Copyright (c) 2026 Patrick Pötz
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
include <BOSL2/std.scad>
include <../../core/lib/constants.scad>
include <../../core/lib/lockpin.scad>
// Hidden constants for the bracket library (no Customizer section marker)
HR_BRACKET_PRIMARY_COLOR = HR_YELLOW;
// Front-to-back footprint of a mount wing: one support unit plus its enclosing walls.
BRACKET_MOUNT_DEPTH = BASE_UNIT + BASE_STRENGTH * 2 + TOLERANCE;
// Default overlap of shell material on the device, in mm.
BRACKET_DEFAULT_STRENGTH = 7.5;
// Shallowest device that still leaves room for one column of mount wings.
BRACKET_MIN_DEVICE_DEPTH = BASE_UNIT;
// Lowest device the bridge can span: its skeleton cutout is rounded by half a unit,
// and the rounding has to fit between the bridge floor and its chamfered top.
BRACKET_MIN_DEVICE_HEIGHT = BASE_UNIT / 2 + BASE_CHAMFER * 2 - TOLERANCE / 2;
// Which frame supports the wings hook over.
BRACKET_AXIS_X = "x"; // supports running left to right, parallel to the device face
BRACKET_AXIS_Y = "y"; // supports running front to back, along the sides of the frame
/** Bracket shell module
* The clamp that sits on the device: a top plate with a window cut out of it and
* side walls running down the device flanks.
* @param device_width Device width in mm, tolerance already applied.
* @param device_depth Device depth in mm, tolerance already applied.
* @param device_height Device height in mm.
* @param strength_top How far the top plate reaches inward over the device.
* @param strength_sides How far the side walls reach down the device.
* @param outer_depth Outer depth of the shell in mm.
*/
module bracket_shell(device_width, device_depth, device_height,
strength_top, strength_sides, outer_depth,
color = HR_BRACKET_PRIMARY_COLOR, debug_colors = false, disable_chamfer = false) {
_chamfer = disable_chamfer ? 0 : BASE_CHAMFER;
_strength_sides = min(strength_sides, device_height - BASE_STRENGTH);
// The top plate can reach inward at most to the middle of the device.
_strength_top_limit = min(device_depth, device_width) / 2;
_strength_top = min(strength_top, _strength_top_limit);
_width = device_width + BASE_STRENGTH * 2;
_height = _strength_sides + BASE_STRENGTH;
_z = device_height / 2 - _height / 2 + BASE_STRENGTH;
// A window that reaches the limit leaves no top plate at all, so a chamfer there
// would bite into the side walls instead of breaking a real edge.
_window_chamfer = _strength_top == _strength_top_limit ? 0 : -_chamfer;
tag_scope("bracket_shell")
diff()
color_this(debug_colors ? HR_GREEN : color)
up(_z)
cuboid([_width, outer_depth, _height], chamfer = _chamfer) {
// Device volume
tag("remove") down(_z) cuboid([device_width, device_depth, device_height]);
// Window in the top plate
tag("remove") cuboid([device_width - _strength_top * 2, device_depth - _strength_top * 2, _height],
chamfer = _window_chamfer);
}
}
/** Bracket mount cap module
* The U that drops over a support, with a lock pin hole through both walls.
* Origin sits on the plane where the walls meet the bridge, walls hanging below it.
* Built for a support running along X, then spun a quarter turn for the other axis.
* @param length Cap length along the support in mm.
* @param mount_axis Which way the support runs, BRACKET_AXIS_X or BRACKET_AXIS_Y.
* @param hole_offset Lock pin hole position, measured inward from the cap middle.
*/
module bracket_mount_cap(length, mount_axis = BRACKET_AXIS_X, hole_offset = 0,
color = HR_BRACKET_PRIMARY_COLOR, debug_colors = false, disable_chamfer = false) {
_chamfer = disable_chamfer ? 0 : BASE_CHAMFER;
_wall = [length, BASE_STRENGTH, BASE_UNIT];
zrot(mount_axis == BRACKET_AXIS_Y ? 90 : 0)
tag_scope("bracket_mount_cap")
diff() {
color_this(debug_colors ? HR_BLUE : color)
back(BRACKET_MOUNT_DEPTH / 2 - BASE_STRENGTH / 2)
down(BASE_UNIT / 2)
cuboid(_wall, chamfer = _chamfer, edges = BACK, except = TOP);
color_this(debug_colors ? HR_BLUE : color)
fwd(BRACKET_MOUNT_DEPTH / 2 - BASE_STRENGTH / 2)
down(BASE_UNIT / 2)
cuboid(_wall, chamfer = _chamfer, edges = FRONT, except = TOP);
color_this(debug_colors ? HR_BLUE : color)
up(BASE_STRENGTH / 2)
cuboid([length, BRACKET_MOUNT_DEPTH, BASE_STRENGTH],
chamfer = _chamfer, edges = [FRONT, BACK], except = [TOP, BOTTOM]);
// Lock pin hole, placed by the caller relative to the middle of the cap
tag("remove")
left(hole_offset)
down(BASE_UNIT / 2)
lockpin_hole(depth = BRACKET_MOUNT_DEPTH + HR_EPSILON, orient = BACK,
chamfer_top = !disable_chamfer, chamfer_bottom = !disable_chamfer);
}
}
/** Bracket mount module
* A single mount wing on the left of the device: a skeletonised bridge reaching out
* to the support, with a cap at its end that drops over the support and takes a pin.
* The wing is placed relative to the device, because its reach is what aligns the
* lock pin hole to the 15mm grid.
* @param device_width Device width in mm, tolerance already applied.
* @param device_height Device height in mm.
* @param extra_units Whole units of reach added beyond the nearest grid line.
* @param mount_axis Which way the support runs, BRACKET_AXIS_X or BRACKET_AXIS_Y.
*/
module bracket_mount(device_width, device_height, extra_units = 0, mount_axis = BRACKET_AXIS_X,
color = HR_BRACKET_PRIMARY_COLOR, debug_colors = false, disable_chamfer = false) {
_chamfer = disable_chamfer ? 0 : BASE_CHAMFER;
// Turned sideways the cap wraps the support across the width, so it reaches back
// toward the device by half the difference between its footprint and a support.
// The span has to clear the device edge by that much, which costs a whole unit when
// the width lands just under a grid line. The shell wall needs no room of its own
// here: it sits beside the device while the cap hangs well below it.
_span_clearance = mount_axis == BRACKET_AXIS_Y ? BRACKET_MOUNT_DEPTH - BASE_UNIT : 0;
// Round the clear span between the supports up to the grid, then split what the
// device misses across the two wings. A width already on the grid needs no widening.
_support_span = ceil((device_width + _span_clearance - HR_EPSILON) / BASE_UNIT) * BASE_UNIT;
_grid_offset = max(0, (_support_span - device_width) / 2);
// How far the wing reaches past the device edge, so the pin lands on the grid.
_reach = BASE_UNIT + _grid_offset + extra_units * BASE_UNIT;
// The cap matches the bridge footprint on both axes so the two sit flush: along the
// device face it also closes the gap left by the grid rounding, and spun sideways it
// runs the full depth of the bridge.
_cap_length = mount_axis == BRACKET_AXIS_Y ? BRACKET_MOUNT_DEPTH : BASE_UNIT + _grid_offset;
// Spun sideways the pin sits in the middle of the cap, otherwise half a unit in from
// its outer end, which is what lands it on the support.
_cap_hole_offset = mount_axis == BRACKET_AXIS_Y ? 0 : _cap_length / 2 - BASE_UNIT / 2;
// Spun sideways the cap wraps the support in X, so it reaches past the bridge end.
_cap_overhang = mount_axis == BRACKET_AXIS_Y ? (BRACKET_MOUNT_DEPTH - BASE_UNIT) / 2 : 0;
_cap_center = device_width / 2 + _reach - (mount_axis == BRACKET_AXIS_Y ? BASE_UNIT / 2 : _cap_length / 2);
_height = device_height + BASE_STRENGTH + TOLERANCE / 2 - BASE_CHAMFER;
_z = (BASE_STRENGTH - TOLERANCE / 2 - BASE_CHAMFER) / 2;
_bridge_length = _reach + _cap_overhang;
_bridge_width = _bridge_length - BASE_STRENGTH;
_bridge_height = _height - BASE_CHAMFER;
up(_z)
union() {
// Bridge from shell to cap, hollowed out from below to save material
left(device_width / 2 + _bridge_length / 2)
difference() {
color_this(debug_colors ? HR_RED : color)
up(BASE_STRENGTH - BASE_CHAMFER / 2)
cuboid([_bridge_width + BASE_STRENGTH, BRACKET_MOUNT_DEPTH, _bridge_height],
chamfer = _chamfer, except = BOTTOM);
left(BASE_STRENGTH / 2)
up(BASE_CHAMFER / 2)
cuboid([_bridge_width, BASE_UNIT, _bridge_height - BASE_STRENGTH],
chamfer = BASE_UNIT / 2, edges = [FRONT + BOTTOM, BACK + BOTTOM]);
}
left(_cap_center)
down(_height / 2)
bracket_mount_cap(_cap_length, mount_axis, _cap_hole_offset,
color = color, debug_colors = debug_colors, disable_chamfer = disable_chamfer);
}
}
/** Bracket mount pair module
* Mount wings on both flanks of the device. The two sides take their own reach, so
* an odd number of extra units shifts the device within the frame instead of
* widening it symmetrically.
* @param device_width Device width in mm, tolerance already applied.
* @param device_height Device height in mm.
* @param units_left Whole units of extra reach on the left wing.
* @param units_right Whole units of extra reach on the right wing.
* @param mount_axis Which way the support runs, BRACKET_AXIS_X or BRACKET_AXIS_Y.
*/
module bracket_mount_pair(device_width, device_height, units_left = 0, units_right = 0,
mount_axis = BRACKET_AXIS_X,
color = HR_BRACKET_PRIMARY_COLOR, debug_colors = false, disable_chamfer = false) {
bracket_mount(device_width, device_height, units_left, mount_axis,
color = color, debug_colors = debug_colors, disable_chamfer = disable_chamfer);
xflip()
bracket_mount(device_width, device_height, units_right, mount_axis,
color = color, debug_colors = debug_colors, disable_chamfer = disable_chamfer);
}
/** Bracket module
* The full printable part: shell plus one or two columns of mount wings.
* Device dimensions are nominal, the standard TOLERANCE is added internally.
* The attachable bounding box covers the shell only, the wings reach beyond it.
* @param device_width Device width in mm.
* @param device_depth Device depth in mm.
* @param device_height Device height in mm.
* @param strength_top How far the top plate reaches inward over the device.
* @param strength_sides How far the side walls reach down the device.
* @param mount_offset_y Pushes the wings back from the front edge, for mounts that
* need to sit flush behind something such as a front panel.
* @param mount_columns Wing columns, 1 or 2. Left undefined, it picks 2 whenever
* the device is deep enough for them to clear each other.
* @param mount_axis Which frame supports the wings hook over, BRACKET_AXIS_X for the
* ones along the device face or BRACKET_AXIS_Y for the frame sides.
* @param mount_gap_units Whole units added to the span between the wings. An odd
* count lands on the left first, shifting the device.
*/
module bracket(device_width, device_depth, device_height,
strength_top = BRACKET_DEFAULT_STRENGTH, strength_sides = BRACKET_DEFAULT_STRENGTH,
mount_offset_y = 0, mount_columns = undef,
mount_axis = BRACKET_AXIS_X, mount_gap_units = 0,
color = HR_BRACKET_PRIMARY_COLOR, debug_colors = false, disable_chamfer = false,
anchor = CENTER, spin = 0, orient = UP) {
assert(device_width > 0, "Device width must be positive");
assert(device_depth >= BRACKET_MIN_DEVICE_DEPTH,
str("Device depth must be at least ", BRACKET_MIN_DEVICE_DEPTH, "mm to fit a mount wing"));
assert(device_height >= BRACKET_MIN_DEVICE_HEIGHT,
str("Device height must be at least ", BRACKET_MIN_DEVICE_HEIGHT, "mm"));
assert(strength_top >= BASE_STRENGTH && strength_sides >= BASE_STRENGTH,
str("Shell strengths must be at least ", BASE_STRENGTH, "mm, thinner walls would not hold the device"));
assert(mount_offset_y >= 0 && mount_offset_y <= device_depth - BRACKET_MIN_DEVICE_DEPTH,
str("Mount offset must be between 0 and ", device_depth - BRACKET_MIN_DEVICE_DEPTH,
"mm at this device depth, beyond that the wings hang off the back of the shell"));
assert(is_undef(mount_columns) || mount_columns == 1 || mount_columns == 2,
"Mount columns must be 1 or 2");
assert(mount_axis == BRACKET_AXIS_X || mount_axis == BRACKET_AXIS_Y,
str("Mount axis must be \"", BRACKET_AXIS_X, "\" or \"", BRACKET_AXIS_Y, "\""));
assert(is_int(mount_gap_units) && mount_gap_units >= 0,
"Mount gap units must be a whole number of HomeRacker units, zero or more");
_width = device_width + TOLERANCE;
_depth = device_depth + TOLERANCE;
_outer_depth = _depth + BASE_STRENGTH * 2;
_strength_sides = min(strength_sides, device_height - BASE_STRENGTH);
// Wings sit on the 15mm grid, so the usable spacing rounds down to a multiple of it.
// Hooking the side supports puts the pins through the Y axis, where both columns have
// to sit a whole number of units from the middle rather than just from each other, so
// the spacing rounds down two units at a time and the pair stays centred.
_spacing_step = mount_axis == BRACKET_AXIS_Y ? BASE_UNIT * 2 : BASE_UNIT;
_max_spacing = floor((_outer_depth - mount_offset_y - BRACKET_MOUNT_DEPTH) / _spacing_step) * _spacing_step;
_columns = is_undef(mount_columns) ? (_max_spacing >= BRACKET_MOUNT_DEPTH ? 2 : 1) : mount_columns;
_spacing = _columns > 1 ? _max_spacing : 0;
// Without an offset the wings stay centred, otherwise they line up behind it.
_shift_y = mount_offset_y > 0 ? (-_outer_depth + _spacing + BRACKET_MOUNT_DEPTH) / 2 + mount_offset_y : 0;
// An offset moves the pair off the grid, so it rounds to the nearest whole unit too.
// Rounding up can push the outermost cap past the back of the shell, which the
// mount_offset_y limit alone does not catch because it is checked before rounding.
// Where that happens the offset steps down to the last unit that still fits.
_max_shift = max(0, _outer_depth / 2 - _spacing / 2 - BRACKET_MOUNT_DEPTH / 2);
_snapped_shift = round(_shift_y / BASE_UNIT) * BASE_UNIT;
_safe_shift = abs(_snapped_shift) <= _max_shift + HR_EPSILON
? _snapped_shift
: sign(_snapped_shift) * floor(_max_shift / BASE_UNIT) * BASE_UNIT;
_grid_snap = mount_axis == BRACKET_AXIS_Y ? _safe_shift - _shift_y : 0;
// An odd unit count starts on the left, so the device shifts within the frame.
_units_left = ceil(mount_gap_units / 2);
_units_right = floor(mount_gap_units / 2);
assert(_columns < 2 || _max_spacing >= BRACKET_MOUNT_DEPTH,
str("Two mount columns need a device depth of at least ",
BASE_UNIT * 3 + mount_offset_y, "mm at this mount offset, otherwise they overlap"));
attachable(anchor = anchor, spin = spin, orient = orient,
size = [_width + BASE_STRENGTH * 2, _outer_depth, _strength_sides + BASE_STRENGTH]) {
down((device_height + BASE_STRENGTH - _strength_sides) / 2)
union() {
bracket_shell(_width, _depth, device_height, strength_top, _strength_sides, _outer_depth,
color = color, debug_colors = debug_colors, disable_chamfer = disable_chamfer);
back(_shift_y + _grid_snap)
ycopies(spacing = _spacing, n = _columns)
bracket_mount_pair(_width, device_height, _units_left, _units_right, mount_axis,
color = color, debug_colors = debug_colors, disable_chamfer = disable_chamfer);
}
children();
}
}