3D Printing
For this project, I originally wanted to print a hinge assembly that could be assembled to make a 3D “H”. This required unfolding the H flat and compressing the result so that it could fit in a 3D printer and hopefully print in a reasonable amount of time.
66 Problematic Slabs
Since this is made of 66 slabs, I made a quick mockup of 66 slabs in Blender and loaded the result to PrusaSlicer to see how long it would take. Which ended up being about 5-ish hours. (In fairness, I used a 0.25mm nozzle, which was incorrect. The printer uses a 0.4mm nozzle, which makes the print more like 1.5 hours).
There’s also the result of the clearance design rule, which says that 0.4mm clearance is a safe amount. The slabs were 15mm×15mm×3mm and I was afraid that they weren’t big enough. So I gave up on that for now and decided to make a hinge assembly that unfolds into a 2D “H” instead, using 30mm×30mm×4mm slabs.
H is for Hinge Testing
Of course, we render CAD in Blender.
I made a test hinge assembly in Blender, which consisted of two slabs with a hinge in the middle. One of the slabs contains the bar of the hinge, while the other slab contains the rings. A clearance of 0.4mm was used everywhere there needed to be a clearance, except where the slabs meet. If I use clearance there, the hinge would be able to bend backwards, which I don’t want. So instead, I rotated a slab. Additionally, I made a horizontal hinge and a vertical hinge because I wanted to make sure even horizontal hinges would work.
Alas, the horizontal one worked and the vertical one didn’t. The connection between the bar and the slab was too weak.
So I strengthened the connection, which resulted in a successful attempt. In addition, it turns out that the ring chamfer was causing the clearance to be a little too big between an end ring and the slab it’s hinged to, so I removed the chamfers because I can.
This test still worked.
Hinge Starts With H
So now it’s time to build the actual H assembly out of hinges. I made the assembly and manually folded it up to fit in a smaller space (and also because otherwise the clearance between slabs is a big fat unforgiving 0mm). When folding, I tried to optimize so that slabs would be as flat as possible (flatter slabs require less printing time) and require as little support as possible (supports take time to print and take off. I originally wanted to use the Stratasys because of dissolvable supports, but was told during training that it was basically inferior in every other way, so…). Thus, all slabs are either flat or at angles between 40° and 50°.
And now, 3D printing time! The 3D printer used was a Prusa MK4 with layer spacing 0.2mm and nozzle size 0.4mm. The print was done with PLA. I added manual support blockers to take advantage of bridging, and the overhang angle threshold was set to 35° so that supports wouldn’t be placed on those 40° angles. The print took about 3 hours 45 minutes.
The moment of truth…
├ │ is for Hole Rudely Filled In
So, well, unfortunately, as you can see, umm, it failed. A hinge broke. So I had to do some bug investigating, and it turns out that PrusaSlicer rudely filled in a >2mm hole in the model!
After more investigating, it turns out that the mesh wasn’t a manifold. There are several manifold rules, but the important one here is:
- every edge much touch exactly 2 faces, with opposite orientations around the edge.
It turns out that there were edges that were secretly touching 1 face or 3 or more faces. And PrusaSlicer was able to cope except in that one spot.
I thus learned the hard way that Blender can create a non-manifold mesh from the union of 2 manifold meshes, particularly when they overlap faces. And there’s no easy way around that. So, it’s finally time to use Fusion OpenSCAD. The Blender file I used to play around with hinge making can be found here.
*opens CAD application*
So actually, I found this cool Python library called SolidPython2, which uses Python to generate OpenSCAD code. So I don’t have to deal with clunky limited OpenSCAD code. The code is as follows:
import solid2 as s2
TY_END = 0
TY_STRAIGHT = 1
TY_3_WAY = 2
DECORATION_BITFIELDS = [0b0001, 0b0101, 0b0111]
RINGS_BITFIELDS = [0b0001, 0b0001, 0b0000]
BARS_BITFIELDS = [0b0000, 0b0100, 0b0111]
side_length = 30
depth = 4
thickness = 4
diff_buffer = 0.1
decor_width = 10
bar_radius = 1
bar_carve_radius = 3
cylinder_fragments = 64
clearance = 0.4
ring_height = 2.2
# Decor stuff
decor_minus = s2.cube(decor_width, (side_length - decor_width) / 2 + diff_buffer, thickness / 2 + diff_buffer, True).translate(
0, -decor_width / 2 - (side_length - decor_width) / 4, -(thickness / 2 + diff_buffer) / 2)
decor_plus_length = (side_length - decor_width) / 2 + thickness / 2
decor_plus = s2.cube(thickness, decor_plus_length, thickness, True).back(side_length / 2 - decor_plus_length / 2)
decor_plus = decor_plus.left(decor_width / 2) + decor_plus.right(decor_width / 2)
decor_plus_fill = s2.cube(decor_width + thickness, thickness, thickness, True).back(decor_width / 2)
# Rings
ring_outer_radius = bar_carve_radius - clearance
ring_inner_radius = bar_radius + clearance
ring_distance = side_length / 2 - thickness - clearance - ring_height / 2
ring_minus = s2.cylinder(side_length + diff_buffer, r=ring_inner_radius, center=True, _fn=cylinder_fragments).rotateY(90).translate(0, -side_length / 2, thickness / 2)
ring_plus = (s2.cylinder(ring_height, r=ring_outer_radius, center=True, _fn=cylinder_fragments) - s2.cylinder(ring_height + diff_buffer, r=ring_inner_radius, center=True, _fn=cylinder_fragments)) \
.rotateY(90).translate(0, -side_length / 2, thickness / 2)
ring_plus = ring_plus.left(ring_distance) + ring_plus + ring_plus.right(ring_distance)
# Bars
bar_minus = s2.cylinder(side_length - 2 * thickness, r=bar_carve_radius, center=True, _fn=cylinder_fragments).rotateY(90).translate(0, -side_length / 2, thickness / 2)
bar_plus = s2.polygon([(-side_length / 2, 0), (-side_length / 2 + thickness, bar_radius), (side_length / 2 - thickness, bar_radius), (side_length / 2, 0)]) \
.rotateZ(90).rotate_extrude(_fn=cylinder_fragments).rotateY(90).translate(0, -side_length / 2, thickness / 2)
# Stubs
stub_height = ring_outer_radius
stub_plus = s2.cube(thickness, thickness, stub_height + diff_buffer, True).translate(0, side_length / 2 - thickness / 2, stub_height + thickness / 2 - (stub_height + diff_buffer) / 2)
stub_plus = stub_plus.left(side_length / 2 - 3 * thickness / 2) + stub_plus.right(side_length / 2 - 3 * thickness / 2)
def slab(type, flip_rings: bool, text: str = "", text_offset: float = 0):
model = s2.cube(side_length, side_length, depth, True) - s2.cube(side_length - 2 * thickness, side_length - 2 * thickness, depth + diff_buffer, True)
if type == TY_END:
model += stub_plus
for i in range(4):
if DECORATION_BITFIELDS[type] >> i & 1:
model -= decor_minus.rotateZ(90 * i)
for i in range(4):
model += (decor_plus if DECORATION_BITFIELDS[type] >> i & 1 else decor_plus_fill).rotateZ(90 * i)
rings_bitfield = (BARS_BITFIELDS if flip_rings else RINGS_BITFIELDS)[type]
bars_bitfield = (RINGS_BITFIELDS if flip_rings else BARS_BITFIELDS)[type]
for i in range(4):
if rings_bitfield >> i & 1:
model -= ring_minus.rotateZ(90 * i)
if bars_bitfield >> i & 1:
model -= bar_minus.rotateZ(90 * i)
for i in range(4):
if rings_bitfield >> i & 1:
model += ring_plus.rotateZ(90 * i)
if bars_bitfield >> i & 1:
model += bar_plus.rotateZ(90 * i)
label = s2.text(text, (decor_width - thickness) * 1.1, "Counter:style=Black", "center", "center", spacing=1.5) \
.linear_extrude(thickness / 2, True).rotateZ(90).rotateX(180).translateY(text_offset)
return model + label
model = (slab(TY_END, False).translate(-1.5 * side_length, 1.5 * side_length, 0)
+ slab(TY_STRAIGHT, False).translate(0, 1.5 * side_length, 0)
+ slab(TY_3_WAY, False).translate(1.5 * side_length, 1.5 * side_length, 0)
+ slab(TY_END, True).translate(-1.5 * side_length, -1.5 * side_length, 0)
+ slab(TY_STRAIGHT, True).translate(0, -1.5 * side_length, 0)
+ slab(TY_3_WAY, True).translate(1.5 * side_length, -1.5 * side_length, 0)
+ slab(TY_STRAIGHT, False, "HA", -1.5).translate(-1.5 * side_length, 0, 0)
+ slab(TY_STRAIGHT, False, "YA", -1.5).translate(0, 0, 0)
+ slab(TY_STRAIGHT, False, "SHI", 0).translate(1.5 * side_length, 0, 0)
)
model.save_as_scad("Hinge.scad")
Then I exported an STL, imported it into Blender and rearranged the hinges into the previous folded pattern. And I made sure to check that it’s a manifold this time, which it is! Here is the STL file.
(Now, technically the hinge assembly can be made subtractively, if you had a really precise tool to dig out the hinges and also made sure to add outer supports so the hinges say fixed while you’re digging them out, but it’s so much easier to 3D print, I bet.)
And now the moment of truth:
I got greedy during support removal here. Support removal turns out to be a really annoying process when you have tiny precise parts. Oh well, time to try again.
Clearing the H
This time, for a different reason, I added a 0.4mm clearance between hinges. This should hopefully solve the unrelated problem that the H tends to bend inward when laid flat, and also allow me to lay flat slabs next to each other in the model to send to the 3D printer.
Turns out that I messed up in the OpenSCAD code when adding the clearance and didn’t move some of the cuboids properly, so they ended up touching each other. I failed to notice this because I was modelling the assembly hingeside-up and it’s hard to spot when you’re looking at the hinge side. So, fourth attempt:
It works, and only one ring (not a whole hinge) broke off!
Well, that was too easy, so let’s print something else.
The Line Slide
That is, linear tracks. The basic schematic I used for a linear track looks like this:
I tried various tests to see which linear track works the best, and I arrived at wall thickness 5mm, vertical clearance 0.4mm, horizontal clearance 0.2mm, and track inset 1mm. The outer track height and inner track length, among other parameters, would vary based on where the track gets used and how much I want to protect against rotation.
The Plane Slide
Now that I have a good linear track, its time to do stuff with it. First, planar movement (with a bonus attachable funnel!), which is relevant to my final project. This was quickly modelled in Blender and printed, and it pretty much works first try.
The axis that’s attached at 2 points experiences a lot of friction due to torque, and it’s best to move both ends at the same speed at the same time, which makes it move smoothly.
Exponential Locking Power!
Again, that was too easy. So now it’s time for an exponential lock!
Doing the Math
The basic idea was to make a lock that would take an exponential number of steps to unlock. Exponential on the number of parts it has. Each part represents a bit in a binary counter. Except that I can’t make a binary counter directly, because on every second count, multiple bits change at the same time. After some thinking, I came up with a rule that worked:
\[b_k\textrm{ is flippable}\iff k = 0 \vee \left(b_{k-1} = 1 \wedge \forall i < k - 1, b_i = 0\right)\]where \(b_k\) is the value of bit \(k\). In other words, a bit is flippable if the bit directly before it (if there is one) is 1 and all bits before that are 0. Then we have
\[\begin{align*}T(0\to 1) & = 1 \\ T(0^{\cdot k}0\to 0^{\cdot k}1) & = T(0^{\cdot k-1}0\to 0^{\cdot k-1}1) + 1 + T(0^{\cdot k-1}1\to 0^{\cdot k-1}0) \end{align*}\]where \(T(a\to b)\) is the number of moves it takes to go from configuration \(a\) to \(b\), with the bit on the left being bit 0. The notation \(0^{\cdot k}\) here means \(k\) 0’s in a row, and \(1^{\cdot k}\) is used similarly. The flipping rule only says when a bit is flippable, not when it can specifically be changed to 0 or 1, so moves are reversible, simplifying the recursion to
\[\begin{align*}T(0\to 1) & = 1 \\ T(0^{\cdot k}0\to 0^{\cdot k}1) & = 2T(0^{\cdot k-1}0\to 0^{\cdot k-1}1) + 1 \end{align*}\]If \(f(k) = T(0^{\cdot k-1}0\to 0^{\cdot k-1}1)\), then we have \(f(1) = 1\) and \(f(k + 1) = 2f(k) + 1\), giving us \(f(k) = 2^k - 1\), an exponential function. So the rule works.
Doing the Physics
Now to design a system that actually implements the rule:
\[b_k\textrm{ is flippable}\iff k = 0 \vee \left(b_{k-1} = 1 \wedge \forall i < k - 1, b_i = 0\right)\]To simplify things, I split the rule into the following constraints:
\[\textrm{(Latch Rule) }b_k\textrm{ is flippable}\implies k = 0 \vee b_{k-1} = 1\] \[\textrm{(Wall Rule) }b_k\textrm{ is flippable}\implies \forall i < k - 1, b_i = 0\]The latch rule is implemented with a latch attached to each bit that couples it with the previous bit. The wall rule is implemented with a big sliding wall that has only 2 free slots. Any bit to the left of the slots is forced to be 0, and any bit to the right of the slots is forced to stay where it is.
I did a test print of the latch mechanic to make sure that it worked first. Then I printed the big lock.
In this design, the wall is split into 2 parts because otherwise the linear track would be too long. Unfortunately, when removing supports, a very important limiter broke off, so back to the drawing board. Also, torque shows up again as an annoying enemy.
Take 2: The Skyline
In this design, I made the limiter fatter so it won’t break off. I also added a skyline track to make torque less of a problem.
Take 3: We CAD in Blender (again)
But while that was printing, I thought of a better idea that used much simpler moving parts: make the latches their own moving parts separate from the bits. Then everything can stay low to the ground and I can use simple linear tracks without skylines without worrying about torque. Time was running out, so I modelled this in Blender instead of using OpenSCAD.
A nice thing I could do here is combine the walls. As it turns out, if you have \(n\) bits, the wall only needs to move the distance of \(n - 2\) bits. More importantly, I managed to decrease the spacing between bits so I could fit the wall on a not ridiculously long linear track.
Support removal for this took about an hour. An hour full of suspense, hoping that nothing would break along the way. Unfortunately, a locking hook did break, but it was easy to fix with super glue and accelerator.
When I printed this, the actual lock (a slab attacked to a bar) came out bent, so I added a little rectangle over the slab that goes in the box so that PrusaSlicer can add supports to protect the slab from bending while printing. This solved the problem.
Appendix
Code for H hinge assembly (I used a nice library called Solid Python so I wouldn’t have to deal with OpenSCAD code):
import solid2 as s2
from math import tau
TY_END = 0
TY_STRAIGHT = 1
TY_3_WAY = 2
DECORATION_BITFIELDS = [0b0001, 0b0101, 0b0111]
RINGS_BITFIELDS = [0b0001, 0b0001, 0b0000]
BARS_BITFIELDS = [0b0000, 0b0100, 0b0111]
side_length = 30
depth = 4
thickness = 4
diff_buffer = 0.1
decor_width = 10
bar_radius = 1
bar_carve_radius = 3
cylinder_fragments = 64
clearance = 0.4
ring_height = 2.2
# Decor stuff
decor_minus = s2.cube(decor_width, (side_length - decor_width) / 2 + diff_buffer, thickness / 2 + diff_buffer, True).translate(
0, -decor_width / 2 - (side_length - decor_width) / 4, -(thickness / 2 + diff_buffer) / 2)
decor_plus_length = (side_length - decor_width) / 2 + thickness / 2 - clearance / 2
decor_plus = s2.cube(thickness, decor_plus_length, thickness, True).back(side_length / 2 - decor_plus_length / 2 - clearance / 2)
decor_plus = decor_plus.left(decor_width / 2) + decor_plus.right(decor_width / 2)
decor_plus_fill = s2.cube(decor_width + thickness, thickness, thickness, True).back(decor_width / 2)
# Rings
ring_outer_radius = bar_carve_radius - clearance
ring_inner_radius = bar_radius + clearance
ring_distance = side_length / 2 - thickness - clearance - ring_height / 2
ring_minus = s2.cylinder(side_length + diff_buffer, r=ring_inner_radius, center=True, _fn=cylinder_fragments).rotateY(90).translate(0, -side_length / 2, thickness / 2)
ring_plus = (s2.cylinder(ring_height, r=ring_outer_radius, center=True, _fn=cylinder_fragments) - s2.cylinder(ring_height + diff_buffer, r=ring_inner_radius, center=True, _fn=cylinder_fragments)) \
.rotateY(90).translate(0, -side_length / 2, thickness / 2)
ring_plus = ring_plus.left(ring_distance) + ring_plus + ring_plus.right(ring_distance)
# Bars
bar_minus = s2.cylinder(side_length - 2 * thickness, r=bar_carve_radius, center=True, _fn=cylinder_fragments).rotateY(90).translate(0, -side_length / 2, thickness / 2)
bar_plus = s2.polygon([(-side_length / 2, 0), (-side_length / 2 + thickness / 2, bar_radius), (side_length / 2 - thickness / 2, bar_radius), (side_length / 2, 0)]) \
.rotateZ(90).rotate_extrude(_fn=cylinder_fragments).rotateY(90).translate(0, -side_length / 2, thickness / 2)
# Stubs
stub_height = ring_outer_radius
stub_plus = s2.cube(thickness, thickness, stub_height + diff_buffer, True).translate(0, side_length / 2 - thickness / 2, stub_height + thickness / 2 - (stub_height + diff_buffer) / 2)
stub_plus = stub_plus.left(side_length / 2 - 3 * thickness / 2) + stub_plus.right(side_length / 2 - 3 * thickness / 2)
def slab(type, flip_rings: bool, text: str = "", text_offset: float = 0):
model = s2.cube(side_length - clearance, side_length - clearance, depth, True) - s2.cube(side_length - 2 * thickness, side_length - 2 * thickness, depth + diff_buffer, True)
if type == TY_END:
model += stub_plus
for i in range(4):
if DECORATION_BITFIELDS[type] >> i & 1:
model -= decor_minus.rotateZ(90 * i)
for i in range(4):
model += (decor_plus if DECORATION_BITFIELDS[type] >> i & 1 else decor_plus_fill).rotateZ(90 * i)
rings_bitfield = (BARS_BITFIELDS if flip_rings else RINGS_BITFIELDS)[type]
bars_bitfield = (RINGS_BITFIELDS if flip_rings else BARS_BITFIELDS)[type]
for i in range(4):
if rings_bitfield >> i & 1:
model -= ring_minus.rotateZ(90 * i)
if bars_bitfield >> i & 1:
model -= bar_minus.rotateZ(90 * i)
for i in range(4):
if rings_bitfield >> i & 1:
model += ring_plus.rotateZ(90 * i)
if bars_bitfield >> i & 1:
model += bar_plus.rotateZ(90 * i)
label = s2.text(text, (decor_width - thickness) * 1.1, "Counter:style=Black", "center", "center", spacing=1.5) \
.linear_extrude(thickness / 2, True).rotateZ(90).rotateX(180).translateY(text_offset)
return model + label
model = (slab(TY_END, False).translate(-1.5 * side_length, 1.5 * side_length, 0)
+ slab(TY_STRAIGHT, False).translate(0, 1.5 * side_length, 0)
+ slab(TY_3_WAY, False).translate(1.5 * side_length, 1.5 * side_length, 0)
+ slab(TY_END, True).translate(-1.5 * side_length, -1.5 * side_length, 0)
+ slab(TY_STRAIGHT, True).translate(0, -1.5 * side_length, 0)
+ slab(TY_3_WAY, True).translate(1.5 * side_length, -1.5 * side_length, 0)
+ slab(TY_STRAIGHT, False, "HA", -1.5).translate(-1.5 * side_length, 0, 0)
+ slab(TY_STRAIGHT, False, "YA", -1.5).translate(0, 0, 0)
+ slab(TY_STRAIGHT, False, "SHI", 0).translate(1.5 * side_length, 0, 0)
)
model.save_as_scad("Hinge.scad")
Code for exponential lock design 2:
import solid2 as s2
import numpy as np
from math import sqrt
horz_clearance = 0.2
vert_clearance = 0.4
moving_clearance = 1.0
l_track_moving_clearance = moving_clearance# / sqrt(2)
support_clearance = 4.0
diff_buffer = 0.1
track_inset = 1
inner_track_extension_thickness = 2
outer_track_height = 3
p_outer_track_height = outer_track_height
wall_thickness = 5
long_wall_thickness = 5
blocker_length = 5
blocker_width = 4
p_width = 12
l_right_back_y = -p_width
l_right_front_y = l_right_back_y + wall_thickness * sqrt(2)
l_movement_x = blocker_width + moving_clearance
p_inner_track_length = 20
p_outer_track_length = p_inner_track_length + 2 * moving_clearance * sqrt(2) + blocker_length + l_right_front_y + moving_clearance * sqrt(2) - l_movement_x - l_right_back_y + 2 #fudge #wall_thickness * sqrt(2)
p_movement = p_outer_track_length - p_inner_track_length
l_left_length = 20
extra_width = 4
spacing = p_width + extra_width + moving_clearance * sqrt(2)
margin = 5
l_blocker_thickness = 3
l_blocker_track_margin = 2
l_blocker_track_width_x = p_width - 2 * l_blocker_track_margin
l_blocker_width_x = l_blocker_track_width_x - l_movement_x
copies = 7
lock_clearance = 2
lock_spiral_thickness = 3
lock_spiral_top_length = lock_spiral_thickness + lock_clearance + wall_thickness + l_movement_x
lock_spiral_length = lock_spiral_top_length + 2 * lock_clearance + wall_thickness + l_movement_x - 1 # fudge
box_length = 36
box_width = 30
box_height = 36
box_thickness = 1.5
box_tab_inset = 5
sky_track_thickness = outer_track_height
zero_one_walls_exist = copies > 2
zero_wall_offset_y = -p_outer_track_length - margin
zero_wall_inner_track_length = 20
zero_wall_width = spacing * (copies - 2)
zero_wall_movement = zero_wall_width
zero_wall_inner_track_x = zero_wall_movement + zero_wall_inner_track_length # There's another track that wants to move here, so account for that
one_wall_x = spacing * 2 - wall_thickness
one_wall_width = spacing * (copies - 2)
p_length = -zero_wall_offset_y - horz_clearance
zero_one_wall_offset_x = wall_thickness - spacing / 2
zero_wall_escape_y = -p_length - p_movement - moving_clearance
bottom_z = 0
p_bottom_z = bottom_z + moving_clearance
p_inner_track_bottom_z = p_bottom_z + 2
p_outer_track_bottom_z = p_inner_track_bottom_z + vert_clearance
base_top_z = p_outer_track_bottom_z + p_outer_track_height
p_inner_track_top_z = base_top_z + vert_clearance
l_bottom_z = base_top_z + support_clearance
blocker_z = l_bottom_z + 3
p_extension_bottom_z = blocker_z + support_clearance
l_bottom_2_z = p_extension_bottom_z - support_clearance # Note: lower than the previous layer!
p_extension_bottom_2_z = p_extension_bottom_z + 2
l_extension_bottom_z = p_extension_bottom_2_z + support_clearance
l_inner_track_bottom_z = l_extension_bottom_z + 2
l_outer_track_bottom_z = l_inner_track_bottom_z + vert_clearance
l_outer_track_top_z = l_outer_track_bottom_z + outer_track_height
l_inner_track_top_z = l_outer_track_top_z + vert_clearance
l_top_z = l_inner_track_top_z + 2
p_extension_top_2_z = l_top_z + support_clearance
p_extension_top_z = p_extension_top_2_z + 2
p_tip_top_z = p_extension_top_z + moving_clearance
p_inner_sky_track_bottom_z = p_tip_top_z + support_clearance
p_outer_sky_track_bottom_z = p_inner_sky_track_bottom_z + vert_clearance
p_outer_sky_track_top_z = p_outer_sky_track_bottom_z + p_outer_track_height
p_inner_sky_track_top_z = p_outer_sky_track_top_z + vert_clearance
p_top_over_track_z = p_inner_sky_track_top_z + 2
zero_one_wall_thickness = long_wall_thickness
zero_wall_bottom_z = p_extension_bottom_z
zero_wall_top_z = zero_wall_bottom_z + zero_one_wall_thickness
p_1_z = zero_wall_top_z
one_wall_bottom_z = p_1_z + support_clearance
one_wall_top_z = one_wall_bottom_z + zero_one_wall_thickness
one_wall_escape_bottom_z = p_tip_top_z + moving_clearance
one_wall_escape_top_z = one_wall_escape_bottom_z + wall_thickness
zero_wall_escape_bottom_z = one_wall_escape_top_z + moving_clearance
zero_wall_escape_top_z = zero_wall_escape_bottom_z + 2
zero_wall_outer_sky_track_bottom_z = zero_wall_escape_top_z + moving_clearance
zero_wall_outer_sky_track_top_z = zero_wall_outer_sky_track_bottom_z + outer_track_height
zero_wall_inner_sky_track_top_z = zero_wall_outer_sky_track_top_z + vert_clearance
zero_wall_top_over_track_z = zero_wall_inner_sky_track_top_z + 2
zero_wall_right_top_z = p_extension_bottom_z - moving_clearance
zero_wall_right_top_2_z = zero_wall_right_top_z - wall_thickness
one_wall_left_top_z = zero_wall_right_top_2_z - moving_clearance
one_wall_left_top_2_z = one_wall_left_top_z - 2
lock_spiral_bottom_z = l_extension_bottom_z
lock_spiral_in_bottom_z = lock_spiral_bottom_z + lock_spiral_thickness
lock_spiral_in_top_z = lock_spiral_in_bottom_z + 2 * lock_clearance + wall_thickness
lock_spiral_top_z = lock_spiral_in_top_z + lock_spiral_thickness
lock_spiral_back_x = l_left_length
lock_distance = l_top_z - l_extension_bottom_z + wall_thickness + 2 * lock_clearance
lock_bar_x = -l_movement_x - spacing
lock_bar_x_right = spacing * copies
lock_bar_y = lock_spiral_back_x + lock_spiral_length - lock_spiral_thickness - p_movement - 1
box_x = lock_bar_x_right - horz_clearance - box_thickness
box_y = l_left_length - wall_thickness / 2 - box_length / 2 + 5
lock_bar_bottom_z = lock_spiral_in_bottom_z + lock_clearance
lock_bar_top_z = lock_bar_bottom_z + wall_thickness
text_y = zero_wall_offset_y - wall_thickness - margin
text_size = 8
def square_corners(a, b):
a = np.array(a)
b = np.array(b)
min_ = np.minimum(a, b)
max_ = np.maximum(a, b)
return s2.square(max_ - min_).translate(min_)
def cube_corners(a, b):
a = np.array(a)
b = np.array(b)
min_ = np.minimum(a, b)
max_ = np.maximum(a, b)
return s2.cube(max_ - min_).translate(min_)
def extrude_bottom_top(shape, bottom, top):
return shape.linear_extrude(top - bottom).up(bottom)
def array(shape, num_copies, iter_func):
transformed = shape
result = shape
for i in range(num_copies - 1):
transformed = iter_func(transformed)
result = result + transformed
return result
p_profile = s2.polygon([(0, -p_length),
(wall_thickness, -p_length),
(wall_thickness, -p_width),
(p_width, -p_width),
(p_width, 0),
(0, p_width)])
l_profile = s2.polygon([(0, l_left_length),
(0, 0),
(p_width, l_right_back_y),
(p_width, l_right_front_y),
(wall_thickness, l_right_front_y + (p_width - wall_thickness)),
(wall_thickness, l_left_length)])
l_track_rect = s2.square(p_width * sqrt(8), wall_thickness - 2 * track_inset, True).rotateZ(-45).translate(p_width / 2, l_right_front_y / 2, 0)
l_blocker_track_min_coords = (p_width / 2 - l_blocker_track_width_x / 2, l_right_front_y + p_width / 2 + l_blocker_track_width_x / 2)
l_blocker_rect = s2.square(p_width * sqrt(8), l_blocker_thickness, True).rotateZ(-45).translate(p_width / 2, l_right_front_y + p_width / 2 - l_blocker_thickness * sqrt(2) / 2, 0)
l_blocker_track_profile = l_blocker_rect.offset(delta=moving_clearance) * s2.square(l_blocker_track_width_x, p_length + l_left_length).translate(l_blocker_track_margin, -p_length, 0)
l_blocker_profile = l_blocker_rect * s2.square(l_blocker_width_x, p_length + l_left_length).translate(l_blocker_track_margin + l_movement_x, -p_length, 0)
# Base
base = cube_corners((-margin - spacing + 4, margin, bottom_z),
(spacing + margin, text_y - text_size - margin, base_top_z))
base_minus = cube_corners((-moving_clearance, horz_clearance, bottom_z - diff_buffer),
(wall_thickness + moving_clearance, -p_outer_track_length, p_outer_track_bottom_z))
base_minus += cube_corners((track_inset - horz_clearance, horz_clearance, p_outer_track_bottom_z - diff_buffer),
(wall_thickness - track_inset + horz_clearance, -p_outer_track_length, base_top_z + diff_buffer))
# Blocker
blocker_profile = s2.polygon([(p_width - blocker_width, -(p_width - blocker_width + moving_clearance * sqrt(2))),
(p_width - blocker_width, -(p_width - blocker_width + moving_clearance * sqrt(2) + blocker_length)),
(p_width, -(p_width + moving_clearance * sqrt(2) + blocker_length)),
(p_width, -(p_width + moving_clearance * sqrt(2)))])
blocker = extrude_bottom_top(blocker_profile, base_top_z - diff_buffer, blocker_z)
# P
p_part = cube_corners((0, 0, p_bottom_z),
(wall_thickness, -p_inner_track_length, p_inner_track_bottom_z))
p_part += cube_corners((track_inset, 0, p_inner_track_bottom_z - diff_buffer),
(wall_thickness - track_inset, -p_inner_track_length, p_extension_bottom_z + diff_buffer))
#p_part += cube_corners((0, 0, p_inner_track_top_z),
# (wall_thickness, -p_inner_track_length, p_extension_bottom_z + diff_buffer))
p_part += extrude_bottom_top(p_profile, p_extension_bottom_z, p_tip_top_z)
p_minus_profile = l_track_rect.offset(delta=track_inset + l_track_moving_clearance) + l_profile
p_minus = extrude_bottom_top(p_minus_profile, p_extension_bottom_2_z, p_extension_top_2_z)
p_minus += extrude_bottom_top(l_track_rect.offset(delta=track_inset + l_track_moving_clearance), l_bottom_z - moving_clearance, p_extension_bottom_2_z + diff_buffer)
p_minus += extrude_bottom_top(l_blocker_track_profile, l_top_z, p_tip_top_z + diff_buffer)
p_part -= p_minus
l_outer_track_profile = p_profile - l_track_rect.offset(delta=horz_clearance)
p_part += extrude_bottom_top(l_outer_track_profile, l_outer_track_bottom_z, l_outer_track_top_z)
p_bottom_support_profile = p_profile * s2.polygon([(0, l_left_length),
(0, l_right_front_y + moving_clearance * sqrt(2) - wall_thickness),
#(wall_thickness, l_right_front_y + moving_clearance * sqrt(2)),
(wall_thickness + (p_width - wall_thickness) / 2, l_right_front_y + moving_clearance * sqrt(2) + (p_width - wall_thickness) / 2),
(p_width, l_right_front_y + moving_clearance * sqrt(2)),
(p_width, l_left_length)])
p_part += extrude_bottom_top(p_bottom_support_profile, p_extension_bottom_z, p_extension_bottom_2_z)
p_part += cube_corners((p_width, 0, p_extension_bottom_z),
(p_width + extra_width, l_right_front_y + moving_clearance * sqrt(2), p_extension_top_z))
p_1_hole = cube_corners((-diff_buffer, zero_wall_offset_y + moving_clearance, one_wall_bottom_z - support_clearance / 2),
(wall_thickness + diff_buffer, zero_wall_offset_y - wall_thickness - moving_clearance, p_tip_top_z + diff_buffer))
p_part = (p_part.back(p_movement) - p_1_hole).forward(p_movement)
# Sky track of P
#p_part += cube_corners((0, 0, p_tip_top_z),
# (wall_thickness, -p_inner_track_length, p_inner_sky_track_bottom_z))
p_part += cube_corners((track_inset, 0, p_tip_top_z),
(wall_thickness - track_inset, -p_inner_track_length, p_inner_sky_track_top_z + diff_buffer))
p_part += cube_corners((0, 0, p_inner_sky_track_top_z),
(wall_thickness, -p_inner_track_length, p_top_over_track_z))
p_sky_track_profile = square_corners((track_inset - horz_clearance, horz_clearance),
(wall_thickness - track_inset + horz_clearance, -p_outer_track_length))
p_sky_track_profile = p_sky_track_profile.offset(delta=sky_track_thickness) - p_sky_track_profile
p_sky_track_profile += square_corners((wall_thickness / 2 - spacing, horz_clearance + sky_track_thickness),
(wall_thickness / 2 + spacing, horz_clearance))
if zero_one_walls_exist:
pass
#p_sky_track_profile += square_corners((wall_thickness / 2 - sky_track_thickness / 2, -p_outer_track_length),
# (wall_thickness / 2 + sky_track_thickness / 2, zero_wall_offset_y + track_inset - horz_clearance))
else:
p_sky_track_profile += square_corners((wall_thickness / 2 - spacing, -p_outer_track_length - sky_track_thickness),
(wall_thickness / 2 + spacing, -p_outer_track_length))
p_part += extrude_bottom_top(p_sky_track_profile, p_outer_sky_track_bottom_z, p_outer_sky_track_top_z)
if zero_one_walls_exist:
p_part += cube_corners((wall_thickness / 2 - sky_track_thickness / 2, -p_outer_track_length, zero_wall_outer_sky_track_bottom_z),
(wall_thickness / 2 + sky_track_thickness / 2, zero_wall_offset_y + track_inset - horz_clearance, zero_wall_outer_sky_track_top_z))
p_part += cube_corners((wall_thickness / 2 - sky_track_thickness / 2, -p_outer_track_length, p_outer_sky_track_bottom_z),
(wall_thickness / 2 + sky_track_thickness / 2, -p_outer_track_length - sky_track_thickness, zero_wall_outer_sky_track_top_z))
sky_supports = cube_corners((wall_thickness / 2 - spacing, horz_clearance + sky_track_thickness, base_top_z),
(wall_thickness / 2 - spacing + sky_track_thickness, horz_clearance, p_outer_sky_track_top_z))
sky_supports += sky_supports.back(p_outer_track_length + horz_clearance + sky_track_thickness)
sky_supports += sky_supports.right(2 * spacing - sky_track_thickness + spacing * (copies - 1))
if zero_one_walls_exist:
back_sky_supports = cube_corners((wall_thickness / 2 - spacing, -p_outer_track_length, p_outer_sky_track_bottom_z),
(wall_thickness / 2, -p_outer_track_length - sky_track_thickness, p_outer_sky_track_top_z))
back_sky_supports += cube_corners((wall_thickness / 2 + spacing * (copies - 1), -p_outer_track_length, p_outer_sky_track_bottom_z),
(wall_thickness / 2 + spacing * copies, -p_outer_track_length - sky_track_thickness, p_outer_sky_track_top_z))
sky_supports += back_sky_supports
# L
l_part = extrude_bottom_top(l_profile, l_bottom_z, l_top_z)
l_minus = cube_corners((-diff_buffer, l_left_length + diff_buffer, l_bottom_z - diff_buffer),
(wall_thickness, -p_length, l_extension_bottom_z))
l_part -= l_minus
l_bottom_antisupport_profile = p_bottom_support_profile + s2.square(p_width, l_left_length + p_width).translate(-p_width + diff_buffer, l_right_front_y + moving_clearance * sqrt(2) - wall_thickness, 0)
l_minus = extrude_bottom_top(l_bottom_antisupport_profile, l_bottom_2_z, l_extension_bottom_z)
l_part = (l_part.translate(-l_movement_x, l_movement_x, 0) - l_minus).translate(l_movement_x, -l_movement_x, 0)
l_track_minus = l_track_rect.offset(delta=track_inset - l_right_front_y / sqrt(2) + l_track_moving_clearance) - l_track_rect
l_track_minus = extrude_bottom_top(l_track_minus, l_inner_track_bottom_z, l_inner_track_top_z)
l_part -= l_track_minus
l_part += extrude_bottom_top(l_blocker_profile, l_top_z - diff_buffer, p_extension_top_z)
# (Lock spiral)
l_part += cube_corners((0, lock_spiral_back_x + lock_spiral_length, lock_spiral_bottom_z),
(wall_thickness, lock_spiral_back_x, lock_spiral_in_bottom_z))
l_part += cube_corners((0, lock_spiral_back_x + lock_spiral_length, lock_spiral_bottom_z),
(wall_thickness, lock_spiral_back_x + lock_spiral_length - lock_spiral_thickness, lock_spiral_top_z))
l_part += cube_corners((0, lock_spiral_back_x + lock_spiral_length, lock_spiral_in_top_z),
(wall_thickness, lock_spiral_back_x + lock_spiral_length - lock_spiral_top_length, lock_spiral_top_z))
#l_part = l_part.translate(-vert_clearance / sqrt(2), vert_clearance / sqrt(2), 0)
l_part = l_part.translate(-l_movement_x / 2, l_movement_x / 2, 0)
#l_part = l_part.translate(-l_movement_x / 2, l_movement_x / 2, 0)
# Zero wall
zero_wall = cube_corners((0, zero_wall_offset_y, zero_wall_bottom_z),
(zero_wall_width, zero_wall_offset_y - wall_thickness, zero_wall_top_z))
zero_wall += cube_corners((zero_wall_width - wall_thickness, zero_wall_offset_y, zero_wall_right_top_2_z),
(zero_wall_width, zero_wall_offset_y - wall_thickness, zero_wall_top_z))
zero_wall += cube_corners((zero_wall_width - wall_thickness, zero_wall_offset_y, zero_wall_right_top_2_z),
(zero_wall_inner_track_x + zero_wall_inner_track_length, zero_wall_offset_y - wall_thickness, zero_wall_right_top_z))
#zero_wall += cube_corners((zero_wall_inner_track_x, zero_wall_offset_y, p_inner_track_top_z),
# (zero_wall_inner_track_x + zero_wall_inner_track_length, zero_wall_offset_y - wall_thickness, zero_wall_right_top_z))
zero_wall += cube_corners((zero_wall_inner_track_x, zero_wall_offset_y - track_inset, p_inner_track_bottom_z),
(zero_wall_inner_track_x + zero_wall_inner_track_length, zero_wall_offset_y - wall_thickness + track_inset, zero_wall_right_top_z))
zero_wall += cube_corners((zero_wall_inner_track_x, zero_wall_offset_y, p_bottom_z),
(zero_wall_inner_track_x + zero_wall_inner_track_length, zero_wall_offset_y - wall_thickness, p_inner_track_bottom_z))
zero_wall_escape = cube_corners((zero_wall_inner_track_x, zero_wall_offset_y, zero_wall_right_top_2_z),
(zero_wall_inner_track_x + wall_thickness, zero_wall_escape_y - wall_thickness, zero_wall_right_top_z))
zero_wall_escape += cube_corners((zero_wall_inner_track_x, zero_wall_offset_y, zero_wall_escape_bottom_z),
(zero_wall_inner_track_x + wall_thickness, zero_wall_escape_y - wall_thickness, zero_wall_escape_top_z))
zero_wall_escape += cube_corners((zero_wall_inner_track_x, zero_wall_escape_y, zero_wall_right_top_2_z),
(zero_wall_inner_track_x + wall_thickness, zero_wall_escape_y - wall_thickness, zero_wall_escape_top_z))
zero_wall += zero_wall_escape.translateX(zero_wall_inner_track_length / 2 - wall_thickness / 2)
# (Sky track)
zero_wall += cube_corners((zero_wall_inner_track_x, zero_wall_offset_y, zero_wall_inner_sky_track_top_z),
(zero_wall_inner_track_x + zero_wall_inner_track_length, zero_wall_offset_y - wall_thickness, zero_wall_top_over_track_z))
zero_wall += cube_corners((zero_wall_inner_track_x, zero_wall_offset_y - track_inset, zero_wall_escape_bottom_z),
(zero_wall_inner_track_x + zero_wall_inner_track_length, zero_wall_offset_y - wall_thickness + track_inset, zero_wall_inner_sky_track_top_z))
zero_wall_sky_track_profile = square_corners((-horz_clearance, zero_wall_offset_y - track_inset + horz_clearance),
(zero_wall_inner_track_x + zero_wall_inner_track_length + horz_clearance, zero_wall_offset_y - wall_thickness + track_inset - horz_clearance))
zero_wall_sky_track_profile = zero_wall_sky_track_profile.offset(delta=sky_track_thickness) - zero_wall_sky_track_profile
zero_wall += extrude_bottom_top(zero_wall_sky_track_profile, zero_wall_outer_sky_track_bottom_z, zero_wall_outer_sky_track_top_z)
zero_wall = zero_wall.translateX(zero_one_wall_offset_x)
zero_wall_base_minus = cube_corners((-horz_clearance, zero_wall_offset_y + moving_clearance, bottom_z - diff_buffer),
(zero_wall_inner_track_x + zero_wall_inner_track_length + horz_clearance, zero_wall_offset_y - wall_thickness - moving_clearance, p_outer_track_bottom_z))
zero_wall_base_minus += cube_corners((-horz_clearance, zero_wall_offset_y - track_inset + horz_clearance, p_outer_track_bottom_z - diff_buffer),
(zero_wall_inner_track_x + zero_wall_inner_track_length + horz_clearance, zero_wall_offset_y - wall_thickness + track_inset - horz_clearance, base_top_z + diff_buffer))
zero_wall_base_minus = zero_wall_base_minus.translateX(zero_one_wall_offset_x)
# One wall
one_wall = cube_corners((one_wall_x, zero_wall_offset_y, one_wall_bottom_z),
(one_wall_x + one_wall_width, zero_wall_offset_y - wall_thickness, one_wall_top_z))
one_wall += cube_corners((one_wall_x, zero_wall_offset_y, one_wall_bottom_z),
(one_wall_x + wall_thickness, zero_wall_offset_y - wall_thickness, one_wall_escape_top_z))
one_wall += cube_corners((-wall_thickness, zero_wall_offset_y, one_wall_escape_bottom_z),
(one_wall_x + wall_thickness, zero_wall_offset_y - wall_thickness, one_wall_escape_top_z))
one_wall += cube_corners((-wall_thickness, zero_wall_offset_y, one_wall_escape_bottom_z),
(0, zero_wall_offset_y - wall_thickness - wall_thickness - moving_clearance, one_wall_escape_top_z))
one_wall += cube_corners((-wall_thickness, zero_wall_offset_y - wall_thickness - moving_clearance, one_wall_left_top_2_z),
(0, zero_wall_offset_y - wall_thickness - wall_thickness - moving_clearance, one_wall_escape_top_z))
one_wall += cube_corners((-wall_thickness, zero_wall_offset_y, one_wall_left_top_2_z),
(0, zero_wall_offset_y - wall_thickness - wall_thickness - moving_clearance, one_wall_left_top_z))
one_wall += cube_corners((-wall_thickness, zero_wall_offset_y, one_wall_left_top_2_z),
(zero_wall_inner_track_length, zero_wall_offset_y - wall_thickness, one_wall_left_top_z))
# (Track)
#one_wall += cube_corners((0, zero_wall_offset_y, p_inner_track_top_z),
# (zero_wall_inner_track_length, zero_wall_offset_y - wall_thickness, one_wall_left_top_z))
one_wall += cube_corners((0, zero_wall_offset_y - track_inset, p_inner_track_bottom_z),
(zero_wall_inner_track_length, zero_wall_offset_y - wall_thickness + track_inset, one_wall_left_top_z))
one_wall += cube_corners((0, zero_wall_offset_y, p_bottom_z),
(zero_wall_inner_track_length, zero_wall_offset_y - wall_thickness, p_inner_track_bottom_z))
# (Sky track)
one_wall += cube_corners((0, zero_wall_offset_y, zero_wall_inner_sky_track_top_z),
(zero_wall_inner_track_length, zero_wall_offset_y - wall_thickness, zero_wall_top_over_track_z))
one_wall += cube_corners((0, zero_wall_offset_y - track_inset, one_wall_escape_bottom_z),
(zero_wall_inner_track_length, zero_wall_offset_y - wall_thickness + track_inset, zero_wall_inner_sky_track_top_z))
#one_wall += cube_corners((0, zero_wall_offset_y, one_wall_escape_bottom_z),
# (zero_wall_inner_track_length, zero_wall_offset_y - wall_thickness, p_inner_sky_track_bottom_z))
one_wall = one_wall.translateX(zero_one_wall_offset_x)
# Lock
lock_bar = cube_corners((lock_bar_x, lock_bar_y, lock_bar_bottom_z),
(lock_bar_x_right, lock_bar_y - wall_thickness, lock_bar_top_z))
lock_bar += s2.cube(box_thickness, box_length - 2 * (box_thickness + horz_clearance), box_height - box_thickness).translate(box_x + box_thickness + horz_clearance, box_y + box_thickness + horz_clearance, base_top_z + box_thickness)
lock_bar = lock_bar.up(lock_distance)
# Box
box = s2.cube(box_width, box_length, box_height).translate(box_x, box_y, base_top_z)
box -= s2.cube(box_width - 4 * box_thickness - 2 * horz_clearance, box_length - 2 * box_thickness, box_height).translate(box_x + 3 * box_thickness + 2 * horz_clearance, box_y + box_thickness, base_top_z + box_thickness)
box -= s2.cube(box_width - box_thickness, box_length - 2 * box_tab_inset, box_height).translate(box_x, box_y + box_tab_inset, base_top_z + box_thickness)
box -= s2.cube(box_thickness + 2 * horz_clearance, box_length - 2 * box_thickness, box_height).translate(box_x + box_thickness, box_y + box_thickness, base_top_z + box_thickness)
box += s2.cube(box_width - 2 * (box_thickness + horz_clearance), box_length, box_thickness).translate(box_x + 2 * (box_thickness + horz_clearance), box_y, base_top_z + box_height - box_thickness)
# Box base
box += s2.cube(box_width + 2 * margin, box_length + 2 * margin, base_top_z - bottom_z).translate(box_x - margin, box_y - margin, bottom_z)
# Text
text_height = 2
text = s2.text("カラクリ錠 2号", text_size * 0.9, "Noto Sans CJK JP:style=Bold", "center", "top", spacing=1) \
.linear_extrude(text_height).translate(spacing * copies / 2 - spacing / 2, text_y, base_top_z - text_height)
spacing_func = lambda s: s.translateX(spacing)
base = array(base, copies, spacing_func)
base_minus = array(base_minus, copies, spacing_func)
base -= base_minus
if zero_one_walls_exist:
base -= zero_wall_base_minus
model = blocker + p_part + l_part
model = array(model, copies, spacing_func)
model += base + sky_supports + lock_bar + box
if zero_one_walls_exist:
model += zero_wall + one_wall
model -= text
model.save_as_scad("Power Lock.scad")