# Copyright (C) 2024 Oliver Jensen
#
# 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/>.
from boxes import *
[docs]
class CoinBankSafe(Boxes):
"""A piggy-bank designed to look like a safe."""
description ='''
Make sure not to discard the circle cutouts from the lid, base, and door. They are all needed.
![Closed](static/samples/CoinBankSafe-closed.jpg)
![Open](static/samples/CoinBankSafe-open.jpg)
Assemble the locking pins like this: wiggle-disc, number-disc, doorhole-disc, spacer-disc, D-disc.
Glue the first three in place, but do not glue the last two.
Leaving them unglued will allow you change the code, and to remove the pin from the door.
![Pins](static/samples/CoinBankSafe-pins.jpg)
'''
ui_group = "Misc"
def __init__(self) -> None:
Boxes.__init__(self)
self.addSettingsArgs(edges.FingerJointSettings)
self.buildArgParser("x", "y", "h")
self.argparser.add_argument(
"--slotlength", action="store", type=float, default=30,
help="Length of the coin slot in mm")
self.argparser.add_argument(
"--slotwidth", action="store", type=float, default=4,
help="Width of the coin slot in mm")
self.argparser.add_argument(
"--handlelength", action="store", type=float, default=8,
help="Length of handle in multiples of thickness")
self.argparser.add_argument(
"--handleclearance", action="store", type=float, default=1.5,
help="Clearance of handle in multiples of thickness")
def drawNumbers(self, radius, cover):
fontsize = 0.8 * (radius - cover)
for num in range(8):
angle = num*45
x = (cover + fontsize *0.4) * math.sin(math.radians(angle))
y = (cover + fontsize *0.4) * math.cos(math.radians(angle))
self.text(str(num+1), align="center middle", fontsize=fontsize, angle=-angle, color=[1,0,0],
y=y, x=x)
def lockPin(self, layers, move=None):
t = self.thickness
cutout_width = t/3
barb_length = t
base_length = layers * t
base_width = t
total_length = base_length + barb_length
total_width = base_width + cutout_width * 0.5
cutout_angle = math.degrees(math.atan(cutout_width / base_length))
cutout_length = math.sqrt(cutout_width**2 + base_length**2)
#self.rectangularWall(5*t, t)
if self.move(total_length, total_width, move, True):
return
self.edge(total_length)
self.corner(90)
self.edge(base_width * 1/3)
self.corner(90)
self.edge(base_length)
self.corner(180+cutout_angle, 0)
self.edge(cutout_length)
self.corner(90-cutout_angle)
self.edge(cutout_width * 1.5)
self.corner(90)
self.edge(barb_length)
self.corner(90)
self.corner(-90, cutout_width * 0.5)
self.edge(base_length - cutout_width * 0.5)
self.corner(90)
self.edge(t)
self.corner(90)
self.move(total_length, total_width, move)
def render(self):
x, y, h = self.x, self.y, self.h
t = self.thickness
slot_length = self.slotlength
slot_width = self.slotwidth
handle_length = self.handlelength * t
handle_clearance = self.handleclearance * t
# lock parameters
big_radius = 2.25 * t
small_radius = 1.4 * t
doorhole_radius = 1.25 * t
spacing = 1
# side walls
with self.saved_context():
self.rectangularWall(x, h, "seFf", move="mirror right")
self.rectangularWall(y, h, "sFFF", move="right")
# wall with holes for the locking bar
self.rectangularWall(
x, h, "sfFe", ignore_widths=[3,4,7,8],
callback=[lambda: self.fingerHolesAt(2.75*t, 0, h, 90)],
move="mirror right")
# locking bar
self.moveTo(0, self.edges['s'].spacing() + t)
self.rectangularWall(1.33*t, h, "eeef", move="right")
# door
door_clearance = .1 * t # amount to shave off of the door width so it can open
before_hinge = 1.25 * t - door_clearance
after_hinge = y - 2.25 * t - door_clearance
self.moveTo(self.spacing/2, -t)
self.polyline(
after_hinge, -90, t, 90, t, 90, t, -90, before_hinge, 90,
h, 90,
before_hinge, -90, t, 90, t, 90, t, -90, after_hinge, 90,
h, 90)
num_dials = 3
space_under_dials = 6*big_radius
space_not_under_dials = h - space_under_dials
dial_spacing = space_not_under_dials / (num_dials + 1)
if dial_spacing < 1 :
min_height = 6*big_radius + 4
raise ValueError(f"With thickness {t}, h must be at least {min_height} to fit the dials.")
for pos_y in (h/2,
h/2 - (2*big_radius + dial_spacing),
h/2 + (2*big_radius + dial_spacing)):
self.hole(3*t - door_clearance, pos_y, doorhole_radius)
self.rectangularHole(3*t - door_clearance, pos_y, t, t)
self.rectangularHole(y/2 - door_clearance, h/2, t, handle_length / 2)
self.rectangularWall(x, h, "seff", move="up only")
# top
self.rectangularWall(
y, x, "efff", callback=[
lambda: self.rectangularHole(y/2, x/2, slot_length, slot_width),
lambda: (self.hole(1.75*t, 1.75*t, 1.15*t),
self.rectangularHole(1.75*t, 1.75*t, t, t))],
label="top", move="right")
# bottom
self.rectangularWall(
y, x, "efff", callback=[
lambda: (self.hole(1.75*t, 1.75*t, 1.15*t),
self.rectangularHole(1.75*t, 1.75*t, t, t))],
label="bottom", move="right")
def holeCB():
self.rectangularHole(0, 0, t, t)
self.moveTo(0, 0, 45)
self.rectangularHole(0, 0, t, t)
# locks
with self.saved_context():
self.partsMatrix(3, 1, "right", self.parts.disc, 2*big_radius,
callback=lambda: (self.drawNumbers(big_radius, small_radius), self.rectangularHole(0, 0, t, t)))
self.partsMatrix(3, 1, "right", self.parts.disc, 2*big_radius,
dwidth=0.8,callback=holeCB)
self.partsMatrix(
3, 1, "right", self.parts.disc, 2*small_radius,
callback=lambda:self.rectangularHole(0, 0, t, t))
self.partsMatrix(
3, 1, "right", self.parts.wavyKnob, 2*small_radius,
callback=lambda:self.rectangularHole(0, 0, t, t))
self.partsMatrix(3, 1, "up only", self.parts.disc, 2*big_radius)
# lock pins
with self.saved_context():
self.lockPin(5, move="up")
self.lockPin(5, move="up")
self.lockPin(5, move="up")
self.lockPin(5, move="right only")
# handle
self.moveTo(0)
handle_curve_radius = 0.2 * t
self.moveTo(t * 2.5)
self.polyline(
0,
(90, handle_curve_radius),
handle_length - 2 * handle_curve_radius,
(90, handle_curve_radius),
handle_clearance - handle_curve_radius,
90,
handle_length / 4,
-90,
t,
90,
handle_length / 2,
90,
t,
-90,
handle_length / 4,
90,
handle_clearance - handle_curve_radius,
)