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:
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.
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:
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)
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
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 |
6 |
Size: Width, Length and Height of the bin in grid units (42mm for Width and Length, 7mm for Height)
7 |
Other: Select the output format (STL or STEP) and whether to include a stacking lip
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
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 |
6 |
Size: Width, Length and Height of the bin in grid units (42mm for Width and Length, 7mm for Height)
7 |
Magnets: Control whether magnet holes should be added, their size, and if additional holes for screws and/or magnet removal should be added as well
8 |
Other: Select the output format (STL or STEP) and whether to include a stacking lip
9 |
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 |
8 |
Hole Grid: How many holes you want in the Width and Length directions. The generator will create a bin of the required size
9 |
Holes: Control whether magnet holes should be added, their size, and if additional holes for screws and/or
10 | magnet removal should be added as well
11 |
Other: Select the output format (STL or STEP) and whether to include a stacking lip
12 |
--------------------------------------------------------------------------------
/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 |
6 |
Size: Width, Length and Height of the bin in grid units (42mm for Width and Length, 7mm for Height)
7 |
Compartments: The number of compartments in the Length and Width direction
8 |
Magnets: Control whether magnet holes should be added, their size, and if additional holes for screws and/or magnet removal should be added as well
9 |
Labels: Control whether no label tab, a single label tab or a label tab for each row of compartments should be added
10 |
Other: Select the output format (STL or STEP) and whether to include a stacking lip and/or scoop ramp
The number of holes in the grid can be specified in one of two ways:
2 |
3 |
4 |
By setting the number of holes in the Width and Length directions
5 |
By setting the bin-size in gridfinity units.
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 |
3 |
Magnets are installed in the bottom of the bins and the base-plate has a suitable metal content (e.g. sheet metal)
4 |
Magnets are part of the base-plate, and there are screws in the bottom of the bins
5 |
6 |
7 |
To accomodate this, there are a few options to control the cutouts on the bottom of the bin:
8 |
9 |
10 |
Magnet holes: Controls whether magnet-sized holes are added to the bottom
11 |
Magnet removal holes: If selected this adds smaller holes slightly offset from the main magnet holes. This allows easier removal of magnets, should this be necessary
12 |
Screw holes: Controls whether screw holes are added to the bottom. Should be combined with the "Magnet holes" option to make room for a screw-head
13 |
Magnet-hole diameter: Controls how large the magnet holes are to accomodate different sizes. The GF default is 6.5mm
14 |
15 |
16 |
Below is a render of what different combinations of these options look like:
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:
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 |
42 |
CadQuery (Python framework for procedural CAD modelling)
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 |
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 |
90 |
91 |
133 | {% endblock %}
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](#contributors)
2 | [](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 | 
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 |