Skip to content

Commit

Permalink
feat: new side-hinge box
Browse files Browse the repository at this point in the history
  • Loading branch information
gcollic committed May 14, 2024
1 parent 8527b71 commit 0addb4d
Show file tree
Hide file tree
Showing 5 changed files with 412 additions and 14 deletions.
16 changes: 16 additions & 0 deletions boxes/edges.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
#############################################################################
Expand Down
15 changes: 1 addition & 14 deletions boxes/generators/agricolainsert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
305 changes: 305 additions & 0 deletions boxes/generators/sidehingebox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
# 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 <http://www.gnu.org/licenses/>.

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.
![Bottom view](static/samples/SideHingeBox.jpg)
"""

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=None,
help="distance between the hinge center and adjacent sides")

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_latch", action="store", type=bool, default=False,
help="add a latch, 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
with_latch = self.cherrymx_latch
if with_latch and t>3.7:
raise "cherrymx_latch not compatible with thickness > 3.7mm"
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, with_latch=with_latch, reverse=True)
d_side_callback = [
lambda:self.fingerHolesAt(self.mx_length+t/2, 0, self.mx_width),
] if with_latch else None
self.rectangularWall(yi, hi, "fFeF", callback=d_side_callback, move="right", label="inner - full side D")
self.inner_side(x, hi, hinge_center, hinge_radius, fingered_hi)
self.rectangularWall(0, hi, "ffef", move="up only")

with self.saved_context():
self.outer_side(x, ho, hinge_center, hinge_radius, fingered_ho, with_latch=with_latch)
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, 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.fingerHolesAt(self.mx_length+t/2, 0, self.mx_width),
] if with_latch 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 with_latch:
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.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_h = ear_t + plywood_t*2;
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(plywood_t+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, ear_t/2+plywood_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 inner_side_cb(self, x):
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)

def inner_side(self, x, h, hinge_center, hinge_radius, fingered_h, with_latch=False, 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)
] if with_latch and reverse else None,
)

def outer_side(self, x, h, hinge_center, hinge_radius, fingered_h, with_latch=False, 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)

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(t+self.mx_width/2, self.mx_width/2, 5.7+self.burn)
] if with_latch and not reverse 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()
Loading

0 comments on commit 0addb4d

Please sign in to comment.