├── generators ├── __init__.py ├── common │ ├── __init__.py │ ├── settings_form.py │ ├── settings_form.html │ └── bin_base.py ├── baseplate │ ├── baseplate_settings.py │ ├── baseplate_description.html │ ├── baseplate_form.py │ ├── main.py │ └── baseplate_generator.py ├── lightbin │ ├── lightbin_description.html │ ├── lightbin_settings.py │ ├── main.py │ ├── lightbin_form.py │ └── lightbin_generator.py ├── solidbin │ ├── solidbin_description.html │ ├── solidbin_settings.py │ ├── solidbin_form.py │ ├── main.py │ └── solidbin_generator.py ├── holeybin │ ├── holeybin_description.html │ ├── holeybin_settings.py │ ├── main.py │ ├── holeybin_form.py │ ├── holeybin_settings_form.html │ └── holeybin_generator.py └── classicbin │ ├── classicbin_description.html │ ├── classicbin_settings.py │ ├── main.py │ ├── classicbin_form.py │ └── classicbin_generator.py ├── version.py ├── .env.container ├── .gitignore ├── debug.sh ├── deploy.sh ├── static ├── holeybin_sample.jpg ├── lightbin_sample.jpg ├── solidbin_sample.jpg ├── baseplate_sample.jpg ├── classicbin_sample.jpg ├── holeybin_holeshape.jpg ├── holeybin_holesize.jpg ├── holeybin_numholes.jpg ├── labeltab_options.jpg ├── scoopramp_options.jpg ├── magnet_hole_options.jpg ├── stackinglip_options.jpg ├── holeybin_keepoutdiameter.jpg └── multirow_labeltab_options.jpg ├── .github ├── FUNDING.yml └── workflows │ └── serverstatus.yml ├── .env.container.template ├── requirements.txt ├── help_files ├── scoopramp_help.html ├── stackinglip_help.html ├── labeltab_help.html ├── holey_shape_help.html ├── export_format_help.html ├── size_help.html ├── compartment_help.html ├── holey_size_help.html ├── holey_keepout_help.html ├── holey_numholes_help.html └── magnet_help.html ├── docker-compose.debug.yml ├── .vscode └── tasks.json ├── Dockerfile ├── CITATION.cff ├── docker-compose.yml ├── .all-contributorsrc ├── templates ├── base.html.j2 ├── front_page.html.j2 ├── settings_offcanvas.html.j2 └── index.html.j2 ├── help_provider.py ├── grid_constants.py ├── README.md ├── gfg_main.py └── LICENSE /generators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /generators/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.4.5" 2 | -------------------------------------------------------------------------------- /.env.container: -------------------------------------------------------------------------------- 1 | GFG_DOMAIN=gridfinity.bouwens.co 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | logs 3 | 4 | .vscode 5 | -------------------------------------------------------------------------------- /debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker-compose --env-file ./.env.container -f docker-compose.debug.yml up -d --build 4 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker-compose --env-file ./.env.container up --build --force-recreate --no-deps -d 4 | -------------------------------------------------------------------------------- /static/holeybin_sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroen94704/gridfinitycreator/HEAD/static/holeybin_sample.jpg -------------------------------------------------------------------------------- /static/lightbin_sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroen94704/gridfinitycreator/HEAD/static/lightbin_sample.jpg -------------------------------------------------------------------------------- /static/solidbin_sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroen94704/gridfinitycreator/HEAD/static/solidbin_sample.jpg -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: jeroen94704 4 | ko_fi: jeroen94704 5 | -------------------------------------------------------------------------------- /static/baseplate_sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroen94704/gridfinitycreator/HEAD/static/baseplate_sample.jpg -------------------------------------------------------------------------------- /static/classicbin_sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroen94704/gridfinitycreator/HEAD/static/classicbin_sample.jpg -------------------------------------------------------------------------------- /static/holeybin_holeshape.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroen94704/gridfinitycreator/HEAD/static/holeybin_holeshape.jpg -------------------------------------------------------------------------------- /static/holeybin_holesize.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroen94704/gridfinitycreator/HEAD/static/holeybin_holesize.jpg -------------------------------------------------------------------------------- /static/holeybin_numholes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroen94704/gridfinitycreator/HEAD/static/holeybin_numholes.jpg -------------------------------------------------------------------------------- /static/labeltab_options.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroen94704/gridfinitycreator/HEAD/static/labeltab_options.jpg -------------------------------------------------------------------------------- /static/scoopramp_options.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroen94704/gridfinitycreator/HEAD/static/scoopramp_options.jpg -------------------------------------------------------------------------------- /static/magnet_hole_options.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroen94704/gridfinitycreator/HEAD/static/magnet_hole_options.jpg -------------------------------------------------------------------------------- /static/stackinglip_options.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroen94704/gridfinitycreator/HEAD/static/stackinglip_options.jpg -------------------------------------------------------------------------------- /static/holeybin_keepoutdiameter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroen94704/gridfinitycreator/HEAD/static/holeybin_keepoutdiameter.jpg -------------------------------------------------------------------------------- /static/multirow_labeltab_options.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroen94704/gridfinitycreator/HEAD/static/multirow_labeltab_options.jpg -------------------------------------------------------------------------------- /generators/common/settings_form.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | def get_standard_settings_form(): 4 | with open(os.path.dirname(__file__) + '/settings_form.html', 'r') as reader: 5 | return reader.read() -------------------------------------------------------------------------------- /generators/baseplate/baseplate_settings.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | # Generator inputs 4 | @dataclass 5 | class Settings: 6 | sizeUnitsX: int = 2 # Width (X) of the brick in grid units 7 | sizeUnitsY: int = 2 # Length (Y) of the brick in grid units -------------------------------------------------------------------------------- /.env.container.template: -------------------------------------------------------------------------------- 1 | # Replace YOURDOMAIN with your own domain here (e.g. gridfinity.megacorp.com). Don't use quotes or anything like that. 2 | GFG_DOMAIN=YOURDOMAIN 3 | 4 | # Set DATA_ROOT to the local path where you want the log files of GFCreator to go 5 | DATA_ROOT=/path/to/your/logs -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click>=8.0.3 2 | colorama>=0.4.4 3 | Flask>=2.0.2 4 | itsdangerous>=2.0.1 5 | Jinja2>=3.0.3 6 | MarkupSafe>=2.0.1 7 | Werkzeug>=2.0.2 8 | gunicorn>=20.1.0 9 | Flask-WTF>=1.1.1 10 | bootstrap-flask>=2.2.0 11 | waitress>=2.1.2 12 | gunicorn>=20.1.0 13 | nlopt>=2.9.0 -------------------------------------------------------------------------------- /help_files/scoopramp_help.html: -------------------------------------------------------------------------------- 1 |

Bins can be generated with or without a scoop ramp, which helps pick small parts out of the bin with your finger. In bins with compartments there is a scoop ramp for each row:

2 | 3 | 4 | -------------------------------------------------------------------------------- /help_files/stackinglip_help.html: -------------------------------------------------------------------------------- 1 |

Bins can be generated with or without a stacking lip. The stacking lip is an extra raised edge that allows bins to be stacked on top of each other. Below is what a bin looks like with and without the stacking lip.

2 | 3 | -------------------------------------------------------------------------------- /generators/baseplate/baseplate_description.html: -------------------------------------------------------------------------------- 1 |
Description
2 |

Bare bones baseplate. No screws or magnets, just the minimum required to snugly hold your bins.

3 | 4 |
Parameters
5 | 9 | -------------------------------------------------------------------------------- /help_files/labeltab_help.html: -------------------------------------------------------------------------------- 1 |

Bins can be generated with or without a label tab:

2 | 3 | 4 | 5 |

For bins that contain compartments it is possible to have a label tab for each row of compartments:

6 | 7 | -------------------------------------------------------------------------------- /help_files/holey_shape_help.html: -------------------------------------------------------------------------------- 1 |

The hole shape controls the shape of the holes in the grid. Hex holes can be used for screw-bits, circular holes for things like batteries or drill-bits. Squares are useful for ... square things.

2 | 3 |

Here you can see the options available for the hole shape:

4 | 5 | 6 | -------------------------------------------------------------------------------- /help_files/export_format_help.html: -------------------------------------------------------------------------------- 1 |

Files can be generated either in STL or STEP format. STL is widely supported by 3D print software, but STEP is better suited if you intend to modify the model manually

2 | 3 |

