diff --git a/boxes/edges.py b/boxes/edges.py index 18f6643e..4523dcf4 100644 --- a/boxes/edges.py +++ b/boxes/edges.py @@ -367,6 +367,22 @@ def startwidth(self) -> float: return self.settings if self.settings is not None else self.boxes.thickness +class NoopEdge(BaseEdge): + """ + Edge which does nothing, not even turn or move. + """ + + def __init__(self, boxes, margin=0) -> None: + super().__init__(boxes, None) + self._margin = margin + + def __call__(self, _, **kw): + # cancel turn + self.corner(-90) + + def margin(self) -> float: + return self._margin + ############################################################################# #### MountingEdge ############################################################################# diff --git a/boxes/generators/agricolainsert.py b/boxes/generators/agricolainsert.py index 3b1a8a63..b784c18e 100644 --- a/boxes/generators/agricolainsert.py +++ b/boxes/generators/agricolainsert.py @@ -557,7 +557,7 @@ def render_player_box(self, player_box_height, player_box_inner_width): bed_edge = Bed2SidesEdge( self, bed_inner_length, bed_head_length, bed_foot_height ) - noop_edge = NoopEdge(self) + noop_edge = edges.NoopEdge(self) self.ctx.save() optim_180_x = ( bed_inner_length + self.thickness + bed_head_length + 2 * self.spacing @@ -916,16 +916,3 @@ def __call__(self, bed_height, **kw): (90, head_corner), head_length, ) - - -class NoopEdge(edges.BaseEdge): - """ - Edge which does nothing, not even turn or move. - """ - - def __init__(self, boxes) -> None: - super().__init__(boxes, None) - - def __call__(self, length, **kw): - # cancel turn - self.corner(-90) diff --git a/boxes/generators/sidehingebox.py b/boxes/generators/sidehingebox.py new file mode 100644 index 00000000..d8dbe194 --- /dev/null +++ b/boxes/generators/sidehingebox.py @@ -0,0 +1,324 @@ +# Copyright (C) 2024 Guillaume Collic +# +# 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 . + +import math + +from boxes import * + + +class SideHingeBox(Boxes): + """Box, with an hinge that does not protrude from the back of the box, and a latch.""" + + description = """ +This box is another take on a hinge box. +The hinges doesn't protrude from the box, but the sides needs double walls. +When opened, 2 sides are opening, improving access inside the box. +An optional latch is included, based on a mechanical switch and a 3D printed button. +The latch is one-way: the box can be closed freely +(this side of the button is angled, and totally smooth since it's the printing bed surface), +but can't be inadvertently opened. +""" + + ui_group = "Box" + + def __init__(self) -> None: + Boxes.__init__(self) + self.buildArgParser("x", "y", "h", "outside") + self.addSettingsArgs(edges.FingerJointSettings, finger=2.0, space=2.0) + + self.argparser.add_argument( + "--play", action="store", type=float, default=0.15, + help="play between the two sides as multiple of the wall thickness") + + self.argparser.add_argument( + "--hinge_center", action="store", type=float, default=0.0, + help="distance between the hinge center and adjacent sides (0.0 for default)") + + self.argparser.add_argument( + "--hinge_radius", action="store", type=float, default=5.5, + help="radius of the hinge inner circle") + + self.argparser.add_argument( + "--cherrymx_latches", action="store", type=int, default=0, + choices=[0, 1, 2], + help="add one or two latches, based on 3D printing and a cherry mx compatible mechanical keyboard switch") + + + def render(self): + x, yi, hi = self.x, self.y, self.h + t = self.thickness + p = self.play * t + hinge_radius = self.hinge_radius + hinge_center = self.hinge_center if self.hinge_center else 2*t + hinge_radius + latches = self.cherrymx_latches + self.mx_width = 15.4 + self.mx_length = t+16.4+2.8 #2.8 can be removed if the switch is trimmed flush + + if self.outside: + x -= 2*t + yi -= 4*t + 2*p + hi -= 2*t + + yo = yi + 2*(t+p) + ho = hi + t + + # one side is shared between inside and outside part, + # so that the lid can rotate and lay flat, without touching the inner bottom + fingered_hi = 2*hinge_center-t + # a small gap is also needed for both part to rotate freely + # (for a rounded angled finish, a gapless version could be added, with manual sanding or mechanical round milling) + gap = math.sqrt(abs(pow(hinge_center*math.sqrt(2),2)-pow(hinge_center-t,2)))-hinge_center + fingered_ho = ho - gap - 2*hinge_center + + with self.saved_context(): + self.inner_side(x, hi, hinge_center, hinge_radius, fingered_hi, latches, reverse=True) + self.rectangularWall( + yi, + hi, + "fFeF", + callback=[lambda:self.back_cb(yi, latches)], + move="right", + label="inner - full side D") + self.inner_side(x, hi, hinge_center, hinge_radius, fingered_hi, latches) + self.rectangularWall(0, hi, "ffef", move="up only") + + with self.saved_context(): + self.outer_side(x, ho, hinge_center, hinge_radius, fingered_ho, latches) + with self.saved_context(): + self.rectangularWall(yo, fingered_ho, "fFeF", move="up", label="outer - small side B") + self.moveTo(t+p,0) + self.rectangularWall(yi, fingered_hi, "eFfF", move="right", label="inner - small side B") + self.rectangularWall(yo, 0, "fFeF", move="right only") + self.outer_side(x, ho, hinge_center, hinge_radius, fingered_ho, latches, reverse=True) + self.rectangularWall(0, ho, "ffef", move="up only") + + bottom_callback = [ + lambda:self.fingerHolesAt(x-self.mx_width-t/2, 0, self.mx_length), + lambda:self.back_cb(yi, latches), + lambda:self.fingerHolesAt(self.mx_width+t/2, 0, self.mx_length) if latches>1 else None, + ] if latches else None + self.rectangularWall(x, yi, "FFFF", callback=bottom_callback, move="right", label="inner - bottom") + self.rectangularWall(x, yo, "FEFF", move="right", label="outer - upper lid") + for _ in range(2): + self.rectangularWall(2*t, 1.5*t, "eeee", move="right") + + if latches: + for _ in range(latches): + with self.saved_context(): + self.rectangularWall(self.mx_width, self.mx_width, "eeee", move="right") + self.rectangularWall(self.mx_width, self.mx_width, "ffef", move="right") + self.rectangularWall(self.mx_length, self.mx_width, "ffeF", move="right") + self.rectangularWall(self.mx_length, self.mx_width, "ffeF", move="up only") + self.text(f""" +OpenSCAD code for 3D printing the cherry MX latch button: +############################################# +play = 0.1; +plywood_t = {t}; +ear_t = 0.4; +ear_d = 15; +btn_d = 11.4; +btn_ext_h = min(plywood_t, 3.7); +btn_h = ear_t + plywood_t + btn_ext_h; +module mx_outer() {{ + translate([0,0,btn_h+4]) + mirror([0,0,1]) + linear_extrude(height = 4.2) {{ + offset(r=1, $fn=32){{ + square([4.5, 2.8], center=true); + }} + }}; +}} +module mx_inner() {{ + translate([0,0,btn_h+4.01]) + mirror([0,0,1]) + for (rect = [ [4.05, 1.32], [1.22, 5] ]) {{ + linear_extrude(height = 4) + square(rect, center=true); + hull() {{ + linear_extrude(height = 0.01) + offset(delta = 0.4) + square(rect, center=true); + translate([0, 0, 0.5]) + linear_extrude(height = 0.01) + square(rect, center=true); + }}; + }} +}} +angle = atan2(btn_ext_h+0.2, btn_d/2); +rotate([0, angle, 0]) difference(){{ + union(){{ + cylinder(d=btn_d-2*play, h=btn_h, $fn=512); + translate([0, 0, btn_h-ear_t/2]) + cube([btn_d/2, ear_d, ear_t], center=true); + mx_outer(); + }} + rotate([0, 90-angle, 0]) + translate([0, -btn_d/2, 0]) + cube(btn_d); + mx_inner(); +}}""") + + def back_cb(self, y, latches): + if latches>0: + self.fingerHolesAt(self.mx_length+self.thickness/2, 0, self.mx_width) + if latches>1: + self.fingerHolesAt(y-self.mx_length-self.thickness/2, 0, self.mx_width) + + def inner_side_cb(self, x, reverse): + if reverse: + self.fingerHolesAt(x-self.mx_width-self.thickness/2, 0, self.mx_width) + self.circle(x-self.mx_width/2, self.mx_width/2, 5.7+self.burn) + else: + self.fingerHolesAt(self.mx_width+self.thickness/2, 0, self.mx_width) + self.circle(self.mx_width/2, self.mx_width/2, 5.7+self.burn) + + def inner_side(self, x, h, hinge_center, hinge_radius, fingered_h, latches, reverse=False): + sides = Inner2SidesEdge( + self, x, h, hinge_center, hinge_radius, fingered_h, reverse + ) + noop_edge = edges.NoopEdge(self, margin=self.thickness if reverse else 0) + + self.rectangularWall( + x, + h, + ["f", "f", sides, noop_edge] if reverse else["f", sides, noop_edge, "f"], + move="right", + label="inner - hinge side " + ("A" if reverse else "C"), + callback=[ + lambda: self.inner_side_cb(x, reverse) + ] if (latches and reverse) or latches>1 else None, + ) + + def outer_side(self, x, h, hinge_center, hinge_radius, fingered_h, latches, reverse=False): + t = self.thickness + sides = Outer2SidesEdge( + self, x, h, hinge_center, hinge_radius, fingered_h, reverse + ) + noop_edge = edges.NoopEdge(self, margin=t if reverse else 0) + + latch_x, latch_y = (t+self.mx_width/2, self.mx_width/2) + if reverse: + latch_x, latch_y = latch_y, latch_x + self.rectangularWall( + x, + h, + ["f", "E", sides, noop_edge] if reverse else["f", sides, noop_edge, "E"], + move="right", + label="outer - hinge side " + ("C" if reverse else "A"), + callback=[ + None, + None, + lambda: self.circle(latch_x, latch_y, 5.7+self.burn) + ] if (latches and not reverse) or latches>1 else None, + ) + +class Inner2SidesEdge(edges.BaseEdge): + """ + The next edge should be a NoopEdge + """ + + def __init__(self, boxes, length, height, hinge_center, hinge_radius, fingered_h, reverse) -> None: + super().__init__(boxes, None) + self.length = length + self.height = height + self.hinge_center=hinge_center + self.hinge_radius=hinge_radius + self.fingered_h=fingered_h + self.reverse=reverse + + def __call__(self, _, **kw): + actions = [self.hinge_hole, self.fingers, self.smooth_corner] + actions = list(reversed(actions)) if self.reverse else actions + for action in actions: + action() + + def fingers(self): + self.boxes.edges['f'](self.fingered_h) + + def smooth_corner(self): + # the corner has to be rounded to rotate freely + hinge_to_lid = self.height+self.boxes.thickness-self.hinge_center + hinge_to_side = self.hinge_center-self.boxes.thickness + corner_height = hinge_to_lid-math.sqrt(math.pow(hinge_to_lid, 2) - math.pow(hinge_to_side, 2)) + angle = math.degrees(math.asin(hinge_to_side/hinge_to_lid)) + path = [ + self.height-self.fingered_h-corner_height, + (90-angle, 0), + 0, + (angle, hinge_to_lid), + self.boxes.thickness+self.length-self.hinge_center, + ] + path = list(reversed(path)) if self.reverse else path + self.polyline(*path) + + def hinge_hole(self): + direction = -1 if self.reverse else 1 + x = direction*(self.hinge_center-self.boxes.thickness-self.boxes.burn) + y = self.hinge_center-self.boxes.thickness + t = self.boxes.thickness + self.boxes.rectangularHole(x, y, 1.5*t, t) + + def margin(self) -> float: + return 0 if self.reverse else self.boxes.edges['f'].margin() + +class Outer2SidesEdge(edges.BaseEdge): + """ + The next edge should be a NoopEdge + """ + + def __init__(self, boxes, length, height, hinge_center, hinge_radius, fingered_h, reverse) -> None: + super().__init__(boxes, None) + self.length = length + self.height = height + self.hinge_center=hinge_center + self.hinge_radius=hinge_radius + self.fingered_h=fingered_h + self.reverse=reverse + + def __call__(self, _, **kw): + actions = [self.fingers, self.smooth_corner, self.hinge_hole] + actions = list(reversed(actions)) if self.reverse else actions + for action in actions: + action() + + def fingers(self): + self.boxes.edges['f'](self.fingered_h) + + def smooth_corner(self): + # the corner has to be rounded to rotate freely + path = [ + 0, + (-90, 0), + self.boxes.thickness, + (90, 0), + self.height-self.fingered_h-self.hinge_center, + (90, self.hinge_center), + self.boxes.thickness+self.length-self.hinge_center, + ] + path = list(reversed(path)) if self.reverse else path + self.polyline(*path) + + @restore + @holeCol + def hinge_hole(self): + direction = -1 if self.reverse else 1 + x = direction*(self.hinge_center-self.length-self.boxes.thickness-self.boxes.burn) + y = self.hinge_center + t = self.boxes.thickness + self.boxes.circle(x, y, self.hinge_radius) + self.boxes.rectangularHole(x, y, t, 1.5*t) + + def margin(self) -> float: + return 0 if self.reverse else self.boxes.edges['f'].margin() diff --git a/examples/SideHingeBox.svg b/examples/SideHingeBox.svg new file mode 100644 index 00000000..3e279a54 --- /dev/null +++ b/examples/SideHingeBox.svg @@ -0,0 +1,86 @@ + + + +SideHingeBox + + +Box - SideHingeBox +boxes SideHingeBox +Box, with an hinge that does not protrude from the back of the box, and a latch. + + +This box is another take on a hinge box. +The hinges doesn't protrude from the box, but the sides needs double walls. +When opened, 2 sides are opening, improving access inside the box. +An optional latch is included, based on a mechanical switch and a 3D printed button. +The latch is one-way: the box can be closed freely +(this side of the button is angled, and totally smooth since it's the printing bed surface), +but can't be inadvertently opened. + + +Created with Boxes.py (https://festi.info/boxes.py) +Command line: boxes SideHingeBox +Command line short: boxes SideHingeBox + + + + + 100.0mm, burn:0.10mm + + + inner - hinge side A + + + + inner - full side D + + + inner - hinge side C + + + + outer - hinge side A + + + + + outer - small side B + + + inner - small side B + + + outer - hinge side C + + + + + inner - bottom + + + outer - upper lid + + + + + + + + \ No newline at end of file diff --git a/static/samples/SideHingeBox-thumb.jpg b/static/samples/SideHingeBox-thumb.jpg new file mode 100644 index 00000000..0d5a1921 Binary files /dev/null and b/static/samples/SideHingeBox-thumb.jpg differ diff --git a/static/samples/SideHingeBox.jpg b/static/samples/SideHingeBox.jpg new file mode 100644 index 00000000..a325c0e0 Binary files /dev/null and b/static/samples/SideHingeBox.jpg differ diff --git a/static/samples/samples.sha256 b/static/samples/samples.sha256 index 99dafde5..99a59df5 100644 --- a/static/samples/samples.sha256 +++ b/static/samples/samples.sha256 @@ -175,3 +175,4 @@ f51e3cb0e74e380beda4c0966ee770142ff4fa38c266ca026177f9f4978190a4 ../static/samp a0865738425d5d9966dc6975d7e73559bac3c307c9614e8b48bae4abdf3efb5b ../static/samples/BrickSorter-2.jpg 2dcb314cdfa8b136b59288d2f4f7e501b4290ff68560216b6bedd779a32095ad ../static/samples/Shadowbox-diagram.jpg 21333db253007b6e101333826a1231d788cf10e0e2afec84ff2a3f983330091d ../static/samples/Matrix.jpg +9855c836088d93e508f1c1899ae62733111062f932c1d79ddcc827da2b73335c ../static/samples/SideHingeBox.jpg