Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
099b754156 | ||
|
|
021cf7b78d | ||
|
|
d9eac222e4 | ||
|
|
4c7c8f2dc3 | ||
|
|
27213d9d50 | ||
|
|
a1c8d11b6a | ||
|
|
b5508af9d3 | ||
|
|
beb5f39722 | ||
|
|
a1a6777aea | ||
|
|
ebeb258d60 | ||
|
|
98e19efa60 | ||
|
|
44ed1746f9 | ||
|
|
a5bad40082 | ||
|
|
bbb742f271 | ||
|
|
9dd9add861 | ||
|
|
bc80d1e65f | ||
|
|
34cd659177 | ||
|
|
3a7f1ebb1b | ||
|
|
631ef6e837 | ||
|
|
91a43c8259 | ||
|
|
e91dffa6cc | ||
|
|
1ad8a68308 | ||
|
|
9ed1685b80 | ||
|
|
3e935d4f3d |
161
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# HomeRacker Copilot Instructions
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
HomeRacker is a fully modular 3D-printable rack-building system designed for versatile "racking needs" including server racks, shoe racks, bookshelves, and more. The project consists of:
|
||||||
|
|
||||||
|
- **HomeRacker Core**: Open-spec modular building system (MIT License)
|
||||||
|
- **3D Models**: Parametric and non-parametric models in OpenSCAD and Fusion 360 formats (CC BY 4.0)
|
||||||
|
- **Automation Scripts**: Fusion 360 export scripts for batch model generation
|
||||||
|
- **GitHub Pages Site**: Documentation and showcase website
|
||||||
|
|
||||||
|
## Core Technologies & Frameworks
|
||||||
|
- **3D Modeling**: OpenSCAD (parametric), Fusion 360 (.f3d files)
|
||||||
|
- **Languages**: SCAD (OpenSCAD), Python (Fusion 360 scripts)
|
||||||
|
- **Dependencies**: BOSL2 library for OpenSCAD
|
||||||
|
- **Web**: GitHub Pages with Jekyll (Architect theme)
|
||||||
|
- **Standards**: 10" and 19" rack specifications, custom HomeRacker dimensions
|
||||||
|
|
||||||
|
## Key Components Architecture
|
||||||
|
### Building Blocks
|
||||||
|
1. **Supports**: Vertical structural elements with standardized connection points
|
||||||
|
2. **Connectors**: 6-way connection system for supports (front/back, left/right, up/down)
|
||||||
|
3. **Lock Pins**: Securing mechanism for assembled connections
|
||||||
|
4. **Rebar**: Horizontal structural reinforcement elements
|
||||||
|
5. **Splints**: Additional reinforcement components
|
||||||
|
|
||||||
|
### Models Structure
|
||||||
|
- `/models/rackmount_ears/`: Fully customizable rackmount ears (OpenSCAD)
|
||||||
|
- `/models/gridfinity/`: Gridfinity-compatible base plates
|
||||||
|
- `/models/flexmount/`: Flexible mounting solutions
|
||||||
|
- `/models/shelf/`: Parametric shelf components
|
||||||
|
- `/models/wallmount/`: Wall mounting brackets
|
||||||
|
|
||||||
|
## Development Guidelines
|
||||||
|
|
||||||
|
### OpenSCAD Best Practices
|
||||||
|
- Use BOSL2 library for advanced geometric operations
|
||||||
|
- Set `$fn=100` for smooth curves in production models
|
||||||
|
- Implement parametric designs with customizer-friendly variables
|
||||||
|
- Group parameters logically with `/* [Section Name] */` comments
|
||||||
|
- Provide sensible defaults and value ranges for sliders
|
||||||
|
- Include sanity checks for critical parameters
|
||||||
|
- Use descriptive variable names following `CONSTANT_CASE` for constants
|
||||||
|
|
||||||
|
### File Organization
|
||||||
|
- Keep `.scad` files in appropriate `/models/` subdirectories
|
||||||
|
- Include example images showing customization options
|
||||||
|
- Maintain `README.md` files explaining each model's purpose
|
||||||
|
|
||||||
|
### Documentation Standards
|
||||||
|
- Use emoji headers for visual organization (🔧, ✨, 📐, etc.)
|
||||||
|
- Include clear assembly instructions with diagrams
|
||||||
|
- Provide printing tips and material recommendations
|
||||||
|
- Document all customizable parameters
|
||||||
|
- Include real-world use case examples
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- Comment complex geometric calculations
|
||||||
|
- Use meaningful module names
|
||||||
|
- Separate configuration from implementation
|
||||||
|
- Test parameter ranges for edge cases
|
||||||
|
- Validate dimensional accuracy against rack standards
|
||||||
|
|
||||||
|
## Project-Specific Context
|
||||||
|
|
||||||
|
### HomeRacker Core Dimensional Standards
|
||||||
|
- **Base Unit**: 15mm (core measurement for all HomeRacker components)
|
||||||
|
- **Lock Pin Side**: 4mm (square profile for pins and matching holes)
|
||||||
|
- **Wall Thickness**: 2mm (standard connector wall thickness)
|
||||||
|
- **Standard Tolerance**: 0.2mm (added to connector interiors for print variances)
|
||||||
|
- **Support Dimensions**: 15mm x 15mm base, height = multiples of 15mm
|
||||||
|
- **Connector Centers**: Always 1 base unit (15mm) in height
|
||||||
|
- **Lock Pin Edge Distance**: 5.5mm from support edges
|
||||||
|
- **Standard Chamfer**: 1mm for printability
|
||||||
|
|
||||||
|
### Licensing Requirements
|
||||||
|
- Source code: MIT License (commercial use allowed)
|
||||||
|
- 3D models & assets: CC BY 4.0 (attribution required, share-alike)
|
||||||
|
- Always include proper license headers in new files
|
||||||
|
- Credit original work when modifying existing models
|
||||||
|
|
||||||
|
### Brand Guidelines
|
||||||
|
- Use "HomeRacker" (not "Home Racker" or "home-racker")
|
||||||
|
- Include HomeRacker logo overlay on compatible model thumbnails
|
||||||
|
- Maintain consistent visual style across documentation
|
||||||
|
- Reference Makerworld links for published models
|
||||||
|
|
||||||
|
## Common Tasks & Patterns
|
||||||
|
|
||||||
|
### Creating New Models
|
||||||
|
1. Start with parametric design in OpenSCAD
|
||||||
|
2. Use BOSL2 library for complex operations
|
||||||
|
3. Include customizer parameters with proper grouping
|
||||||
|
4. Test with common device dimensions
|
||||||
|
5. Export STL files for immediate use
|
||||||
|
6. Document parameters and use cases
|
||||||
|
7. Add example images showing variations
|
||||||
|
|
||||||
|
### Fusion 360 Scripts
|
||||||
|
- Place scripts in `/scripts/` with proper manifest files
|
||||||
|
- Include usage instructions in README.md
|
||||||
|
- Test export functionality across parameter ranges
|
||||||
|
- Validate output file naming conventions
|
||||||
|
|
||||||
|
### Documentation Updates
|
||||||
|
- Update main README.md for new features
|
||||||
|
- Include visual examples with actual photos
|
||||||
|
- Link to Makerworld models where applicable
|
||||||
|
- Maintain table of contents structure
|
||||||
|
|
||||||
|
## Self-Improvement Instructions
|
||||||
|
|
||||||
|
### When Encountering Wrong Turns
|
||||||
|
If you find yourself making incorrect assumptions or heading down the wrong path during development assistance:
|
||||||
|
|
||||||
|
1. **Pause and Reassess**: Stop the current approach and explicitly state what went wrong
|
||||||
|
2. **Gather Context**: Re-read project documentation, examine existing code patterns, and understand the specific use case better
|
||||||
|
3. **Ask Clarifying Questions**: Request specific details about requirements, constraints, or expected outcomes
|
||||||
|
4. **Document the Learning**: Add insights to these instructions for future reference
|
||||||
|
|
||||||
|
### Instruction Enhancement Protocol
|
||||||
|
When updating these instructions based on new learnings:
|
||||||
|
|
||||||
|
1. **Preserve Core Structure**: Maintain the organized sections and GitHub best practices format
|
||||||
|
2. **Add Specific Examples**: Include concrete code snippets or configuration examples that caused confusion
|
||||||
|
3. **Update Technology Stack**: Keep dependencies, tools, and version requirements current
|
||||||
|
4. **Expand Edge Cases**: Document unusual scenarios or parameter combinations that need special handling
|
||||||
|
5. **Validate Changes**: Ensure new instructions don't conflict with existing project patterns
|
||||||
|
|
||||||
|
### Learning from Mistakes
|
||||||
|
Common areas for improvement:
|
||||||
|
- **Dimensional Accuracy**: Double-check rack standards and measurement conversions
|
||||||
|
- **Parameter Validation**: Ensure SCAD parameters have proper bounds and error checking
|
||||||
|
- **File Naming**: Follow established conventions for exports and derivatives
|
||||||
|
- **License Compliance**: Verify all new content uses appropriate licensing
|
||||||
|
- **Cross-Platform Compatibility**: Consider Windows/Linux/macOS differences in file paths and tools
|
||||||
|
|
||||||
|
### Feedback Integration
|
||||||
|
When receiving feedback about incorrect suggestions:
|
||||||
|
1. Acknowledge the specific error made
|
||||||
|
2. Explain the corrected approach
|
||||||
|
3. Update relevant sections of these instructions
|
||||||
|
4. Test the corrected approach before suggesting it again
|
||||||
|
|
||||||
|
## Troubleshooting Common Issues
|
||||||
|
|
||||||
|
### OpenSCAD Problems
|
||||||
|
- **Rendering Issues**: Check for overlapping geometry, invalid meshes, or extreme parameter values
|
||||||
|
- **BOSL2 Errors**: Verify library installation and include statements
|
||||||
|
- **Performance**: Reduce `$fn` for development, increase for final exports
|
||||||
|
|
||||||
|
### Export Script Issues
|
||||||
|
- **Fusion 360 API**: Check for API version compatibility and parameter access methods
|
||||||
|
- **File Paths**: Use cross-platform path handling in Python scripts
|
||||||
|
- **Batch Operations**: Implement proper error handling for automated exports
|
||||||
|
|
||||||
|
### Documentation Sync
|
||||||
|
- **Broken Links**: Validate all external references to Makerworld and GitHub
|
||||||
|
- **Image References**: Ensure all images exist and use correct relative paths
|
||||||
|
- **Version Mismatches**: Keep model versions synchronized between repository and published versions
|
||||||
|
|
||||||
|
Remember: HomeRacker is about modularity, openness, and practical maker solutions. Always consider how suggestions support these core principles while maintaining professional engineering standards.
|
||||||
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
*.3mf
|
*.3mf
|
||||||
*.stl
|
*.stl
|
||||||
|
.vscode
|
||||||
|
bin/OpenSCAD-*/
|
||||||
|
|||||||
11
.lint/pylint/.pylintrc
Normal 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
|
||||||
@@ -14,4 +14,12 @@ repos:
|
|||||||
- id: renovate-config-validator
|
- id: renovate-config-validator
|
||||||
args: ["--strict"]
|
args: ["--strict"]
|
||||||
language_version: 20.18.0 # workaround till https://github.com/renovatebot/pre-commit-hooks/issues/2460 is fixed
|
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"
|
||||||
|
]
|
||||||
...
|
...
|
||||||
|
|||||||
276
README.md
@@ -1,13 +1,275 @@
|
|||||||
# 🏗️ HomeRacker - The universal modular rack building system
|
[📚 What's it for?](#-use-cases) | [⚙️ How does it work?](#-features) | [🌐 Free & OpenSource](#-open-specs)
|
||||||
[](https://makerworld.com/en/@kellervater)
|
|
||||||

|

|
||||||
|
|
||||||
HomeRacker is a fully modular rack building system for virtually any "racking needs" (Server Rack, shoe rack, book shelf, you name it).
|
HomeRacker is a fully modular 3D-printable rack-building system for virtually any “racking needs” (server rack, shoe rack, bookshelf, you name it).
|
||||||
|
|
||||||
This repo contains the respective `scad` files for all fully customizable models as well as the documentation for the entire project which seeds the page https://homeracker.org.
|
You can find all parametric and non-parametric models, as well as the `.f3d` files (like the `HomeRacker - Core`), on [Makerworld](https://makerworld.com/en/@kellervater).
|
||||||
|
|
||||||
You can find all parametric and non-parametric models as well as the `f3d` files (like the `HomeRacker - Core`) on [Makerworld](https://makerworld.com/en/@kellervater)).
|
The parametric models are available in the [HomeRacker GitHub Repository](https://github.com/kellervater/homeracker/tree/main/models).
|
||||||
|
|
||||||
|
> 💡 **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](#%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](#%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.
|
||||||
|
Many designs were too specific—accommodating only certain devices owned by their creators. Others supported only the 10" standard, with no flexibility for deviations, often requiring additional adapters.
|
||||||
|
|
||||||
|
As I began my homelab journey in April 2025, I wanted a modular solution that could adapt and grow with my evolving needs. This would eliminate the need to purchase larger racks or completely change concepts when the original design no longer met my requirements.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
As it is my nature to overengineer everything, I came up with a more generic solution to serve ANY racking need. Be it a small rack for a few Raspberry Pis, a 10" standard rack for homelabs, or even a 19" standard rack (still working on that though). You can even create bookshelves, shoe racks—or combine all of the above into an abomination of a rack.
|
||||||
|
|
||||||
|
To give you an idea of how this may look (10" rack, half-constructed Pi mini-rack, bookshelf):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Aside from the basic [building blocks](#-tech-specs), the rack above also contains the following parts:
|
||||||
|
|
||||||
|
* **10" Rack**
|
||||||
|
* [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](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)
|
||||||
|
|
||||||
|
* **Shelf** (Build any shelf configuration you like.)
|
||||||
|
|
||||||
|
# ✨ Features
|
||||||
|
|
||||||
|
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 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.
|
||||||
|
|
||||||
|
I might post a YouTube video here to show how it works.
|
||||||
|
|
||||||
|
### 🛠️ Assembly Basics
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Assembly is straightforward and requires no tools:
|
||||||
|
|
||||||
|
1. **Prepare the components**: Download the HomeRacker - Core (❗Todo: insert link), and print all required parts. Clean off any debris.
|
||||||
|
2. **Connect supports and connectors**: Attach connectors to supports based on your desired configuration.
|
||||||
|
3. **Secure with Lock Pins**: Use Lock Pins to lock parts in place. They can be inserted horizontally or vertically thanks to their square profile.
|
||||||
|
4. **Add features**: Attach panels, shelves, and other accessories as needed.
|
||||||
|
|
||||||
|
### 💡 Assembly Tips
|
||||||
|
|
||||||
|
> 💡 **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:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Make a parts list:
|
||||||
|
* How many supports of what lengths (in `base units`)?
|
||||||
|
* How many connectors of each type (pull-throughs, feet, regular)?
|
||||||
|
* Print a ton of Lock Pins. You’ll need more than you might think. The model (❗Todo: insert link) on Makerworld should include a 100-pin plate and a [Gridfinity](https://gridfinity.xyz/) box for storage.
|
||||||
|
|
||||||
|
2. Build layer by layer: Start with the base frame, add vertical supports, then stack intermediate/top frames.
|
||||||
|
3. Make sure Lock Pins are fully inserted—gentle force might be needed.
|
||||||
|
|
||||||
|
### 🖨️ Printing Tips
|
||||||
|
|
||||||
|
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!
|
||||||
|
> 💡 **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.
|
||||||
|
|
||||||
|
The system is based on 4 core measurements:
|
||||||
|
|
||||||
|
1. **15mm** – The `base_unit`. Each support has 15mm x/y dimensions, with z being a multiple of 15mm.
|
||||||
|
2. **4mm** – Side length of Lock Pins and matching holes.
|
||||||
|
3. **2mm** – Wall thickness of connectors.
|
||||||
|
4. **0.2mm** – Tolerance added to connector interiors for print/material variances.
|
||||||
|
|
||||||
|
> These values (except tolerance) are arbitrary—just made sense during design.
|
||||||
|
|
||||||
|
### 🧱 Supports
|
||||||
|
|
||||||
|
Supports are the structural spine of HomeRacker.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Height is a multiple of 15mm (base units).
|
||||||
|
E.g.: A 3-unit support = 45mm tall, a 17-unit = 255mm.
|
||||||
|
|
||||||
|
Each unit height includes a 4mm hole for a Lock Pin.
|
||||||
|
|
||||||
|
* Holes match Lock Pin dimensions (no tolerance).
|
||||||
|
* Holes are convex on x and z axes for multi-directional insertion.
|
||||||
|
|
||||||
|
Schematics:
|
||||||
|
|
||||||
|
**Front View**
|
||||||
|

|
||||||
|
|
||||||
|
**Side View**
|
||||||
|

|
||||||
|
|
||||||
|
**Top View**
|
||||||
|

|
||||||
|
|
||||||
|
### 🔗 Connectors
|
||||||
|
|
||||||
|
Connectors join supports in 1 to 3 dimensions.
|
||||||
|
From straight extenders to 6-way junctions.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Types:
|
||||||
|
* **Standard** – Solid center; best for load-bearing.
|
||||||
|
* **Pull-Through** – Open center for complex builds (e.g. 10" rack).
|
||||||
|
* **Feet** – Solid end pieces; used as rack feet.
|
||||||
|
|
||||||
|
> ❗ **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).
|
||||||
|
|
||||||
|
Schematics:
|
||||||
|
|
||||||
|
**Top View – Outer**
|
||||||
|

|
||||||
|
|
||||||
|
**Top View – Inner**
|
||||||
|

|
||||||
|
|
||||||
|
### 📏 lock Pins
|
||||||
|
|
||||||
|
Lock Pins hold the system together.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
They rely on tension from their convex shape to stay in place.
|
||||||
|
|
||||||
|
Schematics:
|
||||||
|
|
||||||
|
**Top View**
|
||||||
|

|
||||||
|
|
||||||
|
**Side View**
|
||||||
|
> Don’t ask why the height is 3.791mm—it works. I left it as-is.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> 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.
|
||||||
|
|
||||||
|
## 🌍 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.)
|
||||||
|
|
||||||
|
I encourage you to make your own models based on HomeRacker!
|
||||||
|
Let me know, and I’ll feature your work on this page and cross-link it on Makerworld (subject to my "very objective" approval 😄).
|
||||||
|
Just [create an issue](https://github.com/kellervater/homeracker/issues/new) if you want to be featured.
|
||||||
|
|
||||||
|
# ❓ Why the name?
|
||||||
|
|
||||||
|
After ~4 hours of research, I found all my original ideas (UniRack, OpenRack, etc.) were taken.
|
||||||
|
|
||||||
|
So, "HomeRacker" was born—part practical, part tongue-in-cheek. It fits the homelab theme, but also hints at the "home-wrecking" time sink this can become.
|
||||||
|
|
||||||
# 📜 Licensing
|
# 📜 Licensing
|
||||||
* The source code in this repository is licensed under the `MIT License` (see [LICENSE](./LICENSE)).
|
|
||||||
* All 3D models and creative assets (in /models/) are licensed under the `CC BY-NC 4.0 License` (see [/models/LICENSE](./models/LICENSE)).
|
> 💡 **tl;dr**
|
||||||
|
> – Use it for ANY purpose (even commercial), but credit me and share alike!
|
||||||
|
|
||||||
|
* 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**
|
||||||
|
> 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.
|
||||||
|
|
||||||
|
# 🧪 Tests
|
||||||
|
|
||||||
|
Of course I tested stuff... It took ~4 months from idea to this release.
|
||||||
|
Look at all the prototypes:
|
||||||
|

|
||||||
|
|
||||||
|
## ⚠️ Disclaimer
|
||||||
|
|
||||||
|
> ⚠️ **Warning**
|
||||||
|
> This project is provided “as is,” without any warranty. Use at your own risk. I’m not responsible for damage, injury, or loss caused by using this system or its parts.
|
||||||
|
|
||||||
|
|
||||||
|
Aside from the scary warning above, I need to mention, that due to the high modularity of this system combined with limited time and resources I was of course not able to test every combination of filaments, printers, print-settings, room conditions (temperature, humidity) or to do extensive load-bearing tests.
|
||||||
|
|
||||||
|
What I want to say:
|
||||||
|
I feel like the model turned out to be really nice and versatile. That's why I shared it in the first place.
|
||||||
|
But since I do not have control over the manufacturing conditions of any consumer of this model, I cannot give any guarantees on how your specific print will turn out in the end. There are just too much variables which not even the best model design can compensate for. (Writing this feels a bit like an upfront apology... seems like I'm a people pleaser)
|
||||||
|
|
||||||
|
## 🔬 How I tested
|
||||||
|
My setup is as follows:
|
||||||
|
* a room temperature between 17 and 25°C
|
||||||
|
* Humidity levels between 29% and 36% (depends on when I'm doing my laundry)
|
||||||
|
* A BambuLab X1C printer
|
||||||
|
* Exclusively BambuLab filament (haven't tried others yet)
|
||||||
|
* PLA Matte (I love the charcoal color. Looks so silky)
|
||||||
|
* PLA Basic
|
||||||
|
* ABS
|
||||||
|
* mostly I used the Textured PEI plate. It just works (provided you regularly clean it using Isopropyl alcohol). For the rest of the time I tried out the Cold Plate Super Track (it's nice but very hard to get your prints of the plates when it cools)
|
||||||
|
|
||||||
|
All above's filament types can be be combined in any possible way (just make sure you do flow calibration before using new filaments. First ABS print turned out horribly just because I forgot to click the calibration checkbox).
|
||||||
|
E.g.: you could print a connector in ABS, a support in PLA Matte and a Lock Pin in PLA and they will just fit when being assembled.
|
||||||
|
|
||||||
|
> 🛠️ **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
|
||||||
|
* [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
|
||||||
|
* [x] HomeRacker - Airflow Kit (exclusive) -> under 10" Rack
|
||||||
|
* [x] HomeRacker - Shelf
|
||||||
|
* [ ] Contributing.md stub?
|
||||||
|
* [ ] 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 we’ll 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, it’ll be immediately visible to users that a model is designed to be mounted on a HomeRacker system.
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
remote_theme: pages-themes/architect@v0.2.0
|
remote_theme: pages-themes/architect@v0.2.0
|
||||||
title: HomeRacker
|
title: HomeRacker
|
||||||
description: "The universal modular rack building system"
|
description: "A printable modular rack building system"
|
||||||
author: "kellervater"
|
author: "kellervater"
|
||||||
|
|||||||
BIN
favicon.ico
Normal file
|
After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
BIN
img/core_connector_1d.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
BIN
img/core_lock_pin_3d.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
img/core_lock_pin_side.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
img/core_lock_pin_top.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
img/core_support_3d_transparent.png
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
img/core_support_front_profile.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
img/core_support_side_profile.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
img/core_support_top_profile.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
img/homeracker_army.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
img/homeracker_favicon_transparent.xcf
Normal file
BIN
img/homeracker_icon_yellow.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
img/homeracker_logo.png
Normal file
|
After Width: | Height: | Size: 481 KiB |
BIN
img/homeracker_logo.xcf
Normal file
BIN
img/homeracker_logo_transparent.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
img/prototypes.jpg
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
img/real_life_example.jpg
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
@@ -1,4 +1,4 @@
|
|||||||
Attribution-NonCommercial-ShareAlike 4.0 International
|
Attribution-ShareAlike 4.0 International
|
||||||
|
|
||||||
=======================================================================
|
=======================================================================
|
||||||
|
|
||||||
@@ -54,18 +54,18 @@ exhaustive, and do not form part of our licenses.
|
|||||||
|
|
||||||
=======================================================================
|
=======================================================================
|
||||||
|
|
||||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
|
Creative Commons Attribution-ShareAlike 4.0 International Public
|
||||||
Public License
|
License
|
||||||
|
|
||||||
By exercising the Licensed Rights (defined below), You accept and agree
|
By exercising the Licensed Rights (defined below), You accept and agree
|
||||||
to be bound by the terms and conditions of this Creative Commons
|
to be bound by the terms and conditions of this Creative Commons
|
||||||
Attribution-NonCommercial-ShareAlike 4.0 International Public License
|
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||||
("Public License"). To the extent this Public License may be
|
License"). To the extent this Public License may be interpreted as a
|
||||||
interpreted as a contract, You are granted the Licensed Rights in
|
contract, You are granted the Licensed Rights in consideration of Your
|
||||||
consideration of Your acceptance of these terms and conditions, and the
|
acceptance of these terms and conditions, and the Licensor grants You
|
||||||
Licensor grants You such rights in consideration of benefits the
|
such rights in consideration of benefits the Licensor receives from
|
||||||
Licensor receives from making the Licensed Material available under
|
making the Licensed Material available under these terms and
|
||||||
these terms and conditions.
|
conditions.
|
||||||
|
|
||||||
|
|
||||||
Section 1 -- Definitions.
|
Section 1 -- Definitions.
|
||||||
@@ -84,7 +84,7 @@ Section 1 -- Definitions.
|
|||||||
and Similar Rights in Your contributions to Adapted Material in
|
and Similar Rights in Your contributions to Adapted Material in
|
||||||
accordance with the terms and conditions of this Public License.
|
accordance with the terms and conditions of this Public License.
|
||||||
|
|
||||||
c. BY-NC-SA Compatible License means a license listed at
|
c. BY-SA Compatible License means a license listed at
|
||||||
creativecommons.org/compatiblelicenses, approved by Creative
|
creativecommons.org/compatiblelicenses, approved by Creative
|
||||||
Commons as essentially the equivalent of this Public License.
|
Commons as essentially the equivalent of this Public License.
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ Section 1 -- Definitions.
|
|||||||
|
|
||||||
g. License Elements means the license attributes listed in the name
|
g. License Elements means the license attributes listed in the name
|
||||||
of a Creative Commons Public License. The License Elements of this
|
of a Creative Commons Public License. The License Elements of this
|
||||||
Public License are Attribution, NonCommercial, and ShareAlike.
|
Public License are Attribution and ShareAlike.
|
||||||
|
|
||||||
h. Licensed Material means the artistic or literary work, database,
|
h. Licensed Material means the artistic or literary work, database,
|
||||||
or other material to which the Licensor applied this Public
|
or other material to which the Licensor applied this Public
|
||||||
@@ -122,15 +122,7 @@ Section 1 -- Definitions.
|
|||||||
j. Licensor means the individual(s) or entity(ies) granting rights
|
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||||
under this Public License.
|
under this Public License.
|
||||||
|
|
||||||
k. NonCommercial means not primarily intended for or directed towards
|
k. Share means to provide material to the public by any means or
|
||||||
commercial advantage or monetary compensation. For purposes of
|
|
||||||
this Public License, the exchange of the Licensed Material for
|
|
||||||
other material subject to Copyright and Similar Rights by digital
|
|
||||||
file-sharing or similar means is NonCommercial provided there is
|
|
||||||
no payment of monetary compensation in connection with the
|
|
||||||
exchange.
|
|
||||||
|
|
||||||
l. Share means to provide material to the public by any means or
|
|
||||||
process that requires permission under the Licensed Rights, such
|
process that requires permission under the Licensed Rights, such
|
||||||
as reproduction, public display, public performance, distribution,
|
as reproduction, public display, public performance, distribution,
|
||||||
dissemination, communication, or importation, and to make material
|
dissemination, communication, or importation, and to make material
|
||||||
@@ -138,13 +130,13 @@ Section 1 -- Definitions.
|
|||||||
public may access the material from a place and at a time
|
public may access the material from a place and at a time
|
||||||
individually chosen by them.
|
individually chosen by them.
|
||||||
|
|
||||||
m. Sui Generis Database Rights means rights other than copyright
|
l. Sui Generis Database Rights means rights other than copyright
|
||||||
resulting from Directive 96/9/EC of the European Parliament and of
|
resulting from Directive 96/9/EC of the European Parliament and of
|
||||||
the Council of 11 March 1996 on the legal protection of databases,
|
the Council of 11 March 1996 on the legal protection of databases,
|
||||||
as amended and/or succeeded, as well as other essentially
|
as amended and/or succeeded, as well as other essentially
|
||||||
equivalent rights anywhere in the world.
|
equivalent rights anywhere in the world.
|
||||||
|
|
||||||
n. You means the individual or entity exercising the Licensed Rights
|
m. You means the individual or entity exercising the Licensed Rights
|
||||||
under this Public License. Your has a corresponding meaning.
|
under this Public License. Your has a corresponding meaning.
|
||||||
|
|
||||||
|
|
||||||
@@ -158,10 +150,9 @@ Section 2 -- Scope.
|
|||||||
exercise the Licensed Rights in the Licensed Material to:
|
exercise the Licensed Rights in the Licensed Material to:
|
||||||
|
|
||||||
a. reproduce and Share the Licensed Material, in whole or
|
a. reproduce and Share the Licensed Material, in whole or
|
||||||
in part, for NonCommercial purposes only; and
|
in part; and
|
||||||
|
|
||||||
b. produce, reproduce, and Share Adapted Material for
|
b. produce, reproduce, and Share Adapted Material.
|
||||||
NonCommercial purposes only.
|
|
||||||
|
|
||||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||||
Exceptions and Limitations apply to Your use, this Public
|
Exceptions and Limitations apply to Your use, this Public
|
||||||
@@ -229,9 +220,7 @@ Section 2 -- Scope.
|
|||||||
Rights, whether directly or through a collecting society
|
Rights, whether directly or through a collecting society
|
||||||
under any voluntary or waivable statutory or compulsory
|
under any voluntary or waivable statutory or compulsory
|
||||||
licensing scheme. In all other cases the Licensor expressly
|
licensing scheme. In all other cases the Licensor expressly
|
||||||
reserves any right to collect such royalties, including when
|
reserves any right to collect such royalties.
|
||||||
the Licensed Material is used other than for NonCommercial
|
|
||||||
purposes.
|
|
||||||
|
|
||||||
|
|
||||||
Section 3 -- License Conditions.
|
Section 3 -- License Conditions.
|
||||||
@@ -276,6 +265,7 @@ following conditions.
|
|||||||
reasonable to satisfy the conditions by providing a URI or
|
reasonable to satisfy the conditions by providing a URI or
|
||||||
hyperlink to a resource that includes the required
|
hyperlink to a resource that includes the required
|
||||||
information.
|
information.
|
||||||
|
|
||||||
3. If requested by the Licensor, You must remove any of the
|
3. If requested by the Licensor, You must remove any of the
|
||||||
information required by Section 3(a)(1)(A) to the extent
|
information required by Section 3(a)(1)(A) to the extent
|
||||||
reasonably practicable.
|
reasonably practicable.
|
||||||
@@ -287,7 +277,7 @@ following conditions.
|
|||||||
|
|
||||||
1. The Adapter's License You apply must be a Creative Commons
|
1. The Adapter's License You apply must be a Creative Commons
|
||||||
license with the same License Elements, this version or
|
license with the same License Elements, this version or
|
||||||
later, or a BY-NC-SA Compatible License.
|
later, or a BY-SA Compatible License.
|
||||||
|
|
||||||
2. You must include the text of, or the URI or hyperlink to, the
|
2. You must include the text of, or the URI or hyperlink to, the
|
||||||
Adapter's License You apply. You may satisfy this condition
|
Adapter's License You apply. You may satisfy this condition
|
||||||
@@ -307,8 +297,7 @@ apply to Your use of the Licensed Material:
|
|||||||
|
|
||||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||||
to extract, reuse, reproduce, and Share all or a substantial
|
to extract, reuse, reproduce, and Share all or a substantial
|
||||||
portion of the contents of the database for NonCommercial purposes
|
portion of the contents of the database;
|
||||||
only;
|
|
||||||
|
|
||||||
b. if You include all or a substantial portion of the database
|
b. if You include all or a substantial portion of the database
|
||||||
contents in a database in which You have Sui Generis Database
|
contents in a database in which You have Sui Generis Database
|
||||||
@@ -415,6 +404,7 @@ Section 8 -- Interpretation.
|
|||||||
that apply to the Licensor or You, including from the legal
|
that apply to the Licensor or You, including from the legal
|
||||||
processes of any jurisdiction or authority.
|
processes of any jurisdiction or authority.
|
||||||
|
|
||||||
|
|
||||||
=======================================================================
|
=======================================================================
|
||||||
|
|
||||||
Creative Commons is not a party to its public
|
Creative Commons is not a party to its public
|
||||||
|
|||||||
199
models/flexmount/flexmount.scad
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
98
models/gridfinity/base.scad
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// 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=2.5; // mm
|
||||||
|
// Lock Pin side length
|
||||||
|
LOCK_PIN_SIDE=3; // mm
|
||||||
|
// lock pin edge distance
|
||||||
|
LOCK_PIN_EDGE_DISTANCE=5.5; // mm
|
||||||
|
// lock pin chamfer
|
||||||
|
LOCK_PIN_CHAMFER=0.8; // mm
|
||||||
|
|
||||||
|
GRIDFINITY_BASEPLATE_SIDE_LENGTH=42; // mm
|
||||||
|
GRIDFINITY_BASEPLATE_STRENGTH=8; // mm
|
||||||
|
GRIDFINITY_BASEPLATE_SIDE_LENGTH_INNER=GRIDFINITY_BASEPLATE_SIDE_LENGTH-GRIDFINITY_BASEPLATE_STRENGTH/2; // mm
|
||||||
|
|
||||||
|
GRIDFINITY_INCLINE_1=0.7; // mm
|
||||||
|
GRIDFINITY_INCLINE_2=1.8; // mm
|
||||||
|
GRIDFINITY_INCLINE_3=2.15; // mm
|
||||||
|
GRIDFINITY_BASEPLATE_HEIGHT=GRIDFINITY_INCLINE_1+GRIDFINITY_INCLINE_2+GRIDFINITY_INCLINE_3; // mm
|
||||||
|
|
||||||
|
/* [Grdifinity] */
|
||||||
|
x_units=1; // [1:1:10]
|
||||||
|
y_units=1; // [1:1:10]
|
||||||
|
|
||||||
|
module recess(side_length,chamfer){
|
||||||
|
//chamfer
|
||||||
|
rotate([180,0,0])
|
||||||
|
prismoid(
|
||||||
|
size1=[side_length,side_length],
|
||||||
|
rounding=chamfer,
|
||||||
|
h=chamfer,
|
||||||
|
xang=45,
|
||||||
|
yang=45
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module gridfinity_unit(){
|
||||||
|
difference(){
|
||||||
|
//outer cube
|
||||||
|
cuboid(
|
||||||
|
size=[GRIDFINITY_BASEPLATE_SIDE_LENGTH,GRIDFINITY_BASEPLATE_SIDE_LENGTH,GRIDFINITY_BASEPLATE_HEIGHT],
|
||||||
|
anchor=CENTER+BOTTOM
|
||||||
|
);
|
||||||
|
//1st recess
|
||||||
|
|
||||||
|
translate([0,0,GRIDFINITY_BASEPLATE_HEIGHT])
|
||||||
|
recess(
|
||||||
|
GRIDFINITY_BASEPLATE_SIDE_LENGTH,
|
||||||
|
GRIDFINITY_INCLINE_3);
|
||||||
|
//2nd recess
|
||||||
|
translate([0,0,GRIDFINITY_BASEPLATE_HEIGHT-GRIDFINITY_INCLINE_3-GRIDFINITY_INCLINE_2])
|
||||||
|
cuboid(
|
||||||
|
size=[
|
||||||
|
GRIDFINITY_BASEPLATE_SIDE_LENGTH-GRIDFINITY_INCLINE_3*2,
|
||||||
|
GRIDFINITY_BASEPLATE_SIDE_LENGTH-GRIDFINITY_INCLINE_3*2,
|
||||||
|
GRIDFINITY_INCLINE_2
|
||||||
|
],
|
||||||
|
rounding=GRIDFINITY_INCLINE_2,
|
||||||
|
except=[BOTTOM,TOP],
|
||||||
|
anchor=CENTER+BOTTOM
|
||||||
|
);
|
||||||
|
// //3rd recess
|
||||||
|
translate([0,0,GRIDFINITY_INCLINE_1+0.01])
|
||||||
|
recess(
|
||||||
|
GRIDFINITY_BASEPLATE_SIDE_LENGTH_INNER,
|
||||||
|
GRIDFINITY_INCLINE_1+0.01
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module grid(x,y){
|
||||||
|
intersection(){
|
||||||
|
grid_copies(spacing=[GRIDFINITY_BASEPLATE_SIDE_LENGTH,GRIDFINITY_BASEPLATE_SIDE_LENGTH],n=[x,y])
|
||||||
|
gridfinity_unit();
|
||||||
|
// roundings
|
||||||
|
cuboid(
|
||||||
|
size=[GRIDFINITY_BASEPLATE_SIDE_LENGTH*x,GRIDFINITY_BASEPLATE_SIDE_LENGTH*y,GRIDFINITY_BASEPLATE_HEIGHT*2],
|
||||||
|
anchor=CENTER+BOTTOM,
|
||||||
|
rounding=CHAMFER,
|
||||||
|
except=[BOTTOM,TOP]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
grid(x=2,y=3);
|
||||||
@@ -1,13 +1,28 @@
|
|||||||
// import BOSL2
|
// import BOSL2
|
||||||
include <BOSL2/std.scad>
|
include <BOSL2/std.scad>
|
||||||
|
|
||||||
|
/* [Hidden] */
|
||||||
$fn=100;
|
$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] */
|
/* [Base] */
|
||||||
// automatically chooses the tightest fit for the rackmount ears based on the device width. If true, rack_size will be ignored.
|
// automatically chooses the tightest fit for the rackmount ears based on the device width. If true, rack_size will be ignored.
|
||||||
autosize=true;
|
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 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]
|
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.
|
// Width of the device in mm. Will determine the width of the rackmount ears depending on rack_size.
|
||||||
device_width=201;
|
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.
|
// 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;
|
center_device_bore_alignment=false;
|
||||||
|
|
||||||
|
/* [Derived] */
|
||||||
/* [CONSTANTS (shouldn't need to be changed)] */
|
|
||||||
CAGE_BOLT_DIAMETER=6.5;
|
|
||||||
CHAMFER=min(strength/3,0.5);
|
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_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_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_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_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
|
// Debug
|
||||||
echo("Height: ", RACK_HEIGHT);
|
echo("Height: ", RACK_HEIGHT);
|
||||||
echo("Rack Bore Count: ", RACK_BORE_COUNT);
|
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) =
|
function get_bore_depth(device_bore_margin_horizontal,device_bore_columns) =
|
||||||
(device_bore_columns - 1) * device_bore_margin_horizontal
|
(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) {
|
module base_ear(width,strength,height) {
|
||||||
union() {
|
union() {
|
||||||
// Front face
|
// 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
|
// Side face
|
||||||
cuboid([strength,depth,height],anchor=LEFT+BOTTOM+FRONT,chamfer=CHAMFER);
|
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
|
// 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() {
|
difference() {
|
||||||
// Create the base of the ear
|
difference() {
|
||||||
base_ear(device_width,strength,RACK_HEIGHT);
|
// Create the base of the ear
|
||||||
// Create the holes for the device screws
|
base_ear(rack_ear_width,strength,RACK_HEIGHT);
|
||||||
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 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])
|
// Ear distance
|
||||||
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 = 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);
|
||||||
}
|
}
|
||||||
|
|||||||
130
models/wallmount/wallmount.scad
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
BIN
png_creation/PXL_20250915_172044293.jpg
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
25
png_creation/create_png.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import google.generativeai as genai
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Configure with your API key
|
||||||
|
genai.configure(api_key="YOUR_API_KEY")
|
||||||
|
|
||||||
|
# The prompt from our refined prompt file
|
||||||
|
prompt_text = """
|
||||||
|
Analyze the attached image and generate a PNG file...
|
||||||
|
[...your full prompt text here...]
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Load the image and prompt
|
||||||
|
model = genai.GenerativeModel('gemini-1.5-pro')
|
||||||
|
source_image = genai.upload_file(path="PXL_20250915_172025980.jpg")
|
||||||
|
|
||||||
|
# Make the API call
|
||||||
|
response = model.generate_content([prompt_text, source_image])
|
||||||
|
|
||||||
|
# Save the generated image data from the response
|
||||||
|
# (The exact syntax for saving the file may vary based on API response structure)
|
||||||
|
with open("output_silhouette.png", "wb") as f:
|
||||||
|
f.write(response.parts[0].blob.data)
|
||||||
|
|
||||||
|
print("Silhouette PNG created successfully!")
|
||||||
17
png_creation/png_creation.scad
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/* [SVG Parameters] */
|
||||||
|
// Amount to "thicken" the SVG shape in mm. This helps to fill small gaps.
|
||||||
|
thicken_amount = 0.1; // [0:0.1:5]
|
||||||
|
|
||||||
|
// Height of the extrusion in mm
|
||||||
|
extrusion_height = 20; // [1:1:100]
|
||||||
|
|
||||||
|
/* [Global Settings] */
|
||||||
|
$fn = 100;
|
||||||
|
|
||||||
|
// --- Implementation ---
|
||||||
|
|
||||||
|
linear_extrude(height = extrusion_height) {
|
||||||
|
offset(delta = thicken_amount) {
|
||||||
|
import("spitzzangerl.svg", center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
png_creation/prompt.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# --- Gemini Agent Prompt: Create Tool Silhouette ---
|
||||||
|
|
||||||
|
**ROLE:**
|
||||||
|
You are an expert image processing agent. Your sole function is to create a clean, solid black silhouette from a user-provided image of a tool.
|
||||||
|
|
||||||
|
**TASK:**
|
||||||
|
Analyze the attached image and generate a PNG file that meets the precise output requirements below. This PNG will be used as a source for a vector tracing program (Potrace), so precision is critical.
|
||||||
|
|
||||||
|
**INPUT:**
|
||||||
|
- An image file of a single tool lying on a high-contrast background.
|
||||||
|
|
||||||
|
**OUTPUT REQUIREMENTS:**
|
||||||
|
1. **Format:** PNG with a transparent background.
|
||||||
|
2. **Content:** A silhouette representing the **single, continuous, outermost contour only**. All internal holes, lines, and details must be completely filled in.
|
||||||
|
3. **Color:** The silhouette must be 100% solid black (`#000000`). No anti-aliasing or grey pixels.
|
||||||
|
4. **Cropping:** The final image must be tightly cropped around the silhouette with minimal transparent padding.
|
||||||
|
|
||||||
|
**ACTION:**
|
||||||
|
Process the attached photo and provide only the resulting PNG file as your output.
|
||||||
53
png_creation/spitzzangerl.svg
Normal file
|
After Width: | Height: | Size: 687 KiB |
6
renovate.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended"
|
||||||
|
]
|
||||||
|
}
|
||||||
2
scripts/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.env
|
||||||
|
.vscode
|
||||||
12
scripts/ExportCoreComponents/ExportCoreComponents.manifest
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"autodeskProduct": "Fusion",
|
||||||
|
"type": "script",
|
||||||
|
"author": "kellervater (Patrick Poetz)",
|
||||||
|
"description": {
|
||||||
|
"": "Exports\n* Supports in all sane dimensions\n* Lock Pin\n* All Bodies which can be found in the Group \"Connectors\" and its subgroups"
|
||||||
|
},
|
||||||
|
"version": "",
|
||||||
|
"supportedOS": "windows|mac",
|
||||||
|
"editEnabled": true,
|
||||||
|
"iconFilename": "ScriptIcon.svg"
|
||||||
|
}
|
||||||
219
scripts/ExportCoreComponents/ExportCoreComponents.py
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
#Fusion360API Python Script
|
||||||
|
#Description: Exports Core components. Connectors identified by naming convention.
|
||||||
|
# - "Support" body (parametric by 'support_units')
|
||||||
|
# - "Lock Pin" body (single export)
|
||||||
|
# - "Connector" bodies (categorized by name, exported to subfolders)
|
||||||
|
#Version: 1.4.1 - Added 'pr' suffix for PullThrough connectors.
|
||||||
|
|
||||||
|
import adsk.core, adsk.fusion, adsk.cam, traceback, os
|
||||||
|
import re # Import regular expression module
|
||||||
|
|
||||||
|
def run(context):
|
||||||
|
ui = None
|
||||||
|
try:
|
||||||
|
# --- Basic Fusion 360 setup ---
|
||||||
|
app = adsk.core.Application.get()
|
||||||
|
ui = app.userInterface
|
||||||
|
# Using Design.cast for robustness
|
||||||
|
design = adsk.fusion.Design.cast(app.activeProduct)
|
||||||
|
if not design:
|
||||||
|
if ui: ui.messageBox('No active Fusion 360 design (or product is not a Design). Aborting script.', 'Export Script Error')
|
||||||
|
return
|
||||||
|
|
||||||
|
rootComp = design.rootComponent
|
||||||
|
doc = app.activeDocument
|
||||||
|
if not rootComp or not doc:
|
||||||
|
if ui: ui.messageBox('Could not retrieve root component or active document. Aborting script.', 'Export Script Error')
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- Get Document Version (Simplified) ---
|
||||||
|
version_str = "v_unknown"
|
||||||
|
try:
|
||||||
|
if doc.dataFile:
|
||||||
|
version_number = doc.dataFile.versionNumber
|
||||||
|
version_str = f"v{version_number}" if version_number is not None else "v_no_version_num"
|
||||||
|
if version_number is None and ui: ui.messageBox(f"Cloud-saved, but version unavailable. Using '{version_str}'.", "Version Warning")
|
||||||
|
else:
|
||||||
|
version_str = "v_local_unversioned"
|
||||||
|
if ui: ui.messageBox(f"Document not cloud-saved. Using suffix '{version_str}'.", "Unsaved Document Info")
|
||||||
|
except Exception as e_ver:
|
||||||
|
version_str = "v_version_error"
|
||||||
|
if ui: ui.messageBox(f"Version retrieval error: {e_ver}\nUsing '{version_str}'.", "Version Retrieval Error")
|
||||||
|
# --- End Get Document Version ---
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
support_param_name = "support_units"
|
||||||
|
min_support_units, max_support_units = 2, 25
|
||||||
|
support_body_name = "Support" # Exact name of the Support BRepBody
|
||||||
|
lock_pin_body_name = "Lock Pin" # Exact name of the Lock Pin BRepBody
|
||||||
|
base_export_name_prefix = "Core" # Prefix for Support and LockPin files
|
||||||
|
connectors_output_base_folder_name = "Connectors" # Main output folder for all connector types
|
||||||
|
|
||||||
|
# Regex for connector names: extracts dimensions, optional ways, and optional type suffix
|
||||||
|
# Pattern: e.g., "3d", "2d4w", "3d5wp", "3d4wpr", "3d4wps", "1d2wf"
|
||||||
|
# Groups: 1=(\d+ dimensions), 2=(\d+ ways, optional), 3=(p|ps|pr|f suffix, optional)
|
||||||
|
connector_name_pattern = re.compile(r"^(\d+)d(?:(\d+)w)?(p|ps|pr|f)?$") # Updated for 'pr'
|
||||||
|
# --- End Configuration ---
|
||||||
|
|
||||||
|
userParams = design.userParameters
|
||||||
|
if userParams is None:
|
||||||
|
if ui: ui.messageBox('Could not access user parameters. Aborting.', 'Export Script Error')
|
||||||
|
return
|
||||||
|
|
||||||
|
support_param = userParams.itemByName(support_param_name)
|
||||||
|
support_body_object_for_parametric_export = rootComp.bRepBodies.itemByName(support_body_name)
|
||||||
|
lock_pin_body_object_for_export = rootComp.bRepBodies.itemByName(lock_pin_body_name)
|
||||||
|
|
||||||
|
all_export_tasks = []
|
||||||
|
|
||||||
|
# 1. Support Tasks
|
||||||
|
if support_body_object_for_parametric_export and support_body_object_for_parametric_export.isValid and \
|
||||||
|
support_param and support_param.isValid:
|
||||||
|
for i in range(min_support_units, max_support_units + 1):
|
||||||
|
all_export_tasks.append({
|
||||||
|
"type": "support", "units": i, "body_object": support_body_object_for_parametric_export,
|
||||||
|
"base_filename": f"{base_export_name_prefix}_{support_body_name}_x{i}"})
|
||||||
|
else:
|
||||||
|
missing_s_info = []
|
||||||
|
if not (support_body_object_for_parametric_export and support_body_object_for_parametric_export.isValid):
|
||||||
|
missing_s_info.append(f"Body '{support_body_name}'")
|
||||||
|
if not (support_param and support_param.isValid):
|
||||||
|
missing_s_info.append(f"Parameter '{support_param_name}'")
|
||||||
|
if missing_s_info and ui:
|
||||||
|
ui.messageBox(f"Support Export Warning: {', '.join(missing_s_info)} not found/valid. Skipping Support exports.", "Config Warning")
|
||||||
|
|
||||||
|
# 2. Lock Pin Task
|
||||||
|
if lock_pin_body_object_for_export and lock_pin_body_object_for_export.isValid:
|
||||||
|
all_export_tasks.append({
|
||||||
|
"type": "lock_pin", "body_object": lock_pin_body_object_for_export,
|
||||||
|
"base_filename": f"{base_export_name_prefix}_{lock_pin_body_name}"})
|
||||||
|
elif lock_pin_body_name and ui:
|
||||||
|
ui.messageBox(f"Lock Pin Export Warning: Body '{lock_pin_body_name}' not found/valid. Skipping Lock Pin export.", "Config Warning")
|
||||||
|
|
||||||
|
# 3. Connector Tasks (by Name Parsing)
|
||||||
|
connector_tasks_temp = []
|
||||||
|
found_connectors_by_name = 0
|
||||||
|
if rootComp.bRepBodies:
|
||||||
|
for body_iter in rootComp.bRepBodies:
|
||||||
|
if not body_iter.isValid or not body_iter.name:
|
||||||
|
continue
|
||||||
|
body_name_iter = body_iter.name
|
||||||
|
|
||||||
|
if body_name_iter == support_body_name or body_name_iter == lock_pin_body_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = connector_name_pattern.match(body_name_iter)
|
||||||
|
if match:
|
||||||
|
type_suffix = match.group(3)
|
||||||
|
|
||||||
|
target_subfolder_name = None
|
||||||
|
if type_suffix == "p" or type_suffix == "ps" or type_suffix == "pr": # Added 'pr'
|
||||||
|
target_subfolder_name = "PullThrough"
|
||||||
|
elif type_suffix == "f":
|
||||||
|
target_subfolder_name = "Feet"
|
||||||
|
elif type_suffix is None:
|
||||||
|
target_subfolder_name = "Standard"
|
||||||
|
|
||||||
|
if target_subfolder_name:
|
||||||
|
found_connectors_by_name += 1
|
||||||
|
connector_tasks_temp.append({
|
||||||
|
"type": "connector",
|
||||||
|
"body_object": body_iter,
|
||||||
|
"relative_export_path_dir": os.path.join(connectors_output_base_folder_name, target_subfolder_name),
|
||||||
|
"base_filename": body_name_iter
|
||||||
|
})
|
||||||
|
|
||||||
|
if connector_tasks_temp:
|
||||||
|
all_export_tasks.extend(connector_tasks_temp)
|
||||||
|
|
||||||
|
if found_connectors_by_name == 0 and ui:
|
||||||
|
ui.messageBox("Connector Export Warning: No bodies matching the defined connector naming patterns were found in the root component.", "Name Matching Warning")
|
||||||
|
|
||||||
|
if not all_export_tasks:
|
||||||
|
if ui: ui.messageBox("No export tasks were generated (possibly due to configuration or missing items). Aborting.", "Export Script Error")
|
||||||
|
return
|
||||||
|
|
||||||
|
folderDialog = ui.createFolderDialog()
|
||||||
|
folderDialog.title = "Select Folder to Save STEP Files"
|
||||||
|
dialogResult = folderDialog.showDialog()
|
||||||
|
if dialogResult == adsk.core.DialogResults.DialogOK: exportFolder = folderDialog.folder
|
||||||
|
else:
|
||||||
|
if ui: ui.messageBox('Export cancelled by user. Aborting script.')
|
||||||
|
return
|
||||||
|
|
||||||
|
exportMgr = design.exportManager
|
||||||
|
total_exports = len(all_export_tasks)
|
||||||
|
progressDialog = ui.createProgressDialog()
|
||||||
|
progressDialog.isCancelEnabled = False
|
||||||
|
progressDialog.show(f'Exporting {base_export_name_prefix} Items', 'Initializing...', 0, total_exports)
|
||||||
|
|
||||||
|
exported_count = 0
|
||||||
|
all_root_bodies_for_visibility_ctrl = [b for b in rootComp.bRepBodies if b.isValid] if rootComp.bRepBodies else []
|
||||||
|
|
||||||
|
if not all_root_bodies_for_visibility_ctrl and total_exports > 0 and ui:
|
||||||
|
ui.messageBox(f"Warning: No bodies retrieved from root for visibility control, but {total_exports} tasks are planned. Exports may fail or include unintended geometry.", "Visibility Control Warning")
|
||||||
|
|
||||||
|
for i, task in enumerate(all_export_tasks):
|
||||||
|
try:
|
||||||
|
target_body_for_export = task.get("body_object")
|
||||||
|
if not target_body_for_export or not target_body_for_export.isValid:
|
||||||
|
progressDialog.message = f'Skipping invalid task {i+1}/{total_exports}'
|
||||||
|
adsk.doEvents(); continue
|
||||||
|
|
||||||
|
progressDialog.progressValue = exported_count
|
||||||
|
task_desc = task["base_filename"]
|
||||||
|
if task["type"] == "support": task_desc += f" (Units: {task['units']})"
|
||||||
|
progressDialog.message = f'Exporting: {task_desc} ({exported_count + 1}/{total_exports})'
|
||||||
|
adsk.doEvents()
|
||||||
|
|
||||||
|
for body_in_root_list in all_root_bodies_for_visibility_ctrl:
|
||||||
|
body_in_root_list.isLightBulbOn = (body_in_root_list.entityToken == target_body_for_export.entityToken)
|
||||||
|
adsk.doEvents()
|
||||||
|
|
||||||
|
if task["type"] == "support":
|
||||||
|
if support_param and support_param.isValid:
|
||||||
|
support_param.expression = str(task["units"])
|
||||||
|
adsk.doEvents()
|
||||||
|
else:
|
||||||
|
if ui: ui.messageBox(f"Support parameter '{support_param_name}' became invalid during export. Skipping support task for {target_body_for_export.name}.", "Export Loop Error")
|
||||||
|
continue
|
||||||
|
|
||||||
|
filename_base = task['base_filename']
|
||||||
|
filename = f"{filename_base}_{version_str}.step"
|
||||||
|
|
||||||
|
if task["type"] == "connector":
|
||||||
|
current_export_dir = os.path.join(exportFolder, task["relative_export_path_dir"])
|
||||||
|
if not os.path.exists(current_export_dir): os.makedirs(current_export_dir)
|
||||||
|
full_path = os.path.join(current_export_dir, filename)
|
||||||
|
else:
|
||||||
|
full_path = os.path.join(exportFolder, filename)
|
||||||
|
|
||||||
|
stepSaveOptions = exportMgr.createSTEPExportOptions(full_path, rootComp)
|
||||||
|
exportMgr.execute(stepSaveOptions)
|
||||||
|
exported_count += 1
|
||||||
|
except Exception as e_loop:
|
||||||
|
if ui: ui.messageBox(f'Failed during export of: {task.get("base_filename", f"Task {i+1}")}\n'
|
||||||
|
f'Error: {traceback.format_exc()}\nSkipping this item.', 'Export Loop Error')
|
||||||
|
continue
|
||||||
|
|
||||||
|
progressDialog.hide()
|
||||||
|
|
||||||
|
if exported_count > 0:
|
||||||
|
success_msg = f'Successfully exported {exported_count} STEP files'
|
||||||
|
if exported_count < total_exports:
|
||||||
|
success_msg += f' (out of {total_exports} planned tasks).'
|
||||||
|
else:
|
||||||
|
success_msg += '.'
|
||||||
|
success_msg += f'\nFiles saved to:\n{exportFolder}'
|
||||||
|
if ui: ui.messageBox(success_msg, 'Export Complete')
|
||||||
|
elif total_exports > 0 and ui:
|
||||||
|
ui.messageBox(f'Export process ran for {total_exports} planned items, but 0 files were successfully exported. Please check warnings or naming conventions.', 'Export Result')
|
||||||
|
elif ui:
|
||||||
|
ui.messageBox('No files were exported as no tasks were identified or an early abort occurred.', 'Export Result')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if ui: ui.messageBox('Top-Level Script Failed Unexpectedly:\n{}'.format(traceback.format_exc()))
|
||||||
|
finally:
|
||||||
|
if 'progressDialog' in locals() and progressDialog and hasattr(progressDialog, 'isShowing') and progressDialog.isShowing:
|
||||||
|
try: progressDialog.hide()
|
||||||
|
except: pass
|
||||||
2569
scripts/ExportCoreComponents/ScriptIcon.svg
Normal file
|
After Width: | Height: | Size: 85 KiB |
12
scripts/ExportParametricShelf/ExportParametricShelf.manifest
Normal 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"
|
||||||
|
}
|
||||||
176
scripts/ExportParametricShelf/ExportParametricShelf.py
Normal 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()
|
||||||
6
scripts/ExportParametricShelf/README.md
Normal 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.
|
||||||
2569
scripts/ExportParametricShelf/ScriptIcon.svg
Normal file
|
After Width: | Height: | Size: 85 KiB |