first commit
Some checks failed
Test / pre-commit (push) Has been cancelled

This commit is contained in:
Arne Moerman
2024-12-15 18:59:19 +01:00
parent d0cfab6622
commit 8bf0f36c35
87 changed files with 3719 additions and 0 deletions

34
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Test
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
jobs:
Test:
name: pre-commit
runs-on: ubuntu-latest
steps:
- name: 💾 Check out repository
uses: actions/checkout@v3
- name: 🪝 Cache pre-commit hooks
uses: actions/cache@v3
with:
path: ~/.cache/pre-commit
key: "pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml')
}}"
- name: ✨ Install pre-commit
shell: bash
run: python3 -m pip install pre-commit
- name: 🔥 Test
run: pre-commit run --show-diff-on-failure --all-files
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

12
.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
ignore/
stl/
batch/
site/
/*.json
# From https://github.com/github/gitignore/blob/main/Python.gitignore
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

7
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,7 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-merge-conflict
- id: end-of-file-fixer
- id: trailing-whitespace

46
LICENSE Normal file
View File

@@ -0,0 +1,46 @@
MIT License
Copyright (c) 2023 Kenneth Hodson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This repository is based on Gridfinity:
MIT License
Copyright (c) 2023 Zachary Freedman and Voidstar Lab LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

41
docs/baseplates.md Normal file
View File

@@ -0,0 +1,41 @@
# gridfinity-rebuilt-baseplate
To round out the rebuilt catalog, we have baseplates. Originally, baseplates were not included as there was not anything really interesting to parameterize, unlike with bins. However, upon closer inspection, there is a couple things that would make peoples' lives easier. For instance, what is called here the "skeletonized" style, in that the baseplate is still thick to allow for magnets, but the center portion is removed. Additionally, being able to generate spacers for the sides of the bases, so that they fit perfectly inside your drawers.
![Bin](images/baseplate.gif)
## Script Parameters
Parameter | Range | Description
--- | ----- | ---
gridx | { n>=0 \| n∈R } | number of bases along the x-axis<br> If set to zero, will fill with as many <br>bases that fit within `distancex`
gridy | { n>=0 \| n∈R } | number of bases along the y-axis<br> If set to zero, will fill with as many <br>bases that fit within `distancey`
length | { n>0 \| n∈R } | length of one unit of the base. <br> default: 42 (The Answer to the Ultimate Question of Life, <br>the Universe, and Everything.)
distancex | { n>0 \| n∈R } | minimum length of baseplate along x <br>alternatively, how large is the drawer along x <br>(leave zero to ignore)
distancey | { n>0 \| n∈R } | minimum length of baseplate along y <br>alternatively, how large is the drawer along y <br>(leave zero to ignore)
fitx | { -1<=n<=1 \| n∈R } | alignment factor for extra space along x axis
fity | { -1<=n<=1 \| n∈R } | alignment factor for extra space along y axis
style_plate | { 0, 1, 2 } | the style of baseplate <br> • (0) thin (minimum material, only outline) <br> • (1) weighted (thick with space for tire iron) <br> • (2) skeletonized (thick, center hollowed)
enable_magnet | boolean | toggle hole for magnet on top
style_hole | { 0, 1, 2 } | the style of holes underneath the baseplate, if applicable <br> • (0) none <br> • (1) countersink <br> • (2) counterbore
## Modules
### gridfinityBaseplate
Generates a baseplate to use with gridfinity bins.
** `gridfinityBaseplate(gridx, gridy, length, distancex, distancey, style_plate, enable_magnet, style_hole)` **
Parameter | Range | Description
--- | ----- | ---
gridx | { n>=0 \| n∈R } | number of bases along the x-axis<br> If set to zero, will fill with as many <br>bases that fit within `distancex`
gridy | { n>=0 \| n∈R } | number of bases along the y-axis<br> If set to zero, will fill with as many <br>bases that fit within `distancey`
length | { n>0 \| n∈R } | length of one unit of the base. <br> default: 42 (The Answer to the Ultimate Question of Life, <br>the Universe, and Everything.)
distancex | { n>0 \| n∈R } | minimum length of baseplate along x <br>alternatively, how large is the drawer along x <br>(leave zero to ignore)
distancey | { n>0 \| n∈R } | minimum length of baseplate along y <br>alternatively, how large is the drawer along y <br>(leave zero to ignore)
fitx | { -1<=n<=1 \| n∈R } | alignment factor for extra space along x axis
fity | { -1<=n<=1 \| n∈R } | alignment factor for extra space along y axis
style_plate | { 0, 1, 2 } | the style of baseplate <br> • (0) thin (minimum material, only outline) <br> • (1) weighted (thick with space for tire iron) <br> • (2) skeletonized (thick, center hollowed)
enable_magnet | boolean | toggle hole for magnet on top
style_hole | { 0, 1, 2 } | the style of holes underneath the baseplate, if applicable <br> • (0) none <br> • (1) countersink <br> • (2) counterbore

173
docs/bins.md Normal file
View File

@@ -0,0 +1,173 @@
# gridfinity-rebuilt-bins
Generates stock bins, with a great number of variations.
![Bin](images/custom_dimension.gif)
## Script Parameters
Parameter | Range | Description
--- | ----- | ---
gridx | { n>0 \| n∈R } | number of bases along the x-axis
gridy | { n>0 \| n∈R } | number of bases along the y-axis
gridz | { n>0 \| n∈R } | bin height. See bin height information and "gridz_define" below.
length | { n>0 \| n∈R } | length of one unit of the base. <br> default: 42 (The Answer to the Ultimate Question of Life, <br>the Universe, and Everything.)
divx | { n>0 \| n∈Z } | number of compartments along X
divy | { n>0 \| n∈Z } | number of compartments along Y
scoop | n>0 \| n∈R | controls the fillet on the bottom of the compartment for easy <br> item removal. 0 is disabled, 1 is full, any other real number will <br> scale from full.
enable_zsnap | boolean | automatically snap the bin size to the nearest 7mm increment. <br> default: true
style_lip | {0, 1, 2} | if you are not stacking the bin, you can disable the top lip <br> to save space. <br> • (0) Regular lip <br> • (1) Subtract lip to save space <br> • (2) Disable lip while retaining height
gridz_define | { n>0 \| n∈R } | determine what the variable "gridz" applies to based on <br> your use case. default: 0. <br> • (0) gridz is the height in # of 7mm increments (Zack) <br> • (1) gridz is the internal height in millimeters <br> • (2) gridz is the overall external height of the bin in millimeters
height_internal | { n>0 \| n∈R } | height of the internal block. <br> Can be lower than bin height to save filament on custom bins. <br> default of 0 means use the calculated height.
style_tab | { 0, 1, 2, 3, 4, 5 } | how the tabs for labels are generated. <br> • (0) Full tabs across the entire compartment <br> • (1) automatic tabs <br> - left aligned tabs on the left edge<br> - right aligned tabs on right edge<br> - center tabs otherwise <br> • (2) left aligned tabs <br> • (3) center aligned tabs <br> • (4) right aligned tabs <br> • (5) no tabs
style_hole | { 0, 1, 2, 3 } | the style of holes in the bases <br> • (0) No holes <br> • (1) Magnet holes only <br> • (2) Magnet and screw holes - no printable slit <br> • (3) Magnet and screw holes - with printable slit
div_base_x | { n>=0 \| n∈Z } | number of divisions per 1 unit of base along the X axis. <br>(default 1, only use integers. <br>0 means automatically guess the division)
div_base_y | { n>=0 \| n∈Z } | number of divisions per 1 unit of base along the Y axis. <br>(default 1, only use integers. <br>0 means automatically guess the division)
<br>
## Modules
---
### gridfinityInit
Initializes the top part of the bin (walls and solid section). All bins have to use this module, and have the compartments cut out from it.
** `gridfinityInit (gridx, gridy, height, height_internal, length)` **
Parameter | Range | Description
--- | ----- | ---
gridx | { n>0 \| n∈R } | number of bases along the x-axis
gridy | { n>0 \| n∈R } | number of bases along the y-axis
height | { n>0 \| n∈R } | height of the bin, in millimeters (but not exactly). <br> See the `height()` function for more info.
height_internal | { n>0 \| n∈R } | height of the internal block. <br> Can be lower than bin height to save filament on custom bins. <br> default of 0 means use the calculated height.
length | { n>0 \| n∈R } | length of one unit of the base. <br> default: 42 (The Answer to the Ultimate Question of Life, <br>the Universe, and Everything.)
style_lip | {0, 1, 2} | if you are not stacking the bin, you can disable the top lip <br> to save space. <br> • (0) Regular lip <br> • (1) Subtract lip to save space <br> • (2) Disable lip while retaining height
```
// Example: generate a 3x3x6 bin with a 42mm unit size
gridfinityInit(3, 3, height(6), 0, 42) {
cutEqual(n_divx = 3, n_divy = 3, style_tab = 0, scoop_weight = 1);
}
```
---
### height
Calculates the proper height for bins.
** `height (gridz, gridz_define, style_lip, enable_zsnap)` **
Parameter | Range | Description
--- | ----- | ---
gridz | { n>0 \| n∈R } | bin height. See bin height information and "gridz_define" below.
gridz_define | { n>0 \| n∈R } | determine what the variable "gridz" applies to based on <br> your use case. default: 0. <br> • (0) gridz is the height in # of 7mm increments (Zack) <br> • (1) gridz is the internal height in millimeters <br> • (2) gridz is the overall external height of the bin in millimeters
style_lip | {0, 1, 2} | if you are not stacking the bin, you can disable the top lip <br> to save space. <br> • (0) Regular lip <br> • (1) Subtract lip to save space <br> • (2) Disable lip while retaining height
enable_zsnap | boolean | automatically snap the bin size to the nearest 7mm increment. <br> default: true
```
// Example: height for a 6 unit high bin
height(6);
// Example: height for a bin that can fit (at maximum) a 30mm high object inside
height(30, 1, 0, false);
```
---
### gridfinityBase
Generates the bases for bins. Has various different hole styles, and can be subdivided.
** `gridfinityBase (gridx, gridy, length, div_base_x, div_base_y, style_hole)` **
Parameter | Range | Description
--- | ----- | ---
gridx | { n>0 \| n∈R } | number of bases along the x-axis
gridy | { n>0 \| n∈R } | number of bases along the y-axis
length | { n>0 \| n∈R } | length of one unit of the base. default: 42
div_base_x | { n>=0 \| n∈Z } | number of divisions per 1 unit of base along the X axis. <br>(default 1, only use integers. 0 means automatically guess the division)
div_base_y | { n>=0 \| n∈Z } | number of divisions per 1 unit of base along the Y axis. <br>(default 1, only use integers. 0 means automatically guess the division)
style_hole | { 0, 1, 2, 3 } | the style of holes in the bases <br> • (0) No holes <br> • (1) Magnet holes only <br> • (2) Magnet and screw holes - no printable slit <br> • (3) Magnet and screw holes - with printable slit
```
// Example: generate a 3x3 base with a 42mm unit size and clean magnet holes
gridfinityBase(3, 3, 42, 0, 0, 1);
```
---
### cutEqual
Generates the "traditional" bin cutters. It is a utility function that creates evenly distributed compartments.
** `cutEqual (n_divx, n_divy, style_tab, scoop_weight)` **
Parameter | Range | Description
--- | ----- | ---
n_divx | { n>0 \| n∈Z } | number of compartments along X
n_divy | { n>0 \| n∈Z } | number of compartments along Y
style_tab | { 0, 1, 2, 3, 4, 5 } | how the tabs for labels are generated. <br> • (0) Full tabs across the entire compartment <br> • (1) automatic tabs <br> - left aligned tabs on the left edge<br> - right aligned tabs on right edge<br> - center tabs otherwise <br> • (2) left aligned tabs <br> • (3) center aligned tabs <br> • (4) right aligned tabs <br> • (5) no tabs
scoop | n>0 \| n∈R | controls the fillet on the bottom of the compartment for easy <br> item removal. 0 is disabled, 1 is full, any other real number will <br> scale from full.
```
// Example: this generates 9 compartments in a 3x3 grid, and all compartments have a full tab and a scoop
gridfinityInit(3, 3, height(6), 0, 42) {
cutEqual(n_divx = 3, n_divy = 3, style_tab = 0, scoop_weight = 1);
}
```
---
### cut
Cuts a single compartment into the bin at the provided location with the provided attributes. The coordinate system for compartments originates (0,0) at the bottom left corner of the bin, where 1 unit is the length of 1 base. Positive X and positive Y are in the same direction as the global coordinate system.
** `cut (x, y, w, h, t, s)` **
Parameter | Range | Description
--- | ----- | ---
x | { n>=0 \| n∈R } | X coordinate of the compartment <br>(position of left edge of compartment)
y | { n>=0 \| n∈R } | Y coordinate of the compartment <br>(position of bottom edge of compartment)
w | { n>0 \| n∈R } | Width of the compartment, in base units <br>(1 unit = 1 `length`)
h | { n>0 \| n∈R } | Height of the compartment, in base units <br>(1 unit = 1 `length`)
t | { 0, 1, 2, 3, 4, 5 } | how the tabs for labels are generated for this compartment. <br> • (0) Full tabs across the entire compartment <br> • (1) automatic tabs <br> - left aligned tabs on the left edge<br> - right aligned tabs on right edge<br> - center tabs otherwise <br> • (2) left aligned tabs <br> • (3) center aligned tabs <br> • (4) right aligned tabs <br> • (5) no tabs
s | n>0 \| n∈R | controls the fillet on the bottom of the compartment for easy <br> item removal. 0 is disabled, 1 is full, any other real number will <br> scale from full, only for this specific compartment
```
// Example:
// this cuts two compartments that are both 1 wide and 2 high.
// One is on the bottom left, and the other is at the top right.
gridfinityInit(3, 3, height(6), 0, 42) {
cut(0, 0, 1, 2, 0, 1);
cut(2, 1, 1, 2, 0, 1);
}
```
---
### cut_move
Moves all of its children from the global origin to the center of the area that a compartment would normally fill, and uses them to cut from the bin. This allows you to easily make custom cutouts in the bin.
** `cut_move (x, y, w, h)` **
Parameter | Range | Description
--- | ----- | ---
x | { n>=0 \| n∈R } | X coordinate of the area (position of left edge)
y | { n>=0 \| n∈R } | Y coordinate of the area (position of bottom edge)
w | { n>0 \| n∈R } | Width of the area, in base units (1 unit = 1 `length`)
h | { n>0 \| n∈R } | Height of the area, in base units (1 unit = 1 `length`)
```
// Example:
// cuts a cylindrical hole of radius 5
// hole center is located 1/2 units from the right edge of the bin, and 1 unit from the top
gridfinityInit(3, 3, height(6), 0, 42) {
cut_move(x=2, y=1, w=1, h=2) {
cylinder(r=5, h=100, center=true);
}
}
```

38
docs/constants.md Normal file
View File

@@ -0,0 +1,38 @@
# gridfinity-rebuilt-constants
This file contains dimensions that are critical to the constructiuon of the other models, but are not values that often need to be changed. Thus, they were outsourced to this file such that the rest of the files would retain parity. All values here have the same range, this is positive real numbers. Some can be zero, but that may result in strange or invalid geometry,
Parameter | Description
--- | ------
h_base | height of the base
r_base| outside rounded radius of bin
r_c1 | lower base chamfer "radius"
r_c2 | upper base chamfer "radius"
h_bot| bottom thiccness of bin
r_fo1| outside radii 1
r_fo2| outside radii 2
r_fo3 | outside radii 3
r_hole1| screw hole radius
r_hole2| magnet hole radius
d_hole| center-to-center distance between holes
h_hole| magnet hole depth
h_slit| slit depth (printer layer height)
r_f1| top edge fillet radius
r_f2 | internal fillet radius
d_div | width of divider between compartments
d_wall| minimum wall thickness
d_clear| tolerance fit factor
d_tabh| height of tab (yaxis, measured from inner wall)
d_tabw| maximum width of tab
a_tab| angle of tab
bp_h_bot| Baseplate bottom part height (part added with weigthed=true)
bp_cut_size| Baseplate bottom cutout rectangle size
bp_cut_depth| Baseplate bottom cutout rectangle depth
bp_rcut_width| Baseplate bottom cutout finger gap width
bp_rcut_length| Baseplate bottom cutout finger gap left
bp_rcut_depth| Baseplate bottom cutout finger gap depth
d_cs | countersink diameter for baseplate
r_skel| radius of cutout for skeletonized baseplate
r_cb| baseplate counterbore radius
h_cb| baseplate counterbore depth
h_skel | minimum baseplate thickness (when skeletonized)

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

BIN
docs/images/baseplate.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

BIN
docs/images/lite.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

BIN
docs/images/slicer_base.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

BIN
docs/images/slicer_bin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

BIN
docs/images/spin.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
docs/images/vase_base.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/images/vase_bottom.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
docs/images/vase_tabs.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

22
docs/index.md Normal file
View File

@@ -0,0 +1,22 @@
## Introduction
Gridfinity rebuilt aims to remake the brilliant Gridfinity project from [Zack Freedman](https://www.youtube.com/c/ZackFreedman/about) in a more robust and open-source way than the original Fusion 360 files. Many major CAD suites struggle with making parametric models constructed from linear patterns, due to changing fillet edges and seams. Thus, a pure mathematical approach using OpenSCAD can allow a single solution for all possible bin variants.
The [project](https://github.com/kennetek/gridfinity-rebuilt-openscad/) has expanded into more eccentric models that use modules from the original generator. This wiki aims to document these modules in great detail.
Models are generated subtractively. First, the solid bin and bases are constructed, and then the compartments and holes are removed. This allows for internal fillets that nearly match the originals. However, they are not exactly perfect. There are some fillets that are too small and too difficult to be worth implementing, as most printers do not have a high enough resolution for it to matter.
## Getting Started
For best results, use a version of OpenSCAD with the fast-csg feature. As of writing, this feature is only implemented in the [development snapshots](https://openscad.org/downloads.html). To enable the feature, go to Edit > Preferences > Features > fast-csg. This can speed up rendering from 10 minutes down to a couple of seconds, even for comically large bins. It is not a requirement to use development versions of OpenSCAD.
Most files will come ready-to-run, so parameters can be changed using OpenSCAD's built-in customizer window, and the bins will automatically generate. However, all modules are packed up nicely, so any module can be imported into other files or have more custom modifications beyond tweaking the parameters.
## Script Structure
* Information (Imports / Script Details)
* Parameters (Shown in Customizer)
* Implementation (Executing the Parameters)
* Construction (Script-Specific Modules and Constants)
* Examples
The two files which do not follow these conventions are `gridfinity-rebuilt-utility` and `gridfinity-rebuilt-constants`. These files are not meant to be exposed to the user, except for special requirements that the normal parameters cannot handle. Their respective wiki pages go into more depth.
**NOTE: This documentation is a work in progress, just like the rest of the repository, so parts may still be under construction.**

55
docs/lite.md Normal file
View File

@@ -0,0 +1,55 @@
# gridfinity-rebuilt-lite
Generates stock bins, but with a twist: the bases are hollow. This is unable to be implemented directly into `gridfinity-rebuilt-bins` due to its surprisingly complex construction. It couldn't be a toggle without causing the structure of the script to fundamentally change (and become more complex) so it was moved to a separate file. Notably, there are some parameters missing, as they are incompatible with the lite variation. Additionally, the complex geometry means rendering is fairly slow, and currently causes many CSG errors.
![Bin](images/lite.gif)
## Script Parameters
Parameter | Range | Description
--- | ----- | ---
gridx | { n>0 \| n∈R } | number of bases along the x-axis
gridy | { n>0 \| n∈R } | number of bases along the y-axis
gridz | { n>0 \| n∈R } | bin height. See bin height information and "gridz_define" below.
length | { n>0 \| n∈R } | length of one unit of the base. <br> default: 42 (The Answer to the Ultimate Question of Life, <br>the Universe, and Everything.)
divx | { n>0 \| n∈Z } | number of compartments along X
divy | { n>0 \| n∈Z } | number of compartments along Y
enable_zsnap | boolean | automatically snap the bin size to the nearest 7mm increment. <br> default: true
style_lip | {0, 1, 2} | if you are not stacking the bin, you can disable the top lip <br> to save space. <br> • (0) Regular lip <br> • (1) Subtract lip to save space <br> • (2) Disable lip while retaining height
gridz_define | { n>0 \| n∈R } | determine what the variable "gridz" applies to based on <br> your use case. default: 0. <br> • (0) gridz is the height in # of 7mm increments (Zack) <br> • (1) gridz is the internal height in millimeters <br> • (2) gridz is the overall external height of the bin in millimeters
style_tab | { 0, 1, 2, 3, 4, 5 } | how the tabs for labels are generated. <br> • (0) Full tabs across the entire compartment <br> • (1) automatic tabs <br> - left aligned tabs on the left edge<br> - right aligned tabs on right edge<br> - center tabs otherwise <br> • (2) left aligned tabs <br> • (3) center aligned tabs <br> • (4) right aligned tabs <br> • (5) no tabs
style_hole | { 0, 1, 2, 3 } | the style of holes in the bases <br> • (0) No holes <br> • (1) Magnet holes only <br> • (2) Magnet and screw holes - no printable slit <br> • (3) Magnet and screw holes - with printable slit
div_base_x | { n>=0 \| n∈Z } | number of divisions per 1 unit of base along the X axis. <br>(default 1, only use integers. <br>0 means automatically guess the division)
div_base_y | { n>=0 \| n∈Z } | number of divisions per 1 unit of base along the Y axis. <br>(default 1, only use integers. <br>0 means automatically guess the division)
<br>
## Modules
---
### gridfinityLite
Wrapper function that calls [`gridfinityInit`](bins.md#gridfinityInit) and [`gridfinityBase`](bins.md#gridfinityBase) functions (hence the sheer quantity of parameters). Like [`gridfinityInit`](bins.md#gridfinityInit), it uses its children as cutters for the compartments.
**`gridfinityLite(gridx, gridy, gridz, gridz_define, enable_lip, enable_zsnap, length, div_base_x, div_base_y, style_hole)`**
Parameter | Range | Description
--- | ----- | ---
gridx | { n>0 \| n∈R } | number of bases along the x-axis
gridy | { n>0 \| n∈R } | number of bases along the y-axis
gridz | { n>0 \| n∈R } | bin height. See bin height information and "gridz_define" below.
gridz_define | { n>0 \| n∈R } | determine what the variable "gridz" applies to based on <br> your use case. default: 0. <br> • (0) gridz is the height in # of 7mm increments (Zack) <br> • (1) gridz is the internal height in millimeters <br> • (2) gridz is the overall external height of the bin in millimeters
style_lip | {0, 1, 2} | if you are not stacking the bin, you can disable the top lip <br> to save space. <br> • (0) Regular lip <br> • (1) Subtract lip to save space <br> • (2) Disable lip while retaining height
enable_zsnap | boolean | automatically snap the bin size to the nearest 7mm increment. <br> default: true
length | { n>0 \| n∈R } | length of one unit of the base. <br> default: 42 (The Answer to the Ultimate Question of Life, <br>the Universe, and Everything.)
div_base_x | { n>=0 \| n∈Z } | number of divisions per 1 unit of base along the X axis. <br>(default 1, only use integers. <br>0 means automatically guess the division)
div_base_y | { n>=0 \| n∈Z } | number of divisions per 1 unit of base along the Y axis. <br>(default 1, only use integers. <br>0 means automatically guess the division)
style_hole | { 0, 1, 2, 3 } | the style of holes in the bases <br> • (0) No holes <br> • (1) Magnet holes only <br> • (2) Magnet and screw holes - no printable slit <br> • (3) Magnet and screw holes - with printable slit
```
// Example: generate a 3x3x6 bin with 2x2 compartments, that is hollow
gridfinityLite(gridx=3, gridy=3, gridz=6, gridz_define=0, enable_lip=true, enable_zsnap=false, length=42, div_base_x=0, div_base_y=0, style_hole=1) {
cutEqual(n_divx=2, n_divy=2, style_tab=1, enable_scoop = false);
}
```

75
docs/vase.md Normal file
View File

@@ -0,0 +1,75 @@
# gridfinity-spiral-vase
Some assembly required!
Adaptation of Gridfinity bins to work with spiral vase mode, as to save filament and print time. A big drawback of using vase mode for Gridfinity bins was that they would be very flimsy, and would lack the features that make Gridfinity such a good organizational tool. The goal of this implementation was to maintain the design philosophy while working under the constraints of vase mode.
![Bin](images/vase_dividers.gif)
![Bin](images/vase_base.gif)
## Instructions
Normal Gridfinity is impossible to convert to vase mode due to the geometry of the bases, meaning most existing vase mode Gridfinity models are limited to 1x1 bins. How this script gets around the impossible is to use two separate pieces. **The bin and bases must be printed separately, and then glued together to form the final bin.** While this is slightly more work, there is an added bonus to this method, as for larger bins you may not need every single grid slot to have a base, you only really need them on the corners, or the edges, with some in the middle for support. Using less bases saves filament and print time.
All parameters are global. The customizer has descriptions for all parameters. It is **essential** that the section *Printer Settings* matches your preferred slicer's settings, otherwise the model will not slice correctly. Additionally, you have to turn on the spiral vase parameter in your slicer. If you do not know what vase mode is, [this](https://www.youtube.com/watch?v=HZSFoFYpBaA) is a helpful video.
1. Change the *Printer Settings* parameters to match your slicer and printer settings.
2. Run the `gridfinityVaseBase()` module. This will generate a single spiral-capable base. Export as an STL file. You will need to print multiple of these, so it recommended to fill a base plate with them using the "complete individual objects" option (or equivilant) in your slicer. You only need to do this step initially, and then each time your printer settings change afterwards.
3. Change the bin parameters and run `gridfinityVase()` module to generate the main bin.
4. Glue bases to the bottom of the bin. I recommend superglue on the corners and the top of the magnet holes.
How your sliced files should look (cross section shown for 1x1 bin):
![Bin](images/slicer_bin.png)
![Bin](images/slicer_base.png)
Example sliced files can be found on the [Printables](https://www.printables.com/model/284371-spiral-vase-gridfinity-in-openscad) page.
## Statistics
Given how it has become a bit of a [challenge](https://www.printables.com/model/265271-gridfinity-lite-economical-plain-storage-bins) to reduce the weight and print time for these bins, here is a comparison for a large bin:
| Type | Weight | Time |
|--------------|-----------|------------|
Plain 4x2x6 Bin | 114.66g | 3h58m
Vase 4x2x6 Bin with 8 Bases | 68.31g | 2h27m
Vase 4x2x6 Bin with 4 Bases (only corners) | 56.43g | 1h59m
Clearly, vase mode is very quick and quite lightweight. However, this fundamentally means the bins will be weaker, so keep that in mind.
## Script Parameters
Parameter | Range | Description
--- | ----- | ---
type | { 0 , 1 } | generate the bin (0) or base (1)
nozzle | { n>0 \| n∈R } | extrusion width in slicer
layer | { n>0 \| n∈R } | size of layers in slicer
bottom_layer | { n>0 \| n∈Z } | number of layers on the bottom of the print
gridx | { n>0 \| n∈R } | number of bases along the x-axis
gridy | { n>0 \| n∈R } | number of bases along the y-axis
gridz | { n>0 \| n∈R } | bin height. See bin height information and <br> "gridz_define" below.
length | { n>0 \| n∈R } | length of one unit of the base. <br> default: 42 (The Answer to the Ultimate Question of <br>Life, the Universe, and Everything.)
n_divx | { n>0 \| n∈Z } | number of compartments along X
enable_holes | boolean | toggle holes inside the base
enable_zsnap | boolean | automatically snap the bin size to the nearest <br> 7mm increment. default: true
enable_lip | boolean | if you are not stacking the bin, you can disable the <br>top lip to save space. default: true
enable_scoop_chamfer | boolean | toggles the chamfer on bottom edge <br> for easy removal of items
enable_funnel | boolean | toggles funnel on back of tab. <br> acts as a finger grip and pour spout for small parts.
enable_inset | boolean | toggles an inset on the front of the bin. <br> adds strength when using scoop.
enable_pinch | boolean | toggles an outside pinch at the top lip of the bin. <br> adds strength.
gridz_define | { n>0 \| n∈R } | determine what the variable "gridz" applies to based on <br> your use case. default: 0. <br> • (0) gridz is the height in # of 7mm increments (Zack) <br> • (1) gridz is the internal height in mm <br> • (2) gridz is the overall external height of the bin in mm
style_tab | { 0, 1, 2, 3, 4, 5 } | how the tabs for labels are generated. <br> • (0) Full tabs across the entire compartment <br> • (1) automatic tabs <br> - left aligned tabs on the left edge<br> - right aligned tabs on right edge<br> - center tabs otherwise <br> • (2) left aligned tabs <br> • (3) center aligned tabs <br> • (4) right aligned tabs <br> • (5) no tabs
style_base | { 0, 1, 2, 3, 4} | specifies the locations for the "X" cutouts for bases. <br> • (0) all <br> • (1) corners <br> • (2) edges <br> • (3) automatic <br> • (4) none
a_tab | { n>0 \| n∈R } | angle of the tab
## Modules
### gridfinityVase
Generates the compartment section of the bin. No parameters as it uses the global parameters for construction.
**`gridfinityVase()`**
### gridfinityVaseBase
Generates the base section of the bin. No parameters as it uses the global parameters for construction. It only generates a single base, as all of them are the same. Use your slicer with the "outpuit individual objects" option to print a full sheet of these, or as many as you need to fit your bins.
**`gridfinityVaseBase()`**

218
generic-helpers.scad Normal file
View File

@@ -0,0 +1,218 @@
/**
* @file generic-helpers.scad
* @brief Generic Helper Functions. Not gridfinity specific.
*/
function clp(x,a,b) = min(max(x,a),b);
function is_even(number) = (number%2)==0;
/**
* @brief Create `square`, with rounded corners.
* @param size Same as `square`. See details for differences.
* @param radius Radius of the corners. 0 is the same as just calling `square`
* @param center Same as `square`.
* @details "size" accepts both the standard number or a 2d vector the same as `square`.
* However, if passed a 3d vector, this will apply a `linear_extrude` to the resulting shape.
*/
module rounded_square(size, radius, center = false) {
assert(is_num(size) ||
(is_list(size) && (
(len(size) == 2 && is_num(size.x) && is_num(size.y)) ||
(len(size) == 3 && is_num(size.x) && is_num(size.y) && is_num(size.z))
))
);
assert(is_num(radius) && radius >= 0 && is_bool(center));
// Make sure something is produced.
if (is_num(size)) {
assert((size/2) > radius);
} else {
assert((size.x/2) > radius && (size.y/2 > radius));
if (len(size) == 3) {
assert(size.z > 0);
}
}
if (is_list(size) && len(size) == 3) {
linear_extrude(size.z)
_internal_rounded_square_2d(size, radius, center);
} else {
_internal_rounded_square_2d(size, radius, center);
}
}
/**
* @brief Internal module. Do not use. May be changed/removed at any time.
*/
module _internal_rounded_square_2d(size, radius, center) {
diameter = 2*radius;
if (is_list(size)) {
offset(radius)
square([size.x-diameter, size.y-diameter], center = center);
} else {
offset(radius)
square(size-diameter, center = center);
}
}
/**
* @deprecated Use rounded_square(...)
*/
module rounded_rectangle(length, width, height, rad) {
rounded_square([length, width, height], rad, center=true);
}
module copy_mirror(vec=[0,1,0]) {
children();
if (vec != [0,0,0])
mirror(vec)
children();
}
module pattern_linear(x = 1, y = 1, sx = 0, sy = 0) {
yy = sy <= 0 ? sx : sy;
translate([-(x-1)*sx/2,-(y-1)*yy/2,0])
for (i = [1:ceil(x)])
for (j = [1:ceil(y)])
translate([(i-1)*sx,(j-1)*yy,0])
children();
}
module pattern_circular(n=2) {
for (i = [1:n])
rotate(i*360/n)
children();
}
/**
* @brief Unity (no change) affine transformation matrix.
* @details For use with multmatrix transforms.
*/
unity_matrix = [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
/**
* @brief Get the magnitude of a 2d or 3d vector
* @param vector A 2d or 3d vectorm
* @returns Magnitude of the vector.
*/
function vector_magnitude(vector) =
sqrt(vector.x^2 + vector.y^2 + (len(vector) == 3 ? vector.z^2 : 0));
/**
* @brief Convert a 2d or 3d vector into a unit vector
* @returns The unit vector. Where total magnitude is 1.
*/
function vector_as_unit(vector) = vector / vector_magnitude(vector);
/**
* @brief Convert a 2d vector into an angle.
* @details Just a wrapper around atan2.
* @param A 2d vectorm
* @returns Angle of the vector.
*/
function atanv(vector) = atan2(vector.y, vector.x);
function _affine_rotate_x(angle_x) = [
[1, 0, 0, 0],
[0, cos(angle_x), -sin(angle_x), 0],
[0, sin(angle_x), cos(angle_x), 0],
[0, 0, 0, 1]
];
function _affine_rotate_y(angle_y) = [
[cos(angle_y), 0, sin(angle_y), 0],
[0, 1, 0, 0],
[-sin(angle_y), 0, cos(angle_y), 0],
[0, 0, 0, 1]
];
function _affine_rotate_z(angle_z) = [
[cos(angle_z), -sin(angle_z), 0, 0],
[sin(angle_z), cos(angle_z), 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
/**
* @brief Affine transformation matrix equivalent of `rotate`
* @param angle_vector @see `rotate`
* @details Equivalent to `rotate([0, angle, 0])`
* @returns An affine transformation matrix for use with `multmatrix()`
*/
function affine_rotate(angle_vector) =
_affine_rotate_z(angle_vector.z) * _affine_rotate_y(angle_vector.y) * _affine_rotate_x(angle_vector.x);
/**
* @brief Affine transformation matrix equivalent of `translate`
* @param vector @see `translate`
* @returns An affine transformation matrix for use with `multmatrix()`
*/
function affine_translate(vector) = [
[1, 0, 0, vector.x],
[0, 1, 0, vector.y],
[0, 0, 1, vector.z],
[0, 0, 0, 1]
];
/**
* @brief Create a rectangle with rounded corners by sweeping a 2d object along a path.
* Centered on origin.
*/
module sweep_rounded(width=10, length=10) {
assert(width > 0 && length > 0);
half_width = width/2;
half_length = length/2;
path_points = [
[-half_width, half_length], //Start
[half_width, half_length], // Over
[half_width, -half_length], //Down
[-half_width, -half_length], // Back over
[-half_width, half_length] // Up to start
];
path_vectors = [
path_points[1] - path_points[0],
path_points[2] - path_points[1],
path_points[3] - path_points[2],
path_points[4] - path_points[3],
];
// These contain the translations, but not the rotations
// OpenSCAD requires this hacky for loop to get accumulate to work!
first_translation = affine_translate([path_points[0].y, 0,path_points[0].x]);
affine_translations = concat([first_translation], [
for (i = 0, a = first_translation;
i < len(path_vectors);
a=a * affine_translate([path_vectors[i].y, 0, path_vectors[i].x]), i=i+1)
a * affine_translate([path_vectors[i].y, 0, path_vectors[i].x])
]);
// Bring extrusion to the xy plane
affine_matrix = affine_rotate([90, 0, 90]);
walls = [
for (i = [0 : len(path_vectors) - 1])
affine_matrix * affine_translations[i]
* affine_rotate([0, atanv(path_vectors[i]), 0])
];
union()
{
for (i = [0 : len(walls) - 1]){
multmatrix(walls[i])
linear_extrude(vector_magnitude(path_vectors[i]))
children();
// Rounded Corners
multmatrix(walls[i] * affine_rotate([-90, 0, 0]))
rotate_extrude(angle = 90, convexity = 4)
children();
}
}
}

View File

@@ -0,0 +1,406 @@
include <gridfinity-rebuilt-utility.scad>
include <standard.scad>
use <gridfinity-rebuilt-holes.scad>
// ===== INFORMATION ===== //
/*
IMPORTANT: rendering will be better for analyzing the model if fast-csg is enabled. As of writing, this feature is only available in the development builds and not the official release of OpenSCAD, but it makes rendering only take a couple seconds, even for comically large bins. Enable it in Edit > Preferences > Features > fast-csg
https://github.com/kennetek/gridfinity-rebuilt-openscad
*/
// ===== PARAMETERS ===== //
/* [Setup Parameters] */
$fa = 8;
$fs = 0.25;
/* [General Settings] */
// number of bases along x-axis
gridx = 1;
// number of bases along y-axis
gridy = 1;
/* [Screw Together Settings - Defaults work for M3 and 4-40] */
// screw diameter
d_screw = 3.35;
// screw head diameter
d_screw_head = 5;
// screw spacing distance
screw_spacing = .5;
// number of screws per grid block
n_screws = 1; // [1:3]
/* [Fit to Drawer] */
// minimum length of baseplate along x (leave zero to ignore, will automatically fill area if gridx is zero)
distancex = 0;
// minimum length of baseplate along y (leave zero to ignore, will automatically fill area if gridy is zero)
distancey = 0;
// where to align extra space along x
fitx = 0; // [-1:0.1:1]
// where to align extra space along y
fity = 0; // [-1:0.1:1]
/* [Styles] */
// baseplate styles
style_plate = 3; // [0: thin, 1:weighted, 2:skeletonized, 3: screw together, 4: screw together minimal]
// hole styles
style_hole = 0; // [0:none, 1:countersink, 2:counterbore]
/* [Magnet Hole] */
// Baseplate will have holes for 6mm Diameter x 2mm high magnets.
enable_magnet = true;
// Magnet holes will have crush ribs to hold the magnet.
crush_ribs = true;
// Magnet holes will have a chamfer to ease insertion.
chamfer_holes = true;
hole_options = bundle_hole_options(refined_hole=false, magnet_hole=enable_magnet, screw_hole=false, crush_ribs=crush_ribs, chamfer=chamfer_holes, supportless=false);
// ===== IMPLEMENTATION ===== //
color("tomato")
gridfinityBaseplate([gridx, gridy], l_grid, [distancex, distancey], style_plate, hole_options, style_hole, [fitx, fity]);
// ===== CONSTRUCTION ===== //
/**
* @brief Create a baseplate.
* @param grid_size_bases Number of Gridfinity bases.
* 2d Vector. [x, y].
* Set to [0, 0] to auto calculate using min_size_mm.
* @param length X,Y size of a single Gridfinity base.
* @param min_size_mm Minimum size of the baseplate. [x, y]
* Extra space is filled with solid material.
* Enables "Fit to Drawer."
* @param sp Baseplate Style
* @param hole_options
* @param sh Style of screw hole allowing the baseplate to be mounted to something.
* @param fit_offset Determines where padding is added.
*/
module gridfinityBaseplate(grid_size_bases, length, min_size_mm, sp, hole_options, sh, fit_offset = [0, 0]) {
assert(is_list(grid_size_bases) && len(grid_size_bases) == 2,
"grid_size_bases must be a 2d list");
assert(is_list(min_size_mm) && len(min_size_mm) == 2,
"min_size_mm must be a 2d list");
assert(is_list(fit_offset) && len(fit_offset) == 2,
"fit_offset must be a 2d list");
assert(grid_size_bases.x > 0 || min_size_mm.x > 0,
"Must have positive x grid amount!");
assert(grid_size_bases.y > 0 || min_size_mm.y > 0,
"Must have positive y grid amount!");
additional_height = calculate_offset(sp, hole_options[1], sh);
// Final height of the baseplate. In mm.
baseplate_height_mm = additional_height + BASEPLATE_LIP_MAX.y;
// Final size in number of bases
grid_size = [for (i = [0:1])
grid_size_bases[i] == 0 ? floor(min_size_mm[i]/length) : grid_size_bases[i]];
// Final size of the base before padding. In mm.
grid_size_mm = concat(grid_size * length, [baseplate_height_mm]);
// Final size, including padding. In mm.
size_mm = [
max(grid_size_mm.x, min_size_mm.x),
max(grid_size_mm.y, min_size_mm.y),
baseplate_height_mm
];
// Amount of padding needed to fit to a specific drawer size. In mm.
padding_mm = size_mm - grid_size_mm;
is_padding_needed = padding_mm != [0, 0, 0];
//Convert the fit offset to percent of how much will be added to the positive axes.
// -1 : 1 -> 0 : 1
fit_percent_positive = [for (i = [0:1]) (fit_offset[i] + 1) / 2];
padding_start_point = -grid_size_mm/2 -
[
padding_mm.x * (1 - fit_percent_positive.x),
padding_mm.y * (1 - fit_percent_positive.y),
-grid_size_mm.z/2
];
corner_points = [
padding_start_point + [size_mm.x, size_mm.y, 0],
padding_start_point + [0, size_mm.y, 0],
padding_start_point,
padding_start_point + [size_mm.x, 0, 0],
];
echo(str("Number of Grids per axes (X, Y)]: ", grid_size));
echo(str("Final size (in mm): ", size_mm));
if (is_padding_needed) {
echo(str("Padding +X (in mm): ", padding_mm.x * fit_percent_positive.x));
echo(str("Padding -X (in mm): ", padding_mm.x * (1 - fit_percent_positive.x)));
echo(str("Padding +Y (in mm): ", padding_mm.y * fit_percent_positive.y));
echo(str("Padding -Y (in mm): ", padding_mm.y * (1 - fit_percent_positive.y)));
}
screw_together = sp == 3 || sp == 4;
minimal = sp == 0 || sp == 4;
difference() {
union() {
// Baseplate itself
pattern_linear(grid_size.x, grid_size.y, length) {
// Single Baseplate piece
difference() {
if (minimal) {
square_baseplate_lip(additional_height);
} else {
solid_square_baseplate(additional_height);
}
// Bottom/through pattern for the solid baseplates.
if (sp == 1) {
cutter_weight();
} else if (sp == 2 || sp == 3) {
translate([0,0,-TOLLERANCE])
linear_extrude(additional_height + (2 * TOLLERANCE))
profile_skeleton();
}
// Add holes to the solid baseplates.
hole_pattern(){
// Manget hole
translate([0, 0, additional_height+TOLLERANCE])
mirror([0, 0, 1])
block_base_hole(hole_options);
translate([0,0,-TOLLERANCE])
if (sh == 1) {
cutter_countersink();
} else if (sh == 2) {
cutter_counterbore();
}
}
}
}
// Padding
if (is_padding_needed) {
render()
difference() {
translate(padding_start_point)
cube(size_mm);
translate([
-grid_size_mm.x/2,
-grid_size_mm.y/2,
0
])
cube(grid_size_mm);
}
}
}
// Round the outside corners (Including Padding)
for(i = [0:len(corner_points) - 1]) {
point = corner_points[i];
translate([
point.x + (BASEPLATE_OUTSIDE_RADIUS * -sign(point.x)),
point.y + (BASEPLATE_OUTSIDE_RADIUS * -sign(point.y)),
0
])
rotate([0, 0, i*90])
square_baseplate_corner(additional_height, true);
}
if (screw_together) {
translate([0, 0, additional_height/2])
cutter_screw_together(grid_size.x, grid_size.y, length);
}
}
}
function calculate_offset(style_plate, enable_magnet, style_hole) =
assert(style_plate >=0 && style_plate <=4)
let (screw_together = style_plate == 3 || style_plate == 4)
screw_together ? 6.75 :
style_plate==0 ? 0 :
style_plate==1 ? bp_h_bot :
calculate_offset_skeletonized(enable_magnet, style_hole);
function calculate_offset_skeletonized(enable_magnet, style_hole) =
h_skel + (enable_magnet ? MAGNET_HOLE_DEPTH : 0) +
(
style_hole==0 ? d_screw :
style_hole==1 ? BASEPLATE_SCREW_COUNTERSINK_ADDITIONAL_RADIUS : // Only works because countersink is at 45 degree angle!
BASEPLATE_SCREW_COUNTERBORE_HEIGHT
);
module cutter_weight() {
union() {
linear_extrude(bp_cut_depth*2,center=true)
square(bp_cut_size, center=true);
pattern_circular(4)
translate([0,10,0])
linear_extrude(bp_rcut_depth*2,center=true)
union() {
square([bp_rcut_width, bp_rcut_length], center=true);
translate([0,bp_rcut_length/2,0])
circle(d=bp_rcut_width);
}
}
}
module hole_pattern(){
pattern_circular(4)
translate([l_grid/2-d_hole_from_side, l_grid/2-d_hole_from_side, 0]) {
render();
children();
}
}
module cutter_countersink(){
screw_hole(SCREW_HOLE_RADIUS + d_clear, 2*h_base,
false, BASEPLATE_SCREW_COUNTERSINK_ADDITIONAL_RADIUS);
}
module cutter_counterbore(){
screw_radius = SCREW_HOLE_RADIUS + d_clear;
counterbore_height = BASEPLATE_SCREW_COUNTERBORE_HEIGHT + 2*LAYER_HEIGHT;
union(){
cylinder(h=2*h_base, r=screw_radius);
difference() {
cylinder(h = counterbore_height, r=BASEPLATE_SCREW_COUNTERBORE_RADIUS);
make_hole_printable(screw_radius, BASEPLATE_SCREW_COUNTERBORE_RADIUS, counterbore_height);
}
}
}
/**
* @brief Added or removed from the baseplate to square off or round the corners.
* @param height Baseplate's height, excluding lip and clearance height.
* @param subtract If the corner should be scaled to allow subtraction.
*/
module square_baseplate_corner(height=0, subtract=false) {
assert(height >= 0);
assert(is_bool(subtract));
subtract_ammount = subtract ? TOLLERANCE : 0;
translate([0, 0, -subtract_ammount])
linear_extrude(height + BASEPLATE_LIP_MAX.y + (2 * subtract_ammount))
difference() {
square(BASEPLATE_OUTSIDE_RADIUS + subtract_ammount , center=false);
// TOLLERANCE needed to prevent a gap
circle(r=BASEPLATE_OUTSIDE_RADIUS - TOLLERANCE);
}
}
/**
* @brief Outer edge/lip of the baseplate.
* @details Includes clearance to ensure the base touches the lip
* instead of the bottom.
* @param height Baseplate's height excluding lip and clearance height.
* @param width How wide a single baseplate is. Only set if deviating from the standard!
* @param length How long a single baseplate is. Only set if deviating from the standard!
*/
module baseplate_lip(height=0, width=l_grid, length=l_grid) {
assert(height >= 0);
// How far, in the +x direction,
// the lip needs to be from it's [0, 0] point
// such that when swept by 90 degrees to produce a corner,
// the outside edge has the desired radius.
translation_x = BASEPLATE_OUTSIDE_RADIUS - BASEPLATE_LIP_MAX.x;
additional_height = height + BASEPLATE_CLEARANCE_HEIGHT;
sweep_rounded(width-2*BASEPLATE_OUTSIDE_RADIUS, length-2*BASEPLATE_OUTSIDE_RADIUS)
translate([translation_x, additional_height, 0])
polygon(concat(BASEPLATE_LIP, [
[0, -additional_height],
[BASEPLATE_LIP_MAX.x, -additional_height],
[BASEPLATE_LIP_MAX.x, 0]
]));
}
/**
* @brief Outer edge/lip of the baseplate, with square corners.
* @details Needed to prevent gaps when joining multiples together.
* @param height Baseplate's height excluding lip and clearance height.
* @param size Width/Length of a single baseplate. Only set if deviating from the standard!
*/
module square_baseplate_lip(height=0, size = l_grid) {
assert(height >= 0 && size/2 >= BASEPLATE_OUTSIDE_RADIUS);
corner_center_distance = size/2 - BASEPLATE_OUTSIDE_RADIUS;
render(convexity = 2) // Fixes ghosting in preview
union() {
baseplate_lip(height, size, size);
pattern_circular(4)
translate([corner_center_distance, corner_center_distance, 0])
square_baseplate_corner(height);
}
}
/**
* @brief A single baseplate with square corners, a solid inner section, lip and the set clearance height.
* @param height Baseplate's height excluding lip and clearance height.
* @param size Width/Length of a single baseplate. Only set if deviating from the standard!
* @details A height of zero is the equivalent of just calling square_baseplate_lip()
*/
module solid_square_baseplate(height=0, size = l_grid) {
assert(height >= 0 && size > 0);
union() {
square_baseplate_lip(height, size);
if (height > 0) {
linear_extrude(height)
square(size - BASEPLATE_OUTSIDE_RADIUS, center=true);
}
}
}
/**
* @brief 2d Cutter to skeletonize the baseplate.
* @param size Width/Length of a single baseplate. Only set if deviating from the standard!
* @example difference(){
* cube(large_number);
* linear_extrude(large_number+TOLLERANCE)
* profile_skeleton();
* }
*/
module profile_skeleton(size=l_grid) {
l = size - 2*BASEPLATE_LIP_MAX.x;
offset(r_skel)
difference() {
square(l-2*r_skel, center = true);
hole_pattern()
offset(MAGNET_HOLE_RADIUS+r_skel+2)
square([l,l]);
}
}
module cutter_screw_together(gx, gy, size = l_grid) {
screw(gx, gy);
rotate([0,0,90])
screw(gy, gx);
module screw(a, b) {
copy_mirror([1,0,0])
translate([a*size/2, 0, 0])
pattern_linear(1, b, 1, size)
pattern_linear(1, n_screws, 1, d_screw_head + screw_spacing)
rotate([0,90,0])
cylinder(h=size/2, d=d_screw, center = true);
}
}

View File

@@ -0,0 +1,193 @@
include <gridfinity-rebuilt-utility.scad>
// ===== INFORMATION ===== //
/*
IMPORTANT: rendering will be better for analyzing the model if fast-csg is enabled. As of writing, this feature is only available in the development builds and not the official release of OpenSCAD, but it makes rendering only take a couple seconds, even for comically large bins. Enable it in Edit > Preferences > Features > fast-csg
the magnet holes can have an extra cut in them to make it easier to print without supports
tabs will automatically be disabled when gridz is less than 3, as the tabs take up too much space
base functions can be found in "gridfinity-rebuilt-utility.scad"
comments like ' //.5' after variables are intentional and used by the customizer
examples at end of file
BIN HEIGHT
the original gridfinity bins had the overall height defined by 7mm increments
a bin would be 7*u millimeters tall
the lip at the top of the bin (3.8mm) added onto this height
The stock bins have unit heights of 2, 3, and 6:
Z unit 2 -> 7*2 + 3.8 -> 17.8mm
Z unit 3 -> 7*3 + 3.8 -> 24.8mm
Z unit 6 -> 7*6 + 3.8 -> 45.8mm
https://github.com/kennetek/gridfinity-rebuilt-openscad
*/
// ===== PARAMETERS ===== //
/* [Setup Parameters] */
$fa = 8;
$fs = 0.25; // .01
/* [General Settings] */
// number of bases along x-axis
gridx = 2; //.5
// number of bases along y-axis
gridy = 4; //.5
// bin height. See bin height information and "gridz_define" below.
gridz = 6; //.1
/* [Linear Compartments] */
// number of X Divisions (set to zero to have solid bin)
divx = 1;
// number of Y Divisions (set to zero to have solid bin)
divy = 1;
/* [Cylindrical Compartments] */
// number of cylindrical X Divisions (mutually exclusive to Linear Compartments)
cdivx = 0;
// number of cylindrical Y Divisions (mutually exclusive to Linear Compartments)
cdivy = 0;
// orientation
c_orientation = 2; // [0: x direction, 1: y direction, 2: z direction]
// diameter of cylindrical cut outs
cd = 10; // .1
// cylinder height
ch = 1; //.1
// spacing to lid
c_depth = 1;
// chamfer around the top rim of the holes
c_chamfer = 0.5; // .1
/* [Height] */
// determine what the variable "gridz" applies to based on your use case
gridz_define = 0; // [0:gridz is the height of bins in units of 7mm increments - Zack's method,1:gridz is the internal height in millimeters, 2:gridz is the overall external height of the bin in millimeters]
// overrides internal block height of bin (for solid containers). Leave zero for default height. Units: mm
height_internal = 0;
// snap gridz height to nearest 7mm increment
enable_zsnap = false;
/* [Features] */
// the type of tabs
style_tab = 5; //[0:Full,1:Auto,2:Left,3:Center,4:Right,5:None]
// how should the top lip act
style_lip = 2; //[0: Regular lip, 1:remove lip subtractively, 2: remove lip and retain height]
// scoop weight percentage. 0 disables scoop, 1 is regular scoop. Any real number will scale the scoop.
scoop = 0; //[0:0.1:1]
/* [Base] */
// number of divisions per 1 unit of base along the X axis. (default 1, only use integers. 0 means automatically guess the right division)
div_base_x = 2;
// number of divisions per 1 unit of base along the Y axis. (default 1, only use integers. 0 means automatically guess the right division)
div_base_y = 2;
/* [Base Hole Options] */
// only cut magnet/screw holes at the corners of the bin to save uneccesary print time
only_corners = false;
//Use gridfinity refined hole style. Not compatible with magnet_holes!
refined_holes = false;
// Base will have holes for 6mm Diameter x 2mm high magnets.
magnet_holes = false;
// Base will have holes for M3 screws.
screw_holes = false;
// Magnet holes will have crush ribs to hold the magnet.
crush_ribs = true;
// Magnet/Screw holes will have a chamfer to ease insertion.
chamfer_holes = true;
// Magnet/Screw holes will be printed so supports are not needed.
printable_hole_top = true;
hole_options = bundle_hole_options(refined_holes, magnet_holes, screw_holes, crush_ribs, chamfer_holes, printable_hole_top);
// ===== IMPLEMENTATION ===== //
color("tomato") {
gridfinityInit(gridx, gridy, height(gridz, gridz_define, style_lip, enable_zsnap), height_internal, sl=style_lip) {
if (divx > 0 && divy > 0) {
cutEqual(n_divx = divx, n_divy = divy, style_tab = style_tab, scoop_weight = scoop);
} else if (cdivx > 0 && cdivy > 0) {
cutCylinders(n_divx=cdivx, n_divy=cdivy, cylinder_diameter=cd, cylinder_height=ch, coutout_depth=c_depth, orientation=c_orientation, chamfer=c_chamfer);
}
}
gridfinityBase(gridx, gridy, l_grid, div_base_x, div_base_y, hole_options, only_corners=only_corners);
}
// ===== EXAMPLES ===== //
// 3x3 even spaced grid
/*
gridfinityInit(3, 3, height(6), 0, 42) {
cutEqual(n_divx = 3, n_divy = 3, style_tab = 0, scoop_weight = 0);
}
gridfinityBase(3, 3, 42, 0, 0, 1);
*/
// Compartments can be placed anywhere (this includes non-integer positions like 1/2 or 1/3). The grid is defined as (0,0) being the bottom left corner of the bin, with each unit being 1 base long. Each cut() module is a compartment, with the first four values defining the area that should be made into a compartment (X coord, Y coord, width, and height). These values should all be positive. t is the tab style of the compartment (0:full, 1:auto, 2:left, 3:center, 4:right, 5:none). s is a toggle for the bottom scoop.
/*
gridfinityInit(3, 3, height(6), 0, 42) {
cut(x=0, y=0, w=1.5, h=0.5, t=5, s=0);
cut(0, 0.5, 1.5, 0.5, 5, 0);
cut(0, 1, 1.5, 0.5, 5, 0);
cut(0,1.5,0.5,1.5,5,0);
cut(0.5,1.5,0.5,1.5,5,0);
cut(1,1.5,0.5,1.5,5,0);
cut(1.5, 0, 1.5, 5/3, 2);
cut(1.5, 5/3, 1.5, 4/3, 4);
}
gridfinityBase(3, 3, 42, 0, 0, 1);
*/
// Compartments can overlap! This allows for weirdly shaped compartments, such as this "2" bin.
/*
gridfinityInit(3, 3, height(6), 0, 42) {
cut(0,2,2,1,5,0);
cut(1,0,1,3,5);
cut(1,0,2,1,5);
cut(0,0,1,2);
cut(2,1,1,2);
}
gridfinityBase(3, 3, 42, 0, 0, 1);
*/
// Areas without a compartment are solid material, where you can put your own cutout shapes. using the cut_move() function, you can select an area, and any child shapes will be moved from the origin to the center of that area, and subtracted from the block. For example, a pattern of three cylinderical holes.
/*
gridfinityInit(3, 3, height(6), 0, 42) {
cut(x=0, y=0, w=2, h=3);
cut(x=0, y=0, w=3, h=1, t=5);
cut_move(x=2, y=1, w=1, h=2)
pattern_linear(x=1, y=3, sx=42/2)
cylinder(r=5, h=1000, center=true);
}
gridfinityBase(3, 3, 42, 0, 0, 1);
*/
// You can use loops as well as the bin dimensions to make different parametric functions, such as this one, which divides the box into columns, with a small 1x1 top compartment and a long vertical compartment below
/*
gx = 3;
gy = 3;
gridfinityInit(gx, gy, height(6), 0, 42) {
for(i=[0:gx-1]) {
cut(i,0,1,gx-1);
cut(i,gx-1,1,1);
}
}
gridfinityBase(gx, gy, 42, 0, 0, 1);
*/
// Pyramid scheme bin
/*
gx = 4;
gy = 4;
gridfinityInit(gx, gy, height(6), 0, 42) {
for (i = [0:gx-1])
for (j = [0:i])
cut(j*gx/(i+1),gy-i-1,gx/(i+1),1,0);
}
gridfinityBase(gx, gy, 42, 0, 0, 1);
*/

View File

@@ -0,0 +1,316 @@
/**
* @file gridfinity-rebuilt-holes.scad
* @brief Functions to create different types of holes in an object.
*/
include <standard.scad>
use <generic-helpers.scad>
/**
* @brief Determines the number of fragments in a circle. Aka, Circle resolution.
* @param r Radius of the circle.
* @details Recommended function from the manual as a translation of the OpenSCAD function.
* Used to improve performance by not rendering every single degree of circles/spheres.
* @see https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Other_Language_Features#Circle_resolution:_$fa,_$fs,_and_$fn
*/
function get_fragments_from_r(r) =
assert(r > 0)
($fn>0?($fn>=3?$fn:3):ceil(max(min(360/$fa,r*2*PI/$fs),5)));
/**
* @brief Wave generation function for wrapping a circle.
* @param t An angle of the circle. Between 0 and 360 degrees.
* @param count The number of **full** waves in a 360 degree circle.
* @param range **Half** the difference between minimum and maximum values.
* @param vertical_offset Added to the output.
* When wrapping a circle, radius of that circle.
* @details
* If plotted on an x/y graph this produces a standard sin wave.
* Range only seems weird because it describes half a wave.
* Mapped by doing [sin(t), cost(t)] * wave_function(...).
* When wrapping a circle:
* Final Outer radius is (wave_vertical_offset + wave_range).
* Final Inner radius is (wave_vertical_offset - wave_range).
*/
function wave_function(t, count, range, vertical_offset) =
(sin(t * count) * range) + vertical_offset;
/**
* @brief A circle with crush ribs to give a tighter press fit.
* @details Extrude and use as a negative modifier.
* Idea based on Slant3D's video at 5:20 https://youtu.be/Bd7Yyn61XWQ?t=320
* Implementaiton is completely different.
* Important: Lower ribs numbers just result in a deformed circle.
* @param outer_radius Final outer radius.
* @param inner_radius Final inner radius.
* @param ribs Number of crush ribs the circle has.
**/
module ribbed_circle(outer_radius, inner_radius, ribs) {
assert(outer_radius > 0, "outer_radius must be positive");
assert(inner_radius > 0, "inner_radius must be positive");
assert(ribs > 0, "ribs must be positive");
assert(outer_radius > inner_radius, "outer_radius must be larger than inner_radius");
wave_range = (outer_radius - inner_radius) / 2;
wave_vertical_offset = inner_radius + wave_range;
fragments=get_fragments_from_r(wave_vertical_offset);
degrees_per_fragment = 360/fragments;
// Circe with a wave wrapped around it
wrapped_circle = [ for (i = [0:degrees_per_fragment:360])
[sin(i), cos(i)] * wave_function(i, ribs, wave_range, wave_vertical_offset)
];
polygon(wrapped_circle);
}
/**
* @brief A cylinder with crush ribs to give a tighter press fit.
* @details To be used as the negative for a hole.
* @see ribbed_circle
* @param outer_radius Outer Radius of the crush ribs.
* @param inner_radius Inner Radius of the crush ribs.
* @param height Cylinder's height.
* @param ribs Number of crush ribs.
*/
module ribbed_cylinder(outer_radius, inner_radius, height, ribs) {
assert(height > 0, "height must be positive");
linear_extrude(height)
ribbed_circle(
outer_radius,
inner_radius,
ribs
);
}
/**
* @brief Make a hole printable without suports.
* @see https://www.youtube.com/watch?v=W8FbHTcB05w
* @param inner_radius Radius of the inner hole.
* @param outer_radius Radius of the outer hole.
* @param outer_height Height of the outer hole.
* @param layers Number of layers to make printable.
* @details This is the negative designed to be cut out of the magnet hole.
* Use it with `difference()`.
* Special handling is done to support a single layer,
* and because the last layer (unless there is only one) has a different shape.
*/
module make_hole_printable(inner_radius, outer_radius, outer_height, layers=2) {
assert(inner_radius > 0, "inner_radius must be positive");
assert(outer_radius > 0, "outer_radius must be positive");
assert(layers > 0);
tollerance = 0.01; // Ensure everything is fully removed.
height_adjustment = outer_height - (layers * LAYER_HEIGHT);
// Needed, since the last layer should not be used for calculations,
// unless there is a single layer.
calculation_layers = max(layers-1, 1);
cube_height = LAYER_HEIGHT + 2*tollerance;
inner_diameter = 2*(inner_radius+tollerance);
outer_diameter = 2*(outer_radius+tollerance);
per_layer_difference = (outer_diameter-inner_diameter) / calculation_layers;
initial_matrix = affine_translate([0, 0, cube_height/2-tollerance + height_adjustment]);
// Produces data in the form [affine_matrix, [cube_dimensions]]
// If layers > 1, the last item produced has an invalid "affine_matrix.y", because it is beyond calculation_layers.
// That is handled in a special case to avoid doing a check every loop.
cutout_information = [
for(i=0; i <= layers; i=i+1)
[
initial_matrix * affine_translate([0, 0, (i-1)*LAYER_HEIGHT]) *
affine_rotate([0, 0, is_even(i) ? 90 : 0]),
[outer_diameter-per_layer_difference*(i-1),
outer_diameter-per_layer_difference*i,
cube_height]
]
];
difference() {
translate([0, 0, layers*cube_height/2 + height_adjustment])
cube([outer_diameter+tollerance, outer_diameter+tollerance, layers*cube_height], center = true);
for (i = [1 : calculation_layers]){
data = cutout_information[i];
multmatrix(data[0])
cube(data[1], center = true);
}
if(layers > 1) {
data = cutout_information[len(cutout_information)-1];
multmatrix(data[0])
cube([data[1].x, data[1].x, data[1].z], center = true);
}
}
}
/**
* @brief Refined hole based on Printables @grizzie17's Gridfinity Refined
* @details Magnet is pushed in from +X direction, and held in by friction.
* Small slit on the bottom allows removing the magnet.
* @see https://www.printables.com/model/413761-gridfinity-refined
*/
module refined_hole() {
refined_offset = LAYER_HEIGHT * REFINED_HOLE_BOTTOM_LAYERS;
// Poke through - For removing a magnet using a toothpick
ptl = refined_offset + LAYER_HEIGHT; // Additional layer just in case
poke_through_height = REFINED_HOLE_HEIGHT + ptl;
poke_hole_radius = 2.5;
magic_constant = 5.60;
poke_hole_center = [-12.53 + magic_constant, 0, -ptl];
translate([0, 0, refined_offset])
union() {
// Magnet hole
translate([0, -REFINED_HOLE_RADIUS, 0])
cube([11, REFINED_HOLE_RADIUS*2, REFINED_HOLE_HEIGHT]);
cylinder(REFINED_HOLE_HEIGHT, r=REFINED_HOLE_RADIUS);
// Poke hole
translate([poke_hole_center.x, -poke_hole_radius/2, poke_hole_center.z])
cube([10 - magic_constant, poke_hole_radius, poke_through_height]);
translate(poke_hole_center)
cylinder(poke_through_height, d=poke_hole_radius);
}
}
/**
* @brief Create a cone given a radius and an angle.
* @param bottom_radius Radius of the bottom of the cone.
* @param angle Angle as measured from the bottom of the cone.
* @param max_height Optional maximum height. Cone will be cut off if higher.
*/
module cone(bottom_radius, angle, max_height=0) {
assert(bottom_radius > 0);
assert(angle > 0 && angle <= 90);
assert(max_height >=0);
height = tan(angle) * bottom_radius;
if(max_height == 0 || height < max_height) {
// Normal Cone
cylinder(h = height, r1 = bottom_radius, r2 = 0, center = false);
} else {
top_angle = 90 - angle;
top_radius = bottom_radius - tan(top_angle) * max_height;
cylinder(h = max_height, r1 = bottom_radius, r2 = top_radius, center = false);
}
}
/**
* @brief Create a screw hole
* @param radius Radius of the hole.
* @param height Height of the hole.
* @param supportless If the hole is designed to be printed without supports.
* @param chamfer_radius If the hole should be chamfered, then how much should be added to radius. 0 means don't chamfer
* @param chamfer_angle If the hole should be chamfered, then what angle should it be chamfered at. Ignored if chamfer_radius is 0.
*/
module screw_hole(radius, height, supportless=false, chamfer_radius=0, chamfer_angle = 45) {
assert(radius > 0);
assert(height > 0);
assert(chamfer_radius >= 0);
union(){
difference() {
cylinder(h = height, r = radius);
if (supportless) {
rotate([0, 0, 90])
make_hole_printable(0.5, radius, height, 3);
}
}
if (chamfer_radius > 0) {
cone(radius + chamfer_radius, chamfer_angle, height);
}
}
}
/**
* @brief Create an options list used to configure bin holes.
* @param refined_hole Use gridfinity refined hole type. Not compatible with "magnet_hole".
* @param magnet_hole Create a hole for a 6mm magnet.
* @param screw_hole Create a hole for a M3 screw.
* @param crush_ribs If the magnet hole should have crush ribs for a press fit.
* @param chamfer Add a chamfer to the magnet/screw hole.
* @param supportless If the magnet/screw hole should be printed in such a way that the screw hole does not require supports.
*/
function bundle_hole_options(refined_hole=false, magnet_hole=false, screw_hole=false, crush_ribs=false, chamfer=false, supportless=false) =
[refined_hole, magnet_hole, screw_hole, crush_ribs, chamfer, supportless];
/**
* @brief A single magnet/screw hole. To be cut out of the base.
* @details Supports multiple options that can be mixed and matched.
* @pram hole_options @see bundle_hole_options
* @param o Offset
*/
module block_base_hole(hole_options, o=0) {
assert(is_list(hole_options));
// Destructure the options
refined_hole = hole_options[0];
magnet_hole = hole_options[1];
screw_hole = hole_options[2];
crush_ribs = hole_options[3];
chamfer = hole_options[4];
supportless = hole_options[5];
// Validate said options
if(refined_hole) {
assert(!magnet_hole, "magnet_hole is not compatible with refined_hole");
}
screw_radius = SCREW_HOLE_RADIUS - (o/2);
magnet_radius = MAGNET_HOLE_RADIUS - (o/2);
magnet_inner_radius = MAGNET_HOLE_CRUSH_RIB_INNER_RADIUS - (o/2);
screw_depth = h_base-o;
// If using supportless / printable mode, need to add additional layers, so they can be removed later.
supportless_additional_layers = screw_hole ? 2 : 3;
magnet_depth = MAGNET_HOLE_DEPTH - o +
(supportless ? supportless_additional_layers*LAYER_HEIGHT : 0);
union() {
if(refined_hole) {
refined_hole();
}
if(magnet_hole) {
difference() {
if(crush_ribs) {
ribbed_cylinder(magnet_radius, magnet_inner_radius, magnet_depth, MAGNET_HOLE_CRUSH_RIB_COUNT);
} else {
cylinder(h = magnet_depth, r=magnet_radius);
}
if(supportless) {
make_hole_printable(
screw_hole ? screw_radius : 1, magnet_radius, magnet_depth, supportless_additional_layers);
}
}
if(chamfer) {
cone(magnet_radius + CHAMFER_ADDITIONAL_RADIUS, CHAMFER_ANGLE, MAGNET_HOLE_DEPTH - o);
}
}
if(screw_hole) {
screw_hole(screw_radius, screw_depth, supportless,
chamfer ? CHAMFER_ADDITIONAL_RADIUS : 0, CHAMFER_ANGLE);
}
}
}
//$fa = 8;
//$fs = 0.25;
if(!is_undef(test_options)){
block_base_hole(test_options);
}
//block_base_hole(bundle_hole_options(
// refined_hole=false,
// magnet_hole=true,
// screw_hole=true,
// supportless=true,
// crush_ribs=false,
// chamfer=true
//));
//make_hole_printable(1, 3, 0);

View File

@@ -0,0 +1,151 @@
include <gridfinity-rebuilt-utility.scad>
// ===== INFORMATION ===== //
/*
IMPORTANT: rendering will be better for analyzing the model if fast-csg is enabled. As of writing, this feature is only available in the development builds and not the official release of OpenSCAD, but it makes rendering only take a couple seconds, even for comically large bins. Enable it in Edit > Preferences > Features > fast-csg
https://github.com/kennetek/gridfinity-rebuilt-openscad
*/
// ===== PARAMETERS ===== //
/* [Setup Parameters] */
$fa = 8;
$fs = 0.25;
/* [General Settings] */
// number of bases along x-axis
gridx = 3;
// number of bases along y-axis
gridy = 3;
// bin height. See bin height information and "gridz_define" below.
gridz = 6;
/* [Compartments] */
// number of X Divisions
divx = 2;
// number of y Divisions
divy = 2;
/* [Toggles] */
// snap gridz height to nearest 7mm increment
enable_zsnap = false;
// how should the top lip act
style_lip = 0; //[0: Regular lip, 1:remove lip subtractively, 2: remove lip and retain height]
/* [Other] */
// determine what the variable "gridz" applies to based on your use case
gridz_define = 0; // [0:gridz is the height of bins in units of 7mm increments - Zack's method,1:gridz is the internal height in millimeters, 2:gridz is the overall external height of the bin in millimeters]
// the type of tabs
style_tab = 1; //[0:Full,1:Auto,2:Left,3:Center,4:Right,5:None]
/* [Base] */
// number of divisions per 1 unit of base along the X axis. (default 1, only use integers. 0 means automatically guess the right division)
div_base_x = 0;
// number of divisions per 1 unit of base along the Y axis. (default 1, only use integers. 0 means automatically guess the right division)
div_base_y = 0;
// thickness of bottom layer
bottom_layer = 1;
/* [Base Hole Options] */
// only cut magnet/screw holes at the corners of the bin to save uneccesary print time
only_corners = false;
//Use gridfinity refined hole style. Not compatible with magnet_holes!
refined_holes = false;
// Base will have holes for 6mm Diameter x 2mm high magnets.
magnet_holes = true;
// Base will have holes for M3 screws.
screw_holes = true;
// Magnet holes will have crush ribs to hold the magnet.
crush_ribs = true;
// Magnet/Screw holes will have a chamfer to ease insertion.
chamfer_holes = true;
// Magnet/Screw holes will be printed so supports are not needed.
printable_hole_top = true;
hole_options = bundle_hole_options(refined_holes, magnet_holes, screw_holes, crush_ribs, chamfer_holes, printable_hole_top);
// ===== IMPLEMENTATION ===== //
// Input all the cutter types in here
color("tomato")
gridfinityLite(gridx, gridy, gridz, gridz_define, style_lip, enable_zsnap, l_grid, div_base_x, div_base_y, hole_options, only_corners) {
cutEqual(n_divx = divx, n_divy = divy, style_tab = style_tab, scoop_weight = 0);
}
// ===== CONSTRUCTION ===== //
module gridfinityLite(gridx, gridy, gridz, gridz_define, style_lip, enable_zsnap, length, div_base_x, div_base_y, style_hole, only_corners) {
height_mm = height(gridz, gridz_define, style_lip, enable_zsnap);
union() {
difference() {
union() {
gridfinityInit(gridx, gridy, height_mm, 0, length, sl=style_lip)
children();
gridfinityBase(gridx, gridy, length, div_base_x, div_base_y, style_hole, only_corners=only_corners);
}
difference() {
union() {
intersection() {
difference() {
gridfinityBase(gridx, gridy, length, div_base_x, div_base_y, style_hole, -d_wall*2, false, only_corners=only_corners);
translate([-gridx*length/2,-gridy*length/2,2*h_base])
cube([gridx*length,gridy*length,1000]);
}
translate([0,0,-1])
rounded_rectangle(gridx*length-0.5005-d_wall*2, gridy*length-0.5005-d_wall*2, 1000, r_f2);
translate([0,0,bottom_layer])
rounded_rectangle(gridx*1000, gridy*1000, 1000, r_f2);
}
translate([0,0,h_base+d_clear])
rounded_rectangle(gridx*length-0.5005-d_wall*2, gridy*length-0.5005-d_wall*2, h_base, r_f2);
}
translate([0,0,-4*h_base])
gridfinityInit(gridx, gridy, height(20,0), 0, length, sl=style_lip)
children();
}
}
difference() {
translate([0,0,-1.6])
difference() {
difference() {
union() {
gridfinityInit(gridx, gridy, height_mm, 0, length, sl=style_lip)
children();
}
difference() {
intersection() {
difference() {
gridfinityBase(gridx, gridy, length, div_base_x, div_base_y, style_hole, -d_wall*2, false, only_corners=only_corners);
translate([-gridx*length/2,-gridy*length/2,2*h_base])
cube([gridx*length,gridy*length,1000]);
}
translate([0,0,-1])
rounded_rectangle(gridx*length-0.5005-d_wall*2, gridy*length-0.5005-d_wall*2, 1000, r_f2);
translate([0,0,bottom_layer])
rounded_rectangle(gridx*1000, gridy*1000, 1000, r_f2);
}
translate([0,0,-4*h_base])
gridfinityInit(gridx, gridy, height(20,0), 0, length, sl=style_lip)
children();
}
}
translate([0,0,9])
rounded_rectangle(gridx*1000, gridy*1000, gridz*1000, gridz);
}
translate([0,0,0])
rounded_rectangle(gridx*1000, gridy*1000, 5, r_f2);
}
}
}

View File

@@ -0,0 +1,565 @@
/**
* @file gridfinity-rebuilt-utility.scad
* @brief UTILITY FILE, DO NOT EDIT
* EDIT OTHER FILES IN REPO FOR RESULTS
*/
include <standard.scad>
use <generic-helpers.scad>
use <gridfinity-rebuilt-holes.scad>
// ===== User Modules ===== //
// functions to convert gridz values to mm values
/**
* @Summary Convert a number from Gridfinity values to mm.
* @details Also can include lip when working with height values.
* @param gridfinityUnit Gridfinity is normally on a base 7 system.
* @param includeLipHeight Include the lip height as well.
* @returns The final value in mm.
*/
function fromGridfinityUnits(gridfinityUnit, includeLipHeight = false) =
gridfinityUnit*7 + (includeLipHeight ? h_lip : 0);
/**
* @Summary Height in mm including fixed heights.
* @details Also can include lip when working with height values.
* @param mmHeight Height without other values.
* @param includeLipHeight Include the lip height as well.
* @returns The final value in mm.
*/
function includingFixedHeights(mmHeight, includeLipHeight = false) =
mmHeight + h_bot + h_base + (includeLipHeight ? h_lip : 0);
/**
* @brief Three Functions in One. For height calculations.
* @param z Height value
* @param gridz_define As explained in gridfinity-rebuilt-bins.scad
* @param l style_lip as explained in gridfinity-rebuilt-bins.scad
* @returns Height in mm
*/
function hf (z, gridz_define, style_lip) =
gridz_define==0 ? fromGridfinityUnits(z, style_lip==2) :
gridz_define==1 ? includingFixedHeights(z, style_lip==2) :
z + ( // Just use z (possibly adding/subtracting lip)
style_lip==1 ? -h_lip :
style_lip==2 ? h_lip : 0
)
;
/**
* @brief Calculates the proper height for bins. Three Functions in One.
* @param z Height value
* @param d gridz_define as explained in gridfinity-rebuilt-bins.scad
* @param l style_lip as explained in gridfinity-rebuilt-bins.scad
* @param enable_zsnap Automatically snap the bin size to the nearest 7mm increment.
* @returns Height in mm
*/
function height (z,d=0,l=0,enable_zsnap=true) =
(
enable_zsnap ? (
(abs(hf(z,d,l))%7==0) ? hf(z,d,l) :
hf(z,d,l)+7-abs(hf(z,d,l))%7
)
:hf(z,d,l)
) -h_base;
// Creates equally divided cutters for the bin
//
// n_divx: number of x compartments (ideally, coprime w/ gridx)
// n_divy: number of y compartments (ideally, coprime w/ gridy)
// set n_div values to 0 for a solid bin
// style_tab: tab style for all compartments. see cut()
// scoop_weight: scoop toggle for all compartments. see cut()
module cutEqual(n_divx=1, n_divy=1, style_tab=1, scoop_weight=1) {
for (i = [1:n_divx])
for (j = [1:n_divy])
cut((i-1)*$gxx/n_divx,(j-1)*$gyy/n_divy, $gxx/n_divx, $gyy/n_divy, style_tab, scoop_weight);
}
// Creates equally divided cylindrical cutouts
//
// n_divx: number of x cutouts
// n_divy: number of y cutouts
// set n_div values to 0 for a solid bin
// cylinder_diameter: diameter of cutouts
// cylinder_height: height of cutouts
// coutout_depth: offset from top to solid part of container
// orientation: orientation of cylinder cutouts (0 = x direction, 1 = y direction, 2 = z direction)
// chamfer: chamfer around the top rim of the holes
module cutCylinders(n_divx=1, n_divy=1, cylinder_diameter=1, cylinder_height=1, coutout_depth=0, orientation=0, chamfer=0.5) {
rotation = (orientation == 0)
? [0,90,0]
: (orientation == 1)
? [90,0,0]
: [0,0,0];
gridx_mm = $gxx*l_grid;
gridy_mm = $gyy*l_grid;
padding = 2;
cutout_x = gridx_mm - d_wall*2;
cutout_y = gridy_mm - d_wall*2;
cut_move(x=0, y=0, w=$gxx, h=$gyy) {
translate([0,0,-coutout_depth]) {
rounded_rectangle(cutout_x, cutout_y, coutout_depth*2, r_base);
pattern_linear(x=n_divx, y=n_divy, sx=(gridx_mm - padding)/n_divx, sy=(gridy_mm - padding)/n_divy)
rotate(rotation)
union() {
cylinder(d=cylinder_diameter, h=cylinder_height*2, center=true);
if (chamfer > 0) {
translate([0,0,-chamfer]) cylinder(d1=cylinder_diameter, d2=cylinder_diameter+4*chamfer, h=2*chamfer);
}
};
}
}
}
// initialize gridfinity
// sl: lip style of this bin.
// 0:Regular lip, 1:Remove lip subtractively, 2:Remove lip and retain height
module gridfinityInit(gx, gy, h, h0 = 0, l = l_grid, sl = 0) {
$gxx = gx;
$gyy = gy;
$dh = h;
$dh0 = h0;
$style_lip = sl;
difference() {
color("firebrick")
block_bottom(h0==0?$dh-0.1:h0, gx, gy, l);
children();
}
color("royalblue")
block_wall(gx, gy, l) {
if ($style_lip == 0) profile_wall(h);
else profile_wall2(h);
}
}
// Function to include in the custom() module to individually slice bins
// Will try to clamp values to fit inside the provided base size
//
// x: start coord. x=1 is the left side of the bin.
// y: start coord. y=1 is the bottom side of the bin.
// w: width of compartment, in # of bases covered
// h: height of compartment, in # of basese covered
// t: tab style of this specific compartment.
// alignment only matters if the compartment size is larger than d_tabw
// 0:full, 1:auto, 2:left, 3:center, 4:right, 5:none
// Automatic alignment will use left tabs for bins on the left edge, right tabs for bins on the right edge, and center tabs everywhere else.
// s: toggle the rounded back corner that allows for easy removal
module cut(x=0, y=0, w=1, h=1, t=1, s=1, tab_width=d_tabw, tab_height=d_tabh) {
translate([0,0,-$dh-h_base])
cut_move(x,y,w,h)
block_cutter(clp(x,0,$gxx), clp(y,0,$gyy), clp(w,0,$gxx-x), clp(h,0,$gyy-y), t, s, tab_width, tab_height);
}
// cuts equally sized bins over a given length at a specified position
// bins_x: number of bins along x-axis
// bins_y: number of bins along y-axis
// len_x: length (in gridfinity bases) along x-axis that the bins_x will fill
// len_y: length (in gridfinity bases) along y-axis that the bins_y will fill
// pos_x: start x position of the bins (left side)
// pos_y: start y position of the bins (bottom side)
// style_tab: Style of the tab used on the bins
// scoop: Weight of the scoop on the bottom of the bins
// tab_width: Width of the tab on the bins, in mm.
// tab_height: How far the tab will stick out over the bin, in mm. Default tabs fit 12mm labels, but for narrow bins can take up too much space over the opening. This setting allows 'slimmer' tabs for use with thinner labels, so smaller/narrower bins can be labeled and still keep a reasonable opening at the top. NOTE: The measurement is not 1:1 in mm, so a '3.5' value does not guarantee a tab that fits 3.5mm label tape. Use the 'measure' tool after rendering to check the distance between faces to guarantee it fits your needs.
module cutEqualBins(bins_x=1, bins_y=1, len_x=1, len_y=1, pos_x=0, pos_y=0, style_tab=5, scoop=1, tab_width=d_tabw, tab_height=d_tabh) {
// Calculate width and height of each bin based on total length and number of bins
bin_width = len_x / bins_x;
bin_height = len_y / bins_y;
// Loop through each bin position in x and y direction
for (i = [0:bins_x-1]) {
for (j = [0:bins_y-1]) {
// Calculate the starting position for each bin
// Adjust position by adding pos_x and pos_y to shift the entire grid of bins as needed
bin_start_x = pos_x + i * bin_width;
bin_start_y = pos_y + j * bin_height;
// Call the cut module to create each bin with calculated position and dimensions
// Pass through the style_tab and scoop parameters
cut(bin_start_x, bin_start_y, bin_width, bin_height, style_tab, scoop, tab_width=tab_width, tab_height=tab_height);
}
}
}
// Translates an object from the origin point to the center of the requested compartment block, can be used to add custom cuts in the bin
// See cut() module for parameter descriptions
module cut_move(x, y, w, h) {
translate([0,0,$dh0==0?$dh+h_base:$dh0+h_base])
cut_move_unsafe(clp(x,0,$gxx), clp(y,0,$gyy), clp(w,0,$gxx-x), clp(h,0,$gyy-y))
children();
}
// ===== Modules ===== //
module profile_base() {
polygon([
[0,0],
[0,h_base],
[r_base,h_base],
[r_base-r_c2,h_base-r_c2],
[r_base-r_c2,r_c1],
[r_base-r_c2-r_c1,0]
]);
}
module gridfinityBase(gx, gy, l, dx, dy, hole_options=bundle_hole_options(), off=0, final_cut=true, only_corners=false) {
dbnxt = [for (i=[1:5]) if (abs(gx*i)%1 < 0.001 || abs(gx*i)%1 > 0.999) i];
dbnyt = [for (i=[1:5]) if (abs(gy*i)%1 < 0.001 || abs(gy*i)%1 > 0.999) i];
dbnx = 1/(dx==0 ? len(dbnxt) > 0 ? dbnxt[0] : 1 : round(dx));
dbny = 1/(dy==0 ? len(dbnyt) > 0 ? dbnyt[0] : 1 : round(dy));
xx = gx*l-0.5;
yy = gy*l-0.5;
if (final_cut)
translate([0,0,h_base])
rounded_rectangle(xx+0.002, yy+0.002, h_bot/1.5, r_fo1+0.001);
intersection(){
if (final_cut)
translate([0,0,-1])
rounded_rectangle(xx+0.005, yy+0.005, h_base+h_bot/2*10, r_fo1+0.001);
if(only_corners) {
difference(){
pattern_linear(gx/dbnx, gy/dbny, dbnx*l, dbny*l)
block_base(gx, gy, l, dbnx, dbny, bundle_hole_options(), off);
copy_mirror([0, 1, 0]) {
copy_mirror([1, 0, 0]) {
translate([
(gx/2)*l_grid - d_hole_from_side,
(gy/2) * l_grid - d_hole_from_side,
0
])
block_base_hole(hole_options, off);
}
}
}
}
else {
pattern_linear(gx/dbnx, gy/dbny, dbnx*l, dbny*l)
block_base(gx, gy, l, dbnx, dbny, hole_options, off);
}
}
}
/**
* @brief A single Gridfinity base. With holes (if set).
* @param gx
* @param gy
* @param l
* @param dbnx
* @param dbny
* @param hole_options @see block_base_hole.hole_options
* @param off
*/
module block_base(gx, gy, l, dbnx, dbny, hole_options, off) {
render(convexity = 2)
difference() {
block_base_solid(dbnx, dbny, l, off);
pattern_circular(abs(l-d_hole_from_side/2)<0.001?1:4)
translate([l/2-d_hole_from_side, l/2-d_hole_from_side, 0])
block_base_hole(hole_options, off);
}
}
/**
* @brief A gridfinity base with no holes.
* @details Used as the "base" with holes removed from it later.
* @param dbnx
* @param dbny
* @param l
* @param o
*/
module block_base_solid(dbnx, dbny, l, o) {
xx = dbnx*l-0.05;
yy = dbny*l-0.05;
oo = (o/2)*(sqrt(2)-1);
translate([0,0,h_base])
mirror([0,0,1])
union() {
hull() {
rounded_rectangle(xx-2*r_c2-2*r_c1+o, yy-2*r_c2-2*r_c1+o, h_base+oo, r_fo3);
rounded_rectangle(xx-2*r_c2+o, yy-2*r_c2+o, h_base-r_c1+oo, r_fo2);
}
translate([0,0,oo])
hull() {
rounded_rectangle(xx-2*r_c2+o, yy-2*r_c2+o, r_c2, r_fo2);
mirror([0,0,1])
rounded_rectangle(xx+o, yy+o, h_bot/2+abs(10*o), r_fo1);
}
}
}
/**
* @brief Stacking lip based on https://gridfinity.xyz/specification/
* @details Also includes a support base.
*/
module stacking_lip() {
// Technique: Descriptive constant names are useful, but can be unweildy.
// Use abbreviations if they are going to be re-used repeatedly in a small piece of code.
inner_slope = stacking_lip_inner_slope_height_mm;
wall_height = stacking_lip_wall_height_mm;
support_wall = stacking_lip_support_wall_height_mm;
s_total = stacking_lip_support_height_mm;
polygon([
[0, 0], // Inner tip
[inner_slope, inner_slope], // Go out 45 degrees
[inner_slope, inner_slope+wall_height], // Vertical increase
[stacking_lip_depth, stacking_lip_height], // Go out 45 degrees
[stacking_lip_depth, -s_total], // Down to support bottom
[0, -support_wall], // Up and in
[0, 0] // Close the shape. Tehcnically not needed.
]);
}
/**
* @brief Stacking lip with a with a chamfered (rounded) top.
* @details Based on https://gridfinity.xyz/specification/
* Also includes a support base.
*/
module stacking_lip_chamfered() {
radius_center_y = h_lip - r_f1;
union() {
// Create rounded top
intersection() {
translate([0, radius_center_y, 0])
square([stacking_lip_depth, stacking_lip_height]);
offset(r = r_f1)
offset(delta = -r_f1)
stacking_lip();
}
// Remove pointed top
difference(){
stacking_lip();
translate([0, radius_center_y, 0])
square([stacking_lip_depth*2, stacking_lip_height*2]);
}
}
}
/**
* @brief External wall profile, with a stacking lip.
* @details Translated so a 90 degree rotation produces the expected outside radius.
*/
module profile_wall(height_mm) {
assert(is_num(height_mm))
translate([r_base - stacking_lip_depth, 0, 0]){
translate([0, height_mm, 0])
stacking_lip_chamfered();
translate([stacking_lip_depth-d_wall/2, 0, 0])
square([d_wall/2, height_mm]);
}
}
// lipless profile
module profile_wall2(height_mm) {
assert(is_num(height_mm))
translate([r_base,0,0])
mirror([1,0,0])
square([d_wall, height_mm]);
}
module block_wall(gx, gy, l) {
translate([0,0,h_base])
sweep_rounded(gx*l-2*r_base-0.5-0.001, gy*l-2*r_base-0.5-0.001)
children();
}
module block_bottom( h = 2.2, gx, gy, l ) {
translate([0,0,h_base+0.1])
rounded_rectangle(gx*l-0.5-d_wall/4, gy*l-0.5-d_wall/4, h, r_base+0.01);
}
module cut_move_unsafe(x, y, w, h) {
xx = ($gxx*l_grid+d_magic);
yy = ($gyy*l_grid+d_magic);
translate([(x)*xx/$gxx,(y)*yy/$gyy,0])
translate([(-xx+d_div)/2,(-yy+d_div)/2,0])
translate([(w*xx/$gxx-d_div)/2,(h*yy/$gyy-d_div)/2,0])
children();
}
module block_cutter(x,y,w,h,t,s,tab_width=d_tabw,tab_height=d_tabh) {
v_len_tab = tab_height;
v_len_lip = d_wall2-d_wall+1.2;
v_cut_tab = tab_height - (2*r_f1)/tan(a_tab);
v_cut_lip = d_wall2-d_wall-d_clear;
v_ang_tab = a_tab;
v_ang_lip = 45;
ycutfirst = y == 0 && $style_lip == 0;
ycutlast = abs(y+h-$gyy)<0.001 && $style_lip == 0;
xcutfirst = x == 0 && $style_lip == 0;
xcutlast = abs(x+w-$gxx)<0.001 && $style_lip == 0;
zsmall = ($dh+h_base)/7 < 3;
ylen = h*($gyy*l_grid+d_magic)/$gyy-d_div;
xlen = w*($gxx*l_grid+d_magic)/$gxx-d_div;
height = $dh;
extent = (abs(s) > 0 && ycutfirst ? d_wall2-d_wall-d_clear : 0);
tab = (zsmall || t == 5) ? (ycutlast?v_len_lip:0) : v_len_tab;
ang = (zsmall || t == 5) ? (ycutlast?v_ang_lip:0) : v_ang_tab;
cut = (zsmall || t == 5) ? (ycutlast?v_cut_lip:0) : v_cut_tab;
style = (t > 1 && t < 5) ? t-3 : (x == 0 ? -1 : xcutlast ? 1 : 0);
translate([0,ylen/2,h_base+h_bot])
rotate([90,0,-90]) {
if (!zsmall && xlen - tab_width > 4*r_f2 && (t != 0 && t != 5)) {
fillet_cutter(3,"bisque")
difference() {
transform_tab(style, xlen, ((xcutfirst&&style==-1)||(xcutlast&&style==1))?v_cut_lip:0, tab_width)
translate([ycutlast?v_cut_lip:0,0])
profile_cutter(height-h_bot, ylen/2, s);
if (xcutfirst)
translate([0,0,(xlen/2-r_f2)-v_cut_lip])
cube([ylen,height,v_cut_lip*2]);
if (xcutlast)
translate([0,0,-(xlen/2-r_f2)-v_cut_lip])
cube([ylen,height,v_cut_lip*2]);
}
if (t != 0 && t != 5)
fillet_cutter(2,"indigo")
difference() {
transform_tab(style, xlen, ((xcutfirst&&style==-1)||(xcutlast&&style==1)?v_cut_lip:0), tab_width)
difference() {
intersection() {
profile_cutter(height-h_bot, ylen-extent, s);
profile_cutter_tab(height-h_bot, v_len_tab, v_ang_tab);
}
if (ycutlast) profile_cutter_tab(height-h_bot, v_len_lip, 45);
}
if (xcutfirst)
translate([ylen/2,0,xlen/2])
rotate([0,90,0])
transform_main(2*ylen)
profile_cutter_tab(height-h_bot, v_len_lip, v_ang_lip);
if (xcutlast)
translate([ylen/2,0,-xlen/2])
rotate([0,-90,0])
transform_main(2*ylen)
profile_cutter_tab(height-h_bot, v_len_lip, v_ang_lip);
}
}
fillet_cutter(1,"seagreen")
translate([0,0,xcutlast?v_cut_lip/2:0])
translate([0,0,xcutfirst?-v_cut_lip/2:0])
transform_main(xlen-(xcutfirst?v_cut_lip:0)-(xcutlast?v_cut_lip:0))
translate([cut,0])
profile_cutter(height-h_bot, ylen-extent-cut-(!s&&ycutfirst?v_cut_lip:0), s);
fillet_cutter(0,"hotpink")
difference() {
transform_main(xlen)
difference() {
profile_cutter(height-h_bot, ylen-extent, s);
if (!((zsmall || t == 5) && !ycutlast))
profile_cutter_tab(height-h_bot, tab, ang);
if (!(abs(s) > 0)&& y == 0)
translate([ylen-extent,0,0])
mirror([1,0,0])
profile_cutter_tab(height-h_bot, v_len_lip, v_ang_lip);
}
if (xcutfirst)
color("indigo")
translate([ylen/2+0.001,0,xlen/2+0.001])
rotate([0,90,0])
transform_main(2*ylen)
profile_cutter_tab(height-h_bot, v_len_lip, v_ang_lip);
if (xcutlast)
color("indigo")
translate([ylen/2+0.001,0,-xlen/2+0.001])
rotate([0,-90,0])
transform_main(2*ylen)
profile_cutter_tab(height-h_bot, v_len_lip, v_ang_lip);
}
}
}
module transform_main(xlen) {
translate([0,0,-(xlen-2*r_f2)/2])
linear_extrude(xlen-2*r_f2)
children();
}
module transform_tab(type, xlen, cut, tab_width=d_tabw) {
mirror([0,0,type==1?1:0])
copy_mirror([0,0,-(abs(type)-1)])
translate([0,0,-(xlen)/2])
translate([0,0,r_f2])
linear_extrude((xlen-tab_width-abs(cut))/(1-(abs(type)-1))-2*r_f2)
children();
}
module fillet_cutter(t = 0, c = "goldenrod") {
color(c)
minkowski() {
children();
sphere(r = r_f2-t/1000);
}
}
module profile_cutter(h, l, s) {
scoop = max(s*$dh/2-r_f2,0);
translate([r_f2,r_f2])
hull() {
if (l-scoop-2*r_f2 > 0)
square(0.1);
if (scoop < h) {
translate([l-2*r_f2,h-r_f2/2])
mirror([1,1])
square(0.1);
translate([0,h-r_f2/2])
mirror([0,1])
square(0.1);
}
difference() {
translate([l-scoop-2*r_f2, scoop])
if (scoop != 0) {
intersection() {
circle(scoop);
mirror([0,1]) square(2*scoop);
}
} else mirror([1,0]) square(0.1);
translate([l-scoop-2*r_f2,-1])
square([-(l-scoop-2*r_f2),2*h]);
translate([0,h])
square([2*l,scoop]);
}
}
}
module profile_cutter_tab(h, tab, ang) {
if (tab > 0)
color("blue")
offset(delta = r_f2)
polygon([[0,h],[tab,h],[0,h-tab*tan(ang)]]);
}

539
gridfinity-spiral-vase.scad Normal file
View File

@@ -0,0 +1,539 @@
include <gridfinity-rebuilt-utility.scad>
// ===== INFORMATION ===== //
/*
IMPORTANT: rendering will be better for analyzing the model if fast-csg is enabled. As of writing, this feature is only available in the development builds and not the official release of OpenSCAD, but it makes rendering only take a couple seconds, even for comically large bins. Enable it in Edit > Preferences > Features > fast-csg
https://github.com/kennetek/gridfinity-rebuilt-openscad
*/
// ===== PARAMETERS ===== //
/* [Special Variables] */
$fa = 8;
$fs = 0.25;
/* [Bin or Base] */
type = 1; // [0:bin, 1:base]
/* [Printer Settings] */
// extrusion width (walls will be twice this size)
nozzle = 0.6;
// slicer layer size
layer = 0.35;
// number of base layers on build plate
bottom_layer = 3;
/* [General Settings] */
// number of bases along x-axis
gridx = 1;
// number of bases along y-axis
gridy = 1;
// bin height. See bin height information and "gridz_define" below.
gridz = 6;
// number of compartments along x-axis
n_divx = 2;
/* [Toggles] */
// toggle holes on the base for magnet
enable_holes = true;
// round up the bin height to match the closest 7mm unit
enable_zsnap = false;
// toggle the lip on the top of the bin that allows stacking
enable_lip = true;
// chamfer inside bin for easy part removal
enable_scoop_chamfer = true;
// funnel-like features on the back of tabs for fingers to grab
enable_funnel = true;
// front inset (added for strength when there is a scoop)
enable_inset = true;
// "pinches" the top lip of the bin, for added strength
enable_pinch = true;
/* [Styles] */
// determine what the variable "gridz" applies to based on your use case
gridz_define = 0; // [0:gridz is the height of bins in units of 7mm increments - Zack's method,1:gridz is the internal height in millimeters, 2:gridz is the overall external height of the bin in millimeters]
// how tabs are implemented
style_tab = 0; // [0:continuous, 1:broken, 2:auto, 3:right, 4:center, 5:left, 6:none]
// where to put X cutouts for attaching bases
// selecting none will also disable crosses on bases
style_base = 0; // [0:all, 1:corners, 2:edges, 3:auto, 4:none]
// tab angle
a_tab = 40;
// ===== IMPLEMENTATION ===== //
color("tomato")
if (type != 0) gridfinityBaseVase(); // Generate a single base
else gridfinityVase(); // Generate the bin
// ===== CONSTRUCTION ===== //
d_bottom = layer*(max(bottom_layer,1));
x_l = l_grid/2;
dht = (gridz_define==0)?gridz*7 : (gridz_define==1)?h_bot+gridz+h_base : gridz-(enable_lip?3.8:0);
d_height = (enable_zsnap?((abs(dht)%7==0)?dht:dht+7-abs(dht)%7):dht)-h_base;
d_fo1 = 2*r_fo1;
f2c = sqrt(2)*(sqrt(2)-1); // fillet to chamfer ratio
me = ((gridx*l_grid-0.5)/n_divx)-nozzle*4-d_fo1-12.7-4;
m = min(d_tabw/1.8 + max(0,me), d_tabw/1.25);
d_ramp = f2c*(l_grid*((d_height-2)/7+1)/12-r_f2)+d_wall2;
d_edge = ((gridx*l_grid-0.5)/n_divx-d_tabw-d_fo1)/2;
n_st = gridz <= 3 ? 6 : d_edge < 2 && style_tab != 0 && style_tab != 6 ? 1 : style_tab == 1 && n_divx <= 1? 0 : style_tab;
n_x = (n_st==0?1:n_divx);
spacing = (gridx*l_grid-0.5)/(n_divx);
shift = n_st==3?-1:n_st==5?1:0;
shiftauto = function (a,b) n_st!=2?0:a==1?-1:a==b?1:0;
xAll = function (a,b) true;
xCorner = function(a,b) (a==1||a==gridx)&&(b==1||b==gridy);
xEdge = function(a,b) (a==1)||(a==gridx)||(b==1)||(b==gridy);
xAuto = function(a,b) xCorner(a,b) || (a%2==1 && b%2 == 1);
xNone = function(a,b) false;
xFunc = [xAll, xCorner, xEdge, xAuto, xNone];
module gridfinityVase() {
$dh = d_height;
difference() {
union() {
difference() {
block_vase_base();
if (n_st != 6)
transform_style()
transform_vtab_base((n_st<2?gridx*l_grid/n_x-0.5-d_fo1:d_tabw)-nozzle*4)
block_tab_base(-nozzle*sqrt(2));
}
if (enable_scoop_chamfer)
intersection() {
block_vase();
translate([0,gridy*l_grid/2-0.25-d_wall2/2,d_height/2+0.1])
cube([gridx*l_grid,d_wall2,d_height-0.2],center=true);
}
if (enable_funnel && gridz > 3)
pattern_linear((n_st==0?n_divx>1?n_divx:gridx:1), 1, (gridx*l_grid-d_fo1)/(n_st==0?n_divx>1?n_divx:gridx:1))
transform_funnel()
block_funnel_outside();
if (n_divx > 1)
pattern_linear(n_divx-1,1,(gridx*l_grid-0.5)/(n_divx))
block_divider();
if (n_divx < 1)
pattern_linear(n_st == 0 ? n_divx>1 ? n_divx-1 : gridx-1 : 1, 1, (gridx*l_grid-d_fo1)/((n_divx>1 ? n_divx : gridx)))
block_tabsupport();
}
if (enable_funnel && gridz > 3)
pattern_linear((n_st==0?n_divx>1?n_divx:gridx:1), 1, (gridx*l_grid-d_fo1)/(n_st==0?n_divx>1?n_divx:gridx:1))
transform_funnel()
block_funnel_inside();
if (!enable_lip)
translate([0,0,1.5*d_height])
cube([gridx*l_grid,gridy*l_grid,d_height], center=true);
block_x();
block_inset();
if (enable_pinch)
block_pinch(d_height);
if (bottom_layer <= 0)
translate([0,0,-50+layer+0.01])
cube([gridx*l_grid*10,gridy*l_grid*10,100], center=true);
}
}
module gridfinityBaseVase() {
difference() {
union() {
difference() {
intersection() {
block_base_blank(0);
translate([0,0,-h_base-1])
rounded_square([l_grid-0.5-0.005, l_grid-0.5-0.005, h_base*10], r_fo1+0.001, center=true);
}
translate([0,0,0.01])
difference() {
block_base_blank(nozzle*4);
translate([0,0,-h_base])
cube([l_grid*2,l_grid*2,d_bottom*2],center=true);
}
// magic slice
rotate([0,0,90])
translate([0,0,-h_base+d_bottom+0.01])
cube([0.001,l_grid*gridx,d_height+d_bottom*2]);
}
pattern_circular(4)
intersection() {
rotate([0,0,45])
translate([-nozzle,3,-h_base+d_bottom+0.01])
cube([nozzle*2,l_grid*gridx,d_height+d_bottom*2]);
block_base_blank(nozzle*4-0.1);
}
if (enable_holes)
pattern_circular(4)
block_magnet_blank(nozzle);
}
if (enable_holes)
pattern_circular(4)
block_magnet_blank(0, false);
translate([0,0,h_base/2])
cube([l_grid*2, l_grid*2, h_base], center = true);
}
if (style_base != 4)
linear_extrude(d_bottom)
profile_x(0.1);
}
module block_magnet_blank(o = 0, half = true) {
magnet_radius = MAGNET_HOLE_RADIUS + o;
translate([d_hole/2,d_hole/2,-h_base+0.1])
difference() {
hull() {
cylinder(r = magnet_radius, h = MAGNET_HOLE_DEPTH*2, center = true);
cylinder(r = magnet_radius-(h_base+0.1-MAGNET_HOLE_DEPTH), h = (h_base+0.1)*2, center = true);
}
if (half)
mirror([0,0,1])
cylinder(r=magnet_radius*2, h = (h_base+0.1)*4);
}
}
module block_base_blank(o = 0) {
mirror([0,0,1]) {
hull() {
linear_extrude(h_base)
rounded_square(l_grid-o-0.05-2*r_c2-2*r_c1, r_fo3, center=true);
linear_extrude(h_base-r_c1)
rounded_square(l_grid-o-0.05-2*r_c2, r_fo2, center=true);
}
hull() {
linear_extrude(r_c2)
rounded_square(l_grid-o-0.05-2*r_c2, r_fo2, center=true);
mirror([0,0,1])
linear_extrude(d_bottom)
rounded_square(l_grid-o-0.05, r_fo1, center=true);
}
}
}
module block_pinch(height_mm) {
assert(is_num(height_mm));
translate([0, 0, -h_base])
block_wall(gridx, gridy, l_grid) {
translate([d_wall2-nozzle*2-d_clear*2,0,0])
profile_wall(height_mm);
}
}
module block_tabsupport() {
intersection() {
translate([0,0,0.1])
block_vase(d_height*4);
cube([nozzle*2, gridy*l_grid, d_height*3], center=true);
transform_vtab_base(gridx*l_grid*2)
block_tab_base(-nozzle*sqrt(2));
}
}
module block_divider() {
difference() {
intersection() {
translate([0,0,0.1])
block_vase();
cube([nozzle*2, gridy*l_grid, d_height*2], center=true);
}
if (n_st == 0) block_tab(0.1);
else block_divider_edgecut();
// cut divider clearance on negative Y side
translate([-gridx*l_grid/2,-(gridy*l_grid/2-0.25),0])
cube([gridx*l_grid,nozzle*2+0.1,d_height*2]);
// cut divider clearance on positive Y side
mirror([0,1,0])
if (enable_scoop_chamfer)
translate([-gridx*l_grid/2,-(gridy*l_grid/2-0.25),0])
cube([gridx*l_grid,d_wall2+0.1,d_height*2]);
else block_divider_edgecut();
// cut divider to have clearance with scoop
if (enable_scoop_chamfer)
transform_scoop()
offset(delta = 0.1)
polygon([
[0,0],
[d_ramp,d_ramp],
[d_ramp,d_ramp+nozzle/sqrt(2)],
[-nozzle/sqrt(2),0]
]);
}
// divider slices
difference() {
for (i = [0:(d_height-d_bottom)/(layer)]) {
if (2*i*layer < d_height-layer/2-d_bottom-0.1)
mirror([0,1,0])
translate([0,(gridy*l_grid/2-0.25-nozzle)/2,layer/2+d_bottom+2*i*layer])
cube([nozzle*2-0.01,gridy*l_grid/2-0.25-nozzle,layer],center=true);
if ((2*i+1)*layer < d_height-layer/2-d_bottom-0.1)
translate([0,(gridy*l_grid/2-0.25-nozzle)/2,layer/2+d_bottom+(2*i+1)*layer])
cube([nozzle*2-0.01,gridy*l_grid/2-0.25-nozzle,layer],center=true);
}
// divider slices cut to tabs
if (n_st == 0)
transform_style()
transform_vtab_base((n_st<2?gridx*l_grid/n_x-0.5-d_fo1:d_tabw)-nozzle*4)
block_tab_base(-nozzle*sqrt(2));
}
}
module block_divider_edgecut() {
translate([-50,-gridy*l_grid/2+0.25,0])
rotate([90,0,90])
linear_extrude(100)
offset(delta = 0.1)
mirror([1,0,0])
translate([-r_base,0,0])
profile_wall($dh);
}
module transform_funnel() {
if (me > 6 && enable_funnel && gridz > 3 && n_st != 6)
transform_style()
render()
children();
}
module block_funnel_inside() {
intersection() {
block_tabscoop(m-nozzle*3*sqrt(2), 0.003, nozzle*2, 0.01);
block_tab(0.1);
}
}
module block_funnel_outside() {
intersection() {
difference() {
block_tabscoop(m, 0, 0, 0);
block_tabscoop(m-nozzle*4*sqrt(2), 0.003, nozzle*2, -1);
}
block_tab(-nozzle*sqrt(2)/2);
}
}
module block_vase_base() {
difference() {
// base
translate([0,0,-h_base]) {
translate([0,0,-0.1])
color("firebrick")
block_bottom(d_bottom, gridx, gridy, l_grid);
color("royalblue")
block_wall(gridx, gridy, l_grid) {
if (enable_lip) profile_wall($dh);
else profile_wall2($dh);
}
}
// magic slice
rotate([0,0,90])
mirror([0,1,0])
translate([0,0,d_bottom+0.001])
cube([0.001,l_grid*gridx,d_height+d_bottom*2]);
}
// scoop piece
if (enable_scoop_chamfer)
transform_scoop()
polygon([
[0,0],
[d_ramp,d_ramp],
[d_ramp,d_ramp+0.6/sqrt(2)],
[-0.6/sqrt(2),0]
]);
// outside tab cutter
if (n_st != 6)
translate([-(n_x-1)*spacing/2,0,0])
for (i = [1:n_x])
translate([(i-1)*spacing,0,0])
translate([shiftauto(i,n_x)*d_edge + shift*d_edge,0,0])
intersection() {
block_vase();
transform_vtab_base(n_st<2?gridx*l_grid/n_x-0.5-d_fo1:d_tabw)
profile_tab();
}
}
module block_inset() {
ixx = (gridx*l_grid-0.5)/2;
iyy = d_height/2.1;
izz = sqrt(ixx^2+iyy^2)*tan(40);
if (enable_scoop_chamfer && enable_inset)
difference() {
intersection() {
rotate([0,90,0])
translate([-iyy,0,0])
block_inset_sub(iyy, gridx*l_grid, 45);
rotate([0,90,0])
translate([-iyy,0,0])
rotate([0,90,0])
block_inset_sub(ixx, d_height*2, 45);
}
mirror([0,1,0])
translate([-gridx*l_grid/2,-(gridy*l_grid-0.5)/2+d_wall2-2*nozzle,0])
cube([gridx*l_grid,izz,d_height*2]);
}
}
module block_inset_sub(x, y, ang) {
translate([0,(gridy*l_grid-0.5)/2+r_fo1,0])
mirror([0,1,0])
linear_extrude(y,center=true)
polygon([[-x,0],[x,0],[0,x*tan(ang)]]);
}
module transform_style() {
translate([-(n_x-1)*spacing/2,0,0])
for (i = [1:n_x])
translate([(i-1)*spacing,0,0])
translate([shiftauto(i,n_x)*d_edge + shift*d_edge,0,0])
children();
}
module block_flushscoop() {
translate([0,gridy*l_grid/2-d_wall2-nozzle/2-1,d_height/2])
linear_extrude(d_height)
union() {
copy_mirror([1,0,0])
polygon([[0,0],[gridx*l_grid/2-r_fo1,0],[gridx*l_grid/2-r_fo1,1],[gridx*l_grid/2-r_fo1-r_c1*5,d_wall2-nozzle*2+1],[0,d_wall2-nozzle*2+1]]);
}
transform_scoop()
polygon([[0,0],[d_ramp,0],[d_ramp,d_ramp]]);
}
module profile_tab() {
union() {
copy_mirror([0,1,0])
polygon([[0,0],[d_tabh*cos(a_tab),0],[d_tabh*cos(a_tab),d_tabh*sin(a_tab)]]);
}
}
module profile_tabscoop(m) {
polyhedron([[m/2,0,0],[0,-m,0],[-m/2,0,0],[0,0,m]], [[0,2,1],[1,2,3],[0,1,3],[0,3,2]]);
}
module block_tabscoop(a=m, b=0, c=0, d=-1) {
translate([0,d_tabh*cos(a_tab)-l_grid*gridy/2+0.25+b,0])
difference() {
translate([0,0,-d_tabh*sin(a_tab)*2+d_height+2.1])
profile_tabscoop(a);
translate([-gridx*l_grid/2,-m,-m])
cube([gridx*l_grid,m-d_tabh*cos(a_tab)+0.005+c,d_height*20]);
if (d >= 0)
translate([0,0,-d_tabh*sin(a_tab)+d_height+m/2+d+2.1])
cube([gridx*l_grid,gridy*l_grid,m],center=true);
}
}
module transform_vtab(a=0,b=1) {
transform_vtab_base(gridx*l_grid/b-0.5-d_fo1+a)
children();
}
module transform_vtab_base(a) {
translate([0,d_tabh*cos(a_tab)-l_grid*gridy/2+0.25,-d_tabh*sin(a_tab)+d_height+2.1])
rotate([90,0,270])
linear_extrude(a, center=true)
children();
}
module block_tab(del, b=1) {
transform_vtab(-nozzle*4, b)
block_tab_base(del);
}
module block_tab_base(del) {
offset(delta = del)
union() {
profile_tab();
translate([d_tabh*cos(a_tab),-d_tabh*sin(a_tab),0])
square([l_grid,d_tabh*sin(a_tab)*2]);
}
}
module transform_scoop() {
intersection() {
block_vase();
translate([0,gridy*l_grid/2-d_ramp,layer*max(bottom_layer*1)])
rotate([90,0,90])
linear_extrude(2*l_grid*gridx,center=true)
children();
}
}
module block_vase(h = d_height*2) {
translate([0,0,-0.1])
rounded_square([gridx*l_grid-0.5-nozzle, gridy*l_grid-0.5-nozzle, h], r_base+0.01-nozzle/2, center=true);
}
module profile_x(x_f = 3) {
difference() {
square([x_l,x_l],center=true);
pattern_circular(4)
translate([0,nozzle*sqrt(2),0])
rotate([0,0,45])
translate([x_f,x_f,0])
minkowski() {
square([x_l,x_l]);
circle(x_f);
}
}
}
module block_x() {
translate([-(gridx-1)*l_grid/2,-(gridy-1)*l_grid/2,0])
for (i = [1:gridx])
for (j = [1:gridy])
if (xFunc[style_base](i,j))
translate([(i-1)*l_grid,(j-1)*l_grid,0])
block_x_sub();
}
module block_x_sub() {
linear_extrude(d_bottom*2+0.01,center=true)
offset(0.05)
profile_x();
}

BIN
images/base_dimension.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
images/baseplate.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
images/custom_dimension.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
images/height_dimension.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
images/holes_dimension.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

BIN
images/lite.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

BIN
images/slicer_base.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

BIN
images/slicer_bin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
images/slicer_holes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

BIN
images/slicer_holes_top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

BIN
images/spin.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

BIN
images/tab_dimension.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
images/vase_base.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
images/vase_bottom.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
images/vase_dividers.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
images/vase_tabs.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

12
mkdocs.yml Normal file
View File

@@ -0,0 +1,12 @@
site_name: Gridfinity Rebuilt
theme: readthedocs
nav:
- Home: index.md
- Bins: bins.md
- Baseplates: baseplates.md
- Lite: lite.md
- Vase: vase.md
- Constants: constants.md
plugins:
- search
- autorefs

167
standard.scad Normal file
View File

@@ -0,0 +1,167 @@
// height of the base
h_base = 5;
// lower base chamfer "radius"
r_c1 = 0.8;
// upper base chamfer "radius"
r_c2 = 2.4;
// bottom thiccness of bin
h_bot = 2.2;
// outside radii 1
r_fo1 = 7.5 / 2;
// outside radii 2
r_fo2 = 3.2 / 2;
// outside radii 3
r_fo3 = 1.6 / 2;
// length of a grid unit
l_grid = 42;
// Outside rounded radius of bin
// Per spec, matches radius of upper base section.
r_base = r_fo1;
// Tollerance to make sure cuts don't leave a sliver behind,
// and that items are properly connected to each other.
TOLLERANCE = 0.01;
// ****************************************
// Magnet / Screw Hole Constants
// ****************************************
LAYER_HEIGHT = 0.2;
MAGNET_HEIGHT = 2;
SCREW_HOLE_RADIUS = 3 / 2;
MAGNET_HOLE_RADIUS = 6.5 / 2;
MAGNET_HOLE_DEPTH = MAGNET_HEIGHT + (LAYER_HEIGHT * 2);
// center-to-center distance between holes
d_hole = 26;
// distance of hole from side of bin
d_hole_from_side=8;
// Meassured diameter in Fusion360.
// Smaller than the magnet to keep it squeezed.
REFINED_HOLE_RADIUS = 5.86 / 2;
REFINED_HOLE_HEIGHT = MAGNET_HEIGHT - 0.1;
// How many layers are between a Gridfinity Refined Hole and the bottom
REFINED_HOLE_BOTTOM_LAYERS = 2;
// Experimentally chosen for a press fit.
MAGNET_HOLE_CRUSH_RIB_INNER_RADIUS = 5.9 / 2;
// Mostly arbitrarily chosen.
// 30 ribs does not print with a 0.4mm nozzle.
// Anything 5 or under produces a hole that is not round.
MAGNET_HOLE_CRUSH_RIB_COUNT = 8;
// Radius to add when chamfering magnet and screw holes.
CHAMFER_ADDITIONAL_RADIUS = 0.8;
CHAMFER_ANGLE = 45;
// When countersinking the baseplate, how much to add to the screw radius.
BASEPLATE_SCREW_COUNTERSINK_ADDITIONAL_RADIUS = 5/2;
BASEPLATE_SCREW_COUNTERBORE_RADIUS = 5.5/2;
BASEPLATE_SCREW_COUNTERBORE_HEIGHT = 3;
// ****************************************
// top edge fillet radius
r_f1 = 0.6;
// internal fillet radius
r_f2 = 2.8;
// width of divider between compartments
d_div = 1.2;
// minimum wall thickness
d_wall = 0.95;
// tolerance fit factor
d_clear = 0.25;
// height of tab (yaxis, measured from inner wall)
d_tabh = 15.85;
// maximum width of tab
d_tabw = 42;
// angle of tab
a_tab = 36;
// lip height
h_lip = 3.548;
d_wall2 = r_base-r_c1-d_clear*sqrt(2);
d_magic = -2*d_clear-2*d_wall+d_div;
// ****************************************
// Stacking Lip Constants
// Based on https://gridfinity.xyz/specification/
// ****************************************
stacking_lip_inner_slope_height_mm = 0.7;
stacking_lip_wall_height_mm = 1.8;
stacking_lip_outer_slope_height_mm = 1.9;
stacking_lip_depth =
stacking_lip_inner_slope_height_mm +
stacking_lip_outer_slope_height_mm;
stacking_lip_height =
stacking_lip_inner_slope_height_mm +
stacking_lip_wall_height_mm +
stacking_lip_outer_slope_height_mm;
// Extracted from `profile_wall_sub_sub`.
stacking_lip_support_wall_height_mm = 1.2;
stacking_lip_support_height_mm =
stacking_lip_support_wall_height_mm + d_wall2;
// ****************************************
// Baseplate constants
// Based on https://gridfinity.xyz/specification/
// ****************************************
BASEPLATE_OUTSIDE_RADIUS = 8 / 2;
// Polygon describing the raw baseplate lip.
// Does NOT include clearance height.
BASEPLATE_LIP = [
[0, 0], // Innermost bottom point
[0.7, 0.7], // Up and out at a 45 degree angle
[0.7, (0.7+1.8)], // Straight up
[(0.7+2.15), (0.7+1.8+2.15)], // Up and out at a 45 degree angle
[(0.7+2.15), 0], // Straight down
[0, 0] //Back to start
];
// Height of the baseplate lip.
// This ads clearance height to the polygon
// that ensures the base makes contact with the baseplate lip.
BASEPLATE_LIP_HEIGHT = 5;
// The minimum height between the baseplate lip and anything below it.
// Needed to make sure the base always makes contact with the baseplate lip.
BASEPLATE_CLEARANCE_HEIGHT = BASEPLATE_LIP_HEIGHT - BASEPLATE_LIP[3].y;
assert(BASEPLATE_CLEARANCE_HEIGHT > 0, "Negative clearance doesn't make sense.");
// Maximum [x,y] values/size of the baseplate lip.
// Includes clearance height!
BASEPLATE_LIP_MAX = [BASEPLATE_LIP[3].x, BASEPLATE_LIP_HEIGHT];
// ****************************************
// Weighted Baseplate
// ****************************************
// Baseplate bottom part height (part added with weigthed=true)
bp_h_bot = 6.4;
// Baseplate bottom cutout rectangle size
bp_cut_size = 21.4;
// Baseplate bottom cutout rectangle depth
bp_cut_depth = 4;
// Baseplate bottom cutout rounded thingy width
bp_rcut_width = 8.5;
// Baseplate bottom cutout rounded thingy left
bp_rcut_length = 4.25;
// Baseplate bottom cutout rounded thingy depth
bp_rcut_depth = 2;
// ****************************************
// Baseplate clearance offset
bp_xy_clearance = 0.5;
// radius of cutout for skeletonized baseplate
r_skel = 2;
// minimum baseplate thickness (when skeletonized)
h_skel = 1;

View File

@@ -0,0 +1,24 @@
{
"fileFormatVersion": "1",
"parameterSets": {
"Default": {
"$fa": "8",
"$fs": "0.25",
"d_screw": "3.3500000000000001",
"d_screw_head": "5",
"distancex": "0",
"distancey": "0",
"chamfer_holes": "true",
"crush_ribs": "true",
"enable_magnet": "true",
"fitx": "0",
"fity": "0",
"gridx": "1",
"gridy": "1",
"n_screws": "1",
"screw_spacing": "0.5",
"style_hole": "1",
"style_plate": "2"
}
}
}

View File

@@ -0,0 +1,36 @@
{
"fileFormatVersion": "1",
"parameterSets": {
"Default": {
"$fa": "8",
"$fs": "0.25",
"c_chamfer": "0.5",
"c_depth": "1",
"c_orientation": "2",
"cd": "10",
"cdivx": "0",
"cdivy": "0",
"ch": "1",
"chamfer_holes": "true",
"crush_ribs": "true",
"div_base_x": "0",
"div_base_y": "0",
"divx": "0",
"divy": "0",
"enable_zsnap": "false",
"gridx": "1",
"gridy": "1",
"gridz": "6",
"gridz_define": "0",
"height_internal": "0",
"magnet_holes": "false",
"only_corners": "false",
"printable_hole_top": "true",
"refined_holes": "true",
"scoop": "0",
"screw_holes": "false",
"style_lip": "0",
"style_tab": "1"
}
}
}

View File

@@ -0,0 +1,28 @@
{
"fileFormatVersion": "1",
"parameterSets": {
"Default": {
"$fa": "8",
"$fs": "0.25",
"a_tab": "40",
"bottom_layer": "3",
"enable_funnel": "true",
"enable_holes": "true",
"enable_inset": "true",
"enable_lip": "true",
"enable_pinch": "true",
"enable_scoop_chamfer": "true",
"enable_zsnap": "false",
"gridx": "1",
"gridy": "1",
"gridz": "6",
"gridz_define": "0",
"layer": "0.35",
"n_divx": "2",
"nozzle": "0.6",
"style_base": "0",
"style_tab": "0",
"type": "0"
}
}
}

167
tests/openscad_runner.py Normal file
View File

@@ -0,0 +1,167 @@
"""
Helpful classes for running OpenScad from Python.
@Copyright Arthur Moore 2024 MIT License
"""
from __future__ import annotations
import json
import subprocess
from dataclasses import dataclass, is_dataclass, asdict
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import NamedTuple, Optional
class DataClassJSONEncoder(json.JSONEncoder):
'''Allow json serialization'''
def default(self, o):
if is_dataclass(o):
return asdict(o)
# Let the base class default method raise the TypeError
return super().default(o)
class Vec3(NamedTuple):
'''Simple 3d Vector (x, y, z)'''
x: float
y: float
z: float
@dataclass(frozen=True)
class CameraArguments:
"""
Controls the camera position when outputting to png format.
@see `openscad -h`.
Supports fluid interface.
"""
translate: Vec3
rotate: Vec3
distance: float
def with_translation(self, new_translate: Vec3) -> CameraArguments:
return CameraArguments(translate=new_translate, rotate=self.rotate, distance=self.distance)
def with_rotation(self, new_rotate: Vec3) -> CameraArguments:
return CameraArguments(translate=self.translate, rotate=new_rotate, distance=self.distance)
def with_distance(self, new_distance: float) -> CameraArguments:
return CameraArguments(translate=self.translate, rotate=rotate, distance=new_distance)
def as_argument(self) -> str:
return '--camera=' \
f'{",".join(map(str,self.translate))},{",".join(map(str,self.rotate))},{self.distance}'
@dataclass(kw_only=True, frozen=True)
class ParameterFile:
parameterSets: dict[str, dict]
fileFormatVersion: int = 1
@classmethod
def from_json(cls, *pargs, **nargs):
"""
Wrapper for `json.loads`, with some post-processing.
The Customizer saves everything as strings. --Arthur 2024-04-28
"""
nargs["object_pairs_hook"] = cls.object_pairs_hook
file = ParameterFile(**json.loads(*pargs, **nargs))
assert(file.fileFormatVersion == 1)
return file
@classmethod
def object_pairs_hook(self, pairs: list[tuple]):
'''Fixes customizer turning everything into strings'''
output = dict(pairs)
for (key, value) in output.items():
if(type(value) == str):
if(value == "true"):
output[key] = True
continue
if(value == "false"):
output[key] = False
continue
try:
output[key] = float(value)
except ValueError:
pass
return output
def set_variable_argument(var: str, val: str) -> [str, str]:
"""
Allows setting a variable to a particular value.
@warning value **can** be a function, but this is called for every file, so may generate 'undefined' warnings.
"""
return ['-D', f'{var}={str(val)}']
class CameraRotations:
'''Pre-defined useful camera rotations'''
Default = Vec3(0,0,0),
AngledTop = Vec3(45,0,45)
AngledBottom = Vec3(225,0,225)
Top = Vec3(45,0,0)
class OpenScadRunner:
'''Helper to run the openscad binary'''
scad_file_path: Path
openscad_binary_path: Path
image_folder_base: Path
parameters: Optional[dict]
'''If set, a temporary parameter file is created, and used with these variables'''
WINDOWS_DEFAULT_PATH = 'C:\\Program Files\\OpenSCAD\\openscad.exe'
TOP_ANGLE_CAMERA = CameraArguments(Vec3(0,0,0),Vec3(45,0,45),150)
common_arguments = [
#'--hardwarnings', # Does not work when setting variables by using functions
'--enable=fast-csg',
'--enable=predictible-output',
'--imgsize=1280,720',
'--view=axes',
'--projection=ortho',
#"--summary", "all",
#"--summary-file", "-"
] + \
set_variable_argument('$fa', 8) + set_variable_argument('$fs', 0.25)
def __init__(self, file_path: Path):
self.openscad_binary_path = self.WINDOWS_DEFAULT_PATH
self.scad_file_path = file_path
self.image_folder_base = Path('.')
self.camera_arguments = None
self.parameters = None
def create_image(self, args: [str], image_file_name: str) -> subprocess.CompletedProcess:
"""
Run the code and create an image.
@Important The only verification is that no errors occured.
There is no verification if the image was created, or the image contents.
"""
assert(self.scad_file_path.exists())
assert(self.image_folder_base.exists())
image_path = self.image_folder_base.joinpath(image_file_name)
command_arguments = self.common_arguments + \
([self.camera_arguments.as_argument()] if self.camera_arguments != None else []) + \
args + \
["-o", str(image_path), str(self.scad_file_path)]
#print(command_arguments)
if self.parameters != None:
#print(self.parameters)
params = ParameterFile(parameterSets={"python_generated": self.parameters})
with NamedTemporaryFile(prefix="gridfinity-rebuilt-", suffix=".json", mode='wt',delete_on_close=False) as file:
json.dump(params, file, sort_keys=True, indent=2, cls=DataClassJSONEncoder)
file.close()
command_arguments += ["-p", file.name, "-P", "python_generated"]
return self._run(command_arguments)
else:
return self._run(command_arguments)
def _run(self, args: [str]) -> subprocess.CompletedProcess:
"""
Run openscad with the passed in arguments.
"""
output = subprocess.run([self.openscad_binary_path]+args, capture_output=True)
error_strings = output.stderr.decode().strip().splitlines()
if any(line.startswith("ERROR:") for line in error_strings):
# OpenSCAD doesn't set an error return if it errors from bad SCAD code!
output.returncode = 11
output.check_returncode()
return output

116
tests/test_baseplate.py Normal file
View File

@@ -0,0 +1,116 @@
"""
Tests for gridfinity-rebuilt-baseplate.scad
@Copyright Arthur Moore 2024 MIT License
"""
import dataclasses
import json
import unittest
from pathlib import Path
from tempfile import NamedTemporaryFile
from openscad_runner import *
class TestBasePlateHoles(unittest.TestCase):
"""
Test creating a single base in "gridfinity-spiral-vase.scad"
Currently only makes sure code runs, and outputs pictures for manual verification.
"""
@classmethod
def setUpClass(cls):
parameter_file_path = Path("gridfinity-rebuilt-baseplate.json")
parameter_file_data = ParameterFile.from_json(parameter_file_path.read_text())
cls.default_parameters = parameter_file_data.parameterSets["Default"]
def setUp(self):
self.scad_runner = OpenScadRunner(Path('../gridfinity-rebuilt-baseplate.scad'))
self.scad_runner.image_folder_base = Path('../images/baseplate/')
self.scad_runner.parameters = self.default_parameters.copy()
self.scad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledBottom, 150)
def test_no_holes(self):
vars = self.scad_runner.parameters
vars["enable_magnet"] = False
vars["style_hole"] = 0
self.scad_runner.create_image([], Path('no_holes_bottom.png'))
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
self.scad_runner.create_image([], Path('no_holes_top.png'))
def test_plain_magnet_holes(self):
vars = self.scad_runner.parameters
vars["enable_magnet"] = True
vars["style_hole"] = 0
vars["chamfer_holes"] = False
vars["crush_ribs"] = False
self.scad_runner.create_image([], Path('magnet_holes_bottom.png'))
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
self.scad_runner.create_image([], Path('plain_magnet_holes_top.png'))
def test_chamfered_magnet_holes(self):
vars = self.scad_runner.parameters
vars["enable_magnet"] = True
vars["style_hole"] = 0
vars["chamfer_holes"] = True
vars["crush_ribs"] = False
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
self.scad_runner.create_image([], Path('chamfered_magnet_holes.png'))
def test_ribbed_magnet_holes(self):
vars = self.scad_runner.parameters
vars["enable_magnet"] = True
vars["style_hole"] = 0
vars["chamfer_holes"] = False
vars["crush_ribs"] = True
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
self.scad_runner.create_image([], Path('ribbed_magnet_holes.png'))
def test_chamfered_and_ribbed_magnet_holes(self):
vars = self.scad_runner.parameters
vars["enable_magnet"] = True
vars["style_hole"] = 0
vars["chamfer_holes"] = True
vars["crush_ribs"] = True
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
self.scad_runner.create_image([], Path('chamfered_and_ribbed_magnet_holes.png'))
def test_only_countersunk_screw_holes(self):
vars = self.scad_runner.parameters
vars["enable_magnet"] = False
vars["style_hole"] = 1
self.scad_runner.create_image([], Path('only_countersunk_screw_holes_bottom.png'))
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
self.scad_runner.create_image([], Path('only_countersunk_screw_holes_top.png'))
def test_only_counterbored_screw_holes(self):
vars = self.scad_runner.parameters
vars["enable_magnet"] = False
vars["style_hole"] = 2
self.scad_runner.create_image([], Path('only_counterbored_screw_holes_bottom.png'))
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
self.scad_runner.create_image([], Path('only_counterbored_screw_holes_top.png'))
def test_magnet_and_countersunk_screw_holes(self):
vars = self.scad_runner.parameters
vars["enable_magnet"] = True
vars["chamfer_holes"] = False
vars["crush_ribs"] = False
vars["style_hole"] = 1
self.scad_runner.create_image([], Path('magnet_and_countersunk_screw_holes_bottom.png'))
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
self.scad_runner.create_image([], Path('magnet_and_countersunk_screw_holes_top.png'))
def test_magnet_and_counterbored_screw_holes(self):
vars = self.scad_runner.parameters
vars["enable_magnet"] = True
vars["chamfer_holes"] = False
vars["crush_ribs"] = False
vars["style_hole"] = 2
self.scad_runner.create_image([], Path('magnet_and_counterbored_screw_holes_bottom.png'))
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledTop)
self.scad_runner.create_image([], Path('magnet_and_counterbored_screw_holes_top.png'))
if __name__ == '__main__':
unittest.main()

150
tests/test_bins.py Normal file
View File

@@ -0,0 +1,150 @@
"""
Tests for gridfinity-rebuilt-bins.scad
@Copyright Arthur Moore 2024 MIT License
"""
import dataclasses
import json
import unittest
from pathlib import Path
from tempfile import NamedTemporaryFile
from openscad_runner import *
class TestBinHoles(unittest.TestCase):
"""
Test how a single base looks with holes cut out.
Currently only makes sure code runs, and outputs pictures for manual verification.
"""
@classmethod
def setUpClass(cls):
parameter_file_path = Path("gridfinity-rebuilt-bins.json")
parameter_file_data = ParameterFile.from_json(parameter_file_path.read_text())
cls.default_parameters = parameter_file_data.parameterSets["Default"]
def setUp(self):
self.scad_runner = OpenScadRunner(Path('../gridfinity-rebuilt-bins.scad'))
self.scad_runner.image_folder_base = Path('../images/base_hole_options/')
self.scad_runner.parameters = self.default_parameters.copy()
self.scad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledBottom, 150)
def test_no_holes(self):
vars = self.scad_runner.parameters
vars["refined_holes"] = False
vars["magnet_holes"] = False
vars["screw_holes"] = False
self.scad_runner.create_image([], Path('no_holes.png'))
def test_only_corner_holes(self):
vars = self.scad_runner.parameters
vars["refined_holes"] = True
vars["magnet_holes"] = False
vars["screw_holes"] = False
vars["only_corners"] = True
self.scad_runner.create_image([], Path('only_corner_holes.png'))
def test_refined_holes(self):
vars = self.scad_runner.parameters
vars["refined_holes"] = True
vars["magnet_holes"] = False
vars["screw_holes"] = False
self.scad_runner.create_image([], Path('refined_holes.png'))
def test_refined_and_screw_holes(self):
vars = self.scad_runner.parameters
vars["refined_holes"] = True
vars["magnet_holes"] = False
vars["screw_holes"] = True
vars["printable_hole_top"] = False
self.scad_runner.create_image([], Path('refined_and_screw_holes.png'))
def test_screw_holes_plain(self):
vars = self.scad_runner.parameters
vars["refined_holes"] = False
vars["magnet_holes"] = False
vars["screw_holes"] = True
vars["printable_hole_top"] = False
self.scad_runner.create_image([], Path('screw_holes_plain.png'))
def test_screw_holes_printable(self):
vars = self.scad_runner.parameters
vars["refined_holes"] = False
vars["magnet_holes"] = False
vars["screw_holes"] = True
vars["printable_hole_top"] = True
self.scad_runner.create_image([], Path('screw_holes_printable.png'))
def test_magnet_holes_plain(self):
vars = self.scad_runner.parameters
vars["refined_holes"] = False
vars["magnet_holes"] = True
vars["screw_holes"] = False
vars["crush_ribs"] = False
vars["chamfer_holes"] = False
vars["printable_hole_top"] = False
self.scad_runner.create_image([], Path('magnet_holes_plain.png'))
def test_magnet_holes_chamfered(self):
vars = self.scad_runner.parameters
vars["refined_holes"] = False
vars["magnet_holes"] = True
vars["screw_holes"] = False
vars["crush_ribs"] = False
vars["chamfer_holes"] = True
vars["printable_hole_top"] = False
self.scad_runner.create_image([], Path('magnet_holes_chamfered.png'))
def test_magnet_holes_printable(self):
vars = self.scad_runner.parameters
vars["refined_holes"] = False
vars["magnet_holes"] = True
vars["screw_holes"] = False
vars["crush_ribs"] = False
vars["chamfer_holes"] = False
vars["printable_hole_top"] = True
self.scad_runner.create_image([], Path('magnet_holes_printable.png'))
def test_magnet_holes_with_crush_ribs(self):
vars = self.scad_runner.parameters
vars["refined_holes"] = False
vars["magnet_holes"] = True
vars["screw_holes"] = False
vars["crush_ribs"] = True
vars["chamfer_holes"] = False
vars["printable_hole_top"] = False
self.scad_runner.create_image([], Path('magnet_holes_with_crush_ribs.png'))
def test_magnet_and_screw_holes_plain(self):
vars = self.scad_runner.parameters
vars["refined_holes"] = False
vars["magnet_holes"] = True
vars["screw_holes"] = True
vars["crush_ribs"] = False
vars["chamfer_holes"] = False
vars["printable_hole_top"] = False
self.scad_runner.create_image([], Path('magnet_and_screw_holes_plain.png'))
def test_magnet_and_screw_holes_printable(self):
vars = self.scad_runner.parameters
vars["refined_holes"] = False
vars["magnet_holes"] = True
vars["screw_holes"] = True
vars["crush_ribs"] = False
vars["chamfer_holes"] = False
vars["printable_hole_top"] = True
self.scad_runner.create_image([], Path('magnet_and_screw_holes_printable.png'))
def test_magnet_and_screw_holes_all(self):
vars = self.scad_runner.parameters
vars["refined_holes"] = False
vars["magnet_holes"] = True
vars["screw_holes"] = True
vars["crush_ribs"] = True
vars["chamfer_holes"] = True
vars["printable_hole_top"] = True
self.scad_runner.create_image([], Path('magnet_and_screw_holes_all.png'))
if __name__ == '__main__':
unittest.main()

