Python Behind-the-Scenes

Generate custom 2D patterns for papercraft, lasercutter, or wood

This website is actually a web interface for the RoCo python library. If you are comfortable programming in python, you can also create your own designs for different robots / objects.

Example

Here's an example, creating a six-sided die out of paper. How could you write a program to describe a six-sided die?

The first step is to draw out the flattened shape on paper. For a die we are familiar with the shape (ignore numbers - they're for reference later).

Now how do we tell a computer to do it? Here's one way of thinking about it:

  • We have six squares
  • We can write down which edges connect to which edges when folded
  • We also write down the angle between edges when folded
1. Create Squares

For creating the squares, it turns out there are pre-built components we can use. We can use the Rectangle component, which is pre-specified to have parameters length and width, as well interfaces on the top/right/bottom/left sides.

We will create our component and start by adding six squares.

    from rocolib.api.component import Component
    c = Component()

    c.addSubcomponent("face1","Rectangle")
    ...  # same for faces 3, 4, 5
    c.addSubcomponent("face6","Rectangle")

To make a square, we'll set the width and length to be equal. We'll add a custom parameter called length which we set to 50mm.

    c.addParameter("length", 50)

And then use addConstraint to constrain l and w to both equal our custom length parameter.

    c.addConstraint(("face1", "l"), "length")
    c.addConstraint(("face1", "w"), "length")

    c.addConstraint(("face2", "l"), "length")
    c.addConstraint(("face2", "w"), "length")
    ...  # same for faces 3, 4, 5
    c.addConstraint(("face6", "l"), "length")
    c.addConstraint(("face6", "w"), "length")
2A. Connect Edges (also specify angle when folded)

Next we will specify which edges connect to which edges, and the angle they connect at when folded. For instance the top of face1 should connect to the bottom of face2.

Just from the 2D picture we can start out with five such connections (highlighted in orange below).

die instructions

We can use the addConnection function to do so. We want the top edge of face1 to connect to the bottom edge of face2. These are the t and b interfaces respectively.

    c.addConnection(("face1", "t"), ("face2", "b"), angle=-90)

We also specified an angle parameter - it's a 90 degree fold, and the negative indicates a mountain fold (positive would be a valley fold).

So we have five total connections.

    c.addConnection(("face1", "t"), ("face2", "b"), angle=-90)
    c.addConnection(("face1", "l"), ("face3", "r"), angle=-90)
    c.addConnection(("face1", "r"), ("face4", "l"), angle=-90)
    c.addConnection(("face1", "b"), ("face5", "t"), angle=-90)
    c.addConnection(("face5", "b"), ("face6", "t"), angle=-90)
2B. Connect More Edges, now with Tabs

Now we can think about the edges which are not connected in our 2D picture but are connected in 3D. If you count the green, blue, and black arrows below, you'll see there are seven such connections.

die instructions

When we're physically folding this together, we'll have to attach these edges somehow, e.g. via glue or tape. The RoCo library can actually automatically generate tabs for us! We just have to specify a tabWidth (note that tabs under 10mm hard to use).

We create the seven connections like so:

    c.addConnection(("face5", "l"), ("face3", "b"), angle=-90, tabWidth=10)
    c.addConnection(("face3", "t"), ("face2", "l"), angle=-90, tabWidth=10)
    c.addConnection(("face2", "r"), ("face4", "t"), angle=-90, tabWidth=10)
    c.addConnection(("face4", "b"), ("face5", "r"), angle=-90, tabWidth=10)

    c.addConnection(("face6", "l"), ("face3", "l"), angle=-90, tabWidth=10)
    c.addConnection(("face6", "b"), ("face2", "t"), angle=-90, tabWidth=10)
    c.addConnection(("face6", "r"), ("face4", "r"), angle=-90, tabWidth=10)
3. Presto!

Now we can use RoCo to generate our .stl 3d preview as well as our 2D .svg and .dxf files for paper or lasercutter.

    c.makeOutput("output/die50mm", tree=True, display=False)

3d die

The tabs go into the slots (the gray lines) to hold the cube together.

The 3D file STL can be opened in applications such as MeshLab or Cura, here I used OpenSCAD to open it and take a screenshot.

3d die

Pretty cool huh? :)

