# Copyright (C) 2013-2014 Florian Festi
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import math
from functools import partial
from boxes import *
from boxes import lids
[docs]
class GridfinityBase(Boxes):
"""A parameterized Gridfinity base"""
description = """This is a configurable gridfinity base. This
design is based on
<a href="https://www.youtube.com/watch?app=desktop&v=ra_9zU-mnl8">Zach Freedman's Gridfinity system</a>"""
ui_group = "Tray"
def __init__(self) -> None:
Boxes.__init__(self)
self.addSettingsArgs(edges.DoveTailSettings, size=3, depth=.3, radius=.05, angle=40)
self.addSettingsArgs(edges.FingerJointSettings, space=4, finger=4)
self.addSettingsArgs(lids.LidSettings)
self.argparser.add_argument("--size_x", type=int, default=0, help="size of base in X direction (0=auto)")
self.argparser.add_argument("--size_y", type=int, default=0, help="size of base in Y direction (0=auto)")
self.argparser.add_argument("--x", type=int, default=3, help="number of grids in X direction (0=auto)")
self.argparser.add_argument("--y", type=int, default=2, help="number of grids in Y direction (0=auto)")
self.argparser.add_argument("--h", type=float, default=7*3, help="height of sidewalls of the tray (mm)")
self.argparser.add_argument("--m", type=float, default=0.5, help="Extra margin around the gridfinity base to allow it to drop into the carrier (mm)")
self.argparser.add_argument(
"--bottom_edge", action="store",
type=ArgparseEdgeType("Fhse"), choices=list("Fhse"),
default='F',
help="edge type for bottom edge")
self.argparser.add_argument(
"--panel_edge", action="store",
type=ArgparseEdgeType("De"), choices=list("De"),
default='D',
help="edge type for sub panels")
self.argparser.add_argument("--pitch", type=int, default=42, help="The Gridfinity pitch, in mm. Should always be 42.")
self.argparser.add_argument("--opening", type=int, default=38, help="The cutout for each grid opening. Typical is 38.")
self.argparser.add_argument("--radius", type=float, default=1.6, help="The corner radius for each grid opening. Typical is 1.6.")
self.argparser.add_argument("--cut_pads", type=boolarg, default=False, help="cut pads to be used for gridinity boxes from the grid openings")
self.argparser.add_argument("--cut_pads_mag_diameter", type=float, default=6.5, help="if pads are cut add holes for magnets. Typical is 6.5, zero to disable,")
self.argparser.add_argument("--cut_pads_mag_offset", type=float, default=7.75, help="if magnet hole offset from pitch corners. Typical is 7.75.")
self.argparser.add_argument("--pad_radius", type=float, default=0.8, help="The corner radius for each grid opening. Typical is 0.8,")
self.argparser.add_argument("--panel_x", type=int, default=0, help="the maximum sized panel that can be cut in x direction")
self.argparser.add_argument("--panel_y", type=int, default=0, help="the maximum sized panel that can be cut in y direction")
def generate_grid(self, nx, ny, shift_x=0, shift_y=0):
radius, pad_radius = self.radius, self.pad_radius
pitch = self.pitch
opening = self.opening
for col in range(nx):
for row in range(ny):
lx = col*pitch+pitch/2 + shift_x
ly = row*pitch+pitch/2 + shift_y
self.rectangularHole(lx, ly, opening, opening, r=radius)
if self.cut_pads:
self.rectangularHole(lx, ly, opening - 2, opening - 2, r=pad_radius)
if self.cut_pads_mag_diameter > 0:
# create a shorter variable names for use in the loop
ofs = self.cut_pads_mag_offset
dia = self.cut_pads_mag_diameter
for xoff, yoff in ((1,1), (-1,1), (1,-1), (-1,-1)):
x = lx+((pitch // 2)-ofs)*xoff
y = ly+((pitch // 2)-ofs)*yoff
self.hole(x, y, d=dia)
def subdivide_grid(self, X, Y, A, B):
# Calculate the number of subdivisions needed in each dimension
num_x = math.ceil(X / A)
num_y = math.ceil(Y / B)
# Compute balanced segment sizes
segment_widths = [X // num_x] * num_x
for i in range(X % num_x): # Distribute remainder
segment_widths[i] += 1
segment_heights = [Y // num_y] * num_y
for i in range(Y % num_y): # Distribute remainder
segment_heights[i] += 1
# Generate the subdivisions
grid_segments = {}
y_start = 0
row_index = 0 # Start from bottom row
for h in segment_heights:
x_start = 0
col_index = 0 # Start from left column
for w in segment_widths:
grid_segments[(col_index, row_index)] = (x_start, y_start, w, h)
x_start += w
col_index += 1
y_start += h
row_index += 1
return len(segment_widths), len(segment_heights), grid_segments
def render(self):
if self.x == 0 and self.size_x == 0:
raise ValueError('either --size_x or --x must be provided')
if self.y == 0 and self.size_y == 0:
raise ValueError('either --size_y or --y must be provided')
if self.size_x == 0:
# if we are producing a minimally sized base size_x will be zero
self.size_x = self.x*self.pitch
else:
if self.x == 0:
# if we are producing an automatically determined maximum
# number of grid cols self.x will be zero
self.x = int(self.size_x / self.pitch)
# if both size_x and x were provided, x takes precedence
self.size_x = max(self.size_x, self.x*self.pitch)
if self.size_y == 0:
# if we are producing a minimally sized base size_y will be zero
self.size_y = self.y*self.pitch
else:
if self.y == 0:
# if we are producing an automatically determined maximum
# number of grid rows self.y will be zero
self.y = int(self.size_y / self.pitch)
# if both size_y and y were provided, y takes precedence
self.size_y = max(self.size_y, self.y*self.pitch)
if self.panel_x != 0 and self.panel_y != 0:
self.render_split(self.size_x, self.size_y, self.h, self.x, self.y, self.pitch, self.m)
else:
self.render_unsplit(self.size_x, self.size_y, self.h, self.x, self.y, self.pitch, self.m)
def render_split(self, x, y, h, nx, ny, pitch, margin):
"""
x : base width in mm
y : base height in mm
h : box wall height
nx : number of gridfinity holes in x axis
ny : number of gridfinity holes in y axis
pitch : space between gridfinity holes
"""
pad_x = x - (nx * pitch)
pad_y = y - (ny * pitch)
# compute maximum number of grids in each panel
panel_nx = ((self.panel_x - pad_x) // pitch)
panel_ny = ((self.panel_y -pad_y) // pitch)
# Sub-divide the larger Grid into approximately equal sized segments
# in both X and Y direction, not exceeding the provided panel size
segments_cols, segments_rows, segments = self.subdivide_grid(nx, ny, panel_nx, panel_ny)
# Render the primary grid
for row in range(segments_rows):
t0 = "e" if row == 0 else ("d" if self.panel_edge != "e" else "e")
t2 = "e" if row == segments_rows - 1 else ("D" if self.panel_edge != "e" else "e")
segment_pad_bottom, segment_pad_top = 0, 0
if (row == 0):
segment_pad_bottom = pad_y // 2
if (row == segments_rows - 1):
segment_pad_top = pad_y // 2
with self.saved_context():
for col in range(segments_cols):
nx, ny = segments[(col, row)][2:4]
t1 = "e" if col == segments_cols - 1 else ("d" if self.panel_edge != "e" else "e")
t3 = "e" if col == 0 else ("D" if self.panel_edge != "e" else "e")
segment_pad_left, segment_pad_right = 0, 0
if (col == 0):
segment_pad_left = pad_x // 2
if (col == segments_cols - 1):
segment_pad_right = pad_x // 2
box_width = nx * self.pitch + segment_pad_left + segment_pad_right
box_height = ny * self.pitch + segment_pad_bottom + segment_pad_top
self.rectangularWall(
box_width,
box_height,
[t0, t1, t2, t3],
callback=[
partial(
self.generate_grid,
nx, ny,
segment_pad_left,
segment_pad_bottom
)
]
)
self.rectangularWall(
box_width,
box_height,
[t0, t1, t2, t3],
move="right only",
label=str((row, col))
)
self.rectangularWall(
box_width,
box_height,
[t0, t1, t2, t3],
move="up only",
label=str((row, col))
)
# If requested, render the walls and floor
if h > 0:
# Render the floor, if the wall edge is not a plain edge
if self.bottom_edge != "e":
# TODO - figure out how to make the dovetail different for the bottom panel
for row in range(segments_rows):
t0 = "f" if row == 0 else "d"
t2 = "f" if row == segments_rows - 1 else "D"
segment_pad_bottom, segment_pad_top = 0, 0
if (row == 0):
segment_pad_bottom = pad_y // 2
if (row == segments_rows - 1):
segment_pad_top = pad_y // 2
with self.saved_context():
for col in range(segments_cols):
nx, ny = segments[(col, row)][2:4]
t1 = "f" if col == segments_cols - 1 else "d"
t3 = "f" if col == 0 else "D"
segment_pad_left, segment_pad_right = 0, 0
if (col == 0):
segment_pad_left = pad_x // 2
m = margin
if (col == segments_cols - 1):
segment_pad_right = pad_x // 2
m = margin
box_width = nx * pitch + segment_pad_left + segment_pad_right + m
box_height = ny * pitch + segment_pad_bottom + segment_pad_top + m
self.rectangularWall(
box_width,
box_height,
[t0, t1, t2, t3],
)
self.rectangularWall(
box_width,
box_height,
[t0, t1, t2, t3],
move="right only",
label=str((row, col))
)
self.rectangularWall(
box_width,
box_height,
[t0, t1, t2, t3],
move="up only",
label=str((row, col))
)
# Render walls
t1, t2, t3, t4 = "eeee"
b = self.edges.get(self.bottom_edge, self.edges["F"])
sideedge = "F" # if self.vertical_edges == "finger joints" else "h"
# Render walls in x direction
for ii in range(2):
resets = []
for col in range(segments_cols):
nx, ny = segments[(col, 0)][2:4]
segment_pad_left, segment_pad_right = 0, 0
if (col == 0):
segment_pad_left = pad_x // 2
if (col == segments_cols - 1):
segment_pad_right = pad_x // 2
box_width = nx * self.pitch + segment_pad_left + segment_pad_right
if (col == 0):
ee = [b, "f", "e", "f"]
m = margin
elif (col == (segments_cols-1)):
ee = [b, "f", "e", "F"]
m = margin
else:
ee = [b, "f", "e", "F"]
m = 0
self.rectangularWall(box_width+m, h, ee,
ignore_widths=[1, 6], move="right")
resets.append((box_width+m, ee))
for val, ee in resets:
self.rectangularWall(val, 0, ee,
ignore_widths=[1, 6], move="left only")
self.rectangularWall(x, h, ee,
ignore_widths=[1, 6], move="up only")
# Render walls in y direction
for ii in range(2):
resets = []
for row in range(segments_rows):
nx, ny = segments[(0, row)][2:4]
segment_pad_bottom, segment_pad_top = 0, 0
if (row == 0):
segment_pad_bottom = pad_y // 2
if (row == segments_rows - 1):
segment_pad_top = pad_y // 2
box_height = ny * pitch + segment_pad_bottom + segment_pad_top
if (row == 0):
ee = [b, "f", "e", "F"]
m = margin
elif (row == (segments_rows-1)):
ee = [b, "F", "e", "F"]
m = margin
else:
ee = [b, "f", "e", "F"]
m = 0
self.rectangularWall(box_height+m, h, ee,
ignore_widths=[1, 6], move="right")
resets.append((box_height+m, ee))
for val, ee in resets:
self.rectangularWall(val, 0, ee,
ignore_widths=[1, 6], move="left only")
self.rectangularWall(y, h, ee,
ignore_widths=[1, 6], move="up only")
#self.moveTo(-y-h-self.thickness, -h-self.thickness*2)
# TODO - Lid not supported in split mode
# self.lid(x, y)
def render_unsplit(self, x, y, h, nx, ny, pitch, margin):
"""
x : base width in mm
y : base height in mm
h : box wall height
nx : number of gridfinity holes in x axis
ny : number of gridfinity holes in y axis
pitch : space between gridfinity holes
"""
t1, t2, t3, t4 = "eeee"
b = self.edges.get(self.bottom_edge, self.edges["F"])
sideedge = "F" # if self.vertical_edges == "finger joints" else "h"
shift_x = (x - (nx * pitch)) // 2
shift_y = (y - (ny * pitch)) // 2
self.rectangularWall(
x,
y,
move="up",
callback=[partial(self.generate_grid, nx, ny, shift_x, shift_y)]
)
# add margin for walls and lid
x += 2 * margin
y += 2 * margin
if h > 0:
self.rectangularWall(x, h, [b, sideedge, t1, sideedge],
ignore_widths=[1, 6], move="right")
self.rectangularWall(y, h, [b, "f", t2, "f"],
ignore_widths=[1, 6], move="up")
self.rectangularWall(y, h, [b, "f", t4, "f"],
ignore_widths=[1, 6], move="")
self.rectangularWall(x, h, [b, sideedge, t3, sideedge],
ignore_widths=[1, 6], move="left up")
if self.bottom_edge != "e":
self.rectangularWall(x, y, "ffff", move="up")
self.lid(x, y)