├── .devcontainer └── devcontainer.json ├── .gitignore ├── LICENSE ├── README.md ├── baseplate.scad.j2 ├── gridfinity_calculator.py └── requirements.txt /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Python 3", 3 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 4 | "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye", 5 | "customizations": { 6 | "codespaces": { 7 | "openFiles": [ 8 | "README.md", 9 | "gridfinity_calculator.py" 10 | ] 11 | }, 12 | "vscode": { 13 | "settings": {}, 14 | "extensions": [ 15 | "ms-python.python", 16 | "ms-python.vscode-pylance" 17 | ] 18 | } 19 | }, 20 | "updateContentCommand": "[ -f packages.txt ] && sudo apt update && sudo apt upgrade -y && sudo xargs apt install -y ; 2 | 3 | // Override parameters here 4 | gridx = {{ grid_x }}; 5 | gridy = {{ grid_y }}; 6 | distancex = {{ grid_x * 42 + padding_x }}; 7 | distancey = {{ grid_y * 42 + padding_y }}; 8 | fitx = {{ fit_x }}; 9 | fity = {{ fit_y }}; 10 | style_plate = 0; 11 | enable_magnet = false; 12 | style_hole = 0; 13 | 14 | // Call the function after parameter overrides 15 | gridfinityBaseplate([gridx, gridy], l_grid, [distancex, distancey], style_plate, hole_options, style_hole, [fitx, fity]); 16 | -------------------------------------------------------------------------------- /gridfinity_calculator.py: -------------------------------------------------------------------------------- 1 | import io 2 | import zipfile 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | import streamlit as st 7 | from jinja2 import Template 8 | 9 | UNITS = ["Millimeters", "Inches"] 10 | 11 | 12 | # Function to convert inches to millimeters if needed 13 | def convert_to_mm(value, units): 14 | if units == "Inches": 15 | return value * 25.4 16 | return value 17 | 18 | 19 | def build_plate_matrix(total_units_x, total_units_y, max_units_x, max_units_y): 20 | plate_matrix = np.zeros((total_units_y, total_units_x), dtype=int) 21 | plate_counter = 1 22 | 23 | for y in range(0, total_units_y, max_units_y): 24 | for x in range(0, total_units_x, max_units_x): 25 | plate_x = min(max_units_x, total_units_x - x) 26 | plate_y = min(max_units_y, total_units_y - y) 27 | 28 | # Prevent the last row/column from being a 1x dimension 29 | if plate_x == 1 and x > 0: 30 | plate_matrix[y:y + plate_y, x - 1:x + 1] = plate_counter 31 | elif plate_y == 1 and y > 0: 32 | plate_matrix[y - 1:y + 1, x:x + plate_x] = plate_counter 33 | else: 34 | plate_matrix[y:y + plate_y, x:x + plate_x] = plate_counter 35 | 36 | plate_counter += 1 37 | 38 | return plate_matrix, plate_counter - 1 39 | 40 | 41 | def determine_padding(plate_matrix, leftover_x, leftover_y, padding_option): 42 | y, x = plate_matrix.shape 43 | unique_plates = np.unique(plate_matrix) 44 | bill_of_materials_with_padding = {} 45 | 46 | for plate in unique_plates: 47 | if plate == 0: 48 | continue 49 | 50 | # Find the bounding box of this plate 51 | rows, cols = np.where(plate_matrix == plate) 52 | min_row, max_row = rows.min(), rows.max() 53 | min_col, max_col = cols.min(), cols.max() 54 | 55 | plate_x = max_col - min_col + 1 56 | plate_y = max_row - min_row + 1 57 | 58 | padding_info = [] 59 | fitx, fity = 0, 0 60 | if padding_option == "Corner Justify": 61 | if max_col == x - 1 and leftover_x > 0: # Rightmost plate 62 | padding_info.append(f"{round(leftover_x, 1)}mm Right") 63 | fitx = 1 64 | if max_row == y - 1 and leftover_y > 0: # Topmost plate 65 | padding_info.append(f"{round(leftover_y, 1)}mm Top") 66 | fity = 1 67 | elif padding_option == "Center Justify": 68 | if min_col == 0 and leftover_x > 0: # Leftmost plate 69 | padding_info.append(f"{round(leftover_x / 2, 1)}mm Left") 70 | fitx = -1 71 | if max_col == x - 1 and leftover_x > 0: # Rightmost plate 72 | padding_info.append(f"{round(leftover_x / 2, 1)}mm Right") 73 | fitx = 1 74 | if min_row == 0 and leftover_y > 0: # Bottommost plate 75 | padding_info.append(f"{round(leftover_y / 2, 1)}mm Bottom") 76 | fity = -1 77 | if max_row == y - 1 and leftover_y > 0: # Topmost plate 78 | padding_info.append(f"{round(leftover_y / 2, 1)}mm Top") 79 | fity = 1 80 | 81 | plate_key = f"{plate_x}x{plate_y}" 82 | if padding_info: 83 | plate_key += f" ({', '.join(padding_info)})" 84 | 85 | if plate_key in bill_of_materials_with_padding: 86 | bill_of_materials_with_padding[plate_key] += 1 87 | else: 88 | bill_of_materials_with_padding[plate_key] = 1 89 | 90 | return bill_of_materials_with_padding 91 | 92 | 93 | def calculate_baseplates(printer_x, printer_y, space_x, space_y, grid_size=42): 94 | total_units_x = int(space_x // grid_size) 95 | total_units_y = int(space_y // grid_size) 96 | 97 | max_units_x = int(printer_x // grid_size) 98 | max_units_y = int(printer_y // grid_size) 99 | 100 | layout = np.zeros((total_units_y, total_units_x), dtype=int) 101 | 102 | plate_matrix, _ = build_plate_matrix(total_units_x, total_units_y, max_units_x, max_units_y) 103 | 104 | leftover_x = space_x - total_units_x * grid_size 105 | leftover_y = space_y - total_units_y * grid_size 106 | 107 | return plate_matrix, leftover_x, leftover_y, total_units_x, total_units_y, max_units_x, max_units_y 108 | 109 | 110 | def summarize_bom(plate_matrix): 111 | y, x = plate_matrix.shape 112 | unique_plates = np.unique(plate_matrix) 113 | bill_of_materials = {} 114 | 115 | for plate in unique_plates: 116 | if plate == 0: 117 | continue 118 | 119 | rows, cols = np.where(plate_matrix == plate) 120 | min_row, max_row = rows.min(), rows.max() 121 | min_col, max_col = cols.min(), cols.max() 122 | 123 | plate_x = max_col - min_col + 1 124 | plate_y = max_row - min_row + 1 125 | 126 | plate_key = f"{plate_x}x{plate_y}" 127 | if plate_key in bill_of_materials: 128 | bill_of_materials[plate_key] += 1 129 | else: 130 | bill_of_materials[plate_key] = 1 131 | 132 | return bill_of_materials 133 | 134 | 135 | def generate_openscad_code(gridx: int, gridy: int, padding_x: int = 0, padding_y: int = 0, fitx: int = 0, 136 | fity: int = 0): 137 | with open("baseplate.scad.j2", "r") as f: 138 | scad_template = Template(str(f.read())) 139 | 140 | return scad_template.render(grid_x=gridx, 141 | grid_y=gridy, 142 | padding_x=padding_x, 143 | padding_y=padding_y, 144 | fit_x=fitx, 145 | fit_y=fity) 146 | 147 | 148 | def create_zip_from_scad_dict(scads_dict): 149 | zip_buffer = io.BytesIO() 150 | with zipfile.ZipFile(zip_buffer, "w") as zip_file: 151 | for key, value in scads_dict.items(): 152 | zip_file.writestr(key, value) 153 | 154 | return zip_buffer.getvalue() 155 | 156 | 157 | def main(): 158 | st.title("Gridfinity Baseplate Layout Calculator - Optimized to Avoid Any 1x Dimension Baseplates") 159 | 160 | # Dropdowns for units selection 161 | printer_units = st.selectbox("Select Printer Dimensions Units:", options=UNITS) 162 | space_units = st.selectbox("Select Area Dimensions Units:", options=UNITS, ) 163 | 164 | # Inputs with updated labels 165 | printer_x = st.number_input(f"Printer Max Build Size X ({printer_units}):", 166 | value=227 if printer_units == "Millimeters" else 8.94) 167 | printer_y = st.number_input(f"Printer Max Build Size Y ({printer_units}):", 168 | value=255 if printer_units == "Millimeters" else 10.04) 169 | space_x = st.number_input(f"Enter the space's X dimension you want to fill ({space_units}):", 170 | value=1000 if space_units == "Millimeters" else 39.37) 171 | space_y = st.number_input(f"Enter the space's Y dimension you want to fill ({space_units}):", 172 | value=800 if space_units == "Millimeters" else 31.5) 173 | 174 | # Convert to millimeters if needed 175 | printer_x_mm = convert_to_mm(printer_x, printer_units) 176 | printer_y_mm = convert_to_mm(printer_y, printer_units) 177 | space_x_mm = convert_to_mm(space_x, space_units) 178 | space_y_mm = convert_to_mm(space_y, space_units) 179 | 180 | # Padding option dropdown 181 | padding_option = st.selectbox("Select Padding Calculation Option:", 182 | ["Corner Justify", "Center Justify", "No Padding Calculation"]) 183 | 184 | if st.button("Calculate Layout"): 185 | # Get the baseplates based on printer size and desired space dimensions. 186 | layout, leftover_x, leftover_y, total_units_x, total_units_y, max_units_x, max_units_y = calculate_baseplates( 187 | printer_x_mm, printer_y_mm, space_x_mm, space_y_mm) 188 | 189 | if padding_option != "No Padding Calculation": 190 | # The following loop ensures every baseplate (including padding) will fit in the printer's specified 191 | # size. If either x or y is too big, try again by adjusting the printer size down by 1 mm. Continue to do 192 | # so, until all fits within the max allowed print size. 193 | adjustment = 1 194 | while (max_units_x * 42) + leftover_x >= printer_x_mm or (max_units_y * 42) + leftover_y >= printer_y_mm: 195 | layout, leftover_x, leftover_y, total_units_x, total_units_y, max_units_x, max_units_y = ( 196 | calculate_baseplates(printer_x_mm - adjustment, printer_y_mm - adjustment, space_x_mm, space_y_mm)) 197 | adjustment += 1 198 | 199 | # Store results in session state 200 | st.session_state.layout = layout 201 | st.session_state.leftover_x = leftover_x 202 | st.session_state.leftover_y = leftover_y 203 | st.session_state.total_units_x = total_units_x 204 | st.session_state.total_units_y = total_units_y 205 | 206 | # Display results 207 | st.write(f"Total fill area Gridfinity units (X x Y): {total_units_x} x {total_units_y}") 208 | st.write(f"Leftover X distance: {round(leftover_x, 1)} mm") 209 | st.write(f"Leftover Y distance: {round(leftover_y, 1)} mm") 210 | max_plate_size = f"{max_units_x}x{max_units_y} Gridfinity units" 211 | st.write(f"Maximum plate size your printer can handle (including padding): {max_plate_size}") 212 | 213 | if 'layout' in st.session_state: 214 | layout = st.session_state.layout 215 | leftover_x = st.session_state.leftover_x 216 | leftover_y = st.session_state.leftover_y 217 | total_units_x = st.session_state.total_units_x 218 | total_units_y = st.session_state.total_units_y 219 | 220 | scad_dict = dict() 221 | 222 | if padding_option != "No Padding Calculation": 223 | bill_of_materials_with_padding = determine_padding(layout, leftover_x, leftover_y, padding_option) 224 | st.write("Bill of Materials with Padding:") 225 | 226 | for size, quantity in bill_of_materials_with_padding.items(): 227 | st.write(f"{quantity} x {size}") 228 | size_part = size.split(' ')[0] 229 | gridx, gridy = map(int, size_part.split('x')) 230 | 231 | # Extract padding based on size 232 | if padding_option == "Corner Justify": 233 | padding_x = leftover_x if 'Right' in size else 0 234 | padding_y = leftover_y if 'Top' in size else 0 235 | fitx, fity = 0, 0 236 | if 'Left' in size: 237 | fitx = -1 238 | elif 'Right' in size: 239 | fitx = 1 240 | if 'Bottom' in size: 241 | fity = -1 242 | elif 'Top' in size: 243 | fity = 1 244 | 245 | elif padding_option == "Center Justify": 246 | # Center Justify: split padding equally between both sides 247 | padding_x = leftover_x / 2 248 | padding_y = leftover_y / 2 249 | fitx, fity = 0, 0 250 | if 'Left' in size and 'Right' in size: 251 | fitx = 0 # Center padding 252 | elif 'Left' in size: 253 | fitx = -1 254 | elif 'Right' in size: 255 | fitx = 1 256 | 257 | # Adjust fity based on top/bottom padding 258 | if 'Top' in size and 'Bottom' in size: 259 | fity = 0 # Center padding 260 | elif 'Bottom' in size: 261 | fity = -1 262 | elif 'Top' in size: 263 | fity = 1 264 | 265 | if (fitx == 0) and (fity == 0): 266 | scad_code = generate_openscad_code(gridx, gridy, 0, 0, fitx, fity) 267 | elif (fitx == -1 and fity == 0) or (fitx == 1 and fity == 0): 268 | scad_code = generate_openscad_code(gridx, gridy, padding_x, 0, fitx, fity) 269 | elif (fitx == 0 and fity == -1) or (fitx == 0 and fity == 1): 270 | scad_code = generate_openscad_code(gridx, gridy, 0, padding_y, fitx, fity) 271 | else: 272 | scad_code = generate_openscad_code(gridx, gridy, padding_x, padding_y, fitx, fity) 273 | 274 | scad_dict[f"OpenSCAD_Code_{size.replace(' ', '_')}.scad"] = scad_code 275 | 276 | # Download button 277 | buffer = io.BytesIO() 278 | buffer.write(scad_code.encode()) 279 | buffer.seek(0) 280 | 281 | st.download_button( 282 | label=f"Download OpenSCAD Code for {size}", 283 | data=buffer, 284 | file_name=f"OpenSCAD_Code_{size.replace(' ', '_')}.scad", 285 | mime="text/plain" 286 | ) 287 | 288 | zip_data = create_zip_from_scad_dict(scad_dict) 289 | st.download_button( 290 | label="Download all SCADs with padding as ZIP file", 291 | data=zip_data, 292 | file_name="allScads.zip", 293 | mime="application/zip" 294 | ) 295 | 296 | else: 297 | # Summarize the plates without padding 298 | bill_of_materials = summarize_bom(layout) 299 | st.write("Bill of Materials:") 300 | 301 | for size, quantity in bill_of_materials.items(): 302 | st.write(f"{quantity} x {size}") 303 | size_part = size.split(' ')[0] 304 | gridx, gridy = map(int, size_part.split('x')) 305 | 306 | # Download button 307 | scad_code = generate_openscad_code(gridx, gridy) 308 | scad_dict[f"OpenSCAD_Code_{size.replace(' ', '_')}.scad"] = scad_code 309 | buffer = io.BytesIO() 310 | buffer.write(scad_code.encode()) 311 | buffer.seek(0) 312 | 313 | st.download_button( 314 | label=f"Download OpenSCAD Code for {size}", 315 | data=buffer, 316 | file_name=f"OpenSCAD_Code_{size.replace(' ', '_')}.scad", 317 | mime="text/plain" 318 | ) 319 | 320 | zip_data = create_zip_from_scad_dict(scad_dict) 321 | st.download_button( 322 | label="Download all SCADs with no padding as ZIP file", 323 | data=zip_data, 324 | file_name="allScads.zip", 325 | mime="application/zip" 326 | ) 327 | 328 | # Plotting section 329 | fig, ax = plt.subplots() 330 | 331 | # Plot the leftover space in grey 332 | ax.add_patch(plt.Rectangle((0, 0), space_x_mm, space_y_mm, edgecolor='black', facecolor='lightgrey', lw=2)) 333 | 334 | # Plot the layout on top of the grey background 335 | ax.imshow(layout, cmap='tab20', origin='lower', extent=[0, total_units_x * 42, 0, total_units_y * 42], zorder=2) 336 | 337 | # Manually draw the gridlines on top of everything 338 | for y in np.arange(0, total_units_y * 42 + 42, 42): 339 | ax.hlines(y, 0, total_units_x * 42, color='white', linewidth=1.5, zorder=4) 340 | for x in np.arange(0, total_units_x * 42 + 42, 42): 341 | ax.vlines(x, 0, total_units_y * 42, color='white', linewidth=1.5, zorder=4) 342 | 343 | ax.set_xlim(-leftover_x / 2 if padding_option == "Center Justify" else 0, 344 | total_units_x * 42 + leftover_x / 2 if padding_option == "Center Justify" else total_units_x * 42 + leftover_x) 345 | ax.set_ylim(-leftover_y / 2 if padding_option == "Center Justify" else 0, 346 | total_units_y * 42 + leftover_y / 2 if padding_option == "Center Justify" else total_units_y * 42 + leftover_y) 347 | ax.set_aspect('equal', adjustable='box') 348 | 349 | st.pyplot(fig) 350 | 351 | 352 | if __name__ == "__main__": 353 | main() 354 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit 2 | numpy 3 | matplotlib 4 | jinja2 --------------------------------------------------------------------------------