Generative AI for 3D Modeling and Printing
Tags: ai, 3dprinting, beginners, programming
Published: 2023-07-07 19:00:17

## Introduction

While exploring generative AI, I've seen what it can do with different web programming languages like JavaScript and I thought it might be interesting to see what it can do with 3d modeling.

One tool many people use for parametric modeling is [OpenSCAD](https://openscad.org/) which is very utilitarian and has methods for constructing 3d and 2d geometry (and 3d from 2d such as extrusion).

Since "OpenSCAD focuses on the CAD aspects rather than the artistic aspects of 3D modeling" it's great for generating practical and functional 3d models (and also things like fractals, which might be a future topic), and I wanted to explore how chatGPT can help - in cutting down on time, exploring different designs, or at the very least teaching me new things.

The project I decided on was a **business card holder** - it's something that has a little bit of complexity, can lend itself to creativity, and is also relatively geometric and inorganic; not to mention it's something that I can use!

For a little inspiration, I turned to MidJourney:

![AI-generated business card holder designs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rtcehy85sw0t5ktndcth.png)

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lhgfc7nyrq2difvmjbky.png)

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8jqybjeyclhwlx5z2ubi.png)

## Initial Attempt

First, I wanted to see what GPT-4 would give me out of the box, without much direction:

> generate openSCAD code for a standard size business card holder


