Jan 2025 – 12 min readFeatured: ★ Hackaday ★ Hacker News ★ Real Python Podcast
For anyone passionate about bringing digital designs into the physical world, 3D printing offers incredible possibilities. Like many creators, I’m drawn to the elegance of automation, especially when building projects. There’s a unique satisfaction in setting up a process and watching a machine execute the work. Combine this with parametric design, and the ability to iterate and customize becomes incredibly smooth. My recent speaker project perfectly illustrates this automated workflow.
During that speaker project, I encountered the limits of my 3D printer’s build volume. Creating larger speakers would have required significantly more effort in splitting the design manually. Exploring large format 3D printers revealed they are expensive and not necessarily superior to my existing setup in terms of capability.
Then, I discovered a fascinating video by Richard from RMC where he 3D printed an entire arcade machine using a network of smaller printers. His method involved designing the model and then dividing it into macro layers, each further segmented into smaller parts with dovetail joints for assembly. This approach proved remarkably effective.
Richard assembling the arcade machine on RMC. Lots of colours, great use of spare filament. It’s where I got the idea!
Could this segmentation process be automated? This question sparked an idea. In my previous speaker design article, I briefly mentioned the potential for floor-standing speakers, acknowledging the need to segment the print. If I could focus on panel-based designs, like these speakers, automating the segmentation process seemed achievable.
Having already developed a system for nesting parts onto print beds, I decided to adapt that code. The goal was to create a system that could automatically split panels into jigsaw-like pieces. Dovetail joints appeared to be the ideal solution, offering strong, glue-ready connections without requiring additional hardware like dowels.
If implemented correctly, this system would divide models with intricate geometry while maintaining a seamless final appearance. This approach opens up exciting possibilities for creating large 3D prints – truly giant things – even with desktop 3D printers.
The Dovetail Profile
Dovetail joints are a time-honored woodworking technique, valued for both their strength and aesthetic appeal. Traditionally crafted by hand or with tools like routers and CNC machines, dovetails excel at creating robust connections.
Their strength comes from the interlocking wedge shape, which tightens as the joint is pulled apart. Tapered dovetails further enhance this effect, ensuring a snug fit that’s ideal for gluing, preventing adhesive from being scraped away during assembly.
Dovetail example (from wikipedia!)
I explored existing dovetail implementations in OpenSCAD, but none quite met my specific requirements for 3D printing.
My aim was to create a system that subtracts material in a way that results in perfectly mating joints right off the printer. Ideally, this would simplify the design process, avoiding the need for complex, overlapping negative shapes for each part.
This led me to consider a thin, shell-like dovetail structure, resembling a zig-zagging ribbon – and even better if it could be tapered!
Through the magic of anachronism, this is what I had in mind
OpenSCAD, being based on Constructive Solid Geometry (CSG), isn’t inherently designed for creating thin shells. This aspect posed a slight challenge in the design process.
Deriving the Geometry
After several iterations and optimizations to refine the design and eliminate unwanted artifacts, I arrived at the following step-by-step deconstruction of the dovetail profile:
Step 1: square tooth design, with half length shoulders using polygon(). Note that x-axis is half way up tooth.
Step 2: Chamfer edges with a ratio of 1:1.4 to allow locking
Step 3: Round internal and external edges with a quad offset(), intersection to remove the unwanted roundovers
Step 4: Subtract a negative offset of itself to form an outline
Step 5: Subtract the sacrificial tab
Step 6: Extrude the individual tooth as we enter 3D
Step 7: Scale the extrusion by the same ratio (1:1.4)
Step 8: Remove the sides which interfere with the mating teeth
Interestingly, Step 8 initially used intersection, but this drastically reduced OpenSCAD’s performance, slowing rendering to a crawl.
Step 9: Iterate to join several together
Initially, I didn’t remove the interfering edges, resulting in unexpected hanging artifacts (as seen in a later screenshot). It took closer inspection of the 3D tooth design to identify and correct this issue.
Step 10: Subtract this from the work piece
Success! Note, the parts are not separable in OpenSCAD
The following code block contains the OpenSCAD code that implements the dovetail joint, with annotations corresponding to each step described above. It’s surprisingly compact, just 68 lines of code!
<span> 1 </span><span> 2 </span><span> 3 </span><span> 4 </span><span> 5 </span><span> 6 </span><span> 7 </span><span> 8 </span><span> 9 </span><span>10 </span><span>11 </span><span>12 </span><span>13 </span><span>14 </span><span>15 </span><span>16 </span><span>17 </span><span>18 </span><span>19 </span><span>20 </span><span>21 </span><span>22 </span><span>23 </span><span>24 </span><span>25 </span><span>26 </span><span>27 </span><span>28 </span><span>29 </span><span>30 </span><span>31 </span><span>32 </span><span>33 </span><span>34 </span><span>35 </span><span>36 </span><span>37 </span><span>38 </span><span>39 </span><span>40 </span><span>41 </span><span>42 </span><span>43 </span><span>44 </span><span>45 </span><span>46 </span><span>47 </span><span>48 </span><span>49 </span><span>50 </span><span>51 </span><span>52 </span><span>53 </span><span>54 </span><span>55 </span><span>56 </span><span>57 </span><span>58 </span><span>59 </span><span>60 </span><span>61 </span><span>62 </span><span>63 </span><span>64 </span><span>65 </span><span>66 </span><span>67 </span><span>68 </span><span>69 </span><span>70 </span><span>71 </span><span>72 </span><span>73 </span><span>74 </span><span>75 </span><span>76 </span> |
<span><span><span>height</span> <span>=</span> <span>20</span><span>;</span> </span></span><span><span><span>size</span> <span>=</span> <span>height</span><span>/</span><span>4</span><span>;</span> </span></span><span><span><span>ratio</span> <span>=</span> <span>1.4</span><span>;</span> </span></span><span><span><span>rounding</span> <span>=</span> <span>size</span><span>/</span><span>5</span><span>;</span> </span></span><span><span><span>gap</span> <span>=</span> <span>0.2</span><span>;</span> </span></span><span></span><span><span><span>module</span> <span>tooth</span><span>()</span> <span>intersection</span><span>()</span> <span>{</span> </span></span><span><span> <span>$fn</span><span>=</span><span>160</span><span>;</span> </span></span><span><span> <span>translate</span><span>([</span><span>0</span><span>,</span> <span>gap</span><span>/</span><span>2</span><span>])</span> </span></span><span><span> <span>// STEP 3: Quad offset </span></span></span><span><span> <span>// external </span></span></span><span><span> <span>offset</span><span>(</span><span>rounding</span><span>)</span> <span>offset</span><span>(</span><span>-</span><span>rounding</span><span>)</span> </span></span><span><span> <span>// internal </span></span></span><span><span> <span>offset</span><span>(</span><span>-</span><span>rounding</span><span>)</span> <span>offset</span><span>(</span><span>rounding</span><span>)</span> </span></span><span><span> <span>// adding rounding to sides to cancel radius when looping </span></span></span><span><span> <span>// STEP 1 </span></span></span><span><span> <span>polygon</span><span>([</span> </span></span><span><span> <span>[</span><span>-</span><span>size</span> <span>-</span> <span>rounding</span><span>,</span> <span>-</span><span>size</span><span>/</span><span>2</span><span>],</span> <span>// left shoulder </span></span></span><span><span> <span>[</span><span>-</span><span>size</span><span>/</span><span>2</span> <span>*</span> <span>(</span><span>2</span><span>-</span><span>ratio</span><span>),</span> <span>-</span><span>size</span><span>/</span><span>2</span><span>],</span> <span>// left neck </span></span></span><span><span> <span>// STEP 2 (ratio) </span></span></span><span><span> <span>[</span><span>-</span><span>size</span><span>*</span><span>ratio</span><span>/</span><span>2</span><span>,</span> <span>size</span><span>/</span><span>2</span><span>],</span> <span>// left ear </span></span></span><span><span> <span>[</span><span>size</span><span>*</span><span>ratio</span><span>/</span><span>2</span><span>,</span> <span>size</span><span>/</span><span>2</span><span>],</span> <span>// right ear </span></span></span><span><span> <span>[</span><span>size</span><span>/</span><span>2</span> <span>*</span> <span>(</span><span>2</span><span>-</span><span>ratio</span><span>),</span> <span>-</span><span>size</span><span>/</span><span>2</span><span>],</span> <span>// right neck </span></span></span><span></span><span><span> <span>[</span><span>size</span> <span>+</span> <span>rounding</span><span>,</span> <span>-</span><span>size</span><span>/</span><span>2</span><span>],</span> <span>// right shoulder </span></span></span><span><span> <span>[</span><span>size</span> <span>+</span> <span>rounding</span><span>,</span> <span>-</span><span>size</span><span>*</span><span>2</span><span>],</span> <span>// right bottom </span></span></span><span><span> <span>[</span><span>-</span><span>size</span> <span>-</span> <span>rounding</span><span>,</span> <span>-</span><span>size</span><span>*</span><span>2</span><span>],</span> <span>// left bottom </span></span></span><span><span> <span>]);</span> </span></span><span></span><span><span> <span>// shave off the rounded bit of the shoulders </span></span></span><span><span> <span>translate</span><span>([</span><span>-</span><span>size</span><span>,</span> <span>-</span><span>size</span><span>*</span><span>4</span><span>])</span> <span>square</span><span>([</span><span>size</span><span>*</span><span>2</span><span>,</span> <span>size</span><span>*</span><span>5</span><span>]);</span> </span></span><span><span><span>}</span> </span></span><span></span><span></span><span><span><span>module</span> <span>tooth_cut</span><span>()</span> <span>difference</span><span>()</span> <span>{</span> </span></span><span><span> <span>// STEP 4 </span></span></span><span><span> <span>tooth</span><span>();</span> </span></span><span><span> <span>offset</span><span>(</span><span>-</span><span>gap</span><span>)</span> <span>tooth</span><span>();</span> </span></span><span><span> <span>// STEP 5 </span></span></span><span><span> <span>translate</span><span>([</span><span>-</span><span>size</span><span>*</span><span>2</span><span>,</span> <span>-</span><span>size</span><span>*</span><span>5</span> <span>-</span> <span>size</span><span>/</span><span>2</span> <span>-</span> <span>gap</span><span>/</span><span>2</span><span>,</span> <span>0</span><span>])</span> <span>square</span><span>([</span><span>size</span><span>*</span><span>4</span><span>,</span> <span>size</span><span>*</span><span>5</span><span>]);</span> </span></span><span><span><span>}</span> </span></span><span></span><span></span><span><span><span>// this is done in 3D rather than 2D to allow for a taper -- this way the fit </span></span></span><span><span><span>// tightens as the teeth are pushed in, and the glue is squeezed instead of </span></span></span><span><span><span>// scraped </span></span></span><span><span> <span>module</span> <span>tooth_cut_3d</span><span>()</span> <span>difference</span><span>()</span> <span>{</span> </span></span><span><span> <span>translate</span><span>([</span><span>size</span><span>,</span> <span>0</span><span>,</span> <span>-</span><span>1</span><span>])</span> </span></span><span><span> <span>// STEP 6 & 7 </span></span></span><span><span> <span>linear_extrude</span><span>(</span><span>height</span><span>,</span> <span>scale</span><span>=</span><span>ratio</span><span>,</span> <span>convexity</span><span>=</span><span>3</span><span>)</span> </span></span><span><span> <span>tooth_cut</span><span>();</span> </span></span><span><span> <span>// STEP 8 </span></span></span><span><span> <span>// difference is used to constrain the edges to prevent overlap </span></span></span><span><span> <span>// I tried intersection (to do it in one pass) but performance TANKED! </span></span></span><span><span> <span>translate</span><span>([</span><span>-</span><span>size</span><span>*</span><span>2</span><span>-</span><span>0.01</span><span>,</span><span>-</span><span>size</span><span>,</span> <span>-</span><span>1</span><span>])</span> <span>linear_extrude</span><span>(</span><span>height</span><span>+</span><span>2</span><span>)</span> <span>square</span><span>([</span><span>size</span><span>*</span><span>2</span><span>,</span> <span>size</span><span>*</span><span>2</span><span>]);</span> </span></span><span><span> <span>translate</span><span>([</span><span>size</span><span>*</span><span>2</span><span>+</span><span>0.01</span><span>,</span><span>-</span><span>size</span><span>,</span> <span>-</span><span>1</span><span>])</span> <span>linear_extrude</span><span>(</span><span>height</span><span>+</span><span>2</span><span>)</span> <span>square</span><span>([</span><span>size</span><span>*</span><span>2</span><span>,</span> <span>size</span><span>*</span><span>2</span><span>]);</span> </span></span><span><span><span>}</span> </span></span><span></span><span><span><span>// will get at least length </span></span></span><span><span><span>module</span> <span>teeth_cut_3d</span><span>(</span><span>length</span><span>)</span> <span>{</span> </span></span><span><span> <span>n</span> <span>=</span> <span>ceil</span><span>(</span><span>length</span> <span>/</span> <span>(</span><span>size</span><span>*</span><span>2</span><span>))</span><span>+</span><span>2</span><span>;</span> </span></span><span><span> <span>real_length</span> <span>=</span> <span>n</span> <span>*</span> <span>size</span><span>*</span><span>2</span><span>;</span> </span></span><span></span><span><span> <span>// STEP 9 </span></span></span><span><span> <span>for</span> <span>(</span><span>i</span><span>=</span><span>[</span><span>0</span><span>:</span><span>n</span><span>-</span><span>1</span><span>])</span> </span></span><span><span> <span>translate</span><span>([</span><span>i</span><span>*</span><span>size</span><span>*</span><span>2</span> <span>-</span> <span>real_length</span><span>/</span><span>2</span><span>,</span> <span>0</span><span>])</span> <span>tooth_cut_3d</span><span>();</span> </span></span><span></span><span><span><span>}</span> </span></span><span></span><span><span><span>module</span> <span>dovetail_demo</span><span>()</span> <span>difference</span><span>()</span> <span>{</span> </span></span><span><span> <span>linear_extrude</span><span>(</span><span>18</span><span>)</span> <span>square</span><span>([</span><span>110</span><span>,</span> <span>60</span><span>],</span> <span>center</span><span>=</span><span>true</span><span>);</span> </span></span><span><span> <span>// STEP 10 </span></span></span><span><span> <span>teeth_cut_3d</span><span>(</span><span>300</span><span>);</span> </span></span><span><span><span>}</span> </span></span><span></span><span><span><span>dovetail_demo</span><span>();</span> </span></span> |
---|
Test Prints
To determine the optimal parameters for the dovetail joint ratios, I conducted several test prints. My goal was to find a balance that minimized printing artifacts while ensuring a tight fit. I opted for smaller teeth, which not only reduced potential artifacts but also seemed to create a more secure joint.
First attempt: giant 1:1 teeth. Taper visible
1:2 teeth with edge artefacts
Small 1:4 teeth that seem best!
Following Richard’s lead, I found that a 0.2mm interference fit worked best. This provided a slight tolerance while still ensuring a snug fit and leaving space for glue.
Successful implementation after several iterations
To maintain performance, I limited the number of faces in the tooth code ($fn=16
). This offered an acceptable balance between smoothness and reduced stress concentration, crucial for reliable 3D prints.
I did observe some Z-fighting when virtually joining parts. Typically, I ensure slight intersections to avoid this, but for now, it appears visually acceptable, and I proceeded with the design, hoping to avoid manifold issues later.
Edge Artefacts
A potential issue arose with tapered teeth: the top part could sometimes become suspended in mid-air, particularly at tooth edges where boundaries occur. This can lead to printing failures like spaghetti and missing sections in the final 3D printed object.
Close up of hanging artefacts together with the scaled edges interfering (fixed since)
To mitigate this, I implemented a post-processing step in the code to detect and remove these artifacts. By identifying small, detached fragments not connected to the print bed, the code can eliminate the spaghetti issue. While this might leave small holes, these can usually be addressed during the finishing process, ensuring high-quality large 3D prints.
Automatically Splitting up STLs
With the dovetail geometry defined and tested, the next crucial step was automating the process of splitting any STL file into printable sections. This automation would allow users to take virtually any 3D model and prepare it for printing on smaller 3D printers by intelligently segmenting it.
As mentioned earlier, my focus is on panel-like designs, simplifying the process to primarily 2D operations, assuming relatively flat and rectangular parts.
To achieve automation, I opted to work directly with STLs outside of OpenSCAD. This approach offers greater flexibility, making the system compatible with 3D models from any CAD software, not just those created in OpenSCAD.
Leveraging my existing part nesting software, built using rectpack and numpy-stl, I developed a streamlined Python script to handle the STL splitting.
Here’s how the automated STL splitting process works:
- STL Loading and Bounding Box: The Python script loads the STL file and calculates the model’s bounding box.
- Orientation and Rotation: The model is rotated to align its aspect ratio with the printer bed dimensions. Optionally, it can be oriented so its longest axis aligns with the printer bed’s longest axis.
- Subdivision Calculation: Based on the model’s dimensions and printer bed size (accounting for the dovetail tooth margin), the script calculates the necessary subdivisions along each axis to ensure each part fits within the printable area.
- OpenSCAD Template Execution: An OpenSCAD template is executed, which subtracts the dovetail teeth from the model. The STL is translated within OpenSCAD to position each cut correctly for seamless assembly.
- STL Splitting with Slic3r: The resulting STLs, now segmented with dovetail joints, are split into individual files using the
slic3r
command-line interface. - Edge Artifact Removal: Finally, the script removes any remaining edge artifacts by identifying and deleting small, detached objects, ensuring clean and printable parts.
Results
Refactoring my nesting code made it possible to integrate the dovetail splitting functionality cleanly. After significant debugging, I ran the code, eager to see the results. Initially, I was met with disappointment. Each operation took four hours and ultimately failed with errors related to CGAL:
ERROR: CGAL error in CGALUtils::applyBinaryOperator difference: CGAL ERROR: assertion violation!
ERROR: The given mesh is not closed! Unable to convert to CGAL_Nef_Polyhedron
OpenSCAD relies on the CGAL library, which, while powerful, is known for its slow performance and potential to produce non-manifold meshes. The speaker design I used for testing, unfortunately, generated non-manifold STLs, possibly due to internal OpenSCAD issues or aspects of my design code. These errors likely stemmed from these non-manifold edges.
Facing this setback, I considered abandoning the project. However, OpenSCAD’s recent integration of the manifold geometry library offered a glimmer of hope. Manifold is reported to be significantly faster and more robust than CGAL.
Experimenting with an unstable version of OpenSCAD using the --backend=manifold
flag proved transformative. Not only did it resolve the errors, but it also dramatically improved performance. The design computation time plummeted to a mere 223ms! This represented an astonishing 64500x speed improvement.
Encouraged, I decided to fully integrate Manifold into the workflow. Manifold’s higher memory usage and multi-threaded nature required adjustments to my build scripts, which previously processed 16 parts simultaneously, potentially overwhelming the system.
After further hacking and optimization, the system was fully integrated and performing as expected. Below are the results using the previous speaker design, resized to fit a 200×200 bed, making it printable even on a Prusa i3!
bed-1.png
bed-11.png
bed-13.png
bed-15.png
The original design, so you can imagine how the parts assemble.
bed-17.png
bed-3.png
bed-5.png
bed-7.png
bed-9.png
The results are remarkably successful. The nesting algorithm effectively groups split parts, minimizing the number of print beds needed, which translates to reduced printing time and effort. In one instance (bed 17), a part was automatically split further to fit within a single bed.
Worryingly thin tooth that could cause print issues
Upon closer inspection of the corners, thin dovetail teeth are sometimes generated. Knowing the intersection points of dovetail joints, we could potentially skip teeth in these areas to avoid such thin features – a refinement for future development.
The Python code for this project is available for download above. It requires slic3r
, rectpack
, openscad-nightly
, and numpy-stl
, along with the dovetail.scad
file placed in the lib/
subdirectory. Being a quick hack from two separate files, it might require some adjustments to run smoothly.
Conclusion
The chosen dovetail profile offers self-alignment and increased surface area for strong glue bonds. Alignment during assembly is simplified by using a straight edge and a silicone mat to prevent unwanted adhesion.
This project successfully paves the way for creating floor-standing speaker designs and other large format 3D prints using this automated segmentation method. Future steps include validating the design through physical builds and experimenting with various adhesives and finishing techniques. Repurposing an existing small MDF subwoofer enclosure using this method is also a compelling possibility.
While a manual implementation with specialized cases within the speaker design was feasible, this automated approach offers broader applicability and greater elegance. It also opens doors to further improvements:
- Variable part division locations for stronger, more practical joints.
- Skipping teeth at joint intersections to avoid thin features.
- Avoiding teeth near voids and edges.
- Center-aligning teeth to middle axes.
Exploring libraries like trimesh could further enhance the system, potentially replacing numpy-stl and/or OpenSCAD for even greater capabilities.
Based on my experience with previous projects, I am confident that filled and sanded assembled parts will achieve a flawless finish.
The code is shared in its current form, and I am open to properly packaging it if there is significant community interest. I am also eager to see how others might utilize or adapt this idea for their own large 3D printing projects.
Thank you for reading! Share your thoughts and support by upvoting or commenting on Hacker news, Twitter, Hackaday, Lobste.rs, Reddit and/or LinkedIn.
Please email me with any corrections or feedback.
Tags:
3d-printingmanufacturingopenscadpythonsoftware
Related:
[