78
tests/test_holes.py Normal file
View File

@@ -0,0 +1,78 @@
"""
Tests for gridfinity-rebuilt-holes.scad
@Copyright Arthur Moore 2024 MIT License
"""
from pathlib import Path
from openscad_runner import *
import unittest
class TestHoleCutouts(unittest.TestCase):
"""
Test Hole Cutouts. The negatives used with `difference()` to create a hole.
Currently only makes sure code runs, and outputs pictures for manual verification.
"""
def setUp(self):
self.scad_runner = OpenScadRunner(Path('../gridfinity-rebuilt-holes.scad'))
self.scad_runner.image_folder_base = Path('../images/hole_cutouts/')
self.scad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledTop, 50)
def test_refined_hole(self):
"""
refined_hole() is special, since top_angle_camera is not appropriate for it.
"""
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.AngledBottom)
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=true, magnet_hole=false, screw_hole=false, crush_ribs=false, chamfer=false, supportless=false)')
self.scad_runner.create_image(test_args, Path('refined_hole.png'))
def test_plain_magnet_hole(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=false, crush_ribs=false, chamfer=false, supportless=false)')
self.scad_runner.create_image(test_args, Path('magnet_hole.png'))
def test_plain_screw_hole(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=false, screw_hole=true, crush_ribs=false, chamfer=false, supportless=false)')
self.scad_runner.create_image(test_args, Path('screw_hole.png'))
def test_magnet_and_screw_hole(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=true, crush_ribs=false, chamfer=false, supportless=false)')
self.scad_runner.create_image(test_args, Path('magnet_and_screw_hole.png'))
def test_chamfered_magnet_hole(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=false, crush_ribs=false, chamfer=true, supportless=false)')
self.scad_runner.create_image(test_args, Path('chamfered_magnet_hole.png'))
def test_magnet_hole_crush_ribs(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=false, crush_ribs=true, chamfer=false, supportless=false)')
self.scad_runner.create_image(test_args, Path('magnet_hole_crush_ribs.png'))
def test_magnet_hole_supportless(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=false, crush_ribs=false, chamfer=false, supportless=true)')
self.scad_runner.create_image(test_args, Path('magnet_hole_supportless.png'))
def test_magnet_and_screw_hole_supportless(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=true, crush_ribs=false, chamfer=false, supportless=true)')
self.scad_runner.create_image(test_args, Path('magnet_and_screw_hole_supportless.png'))
def test_all_hole_options(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=true, screw_hole=true, crush_ribs=true, chamfer=true, supportless=true)')
self.scad_runner.create_image(test_args, Path('all_hole_options.png'))
def test_no_hole(self):
test_args = set_variable_argument('test_options',
'bundle_hole_options(refined_hole=false, magnet_hole=false, screw_hole=false, crush_ribs=true, chamfer=true, supportless=true)')
self.scad_runner.create_image(test_args, Path('no_hole.png'))
if __name__ == '__main__':
unittest.main()

