10 Commits

Author SHA1 Message Date
kellervater
099b754156 feat: jpg to png to svg 2025-09-15 22:49:48 +02:00
renovate[bot]
021cf7b78d chore: Configure Renovate (#8)
Welcome to [Renovate](https://redirect.github.com/renovatebot/renovate)!
This is an onboarding PR to help you understand and configure settings
before regular Pull Requests begin.

🚦 To activate Renovate, merge this Pull Request. To disable Renovate,
simply close this Pull Request unmerged.



---

### Configuration Summary

Based on the default config's presets, Renovate will:

  - Start dependency updates only once this onboarding PR is merged
  - Hopefully safe environment variables to allow users to configure.
  - Show all Merge Confidence badges for pull requests.
  - Enable Renovate Dependency Dashboard creation.
- Use semantic commit type `fix` for dependencies and `chore` for all
others if semantic commits are in use.
- Ignore `node_modules`, `bower_components`, `vendor` and various
test/tests (except for nuget) directories.
  - Group known monorepo packages together.
  - Use curated list of recommended non-monorepo package groupings.
- Show only the Age and Confidence Merge Confidence badges for pull
requests.
  - Apply crowd-sourced package replacement rules.
  - Apply crowd-sourced workarounds for known problems with packages.

🔡 Do you want to change how Renovate upgrades your dependencies? Add
your custom config to `renovate.json` in this branch. Renovate will
update the Pull Request description the next time it runs.

---

### What to Expect

It looks like your repository dependencies are already up-to-date and no
Pull Requests will be necessary right away.

---

 Got questions? Check out Renovate's
[Docs](https://docs.renovatebot.com/), particularly the Getting Started
section.
If you need any further assistance then you can also [request help
here](https://redirect.github.com/renovatebot/renovate/discussions).

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/kellervater/homeracker).


<!--renovate-config-hash:e80b4e42a3043bc12fa0640db4bac392d2bf770acf841360d7c8ceeeac2ec1a9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-24 11:48:31 +02:00
kellervater
d9eac222e4 feat(copilot): added 1st instruction draft 2025-08-23 13:58:00 +02:00
kellervater
4c7c8f2dc3 feat: gridfinity baseplate 2025-05-18 16:06:39 +02:00
kellervater
27213d9d50 feat: core export script 2025-05-10 11:20:13 +02:00
Patrick Pötz
a1c8d11b6a feat: wallmount (#14)
## 🧰 Wallmount for HomeRacker
A new utility for your modular setup!

With this accessory, you can mount a HomeRacker frame to a wall, the
floor — or even the ceiling if you're feeling adventurous.

### 🔧 Customizable Bore Holes
You can choose between:
* Countersunk Holes
* Flathead Holes

For both options, you can specify the shaft diameter to match your
screws.
For the countersunk option, you can also set the countersink angle — the
angle at which the screw head tapers.
By default, this is set to 90°. 📐
2025-05-06 19:54:03 +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
15 changed files with 3360 additions and 40 deletions

161
.github/copilot-instructions.md vendored Normal file
View 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.

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
*.3mf *.3mf
*.stl *.stl
.vscode .vscode
bin/OpenSCAD-*/

View File

@@ -22,8 +22,8 @@ LOCK_PIN_EDGE_DISTANCE=5.5; // mm
/* [Device Measurements] */ /* [Device Measurements] */
// Width of the device in mm. Will determine how far apart the actual mounts are in width. // Width of the device in mm. Will determine how far apart the actual mounts are in width.
device_width=100 // [20:0.1:500] device_width=100; // [20:0.1:500]
; // TODO test for zero cube // TODO test for zero cube
// Depth of the device in mm. Will determine how far apart the actual mounts are in depth. // Depth of the device in mm. Will determine how far apart the actual mounts are in depth.
device_depth=99; // [54:0.1:400] device_depth=99; // [54:0.1:400]
// Height of the device in mm. Will determine how far apart the actual mounts are in height. // Height of the device in mm. Will determine how far apart the actual mounts are in height.

View 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);

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View 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!")

View 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
View 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.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 687 KiB

6
renovate.json Normal file
View File

@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}

View 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"
}

View 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

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 85 KiB