Unleashing Giant 3D Prints: A Python-Powered Jigsaw Generator for Large-Scale Creations

The allure of building something entirely automated is undeniable. Witnessing a machine execute a complex task from start to finish is deeply satisfying, especially when combined with the flexibility of parametric design. This synergy simplifies iteration and customization, as exemplified in my recent speaker project.

During that speaker endeavor, I pushed the boundaries of my 3D printer’s capabilities, almost reaching the limit for a single, seamless print. Exploring large format printers revealed them to be costly and potentially less versatile than my current setup.

Then, I encountered a video by Richard from RMC showcasing the 3D printing of a complete arcade machine using a cluster of smaller printers. Richard meticulously designed the model,2 dividing it into macro layers and then into smaller, interlocking parts with dovetail joints for easy assembly. The result was remarkably successful.3

This sparked a crucial question: Could this segmentation process be automated?

In my previous speaker design article, I briefly mentioned the possibility of floor-standing speakers, acknowledging the necessity of dividing the print. If I could tailor a system specifically for panel-based designs like this speaker system, automating the segmentation would become significantly more manageable.

Having already developed a system for nesting parts on individual print beds, I decided to expand its functionality to split panels into jigsaw-like pieces. Dovetail joints emerged as the ideal solution, offering robust connections that require only glue and no additional hardware like dowels.

The goal was to create a system that could divide designs with intricate geometries without compromising the final aesthetic or structural integrity. This approach allows makers to overcome the size limitations of standard 3D printers and venture into creating truly giant 3D prints.

The Dovetail Joint Advantage

Dovetail joints are a time-honored woodworking technique, prized for both their strength and visual appeal. Traditionally crafted by hand or with specialized tools like routers and CNC machines, dovetails excel at resisting tensile forces, creating exceptionally strong bonds.

The inherent wedging action of a dovetail joint, particularly when tapered, ensures a tight fit that becomes even more secure when glued. This is crucial for 3D printed assemblies as it prevents glue from being pushed out during assembly, maximizing adhesion.

Exploring existing OpenSCAD dovetail implementations, I found none that fully met my requirements for 3D printing.

My vision demanded a method that subtracted material in a way that produced perfectly mating joints right off the printer. Ideally, this would eliminate the need for complex, overlapping negative spaces in the design, simplifying the process.

This led to the concept of a thin “shell” structure, resembling a zig-zagging ribbon, preferably with a taper for enhanced joint tightness.

However, OpenSCAD, being rooted in Constructive Solid Geometry (CSG), isn’t inherently designed for creating thin shells, presenting a unique challenge.

Deconstructing the Dovetail Geometry

After several iterations focused on eliminating artifacts and optimizing performance, I arrived at the following step-by-step breakdown of the dovetail profile generation:

Interestingly, Step 8 initially utilized an intersection operation, which drastically hampered OpenSCAD’s performance, reducing rendering speed to a crawl.

Initially, I overlooked removing the interfering edges, leading to unexpected artifacts (as shown later). I initially mistook this for a bug until closer examination of the 3D tooth revealed the issue.

The OpenSCAD code below encapsulates this process, with annotations linking each step to the visual breakdown. Remarkably, it’s achieved in 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>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> |

dovetail.scad Download Copy

Test Printing and Refinement

To dial in the optimal parameters for the dovetail joints, I conducted a series of test prints with varying ratios. I opted for smaller teeth as they minimized potential artifacts and provided a tighter, more secure fit.

Following Richard’s lead, I found that a 0.2mm interference fit worked best, providing slight play while leaving ample space for glue.

To maintain optimal performance, I limited the number of faces ($fn=16) in the tooth code. This provided a good balance between smoothness and reduced stress concentration.4

While minor Z-fighting appeared when joining parts virtually in OpenSCAD, I remained optimistic about avoiding manifold issues in the final prints.

Addressing Edge Artifacts

A challenge emerged where the top of tapered teeth could sometimes be suspended in mid-air, particularly at tooth edge boundaries. This would lead to “spaghetti” printing and missing sections in the final part.

To mitigate this, I implemented artifact detection in the post-processing5 code. By identifying small, isolated fragments not connected to the print bed, the system could remove these potential spaghetti sources. While holes might still occur in the print, they could generally be addressed during finishing.

Automating STL Splitting for Giant 3D Printer Projects