50
tests/test_spiral_vase.py Normal file
View File

@@ -0,0 +1,50 @@
"""
Tests for gridfinity-spiral-vase.scad
@Copyright Arthur Moore 2024 MIT License
"""
import dataclasses
import json
import unittest
from pathlib import Path
from tempfile import NamedTemporaryFile
from openscad_runner import *
class TestSpiralVaseBase(unittest.TestCase):
"""
Test creating a single base in "gridfinity-spiral-vase.scad"
Currently only makes sure code runs, and outputs pictures for manual verification.
"""
@classmethod
def setUpClass(cls):
parameter_file_path = Path("gridfinity-spiral-vase.json")
parameter_file_data = ParameterFile.from_json(parameter_file_path.read_text())
cls.default_parameters = parameter_file_data.parameterSets["Default"]
def setUp(self):
self.scad_runner = OpenScadRunner(Path('../gridfinity-spiral-vase.scad'))
self.scad_runner.image_folder_base = Path('../images/spiral_vase_base/')
self.scad_runner.parameters = self.default_parameters.copy()
self.scad_runner.parameters["type"] = 1 # Create a Base
self.scad_runner.camera_arguments = CameraArguments(Vec3(0,0,0), CameraRotations.AngledBottom, 150)
def test_no_holes(self):
vars = self.scad_runner.parameters
vars["enable_holes"] = False
self.scad_runner.create_image([], Path('no_holes_bottom.png'))
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.Top)
self.scad_runner.create_image([], Path('no_holes_top.png'))
def test_refined_holes(self):
vars = self.scad_runner.parameters
vars["enable_holes"] = True
self.scad_runner.create_image([], Path('with_holes_bottom.png'))
self.scad_runner.camera_arguments = self.scad_runner.camera_arguments.with_rotation(CameraRotations.Top)
self.scad_runner.create_image([], Path('with_holes_top.png'))
if __name__ == '__main__':
unittest.main()