Produces the following code (for the brevity, the "fluff" around it is omitted"):

```OpenSCAD
cardWidth = 90; 
cardHeight = 55; 
cardThickness = 5; 

boxWallThickness = 3; 
boxBottomThickness = 3; 
boxInsideWidth = cardWidth - 2; 
boxInsideHeight = cardHeight - 2; 
boxInsideDepth = cardThickness + boxBottomThickness; 

module box() {
    difference() {
        // Outer box
        cube([cardWidth, cardHeight, cardThickness]);
        // Inner box
        translate([boxWallThickness, boxWallThickness, boxBottomThickness])
        cube([boxInsideWidth, boxInsideHeight, boxInsideDepth]);
    }
}

box();
```

This code generates two "cubes" (well, rectangular prisms) - one representing a stack of business cards, and larger one - and subtracts one from the other.

This code is valid and renders the following shape (I added colors to illustrate the parts):

![simple business card holder 3d model](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/28hqszbgmovzhhgzhzbg.png)

The general model is right, but there are a few problems with this:

1. The negative space is a bit too small - a standard business card is 88.9mm x 50.8mm but the cutout is 87mm long, so there will be an overhang.
2. The depth of this container doesn't allow for many business cards.
3. I am _pretty sure_ that chatGPT was "trying" to generate a box with walls on each side, but ended up completely removing two of them.

We can play around with some of the variables to give us something more functional, or we can ask chatGPT do it for us:

> adjust the model so that there are four walls and a "base" and that a stack of standard business cards fits in the cutout.

Gives us:

```
cardWidth = 90; 
cardHeight = 55; 
stackThickness = 10; 

boxWallThickness = 3; 
boxBaseThickness = 3; 
boxInsideWidth = cardWidth + 2; 
boxInsideHeight = cardHeight + 2; 
boxInsideDepth = stackThickness + boxBaseThickness;

module box() {
    difference() {
        // Outer box
        cube([boxInsideWidth + (2 * boxWallThickness), boxInsideHeight + (2 * boxWallThickness), boxInsideDepth]);
        // Inner box
        translate([boxWallThickness, boxWallThickness, boxBaseThickness])
        cube([boxInsideWidth, boxInsideHeight, stackThickness]);
    }
}

box();
```

This results in a cutout that is 92mm x 57mm and 10mm high which gives us a little clearance for the business cards:

![business card box 3d model from complex code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uaap7xh5ubu2fis0rxaz.png)

The code could be cleaned up (for example by starting off with the dimensions of the entire stack of cards, and going from there) but the result is a perfectly functional container.

#### Another Approach

We can take a more "engineering" approach by crafting our prompt as if we were writing the code ourselves. This might be less error-prone if the prompt is crafted well:

> generate openSCAD code for the following:
> 
> a rectangular prism that is 98mm across, 63mm deep, and 13mm high.
> 
> subtract a prism that is 92mm x 57mm and 10mm high from the top center of the previous prism.

Generates a very simple block of code (and goes on to explain what it does):

```
difference() {
    cube([98, 63, 13]); // Outer rectangle
    translate([3, 3, 3]) cube([92, 57, 10]); // Inner rectangle subtracted from the outer rectangle
}
```

And looks identical to the previous version:

![business card box 3d model from simple code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/psgzxueyzjeqrpkju1by.png)

#### Tradeoffs

The second approach resulted in much less code (generally good!) which was also more straightforward, but we had to do a little more "manual" work like finding the exact dimensions we need and breaking the prompt up into steps, as if we were writing the code ourselves.

The first approach might still be better for exploration and experimentation, if we want to see wildly different versions of what a "business card holder" can look like.

Either way, we get the same model which can be 3d-printed in a little under two hours:

![screenshot of model imported and sliced in PrusaSlicer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3hmv95ez0e6z6lh3dcvm.png)

## Going Further

Let's combine these approaches to create a more interesting design.

> Generate openSCAD code for standard size business card holder composed of the following modules:
> 
> - a base that is flat against the surface it will be placed.
> - a small front face "lip" connected to the base that keeps the cards from slipping out.
> - a back that has a bottom side that connects to the back side of the "base". This "back" shape is almost vertical but with the top rotated away from the front.
> 
> Color-code each module.

Skipping the code here, this renders:

![openSCAD screenshot of model generated from above code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vgpee94j1l4ybzx0tj1e.png)

Obviously, that's not how we want the "back" oriented.

Another attempt:

> Generate openSCAD code for standard size business card holder composed of the following modules:
> 
> - a base that is flat against the surface it will be placed.
> - a small front face "lip" connected to the base that keeps the cards from slipping out.
> - a back that has a bottom side that connects to the back side of the "base". This "back" shape is almost vertical but rotated so that this "back" shape is tilted back, away from the front of the model.
> 
> Color-code each module.

![another attempt; back section is angled the wrong way](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3whbskl2578xp25y5qqt.png)

Regenerating the response strays further away from what we want:

![attempted model with parts not aligned or rotated properly](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fz6p9t6rkalo8fktpzcf.png)

Further generations aren't much better, so we'll be direct when discussing these actions by giving it specific dimensions:

> Generate openSCAD code for standard size business card holder composed of the following modules:
> 
> - a 2mm thick, 90mm across, and 20mm deep bottom that is horizontal to the surface it will be placed on.
> - a 2mm thick and 5mm tall front face "lip" at the top front of the base that will keep the cards from slipping out.
> - a 2mm thick and 40mm tall vertical back side. Cards will rest on this.
> 
> Rotate and translate each module to adhere to the description.
> 
> Color-code each module for debugging. Do not provide anything except for the code.

Does pretty much we want:

```
$fn = 100; // Higher resolution

module base() {
    color("Blue")
    cube([90, 20, 2]);
}

module lip() {
    color("Green")
    translate([0, 15, 0])
    cube([90, 2, 5]);
}

module back() {
    color("Red")
    translate([0, 0, 2])
    cube([90, 2, 40]);
}

// Combine modules
base();
lip();
back();

```

![simple card holder model](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dsnbynbf6vlokeq0ypgc.png)

Rotating the back part and placing it correctly took some trial and error: `rotate the "back" 10 degrees so that the top is angled away from the front and move the "back" module so that the bottom edge connects to the back edge of the base.` ... `fix the rotation direction` ... `fix the rotation direction and placement so that the back is at the back, away from the lip` ... `move the back piece to the back edge of the base.`

Eventually, I got more or less what I wanted:

```
$fn = 100; // Higher resolution

module base() {
    color("Blue")
    cube([90, 20, 2]);
}

module lip() {
    color("Green")
    translate([0, 15, 0])
    cube([90, 2, 5]);
}

module back() {
    color("Red")
    translate([45, 0, 1]) // Move upwards by 1mm and center for rotation
    rotate([10, 0, 0]) // Rotate 10 degrees
    translate([-45, 0, 0]) // Re-center after rotation
    cube([90, 2, 40]);
}

// Combine modules
base();
lip();
back();
```

![simple business card holder with front lip, base, and tilted back](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/joc8v8yugbdm7s0cyoig.png)

After this point, I prompted GPT-4 to add "caps" to the sides:

> create two quadrilaterals that will act as left and right "caps" described as:
> 
> - each cap is 2mm wide
> - bottom edges are flush with the bottom of the base
> - front edges are flush with the front lip
> - back edges are flush with the back part
> - top edges connect between the middle of the back part and the top of the front lip
> 
> The best way would probably be to create the points and then extrude and then move them to different positions based on "left" or "right"

Which produced a syntactical error and not what I was going for:

```
module cap(right) {
    color("Yellow")
    points = [[0,0],[90,0],[90,15],[45,35]]; // Define points of a polygon
    linear_extrude(height = 2)
        polygon(points=points); // Create the polygon
    translate([right ? 88 : 0, 0, 0]); // Position the cap
}
```

![incorrect "caps" rendered](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x0jkhly9aee73awjfb9a.png)

But it was on the right track, programmatically-speaking.

Given that one of my initial goals was to _speed up_ development, I took the reigns and fixed the issues manually:

```
module cap(right) {
    color("Yellow");
    points = [[0, 0], [24, -4], [4, 16], [0, 16]]; // Points on the X-Y plane
    translate([right ? 90 : 2, 0, 0]) 
    rotate([0, -90, 0]) // Rotate points to Y-Z plane
    linear_extrude(height = 2) 
    polygon(points = points);
}
```

and fed it back to chatGPT.

After this point, I asked chatGPT to increase the width from 90mm to 95mm to account for the end caps, "etched" text into the back, and then made some manual adjustments to arrive at my final (for now) product:

```
module base() {
    color("Blue")
    cube([95, 20, 2]);
}

module lip() {
    color("Green")
    translate([0, 18, 0])
    cube([95, 2, 5]);
}

module back() {
    color("Red")
    difference() {
        translate([0, 0, 1]) // Move upwards by 1mm and center for rotation
        rotate([10, 0, 0]) // Rotate 10 degrees
        cube([95, 2, 40]);
        translate([85.5, -2, 18]) // Adjusted position of the text
        scale([.65, 1, .65]) // Decrease scale of the text
        rotate([260, 180, 0]) // Adjusted rotation to make the text parallel with the 'back'
        linear_extrude(height = 2, convexity = 2)
        text("digitalcanvas.dev", font = "Merriweather"); // Text to be cut out
    }
}

module cap(right) {
    color("Yellow");
    points = [[0, 0], [24, -4], [5, 18], [0, 18]]; // Points on the X-Y plane
    translate([right ? 95 : 2, 0, 0]) 
    rotate([0, -90, 0]) // Rotate points to Y-Z plane
    linear_extrude(height = 2) 
    polygon(points = points);
}

// Combine modules
base();
lip();
back();
cap(true); // Right cap
cap(false); // Left cap
```


![3d model preview with "digitalcanvas.dev" etched into the back](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mo5g8qqcrop1lefoyyr8.png)



I loaded this into my slicer ([PrusaSlicer](www.prusa3d.com/prusaslicer/)) and began the print!

![model loaded into PrusaSlicer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7fjcgrb5ftdf6hqztg8k.png)

## Final thoughts & takeaways

Ultimately, I didn't get the level of polish that MidJourney teased was possible, and the process was imperfect but a good learning experience for both working with Generative AI and OpenSCAD. 

GPT-4 gave valid code the vast majority of the time and I was able to adjust it when needed. Having larger chunks of code generated for me definitely saved time - both in typing and looking up documentation - and being able to tweak specific numbers and feed it back to chatGPT allowed for a relatively smooth workflow; though I concede that as a first try, using chatGPT was slower than coding it by hand; I spent a lot of time checking and tweaking the generated outputs (not to mention having to wait for rate limits to expire).

There were some themes that came out of this. Generated OpenSCAD code was prone to mixing up axes when rotating and translating, and getting the right "Points on the X-Y plane" was a struggle. Doing this manually was much faster, but prompts like "the module was rotated along the wrong axis" usually worked, too.

It's also important to be as direct as possible - do not assume "module A should connect to module B" will result in what you expect; give more direction: "the bottom edge of module A should be flush with the top edge of module B and the smaller edge should be centered on the larger edge." 

Finally, it helps to break the end goal into smaller tasks (e.g. "generate module A", "adjust model A", "add module B") rather than start with a larger prompt that has more things that can go wrong. Interestingly, _generating_ modules was generally less error-prone than _modifying_ them.

In my opinion, it's best to treat it as pair programming where you hand the work off between two software engineers while speaking in "[the highest level of abstraction](https://www.agilealliance.org/resources/experience-reports/mob-programming-agile2014/)" (which, in some cases, is lower than you'd think).

Thank you for reading! Have you used generative AI for 3d modeling or printing? What approach worked well for you? I would love to hear about other experiences.

Of course, I can't leave this post unfinished! Here is the final product:

![photo of final 3d-printed product](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cwzwc1p8qr2rkameu839.jpg)

The real business cards are in the mail, but I printed a fake one!

![photo of final 3d-printed product with a business card placeholder](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i6fgrsqk7pm6kof0fdpn.jpg)