With functional dovetail geometry, the next step was to automate the splitting process, allowing any design to be segmented for printing on a standard-sized Giant 3d Printer – or rather, to overcome the limitations of non-giant printers.

Given that the system was designed for panels6, I could simplify the process to 2D operations, assuming primarily flat, rectangular parts.

Automation was achieved primarily outside OpenSCAD, working directly with STL files. This ensured compatibility with models from any CAD software, broadening the system’s utility for giant 3D printing endeavors.

Building upon existing part nesting software using rectpack and numpy-stl, I developed a streamlined workflow:

  1. STL Loading & Bounding Box: Load the STL file and determine the model’s bounding box.
  2. Orientation: Rotate the model to align its aspect ratio with the printer bed, optionally aligning the longest dimension with the bed’s longest axis.
  3. Subdivision Calculation: Calculate the necessary subdivisions along each axis to ensure parts fit within the print bed (accounting for dovetail tooth margins).
  4. OpenSCAD Dovetail Subtraction: Execute an OpenSCAD template to subtract dovetail teeth from the model, translating the STL for precise cut placement.
  5. STL Splitting: Use slic3r CLI to divide the processed STL into individual files for each part.
  6. Artifact Removal: Eliminate edge artifacts by identifying and removing small, detached objects.

Promising Results and Performance Boost

Refactoring the nesting code enabled seamless integration of the dovetail splitting functionality. However, initial tests were met with disappointment, with operations taking up to 4 hours and failing due to CGAL errors:

  • 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’s reliance on the CGAL library, known for its slowness and potential for non-manifold meshes, seemed to be the bottleneck. The speaker design, likely containing non-manifold STL edges due to OpenSCAD or coding issues, exacerbated these problems.

Facing project abandonment, a breakthrough arrived with OpenSCAD’s integration of the manifold geometry library.7 Manifold promised significantly faster and more robust performance.

Switching to an unstable OpenSCAD version with the --backend=manifold flag yielded immediate success. The same design computed in a mere 223ms! – a staggering 64500x speed improvement.

Manifold’s increased memory usage and multi-threading required adjustments to my build scripts, which previously processed 16 parts simultaneously, overwhelming the system.

After further hacking and integration, the system behaved as expected. Applying it to the previous speaker design with a reduced bed size of 200×200 demonstrated its capability to enable giant 3D prints even on printers like the Prusa i3.8

The system performed exceptionally well, efficiently nesting split parts to minimize print beds and overall printing time. In one instance (bed 17), a part exceeding bed dimensions was intelligently split further to ensure printability.

Thin tooth corners, as seen above, are a recurring issue. Knowing the dovetail joint intersection points, future iterations could skip teeth in these areas to prevent such thin features.

turbojigsaw.py

The Python code is available for download above. It requires slic3r, rectpack, openscad-nightly, and numpy-stl, along with dovetail.scad placed in the lib/ subdirectory. Hacked together from two files, it may require minor adjustments to function perfectly.

Conclusion: Expanding the Realm of Giant 3D Printing

The chosen dovetail profile offers self-alignment and expanded gluing surface area. 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 giant 3D prints, contingent on design validation and adhesive/finish experimentation. Repurposing my existing small MDF subwoofer into a larger, 3D-printed enclosure is another exciting possibility.

While a design-specific implementation with hardcoded joint placements was feasible, this automated, generalized approach is more versatile and aesthetically pleasing.

Future enhancements could further refine the system:

  • Variable part division to concentrate material for stronger joints.
  • Tooth skipping at joint intersections to avoid thin features.
  • Tooth skipping near voids and edges for improved printability.
  • Center-aligned teeth along middle axes for balanced designs.

Exploring trimesh could potentially replace numpy-stl and/or OpenSCAD, offering even greater capabilities.

Based on prior experience, I am confident that filled and sanded assembled parts will achieve a flawless finish.

The code is publicly available for the time being, and I am open to creating a polished package based on community interest. I am eager to see how others might utilize or adapt this approach for their own giant 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 corrections or feedback.

Tags:

3d-printingmanufacturingopenscadpythonsoftware

Related:

About me Latest articles


1: Or at least, as automated as possible, given the constraints of reality.

2: And perhaps also printed it, it’s not clear.

3: See also his twitter.

4: Although I should check this with stress analysis software one day.

5: Perhaps I should call it pre-slicing.

6: At least, for now.

7: Thanks to whoever made that happen!

8: Or similar sized printer.

9: And perhaps also aesthetics.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *