3D Print Things: Conquering Size Limits with a Python Jigsaw Generator

Jan 2025 – 12 min readFeatured: ★ HackadayHacker NewsReal Python Podcast

I’ve always been fascinated by automated workflows when creating anything. There’s a unique satisfaction in watching a machine take over the work, especially when combined with parametric design, which makes iteration and customization incredibly easy. My recent speaker project perfectly exemplifies this approach.

With that speaker project, I was pushing the boundaries of what I could comfortably 3D print in one piece without significant design splitting. Exploring large format 3D printers became a necessity, but these machines are often expensive and might not offer the specific capabilities I already had.

Then, I came across a video by Richard from RMC where he 3D printed an entire arcade machine using a network of smaller, more accessible printers. His process involved designing the model and then dividing it into macro layers, each further segmented into smaller parts with dovetail joints for assembly. This method 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!

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

Reflecting on my previous speaker design article, I briefly mentioned the idea of floor-standing speakers, casually overlooking the challenge of segmenting the print. However, if I narrowed the system’s focus to panel-based designs, like this speaker system, automating the segmentation seemed achievable.

Having already developed a system for nesting parts efficiently on print beds, I decided to expand its functionality. The goal was to enable it to split panels into jigsaw-like pieces. Dovetail joints seemed ideal for this, offering easy gluing and eliminating the need for extra hardware like dowels.

If successful, this approach would allow for dividing complex geometries into printable sections without significantly compromising the final product’s appearance.

The Dovetail Profile

Dovetail joints are a traditional woodworking technique renowned for both strength and aesthetic appeal. They are commonly used where robust and visually pleasing joinery is required. Dovetails can be crafted manually, with handheld routers (using jigs), or by CNC routers.

Their strength comes from the wedge effect, which tightens the joint when pulled apart. Tapered dovetails further enhance this, creating an even tighter fit during alignment. This is particularly beneficial for gluing, as it prevents the glue from being squeezed out.

Dovetail example (from wikipedia!)

I investigated existing dovetail implementations in OpenSCAD, but none fully met my specific requirements.

My aim was a design where plastic is subtracted only once, resulting in perfectly mating joints right from the print bed. This would simplify the design process, avoiding the need for intricate, overlapping negative spaces in the two parts.

This led me to consider a thin, shell-like, zig-zagging ribbon structure – ideally with a taper.

Through the magic of anachronism, this is what I had in mind

However, OpenSCAD, being based on Constructive Solid Geometry (CSG), isn’t ideally suited for creating thin shells, making this a somewhat challenging task.

Deriving the Geometry

The following images illustrate the step-by-step deconstruction of the dovetail profile. This process involved several iterations to refine the design, eliminate artifacts, and optimize performance.

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 was initially attempted using intersection, but it severely impacted OpenSCAD’s performance, dropping rendering speed dramatically.

Step 9: Iterate to join several together

Initially, I didn’t remove the interfering edges, leading to hanging artifacts. This initially seemed like a bug until closer inspection of the 3D tooth revealed the issue.

Step 10: Subtract this from the work piece

Success! Note, the parts are not separable in OpenSCAD

The code below implements the described process, with annotations linking each step to the visual breakdown above. Remarkably, it’s only 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> |

dovetail.scad Download

Test Prints

To determine the optimal parameters for the dovetail joint ratios, I conducted several test prints. I opted for smaller teeth, which minimized potential artifacts and seemed to provide a tighter fit.

First attempt: giant 1:1 teeth. Taper visible

1:2 teeth with edge artefacts

Small 1:4 teeth that seem best!

Richard’s 0.2mm interference fit proved to be the sweet spot, providing a slight play while ensuring ample space for glue.

Successful implementation after several iterations

Performance was optimized by limiting the number of faces ($fn=16) in the tooth code. This offered a good balance between smoothness and reduced stress concentration.

Z fighting (sparkles)

I had some initial concerns about Z-fighting occurring when joining parts. While typically I ensure parts slightly intersect to avoid this, it appeared visually acceptable, and I proceeded cautiously, hoping to avoid manifolding issues.

Edge Artifacts

Occasionally, the top of a tapered tooth might be suspended in mid-air, especially near design boundaries. This can lead to printing issues like spaghetti and missing sections in the final 3D printed thing.

Close up of hanging artefacts together with the scaled edges interfering (fixed since)

I realized that these artifacts could be detected and removed in post-processing. By identifying small, detached fragments not in contact with the print bed, the spaghetti issue could be mitigated. While holes might still occur, they could likely be addressed during the finishing stage.

