tl;dr Laser cutting is fun; I wanted to make a dollhouse for my daughter so I made a script that makes it really easy to fingerjoint 3D models (analogous to 3D printing slicers). Open-sourcing the software (Plycutter) today. No algorithms here, head on to Plycutter’s pages for that side of the story…
Some years back, my local hackerspace (Helsinki Hacklab) got a laser cutter.
Before long, I was using the laser cutter to prototype and make a number things for work and home – the work stuff is confidential but an example of home stuff is at https://hackaday.io/project/167032-dishwasher-rack-for-bottle-teats and in the image gallery below (the drawings used for laser carving on the puzzles are courtesy of my SO).
The thing about laser cutting is: it’s easy and fast. There is no figuring out how to hold a part (as in milling). There is no long set-up time, just put a sheet of material into the machine and go. There is no waiting for hours or days (a la 3D printing).
Of course, there are limitations. Making 3D shapes requires careful design in CAD beforehand, as opposed to 3D printing where you basically draw a 3D shape and print it.
Fast forward to spring 2020 and suddenly there is a new requirement at home from the SO-daughter caucus: a dollhouse. Building it with the laser cutter seems obvious but at the same time, creating all the joints by hand in Fusion 360 seemed dauntingly tedious and error-prone. So…
...the computer should take care of it! Generating geometry for laser cutting or other CAM has been something I have been dying to try for some time and this seems like the perfect opportunity!
And that, dear reader, is how epic hobby programming projects get started.
After looking into making plugins for various CAD software I had access to and being disappointed by the access allowed by the APIs and various other factors, I settled on the following approach:
A stand-alone python program that takes in an STL file and outputs DXF that the laser printer driver software can directly read. The STL file will already be designed in terms of sheet surfaces that the program will autodetect but joints are just solid joins. (see below for examples)
Even now, when releasing Plycutter, this is still its “product definition”.
Basically, this is analogous to a 3D printer slicer *except* that the slices are not all horizontal and there needs to be some figuring out of how to actually manage the joints, especially when more than two sheets meet.
The first iteration of the script didn’t take too long to write. The geometry is fairly simple: on a 3-axis machine, any two sheets determine the direction and location of the fingers between them almost completely, leaving only one degree of freedom along the joint.
So, I modeled a weird irregular hexahedron in Fusion and used the ‘shell’ command to make it hollow and have the sheet structure I wanted. I particularly wanted to have non-90-degree angles to see how they come out (manual modeling something like that would be extra nasty and especially hard to change if you had to change the model). I left two sides out of the ‘shell’ just to have a kind of small hut and to see the structure better. And there I had the first model I wanted to ‘print’ with my laser cutter.
And so I ran my script. And debugged for a while.
After some iterations, the .dxf file started to look right. Off to the hacklab to start cutting -and you can see the result below!
Having this object in my hands, feeling the potential of being able to make a physical object like this in so little time (Fusion modeling was just a few minutes, the simple script ran in seconds and the laser cutter took only a few minutes) – it’s hard to describe the feeling. Wow!
Along the way, I noticed that the objects created by Plycutter are surprisingly fun to assemble. From the early “stool / boat” test object…
…to some geometric demos (at some point I realized that it makes sense to make the joints random to ensure correct assembly)…
the main result was: this is fun!
Testing, testing, more testing
If you have any experience with software development, you can probably predict what happens next. 1) there are lots of new features and edge case fixes that need to be added quickly, 2) they make things more complex, 3) improving the behaviour in one case starts breaking other cases.
At that point, the exploration stage is over and the right thing to do is to get more systematic and start testing to avoid regressions.
I wrote both fixed test scripts and property-based tests with randomly generated test objects from some limited sets. The results got better and I started being able to rely on the results.
I was not ready to make the dollhouse yet. But we could get started on some of the furnishings. I also tried 3D printing on some of the furniture; it was ok but my personal user experience with Plycutter was much better due to the ability to quickly iterate on designs and not having to wait for the printer. And I prefer wood as a material.
Rewrite using persistent data structures
However, the original hacky code was getting unwieldy for adding new features. It was time to put in some real design.
I rewrote the heuristic pipeline in a functional programming style: each step of the heuristics pipeline is a function that takes the current “idea” of the final structure and returns a modified version. Little by little the steps make decisions and at each point, it is possible to see what has been decided and to perform a number of sanity checks on the intermediate step.
This made developing and testing individual steps in jupyterlab (my go-to environment for interactive coding) a breeze.
Useful level achieved!
With Plycutter in my arsenal, I was able to quickly make fun DIY projects for home (of course, “if you have a hammer, everything looks like a nail” applies as well).
I was getting ready to publish the first version when my randomized property-based tester started hitting on a particular type of bug quite often: the 2D library I was using at the time was sensitive to float round-off errors, likely due to the well-known problem of triangle orientation flipping because of rounding errors.
Trying to work around it would have meant having to walk on thin ice all the time – even if I worked around it now, the property-based testing was sure to find another way to exploit that hole at some point – so it had to go. An exact 2D geometry library is what the situation called for, as it eliminates a whole class of test failures.
After a couple of attempts at integrating other geometric libraries I decided to write my own – “how hard can it be”. Facepalm…
In the end, it took me several weeks to come up with a working version of the 2D operations I needed using exact rational arithmetic. Making those algorithms robust took quite a bit more property-based testing but in the end I did get there. The code tries to put simplicity over speed in many places but still fails to be simple in many places. And is much slower than I’m comfortable with (more speed would also speed up testing plycutter itself). Luckily (well, because I decided to very strictly do so) the 2D code is still hidden behind a fairly narrow API from plycutter itself so replacing it is still possible. (Not all of the property-based tests of Plycutter are being published today due to this performance issue)
After a lot of work on making the script work, and lot of design work on the dollhouse, I was ready to cut it. Before doing so, I modularized it into separate floors, just in case I would have to change something or fix something and didn’t want to reprint absolutely everything.
Lessons learned and what’s next
In the end, making the dollhouse took a bit longer than the one evening of scripting I was thinking it might take. Honestly, if all I wanted was to make the dollhouse, there would have been faster ways. But I did have ancillary goals: to learn about generating physical objects with programs; to equip myself with a powerful tool, and to have fun. All those goals were amply fulfilled by this project.
I can honestly say that without hypothesis, the Python package implementing property-based testing, this project would not have succeeded. Most of the corner-case bugs were uncovered by property-based testing; discovering them in “production” and fixing them one by one would very likely have resulted in new bugs.
The functional programming approach to the heuristic pipeline steps was also a huge success and it is an architectural pattern I plan to keep in my arsenal for future endeavors.