Skip to content

New Generator: Apothecary Drawer Box #786

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions boxes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,10 @@ def __init__(self) -> None:
defaultgroup.add_argument(
"--labels", action="store", type=boolarg, default=True,
help="label the parts (where available)")
defaultgroup.add_argument(
"--label_format", action="store", type=str, default="label",
choices=["label", "measurements", "label (measurements)"],
help="information to include as label on parts")
defaultgroup.add_argument(
"--reference", action="store", type=float, default=100.0,
help="print reference rectangle with given length (in mm)(zero to disable) [\U0001F6C8](https://florianfesti.github.io/boxes/html/usermanual.html#reference)")
Expand Down Expand Up @@ -444,7 +448,7 @@ def open(self):
else:
self.text(f"{self.reference:.1f}mm, burn:{self.burn:.2f}mm", self.reference / 2.0, 5,
fontsize=6, align="middle center", color=Color.ANNOTATIONS)
self.move(self.reference, 10, "up")
self.move(self.reference, 10, "up", label=False)
if self.qr_code:
self.renderQrCode()
self.ctx.stroke()
Expand Down Expand Up @@ -718,6 +722,25 @@ def adjustSize(self, l, e1=True, e2=True):
except TypeError:
return l - walls

def formatLabel(self, label: str | bool, width, height):
# Allows passing False to skip labeling
if label == False:
return

label_format = getattr(self, "label_format", "label")
measurements = f"({width:.2f}mm x {height:.2f}mm)"

if label_format == "label":
return label
elif label_format == "measurements":
return measurements
elif label_format == "label (measurements)":
if not label:
return measurements
return f"{label}\n{measurements}"
else:
raise ValueError("Unknown label format", label_format)

def render(self):
"""Implement this method in your subclass.

Expand Down Expand Up @@ -1218,6 +1241,7 @@ def move(self, x, y, where, before=False, label=""):
terms = where.split()
dontdraw = before and "only" in terms

width, height = x, y
x += self.spacing
y += self.spacing

Expand All @@ -1237,8 +1261,10 @@ def move(self, x, y, where, before=False, label=""):
if not before:
# restore position
self.ctx.restore()
if self.labels and label:
self.text(label, x/2, y/2, align="middle center", color=Color.ANNOTATIONS, fontsize=4)
if self.labels:
contents = self.formatLabel(label, width, height)
if contents:
self.text(contents, x/2, y/2, align="middle center", color=Color.ANNOTATIONS, fontsize=4)
self.ctx.stroke()

for term in terms:
Expand Down
14 changes: 11 additions & 3 deletions boxes/edges.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import math
import re
from abc import ABC, abstractmethod
from typing import Any
from typing import Any, Literal

from boxes import gears

Expand Down Expand Up @@ -872,6 +872,8 @@ class FingerJointSettings(Settings):

angle = 90 # Angle of the walls meeting

alternating: Literal['even', 'odd'] | None = None # even or odd for which alternating pattern to use

def checkValues(self) -> None:
if abs(self.space + self.finger) < 0.1:
raise ValueError("FingerJointSettings: space + finger must not be close to zero")
Expand Down Expand Up @@ -976,6 +978,7 @@ def __call__(self, length, bedBolts=None, bedBoltSettings=None, **kw):
play = self.settings.play

fingers, leftover = self.calcFingers(length, bedBolts)
altMod = 1 if self.settings.alternating == "odd" and fingers % 2 == 1 else 0

# not enough space for normal fingers - use small rectangular one
if (fingers == 0 and f and
Expand Down Expand Up @@ -1007,8 +1010,13 @@ def __call__(self, length, bedBolts=None, bedBoltSettings=None, **kw):
self.bedBoltHole(s, bedBoltSettings)
else:
self.edge(s)
self.draw_finger(f, h, style,
positive, i < fingers // 2)

# Skip finger if alternating
if self.settings.alternating and i % 2 == altMod:
self.edge(f)
else:
self.draw_finger(f, h, style,
positive, i < fingers // 2)

self.edge(leftover / 2.0, tabs=1)

Expand Down
251 changes: 251 additions & 0 deletions boxes/generators/apothecarydrawerbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
from boxes import *
from boxes.edges import FingerJointEdge
import copy

# Dependent generators for drawers
from boxes.generators.abox import ABox
from boxes.generators.dividertray import DividerTray

class DrawerSettings(edges.Settings):
"""Settings for the Drawers
Values:
* absolute

