diff --git a/boxes/__init__.py b/boxes/__init__.py index 7ce138be..6b1c7928 100755 --- a/boxes/__init__.py +++ b/boxes/__init__.py @@ -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)") @@ -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() @@ -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. @@ -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 @@ -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: diff --git a/boxes/edges.py b/boxes/edges.py index f49339bf..b0cb335e 100644 --- a/boxes/edges.py +++ b/boxes/edges.py @@ -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 @@ -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") @@ -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 @@ -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) diff --git a/boxes/generators/apothecarydrawerbox.py b/boxes/generators/apothecarydrawerbox.py new file mode 100644 index 00000000..a3e8d58e --- /dev/null +++ b/boxes/generators/apothecarydrawerbox.py @@ -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 diff --git a/examples/ApothecaryDrawerBox.svg b/examples/ApothecaryDrawerBox.svg new file mode 100644 index 00000000..1b61843f --- /dev/null +++ b/examples/ApothecaryDrawerBox.svg @@ -0,0 +1,703 @@ + + + +ApothecaryDrawerBox + + +Box - ApothecaryDrawerBox +boxes ApothecaryDrawerBox +Apothecary style sliding drawer box + +## 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. + + +Created with Boxes.py (https://boxes.hackerspace-bamberg.de/) +Command line: boxes ApothecaryDrawerBox +Command line short: boxes ApothecaryDrawerBox + + + + + 100.0mm, burn:0.10mm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + back + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + top/bottom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + top/bottom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + left/right + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + left/right + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + divider + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + divider + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + divider + + + left shelf + + + right shelf + + + left shelf + + + right shelf + + + inner shelf + + + inner shelf + + + inner shelf + + + inner shelf + + \ No newline at end of file diff --git a/static/samples/ApothecaryDrawerBox-thumb.jpg b/static/samples/ApothecaryDrawerBox-thumb.jpg new file mode 100644 index 00000000..8a9d1d97 Binary files /dev/null and b/static/samples/ApothecaryDrawerBox-thumb.jpg differ diff --git a/static/samples/ApothecaryDrawerBox.jpg b/static/samples/ApothecaryDrawerBox.jpg new file mode 100644 index 00000000..bd422591 Binary files /dev/null and b/static/samples/ApothecaryDrawerBox.jpg differ diff --git a/static/samples/samples.sha256 b/static/samples/samples.sha256 index 2ba6b7ea..fc99c8c7 100644 --- a/static/samples/samples.sha256 +++ b/static/samples/samples.sha256 @@ -200,3 +200,4 @@ b8d653058d7b5cc8498077e9f56695a31eb02e3e35fc823c0686dc82e5cc7675 ../static/samp eb40df11a48da317c6f6c645c21af48f15614ab453305f97b822428ee4a76b22 ../static/samples/NightLightBox.jpg b6884e7cda3b1e03895f614ae65fcbc007bc87627aef42481979a28576503e82 ../static/samples/SlidingLidBox.jpg c47e25069acedf2a50ef4082ab22693193c023d70afc3c4aba153f6a9bac8a04 ../static/samples/SlidingLidBox-2.jpg +e502e9f64672364b2627b3f6b55d9e1dd428cee106a70f5356f797ed138df642 ../static/samples/ApothecaryDrawerBox.jpg