4. Extra Credit

We can also save our Die component for later use (e.g. as subcomponent for something else) to the internal library which Roco automatically checks for.

    c.toLibrary("Die")

After that, we can use our Die component anytime we want!

    from rocolib.library import getComponent
    d = getComponent("Die", length=65)

For fun, we can also see how the Rectangle subcomponents combined to make our new Die component, in the tree.png file.

3d die

5. More Examples

For more examples, the source code for the components in this website can be found under the "library" folder. Some of the files in that folder are also generated using the scripts in the "builders" folder.

You can run the file like so (after installing the library):

$ cat test_stool.py
from rocolib.library import getComponent
    f = getComponent('Stool')
    f.makeOutput('tests/pytest_output/Stool', display=False,
                 thickness=3) # 0.05mm for paper, 3mm for plywood 

$ python test_stool.py 

Appendix: Full Python file for creating die

The full file may be found at: die.py or copied in full below:

    from rocolib.api.component import Component
    # We're making a component
    c = Component()

    # ... 6 repeated squares
    c.addSubcomponent("face1","Rectangle")
    c.addSubcomponent("face2","Rectangle")
    c.addSubcomponent("face3","Rectangle")
    c.addSubcomponent("face4","Rectangle")
    c.addSubcomponent("face5","Rectangle")
    c.addSubcomponent("face6","Rectangle")

    # I pick one parameter, which is the lenght square side
    # let's say default = 50mm

    c.addParameter("length", 50)
    c.addParameter("tabWidth")

    # We need to constrain all parameters of all subcomponents to be functions of the parameter of our new component

    c.addConstraint(("face1", "l"), "length")
    c.addConstraint(("face1", "w"), "length")

    c.addConstraint(("face2", "l"), "length")
    c.addConstraint(("face2", "w"), "length")

    c.addConstraint(("face3", "l"), "length")
    c.addConstraint(("face3", "w"), "length")

    c.addConstraint(("face4", "l"), "length")
    c.addConstraint(("face4", "w"), "length")

    c.addConstraint(("face5", "l"), "length")
    c.addConstraint(("face5", "w"), "length")

    c.addConstraint(("face6", "l"), "length")
    c.addConstraint(("face6", "w"), "length")

    # pick out matching edges
    # (b2-c4) should fold at 90degrees (mountain fold, same for all folds)

    c.addConnection(("face1", "t"), ("face2", "b"), angle=-90)
    c.addConnection(("face1", "l"), ("face3", "r"), angle=-90)
    c.addConnection(("face1", "r"), ("face4", "l"), angle=-90)
    c.addConnection(("face1", "b"), ("face5", "t"), angle=-90)
    c.addConnection(("face5", "b"), ("face6", "t"), angle=-90)

    # in the folded state, (xx-yy) edges should be connected
    # We'll hold the edges together with tab/slot connections
    # The first argument will have a 10mm tab, the second will have the slot

    c.addConnection(("face5", "l"), ("face3", "b"), angle=-90, tabWidth=10)
    c.addConnection(("face3", "t"), ("face2", "l"), angle=-90, tabWidth=10)
    c.addConnection(("face2", "r"), ("face4", "t"), angle=-90, tabWidth=10)
    c.addConnection(("face4", "b"), ("face5", "r"), angle=-90, tabWidth=10)

    c.addConnection(("face6", "l"), ("face3", "l"), angle=-90, tabWidth=10)
    c.addConnection(("face6", "b"), ("face2", "t"), angle=-90, tabWidth=10)
    c.addConnection(("face6", "r"), ("face4", "r"), angle=-90, tabWidth=10)

    # And we're done!  We can make it right here right now, and the design files will end up in ./output/die/*

    c.makeOutput("output/die50mm", tree=True, display=False)

    # Or we can save it to our library to be used later
    c.toLibrary("Die")

    ### Now in another file we can make a larger die from our saved component 

    from rocolib.library import getComponent
    d = getComponent("Die", length=65)
    d.makeOutput("output/die65mm", tree=True, display=False)