20 Commits

Author SHA1 Message Date
kellervater
4ebbd43426 fix(wallmount): orientation 2025-05-03 23:28:36 +02:00
kellervater
acd7403101 feat: wallmount 2025-05-03 12:15:09 +02:00
kellervater
b5508af9d3 fix(unimount): typo 2025-04-27 17:22:40 +02:00
kellervater
beb5f39722 feat(rackmount_ears): option to show actual ear distance 2025-04-27 12:56:06 +02:00
kellervater
a1a6777aea fix(rackmountears): sane defaults 2025-04-27 12:46:13 +02:00
Patrick Pötz
ebeb258d60 feat(rackmount_ears): asymetry slider (#13)
Added "insane" asymetry slider to create differently sized rackmount
ears.
2025-04-27 12:42:26 +02:00
kellervater
98e19efa60 feat: flexmount bracket 2025-04-27 11:28:55 +02:00
kellervater
44ed1746f9 docs: updated links & anchors 2025-04-26 12:32:12 +02:00
kellervater
a5bad40082 feat(shelf): python exporter 2025-04-26 12:31:27 +02:00
kellervater
bbb742f271 chore: pre-commit config for python 2025-04-26 12:31:27 +02:00
Patrick Pötz
9dd9add861 docs: updated ToDos 2025-04-19 11:56:35 +02:00
kellervater
bc80d1e65f fix: logo transparency 2025-04-13 11:22:44 +02:00
kellervater
34cd659177 fix: logo details 2025-04-13 11:21:05 +02:00
kellervater
3a7f1ebb1b feat: add overlay logo 2025-04-13 11:03:38 +02:00
kellervater
631ef6e837 fix: transparent favicon 2025-04-13 10:47:49 +02:00
kellervater
91a43c8259 fix: favicon 2025-04-13 10:43:03 +02:00
kellervater
e91dffa6cc feat: favicon 2025-04-13 10:40:33 +02:00
kellervater
1ad8a68308 docs: try fixing links once more 2025-04-12 20:58:29 +02:00
kellervater
9ed1685b80 docs: fixed links 2025-04-12 20:55:28 +02:00
Patrick Pötz
3e935d4f3d docs: Initial Version (#9)
This is the initial version which I'm happy with. Took me almost the entire day to write.
Proof-read by ChatGPT because I'm bad in phrasing things.
2025-04-12 20:27:18 +02:00
18 changed files with 3215 additions and 85 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
*.3mf
*.stl
.vscode

11
.lint/pylint/.pylintrc Normal file
View File

@@ -0,0 +1,11 @@
[BASIC]
good-names=i,j,k,ex,Run,_,pk,x,y
[FORMAT]
max-line-length=121
[MESSAGES CONTROL]
disable=import-error,logging-fstring-interpolation,missing-module-docstring,missing-function-docstring,missing-class-docstring,duplicate-code

View File

@@ -14,4 +14,12 @@ repos:
- id: renovate-config-validator
args: ["--strict"]
language_version: 20.18.0 # workaround till https://github.com/renovatebot/pre-commit-hooks/issues/2460 is fixed
- repo: https://github.com/PyCQA/pylint
rev: v3.3.6
hooks:
- id: pylint
args:
[
"--rcfile=.lint/pylint/.pylintrc"
]
...

100
README.md
View File

@@ -1,4 +1,4 @@
[📚 What's it for?](#use-cases) | [⚙️ How does it work?](#how-it-works) | [🌐 Free & OpenSource](#open-standard)
[📚 What's it for?](#-use-cases) | [⚙️ How does it work?](#-features) | [🌐 Free & OpenSource](#-open-specs)
![HomeRacker Assembly](./img/assembly_basics_4.png)
@@ -8,27 +8,27 @@ You can find all parametric and non-parametric models, as well as the `.f3d` fil
The parametric models are available in the [HomeRacker GitHub Repository](https://github.com/kellervater/homeracker/tree/main/models).
> [!NOTE]
> 💡 **Note**
> The basic HomeRacker system is also referred to as `HomeRacker - Core`. Free for everyone to use, remix, and reshare.
# 📑 Table of Contents
- [🔧 Use Cases](#use-cases)
- [✨ Features](#features)
- [⚙️ How it works](#how-it-works)
- [🛠️ Assembly Basics](#assembly-basics)
- [💡 Assembly Tips](#assembly-tips)
- [🖨️ Printing Tips](#printing-tips)
- [📐 Tech Specs](#tech-specs)
- [🧱 Supports](#supports)
- [🔗 Connectors](#connectors)
- [📏 Lock Pins](#lock-pins)
- [🌍 Open Specs](#open-specs)
- [❓ Why the name?](#why-the-name)
- [🔧 Use Cases](#-use-cases)
- [✨ Features](#-features)
- [⚙️ How it works](#%EF%B8%8F-how-it-works)
- [🛠️ Assembly Basics](#%EF%B8%8F-assembly-basics)
- [💡 Assembly Tips](#-assembly-tips)
- [🖨️ Printing Tips](#%EF%B8%8F-printing-tips)
- [📐 Tech Specs](#-tech-specs)
- [🧱 Supports](#-supports)
- [🔗 Connectors](#-connectors)
- [📏 Lock Pins](#-lock-pins)
- [🌍 Open Specs](#-open-specs)
- [❓ Why the name?](#-why-the-name)
- [📜 Licensing](#-licensing)
- [🧪 Tests](#tests)
- [⚠️ Disclaimer](#disclaimer)
- [🔬 How I tested](#how-i-tested)
- [📋 Todos](#todos)
- [🧪 Tests](#-tests)
- [⚠️ Disclaimer](#%EF%B8%8F-disclaimer)
- [🔬 How I tested](#-how-i-tested)
- [📋 Todos](#-todos)
# 🔧 Use Cases
I created HomeRacker because I was dissatisfied with the existing solutions available online.
@@ -44,15 +44,15 @@ To give you an idea of how this may look (10" rack, half-constructed Pi mini-rac
![Real Life Example](./img/real_life_example.jpg)
Aside from the basic [building blocks](#basic-building-blocks), the rack above also contains the following parts:
Aside from the basic [building blocks](#-tech-specs), the rack above also contains the following parts:
* **10" Rack**
* HomeRacker - 10" Rackmount Kit (Todo: link to Makerworld model) for standard-height units
* Raspi 5 Mount Kit:
* [HomeRacker - 10" Rackmount Kit](https://makerworld.com/en/models/1353730-modular-10-server-rack#profileId-1396904) for standard-height units
* [Raspi 5 Mount Kit](https://makerworld.com/en/models/1324096-pi-5-snapcase-for-homeracker#profileId-1360937):
* Vertical Mount Adapter for HomeRacker
* Front panel for 10" racks
* Rackmount ears for the switch. These are fully customizable rackmount ears I created as an [OpenSCAD file](./models/rackmount_ears/rackmount_ears.scad). You can customize it directly [here](https://makerworld.com/en/models/1259227-fully-customizable-rackmount-ears#profileId-1283271).
* HomeRacker Airflow Kit (Todo: link to Makerworld model), which consists of:
* Rackmount ears for the switch. These are fully customizable rackmount ears I created as an [OpenSCAD file](https://github.com/kellervater/homeracker/blob/main/models/rackmount_ears/rackmount_ears.scad). You can customize it directly [here](https://makerworld.com/en/models/1259227-fully-customizable-rackmount-ears#profileId-1283271).
* [HomeRacker Airflow Kit](https://makerworld.com/en/models/1353730-modular-10-server-rack#profileId-1396904) (currently part of the 10" rack model), which consists of:
* Front/back panels
* Side panels
* Bottom/top panels with air intake/exhaust grids and bores for standard fans (80/92/120mm)
@@ -66,7 +66,7 @@ The `HomeRacker - Core` features:
* **Fully modular** Thanks to the support-connector system, you can scale in any direction. The only limits are material strength—and how much money, space, and time you have.
* **3D-printable** The entire core system is printable, and no tools are required for assembly.
* **No supports needed** Not a single part of the core system needs printed supports.
* **OpenSource** Build your own adapters and use the system in personal or commercial projects (see [Open Standard](#open-standard) and [Licensing](#-licensing) for details).
* **OpenSource** Build your own adapters and use the system in personal or commercial projects (see [🌍 Open Specs](#-open-specs) and [📜 Licensing](#-licensing) for details).
## ⚙️ How it works
> **tl;dr** Think of a rack shape you want to build, download the model (insert link), print it, assemble it, and add your own mounts/adapters/whatever-you-like.
@@ -86,8 +86,7 @@ Assembly is straightforward and requires no tools:
### 💡 Assembly Tips
> [!NOTE]
> **Pro Tip**: I created a sample 10" Cyberpunk-themed 3D model on MakerWorld (❗Todo: insert link) for inspiration.
> 💡 **Pro Tip**: I created a sample 10" Cyberpunk-themed 3D model on MakerWorld (❗Todo: insert link) for inspiration.
1. Plan ahead! Otherwise, you'll end up with an army of unused parts like me:
@@ -106,13 +105,11 @@ Assembly is straightforward and requires no tools:
1. If bed adhesion is sketchy: Add a brim to the supports. The small contact surface on connectors can cause print failures.
2. Prevent warping: Keep the print bed clean and oil-free. Even a fingerprint can cause issues.
3. When printing new filament: Make sure to calibrate the material flow!
> [!NOTE]
> **Pro Tip**: If you need to disassemble and the pin is stuck, push it from the other side with another pin to release it.
> 💡 **Pro Tip**: If you need to disassemble and the pin is stuck, push it from the other side with another pin to release it.
## 📐 Tech Specs
> [!NOTE]
> For actual dimensions, check out the original Fusion `.f3d` files on Makerworld. All designs are fully parameterized for easy scaling.
> 💡 **Note** - For actual dimensions, check out the original Fusion `.f3d` files on Makerworld. All designs are fully parameterized for easy scaling.
The system is based on 4 core measurements:
@@ -160,7 +157,7 @@ Types:
* **Pull-Through** Open center for complex builds (e.g. 10" rack).
* **Feet** Solid end pieces; used as rack feet.
> [!IMPORTANT]
> ❗ **Important**
> Connector centers are always 1 `base_unit` in height. No offsets, no fluff.
> So: 2 × 3-unit supports + 1 connector = exactly 7 base units (105mm).
@@ -187,6 +184,7 @@ Schematics:
**Side View**
> Dont ask why the height is 3.791mm—it works. I left it as-is.
![lock Pin Side](./img/core_lock_pin_side.png)
> I may tweak the pin grip in future versions for to make it easier to pull-out again. But the base dimensions will remain the same for compatibility.
@@ -194,7 +192,7 @@ Schematics:
## 🌍 Open Specs
I created `HomeRacker - Core` to be an open spec that any maker can build on, with (almost) no strings attached.
(See [Licensing](#-licensing) for more.)
(See [📜 Licensing](#-licensing) for more.)
I encourage you to make your own models based on HomeRacker!
Let me know, and Ill feature your work on this page and cross-link it on Makerworld (subject to my "very objective" approval 😄).
@@ -208,15 +206,15 @@ So, "HomeRacker" was born—part practical, part tongue-in-cheek. It fits the ho
# 📜 Licensing
> [!NOTE]
> tl;dr Use it for ANY purpose (even commercial), but credit me and share alike!
> 💡 **tl;dr**
> Use it for ANY purpose (even commercial), but credit me and share alike!
* Source code: `MIT License` ([LICENSE](./LICENSE))
* 3D models & creative assets (`/models/`): `CC BY 4.0 License` ([/models/LICENSE](./models/LICENSE))
* Source code: `MIT License` ([LICENSE](https://github.com/kellervater/homeracker/blob/main/LICENSE))
* 3D models & creative assets (`/models/`): `CC BY 4.0 License` ([/models/LICENSE](https://github.com/kellervater/homeracker/blob/main/models/LICENSE))
These licenses apply to the `HomeRacker - Core` system and customizable rackmount ears.
> [!IMPORTANT]
> ❗ **Important**
> Other models I publish may have more restrictive licenses. This will be stated clearly on Makerworld.
HomeRacker is an unregistered trademark of Patrick Pötz (kellervater), first used publicly on 12.04.2025.
@@ -229,7 +227,7 @@ Look at all the prototypes:
## ⚠️ Disclaimer
> [!WARNING]
> ⚠️ **Warning**
> This project is provided “as is,” without any warranty. Use at your own risk. Im not responsible for damage, injury, or loss caused by using this system or its parts.
@@ -256,14 +254,22 @@ E.g.: you could print a connector in ABS, a support in PLA Matte and a Lock Pin
> 🛠️ **Btw:** I am not affiliated with Bambu in any way besides uploading my models to MakerWorld and occasionally making use of their Exclusive Model program. But they don't pay me for naming their products anywhere else (I wish 😉).
# 📋 Todos
* [ ] Rename Building blocks in f3d (did bad translations from german to english there)
* [ ] Release models on MakerLab
* [ ] HomeRacker - Core (under above's license, non-exclusive)
* [ ] HomeRacker - 10" Rackmount Kit (exclusive)
* [ ] HomeRacker - Pi5 Mount Kit (exclusive)
* [x] Rename Building blocks in f3d (did bad translations from german to english there)
* [x] Release models on MakerLab
* [x] HomeRacker - Core (under above's license, non-exclusive)
* [x] HomeRacker - 10" Rackmount Kit (exclusive)
* [x] HomeRacker - Pi5 Mount Kit (exclusive)
* [x] Customizable Rackmount Ears
* [ ] HomeRacker - Airflow Kit (exclusive)
* [ ] HomeRacker - Shelf
* [ ] Can we even call it a standard yet?
* [x] HomeRacker - Airflow Kit (exclusive) -> under 10" Rack
* [x] HomeRacker - Shelf
* [ ] Contributing.md stub?
* [ ] Quickstart Guide
* [ ] Quickstart Guide
* [ ] Parts Catalog
* [ ] Link (maybe via table) example models
# Logo
I asked ChatGPT to create a logo, and I think it turned out great — so well stick with it for now. This logo will also be used as an overlay image for the thumbnails of all my 3D models that are compatible with HomeRacker.
I encourage you to do the same if you create models for HomeRacker. That way, itll be immediately visible to users that a model is designed to be mounted on a HomeRacker system.
![HomeRacker Logo](./img/homeracker_logo.png)

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
img/homeracker_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

BIN
img/homeracker_logo.xcf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

View File

@@ -0,0 +1,199 @@
// import BOSL2
include <BOSL2/std.scad>
/* [Hidden] */
// constants which shouldn't be changed
$fn=100;
// This is the HomeRacker base unit. don't change this!
BASE_UNIT=15; // mm
// Standard tolerance for the mount. This is a sane default.
TOLERANCE=0.2; // mm
// Base strength. This is a sane default.
BASE_STRENGTH=2; // mm
// Chamfer size. This is a sane default.
CHAMFER=1; // mm
// Lock Pin side length
LOCK_PIN_SIDE=4; // mm
// lock pin edge distance
LOCK_PIN_EDGE_DISTANCE=5.5; // mm
/* [Device Measurements] */
// Width of the device in mm. Will determine how far apart the actual mounts are in width.
device_width=100; // [20:0.1:500]
// TODO test for zero cube
// Depth of the device in mm. Will determine how far apart the actual mounts are in depth.
device_depth=99; // [54:0.1:400]
// Height of the device in mm. Will determine how far apart the actual mounts are in height.
device_height=25.5; // [10:0.1:400]
/* [Bracket] */
// Defines the bracket thickness on top of the device.
bracket_strength_top=7.5; // [1:0.1:50]
// Defines how much the bracket will overlap to the sides of the device.
bracket_strength_sides=7.5; // [2:0.1:50]
// diff_width resembles the gap between the device and the mount. This gap will be filled with a cuboid
modulo_width=(BASE_UNIT - ( device_width + TOLERANCE ) % BASE_UNIT);
WIDTH_DIFF = modulo_width==15 ? 0 : modulo_width;
echo("Diff Width: ", WIDTH_DIFF);
mount_gap_filler_start=(device_width+TOLERANCE)/2;
// echo("mount_gap_filler_start: ", mount_gap_filler_start);
// echo("effective mount distance: ", device_width+WIDTH_DIFF+TOLERANCE);
DEPTH_DIFF=device_depth % BASE_UNIT;
echo("Diff Depth: ", DEPTH_DIFF);
mount_offset_depth=(device_depth-DEPTH_DIFF-BASE_UNIT)/2;
echo("mount_offset_depth*2: ", mount_offset_depth*2);
/* Code */
// creates a bracket around the device with TOLERANCE as additional space and BASE_STRENGTH as the strength of the bracket
module bracket(width,depth,height) {
// Bracket
outer_width=width+BASE_STRENGTH*2+TOLERANCE;
outer_depth=depth+BASE_STRENGTH*2+TOLERANCE;
outer_height=height+BASE_STRENGTH;
inner_width=width-bracket_strength_top*2+TOLERANCE;
inner_depth=depth-bracket_strength_top*2+TOLERANCE;
bottom_recess_height=height-bracket_strength_sides;
intersection(){
difference() {
// Outer
cuboid(
size=[outer_width,outer_depth,outer_height],
anchor=BOTTOM,
chamfer=CHAMFER, edges=[TOP,FRONT,BACK,LEFT,RIGHT], except=[BOTTOM]
);
// Top Skeleton (cut the middle to leave only a bracket to the sides)
cuboid(
size=[inner_width,inner_depth,outer_height],
anchor=BOTTOM,
chamfer=-CHAMFER, edges=[TOP]
);
// Bottom Recess (cut the cube to leave only a top bracket)
cuboid(
size=[outer_width,outer_depth,bottom_recess_height],
anchor=BOTTOM
);
// Subtract Device to from bracket (cut the device into the bracket)
cuboid(
size=[width+TOLERANCE,depth+TOLERANCE,height+TOLERANCE/2],
anchor=BOTTOM
);
}
// chamfer intersection for the outer bottom chamfer of the bracket
translate([0,0,outer_height])
cuboid(
size=[outer_width,outer_depth,bracket_strength_sides+BASE_STRENGTH-TOLERANCE/2],
anchor=TOP,
chamfer=CHAMFER, edges=[BOTTOM]
);
}
}
// Mount
module mount(){
depth=BASE_UNIT+BASE_STRENGTH*2+TOLERANCE;
gap_filler_width=(WIDTH_DIFF)/2;
// depth translation
translate([0,mount_offset_depth,0])
union(){
translate([mount_gap_filler_start,0,0])
// Mount
union(){
top_width=BASE_STRENGTH;
bottom_width=BASE_UNIT+gap_filler_width;
// Bridge
difference(){
cuboid_width = bottom_width;
cuboid_chamfer = bottom_width;
prismoid(
size1 = [bottom_width, depth], // Bottom face: width 30, depth 60
size2 = [top_width, depth], // Top face: becomes a line segment (width 0)
shift = [(-bottom_width+top_width)/2, 0], // Shift top edge center to X=+15 (aligns with right edge of base)
chamfer=CHAMFER,
h = device_height, // Height
//chamfer=CHAMFER/2,
anchor = BOTTOM+LEFT // Anchor bottom left
);
translate([0,0,BASE_STRENGTH])
cuboid(
size=[cuboid_width,BASE_UNIT,device_height],
anchor=BOTTOM+LEFT,
chamfer=BASE_UNIT/2, edges=[BOTTOM], except=[RIGHT]
);
}
// Depth enforcement
depth_enforcement_x = BASE_STRENGTH;
depth_enforcemnt_y2 = BASE_UNIT*2;
prismoid(
size1 = [depth_enforcement_x, depth],
size2 = [depth_enforcement_x, depth_enforcemnt_y2],
shift = [0, (depth-depth_enforcemnt_y2)/2],
h = device_height,
chamfer=[CHAMFER,0,0,CHAMFER],
anchor = BOTTOM+LEFT
);
// Mount Rail with holes
difference(){
// Mount Rail
difference(){
height=BASE_UNIT+TOLERANCE/2;
// outer
cuboid(
size=[bottom_width,depth,height],
anchor=TOP+LEFT,
chamfer=CHAMFER, edges=[BOTTOM,RIGHT], except=[TOP]
);
// support recess
cuboid(
size=[bottom_width,BASE_UNIT+TOLERANCE,height],
anchor=TOP+LEFT
);
}
// holes
translate([bottom_width-LOCK_PIN_EDGE_DISTANCE,0,-LOCK_PIN_EDGE_DISTANCE-TOLERANCE/2])
cuboid(
size=[LOCK_PIN_SIDE,depth,LOCK_PIN_SIDE],
anchor=TOP+RIGHT
);
}
}
}
}
module mirror_mount_x_plane(){
mount();
x_mirror_plane = [1,0,0];
mirror(x_mirror_plane) {
mount();
}
}
// Assembly
rotate([180,0,0])
union() {
bracket(device_width,device_depth,device_height);
// Mount Right
mirror_mount_x_plane();
y_mirror_plane = [0,1,0];
mirror(y_mirror_plane) {
mirror_mount_x_plane();
}
}

View File

@@ -1,13 +1,28 @@
// import BOSL2
include <BOSL2/std.scad>
/* [Hidden] */
$fn=100;
CAGE_BOLT_DIAMETER=6.5;
RACK_BORE_DISTANCE_VERTICAL=15.875;
RACK_BORE_DISTANCE_TOP_BOTTOM=6.35;
RACK_MOUNT_SURFACE_WIDTH=15.875;
RACK_BORE_DISTANCE_HORIZONTAL=RACK_MOUNT_SURFACE_WIDTH/2;
RACK_HEIGHT_UNIT=44.5; // mm
RACK_WIDTH_10_INCH_INNER=222.25; // mm
RACK_WIDTH_10_INCH_OUTER=254; // mm
RACK_WIDTH_19_INCH=482.6; // mm
/* [Base] */
// automatically chooses the tightest fit for the rackmount ears based on the device width. If true, rack_size will be ignored.
autosize=true;
// rack size in inches. If autosize is true, this value will be ignored. Only 10 and 19 inch racks are supported.
rack_size=10; // [10:10 inch,19:19 inch]
// Asymetry Slider. CAUTION: there's no sanity check for this slider!
asymetry=0; // [-150:0.1:150]
// shows the distance between the rackmount ears considering the device width.
show_distance=false;
// Width of the device in mm. Will determine the width of the rackmount ears depending on rack_size.
device_width=201;
@@ -38,44 +53,18 @@ device_bore_rows=2;
// If true, the device will be aligned to the center of the rackmount ear. Otherwise it will be aligned to the bottom of the rackmount ear.
center_device_bore_alignment=false;
/* [CONSTANTS (shouldn't need to be changed)] */
CAGE_BOLT_DIAMETER=6.5;
/* [Derived] */
CHAMFER=min(strength/3,0.5);
RACK_BORE_DISTANCE_VERTICAL=15.875;
RACK_BORE_DISTANCE_TOP_BOTTOM=6.35;
RACK_MOUNT_SURFACE_WIDTH=15.875;
RACK_BORE_DISTANCE_HORIZONTAL=RACK_MOUNT_SURFACE_WIDTH/2;
RACK_BORE_WIDTH=RACK_MOUNT_SURFACE_WIDTH-2*max(strength,2);
RACK_HEIGHT_UNIT=44.5; // mm
RACK_HEIGHT_UNIT_COUNT=max(1,ceil(device_height/RACK_HEIGHT_UNIT));
RACK_HEIGHT=RACK_HEIGHT_UNIT_COUNT*RACK_HEIGHT_UNIT; // actual height calculated by height unit size x number of units
RACK_BORE_COUNT=RACK_HEIGHT_UNIT_COUNT*3; // 3 holes for each units
RACK_WIDTH_10_INCH_INNER=222.25; // mm
RACK_WIDTH_10_INCH_OUTER=254; // mm
RACK_WIDTH_19_INCH=482.6; // mm
// Base assertions
module validate_params() {
valid_rack_sizes=[10,19];
if(autosize == false){
assert(rack_size == 10 || rack_size == 19, "Invalid rack_size. Only 10 and 19 inch racks are supported. Choose a valid one or set autosize to true.");
}
}
validate_params();
// Debug
echo("Height: ", RACK_HEIGHT);
echo("Rack Bore Count: ", RACK_BORE_COUNT);
// Calculate the width of the ear
function get_ear_width(device_width) =
device_width > RACK_WIDTH_10_INCH_INNER || autosize == false && rack_size == 19 ?
(RACK_WIDTH_19_INCH - device_width) / 2 :
(RACK_WIDTH_10_INCH_OUTER - device_width) / 2
;
rack_ear_width = get_ear_width(device_width);
function get_bore_depth(device_bore_margin_horizontal,device_bore_columns) =
(device_bore_columns - 1) * device_bore_margin_horizontal
@@ -92,7 +81,7 @@ device_screw_alignment = [strength,depth/2,device_screw_alignment_vertical];
module base_ear(width,strength,height) {
union() {
// Front face
cuboid([rack_ear_width,strength,height],anchor=LEFT+BOTTOM+FRONT,chamfer=CHAMFER);
cuboid([width,strength,height],anchor=LEFT+BOTTOM+FRONT,chamfer=CHAMFER);
// Side face
cuboid([strength,depth,height],anchor=LEFT+BOTTOM+FRONT,chamfer=CHAMFER);
}
@@ -110,15 +99,36 @@ module screws_countersunk(length, diameter_head, length_head, diameter_shaft) {
// Assemble the rackmount ear
difference() {
module rackmount_ear(asym=0){
ear_width_19_inch=(RACK_WIDTH_19_INCH - device_width) / 2 + asym;
ear_width_10_inch=(RACK_WIDTH_10_INCH_OUTER - device_width) / 2 + asym;
// Calculate the width of the ear
rack_ear_width = device_width > RACK_WIDTH_10_INCH_INNER || autosize == false && rack_size == 19 ?
ear_width_19_inch:
ear_width_10_inch
;
difference() {
// Create the base of the ear
base_ear(device_width,strength,RACK_HEIGHT);
// Create the holes for the device screws
screws_countersunk(length=strength,diameter_head=device_bore_hole_head_diameter,length_head=device_bore_hole_head_length,diameter_shaft=device_bore_hole_diameter);
difference() {
// Create the base of the ear
base_ear(rack_ear_width,strength,RACK_HEIGHT);
// Create the holes for the device screws
screws_countersunk(length=strength,diameter_head=device_bore_hole_head_diameter,length_head=device_bore_hole_head_length,diameter_shaft=device_bore_hole_diameter);
}
// Create the holes for the rackmount screws
zcopies(spacing=RACK_HEIGHT_UNIT,n=RACK_HEIGHT_UNIT_COUNT,sp=[0,0,0])
zcopies(spacing=RACK_BORE_DISTANCE_VERTICAL,n=3,sp=[rack_ear_width-RACK_BORE_DISTANCE_HORIZONTAL,0,RACK_BORE_DISTANCE_TOP_BOTTOM])
cuboid([RACK_BORE_WIDTH,strength+1,CAGE_BOLT_DIAMETER], rounding=CAGE_BOLT_DIAMETER/2, edges=[TOP+LEFT,TOP+RIGHT,BOTTOM+LEFT,BOTTOM+RIGHT], anchor=FRONT);
}
// Create the holes for the rackmount screws
zcopies(spacing=RACK_HEIGHT_UNIT,n=RACK_HEIGHT_UNIT_COUNT,sp=[0,0,0])
zcopies(spacing=RACK_BORE_DISTANCE_VERTICAL,n=3,sp=[rack_ear_width-RACK_BORE_DISTANCE_HORIZONTAL,0,RACK_BORE_DISTANCE_TOP_BOTTOM])
cuboid([RACK_BORE_WIDTH,strength+1,CAGE_BOLT_DIAMETER], rounding=CAGE_BOLT_DIAMETER/2, edges=[TOP+LEFT,TOP+RIGHT,BOTTOM+LEFT,BOTTOM+RIGHT], anchor=FRONT);
}
// Ear distance
ear_distance = show_distance ? -device_width : -CAGE_BOLT_DIAMETER;
// Place the ears
rackmount_ear(asymetry);
x_mirror_plane = [1,0,0];
translate([ear_distance,0,0])
mirror(x_mirror_plane){
rackmount_ear(-asymetry);
}

View File

@@ -0,0 +1,130 @@
// import BOSL2
include <BOSL2/std.scad>
/* [Hidden] */
// constants which shouldn't be changed
$fn=100;
// This is the HomeRacker base unit. don't change this!
BASE_UNIT=15; // mm
// Standard tolerance for the mount. This is a sane default.
TOLERANCE=0.2; // mm
// Base strength. This is a sane default.
BASE_STRENGTH=2; // mm
// Chamfer size. This is a sane default.
CHAMFER=1; // mm
// Lock Pin side length
LOCK_PIN_SIDE=4; // mm
// lock pin edge distance
LOCK_PIN_EDGE_DISTANCE=5.5; // mm
// lock pin chamfer
LOCK_PIN_CHAMFER=0.8; // mm
// Negative chamfer fix:
NEGATIVE_CHAMFER_TRANSLATE=0.01;
mount_units=2;
/* [Base] */
// TODO: enable mount units
// How many units shall the mount span across the support
//mount_units=2; //[2:1:10]
// Shall bores be countersunk or flathead
bore_type="flathead"; //[flathead,countersunk]
// Bore Shaft Diameter in mm
bore_shaft_diameter=4;
// Bore Head Diameter in mm (only relevant if bore_type=countersunk)
bore_head_diameter=8.5;
/* [Finetuning] */
// Defines the angle of the head rejuvenation. 90° is ISO standard. (only relevant if bore_type=countersunk)
countersunk_angle=90;
// Tolerance (in mm) for the bore holes. This is a sane default.
bore_tolerance=0.2;
// Calculate the head depth based on the countersunk angle.
head_depth = bore_head_diameter/2 - bore_shaft_diameter/2 * tan( countersunk_angle/2 );
echo("head_depth: ", head_depth);
connector_width_net=BASE_UNIT+TOLERANCE;
connector_width_gross=connector_width_net+BASE_STRENGTH*2;
//Wall Mount
mount_thickness=head_depth+BASE_STRENGTH;
mount_height=mount_units*BASE_UNIT;
mount_width=mount_units*2*BASE_UNIT+connector_width_gross;
// Support Interface
module mount(){
difference() {
union(){
cuboid([mount_width,mount_height,mount_thickness],anchor=CENTER+BOTTOM,chamfer=CHAMFER);
cuboid([connector_width_gross,mount_height,connector_width_gross],anchor=CENTER+BOTTOM,chamfer=CHAMFER);
}
translate([0,0,BASE_STRENGTH])
cuboid([connector_width_net,mount_height,connector_width_net],anchor=CENTER+BOTTOM);
}
}
// Lock pin Holes
module lock_pin_hole(){
// fix for negative chamfer. Otherwise the holes would not reach the surface
translate([0,0,-NEGATIVE_CHAMFER_TRANSLATE/2])
cuboid(
size=[LOCK_PIN_SIDE,LOCK_PIN_SIDE,connector_width_gross+0.01],
anchor=CENTER+BOTTOM,
chamfer=-LOCK_PIN_CHAMFER,edges=[TOP,BOTTOM]
);
}
module lock_pin_holes(){
// ycopies
ycopies(BASE_UNIT,mount_units)
// Create cross
union(){
lock_pin_hole();
yrot(90,cp=[0,0,connector_width_gross/2])
lock_pin_hole();
}
}
//// Bores
// Shafts
module bore(){
bore_shaft_radius=(bore_shaft_diameter+bore_tolerance)/2;
bore_head_radius=(bore_head_diameter+bore_tolerance)/2;
if (bore_type=="flathead") {
cylinder(r=bore_shaft_radius,h=mount_thickness,anchor=CENTER+BOTTOM);
} else if (bore_type=="countersunk") {
// Countersunk
union() {
// Head
translate([0,0,mount_thickness])
cylinder(r1=bore_shaft_radius,r2=bore_head_radius,h=head_depth,anchor=CENTER+TOP);
// Shaft
cylinder(r=bore_shaft_radius,h=mount_thickness,anchor=CENTER+BOTTOM);
}
}
}
bore_position_x=BASE_STRENGTH+BASE_UNIT/2+TOLERANCE/2+BASE_UNIT;
echo("bore_position_x: ", bore_position_x);
// Assembly
translate([0,0,BASE_UNIT]) rotate([90,0,0])
union(){
difference() {
mount();
translate([-bore_position_x,0,0])
bore();
translate([bore_position_x,0,0])
bore();
// Lock Pin Holes
lock_pin_holes();
}
}

2
scripts/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.env
.vscode

View File

@@ -0,0 +1,12 @@
{
"autodeskProduct": "Fusion",
"type": "script",
"author": "kellervater (Patrick Pötz)",
"description": {
"": "Exports all common dimensions of the shelf."
},
"version": "",
"supportedOS": "windows|mac",
"editEnabled": true,
"iconFilename": "ScriptIcon.svg"
}

View File

@@ -0,0 +1,176 @@
#Fusion360API Python Script
#Description: Exports STEP files. Exports "Support" once per depth,
# and "Shelf" for each width/depth combination.
import adsk.core, adsk.fusion, adsk.cam, traceback, os
def run(context):
ui = None
try:
# --- Basic Fusion 360 setup ---
app = adsk.core.Application.get()
ui = app.userInterface
design = app.activeProduct
if not design:
ui.messageBox('No active Fusion 360 design', 'Export Script Error')
return
# --- Get Document Version ---
doc = app.activeDocument
version_str = "v_unknown" # Default in case of error
try:
if doc.dataFile:
version_number = doc.dataFile.versionNumber
version_str = f"v{version_number}"
else:
version_str = "v_unsaved"
ui.messageBox('The current document has not been saved to the cloud yet.\n'
'Filenames will use "v_unsaved" instead of a version number.',
'Unsaved Document Warning')
except Exception as e:
ui.messageBox(f'Could not retrieve document version:\n{e}\n'
f'Filenames will use "{version_str}".', 'Version Error')
# --- End Get Document Version ---
# --- Configuration
widthParamName = "shelf_width_units"
depthParamName = "shelf_depth_units"
minWidth = 4
maxWidth = 15
minDepth = 7
maxDepth = 15
baseFileName = "homeracker_shelf"
shelfBodyName = "Shelf"
supportBodyName = "Support"
# --- End Configuration ---
# Get the root component
rootComp = design.rootComponent
# Get User Parameters collection
userParams = design.userParameters
if not userParams:
ui.messageBox('Could not access user parameters.', 'Export Script Error')
return
# Find the specific parameters
widthParam = userParams.itemByName(widthParamName)
depthParam = userParams.itemByName(depthParamName)
if not widthParam or not depthParam:
ui.messageBox(f'Could not find parameters: "{widthParamName}" or "{depthParamName}". Check names.', 'Param Error')
return
# --- Find the bodies to control visibility (once before loops) ---
shelfBody = None
supportBody = None
try:
shelfBody = rootComp.bRepBodies.itemByName(shelfBodyName)
supportBody = rootComp.bRepBodies.itemByName(supportBodyName)
if not shelfBody:
ui.messageBox(f'Body "{shelfBodyName}" not found. Aborting...', 'Body Not Found Warning')
return
if not supportBody:
ui.messageBox(f'Body "{supportBodyName}" not found. Aborting...', 'Body Not Found Warning')
return
except Exception as e:
ui.messageBox(f'Error finding bodies:\n{traceback.format_exc()}', 'Error Finding Bodies')
return
# --- Get Export Directory from User ---
folderDialog = ui.createFolderDialog()
folderDialog.title = "Select Folder to Save STEP Files"
dialogResult = folderDialog.showDialog()
if dialogResult == adsk.core.DialogResults.DialogOK:
exportFolder = folderDialog.folder
else:
ui.messageBox('Export cancelled by user.')
return
# --- Get Export Manager ---
exportMgr = design.exportManager
# --- Progress Bar Setup ---
num_depths = (maxDepth - minDepth + 1)
num_widths = (maxWidth - minWidth + 1)
# Total exports = one support per depth + one shelf per width*depth
total_exports = num_depths + (num_depths * num_widths)
progressDialog = ui.createProgressDialog()
progressDialog.isCancelEnabled = False
progressDialog.show(f'Exporting {baseFileName}', 'Initializing...', 0, total_exports)
exported_count = 0
# --- Loop through parameter combinations ---
for d in range(minDepth, maxDepth + 1):
try:
# --- Set Depth Parameter ---
depthParam.value = float(d)
adsk.doEvents() # Allow update
progressDialog.progressValue = exported_count
progressDialog.message = f'Exporting Support: D={d} ({exported_count + 1}/{total_exports})'
adsk.doEvents()
# Set visibility: Support ONLY
supportBody.isLightBulbOn = True
shelfBody.isLightBulbOn = False # Hide shelf if it exists
adsk.doEvents() # Allow visibility change
# Construct filename & path for Support
supportFileName = f"{baseFileName}_Support_D{d}_{version_str}"
supportFullPath = os.path.join(exportFolder, supportFileName + ".step")
# Export Support
supportStepOptions = exportMgr.createSTEPExportOptions(supportFullPath, rootComp)
exportMgr.execute(supportStepOptions)
exported_count += 1
# --- End Support Export ---
except Exception:
progressDialog.hide() # Hide progress on error
ui.messageBox(f'Failed during processing or Support export for Depth={d}:\n{traceback.format_exc()}', 'Outer Loop Error')
return
# --- INNER LOOP: Width (for Shelf Export) ---
for w in range(minWidth, maxWidth + 1):
try:
progressDialog.progressValue = exported_count
progressDialog.message = f'Exporting Shelf: W={w}, D={d} ({exported_count + 1}/{total_exports})'
adsk.doEvents()
# --- Set Width Parameter ---
widthParam.value = float(w)
adsk.doEvents() # Allow update
# --- Set Visibility for Shelf Export: Shelf ONLY ---
shelfBody.isLightBulbOn = True
supportBody.isLightBulbOn = False # Hide support if it exists
adsk.doEvents() # Allow visibility change
# --- Construct Filename & Path for Shelf ---
shelfFileName = f"{baseFileName}_Shelf_W{w}_D{d}_{version_str}"
shelfFullPath = os.path.join(exportFolder, shelfFileName + ".step")
# --- Export Shelf ---
shelfStepOptions = exportMgr.createSTEPExportOptions(shelfFullPath, rootComp)
exportMgr.execute(shelfStepOptions)
exported_count += 1
except Exception as e_inner:
# Log error for this specific shelf, but continue loop
ui.messageBox(f'Failed exporting Shelf W={w}, D={d}:\n{traceback.format_exc()}\nSkipping this combination.', 'Inner Loop Error')
# Optionally add logging here instead of message box spam
continue # Continue to next width value
# --- Clean up ---
progressDialog.hide()
if exported_count > 0:
ui.messageBox(f'Successfully exported {exported_count} STEP files to:\n{exportFolder}', 'Export Complete')
else:
ui.messageBox('No files were exported. Check script configuration or logs for errors.', 'Export Result')
except Exception as e:
if ui:
ui.messageBox('Script Failed Unexpectedly:\n{}'.format(traceback.format_exc()))
if 'progressDialog' in locals() and progressDialog.isShowing:
progressDialog.hide()

View File

@@ -0,0 +1,6 @@
This script is intended to be used with the Fusion360 file `HomeRacker - Shelf`.
It exports all sane dimensions of the shelf to avoid tedious manual work.
It is ready to go and just needs to be run from within Fusion360 (`Shift + S`).
> [!NOTE]
> Since I bootstrapped the script from within Fusion, idk how to import it. Maybe you need to create a new one and just import the contents. Just try it and let me know how it works.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 85 KiB