-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathgridfinity_calculator.py
More file actions
executable file
·382 lines (305 loc) · 15.4 KB
/
gridfinity_calculator.py
File metadata and controls
executable file
·382 lines (305 loc) · 15.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
import io
import zipfile
import matplotlib.pyplot as plt
import numpy as np
import streamlit as st
from jinja2 import Template
UNITS = ["Millimeters", "Inches"]
# Function to convert inches to millimeters if needed
def convert_to_mm(value, units):
if units == "Inches":
return value * 25.4
return value
# Split total units into printable chunks, merging a 1-unit remainder into the previous chunk
def compute_splits(total, max_chunk):
chunks = []
remaining = total
while remaining > 0:
chunk = min(max_chunk, remaining)
chunks.append(chunk)
remaining -= chunk
if len(chunks) > 1 and chunks[-1] == 1 and chunks[-2] >= 3:
chunks[-2] -= 1
chunks[-1] = 2
return chunks
def build_plate_matrix(total_units_x, total_units_y, max_units_x, max_units_y):
plate_matrix = np.zeros((total_units_y, total_units_x), dtype=int)
plate_counter = 1
x_splits = compute_splits(total_units_x, max_units_x)
y_splits = compute_splits(total_units_y, max_units_y)
y_pos = 0
for plate_y in y_splits:
x_pos = 0
for plate_x in x_splits:
plate_matrix[y_pos:y_pos + plate_y, x_pos:x_pos + plate_x] = plate_counter
plate_counter += 1
x_pos += plate_x
y_pos += plate_y
return plate_matrix, plate_counter - 1
# Reduce max_units so that every plate (including its padding) fits in the printer.
# The old approach checked max_units * 42 + leftover, but the rightmost/topmost plate
# is often smaller than max_units — compute_splits gives the actual plate sizes.
def adjust_max_units_for_padding(total_units_x, total_units_y, max_units_x, max_units_y,
leftover_x, leftover_y, printer_x_mm, printer_y_mm, padding_option):
while True:
x_splits = compute_splits(total_units_x, max_units_x)
y_splits = compute_splits(total_units_y, max_units_y)
if padding_option == "Corner Justify":
x_ok = x_splits[-1] * 42 + leftover_x <= printer_x_mm
y_ok = y_splits[-1] * 42 + leftover_y <= printer_y_mm
else: # Center Justify: largest plate + half padding on each edge
x_ok = x_splits[0] * 42 + leftover_x / 2 <= printer_x_mm
y_ok = y_splits[0] * 42 + leftover_y / 2 <= printer_y_mm
if x_ok and y_ok:
return max_units_x, max_units_y
if not x_ok:
max_units_x -= 1
if not y_ok:
max_units_y -= 1
def determine_padding(plate_matrix, leftover_x, leftover_y, padding_option):
y, x = plate_matrix.shape
unique_plates = np.unique(plate_matrix)
bill_of_materials_with_padding = {}
for plate in unique_plates:
if plate == 0:
continue
# Find the bounding box of this plate
rows, cols = np.where(plate_matrix == plate)
min_row, max_row = rows.min(), rows.max()
min_col, max_col = cols.min(), cols.max()
plate_x = max_col - min_col + 1
plate_y = max_row - min_row + 1
padding_info = []
fitx, fity = 0, 0
if padding_option == "Corner Justify":
if max_col == x - 1 and leftover_x > 0: # Rightmost plate
padding_info.append(f"{round(leftover_x, 1)}mm Right")
fitx = 1
if max_row == y - 1 and leftover_y > 0: # Topmost plate
padding_info.append(f"{round(leftover_y, 1)}mm Top")
fity = 1
elif padding_option == "Center Justify":
if min_col == 0 and leftover_x > 0: # Leftmost plate
padding_info.append(f"{round(leftover_x / 2, 1)}mm Left")
fitx = -1
if max_col == x - 1 and leftover_x > 0: # Rightmost plate
padding_info.append(f"{round(leftover_x / 2, 1)}mm Right")
fitx = 1
if min_row == 0 and leftover_y > 0: # Bottommost plate
padding_info.append(f"{round(leftover_y / 2, 1)}mm Bottom")
fity = -1
if max_row == y - 1 and leftover_y > 0: # Topmost plate
padding_info.append(f"{round(leftover_y / 2, 1)}mm Top")
fity = 1
plate_key = f"{plate_x}x{plate_y}"
if padding_info:
plate_key += f" ({', '.join(padding_info)})"
if plate_key in bill_of_materials_with_padding:
bill_of_materials_with_padding[plate_key] += 1
else:
bill_of_materials_with_padding[plate_key] = 1
return bill_of_materials_with_padding
def calculate_baseplates(printer_x, printer_y, space_x, space_y, grid_size=42):
total_units_x = int(space_x // grid_size)
total_units_y = int(space_y // grid_size)
max_units_x = int(printer_x // grid_size)
max_units_y = int(printer_y // grid_size)
layout = np.zeros((total_units_y, total_units_x), dtype=int)
plate_matrix, _ = build_plate_matrix(total_units_x, total_units_y, max_units_x, max_units_y)
leftover_x = space_x - total_units_x * grid_size
leftover_y = space_y - total_units_y * grid_size
return plate_matrix, leftover_x, leftover_y, total_units_x, total_units_y, max_units_x, max_units_y
def summarize_bom(plate_matrix):
y, x = plate_matrix.shape
unique_plates = np.unique(plate_matrix)
bill_of_materials = {}
for plate in unique_plates:
if plate == 0:
continue
rows, cols = np.where(plate_matrix == plate)
min_row, max_row = rows.min(), rows.max()
min_col, max_col = cols.min(), cols.max()
plate_x = max_col - min_col + 1
plate_y = max_row - min_row + 1
plate_key = f"{plate_x}x{plate_y}"
if plate_key in bill_of_materials:
bill_of_materials[plate_key] += 1
else:
bill_of_materials[plate_key] = 1
return bill_of_materials
def generate_openscad_code(gridx: int, gridy: int, padding_x: int = 0, padding_y: int = 0, fitx: int = 0,
fity: int = 0):
with open("baseplate.scad.j2", "r") as f:
scad_template = Template(str(f.read()))
return scad_template.render(grid_x=gridx,
grid_y=gridy,
padding_x=padding_x,
padding_y=padding_y,
fit_x=fitx,
fit_y=fity)
def create_zip_from_scad_dict(scads_dict):
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w") as zip_file:
for key, value in scads_dict.items():
zip_file.writestr(key, value)
return zip_buffer.getvalue()
def main():
st.title("Gridfinity Baseplate Layout Calculator - Optimized to Avoid Any 1x Dimension Baseplates")
# Dropdowns for units selection
printer_units = st.selectbox("Select Printer Dimensions Units:", options=UNITS)
space_units = st.selectbox("Select Area Dimensions Units:", options=UNITS, )
# Inputs with updated labels
printer_x = st.number_input(f"Printer Max Build Size X ({printer_units}):",
value=227 if printer_units == "Millimeters" else 8.94)
printer_y = st.number_input(f"Printer Max Build Size Y ({printer_units}):",
value=255 if printer_units == "Millimeters" else 10.04)
space_x = st.number_input(f"Enter the space's X dimension you want to fill ({space_units}):",
value=1000 if space_units == "Millimeters" else 39.37)
space_y = st.number_input(f"Enter the space's Y dimension you want to fill ({space_units}):",
value=800 if space_units == "Millimeters" else 31.5)
# Convert to millimeters if needed
printer_x_mm = convert_to_mm(printer_x, printer_units)
printer_y_mm = convert_to_mm(printer_y, printer_units)
space_x_mm = convert_to_mm(space_x, space_units)
space_y_mm = convert_to_mm(space_y, space_units)
# Padding option dropdown
padding_option = st.selectbox("Select Padding Calculation Option:",
["Corner Justify", "Center Justify", "No Padding Calculation"])
if st.button("Calculate Layout"):
# Get the baseplates based on printer size and desired space dimensions.
layout, leftover_x, leftover_y, total_units_x, total_units_y, max_units_x, max_units_y = calculate_baseplates(
printer_x_mm, printer_y_mm, space_x_mm, space_y_mm)
if padding_option != "No Padding Calculation":
max_units_x, max_units_y = adjust_max_units_for_padding(
total_units_x, total_units_y, max_units_x, max_units_y,
leftover_x, leftover_y, printer_x_mm, printer_y_mm, padding_option)
layout, _ = build_plate_matrix(total_units_x, total_units_y, max_units_x, max_units_y)
# Store results in session state
st.session_state.layout = layout
st.session_state.leftover_x = leftover_x
st.session_state.leftover_y = leftover_y
st.session_state.total_units_x = total_units_x
st.session_state.total_units_y = total_units_y
# Display results
st.write(f"Total fill area Gridfinity units (X x Y): {total_units_x} x {total_units_y}")
st.write(f"Leftover X distance: {round(leftover_x, 1)} mm")
st.write(f"Leftover Y distance: {round(leftover_y, 1)} mm")
max_plate_size = f"{max_units_x}x{max_units_y} Gridfinity units"
st.write(f"Maximum plate size your printer can handle (including padding): {max_plate_size}")
if 'layout' in st.session_state:
layout = st.session_state.layout
leftover_x = st.session_state.leftover_x
leftover_y = st.session_state.leftover_y
total_units_x = st.session_state.total_units_x
total_units_y = st.session_state.total_units_y
scad_dict = dict()
if padding_option != "No Padding Calculation":
bill_of_materials_with_padding = determine_padding(layout, leftover_x, leftover_y, padding_option)
st.write("Bill of Materials with Padding:")
for size, quantity in bill_of_materials_with_padding.items():
st.write(f"{quantity} x {size}")
size_part = size.split(' ')[0]
gridx, gridy = map(int, size_part.split('x'))
# Extract padding based on size
if padding_option == "Corner Justify":
padding_x = leftover_x if 'Right' in size else 0
padding_y = leftover_y if 'Top' in size else 0
fitx, fity = 0, 0
if 'Left' in size:
fitx = -1
elif 'Right' in size:
fitx = 1
if 'Bottom' in size:
fity = -1
elif 'Top' in size:
fity = 1
elif padding_option == "Center Justify":
# Center Justify: split padding equally between both sides
padding_x = leftover_x / 2
padding_y = leftover_y / 2
fitx, fity = 0, 0
if 'Left' in size and 'Right' in size:
fitx = 0 # Center padding
elif 'Left' in size:
fitx = -1
elif 'Right' in size:
fitx = 1
# Adjust fity based on top/bottom padding
if 'Top' in size and 'Bottom' in size:
fity = 0 # Center padding
elif 'Bottom' in size:
fity = -1
elif 'Top' in size:
fity = 1
if (fitx == 0) and (fity == 0):
scad_code = generate_openscad_code(gridx, gridy, 0, 0, fitx, fity)
elif (fitx == -1 and fity == 0) or (fitx == 1 and fity == 0):
scad_code = generate_openscad_code(gridx, gridy, padding_x, 0, fitx, fity)
elif (fitx == 0 and fity == -1) or (fitx == 0 and fity == 1):
scad_code = generate_openscad_code(gridx, gridy, 0, padding_y, fitx, fity)
else:
scad_code = generate_openscad_code(gridx, gridy, padding_x, padding_y, fitx, fity)
scad_dict[f"OpenSCAD_Code_{size.replace(' ', '_')}.scad"] = scad_code
# Download button
buffer = io.BytesIO()
buffer.write(scad_code.encode())
buffer.seek(0)
st.download_button(
label=f"Download OpenSCAD Code for {size}",
data=buffer,
file_name=f"OpenSCAD_Code_{size.replace(' ', '_')}.scad",
mime="text/plain"
)
zip_data = create_zip_from_scad_dict(scad_dict)
st.download_button(
label="Download all SCADs with padding as ZIP file",
data=zip_data,
file_name="allScads.zip",
mime="application/zip"
)
else:
# Summarize the plates without padding
bill_of_materials = summarize_bom(layout)
st.write("Bill of Materials:")
for size, quantity in bill_of_materials.items():
st.write(f"{quantity} x {size}")
size_part = size.split(' ')[0]
gridx, gridy = map(int, size_part.split('x'))
# Download button
scad_code = generate_openscad_code(gridx, gridy)
scad_dict[f"OpenSCAD_Code_{size.replace(' ', '_')}.scad"] = scad_code
buffer = io.BytesIO()
buffer.write(scad_code.encode())
buffer.seek(0)
st.download_button(
label=f"Download OpenSCAD Code for {size}",
data=buffer,
file_name=f"OpenSCAD_Code_{size.replace(' ', '_')}.scad",
mime="text/plain"
)
zip_data = create_zip_from_scad_dict(scad_dict)
st.download_button(
label="Download all SCADs with no padding as ZIP file",
data=zip_data,
file_name="allScads.zip",
mime="application/zip"
)
# Plotting section
fig, ax = plt.subplots()
# Plot the leftover space in grey
ax.add_patch(plt.Rectangle((0, 0), space_x_mm, space_y_mm, edgecolor='black', facecolor='lightgrey', lw=2))
# Plot the layout on top of the grey background
ax.imshow(layout, cmap='tab20', origin='lower', extent=[0, total_units_x * 42, 0, total_units_y * 42], zorder=2)
# Manually draw the gridlines on top of everything
for y in np.arange(0, total_units_y * 42 + 42, 42):
ax.hlines(y, 0, total_units_x * 42, color='white', linewidth=1.5, zorder=4)
for x in np.arange(0, total_units_x * 42 + 42, 42):
ax.vlines(x, 0, total_units_y * 42, color='white', linewidth=1.5, zorder=4)
ax.set_xlim(-leftover_x / 2 if padding_option == "Center Justify" else 0,
total_units_x * 42 + leftover_x / 2 if padding_option == "Center Justify" else total_units_x * 42 + leftover_x)
ax.set_ylim(-leftover_y / 2 if padding_option == "Center Justify" else 0,
total_units_y * 42 + leftover_y / 2 if padding_option == "Center Justify" else total_units_y * 42 + leftover_y)
ax.set_aspect('equal', adjustable='box')
st.pyplot(fig)
if __name__ == "__main__":
main()