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.

Early plans for a 3D H
Early plans for a 3D H hinge assembly. On the right side is an attempt to unfold it satisfying certain constraints.

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).

66 slabs
66 slabs modelled in Blender. Click for the STL file.
66 slabs, with time shown in Prusa
66 slabs sliced in PrusaSlicer, with the wrong nozzle size (0.25mm). The total time is 5 hours 33 minutes.

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.

The clearance design rule from the group project. 0.1mm is definitely not enough. 0.2mm is barely enough but there's so much friction. 0.3mm is enough but I went with 0.4mm to be safe.

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.

Hinge test 1
Hinge test 1 made in Blender. Note the chamfered rings and the tiny connection between the bar and the slab it's connected to.

Alas, the horizontal one worked and the vertical one didn’t. The connection between the bar and the slab was too weak.

Result of the horizontal hinge. A success.
Vertical hinge result
Result of the vertical hinge. An unexpected failure.

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.

Hinge test 2
Hinge test 2 made in Blender. Note the un-chamfered rings and the stronger connection between the bar and the slab it's connected to.

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°.

H hinge assembly
The H hinge assembly.
Folded H hinge assembly
The folded version. Folding is necessary because slab clearance is 0mm otherwise. (and also because the length is too big otherwise)

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…

Result with supports
The result, with supports still intact. Suspense.
Folded H hinge assembly
The result after all supports have been removed. Oh no! Some rings broke!

├ │ 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!

Hole before filling
The hole that really shouldn't be filled in. It's so big.
Hole filled in by slicer
And the slicer filled it in anyway, with no less than 3 layers, the same as the rings! No wonder the rings broke first.

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.

Vertices that break the manifold rules
Selected vertices break the manifold rules.

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")
Hinges displayed in OpenSCAD
The hinges displayed in OpenSCAD. They better be manifolds!

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:

Hinge attempt 2
The second attempt. 2 slabs broke off from the main assembly...

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.

Hinge attempt 3
The third attempt. Oh no! Some slabs are stuck together!

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:

Hinge attempt 4
The fourth attempt. Finally, the hinge assembly works.

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:

Linear track schematic
Linear track cross section. Lots of parameters. There's also inner track length, which is perpendicular to the cross section and thus can't be shown here.

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.

Linear track test in Blender
An assortment of linear tracks modelled in Blender. Click for the Blender file.
Linear track test
The printed linear tracks.

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.

Planar movement contraption in Blender
The modelled planar movement contraption. Click for the STL file.
The printed planar movement contraption, with funnel attached.

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!

Early plans for lock
Early plans for the exponential lock. Oops, got carried away and forgot that this needed to be documented for a bit.

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.

Exponential lock 1 design
The design for version 1 of the exponential lock. Double arrows indicate movement directions. The design was in OpenSCAD.

I did a test print of the latch mechanic to make sure that it worked first. Then I printed the big lock.

Testing the latch mechanic for the exponential lock
Exponential lock 1
Exponential lock 1. Most supports have not been removed because of a found bug.
Limiter breakoff
The limiter on the latch that stops the respective bit from moving if the latch isn't retracted broke off.

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.

Exponential lock 2 design
The design for version 2 of the exponential lock. Notice the skylines. Objects on linear tracks have a low and high attachment, reducing torque.
Testing the skyline. Smooth motion, no torque!
Exponential lock 2
Exponential lock 2. Supports have not been removed because lock 3 is just a better design

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.

Exponential lock 3 design
The design for version 3 of the exponential lock. Everything is low to the ground. Click for the Blender file

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.

Testing the new latch mechanic
Exponential lock 3 in action. It works!
Hook breakoff
The hook on the last bit broke off. Thankfully, easy to fix with superglue 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.

Bent lock slab
The lock slab is bent. Probably not the best model to 3D print.
Supports around the slab
The lock slab with supports around it.

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")