(Before asking for 3MF support: The framework used to generate these files does have the option to export to 3MF, but this is currently a little buggy, and produces files that don't slice well)

-------------------------------------------------------------------------------- /.github/workflows/serverstatus.yml: -------------------------------------------------------------------------------- 1 | name: "Check for website health" 2 | 3 | on: 4 | schedule: 5 | - cron: "26 23 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | health_check_badge_job: 10 | runs-on: ubuntu-latest 11 | name: Check status of GC server 12 | steps: 13 | - name: Health check 14 | uses: 102/website-healthcheck@v1.0.2 15 | with: 16 | web-url: https://gridfinity.bouwens.co 17 | 18 | -------------------------------------------------------------------------------- /help_files/size_help.html: -------------------------------------------------------------------------------- 1 |

Bin size is defined in grid-units. For Width and Length, the grid size is 42mm, for height the grid-size is 7mm

2 | 3 |

Note that there are limits to the size of the bins you can generate. For Width and Length the maximum is 6 units (252mm), for Height the maximum is 12 (84mm). These limits exist mainly to protect the server (larger bins are slower to generate), and these sizes are actually larger than fit on common 3D printer beds

4 | -------------------------------------------------------------------------------- /generators/lightbin/lightbin_description.html: -------------------------------------------------------------------------------- 1 |
Description
2 |

This generates a light version of the normal Gridfinity bin that saves plastic and offers more room. This means magnets and/or screws are not possible.

3 | 4 |
Parameters
5 | 9 | -------------------------------------------------------------------------------- /help_files/compartment_help.html: -------------------------------------------------------------------------------- 1 |

Independent of the size of the bin you can specify into how many compartments the bin should be split. So you can have a 2x2 bin with 9, 8 or 36 compartments.

2 | 3 |

Note that there is a maximum to the number of compartments, which is determined by the size of your bin. You can create up to 3 compartments per unit-size in Width and Length. So a 2x2 bin can have at most 6x6 = 36 compartments, and a 1x2 bin can have at most 3x6 = 18 compartments

4 | -------------------------------------------------------------------------------- /help_files/holey_size_help.html: -------------------------------------------------------------------------------- 1 |

The hole size controls the following dimensions for each hole shape:

2 | 3 | 8 | 9 |

Here you can see that the hole size does not immediately change the grid or the size of the bin because of the keepout diameter:

10 | 11 | 12 | -------------------------------------------------------------------------------- /docker-compose.debug.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | cadquery_debug: 5 | build: ./ 6 | container_name: cadquery_debug 7 | tmpfs: 8 | - /tmpfiles 9 | environment: 10 | - PUID=1000 11 | - PGID=1000 12 | - TZ=Europe/Amsterdam 13 | environment: 14 | FLASK_PORT: 5001 15 | FLASK_DEBUG: "True" 16 | restart: unless-stopped 17 | networks: 18 | - proxy 19 | ports: 20 | - 5001:5001 21 | volumes: 22 | - ${DATA_ROOT:?error}/gridfinitycreator/debug_logs:/logs 23 | 24 | networks: 25 | proxy: 26 | external: true 27 | -------------------------------------------------------------------------------- /generators/solidbin/solidbin_description.html: -------------------------------------------------------------------------------- 1 |
Description
2 |

The solid bin is a completely filled solid Gridfinity bin which can be used as a starting point for custom bins.

3 | 4 |
Parameters
5 | 10 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build image", 8 | "type": "shell", 9 | "command": "sudo ./deploy.sh", 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | }, 14 | "presentation": { 15 | "reveal": "always", 16 | "panel": "dedicated" 17 | }, 18 | "problemMatcher": [] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /generators/solidbin/solidbin_settings.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | # Generator inputs 4 | @dataclass 5 | class Settings: 6 | sizeUnitsX: int = 3 # Width (X) of the brick in grid units 7 | sizeUnitsY: int = 1 # Length (Y) of the brick in grid units 8 | sizeUnitsZ: int = 3 # Height (Z) of the brick in height-units 9 | 10 | addStackingLip: bool = True # Add a stacking lip (True) or not (False)? 11 | addMagnetHoles: bool = True # Add holes for magnets 12 | addScrewHoles: bool = True # Add holes for screws 13 | magnetHoleDiameter: float = 6.5 # Diameter of magnet holes 14 | addRemovalHoles: bool = False # Add an extra magnet-removal hole to each magnet hole 15 | -------------------------------------------------------------------------------- /help_files/holey_keepout_help.html: -------------------------------------------------------------------------------- 1 |

The keepout diameter controls the circular area that should be kept open (free of other holes or the edge of the bin) around each hole, for example to ensure there is enough room for your finger tips to grab an item. of holes in Width and Length directions specify how many holes the grid consists of. Combined with the keepout diameter this determines how big the bin will be. The generator will create a bin of the minimum size required to fit the hole-grid

2 | 3 |

Here you can see the result of increasing the keepout diameter while keeping all other parameters unchanged:

4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM continuumio/miniconda3:25.1.1-2 2 | 3 | RUN apt-get update -y && \ 4 | apt install -y libgl1-mesa-glx && \ 5 | apt-get clean && \ 6 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 7 | 8 | RUN conda update conda && \ 9 | conda install conda-forge::cadquery 10 | 11 | # copy the requirements file into the image 12 | COPY ./requirements.txt /app/requirements.txt 13 | 14 | # switch working directory 15 | WORKDIR /app 16 | 17 | # install the dependencies and packages in the requirements file 18 | RUN pip install --no-cache-dir -r requirements.txt 19 | 20 | # copy all local content to the image 21 | COPY . /app 22 | 23 | # configure the container to run in an executed manner 24 | ENTRYPOINT [ "python" ] 25 | 26 | CMD ["gfg_main.py" ] 27 | -------------------------------------------------------------------------------- /generators/holeybin/holeybin_description.html: -------------------------------------------------------------------------------- 1 |
Description
2 |

The holey bin is a solid Gridfinity bin with a configurable grid of holes (round, hex, square, etc). Useful for 3 | things like a screw-bit organizer or battery-storage. You cannot specify the size of the bin. Instead, the 4 | generator will determine the minimum bin-size that fits the specified hole-grid

5 | 6 |
Parameters
7 | -------------------------------------------------------------------------------- /generators/lightbin/lightbin_settings.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | # Generator inputs 4 | @dataclass 5 | class Settings: 6 | sizeUnitsX: int = 3 # Width (X) of the brick in grid units 7 | sizeUnitsY: int = 2 # Length (Y) of the brick in grid units 8 | sizeUnitsZ: int = 3 # Height (Z) of the brick in height-units 9 | 10 | compartmentsX: int = 1 # The number of compartments in the X (width) direction 11 | compartmentsY: int = 1 # The number of compartments in the Y (length) direction 12 | 13 | addStackingLip: bool = True # Add a stacking lip (True) or not (False)? 14 | addLabelRidge: bool = True # Add a ridge to pick up the bin and attach a label 15 | multiLabel: bool = False # Add a ridge to every row of compartments? 16 | 17 | labelRidgeWidth: int = 13 18 | wallThickness: int = 1.5 -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # This CITATION.cff file was generated with cffinit. 2 | # Visit https://bit.ly/cffinit to generate yours today! 3 | 4 | cff-version: 1.2.0 5 | title: GridfinityCreator 6 | message: >- 7 | If you use this software, please cite it using the 8 | metadata from this file. 9 | type: software 10 | authors: 11 | - given-names: Jeroen 12 | family-names: Bouwens 13 | repository-code: 'https://github.com/jeroen94704/gridfinitycreator/' 14 | url: 'https://gridfinity.bouwens.co/' 15 | abstract: >- 16 | GridfinityCreator dynamically generates custom STL or STEP 17 | files for several types of Gridfinity components. 18 | keywords: 19 | - gridfinity 20 | - cadquery 21 | - stl 22 | - step 23 | - 3dprinting 24 | license: CC-BY-NC-SA-4.0 25 | commit: 471f3bbbabadb084ef1bf07ec81898326b21c8a1 26 | version: 0.4.4 27 | date-released: '2024-12-03' 28 | -------------------------------------------------------------------------------- /generators/classicbin/classicbin_description.html: -------------------------------------------------------------------------------- 1 |
Description
2 |

The basic divider bin. Create one of your desired size and with any number of compartments in both length and width directions.

3 | 4 |
Parameters
5 | 12 | -------------------------------------------------------------------------------- /help_files/holey_numholes_help.html: -------------------------------------------------------------------------------- 1 |

The number of holes in the grid can be specified in one of two ways:

2 | 3 |
    4 |
  1. By setting the number of holes in the Width and Length directions
  2. 5 |
  3. By setting the bin-size in gridfinity units.
  4. 6 |
7 | 8 |

In both cases the other quantity will be recalculated automatically. So as soon as you set a number of holes (either in Width or Length), the bin-size will be set to 9 | the minimum size required to fit that number. As soon as you set a bin-size (either in Width or Length), the number of holes will be set to the maximum that fit in that 10 | size bin. Combined with the keepout diameter this determines how big the bin will be.

11 | 12 |

Below you can see the result of increasing either the number of holes by 1 (bottom right), or setting the bin-Width to 2 (top right), while keeping all other 13 | parameters unchanged from the original 4x4-hole bin (left):

14 | 15 |

16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | cadquery: 5 | container_name: cadquery 6 | build: ./ 7 | # Create a RAMdisk to store the generated files until they are downloaded to prevent wearing out the server's SSD 8 | tmpfs: 9 | - /tmpfiles 10 | environment: 11 | - PUID=1000 12 | - PGID=1000 13 | - TZ=Europe/Amsterdam 14 | restart: unless-stopped 15 | networks: 16 | - proxy 17 | ports: 18 | - 5000:5000 19 | env_file: 20 | - .env.container 21 | volumes: 22 | - ${DATA_ROOT:?error}/gridfinitycreator/logs:/logs 23 | # labels: 24 | # - "traefik.enable=true" 25 | # - "traefik.http.routers.gridfinity.rule=Host(`${GFG_DOMAIN:?error}`)" 26 | # - "traefik.http.routers.gridfinity.entrypoints=websecure" 27 | # - "traefik.http.routers.gridfinity.service=gridfinity_service" 28 | # - "traefik.http.routers.gridfinity.tls.certResolver=leresolver" 29 | # - "traefik.http.services.gridfinity_service.loadbalancer.server.port=5000" 30 | 31 | networks: 32 | proxy: 33 | external: true 34 | -------------------------------------------------------------------------------- /generators/classicbin/classicbin_settings.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | # Generator inputs 4 | @dataclass 5 | class Settings: 6 | sizeUnitsX: int = 3 # Width (X) of the brick in grid units 7 | sizeUnitsY: int = 1 # Length (Y) of the brick in grid units 8 | sizeUnitsZ: int = 3 # Height (Z) of the brick in height-units 9 | 10 | compartmentsX: int = 2 # The number of compartments in the X (width) direction 11 | compartmentsY: int = 1 # The number of compartments in the Y (length) direction 12 | 13 | addStackingLip: bool = True # Add a stacking lip (True) or not (False)? 14 | addMagnetHoles: bool = True # Add holes for magnets 15 | magnetHoleDiameter: float = 6.5 # Diameter of magnet holes 16 | addRemovalHoles: bool = False # Add an extra magnet-removal hole to each magnet hole 17 | addScrewHoles: bool = True # Add holes for screws 18 | addGrabCurve: bool = True # Add a curved floor to easily get parts out of the bin 19 | addLabelRidge: bool = True # Add a ridge to pick up the bin and attach a label 20 | multiLabel: bool = False # Add a ridge to every row of compartments? 21 | 22 | exportFormat: str = "stl" 23 | labelRidgeWidth: int = 13 24 | dividerThickness: int = 1.5 -------------------------------------------------------------------------------- /help_files/magnet_help.html: -------------------------------------------------------------------------------- 1 |

Gridfinity bins can (optionally) be held in place on a base-plate using magnets. There are 2 options for this:

2 | 6 | 7 |

To accomodate this, there are a few options to control the cutouts on the bottom of the bin:

8 | 9 | 15 | 16 |

Below is a render of what different combinations of these options look like:

17 | 18 | -------------------------------------------------------------------------------- /generators/holeybin/holeybin_settings.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | 4 | # Hole shapes 5 | class HoleShape(Enum): 6 | CIRCLE = "Circle" 7 | SQUARE = "Square" 8 | HEXAGON = "Hexagon" 9 | 10 | # Generator inputs 11 | @dataclass 12 | class Settings: 13 | numHolesX: int = 3 # Number of holes in the Width (X) direction 14 | numHolesY: int = 3 # Number of holes in the Length (Y) direction 15 | sizeUnitsX: int = 1 # Width (X) of the brick in grid units 16 | sizeUnitsY: int = 1 # Length (Y) of the brick in grid units 17 | 18 | holeShape: HoleShape = HoleShape.CIRCLE # The shape of the hole to use 19 | holeSize: float = 4.0 # Diameter of the hole 20 | holeDepth: float = 5.0 # Depth of the holes 21 | keepoutDiameter: float = 12.0 # Diameter of the keepout area 22 | 23 | addStackingLip: bool = True # Add a stacking lip (True) or not (False)? 24 | addMagnetHoles: bool = True # Add holes for magnets 25 | magnetHoleDiameter: float = 6.5 # Diameter of magnet holes 26 | addRemovalHoles: bool = False # Add an extra magnet-removal hole to each magnet hole 27 | addScrewHoles: bool = True # Add holes for screws 28 | 29 | exportFormat: str = "stl" 30 | 31 | sizeUnitsZ: int = 3 # Height (Z) of the brick in height-units -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "gridfinitycreator", 3 | "projectOwner": "jeroen94704", 4 | "files": [ 5 | "README.md" 6 | ], 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributorsPerLine": 7, 10 | "contributors": [ 11 | { 12 | "login": "NoSQLKnowHow", 13 | "name": "Kirk Kirkconnell", 14 | "avatar_url": "https://avatars.githubusercontent.com/u/2966377?v=4", 15 | "profile": "https://github.com/NoSQLKnowHow", 16 | "contributions": [ 17 | "ideas" 18 | ] 19 | }, 20 | { 21 | "login": "wug-ge", 22 | "name": "wug-ge", 23 | "avatar_url": "https://avatars.githubusercontent.com/u/75441883?v=4", 24 | "profile": "https://github.com/wug-ge", 25 | "contributions": [ 26 | "bug" 27 | ] 28 | }, 29 | { 30 | "login": "chasebolt", 31 | "name": "Chase Bolt", 32 | "avatar_url": "https://avatars.githubusercontent.com/u/1222984?v=4", 33 | "profile": "https://bluelight.co", 34 | "contributions": [ 35 | "bug" 36 | ] 37 | }, 38 | { 39 | "login": "dseifert", 40 | "name": "Daniel Seifert", 41 | "avatar_url": "https://avatars.githubusercontent.com/u/94670?v=4", 42 | "profile": "https://github.com/dseifert", 43 | "contributions": [ 44 | "bug", 45 | "code" 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /templates/base.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | 6 | 7 | 8 | 9 | 15 | 16 | Gridfinity Creator 17 | {% endblock %} 18 | 19 | 20 | 21 | {% block content %}{% endblock %} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /generators/baseplate/baseplate_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField, SelectField, BooleanField 3 | from wtforms.widgets import NumberInput 4 | from grid_constants import * 5 | import os 6 | import help_provider as help 7 | from generators.common.settings_form import get_standard_settings_form 8 | 9 | class Form(FlaskForm): 10 | id = "baseplate" 11 | sizeUnitsX = IntegerField("Width", widget=NumberInput(min = 1, max = Grid.MAX_GRID_UNITS), default=2) 12 | sizeUnitsY = IntegerField("Length", widget=NumberInput(min = 1, max = Grid.MAX_GRID_UNITS), default=2) 13 | exportFormat = SelectField('Export format', choices=[('stl', 'STL'), ('step', 'STEP')]) 14 | 15 | def __init__(self, *args, **kwargs): 16 | super().__init__(*args, **kwargs) 17 | self.sizeUnitsX.description = help.get_size_help() 18 | self.sizeUnitsY.description = help.get_size_help() 19 | self.exportFormat.description = help.get_exportformat_help() 20 | 21 | def get_rows(self): 22 | return [ 23 | ["Size", [self.sizeUnitsX, self.sizeUnitsY]], 24 | ["Options", [self.exportFormat]], 25 | ] 26 | 27 | def get_settings_html(self): 28 | return get_standard_settings_form() 29 | 30 | def get_title(self): 31 | return "Baseplate" 32 | 33 | def get_description(self): 34 | with open(os.path.dirname(__file__) + '/baseplate_description.html', 'r') as reader: 35 | return reader.read() 36 | -------------------------------------------------------------------------------- /generators/baseplate/main.py: -------------------------------------------------------------------------------- 1 | from flask import send_file, Flask, after_this_request 2 | 3 | import baseplate_generator as generator 4 | import baseplate_form as form 5 | import baseplate_settings as settings 6 | import grid_constants 7 | 8 | import uuid 9 | import os 10 | import logging 11 | 12 | logger = logging.getLogger('BPG') 13 | 14 | def process(form, constants): 15 | # Copy the settings from the form 16 | s = settings.Settings() 17 | 18 | # Copy the settings from the form 19 | s.sizeUnitsX = form.sizeUnitsX.data 20 | s.sizeUnitsY = form.sizeUnitsY.data 21 | 22 | # Default grid (Gridfinity) 23 | if not constants: 24 | g = grid_constants.Grid() 25 | else: 26 | g = constants 27 | 28 | # Construct the names for the temporary and downloaded file 29 | filename = "/tmpfiles/" + str(uuid.uuid4()) + "." + form.exportFormat.data 30 | 31 | # Generate the STL file 32 | gen = generator.Generator(s, g) 33 | gen.generate_stl(filename) 34 | 35 | # Delete the temp file after it was downloaded 36 | @after_this_request 37 | def delete_image(response): 38 | try: 39 | os.remove(filename) 40 | logger.debug("Removed temp file {0}".format(filename)) 41 | except Exception as ex: 42 | logger.critical(ex) 43 | return response 44 | 45 | logger.info(s) 46 | 47 | # Send the generated STL file to the client 48 | downloadName = "Baseplate {0}x{1}.{2}".format(s.sizeUnitsX, s.sizeUnitsY, form.exportFormat.data) 49 | return send_file(filename, as_attachment=True, download_name=downloadName) 50 | 51 | def get_form(): 52 | return form.Form() 53 | 54 | def handles(request, form): 55 | if form.id in request.form and form.validate_on_submit(): 56 | return True 57 | 58 | return False -------------------------------------------------------------------------------- /help_provider.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | def get_size_help(): 4 | with open(os.path.dirname(__file__) + '/help_files/size_help.html', 'r') as reader: 5 | return reader.read() 6 | 7 | def get_magnet_help(): 8 | with open(os.path.dirname(__file__) + '/help_files/magnet_help.html', 'r') as reader: 9 | return reader.read() 10 | 11 | def get_stackinglip_help(): 12 | with open(os.path.dirname(__file__) + '/help_files/stackinglip_help.html', 'r') as reader: 13 | return reader.read() 14 | 15 | def get_labeltab_help(): 16 | with open(os.path.dirname(__file__) + '/help_files/labeltab_help.html', 'r') as reader: 17 | return reader.read() 18 | 19 | def get_exportformat_help(): 20 | with open(os.path.dirname(__file__) + '/help_files/export_format_help.html', 'r') as reader: 21 | return reader.read() 22 | 23 | def get_scoopramp_help(): 24 | with open(os.path.dirname(__file__) + '/help_files/scoopramp_help.html', 'r') as reader: 25 | return reader.read() 26 | 27 | def get_compartment_help(): 28 | with open(os.path.dirname(__file__) + '/help_files/compartment_help.html', 'r') as reader: 29 | return reader.read() 30 | 31 | def get_holey_shape_help(): 32 | with open(os.path.dirname(__file__) + '/help_files/holey_shape_help.html', 'r') as reader: 33 | return reader.read() 34 | 35 | def get_holey_size_help(): 36 | with open(os.path.dirname(__file__) + '/help_files/holey_size_help.html', 'r') as reader: 37 | return reader.read() 38 | 39 | def get_holey_keepout_help(): 40 | with open(os.path.dirname(__file__) + '/help_files/holey_keepout_help.html', 'r') as reader: 41 | return reader.read() 42 | 43 | def get_holey_gridspec_help(): 44 | with open(os.path.dirname(__file__) + '/help_files/holey_numholes_help.html', 'r') as reader: 45 | return reader.read() 46 | -------------------------------------------------------------------------------- /generators/lightbin/main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, send_file, after_this_request 2 | 3 | import lightbin_generator as generator 4 | import lightbin_form as form 5 | import lightbin_settings as settings 6 | import grid_constants 7 | 8 | import uuid 9 | import os 10 | import logging 11 | 12 | logger = logging.getLogger('LBG') 13 | 14 | def get_generator(settings): 15 | return generator.Generator(settings) 16 | 17 | def process(form, constants): 18 | # Copy the settings from the form 19 | s = settings.Settings() 20 | s.sizeUnitsX = form.sizeUnitsX.data 21 | s.sizeUnitsY = form.sizeUnitsY.data 22 | s.sizeUnitsZ = form.sizeUnitsZ.data 23 | s.addStackingLip = form.addStackingLip.data 24 | s.addLabelRidge = form.addLabelRidge.data 25 | 26 | # Default grid (Gridfinity) 27 | if not constants: 28 | g = grid_constants.Grid() 29 | else: 30 | g = constants 31 | 32 | # Construct the names for the temporary and downloaded file 33 | filename = "/tmpfiles/" + str(uuid.uuid4()) + "." + form.exportFormat.data 34 | 35 | # Generate the STL file 36 | gen = generator.Generator(s, g) 37 | gen.generate_stl(filename) 38 | 39 | # Delete the temp file after it was downloaded 40 | @after_this_request 41 | def delete_image(response): 42 | try: 43 | os.remove(filename) 44 | except Exception as ex: 45 | print(ex) 46 | return response 47 | 48 | logger.info(s) 49 | 50 | # Send the generated STL file to the client 51 | downloadName = "Light divider bin {0}x{1}x{2}.{3}".format(s.sizeUnitsX, s.sizeUnitsY, s.sizeUnitsZ, form.exportFormat.data) 52 | return send_file(filename, as_attachment=True, download_name=downloadName) 53 | 54 | def get_form(): 55 | return form.Form() 56 | 57 | def handles(request, form): 58 | if form.id in request.form and form.validate_on_submit(): 59 | return True 60 | 61 | return False -------------------------------------------------------------------------------- /generators/solidbin/solidbin_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField, DecimalField, SelectField, BooleanField 3 | from wtforms.widgets import NumberInput 4 | from grid_constants import * 5 | import os 6 | from generators.common.settings_form import get_standard_settings_form 7 | 8 | class Form(FlaskForm): 9 | id = "solidbin" 10 | sizeUnitsX = IntegerField("Width", widget=NumberInput(min = 1, max = Grid.MAX_GRID_UNITS), default=2) 11 | sizeUnitsY = IntegerField("Length", widget=NumberInput(min = 1, max = Grid.MAX_GRID_UNITS), default=2) 12 | sizeUnitsZ = IntegerField("Height", widget=NumberInput(min = 1, max = Grid.MAX_HEIGHT_UNITS), default=6) 13 | addStackingLip = BooleanField("Stacking lip", default="True") 14 | addMagnetHoles = BooleanField("Magnet holes", default="True") 15 | magnetHoleDiameter = DecimalField("Magnet-hole diameter", default = 6.5, places = 2) 16 | addRemovalHoles = BooleanField("Magnet removal holes", default="False") 17 | addScrewHoles = BooleanField("Screw holes", default="False") 18 | exportFormat = SelectField('Export format', choices=[('stl', 'STL'), ('step', 'STEP')]) 19 | 20 | def __init__(self, *args, **kwargs): 21 | super().__init__(*args, **kwargs) 22 | 23 | def get_rows(self): 24 | return [ 25 | ["Size", [self.sizeUnitsX, self.sizeUnitsY, self.sizeUnitsZ]], 26 | ["Magnets", [self.addMagnetHoles, self.addRemovalHoles, self.addScrewHoles, self.magnetHoleDiameter]], 27 | ["Other", [self.addStackingLip, self.exportFormat]], 28 | ] 29 | 30 | def get_title(self): 31 | return "Solid bin" 32 | 33 | def get_settings_html(self): 34 | return get_standard_settings_form() 35 | 36 | def get_description(self): 37 | with open(os.path.dirname(__file__) + '/solidbin_description.html', 'r') as reader: 38 | return reader.read() 39 | 40 | -------------------------------------------------------------------------------- /generators/lightbin/lightbin_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField, SelectField, BooleanField 3 | from wtforms.widgets import NumberInput 4 | from grid_constants import * 5 | import os 6 | import help_provider as help 7 | from generators.common.settings_form import get_standard_settings_form 8 | 9 | class Form(FlaskForm): 10 | id = "lightbin" 11 | sizeUnitsX = IntegerField("Width", widget=NumberInput(min = 1, max = Grid.MAX_GRID_UNITS), default=2) 12 | sizeUnitsY = IntegerField("Length", widget=NumberInput(min = 1, max = Grid.MAX_GRID_UNITS), default=2) 13 | sizeUnitsZ = IntegerField("Height", widget=NumberInput(min = 1, max = Grid.MAX_HEIGHT_UNITS), default=6) 14 | addStackingLip = BooleanField("Stacking lip", default="True") 15 | addLabelRidge = BooleanField("Add label tab", default="True", false_values=(False, "false", "")) 16 | exportFormat = SelectField('Export format', choices=[('stl', 'STL'), ('step', 'STEP')]) 17 | 18 | def __init__(self, *args, **kwargs): 19 | super().__init__(*args, **kwargs) 20 | self.sizeUnitsX.description = help.get_size_help() 21 | self.sizeUnitsY.description = help.get_size_help() 22 | self.sizeUnitsZ.description = help.get_size_help() 23 | self.addStackingLip.description = help.get_stackinglip_help() 24 | self.exportFormat.description = help.get_exportformat_help() 25 | self.addLabelRidge.description = help.get_labeltab_help() 26 | 27 | def get_rows(self): 28 | return [ 29 | ["Size", [self.sizeUnitsX, self.sizeUnitsY, self.sizeUnitsZ]], 30 | ["Options", [self.addStackingLip, self.addLabelRidge, self.exportFormat]], 31 | ] 32 | 33 | def get_title(self): 34 | return "Light bin" 35 | 36 | def get_settings_html(self): 37 | return get_standard_settings_form() 38 | 39 | def get_description(self): 40 | with open(os.path.dirname(__file__) + '/lightbin_description.html', 'r') as reader: 41 | return reader.read() 42 | -------------------------------------------------------------------------------- /generators/solidbin/main.py: -------------------------------------------------------------------------------- 1 | from flask import send_file, Flask, after_this_request 2 | 3 | import solidbin_generator as generator 4 | import solidbin_form as form 5 | import solidbin_settings as settings 6 | import grid_constants 7 | 8 | import uuid 9 | import os 10 | import logging 11 | 12 | logger = logging.getLogger('SBG') 13 | 14 | def get_generator(settings): 15 | return generator.Generator(settings) 16 | 17 | def process(form, constants): 18 | # Copy the settings from the form 19 | s = settings.Settings() 20 | s.sizeUnitsX = form.sizeUnitsX.data 21 | s.sizeUnitsY = form.sizeUnitsY.data 22 | s.sizeUnitsZ = form.sizeUnitsZ.data 23 | s.addStackingLip = form.addStackingLip.data 24 | s.addMagnetHoles = form.addMagnetHoles.data 25 | s.magnetHoleDiameter = float(form.magnetHoleDiameter.data) 26 | s.addRemovalHoles = form.addRemovalHoles.data 27 | s.addScrewHoles = form.addScrewHoles.data 28 | 29 | # Default grid (Gridfinity) 30 | if not constants: 31 | g = grid_constants.Grid() 32 | else: 33 | g = constants 34 | 35 | # Construct the names for the temporary and downloaded file 36 | filename = "/tmpfiles/" + str(uuid.uuid4()) + "." + form.exportFormat.data 37 | 38 | # Generate the STL file 39 | gen = generator.Generator(s, g) 40 | gen.generate_stl(filename) 41 | 42 | # Delete the temp file after it was downloaded 43 | @after_this_request 44 | def delete_image(response): 45 | try: 46 | os.remove(filename) 47 | except Exception as ex: 48 | print(ex) 49 | return response 50 | 51 | logger.info(s) 52 | 53 | # Send the generated STL file to the client 54 | downloadName = "Solid Bin {0}x{1}x{2}.{3}".format(s.sizeUnitsX, s.sizeUnitsY, s.sizeUnitsZ, form.exportFormat.data) 55 | return send_file(filename, as_attachment=True, download_name=downloadName) 56 | 57 | def get_form(): 58 | return form.Form() 59 | 60 | def handles(request, form): 61 | if form.id in request.form and form.validate_on_submit(): 62 | return True 63 | 64 | return False -------------------------------------------------------------------------------- /generators/common/settings_form.html: -------------------------------------------------------------------------------- 1 |
2 | {% for row in form.get_rows() if row[0] %} 3 |
4 |
5 |
{{ row[0] }}
6 |
7 | {% for field in row[1] %} 8 |
9 |
10 | {{ field.label }} 11 | ?
{{field.description | safe}}
: 13 |
14 |
15 | {% if field.widget.input_type == 'checkbox' %} 16 |
17 | {% if field.object_data %} 18 | {{ field(role_="switch", class_="form-check-input", checked_=field.object_data) }} 19 | {% else %} 20 | {{ field(role_="switch", class_="form-check-input") }} 21 | {% endif %} 22 |
23 | {% elif field.type == 'SelectField' %} 24 | {{ field(class_="form-select") }} 25 | {% else %} 26 | {{ field() }} 27 | {% endif %} 28 |
29 |
30 | {% endfor %} 31 |
32 |
33 |
34 | {{ loop.cycle('', '
')|safe }} 35 | {% endfor %} 36 |
37 |
38 |
39 | 42 | 43 |
44 |
45 | 46 | -------------------------------------------------------------------------------- /generators/classicbin/main.py: -------------------------------------------------------------------------------- 1 | from flask import send_file, Flask, after_this_request 2 | 3 | import classicbin_generator as generator 4 | import classicbin_form as form 5 | import classicbin_settings as settings 6 | import grid_constants 7 | 8 | import uuid 9 | import os 10 | import logging 11 | 12 | logger = logging.getLogger('CBG') 13 | 14 | def process(form, constants): 15 | # Copy the settings from the form 16 | s = settings.Settings() 17 | 18 | # Copy the settings from the form 19 | s.sizeUnitsX = form.sizeUnitsX.data 20 | s.sizeUnitsY = form.sizeUnitsY.data 21 | s.sizeUnitsZ = form.sizeUnitsZ.data 22 | s.compartmentsX = form.compartmentsX.data 23 | s.compartmentsY = form.compartmentsY.data 24 | s.addStackingLip = form.addStackingLip.data 25 | s.addMagnetHoles = form.addMagnetHoles.data 26 | s.magnetHoleDiameter = float(form.magnetHoleDiameter.data) 27 | s.addRemovalHoles = form.addRemovalHoles.data 28 | s.addScrewHoles = form.addScrewHoles.data 29 | s.addGrabCurve = form.addGrabCurve.data 30 | s.addLabelRidge = form.addLabelRidge.data 31 | s.multiLabel = form.multiLabel.data 32 | 33 | # Default grid (Gridfinity) 34 | if not constants: 35 | g = grid_constants.Grid() 36 | else: 37 | g = constants 38 | 39 | # Construct the names for the temporary and downloaded file 40 | filename = "/tmpfiles/" + str(uuid.uuid4()) + "." + form.exportFormat.data 41 | 42 | # Generate the STL file 43 | gen = generator.Generator(s, g) 44 | gen.generate_stl(filename) 45 | 46 | # Delete the temp file after it was downloaded 47 | @after_this_request 48 | def delete_image(response): 49 | try: 50 | os.remove(filename) 51 | logger.debug("Removed temp file {0}".format(filename)) 52 | except Exception as ex: 53 | logger.critical(ex) 54 | return response 55 | 56 | logger.info(s) 57 | 58 | # Send the generated STL file to the client 59 | downloadName = "Divider Bin {0}x{1}x{2} {3}x{4} Compartments.{5}".format(s.sizeUnitsX, s.sizeUnitsY, s.sizeUnitsZ, s.compartmentsX, s.compartmentsY, form.exportFormat.data) 60 | return send_file(filename, as_attachment=True, download_name=downloadName) 61 | 62 | def get_form(): 63 | return form.Form() 64 | 65 | def handles(request, form): 66 | if form.id in request.form and form.validate_on_submit(): 67 | return True 68 | 69 | return False -------------------------------------------------------------------------------- /generators/holeybin/main.py: -------------------------------------------------------------------------------- 1 | from flask import send_file, Flask, after_this_request 2 | 3 | import generators.holeybin.holeybin_generator as generator 4 | import generators.holeybin.holeybin_form as form 5 | import generators.holeybin.holeybin_settings as settings 6 | import grid_constants 7 | 8 | import uuid 9 | import os 10 | import logging 11 | 12 | from holeybin_settings import HoleShape 13 | 14 | logger = logging.getLogger('HBG') 15 | 16 | def get_generator(settings): 17 | return generator.Generator(settings) 18 | 19 | def process(form, constants): 20 | # Copy the settings from the form 21 | s = settings.Settings() 22 | s.numHolesX = form.numHolesX.data 23 | s.numHolesY = form.numHolesY.data 24 | s.sizeUnitsX = form.sizeUnitsX.data 25 | s.sizeUnitsY = form.sizeUnitsY.data 26 | s.holeShape = form.holeShape.data 27 | s.holeSize = float(form.holeSize.data) 28 | s.holeDepth = float(form.holeDepth.data) 29 | s.keepoutDiameter = float(form.keepoutDiameter.data) 30 | 31 | s.addStackingLip = form.addStackingLip.data 32 | s.addMagnetHoles = form.addMagnetHoles.data 33 | s.magnetHoleDiameter = float(form.magnetHoleDiameter.data) 34 | s.addRemovalHoles = form.addRemovalHoles.data 35 | s.addScrewHoles = form.addScrewHoles.data 36 | 37 | # Default grid (Gridfinity) 38 | if not constants: 39 | g = grid_constants.Grid() 40 | else: 41 | g = constants 42 | 43 | # Construct the names for the temporary and downloaded file 44 | filename = "/tmpfiles/" + str(uuid.uuid4()) + "." + form.exportFormat.data 45 | 46 | logger.info(s) 47 | 48 | # Generate the STL file 49 | gen = generator.Generator(s, g) 50 | gen.generate_stl(filename) 51 | 52 | logger.debug("Generating completed") 53 | 54 | # Delete the temp file after it was downloaded 55 | @after_this_request 56 | def delete_image(response): 57 | try: 58 | os.remove(filename) 59 | except Exception as ex: 60 | print(ex) 61 | return response 62 | 63 | 64 | # Send the generated STL file to the client 65 | downloadName = "HoleyBin_{0}x{1}x{2}.{3}".format(s.numHolesX, s.numHolesY, s.holeDepth, form.exportFormat.data) 66 | return send_file(filename, as_attachment=True, download_name=downloadName) 67 | 68 | def get_form(): 69 | return form.Form() 70 | 71 | def handles(request, form): 72 | if form.id in request.form and form.validate_on_submit(): 73 | return True 74 | 75 | return False 76 | 77 | -------------------------------------------------------------------------------- /generators/baseplate/baseplate_generator.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | from cadquery import exporters 3 | from grid_constants import * 4 | 5 | class Generator: 6 | def __init__(self, settings, grid) -> None: 7 | self.settings = settings 8 | self.grid = grid 9 | 10 | self.validate_settings() 11 | 12 | def base_grid(self): 13 | """Create the baseplate""" 14 | x_offs = -self.grid.GRID_UNIT_SIZE_X_MM/2 15 | frame_pts = [(0+x_offs,0), (0+x_offs,4.65), (2.25+x_offs, 2.5), (2.25+x_offs, 0.7), (2.85+x_offs,0)] 16 | 17 | path = cq.Workplane("XY").rect(self.grid.GRID_UNIT_SIZE_X_MM, self.grid.GRID_UNIT_SIZE_Y_MM).val() 18 | path = path.fillet2D(4, path.Vertices()) 19 | 20 | corners = ( 21 | cq.Workplane("XY") 22 | .box(self.grid.GRID_UNIT_SIZE_X_MM,self.grid.GRID_UNIT_SIZE_Y_MM,4.65) 23 | .translate((0,0,4.65/2)) 24 | .faces(">Z") 25 | .sketch() 26 | .rect(42, 42) 27 | .vertices() 28 | .fillet(4) 29 | .finalize() 30 | .cutThruAll() 31 | ) 32 | 33 | unit = corners + ( 34 | cq.Workplane("XZ") 35 | .polyline(frame_pts) 36 | .close() 37 | .sweep(path) 38 | ) 39 | 40 | result = cq.Workplane("XY") 41 | 42 | for x in range(self.settings.sizeUnitsX): 43 | for y in range(self.settings.sizeUnitsY): 44 | result.add(unit.translate((x*self.grid.GRID_UNIT_SIZE_X_MM, y*self.grid.GRID_UNIT_SIZE_Y_MM, 0))) 45 | 46 | result = result.combine(clean=True) 47 | result = result.edges("|Z").fillet(3.999) 48 | 49 | return result 50 | 51 | def validate_settings(self): 52 | """Do some sanity checking on the settings to prevent impossible or unreasonable results""" 53 | 54 | # Cap the size in grid-units to avoid thrashing the server 55 | self.settings.sizeUnitsX = min(self.settings.sizeUnitsX, self.grid.MAX_GRID_UNITS) 56 | self.settings.sizeUnitsY = min(self.settings.sizeUnitsY, self.grid.MAX_GRID_UNITS) 57 | 58 | def generate_model(self): 59 | plane = cq.Workplane("XY") 60 | result = plane.workplane() 61 | 62 | # Add the base of Gridfinity profiles 63 | result.add(self.base_grid()) 64 | 65 | # Combine everything together 66 | result = result.combine(clean=True) 67 | 68 | return result 69 | 70 | def generate_stl(self, filename): 71 | model = self.generate_model() 72 | exporters.export(model, filename) 73 | 74 | -------------------------------------------------------------------------------- /templates/front_page.html.j2: -------------------------------------------------------------------------------- 1 |
About
2 | 3 |

4 | GridfinityCreator lets you generate STL or STEP files for several types of customizable Gridfinity components. For 5 | each type you can 6 | specify size, but also other parameters and options such as compartments, magnet-and/or screw-holes, a stacking lip 7 | and more. Just 8 | fill in the parameters and click the "Generate" button. 9 |

10 | 11 |
12 |
Warning
13 |

14 | GridfinityCreator is alpha software. It may produce incorrect output, fail inexplicably or cease to exist at any 15 | moment. 16 |

17 |
18 | 19 |
Alternatives
20 | 21 |

There are quite a few other Gridfinity generators available, both online and offline, each with their own advantages 22 | and use-cases. The ones I know about are:

23 | 24 | 33 | 34 |
Made possible by ...
35 | 36 |

As is the case with most software these days, this application relies heavily on Free and Open Source Software 37 | (FOSS), generously released by their 38 | respective authors under an open license. Below is a list of the major tools, libraries and other components 39 | GridfinityCreator relies on:

40 | 41 | -------------------------------------------------------------------------------- /templates/settings_offcanvas.html.j2: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Advanced settings
4 | 5 |
6 |
7 |
8 |

Below is the specification of the grid used by the generator to create components. You can select one of 9 | the presets or adjust the settings individually.

10 |
11 |
Note
12 |

All measurements are in millimeters

13 |
14 |
15 |
Beware
16 |

The defaults comply with the Gridfinity specification and have been tested to work well. 17 | Don't make any changes unless you have a good reason to do so and you know what you're doing.

18 |
19 |
20 |
21 |
Presets
22 | 26 |

27 |
28 |
Constants
29 |
30 |
31 |
32 |
33 |
:
34 |
35 | 36 |
37 |
38 |
39 |
:
40 |
41 | 42 |
43 |
44 |
45 |
:
46 |
47 | 48 |
49 |
50 |
51 |
52 | 54 |
55 |
56 |
57 |
58 |
59 |
60 |
-------------------------------------------------------------------------------- /grid_constants.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | @dataclass 4 | class Grid: 5 | """This class holds the necessary key dimensions to define the grid used by all 6 | the generators to create components that fit together. The defaults are all 7 | set to Gridfinity standard values, but for flexibility these can be changed 8 | to generate e.g. Raaco compatible bins 9 | """ 10 | 11 | # CQ cannot chamfer an edge all the way to another edge, so reduce the chamfer by a tiny amount to avoid problems 12 | CHAMFER_EPSILON: float = 0.01 13 | 14 | # Fixed dimensions (Magic numbers come from the GridFinity profile specification (https://gridfinity.xyz/specification/)) 15 | GRID_UNIT_SIZE_X_MM: float = 42 16 | GRID_UNIT_SIZE_Y_MM: float = 42 17 | HEIGHT_UNITSIZE_MM: float = 7 18 | BRICK_SIZE_TOLERANCE_MM: float = 0.5 19 | BASE_BOTTOM_THICKNESS: float = 2.6 20 | BASE_BOTTOM_CHAMFER_SIZE: float = 0.8 21 | BASE_BOTTOM_FILLET_RADIUS: float = 1.6 22 | BASE_TOP_THICKNESS: float = 2.15 23 | BASE_TOP_FILLET_RADIUS: float = 3.75 24 | FLOOR_THICKNESS: float = 2.25 # This thickness makes the base exactly 1 height unit high 25 | LIGHT_FLOOR_THICKNESS: float = 0.9 26 | DEFAULT_MAGNET_HOLE_DIAMETER: float = 6.5 27 | DEFAULT_MAGNET_HOLE_DEPTH: float = 2 28 | SCREW_HOLE_DIAMETER: float = 3 29 | SCREW_HOLE_DEPTH: float = 6 30 | 31 | REMOVABLE_MAGNET_HOLE_OFFSET: float = 2.16 # This places the center of the remove-hole on the perimeter of the magnet hole 32 | REMOVABLE_MAGNET_HOLE_DIAMETER: float = 3.5 33 | WALL_THICKNESS: float = 1.9 34 | CORNER_FILLET_RADIUS: float = 3.75 35 | STACKING_LIP_HEIGHT: float = 4.4 36 | 37 | # Derived dimensions 38 | BRICK_UNIT_SIZE_X: float = GRID_UNIT_SIZE_X_MM - BRICK_SIZE_TOLERANCE_MM 39 | BRICK_UNIT_SIZE_Y: float = GRID_UNIT_SIZE_Y_MM - BRICK_SIZE_TOLERANCE_MM 40 | BASE_BOTTOM_SIZE_X: float = BRICK_UNIT_SIZE_X-4.3 41 | BASE_BOTTOM_SIZE_Y: float = BRICK_UNIT_SIZE_Y-4.3 42 | BASE_TOP_CHAMFER_SIZE: float = BASE_TOP_THICKNESS - CHAMFER_EPSILON 43 | HOLE_OFFSET_X: float = BRICK_UNIT_SIZE_X/2 - BASE_TOP_CHAMFER_SIZE - BASE_BOTTOM_CHAMFER_SIZE - 4.8 44 | HOLE_OFFSET_Y: float = BRICK_UNIT_SIZE_Y/2 - BASE_TOP_CHAMFER_SIZE - BASE_BOTTOM_CHAMFER_SIZE - 4.8 45 | 46 | # Some limits for sanity checking the inputs 47 | MAX_COMPARTMENTS_PER_GRID_UNIT: float = 4 48 | MAX_GRID_UNITS: float = 6 49 | MAX_HEIGHT_UNITS: float = 12 50 | MIN_HEIGHT_UNITS: float = 2 # A height of 1 unit would be just the base without anything on top 51 | 52 | def recalculate(self): 53 | # Recalculate the derived dimensions after changing one of the relevant fixed dimensions 54 | self.BRICK_UNIT_SIZE_X = self.GRID_UNIT_SIZE_X_MM - self.BRICK_SIZE_TOLERANCE_MM 55 | self.BRICK_UNIT_SIZE_Y = self.GRID_UNIT_SIZE_Y_MM - self.BRICK_SIZE_TOLERANCE_MM 56 | self.BASE_BOTTOM_SIZE_X = self.BRICK_UNIT_SIZE_X-4.3 57 | self.BASE_BOTTOM_SIZE_Y = self.BRICK_UNIT_SIZE_Y-4.3 58 | self.BASE_TOP_CHAMFER_SIZE = self.BASE_TOP_THICKNESS - self.CHAMFER_EPSILON 59 | self.HOLE_OFFSET_X = self.BRICK_UNIT_SIZE_X/2 - self.BASE_TOP_CHAMFER_SIZE - self.BASE_BOTTOM_CHAMFER_SIZE - 4.8 60 | self.HOLE_OFFSET_Y = self.BRICK_UNIT_SIZE_Y/2 - self.BASE_TOP_CHAMFER_SIZE - self.BASE_BOTTOM_CHAMFER_SIZE - 4.8 61 | -------------------------------------------------------------------------------- /generators/classicbin/classicbin_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField, DecimalField, SelectField, BooleanField 3 | from wtforms.widgets import NumberInput 4 | from grid_constants import * 5 | 6 | import os 7 | import help_provider as help 8 | from generators.common.settings_form import get_standard_settings_form 9 | 10 | class Form(FlaskForm): 11 | id = "classicbin" 12 | sizeUnitsX = IntegerField("Width", widget=NumberInput(min = 1, max = Grid.MAX_GRID_UNITS), default=2) 13 | sizeUnitsY = IntegerField("Length", widget=NumberInput(min = 1, max = Grid.MAX_GRID_UNITS), default=2) 14 | sizeUnitsZ = IntegerField("Height", widget=NumberInput(min = Grid.MIN_HEIGHT_UNITS, max = Grid.MAX_HEIGHT_UNITS), default=6) 15 | compartmentsX = IntegerField("Width direction", widget=NumberInput(min = 1, max = Grid.MAX_COMPARTMENTS_PER_GRID_UNIT*Grid.MAX_GRID_UNITS), default=3) 16 | compartmentsY = IntegerField("Length direction", widget=NumberInput(min = 1, max = Grid.MAX_COMPARTMENTS_PER_GRID_UNIT*Grid.MAX_GRID_UNITS), default=3) 17 | addStackingLip = BooleanField("Stacking lip", default="checked", false_values=(False, "false", "")) 18 | addMagnetHoles = BooleanField("Magnet holes", default="true", false_values=(False, "false", "")) 19 | magnetHoleDiameter = DecimalField("Magnet-hole diameter", default = 6.5, places = 2) 20 | addRemovalHoles = BooleanField("Magnet removal holes", false_values=(False, "false", "")) 21 | addScrewHoles = BooleanField("Screw holes", false_values=(False, "false", "")) 22 | addGrabCurve = BooleanField("Scoop ramp", default="true", false_values=(False, "false", "")) 23 | addLabelRidge = BooleanField("Add label tab(s)", default="true", false_values=(False, "false", "")) 24 | multiLabel = BooleanField("Label tab per row", false_values=(False, "false", "")) 25 | exportFormat = SelectField('Export format', choices=[('stl', 'STL'), ('step', 'STEP')]) 26 | 27 | def __init__(self, *args, **kwargs): 28 | super().__init__(*args, **kwargs) 29 | self.sizeUnitsX.description = help.get_size_help() 30 | self.sizeUnitsY.description = help.get_size_help() 31 | self.sizeUnitsZ.description = help.get_size_help() 32 | self.compartmentsX.description = help.get_compartment_help() 33 | self.compartmentsY.description = help.get_compartment_help() 34 | self.addStackingLip.description = help.get_stackinglip_help() 35 | self.addMagnetHoles.description = help.get_magnet_help() 36 | self.magnetHoleDiameter.description = help.get_magnet_help() 37 | self.addRemovalHoles.description = help.get_magnet_help() 38 | self.addScrewHoles.description = help.get_magnet_help() 39 | self.addGrabCurve.description = help.get_scoopramp_help() 40 | self.addLabelRidge.description = help.get_labeltab_help() 41 | self.multiLabel.description = help.get_labeltab_help() 42 | self.exportFormat.description = help.get_exportformat_help() 43 | 44 | def get_rows(self): 45 | return [ 46 | ["Size", [self.sizeUnitsX, self.sizeUnitsY, self.sizeUnitsZ]], 47 | ["Compartments", [self.compartmentsX, self.compartmentsY]], 48 | ["Magnets", [self.addMagnetHoles, self.addRemovalHoles, self.addScrewHoles, self.magnetHoleDiameter]], 49 | ["Other", [self.addStackingLip, self.addGrabCurve, self.exportFormat]], 50 | ["Labels", [self.addLabelRidge, self.multiLabel]], 51 | ] 52 | 53 | def get_title(self): 54 | return "Divider bin" 55 | 56 | def get_settings_html(self): 57 | return get_standard_settings_form() 58 | 59 | def get_description(self): 60 | with open(os.path.dirname(__file__) + '/classicbin_description.html', 'r') as reader: 61 | return reader.read() -------------------------------------------------------------------------------- /generators/solidbin/solidbin_generator.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | from cadquery import exporters 3 | from grid_constants import * 4 | 5 | from generators.common.bin_base import bin_base 6 | 7 | class Generator: 8 | def __init__(self, settings, grid) -> None: 9 | self.settings = settings 10 | self.grid = grid 11 | # Precalculate both before and after validation to process settings that changes 12 | self.precalculate() 13 | self.validate_settings() 14 | self.precalculate() 15 | 16 | def precalculate(self): 17 | """Precalculate a number of useful derived values used in construction""" 18 | self.brickSizeX = self.settings.sizeUnitsX * self.grid.GRID_UNIT_SIZE_X_MM - self.grid.BRICK_SIZE_TOLERANCE_MM 19 | self.brickSizeY = self.settings.sizeUnitsY * self.grid.GRID_UNIT_SIZE_Y_MM - self.grid.BRICK_SIZE_TOLERANCE_MM 20 | self.brickSizeZ = self.settings.sizeUnitsZ*self.grid.HEIGHT_UNITSIZE_MM 21 | self.internalSizeX = self.brickSizeX-2*self.grid.WALL_THICKNESS 22 | self.internalSizeY = self.brickSizeY-2*self.grid.WALL_THICKNESS 23 | self.compartmentSizeZ = (self.settings.sizeUnitsZ-1)*self.grid.HEIGHT_UNITSIZE_MM 24 | 25 | def outer_wall(self, basePlane): 26 | """Create the outer wall of the bin""" 27 | 28 | # Allow creation of a 1-unit height bin (just the base) 29 | if self.compartmentSizeZ == 0: 30 | return 31 | 32 | sizeZ = self.compartmentSizeZ 33 | 34 | if self.settings.addStackingLip: 35 | sizeZ = sizeZ + self.grid.STACKING_LIP_HEIGHT 36 | 37 | wall = basePlane.box(self.brickSizeX, self.brickSizeY, sizeZ, centered=False, combine = False) 38 | 39 | thickness = self.grid.WALL_THICKNESS 40 | result = wall.edges("|Z").fillet(self.grid.CORNER_FILLET_RADIUS) 41 | 42 | if self.settings.addStackingLip: 43 | cutout = ( 44 | wall.faces(">Z").workplane().center(thickness, thickness) 45 | .box(self.internalSizeX, self.internalSizeY, self.grid.STACKING_LIP_HEIGHT, centered=False, combine = False) 46 | .translate((0,0,-self.grid.STACKING_LIP_HEIGHT)) 47 | ) 48 | 49 | # If the walls are thicker than the outside radius of the corners, skip the fillet 50 | if thickness < self.grid.CORNER_FILLET_RADIUS: 51 | cutout = cutout.edges("|Z").fillet(self.grid.CORNER_FILLET_RADIUS-thickness) 52 | 53 | result = result - cutout 54 | 55 | result = result.edges( 56 | cq.selectors.NearestToPointSelector((self.brickSizeX/2, self.brickSizeY/2, sizeZ*2)) 57 | ).chamfer(thickness - self.grid.CHAMFER_EPSILON) 58 | 59 | return result 60 | 61 | def validate_settings(self): 62 | """Do some sanity checking on the settings to prevent impossible or unreasonable results""" 63 | 64 | # Cap the size in grid-units to avoid thrashing the server 65 | self.settings.sizeUnitsX = min(self.settings.sizeUnitsX, self.grid.MAX_GRID_UNITS) 66 | self.settings.sizeUnitsY = min(self.settings.sizeUnitsY, self.grid.MAX_GRID_UNITS) 67 | self.settings.sizeUnitsZ = min(self.settings.sizeUnitsZ, self.grid.MAX_HEIGHT_UNITS) 68 | 69 | def generate_model(self): 70 | plane = cq.Workplane("XY") 71 | 72 | result = bin_base(plane, self.settings, self.grid) 73 | 74 | plane = result.faces(">Z").workplane() 75 | 76 | # Add the outer wall 77 | result.add(self.outer_wall(plane)) 78 | 79 | # Combine everything together 80 | result = result.combine(clean=True) 81 | 82 | return result 83 | 84 | def generate_stl(self, filename): 85 | model = self.generate_model() 86 | exporters.export(model, filename) 87 | 88 | 89 | -------------------------------------------------------------------------------- /generators/holeybin/holeybin_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import IntegerField, SubmitField, BooleanField, DecimalField, SelectField 3 | from wtforms.widgets import NumberInput 4 | from grid_constants import * 5 | from holeybin_settings import HoleShape 6 | 7 | import os 8 | import help_provider as help 9 | from generators.common.settings_form import get_standard_settings_form 10 | 11 | class Form(FlaskForm): 12 | id = "holeybin" 13 | numHolesX = IntegerField("# holes in width direction", widget=NumberInput(min = 1), default=3) 14 | numHolesY = IntegerField("# holes in length direction", widget=NumberInput(min = 1), default=3) 15 | sizeUnitsX = IntegerField("Width in grid-units", widget=NumberInput(min = 1, max = Grid.MAX_GRID_UNITS), default=1) 16 | sizeUnitsY = IntegerField("Length in grid-units", widget=NumberInput(min = 1, max = Grid.MAX_GRID_UNITS), default=1) 17 | holeDepth = DecimalField("Depth", default = 5.0, places = 2) 18 | holeShape = SelectField("Shape", choices=[(choice.name, choice.value) for choice in HoleShape]) 19 | holeSize = DecimalField("Size", default = 4.0, places = 2) 20 | keepoutDiameter = DecimalField("Keepout diameter", default = 12.0, places = 2) 21 | addStackingLip = BooleanField("Stacking lip", default="checked", false_values=(False, "false", "")) 22 | addMagnetHoles = BooleanField("Magnet holes", default="true", false_values=(False, "false", "")) 23 | magnetHoleDiameter = DecimalField("Magnet-hole diameter", default = 6.5, places = 2) 24 | addRemovalHoles = BooleanField("Magnet removal holes", false_values=(False, "false", "")) 25 | addScrewHoles = BooleanField("Screw holes", false_values=(False, "false", "")) 26 | 27 | exportFormat = SelectField('Export format', choices=[('stl', 'STL'), ('step', 'STEP')]) 28 | 29 | def __init__(self, *args, **kwargs): 30 | super().__init__(*args, **kwargs) 31 | self.numHolesX.description = help.get_holey_gridspec_help() 32 | self.numHolesY.description = help.get_holey_gridspec_help() 33 | self.holeDepth.description = help.get_holey_gridspec_help() 34 | self.holeShape.description = help.get_holey_shape_help() 35 | self.sizeUnitsX.description = help.get_holey_gridspec_help() 36 | self.sizeUnitsY.description = help.get_holey_gridspec_help() 37 | self.holeSize.description = help.get_holey_size_help() 38 | self.keepoutDiameter.description = help.get_holey_keepout_help() 39 | self.addStackingLip.description = help.get_stackinglip_help() 40 | self.addMagnetHoles.description = help.get_magnet_help() 41 | self.magnetHoleDiameter.description = help.get_magnet_help() 42 | self.addRemovalHoles.description = help.get_magnet_help() 43 | self.addScrewHoles.description = help.get_magnet_help() 44 | self.exportFormat.description = help.get_exportformat_help() 45 | 46 | self.numHolesX.onChangedCallback = "onNumHolesChanged()" 47 | self.numHolesY.onChangedCallback = "onNumHolesChanged()" 48 | self.sizeUnitsX.onChangedCallback = "onBinSizeChanged()" 49 | self.sizeUnitsY.onChangedCallback = "onBinSizeChanged()" 50 | self.keepoutDiameter.onChangedCallback = "onHoleSizeChanged()" 51 | self.holeSize.onChangedCallback = "onHoleSizeChanged()" 52 | 53 | def get_rows(self): 54 | return [ 55 | ["Hole grid", [self.numHolesX, self.numHolesY, self.sizeUnitsX, self.sizeUnitsY, self.keepoutDiameter]], 56 | ["Holes", [self.holeShape, self.holeSize, self.holeDepth]], 57 | ["Other", [self.addStackingLip, self.exportFormat]], 58 | ["Magnets", [self.addMagnetHoles, self.addRemovalHoles, self.addScrewHoles, self.magnetHoleDiameter]], 59 | ] 60 | 61 | def get_title(self): 62 | return "Holey bin" 63 | 64 | def get_settings_html(self): 65 | with open(os.path.dirname(__file__) + '/holeybin_settings_form.html', 'r') as reader: 66 | return reader.read() 67 | 68 | def get_description(self): 69 | with open(os.path.dirname(__file__) + '/holeybin_description.html', 'r') as reader: 70 | return reader.read() -------------------------------------------------------------------------------- /generators/common/bin_base.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | 3 | from grid_constants import * 4 | 5 | def unit_base(basePlane, settings, grid): 6 | """Construct a 1x1 GridFinity unit base on the provided workplane""" 7 | 8 | # The elements are constructed "centered" because that makes life easier. 9 | baseBottom = basePlane.box(grid.BASE_BOTTOM_SIZE_X, grid.BASE_BOTTOM_SIZE_Y, grid.BASE_BOTTOM_THICKNESS, combine=False) 10 | baseBottom = baseBottom.edges("|Z").fillet(grid.BASE_BOTTOM_FILLET_RADIUS) 11 | baseBottom = baseBottom.faces("Z").workplane() 14 | baseTop = baseTop.box(grid.BRICK_UNIT_SIZE_X, grid.BRICK_UNIT_SIZE_Y, grid.BASE_TOP_THICKNESS, centered=(True, True, False), combine=False) 15 | baseTop = baseTop.edges("|Z").fillet(grid.CORNER_FILLET_RADIUS) 16 | baseTop = baseTop.faces("Z").workplane() 79 | 80 | # Add the floor of the bin 81 | result.add(brick_floor(plane, settings, grid)) 82 | 83 | result = result.combine(clean=True) 84 | 85 | return result -------------------------------------------------------------------------------- /generators/holeybin/holeybin_settings_form.html: -------------------------------------------------------------------------------- 1 |
2 | {% for row in form.get_rows() if row[0] %} 3 |
4 |
5 |
{{ row[0] }}
6 |
7 | {% for field in row[1] %} 8 |
9 |
10 | {{ field.label }} 11 | ?
{{field.description | safe}}
: 13 |
14 |
15 | {% if field.widget.input_type == 'checkbox' %} 16 |
17 | {% if field.object_data %} 18 | {{ field(role_="switch", class_="form-check-input", checked_=field.object_data) }} 19 | {% else %} 20 | {{ field(role_="switch", class_="form-check-input") }} 21 | {% endif %} 22 |
23 | {% elif field.type == 'SelectField' %} 24 | {{ field(class_="form-select") }} 25 | {% else %} 26 | {{ field(onchange_=field.onChangedCallback) }} 27 | {% endif %} 28 |
29 |
30 | {% endfor %} 31 |
32 |
33 |
34 | {{ loop.cycle('','
')|safe }} 35 | {% endfor %} 36 |
37 |
38 |
39 | 43 | 44 |
45 |
46 | 47 | -------------------------------------------------------------------------------- /templates/index.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'templates/base.html.j2' %} 2 | 3 | {% block content %} 4 | 5 |
6 | 7 |
8 |
9 |

Gridfinity Creator

10 |
11 |
12 | 14 |
15 |
16 | 17 |
18 |
19 | 29 |
30 |
31 | 32 |
33 |
34 |
35 |
36 | {% include 'templates/front_page.html.j2' %} 37 |
38 |
39 |
40 | 41 | {% for form in forms %} 42 |
43 |
44 |
45 | {{ form.get_description()|safe }} 46 |
47 |
48 | 49 |
50 |
51 |
52 |
53 |
54 |
Settings
55 |
56 |
57 | {{ form.csrf_token() }} 58 | {{ form.get_settings_html() | inner_render({"form":form}) }} 59 |
60 |
61 |
62 |
63 |
64 |
65 | {% endfor %} 66 |
67 | 68 | {% include 'templates/settings_offcanvas.html.j2' %} 69 |
70 | 71 |
72 |
73 |

Gridfinity Creator v{{version}}. ©2024 Jeroen Bouwens.

74 |

Source. 75 | Discord. 76 | Donate. 77 |

78 |
79 |
80 | 81 | 90 | 91 | 133 | {% endblock %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![All Contributors](https://img.shields.io/github/all-contributors/jeroen94704/gridfinitycreator?color=ee8449&style=plastic)](#contributors) 2 | [![Server status](https://img.shields.io/website?url=https%3A%2F%2Fgridfinity.bouwens.co&up_message=Online&down_message=Offline&style=plastic&label=gridfinity.bouwens.co&link=https%3A%2F%2Fgridfinity.bouwens.co)](https://gridfinity.bouwens.co) 3 | 4 | 5 | # Gridfinity Creator 6 | 7 | This application generates STL or STEP files of configurable Gridfinity compatible components. For example, for the standard divider bin you can specify width, length, height, number of compartments (in both directions) and whether or not you want a stacking lip, magnet holes, screw holes, curved scoop surface and/or one or more label tabs. Here are some of the possible bins you can create in this way: 8 | ![gridfinity-options](https://github.com/jeroen94704/gridfinitycreator/assets/548463/1577deb0-edc6-48d9-9a54-75fe3ecd335c) 9 | The total number of possible combinations with those options is beyond one million, which is why the 3D models are dynamically created and not pre-calculated. 10 | 11 | ## Available components 12 | 13 | There are currently a few components available, listed below. Other components are in the works. 14 | 15 | - Baseplate: Basic baseplate without screws, magnets or weighting. 16 | - Divider bin: Standard divider bin very similar to Zack's original design. 17 | - Light bin: A light version of the normal Gridfinity bin that saves plastic and offers more room. This means there is no room for magnets and/or screws 18 | - Solid bin: A completely filled solid Gridfinity bin which can be used as a starting point for custom bins 19 | - Holey bin: A solid bin with a grid of holes of user-defined shape, size and depth. Includes option for a keepout-area around each hole 20 | 21 | ## Online generator 22 | 23 | If you don't know how to run your own server (or simply don't want to), there should be an instance of the generator running at https://gridfinity.bouwens.co. No uptime guarantees, but it has been running since early 2023 and seems stable enough, even with the occasional spike in load. 24 | 25 | ## Installation and deployment 26 | 27 | The generator runs as a web-application in a docker container. To run your own instance, perform the following steps from the command line: 28 | 29 | - [Download and unzip the code](https://github.com/jeroen94704/gridfinitycreator/releases/latest) or clone the repository: `git clone https://github.com/jeroen94704/gridfinitycreator` 30 | - cd into the source directory 31 | - Build the Docker Image: `./build.sh` (may need to prefix this with 'sudo') 32 | - Start the server: `./deploy.sh` (may need to prefix this with 'sudo') 33 | 34 | Now you can access the application by opening a browser and navigating to :5000, e.g. 35 | 36 | `http://192.168.1.100:5000/` 37 | 38 | ## Debug mode 39 | 40 | The deploy script results in the server running in production mode using the [Waitress WSGI server](https://flask.palletsprojects.com/en/2.2.x/deploying/waitress/). This is good for performance, but if you want to debug the code, start the server using the "./debug.sh" script instead of "./deploy.sh". This will make the server start itself using the built-in Flask server, which has convenient debugging features. 41 | 42 | ## Reverse proxy 43 | 44 | Because I use Traefik myself I included the Traefik labels I use in the docker-compose file. If you want to use Traefik, uncomment them and comment out the "ports" section. You will also need to fill in your domain in the .env.container file. 45 | 46 | I have no experience with other reverse proxy methods (Apache, nginx, Helm, etc), so if anyone creates instructions for setting up GridfinityCreator with any of those I'd happily accept the pull-request. 47 | 48 | ## Contributors 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
Kirk Kirkconnell
Kirk Kirkconnell

🤔
wug-ge
wug-ge

🐛
Chase Bolt
Chase Bolt

🐛
Daniel Seifert
Daniel Seifert

🐛 💻
63 | 64 | 65 | 66 | 67 | 68 | 69 | ## Donate 70 | 71 | If you find this project useful a small donation is much appreciated (but by no means required or expected): https://ko-fi.com/jeroen94704 72 | 73 | ## License 74 | 75 | GridfinityCreator © 2023 by Jeroen Bouwens is licensed under CC BY-NC-SA 4.0. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/ 76 | -------------------------------------------------------------------------------- /generators/holeybin/holeybin_generator.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | from cadquery import exporters 3 | from grid_constants import * 4 | import math 5 | from holeybin_settings import HoleShape 6 | 7 | from generators.common.bin_base import bin_base 8 | 9 | class Generator: 10 | def __init__(self, settings, grid) -> None: 11 | self.settings = settings 12 | self.grid = grid 13 | # Precalculate both before and after validation to process settings that changes 14 | self.precalculate() 15 | self.validate_settings() 16 | self.precalculate() 17 | 18 | def precalculate(self): 19 | """Precalculate a number of useful derived values used in construction""" 20 | self.settings.sizeUnitsX = math.ceil((self.settings.numHolesX * self.settings.keepoutDiameter + 2*self.grid.WALL_THICKNESS + self.grid.BRICK_SIZE_TOLERANCE_MM) / self.grid.GRID_UNIT_SIZE_X_MM) 21 | self.settings.sizeUnitsY = math.ceil((self.settings.numHolesY * self.settings.keepoutDiameter + 2*self.grid.WALL_THICKNESS + self.grid.BRICK_SIZE_TOLERANCE_MM) / self.grid.GRID_UNIT_SIZE_Y_MM) 22 | self.settings.sizeUnitsZ = 1 + math.ceil(self.settings.holeDepth / self.grid.HEIGHT_UNITSIZE_MM) 23 | 24 | self.brickSizeX = self.settings.sizeUnitsX * self.grid.GRID_UNIT_SIZE_X_MM - self.grid.BRICK_SIZE_TOLERANCE_MM 25 | self.brickSizeY = self.settings.sizeUnitsY * self.grid.GRID_UNIT_SIZE_Y_MM - self.grid.BRICK_SIZE_TOLERANCE_MM 26 | self.brickSizeZ = self.settings.sizeUnitsZ*self.grid.HEIGHT_UNITSIZE_MM 27 | self.internalSizeX = self.brickSizeX-2*self.grid.WALL_THICKNESS 28 | self.internalSizeY = self.brickSizeY-2*self.grid.WALL_THICKNESS 29 | self.compartmentSizeZ = (self.settings.sizeUnitsZ-1)*self.grid.HEIGHT_UNITSIZE_MM 30 | 31 | def outer_wall(self, basePlane): 32 | """Create the outer wall of the bin""" 33 | 34 | sizeZ = self.compartmentSizeZ 35 | 36 | wall = basePlane.box(self.brickSizeX, self.brickSizeY, sizeZ, centered=False, combine = False) 37 | 38 | result = wall.edges("|Z").fillet(self.grid.CORNER_FILLET_RADIUS) 39 | 40 | return result 41 | 42 | def stacking_lip(self, basePlane): 43 | if self.settings.addStackingLip: 44 | thickness = self.grid.WALL_THICKNESS 45 | 46 | # Increase the height of the block by the thickness of the stacking lip 47 | wall = basePlane.box(self.brickSizeX, self.brickSizeY, self.grid.STACKING_LIP_HEIGHT, centered=False, combine = False) 48 | result = wall.edges("|Z").fillet(self.grid.CORNER_FILLET_RADIUS) 49 | 50 | cutout = ( 51 | wall.faces(">Z").workplane().center(thickness, thickness) 52 | .box(self.internalSizeX, self.internalSizeY, self.grid.STACKING_LIP_HEIGHT, centered=False, combine = False) 53 | .translate((0,0,-self.grid.STACKING_LIP_HEIGHT)) 54 | ) 55 | 56 | # If the walls are thicker than the outside radius of the corners, skip the fillet 57 | if thickness < self.grid.CORNER_FILLET_RADIUS: 58 | cutout = cutout.edges("|Z").fillet(self.grid.CORNER_FILLET_RADIUS-thickness) 59 | 60 | result = result - cutout 61 | 62 | result = result.edges( 63 | cq.selectors.NearestToPointSelector((self.brickSizeX/2, self.brickSizeY/2, (self.compartmentSizeZ+self.grid.STACKING_LIP_HEIGHT)*2)) 64 | ).chamfer(thickness - self.grid.CHAMFER_EPSILON) 65 | 66 | return result 67 | 68 | def holey_grid(self, basePlane): 69 | 70 | # Calculate step-size based on actual internal size to spread the holes evenly across the bin 71 | x_step = self.internalSizeX / self.settings.numHolesX 72 | y_step = self.internalSizeY / self.settings.numHolesY 73 | 74 | # Move the entire hole-grid so it is centered on the bin 75 | offset_x = (self.brickSizeX - self.internalSizeX) / 2 + x_step / 2 76 | offset_y = (self.brickSizeY - self.internalSizeY) / 2 + y_step / 2 77 | 78 | # Create the grid of points where the holes go 79 | hole_points = [] 80 | for x in range(self.settings.numHolesX): 81 | for y in range(self.settings.numHolesY): 82 | hole_points.append((x*x_step + offset_x, y*y_step + offset_y)) 83 | 84 | # Extrude the holes based on the shape setting 85 | if self.settings.holeShape == "HEXAGON": 86 | result = basePlane.pushPoints(hole_points).polygon(6, self.settings.holeSize).extrude(-self.settings.holeDepth, combine="cut") 87 | elif self.settings.holeShape == "SQUARE": 88 | result = basePlane.pushPoints(hole_points).rect(self.settings.holeSize, self.settings.holeSize).extrude(-self.settings.holeDepth, combine="cut") 89 | else: # By default, use HoleShape.CIRCLE 90 | result = basePlane.pushPoints(hole_points).hole(self.settings.holeSize, self.settings.holeDepth) 91 | 92 | return result 93 | 94 | def validate_settings(self): 95 | """Do some sanity checking on the settings to prevent impossible or unreasonable results""" 96 | 97 | # Cap the size in grid-units to avoid thrashing the server 98 | self.settings.sizeUnitsX = min(self.settings.sizeUnitsX, self.grid.MAX_GRID_UNITS) 99 | self.settings.sizeUnitsY = min(self.settings.sizeUnitsY, self.grid.MAX_GRID_UNITS) 100 | self.settings.sizeUnitsZ = min(self.settings.sizeUnitsZ, self.grid.MAX_HEIGHT_UNITS) 101 | 102 | def generate_model(self): 103 | plane = cq.Workplane("XY") 104 | 105 | # First create the base 106 | result = bin_base(plane, self.settings, self.grid) 107 | 108 | # Continue at the top of the base 109 | plane = result.faces(">Z").workplane() 110 | 111 | # Add the outer wall 112 | result.add(self.outer_wall(plane)) 113 | 114 | # Continue from the top of the bin 115 | plane = result.faces(">Z").workplane() 116 | 117 | # Create the hole-grid in the same plane as the previous operation 118 | result = self.holey_grid(plane) 119 | 120 | # Add the stacking lip 121 | result.add(self.stacking_lip(plane)) 122 | 123 | # Combine everything together 124 | result = result.combine(clean=True) 125 | 126 | return result 127 | 128 | def generate_stl(self, filename): 129 | model = self.generate_model() 130 | exporters.export(model, filename) 131 | 132 | 133 | -------------------------------------------------------------------------------- /gfg_main.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import logging 3 | import logging.handlers 4 | import os 5 | import sys 6 | import waitress 7 | 8 | from contextlib import contextmanager 9 | 10 | from flask import Flask, make_response, request 11 | from jinja2 import Environment, FileSystemLoader, StrictUndefined, Template 12 | from werkzeug.middleware.proxy_fix import ProxyFix 13 | 14 | import grid_constants 15 | from grid_constants import * 16 | from version import __version__ 17 | 18 | app = Flask(__name__) 19 | 20 | # Flask-WTF requires an encryption key - the string can be anything 21 | app.config['SECRET_KEY'] = 'hPqPfz!y=moJ!MVO{*tqQO$_Itoo:' 22 | 23 | # Apply proxy fix 24 | app.wsgi_app = ProxyFix( 25 | app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1 26 | ) 27 | 28 | # Globals 29 | generators = [] 30 | logger = None 31 | 32 | # Constants 33 | GEN_FOLDER = "./generators" 34 | MAIN_MODULE = "main.py" 35 | 36 | def inner_render(value, context): 37 | return Template(value).render(context) 38 | 39 | def render_index(form_list, constants, message): 40 | jinja_env = Environment(loader=FileSystemLoader(["./", os.path.realpath(__file__)]), undefined=StrictUndefined) 41 | jinja_env.filters["inner_render"] = inner_render 42 | 43 | index_template = jinja_env.get_template("templates/index.html.j2") 44 | return index_template.render(version=__version__, forms=form_list, message=message, gridsize_x=constants.GRID_UNIT_SIZE_X_MM, 45 | gridsize_y=constants.GRID_UNIT_SIZE_Y_MM, gridsize_z=constants.HEIGHT_UNITSIZE_MM) 46 | 47 | # Handle GET requests for "/" 48 | @app.route('/', methods=['GET']) 49 | def index_get(): 50 | 51 | constants = grid_constants.Grid() 52 | 53 | # If a grid spec cookie is found, set the values contained in it 54 | if request.cookies.get('gridspec'): 55 | values = request.cookies.get('gridspec').split(',') 56 | constants.GRID_UNIT_SIZE_X_MM = float(values[0]) 57 | constants.GRID_UNIT_SIZE_Y_MM = float(values[1]) 58 | constants.HEIGHT_UNITSIZE_MM = float(values[2]) 59 | 60 | constants.recalculate() # Recalculate derived measures 61 | 62 | form_list = [] 63 | 64 | # Create a list of forms to pass to Jinja for rendering 65 | for gen in generators: 66 | form_list.append(gen.get_form()) 67 | 68 | response = make_response(render_index(form_list, constants, '')) 69 | 70 | if not request.cookies.get('gridspec'): 71 | response.set_cookie('gridspec', str('{0},{1},{2}').format(constants.GRID_UNIT_SIZE_X_MM, constants.GRID_UNIT_SIZE_Y_MM, constants.HEIGHT_UNITSIZE_MM)) 72 | 73 | return response 74 | 75 | # Handle POST requests for "/" 76 | @app.route('/', methods=['POST']) 77 | def index_post(): 78 | # Default gridspec 79 | constants = grid_constants.Grid() 80 | 81 | message = "" 82 | 83 | # Use the saved grid size if it was overridden 84 | if request.cookies.get('gridspec'): 85 | c = request.cookies.get('gridspec') 86 | values = c.split(',') 87 | constants.GRID_UNIT_SIZE_X_MM = float(values[0]) 88 | constants.GRID_UNIT_SIZE_Y_MM = float(values[1]) 89 | constants.HEIGHT_UNITSIZE_MM = float(values[2]) 90 | constants.recalculate() # Recalculate derived measures 91 | 92 | 93 | # If the request is from the form that specifies the grid size, override these values 94 | if 'advanced_settings' in request.form: 95 | # Save settings 96 | constants.GRID_UNIT_SIZE_X_MM = float(request.form['gridSizeX']) 97 | constants.GRID_UNIT_SIZE_Y_MM = float(request.form['gridSizeY']) 98 | constants.HEIGHT_UNITSIZE_MM = float(request.form['gridSizeZ']) 99 | constants.recalculate() # Recalculate derived measures 100 | 101 | form_list = [] 102 | 103 | for gen in generators: 104 | # Find the generator for this request 105 | f = gen.get_form() 106 | form_list.append(f) 107 | if gen.handles(request, f): 108 | # Generate an STL with the provided settings 109 | logger.info("Generating {0} for: {1}".format(f.get_title(), request.remote_addr)) 110 | return gen.process(f, constants) 111 | 112 | response = make_response(render_index(form_list, constants, message)) 113 | response.set_cookie('gridspec', str('{0},{1},{2}').format(constants.GRID_UNIT_SIZE_X_MM, constants.GRID_UNIT_SIZE_Y_MM, constants.HEIGHT_UNITSIZE_MM)) 114 | return response 115 | 116 | # From this StackOverflow answer: https://stackoverflow.com/a/41904558 117 | @contextmanager 118 | def add_to_path(p): 119 | import sys 120 | old_path = sys.path 121 | sys.path = sys.path[:] 122 | sys.path.insert(0, p) 123 | try: 124 | yield 125 | finally: 126 | sys.path = old_path 127 | 128 | def load_generators(): 129 | """Scan for generators and load any generators found """ 130 | 131 | generators = [] 132 | 133 | # Each generator is contained in its own subdir 134 | possible_generators = sorted(os.listdir(GEN_FOLDER)) 135 | for entry in possible_generators: 136 | location = os.path.join(GEN_FOLDER, entry) 137 | 138 | # It should be a dir and contain the 139 | if not os.path.isdir(location) or not MAIN_MODULE in os.listdir(location): 140 | continue 141 | 142 | logger.debug("Loading generator {0}".format(entry)) 143 | fname = "{0}/{1}".format(location, MAIN_MODULE) 144 | 145 | # Temporarily expand the search path for modules, so the (sub-)modules needed 146 | # by each generator can be found 147 | try: 148 | with add_to_path(location): 149 | # importlib magic. Loads the module and makes it available to call 150 | spec = importlib.util.spec_from_loader( 151 | entry, 152 | importlib.machinery.SourceFileLoader(entry, fname) 153 | ) 154 | module = importlib.util.module_from_spec(spec) 155 | spec.loader.exec_module(module) 156 | sys.modules[entry] = module 157 | generators.append(module) 158 | except Exception as e: 159 | logger.error(f"Failed to load generator {entry}: {e}") 160 | 161 | return generators 162 | 163 | class serverFilter(): 164 | """Filter records coming from the server out of the access log""" 165 | def filter(self, record): 166 | return (record.name != 'werkzeug') and (record.name != 'waitress') 167 | 168 | if __name__ == "__main__": 169 | portNum = 5000 if 'FLASK_PORT' not in os.environ else os.environ['FLASK_PORT'] 170 | debugMode = False if 'FLASK_DEBUG' not in os.environ else (os.environ['FLASK_DEBUG'] == 'True') 171 | 172 | root = logging.getLogger() 173 | root.setLevel(logging.DEBUG) 174 | 175 | # Configure console logger 176 | console = logging.StreamHandler() 177 | console.setLevel(logging.DEBUG) 178 | formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') 179 | console.setFormatter(formatter) 180 | console.addFilter(serverFilter()) 181 | root.addHandler(console) 182 | 183 | # Ensure log directory exists 184 | log_dir = '/logs' 185 | if not os.path.exists(log_dir): 186 | os.makedirs(log_dir) 187 | 188 | # Configure rotating file logger 189 | fh = logging.handlers.RotatingFileHandler('/logs/access.log', maxBytes=1000000, backupCount=10) 190 | fh.setLevel(logging.DEBUG) 191 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s: %(message)s') 192 | fh.setFormatter(formatter) 193 | fh.addFilter(serverFilter()) 194 | root.addHandler(fh) 195 | 196 | logger = logging.getLogger('GFG') 197 | 198 | try: 199 | generators = load_generators() 200 | except Exception as e: 201 | logger.error(f"Failed to load generators: {e}") 202 | exit(1) 203 | 204 | if debugMode: 205 | logger.info("Started in debug mode") 206 | port = int(os.environ.get('PORT', portNum)) 207 | app.run(debug=True, host='0.0.0.0', port=port) 208 | else: 209 | logger.info("Started in production mode") 210 | waitress.serve(app, listen='*:' + str(portNum), threads=6) -------------------------------------------------------------------------------- /generators/classicbin/classicbin_generator.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | from cadquery import exporters 3 | from dataclasses import dataclass 4 | from grid_constants import * 5 | import logging 6 | 7 | from generators.common.bin_base import bin_base 8 | 9 | logger = logging.getLogger('CBG') 10 | 11 | class Generator: 12 | def __init__(self, settings, grid) -> None: 13 | self.settings = settings 14 | self.grid = grid 15 | 16 | # Precalculate both before and after validation to process settings that changes 17 | self.precalculate() 18 | self.validate_settings() 19 | self.precalculate() 20 | 21 | def precalculate(self): 22 | """Precalculate a number of useful derived values used in construction""" 23 | self.brickSizeX = self.settings.sizeUnitsX * self.grid.GRID_UNIT_SIZE_X_MM - self.grid.BRICK_SIZE_TOLERANCE_MM 24 | self.brickSizeY = self.settings.sizeUnitsY * self.grid.GRID_UNIT_SIZE_Y_MM - self.grid.BRICK_SIZE_TOLERANCE_MM 25 | self.brickSizeZ = self.settings.sizeUnitsZ*self.grid.HEIGHT_UNITSIZE_MM 26 | self.internalSizeX = self.brickSizeX-2*self.grid.WALL_THICKNESS 27 | self.internalSizeY = self.brickSizeY-2*self.grid.WALL_THICKNESS 28 | self.compartmentSizeX = self.internalSizeX / self.settings.compartmentsX 29 | self.compartmentSizeY = self.internalSizeY / self.settings.compartmentsY 30 | self.compartmentSizeZ = (self.settings.sizeUnitsZ-1)*self.grid.HEIGHT_UNITSIZE_MM 31 | 32 | def outer_wall(self, basePlane): 33 | """Create the outer wall of the bin""" 34 | 35 | sizeZ = self.compartmentSizeZ 36 | 37 | if self.settings.addStackingLip: 38 | sizeZ = sizeZ + self.grid.STACKING_LIP_HEIGHT 39 | 40 | wall = basePlane.box(self.brickSizeX, self.brickSizeY, sizeZ, centered=False, combine = False) 41 | 42 | thickness = self.grid.WALL_THICKNESS 43 | wall = wall.edges("|Z").fillet(self.grid.CORNER_FILLET_RADIUS) 44 | 45 | cutout = ( 46 | basePlane.center(thickness, thickness) 47 | .box(self.internalSizeX, self.internalSizeY, sizeZ, centered=False, combine = False) 48 | ) 49 | 50 | # If the walls are thicker than the outside radius of the corners, skip the fillet 51 | if thickness < self.grid.CORNER_FILLET_RADIUS: 52 | cutout = cutout.edges("|Z").fillet(self.grid.CORNER_FILLET_RADIUS-thickness) 53 | 54 | result = wall-cutout 55 | 56 | if self.settings.addStackingLip: 57 | result = result.edges( 58 | cq.selectors.NearestToPointSelector((self.brickSizeX/2, self.brickSizeY/2, sizeZ*2)) 59 | ).chamfer(thickness-self.grid.CHAMFER_EPSILON) 60 | 61 | return result 62 | 63 | def divider_walls(self, basePlane): 64 | """Create a regularly spaced grid of internal divider walls""" 65 | 66 | resultPlane = basePlane.center(self.grid.WALL_THICKNESS, self.grid.WALL_THICKNESS) 67 | result = resultPlane.workplane() 68 | 69 | if(self.settings.compartmentsX > 1): 70 | for x in range(self.settings.compartmentsX-1): 71 | result.add(resultPlane.box(self.settings.dividerThickness,self.internalSizeY, self.compartmentSizeZ, 72 | centered=(True, False,False), combine=False).translate(((x+1)*self.compartmentSizeX, 0, 0))) 73 | 74 | if(self.settings.compartmentsY > 1): 75 | for y in range(self.settings.compartmentsY-1): 76 | result.add(resultPlane.box(self.internalSizeX,self.settings.dividerThickness,self.compartmentSizeZ, 77 | centered=(False, True, False), combine=False).translate((0, (y+1)*self.compartmentSizeY, 0))) 78 | 79 | # Combining fails when there is no overlap between the objects, which is the case when there are 0 dividers in one direction 80 | if(self.settings.compartmentsX > 1 and self.settings.compartmentsY > 1): 81 | result = result.combine() 82 | 83 | return result 84 | 85 | def label_tab(self, basePlane): 86 | """Construct the pickup/label tab""" 87 | 88 | result = basePlane.workplane() 89 | 90 | numRidges = self.settings.compartmentsY if self.settings.multiLabel else 1 91 | labelRidgeHeight = min(self.compartmentSizeZ, self.settings.labelRidgeWidth-self.grid.CHAMFER_EPSILON) 92 | 93 | for x in range(numRidges): 94 | startX = self.grid.WALL_THICKNESS + x*self.compartmentSizeY 95 | result.add( 96 | basePlane.sketch() 97 | .segment((startX,self.brickSizeZ-labelRidgeHeight),(startX,self.brickSizeZ)) 98 | .segment((startX+self.settings.labelRidgeWidth,self.brickSizeZ)) 99 | .segment((startX+self.settings.labelRidgeWidth-labelRidgeHeight,self.brickSizeZ-labelRidgeHeight)) 100 | .close() 101 | .reset() 102 | .assemble() 103 | .finalize() 104 | .extrude(self.internalSizeX) 105 | .edges(">Y").fillet(0.5) 106 | ) 107 | 108 | return result 109 | 110 | def grab_curve(self, basePlane): 111 | 112 | result = basePlane.workplane() 113 | 114 | # To ensure the curve fits, take the smallest of: The height of the divider walls, the length of a compartment, half the brick unit-size (Y-direction) 115 | radius = min((self.settings.sizeUnitsZ-1) * self.grid.HEIGHT_UNITSIZE_MM, self.compartmentSizeY, self.grid.BRICK_UNIT_SIZE_Y/2) 116 | 117 | for y in range(self.settings.compartmentsY): 118 | startX = self.grid.WALL_THICKNESS + (y+1)*self.compartmentSizeY 119 | result.add( 120 | basePlane.sketch() 121 | .segment((startX,self.grid.HEIGHT_UNITSIZE_MM+radius),(startX,self.grid.HEIGHT_UNITSIZE_MM)) 122 | .segment((startX-radius,self.grid.HEIGHT_UNITSIZE_MM)) 123 | .arc((startX-radius,self.grid.HEIGHT_UNITSIZE_MM+radius),radius,270,90) 124 | .assemble() 125 | .finalize() 126 | .extrude(self.internalSizeX) 127 | ) 128 | 129 | return result 130 | 131 | def validate_settings(self): 132 | """Do some sanity checking on the settings to prevent impossible or unreasonable results""" 133 | 134 | # Cap the size in grid-units to avoid thrashing the server 135 | self.settings.sizeUnitsX = min(self.settings.sizeUnitsX, self.grid.MAX_GRID_UNITS) 136 | self.settings.sizeUnitsY = min(self.settings.sizeUnitsY, self.grid.MAX_GRID_UNITS) 137 | self.settings.sizeUnitsZ = min(self.settings.sizeUnitsZ, self.grid.MAX_HEIGHT_UNITS) 138 | self.settings.sizeUnitsZ = max(self.settings.sizeUnitsZ, self.grid.MIN_HEIGHT_UNITS) 139 | 140 | # Limit the number of compartment in each direction 141 | self.settings.compartmentsX = min(self.settings.compartmentsX, self.settings.sizeUnitsX*self.grid.MAX_COMPARTMENTS_PER_GRID_UNIT) 142 | self.settings.compartmentsY = min(self.settings.compartmentsY, self.settings.sizeUnitsY*self.grid.MAX_COMPARTMENTS_PER_GRID_UNIT) 143 | 144 | # Ensure the labeltab is smaller than half the compartmentsize, or it will close off a row 145 | self.settings.labelRidgeWidth = min(self.compartmentSizeY/2, self.settings.labelRidgeWidth) 146 | 147 | # Ensure the label tab is not deeper than the interior height of the bin or it will stick out 148 | # self.settings.labelRidgeWidth = min(self.compartmentSizeZ, self.settings.labelRidgeWidth) 149 | 150 | def generate_model(self): 151 | plane = cq.Workplane("XY") 152 | 153 | # First create the base 154 | result = bin_base(plane, self.settings, self.grid) 155 | 156 | # Continue at the top of the base 157 | plane = result.faces(">Z").workplane() 158 | 159 | # Add the outer walls 160 | result.add(self.outer_wall(plane)) 161 | 162 | # Add the divider walls 163 | result.add(self.divider_walls(plane)) 164 | 165 | # Continue from the left-most outside face of the brick 166 | plane = cq.Workplane("YZ").workplane(offset=self.grid.WALL_THICKNESS) 167 | 168 | # Add the grabbing/label tab 169 | if self.settings.addLabelRidge: 170 | result.add(self.label_tab(plane)) 171 | 172 | # Add the curve 173 | if self.settings.addGrabCurve: 174 | result.add(self.grab_curve(plane)) 175 | 176 | # Combine everything together 177 | result = result.combine(clean=True) 178 | 179 | return result 180 | 181 | def generate_stl(self, filename): 182 | model = self.generate_model() 183 | logger.debug("Saved classicbin to {0}".format(filename)) 184 | exporters.export(model, filename) 185 | 186 | 187 | -------------------------------------------------------------------------------- /generators/lightbin/lightbin_generator.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | from cadquery import exporters 3 | from grid_constants import * 4 | import time 5 | import logging 6 | 7 | logger = logging.getLogger('LBG') 8 | 9 | class Generator: 10 | def __init__(self, settings, grid) -> None: 11 | self.settings = settings 12 | self.grid = grid 13 | 14 | # Precalculate both before and after validation to process settings that changes 15 | self.precalculate() 16 | self.validate_settings() 17 | self.precalculate() 18 | 19 | def precalculate(self): 20 | """Precalculate a number of useful derived values used in construction""" 21 | self.brickSizeX = self.settings.sizeUnitsX * self.grid.GRID_UNIT_SIZE_X_MM - self.grid.BRICK_SIZE_TOLERANCE_MM 22 | self.brickSizeY = self.settings.sizeUnitsY * self.grid.GRID_UNIT_SIZE_Y_MM - self.grid.BRICK_SIZE_TOLERANCE_MM 23 | self.brickSizeZ = self.settings.sizeUnitsZ*self.grid.HEIGHT_UNITSIZE_MM 24 | self.internalSizeX = self.brickSizeX-2*self.grid.WALL_THICKNESS 25 | self.internalSizeY = self.brickSizeY-2*self.grid.WALL_THICKNESS 26 | self.compartmentSizeX = self.internalSizeX / self.settings.compartmentsX 27 | self.compartmentSizeY = self.internalSizeY / self.settings.compartmentsY 28 | self.compartmentSizeZ = (self.settings.sizeUnitsZ-1)*self.grid.HEIGHT_UNITSIZE_MM # This is the height in units minus the thickness of the base 29 | 30 | def unit_base(self, basePlane): 31 | """Construct a 1x1 GridFinity unit base on the provided workplane""" 32 | 33 | x_offs = self.grid.BRICK_UNIT_SIZE_X/2-3.5 34 | frame_pts = [(0+x_offs,0), (0+x_offs, 3.15), 35 | (1.6+x_offs, 4.75), (3.5+x_offs, 4.75), 36 | (1.35+x_offs, 2.6), (1.35+x_offs, 0.8), 37 | (0.55+x_offs, 0) 38 | ] 39 | 40 | path = basePlane.rect(self.grid.BRICK_UNIT_SIZE_X, self.grid.BRICK_UNIT_SIZE_X).val() 41 | path = path.fillet2D(self.grid.BASE_TOP_FILLET_RADIUS, path.Vertices()) 42 | 43 | baseUnit = ( 44 | cq.Workplane("XZ") 45 | .polyline(frame_pts) 46 | .close() 47 | .sweep(path) 48 | ) 49 | 50 | floor = basePlane.box(self.grid.BRICK_UNIT_SIZE_X-6.7,self.grid.BRICK_UNIT_SIZE_X-6.7,self.settings.wallThickness).translate((0,0,self.settings.wallThickness/2)) 51 | baseUnit = baseUnit.add(floor) 52 | baseUnit = baseUnit.combine() 53 | 54 | # Translate the result because it is now centered around the origin, which is inconvenient for subsequent steps 55 | baseUnit = baseUnit.translate((self.grid.BRICK_UNIT_SIZE_X/2, self.grid.BRICK_UNIT_SIZE_Y/2)) 56 | 57 | return baseUnit 58 | 59 | def grid_base(self, basePlane): 60 | """Construct a base of WidthxLength grid units""" 61 | 62 | result = basePlane 63 | 64 | baseUnit = self.unit_base(basePlane) 65 | 66 | for x in range(self.settings.sizeUnitsX): 67 | for y in range(self.settings.sizeUnitsY): 68 | result.add(baseUnit.translate((x*self.grid.GRID_UNIT_SIZE_X_MM, y*self.grid.GRID_UNIT_SIZE_Y_MM, 0))) 69 | 70 | return result 71 | 72 | def brick_floor(self, basePlane): 73 | """Create a floor covering all unit bases""" 74 | 75 | # Create the solid floor 76 | floor = basePlane.box(self.grid.GRID_UNIT_SIZE_X_MM, self.grid.GRID_UNIT_SIZE_X_MM, self.grid.LIGHT_FLOOR_THICKNESS, centered = True, combine = False) 77 | 78 | # Create the cutout and remove it for each base unit 79 | cutoutSizeX = self.grid.BRICK_UNIT_SIZE_X-2*self.grid.WALL_THICKNESS 80 | cutoutSizeY = self.grid.BRICK_UNIT_SIZE_Y-2*self.grid.WALL_THICKNESS 81 | cutout = basePlane.box(cutoutSizeX, cutoutSizeY,3, centered = True, combine=False) 82 | cutout = cutout.edges("|Z") 83 | cutout = cutout.fillet(self.grid.CORNER_FILLET_RADIUS-self.grid.WALL_THICKNESS) 84 | 85 | floor = floor - cutout 86 | 87 | # Chamfer the sharp edges 88 | dx = self.grid.BRICK_UNIT_SIZE_X/2 89 | dy = self.grid.BRICK_UNIT_SIZE_Y/2 90 | s = cq.selectors.BoxSelector((0,0,5), (dx,dy,6)) 91 | floor = floor.edges(s).chamfer(self.grid.LIGHT_FLOOR_THICKNESS-self.grid.CHAMFER_EPSILON) 92 | floor = floor.translate((self.grid.BRICK_UNIT_SIZE_X/2, self.grid.BRICK_UNIT_SIZE_Y/2, self.grid.LIGHT_FLOOR_THICKNESS/2)) 93 | 94 | result = basePlane 95 | for x in range(self.settings.sizeUnitsX): 96 | for y in range(self.settings.sizeUnitsY): 97 | result.add(floor.translate((x*self.grid.GRID_UNIT_SIZE_X_MM, y*self.grid.GRID_UNIT_SIZE_Y_MM, 0))) 98 | 99 | result = result.combine() 100 | 101 | # Create 102 | plane = cq.Workplane("XY") 103 | cutout = plane.box(self.brickSizeX, self.brickSizeY, 1.9, centered=True, combine = False) 104 | cutout = cutout.edges("|Z").fillet(self.grid.CORNER_FILLET_RADIUS) 105 | shrink_box = plane.box(self.brickSizeX+5, self.brickSizeY+5, 1.9, centered = True, combine = False) 106 | shrink_box = shrink_box - cutout 107 | shrink_box = shrink_box.translate((self.brickSizeX/2, self.brickSizeY/2, 5.25)) 108 | 109 | result = result - shrink_box 110 | 111 | return result 112 | 113 | def outer_wall(self, basePlane): 114 | """Create the outer wall of the bin""" 115 | 116 | plane = basePlane.workplane() 117 | sizeZ = self.compartmentSizeZ + self.grid.FLOOR_THICKNESS 118 | 119 | if self.settings.addStackingLip: 120 | sizeZ = sizeZ + self.grid.STACKING_LIP_HEIGHT 121 | 122 | wall = plane.box(self.brickSizeX, self.brickSizeY, sizeZ, combine = False) 123 | 124 | thickness = self.grid.WALL_THICKNESS 125 | wall = wall.edges("|Z").fillet(self.grid.CORNER_FILLET_RADIUS) 126 | 127 | cutout = ( 128 | plane.box(self.brickSizeX-2*thickness, self.brickSizeY-2*thickness, sizeZ, combine = False) 129 | ) 130 | 131 | # If the walls are thicker than the outside radius of the corners, skip the fillet 132 | if thickness < self.grid.CORNER_FILLET_RADIUS: 133 | cutout = cutout.edges("|Z").fillet(self.grid.CORNER_FILLET_RADIUS-thickness) 134 | 135 | result = wall-cutout 136 | 137 | if self.settings.addStackingLip: 138 | result = result.edges( 139 | cq.selectors.NearestToPointSelector((0, 0, sizeZ*2)) 140 | ).chamfer(thickness-self.grid.CHAMFER_EPSILON) 141 | 142 | result = result.translate((self.brickSizeX/2,self.brickSizeY/2,sizeZ/2-self.grid.LIGHT_FLOOR_THICKNESS)) 143 | 144 | return result 145 | 146 | def label_tab(self, basePlane): 147 | """Construct the pickup/label tab""" 148 | 149 | result = basePlane 150 | 151 | startX = self.grid.WALL_THICKNESS 152 | 153 | # Limit the height of the label ridge to avoid it being taller than the compartment 154 | labelRidgeHeight = min(self.compartmentSizeZ+2.25, self.settings.labelRidgeWidth-self.grid.CHAMFER_EPSILON) 155 | 156 | # Create the label tab profile and extrude it 157 | result.add( 158 | basePlane.sketch() 159 | .segment((startX,self.brickSizeZ-labelRidgeHeight),(startX,self.brickSizeZ)) 160 | .segment((startX+self.settings.labelRidgeWidth,self.brickSizeZ)) 161 | .close() 162 | .reset() 163 | .assemble() 164 | .finalize() 165 | .extrude(self.internalSizeX) 166 | .edges(">Y").fillet(0.5) 167 | ) 168 | 169 | return result 170 | 171 | def validate_settings(self): 172 | """Do some sanity checking on the settings to prevent impossible or unreasonable results""" 173 | 174 | # Cap the size in grid-units to avoid thrashing the server 175 | self.settings.sizeUnitsX = min(self.settings.sizeUnitsX, self.grid.MAX_GRID_UNITS) 176 | self.settings.sizeUnitsY = min(self.settings.sizeUnitsY, self.grid.MAX_GRID_UNITS) 177 | self.settings.sizeUnitsZ = min(self.settings.sizeUnitsZ, self.grid.MAX_HEIGHT_UNITS) 178 | 179 | # Limit the number of compartment in each direction 180 | self.settings.compartmentsX = min(self.settings.compartmentsX, self.settings.sizeUnitsX*self.grid.MAX_COMPARTMENTS_PER_GRID_UNIT) 181 | self.settings.compartmentsY = min(self.settings.compartmentsY, self.settings.sizeUnitsY*self.grid.MAX_COMPARTMENTS_PER_GRID_UNIT) 182 | 183 | # Ensure the labeltab is smaller than half the compartmentsize, or it will close off a row 184 | self.settings.labelRidgeWidth = min(self.compartmentSizeY/2, self.settings.labelRidgeWidth) 185 | 186 | def generate_model(self): 187 | # Add the base of Gridfinity profiles 188 | result = self.grid_base(cq.Workplane("XY")) 189 | 190 | # Continue from the top of the base 191 | plane = result.faces(">Z").workplane() 192 | 193 | # Add the floor of the bin 194 | result.add(self.brick_floor(plane)) 195 | 196 | # Add the outer walls 197 | plane = result.faces(">Z").workplane() 198 | result.add(self.outer_wall(plane)) 199 | 200 | # Add the grabbing/label tab 201 | if self.settings.addLabelRidge: 202 | plane = cq.Workplane("YZ").workplane(offset=self.grid.WALL_THICKNESS) 203 | result.add(self.label_tab(plane)) 204 | 205 | # Combine everything together 206 | result = result.combine() 207 | 208 | return result 209 | 210 | def generate_stl(self, filename): 211 | model = self.generate_model() 212 | exporters.export(model, filename) 213 | 214 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 2 | Public License 3 | 4 | By exercising the Licensed Rights (defined below), You accept and agree 5 | to be bound by the terms and conditions of this Creative Commons 6 | Attribution-NonCommercial-ShareAlike 4.0 International Public License 7 | ("Public License"). To the extent this Public License may be 8 | interpreted as a contract, You are granted the Licensed Rights in 9 | consideration of Your acceptance of these terms and conditions, and the 10 | Licensor grants You such rights in consideration of benefits the 11 | Licensor receives from making the Licensed Material available under 12 | these terms and conditions. 13 | 14 | 15 | Section 1 -- Definitions. 16 | 17 | a. Adapted Material means material subject to Copyright and Similar 18 | Rights that is derived from or based upon the Licensed Material 19 | and in which the Licensed Material is translated, altered, 20 | arranged, transformed, or otherwise modified in a manner requiring 21 | permission under the Copyright and Similar Rights held by the 22 | Licensor. For purposes of this Public License, where the Licensed 23 | Material is a musical work, performance, or sound recording, 24 | Adapted Material is always produced where the Licensed Material is 25 | synched in timed relation with a moving image. 26 | 27 | b. Adapter's License means the license You apply to Your Copyright 28 | and Similar Rights in Your contributions to Adapted Material in 29 | accordance with the terms and conditions of this Public License. 30 | 31 | c. BY-NC-SA Compatible License means a license listed at 32 | creativecommons.org/compatiblelicenses, approved by Creative 33 | Commons as essentially the equivalent of this Public License. 34 | 35 | d. Copyright and Similar Rights means copyright and/or similar rights 36 | closely related to copyright including, without limitation, 37 | performance, broadcast, sound recording, and Sui Generis Database 38 | Rights, without regard to how the rights are labeled or 39 | categorized. For purposes of this Public License, the rights 40 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 41 | Rights. 42 | 43 | e. Effective Technological Measures means those measures that, in the 44 | absence of proper authority, may not be circumvented under laws 45 | fulfilling obligations under Article 11 of the WIPO Copyright 46 | Treaty adopted on December 20, 1996, and/or similar international 47 | agreements. 48 | 49 | f. Exceptions and Limitations means fair use, fair dealing, and/or 50 | any other exception or limitation to Copyright and Similar Rights 51 | that applies to Your use of the Licensed Material. 52 | 53 | g. License Elements means the license attributes listed in the name 54 | of a Creative Commons Public License. The License Elements of this 55 | Public License are Attribution, NonCommercial, and ShareAlike. 56 | 57 | h. Licensed Material means the artistic or literary work, database, 58 | or other material to which the Licensor applied this Public 59 | License. 60 | 61 | i. Licensed Rights means the rights granted to You subject to the 62 | terms and conditions of this Public License, which are limited to 63 | all Copyright and Similar Rights that apply to Your use of the 64 | Licensed Material and that the Licensor has authority to license. 65 | 66 | j. Licensor means the individual(s) or entity(ies) granting rights 67 | under this Public License. 68 | 69 | k. NonCommercial means not primarily intended for or directed towards 70 | commercial advantage or monetary compensation. For purposes of 71 | this Public License, the exchange of the Licensed Material for 72 | other material subject to Copyright and Similar Rights by digital 73 | file-sharing or similar means is NonCommercial provided there is 74 | no payment of monetary compensation in connection with the 75 | exchange. 76 | 77 | l. Share means to provide material to the public by any means or 78 | process that requires permission under the Licensed Rights, such 79 | as reproduction, public display, public performance, distribution, 80 | dissemination, communication, or importation, and to make material 81 | available to the public including in ways that members of the 82 | public may access the material from a place and at a time 83 | individually chosen by them. 84 | 85 | m. Sui Generis Database Rights means rights other than copyright 86 | resulting from Directive 96/9/EC of the European Parliament and of 87 | the Council of 11 March 1996 on the legal protection of databases, 88 | as amended and/or succeeded, as well as other essentially 89 | equivalent rights anywhere in the world. 90 | 91 | n. You means the individual or entity exercising the Licensed Rights 92 | under this Public License. Your has a corresponding meaning. 93 | 94 | 95 | Section 2 -- Scope. 96 | 97 | a. License grant. 98 | 99 | 1. Subject to the terms and conditions of this Public License, 100 | the Licensor hereby grants You a worldwide, royalty-free, 101 | non-sublicensable, non-exclusive, irrevocable license to 102 | exercise the Licensed Rights in the Licensed Material to: 103 | 104 | a. reproduce and Share the Licensed Material, in whole or 105 | in part, for NonCommercial purposes only; and 106 | 107 | b. produce, reproduce, and Share Adapted Material for 108 | NonCommercial purposes only. 109 | 110 | 2. Exceptions and Limitations. For the avoidance of doubt, where 111 | Exceptions and Limitations apply to Your use, this Public 112 | License does not apply, and You do not need to comply with 113 | its terms and conditions. 114 | 115 | 3. Term. The term of this Public License is specified in Section 116 | 6(a). 117 | 118 | 4. Media and formats; technical modifications allowed. The 119 | Licensor authorizes You to exercise the Licensed Rights in 120 | all media and formats whether now known or hereafter created, 121 | and to make technical modifications necessary to do so. The 122 | Licensor waives and/or agrees not to assert any right or 123 | authority to forbid You from making technical modifications 124 | necessary to exercise the Licensed Rights, including 125 | technical modifications necessary to circumvent Effective 126 | Technological Measures. For purposes of this Public License, 127 | simply making modifications authorized by this Section 2(a) 128 | (4) never produces Adapted Material. 129 | 130 | 5. Downstream recipients. 131 | 132 | a. Offer from the Licensor -- Licensed Material. Every 133 | recipient of the Licensed Material automatically 134 | receives an offer from the Licensor to exercise the 135 | Licensed Rights under the terms and conditions of this 136 | Public License. 137 | 138 | b. Additional offer from the Licensor -- Adapted Material. 139 | Every recipient of Adapted Material from You 140 | automatically receives an offer from the Licensor to 141 | exercise the Licensed Rights in the Adapted Material 142 | under the conditions of the Adapter's License You apply. 143 | 144 | c. No downstream restrictions. You may not offer or impose 145 | any additional or different terms or conditions on, or 146 | apply any Effective Technological Measures to, the 147 | Licensed Material if doing so restricts exercise of the 148 | Licensed Rights by any recipient of the Licensed 149 | Material. 150 | 151 | 6. No endorsement. Nothing in this Public License constitutes or 152 | may be construed as permission to assert or imply that You 153 | are, or that Your use of the Licensed Material is, connected 154 | with, or sponsored, endorsed, or granted official status by, 155 | the Licensor or others designated to receive attribution as 156 | provided in Section 3(a)(1)(A)(i). 157 | 158 | b. Other rights. 159 | 160 | 1. Moral rights, such as the right of integrity, are not 161 | licensed under this Public License, nor are publicity, 162 | privacy, and/or other similar personality rights; however, to 163 | the extent possible, the Licensor waives and/or agrees not to 164 | assert any such rights held by the Licensor to the limited 165 | extent necessary to allow You to exercise the Licensed 166 | Rights, but not otherwise. 167 | 168 | 2. Patent and trademark rights are not licensed under this 169 | Public License. 170 | 171 | 3. To the extent possible, the Licensor waives any right to 172 | collect royalties from You for the exercise of the Licensed 173 | Rights, whether directly or through a collecting society 174 | under any voluntary or waivable statutory or compulsory 175 | licensing scheme. In all other cases the Licensor expressly 176 | reserves any right to collect such royalties, including when 177 | the Licensed Material is used other than for NonCommercial 178 | purposes. 179 | 180 | 181 | Section 3 -- License Conditions. 182 | 183 | Your exercise of the Licensed Rights is expressly made subject to the 184 | following conditions. 185 | 186 | a. Attribution. 187 | 188 | 1. If You Share the Licensed Material (including in modified 189 | form), You must: 190 | 191 | a. retain the following if it is supplied by the Licensor 192 | with the Licensed Material: 193 | 194 | i. identification of the creator(s) of the Licensed 195 | Material and any others designated to receive 196 | attribution, in any reasonable manner requested by 197 | the Licensor (including by pseudonym if 198 | designated); 199 | 200 | ii. a copyright notice; 201 | 202 | iii. a notice that refers to this Public License; 203 | 204 | iv. a notice that refers to the disclaimer of 205 | warranties; 206 | 207 | v. a URI or hyperlink to the Licensed Material to the 208 | extent reasonably practicable; 209 | 210 | b. indicate if You modified the Licensed Material and 211 | retain an indication of any previous modifications; and 212 | 213 | c. indicate the Licensed Material is licensed under this 214 | Public License, and include the text of, or the URI or 215 | hyperlink to, this Public License. 216 | 217 | 2. You may satisfy the conditions in Section 3(a)(1) in any 218 | reasonable manner based on the medium, means, and context in 219 | which You Share the Licensed Material. For example, it may be 220 | reasonable to satisfy the conditions by providing a URI or 221 | hyperlink to a resource that includes the required 222 | information. 223 | 3. If requested by the Licensor, You must remove any of the 224 | information required by Section 3(a)(1)(A) to the extent 225 | reasonably practicable. 226 | 227 | b. ShareAlike. 228 | 229 | In addition to the conditions in Section 3(a), if You Share 230 | Adapted Material You produce, the following conditions also apply. 231 | 232 | 1. The Adapter's License You apply must be a Creative Commons 233 | license with the same License Elements, this version or 234 | later, or a BY-NC-SA Compatible License. 235 | 236 | 2. You must include the text of, or the URI or hyperlink to, the 237 | Adapter's License You apply. You may satisfy this condition 238 | in any reasonable manner based on the medium, means, and 239 | context in which You Share Adapted Material. 240 | 241 | 3. You may not offer or impose any additional or different terms 242 | or conditions on, or apply any Effective Technological 243 | Measures to, Adapted Material that restrict exercise of the 244 | rights granted under the Adapter's License You apply. 245 | 246 | 247 | Section 4 -- Sui Generis Database Rights. 248 | 249 | Where the Licensed Rights include Sui Generis Database Rights that 250 | apply to Your use of the Licensed Material: 251 | 252 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 253 | to extract, reuse, reproduce, and Share all or a substantial 254 | portion of the contents of the database for NonCommercial purposes 255 | only; 256 | 257 | b. if You include all or a substantial portion of the database 258 | contents in a database in which You have Sui Generis Database 259 | Rights, then the database in which You have Sui Generis Database 260 | Rights (but not its individual contents) is Adapted Material, 261 | including for purposes of Section 3(b); and 262 | 263 | c. You must comply with the conditions in Section 3(a) if You Share 264 | all or a substantial portion of the contents of the database. 265 | 266 | For the avoidance of doubt, this Section 4 supplements and does not 267 | replace Your obligations under this Public License where the Licensed 268 | Rights include other Copyright and Similar Rights. 269 | 270 | 271 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 272 | 273 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 274 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 275 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 276 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 277 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 278 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 279 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 280 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 281 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 282 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 283 | 284 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 285 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 286 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 287 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 288 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 289 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 290 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 291 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 292 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 293 | 294 | c. The disclaimer of warranties and limitation of liability provided 295 | above shall be interpreted in a manner that, to the extent 296 | possible, most closely approximates an absolute disclaimer and 297 | waiver of all liability. 298 | 299 | 300 | Section 6 -- Term and Termination. 301 | 302 | a. This Public License applies for the term of the Copyright and 303 | Similar Rights licensed here. However, if You fail to comply with 304 | this Public License, then Your rights under this Public License 305 | terminate automatically. 306 | 307 | b. Where Your right to use the Licensed Material has terminated under 308 | Section 6(a), it reinstates: 309 | 310 | 1. automatically as of the date the violation is cured, provided 311 | it is cured within 30 days of Your discovery of the 312 | violation; or 313 | 314 | 2. upon express reinstatement by the Licensor. 315 | 316 | For the avoidance of doubt, this Section 6(b) does not affect any 317 | right the Licensor may have to seek remedies for Your violations 318 | of this Public License. 319 | 320 | c. For the avoidance of doubt, the Licensor may also offer the 321 | Licensed Material under separate terms or conditions or stop 322 | distributing the Licensed Material at any time; however, doing so 323 | will not terminate this Public License. 324 | 325 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 326 | License. 327 | 328 | 329 | Section 7 -- Other Terms and Conditions. 330 | 331 | a. The Licensor shall not be bound by any additional or different 332 | terms or conditions communicated by You unless expressly agreed. 333 | 334 | b. Any arrangements, understandings, or agreements regarding the 335 | Licensed Material not stated herein are separate from and 336 | independent of the terms and conditions of this Public License. 337 | 338 | 339 | Section 8 -- Interpretation. 340 | 341 | a. For the avoidance of doubt, this Public License does not, and 342 | shall not be interpreted to, reduce, limit, restrict, or impose 343 | conditions on any use of the Licensed Material that could lawfully 344 | be made without permission under this Public License. 345 | 346 | b. To the extent possible, if any provision of this Public License is 347 | deemed unenforceable, it shall be automatically reformed to the 348 | minimum extent necessary to make it enforceable. If the provision 349 | cannot be reformed, it shall be severed from this Public License 350 | without affecting the enforceability of the remaining terms and 351 | conditions. 352 | 353 | c. No term or condition of this Public License will be waived and no 354 | failure to comply consented to unless expressly agreed to by the 355 | Licensor. 356 | 357 | d. Nothing in this Public License constitutes or may be interpreted 358 | as a limitation upon, or waiver of, any privileges and immunities 359 | that apply to the Licensor or You, including from the legal 360 | processes of any jurisdiction or authority. 361 | 362 | ======================================================================= 363 | 364 | Creative Commons is not a party to its public 365 | licenses. Notwithstanding, Creative Commons may elect to apply one of 366 | its public licenses to material it publishes and in those instances 367 | will be considered the “Licensor.” The text of the Creative Commons 368 | public licenses is dedicated to the public domain under the CC0 Public 369 | Domain Dedication. Except for the limited purpose of indicating that 370 | material is shared under a Creative Commons public license or as 371 | otherwise permitted by the Creative Commons policies published at 372 | creativecommons.org/policies, Creative Commons does not authorize the 373 | use of the trademark "Creative Commons" or any other trademark or logo 374 | of Creative Commons without its prior written consent including, 375 | without limitation, in connection with any unauthorized modifications 376 | to any of its public licenses or any other arrangements, 377 | understandings, or agreements concerning use of licensed material. For 378 | the avoidance of doubt, this paragraph does not form part of the 379 | public licenses. 380 | 381 | Creative Commons may be contacted at creativecommons.org. 382 | --------------------------------------------------------------------------------