* style : "ABox" : generator to use for drawers
* notched : False : notches in drawer (DividerTray only)
* bottom_edge : "F" : bottom edge for drawer (ABox only)

* relative (in multiples of thickness)

* depth_reduction : 0.0 : drawer depth reduction for adding stop block inside
* tolerance : 0.5 : tolerance for drawer fit
* num_dividers : 5 : number of dividers in each drawer (DividerTray only)
"""
absolute_params = {
"style": ("none", "ABox", "DividerTray"),
"notched": False,
"bottom_edge": ("F", "e"),
}

relative_params = {
"depth_reduction": 0.0,
"tolerance": 0.5,
"num_dividers": 5,
}

class ApothecaryDrawerBox(Boxes):
"""Apothecary style sliding drawer box"""

ui_group = "Box"
description = """## Apothecary style sliding drawer box
Apothecary style sliding drawer box that uses alternating finger joints
for inner shelves to save on materials.

It leverages existing generators to create drawers, or you can
generate your own to suit your specific use case.

Default settings fit in a Kallax cube giving you 12 drawers. These drawers
can each fit most popular TCG cards.
"""

def __init__(self) -> None:
Boxes.__init__(self)

self.addSettingsArgs(edges.FingerJointSettings, finger=2.0, space=2.0)

self.addSettingsArgs(DrawerSettings, style="none", notched=False,
depth_reduction=0.0, tolerance=0.5)

self.buildArgParser(x=334, y=374, h=334, outside=True)

self.argparser.add_argument(
"--rows", action="store", type=int, default=3,
help="number of rows")

self.argparser.add_argument(
"--cols", action="store", type=int, default=4,
help="number of columns")

self.argparser.add_argument(
"--generate_drawers", action="store", type=BoolArg(), default=True,
help="generate drawers using DividerTray")

def render(self):
x, y, h = self.x, self.y, self.h
rows = self.rows
cols = self.cols
t = self.thickness

if self.outside:
self.x = x = self.adjustSize(x, "f", "f")
self.y = y = self.adjustSize(y, "e")
self.h = h = self.adjustSize(h, "f", "f")

self.unit_w, self.unit_h = unit_w, unit_h = self.unit_dimensions()

drawer_settings = self.edgesettings['Drawer']
drawer_style = drawer_settings['style']

altFingerSettings = copy.copy(self.edges["f"].settings)
# Even joint edge
altEven = FingerJointEdge(self, altFingerSettings)
altEven.settings.alternating = "even"
altEven.char = "a"
self.addPart(altEven)
# Odd joint edge
altOdd = FingerJointEdge(self, altFingerSettings)
altOdd.settings.alternating = "odd"
altOdd.char = "b"
self.addPart(altOdd)

# Back panel
self.rectangularWall(x, h, "ffff", label="back", callback=[self.back_panel_slot_holes_callback], move="up")

# Top/Bottom
with self.saved_context():
for i in range(2):
self.rectangularWall(x, y, "eFFF", label="top/bottom", callback=[self.vertical_slot_holes_callback], move="right")

self.rectangularWall(x, y, "ffff", move="up only")

# Left/Right walls
with self.saved_context():
for i in range(2):
self.rectangularWall(y, h, "fFfe", label="left/right", callback=[self.horizontal_slot_holes_callback], move="right")

self.rectangularWall(x, h, "ffff", move="up only")

# Inner walls
num_inner_walls = cols - 1
with self.saved_context():
for i in range(num_inner_walls):
self.rectangularWall(y, h, "fffe", label="divider", callback=[self.horizontal_slot_holes_callback], move="right")

self.rectangularWall(x, h, "ffff", move="up only")

# Shelves
num_edge_shelves = 0 if cols < 2 else rows -1
num_inner_shelves = (rows - 1) * (cols - 2)
num_single_col_shelves = 0 if cols > 1 else rows - 1
shelf_width = unit_w + (self.thickness * 2)

with self.saved_context():
for i in range(num_edge_shelves):
self.rectangularWall(unit_w, y, "ebff", label="left shelf", move="right")
self.rectangularWall(unit_w, y, "effa", label="right shelf", move="right")

for i in range(num_inner_shelves):
self.rectangularWall(unit_w, y, "ebfa", label="inner shelf", move="right")

for i in range(num_single_col_shelves):
self.rectangularWall(unit_w, y, "efff", label="shelf", move="right")

self.rectangularWall(unit_w, y, "efff", move="up only")

# Drawers
if drawer_style != "none":
num_drawers = rows * cols

if self.labels:
self.text(f"{num_drawers} sets of generated drawers using {drawer_style} generator --^", fontsize=6, color=Color.ANNOTATIONS)
self.moveTo(0, 10)

# Use existing generators to create drawers
for i in range(num_drawers):
with self.saved_context():
drawer_gen, render_width = self.drawerGenerator(drawer_style, drawer_settings, labels=self.labels, outside=self.outside)
drawer_gen._buildObjects()
drawer_gen.render()

# Reset positioning for rendering next iteration
self.moveTo(render_width + 5, 0)

def back_panel_slot_holes_callback(self):
self.vertical_slot_holes_callback(self.h)

for col in range(self.cols):
posx = col * (self.unit_w + self.thickness + (self.burn * 2))
for row in range(self.rows - 1):
posy = ((row + 1) * self.unit_h) + (self.thickness / 2) + (self.thickness * row)
self.fingerHolesAt(posx, posy, self.unit_w, angle=0)

def vertical_slot_holes_callback(self, length=None):
length = length or self.y
for col in range(1, self.cols):
posx = 0.5 * self.thickness + col * self.unit_w + (col - 1) * self.thickness
self.fingerHolesAt(posx, 0, length, angle=90)

def horizontal_slot_holes_callback(self):
for row in range(1, self.rows):
posy = 0.5 * self.thickness + row * self.unit_h + (row - 1) * self.thickness
self.fingerHolesAt(0, posy, self.y, angle=0)

def unit_dimensions(self):
total_inner_width = self.x - (self.thickness * (self.cols - 1))
total_inner_height = self.h - (self.thickness * (self.rows - 1))
unit_w = total_inner_width / self.cols
unit_h = total_inner_height / self.rows
return unit_w, unit_h

def drawerGenerator(self, drawer_style, drawer_settings, labels, outside):
"""Return a generator object based on the style name"""

drawer_width = self.unit_w
drawer_height = self.unit_h
drawer_depth = self.y

if drawer_settings['tolerance'] > 0:
drawer_width -= drawer_settings['tolerance']
drawer_height -= drawer_settings['tolerance']
drawer_depth -= drawer_settings['tolerance']

if drawer_settings['depth_reduction'] > 0:
drawer_depth -= drawer_settings['depth_reduction']

if drawer_style == "ABox":
bottom_edge = drawer_settings['bottom_edge'] or "F"
args = [
f"--x={drawer_width}",
f"--y={drawer_depth}",
f"--h={drawer_height}",
f"--bottom_edge={bottom_edge}",
]

gen = ABox()
gen.parseArgs(args)
render_width = drawer_width + drawer_depth

elif drawer_style == "DividerTray":
num_dividers = drawer_settings['num_dividers'] + 1
div_depth = drawer_depth / num_dividers

args = [
f"--sx={drawer_width}*1",
f"--sy={div_depth}*{num_dividers}",
f"--h={drawer_height}",
f"--bottom=True"
]

if drawer_settings['notched'] == False:
args.append(f"--Notch_depth=0")
args.append(f"--Notch_lower_radius=0")
args.append(f"--Notch_upper_radius=0")

gen = DividerTray()
gen.parseArgs(args)
render_width = max(drawer_width, drawer_depth) + drawer_width

else:
raise ValueError(f"Invalid generator: {drawer_style}")

gen.thickness = self.thickness
gen.ctx = self.ctx
gen.outside = outside
gen.labels = labels
gen.label_format = self.label_format
gen.spacing = self.spacing
gen.edgesettings = self.edgesettings
gen.bedBoltSettings = self.bedBoltSettings
gen.hexHolesSettings = self.hexHolesSettings

return gen, render_width
Loading