Automating STL Splitting for 3D Prints

With functional in-place joints, the next step was automating the process to divide any design for printing on a standard-sized 3D printer bed.

Since this system is designed for panels, operation is primarily 2D, assuming mostly flat and rectangular parts.

Automation was approached by operating on STL files directly, largely outside of OpenSCAD. This makes the system compatible with models from any CAD software, expanding its versatility for various 3d Print Things.

Building upon existing part nesting software using rectpack and numpy-stl, the code was developed as follows:

  1. Load the STL file and determine the model’s bounding box.
  2. Rotate the model to align its aspect ratio with the printer bed, optionally orienting the longest dimension along the bed’s axis.
  3. Calculate the necessary subdivisions on each axis to ensure parts fit within the print bed (accounting for tooth size).
  4. Execute an OpenSCAD template to subtract dovetail teeth from the model, positioning each cut accurately via STL translation.
  5. Split the resulting STLs into individual files using the slic3r CLI.
  6. Remove any edge artifacts by identifying and eliminating small, detached objects.

Results of Automated Splitting

Refactoring the nesting code enabled seamless integration of the dovetail splitting functionality. However, initial runs were disappointing, taking four hours per operation and failing with errors like:

  • 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 to produce non-manifold meshes, was the likely culprit. My speaker design, suspected of generating non-manifold STLs due to OpenSCAD or code issues, seemed to trigger these errors.

Facing this roadblock, I explored OpenSCAD’s newly integrated geometry library, Manifold. This library promised significant speed and robustness improvements.

Switching to an unstable OpenSCAD version with the --backend=manifold flag proved transformative. The computation time plummeted to 223ms, a 64500x improvement!

Manifold’s increased memory usage and multi-threading capabilities required adjustments to my build scripts, which previously processed 16 parts simultaneously. However, after further refinement, the system performed as expected.

Below are the results of applying this process to the previous speaker design, reducing the bed size to 200x200mm – making it printable on a Prusa i3.

bed-1.png

bed-10.png

bed-11.png

bed-12.png

bed-13.png

bed-14.png

bed-15.png

bed-16.png

The original design, so you can imagine how the parts assemble.

bed-17.png

bed-2.png

bed-3.png

bed-4.png

bed-5.png

bed-6.png

bed-7.png

bed-8.png

bed-9.png

The system performed exceptionally well. The nesting algorithm effectively grouped split parts, reducing the number of print beds required, which in turn minimizes printing time and material. In one instance (bed 17), a part was further split to fit within a single bed.

Worryingly thin tooth that could cause print issues

Corners frequently exhibited worryingly thin teeth, which could cause printing problems. Knowing the dovetail joint intersection points, strategically skipping teeth in these areas could mitigate this issue – a refinement for future development.

turbojigsaw.py

The code is linked above. It requires slic3r, rectpack, openscad-nightly, and numpy-stl, with dovetail.scad placed in the lib/ subdirectory. Hacked from two separate files, it may require minor adjustments to run smoothly.

Conclusion

The selected dovetail profile is self-aligning and maximizes the glue bonding surface area. Alignment during assembly is straightforward, especially with a silicone mat to prevent unwanted adhesion.

This successful automation paves the way for realizing a floor-standing speaker design and other large 3D print things in the near future. Further steps include design validation and experimenting with various adhesives and finishes. Alternatively, this method could be applied to revamp my older MDF subwoofer project.

While a more case-specific implementation within the speaker design, with around 12 special cases, might have been possible, this automated approach offers greater versatility and elegance. Such a specific approach could also help avoid edge artifacts and allow for more precise joint placement in less detailed areas.

Future improvements could combine the best aspects of both approaches:

  • Variable part division locations for stronger, more practical joints.
  • Tooth skipping at joint intersections.
  • Tooth skipping near voids or edges.
  • Center-aligning teeth to middle axes.

Exploring trimesh could further enhance the system, potentially replacing numpy-stl and/or OpenSCAD, offering broader capabilities.

Based on past projects, I am confident that filled and sanded assembled parts will achieve a seamless finish.

The code is available for download in this post. If there is significant interest, I am happy to package it more formally. I’m eager to see how others might utilize or adapt this concept for their own 3D printing projects.

Thank you for reading! If you have any comments or enjoyed this article, please consider sharing or upvoting it on Hacker News, Twitter, Hackaday, Lobste.rs, Reddit and/or LinkedIn.

Please email me with any corrections or feedback.

Tags:

3d-printingmanufacturingopenscadpythonsoftware

Related:

[

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 *