From 753422fa46f35c57778855575e59e5ea603df085 Mon Sep 17 00:00:00 2001 From: chrysn Date: Fri, 5 Apr 2019 20:59:44 +0200 Subject: [PATCH] New generator DiscRack A rack for storing disk-shaped objects vertically next to each other --- boxes/generators/discrack.py | 274 +++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 boxes/generators/discrack.py diff --git a/boxes/generators/discrack.py b/boxes/generators/discrack.py new file mode 100644 index 0000000..fc5e76a --- /dev/null +++ b/boxes/generators/discrack.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +# coding: utf-8 +# Copyright (C) 2019 chrysn +# +# 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 . + +from __future__ import division, unicode_literals + +from boxes import * +from math import sqrt, pi, sin, cos + +def offset_radius_in_square(squareside, angle, outset): + """From the centre of a square, rotate by an angle relative to the + vertical, move away from the center (down if angle = 0), and then in a + right angle until the border of the square. Return the length of that last + segment. + + Note that for consistency with other boxes.py methods, angle is given in + degree. + + >>> # Without rotation, it's always half the square length + >>> offset_radius_in_square(20, 0, 0) + 10.0 + >>> offset_radius_in_square(20, 0, 5) + 10.0 + >>> # Without offset, it's half squre length divided by cos(angle) -- at + >>> # least before it hits the next wall + >>> offset_radius_in_square(20, 15, 0) # doctest:+ELLIPSIS + 10.35276... + >>> offset_radius_in_square(20, 45, 0) # doctest:+ELLIPSIS + 14.1421... + >>> # Positive angles make the segment initially shorter... + >>> offset_radius_in_square(20, 5, 10) < 10 + True + >>> # ... while negative angles make it longer. + >>> offset_radius_in_square(20, -5, 10) > 10 + True + """ + + if angle <= -90: + return offset_radius_in_square(squareside, angle + 180, outset) + if angle > 90: + return offset_radius_in_square(squareside, angle - 180, outset) + + angle = angle / 180 * pi + + step_right = outset * sin(angle) + step_down = outset * cos(angle) + + try: + len_right = (squareside / 2 - step_right) / cos(angle) + except ZeroDivisionError: + return squareside / 2 + + if angle == 0: + return len_right + if angle > 0: + len_up = (squareside / 2 + step_down) / sin(angle) + + return min(len_up, len_right) + else: # angle < 0 + len_down = - (squareside / 2 - step_down) / sin(angle) + + return min(len_down, len_right) + +class DiscRack(Boxes): + """A rack for storing disk-shaped objects vertically next to each other""" + + ui_group = "Shelf" + + def __init__(self): + Boxes.__init__(self) + + self.buildArgParser(sx="20*10") + self.argparser.add_argument( + "--disc_diameter", action="store", type=float, default=150.0, + help="Disc diameter in mm") + self.argparser.add_argument( + "--disc_thickness", action="store", type=float, default=5.0, + help="Thickness of the discs in mm") + + self.argparser.add_argument( + "--lower_factor", action="store", type=float, default=0.75, + help="Position of the lower rack grids along the radius") + self.argparser.add_argument( + "--rear_factor", action="store", type=float, default=0.75, + help="Position of the rear rack grids along the radius") + + self.argparser.add_argument( + "--disc_outset", action="store", type=float, default=3.0, + help="Additional space kept between the disks and the outbox of the rack") + + # These can be parameterized, but the default value of pulling them up + # to the box front is good enough for so many cases it'd only clutter + # the user interface. + # + # The parameters can be resurfaced when there is something like rare or + # advanced settings. + ''' + self.argparser.add_argument( + "--lower_outset", action="store", type=float, default=0.0, + help="Space in front of the disk slits (0: automatic)") + self.argparser.add_argument( + "--rear_outset", action="store", type=float, default=0.0, + help="Space above the disk slits (0: automatic)") + ''' + + self.argparser.add_argument( + "--angle", action="store", type=float, default=18, + help="Backwards slant of the rack") + self.addSettingsArgs(edges.FingerJointSettings) + + def parseArgs(self, *args, **kwargs): + Boxes.parseArgs(self, *args, **kwargs) + self.lower_outset = self.rear_outset = 0 + + self.calculate() + + def calculate(self): + self.outer = self.disc_diameter + 2 * self.disc_outset + + r = self.disc_diameter / 2 + + # distance between radius line and front (or rear) end of the slit + self.lower_halfslit = r * sqrt(1 - self.lower_factor**2) + self.rear_halfslit = r * sqrt(1 - self.rear_factor**2) + + if True: # self.lower_outset == 0: # when lower_outset parameter is re-enabled + toplim = offset_radius_in_square(self.outer, self.angle, r * self.lower_factor) + # With typical positive angles, the lower surface of board will be limiting + bottomlim = offset_radius_in_square(self.outer, self.angle, r * self.lower_factor + self.thickness) + self.lower_outset = min(toplim, bottomlim) - self.lower_halfslit + + if True: # self.rear_outset == 0: # when rear_outset parameter is re-enabled + # With typical positive angles, the upper surface of board will be limiting + toplim = offset_radius_in_square(self.outer, -self.angle, r * self.rear_factor) + bottomlim = offset_radius_in_square(self.outer, -self.angle, r * self.rear_factor + self.thickness) + self.rear_outset = min(toplim, bottomlim) - self.rear_halfslit + + # front outset, space to radius, space to rear part, plus nothing as fingers extend out + self.lower_size = self.lower_outset + \ + self.lower_halfslit + \ + r * self.rear_factor + + self.rear_size = r * self.lower_factor + \ + self.rear_halfslit + \ + self.rear_outset + + self.warn_on_demand() + + def warn_on_demand(self): + warnings = [] + + # Are the discs supported on the outer ends? + + def word_thickness(length): + if length > 0: + return "very thin (%.2g mm at a thickness of %.2g mm)" % ( + length, self.thickness) + if length < 0: + return "absent" + + if self.rear_outset < self.thickness: + warnings.append("Rear upper constraint is %s. Consider increasing" + " the disc outset parameter, or move the angle away from 45°." + % word_thickness(self.rear_outset) + ) + + if self.lower_outset < self.thickness: + warnings.append("Lower front constraint is %s. Consider increasing" + " the disc outset parameter, or move the angle away from 45°." + % word_thickness(self.lower_outset)) + + # Are the discs supported where the grids meet? + + r = self.disc_diameter / 2 + inner_lowerdistance = r * self.rear_factor - self.lower_halfslit + inner_reardistance = r * self.lower_factor - self.rear_halfslit + + if inner_lowerdistance < 0 or inner_reardistance < 0: + warnings.append("Corner is inside the disc radios, discs would not" + " be supported. Consider increasing the factor parameters.") + + # Won't the type-H edge on the rear side make the whole contraption + # wiggle? + + max_slitlengthplush = offset_radius_in_square( + self.outer, self.angle, r * self.rear_factor + self.thickness) + slitlengthplush = self.rear_halfslit + self.thickness * ( 1 + \ + self.edgesettings['FingerJoint']['edge_width']) + + if slitlengthplush > max_slitlengthplush: + warnings.append("Joint would protrude from lower box edge. Consider" + " increasing the the disc outset parameter, or move the" + " angle away from 45°.") + + # Can the discs be removed at all? + # Does not need explicit checking, for Thales' theorem tells us that at + # the point wher there is barely support in the corner, three contact + # points on the circle form just a demicircle and the discs can be + # inserted/removed. When we keep the other contact points and move the + # slits away from the corner, the disc gets smaller and thus will fit + # through the opening that is as wide as the diameter of the largest + # possible circle. + + # Act on warnings + + if warnings: + self.argparser.error("\n".join(warnings)) + + def sidewall_holes(self): + r = self.disc_diameter / 2 + + self.moveTo(self.outer/2, self.outer/2, -self.angle) + # can now move down to paint horizontal lower part, or right to paint + # vertical rear part + with self.saved_context(): + self.moveTo( + r * self.rear_factor, + -r * self.lower_factor - self.thickness/2, + 90) + self.fingerHolesAt(0, 0, self.lower_size) + with self.saved_context(): + self.moveTo( + r * self.rear_factor + self.thickness/2, + -r * self.lower_factor, + 0) + self.fingerHolesAt(0, 0, self.rear_size) + + if self.debug: + self.circle(0, 0, self.disc_diameter / 2) + + def _draw_slits(self, inset, halfslit): + total_x = 0 + + for x in self.sx: + center_x = total_x + x / 2 + + total_x += x + self.rectangularHole(inset, center_x, 2 * halfslit, self.disc_thickness) + if self.debug: + self.ctx.rectangle(inset - halfslit, center_x - x/2, 2 * halfslit, x) + + def lower_holes(self): + r = self.disc_diameter / 2 + inset = self.lower_outset + self.lower_halfslit + + self._draw_slits(inset, self.lower_halfslit) + + def rear_holes(self): + r = self.disc_diameter / 2 + inset = r * self.lower_factor + + self._draw_slits(inset, self.rear_halfslit) + + def render(self): + o = self.outer + + self.rectangularWall(o, o, "eeee", move="right", callback=[self.sidewall_holes]) + self.rectangularWall(o, o, "eeee", move="right mirror", callback=[self.sidewall_holes]) + + self.rectangularWall(self.lower_size, sum(self.sx), "fffe", move="right", callback=[self.lower_holes]) + self.rectangularWall(self.rear_size, sum(self.sx), "fefh", move="right", callback=[self.rear_holes])