Designing in code with OpenSCAD
This chapter is a side trip, and it is completely optional. In Chapter 8 you learned to design by dragging shapes around in Tinkercad, and that path takes you all the way to a finished squeezer. Nothing later in the book breaks if you skip this chapter. But some people, once they hear that you can describe a shape by typing instead of dragging, want to try it immediately. If that is you, read on. If it is not, jump straight to Chapter 11 and never look back. You lose nothing.
What OpenSCAD is
OpenSCAD is a free CAD program with one unusual idea at its heart: instead of pushing and pulling shapes with the mouse, you write a few lines of a small scripting language that describe what you want, and the program builds the shape from your description. The window is split. On the left you type code. On the right you see the 3D model that your code produces.
There are two ways to see your model:
- Press F5 for a preview. This is fast and rough, good for checking that the shape looks roughly right while you work.
- Press F6 for a full render. This is slower and exact. You render once you are happy, because rendering is what lets you export.
When the render looks correct, you choose File, Export, Export as STL (or click the STL button on the toolbar). That hands you an STL file, the same kind of file Tinkercad exports, which you then open in your slicer (Chapter 5) to prepare it for printing. So OpenSCAD slots into exactly the same place in your workflow as Tinkercad. Only the way you create the shape is different.
Don't be confused. "OpenSCAD code" and "G-code" are two completely different kinds of code, and they live at opposite ends of the process. OpenSCAD code is design code: you run it and it produces a 3D model, which you export as an STL (the shape). G-code is machine code: the slicer reads your STL and writes thousands of lines telling the printer where to move the nozzle, how fast, and how much plastic to push (the motion). You write OpenSCAD code by hand. You almost never write or read G-code by hand; the slicer generates it for you. One describes what the object is, the other describes how the printer builds it.
Why code is a wonderful fit for this project
Here is the reason this chapter exists at all: OpenSCAD is parametric. That word means you define your shape in terms of named numbers (called variables or parameters), and the whole model is calculated from them. Change one number at the top, render again, and every part of the model that depended on it updates at once.
For a lemon squeezer this is close to magic. Lemons vary. The one in your fruit bowl might be 58 mm across; the big knobbly ones at the market might be 70 mm. If your design is built around a parameter called lemon_d (lemon diameter), then resizing the entire squeezer for a different lemon, or for a lime, or for a small orange, is a matter of typing a new number and pressing F6. No re-drawing, no measuring a dozen separate features and adjusting each by hand. That single idea is what makes the code path pay off in Chapter 13, where we turn measurements into a model, and again in Chapter 16, where we tweak the design after testing version one.
The handful of concepts you need
OpenSCAD has a lot of features, but you can build the squeezer with a small set. Here is each one, with a tiny example you could paste into an empty OpenSCAD window and preview with F5. Dimensions are in millimetres, the same unit your slicer expects.
Primitives are the basic solid shapes you start from:
cube([20, 10, 5]); // a box 20 wide, 10 deep, 5 tall
cylinder(h = 12, d = 8); // a cylinder 12 tall, 8 across
sphere(d = 10); // a ball 10 across
A cylinder can taper. If you give it two diameters, a bottom (d1) and a top (d2), it becomes a cone:
cylinder(h = 20, d1 = 16, d2 = 0); // a cone: wide at the bottom, a point on top
That tapering cylinder is the seed of the reamer, the ridged cone you twist a lemon onto.
Transforms move, turn, and resize a shape. Each one applies to the shape that follows it:
translate([0, 0, 10]) cube(4); // move 10 mm up the z (vertical) axis
rotate([0, 45, 0]) cube(4); // tip it 45 degrees around the y axis
scale([2, 1, 1]) sphere(5); // stretch it to twice as wide
The three numbers in the brackets are always x, y, z: left-right, front-back, up-down.
Boolean operations combine shapes. These are where the real modelling happens:
union() { cube(10); sphere(7); } // glue both into one solid
difference() { cube(10); sphere(7); } // start with the cube, subtract the sphere
intersection() { cube(10); sphere(7); } // keep only the part where they overlap
difference() is the one you will lean on most. It takes the first shape and carves away everything after it, which is exactly how you hollow out a bowl or cut a slot.
Variables give a number a name:
lemon_d = 62;
bowl_id = lemon_d + 6; // the bowl's inside is 6 mm wider than the lemon
Naming a dimension instead of typing a bare number everywhere is the whole trick behind reusability. Write bowl_id in terms of lemon_d, and the bowl grows automatically when the lemon does.
$fn controls how smooth curved surfaces look. OpenSCAD draws circles as many-sided polygons, and $fn is how many sides. A low number is faceted and fast; a high number is smooth and slower:
$fn = 12; sphere(10); // a chunky, low-poly ball
$fn = 96; sphere(10); // looks properly round
for() loops repeat something, which is perfect for evenly spaced features like the ridges around a reamer. This line places eight small cubes in a ring:
for (i = [0 : 7])
rotate([0, 0, i * 45]) translate([15, 0, 0]) cube(2);
Read it as: for each i from 0 to 7, turn by i times 45 degrees, step out 15 mm, and drop a cube. Eight steps of 45 degrees go all the way around the circle.
Modules are named, reusable sub-shapes, like little functions that draw something:
module pin() { cylinder(h = 8, d = 3); }
pin(); // draw one
translate([10, 0, 0]) pin(); // draw another, 10 mm over
Define a complicated shape once as a module, then call it by name wherever you need it. That keeps a model readable: the top of the file reads like a parts list.
That is the entire vocabulary. Primitives, transforms, booleans, variables, $fn, loops, and modules. With those seven ideas you can read every line of the squeezer.
The actual parametric squeezer
Here is the real model for the squeezer in this book. It is the file code/lemon_squeezer.scad, included directly so it always matches what we print. Open it in OpenSCAD and follow along.
// Portable lemon squeezer, parametric, for OpenSCAD.
// ---------------------------------------------------------------------------
// OpenSCAD is a free CAD program where you DESCRIBE a shape in code instead of
// drawing it by hand. Change a number at the top, press F5 to preview, F6 to
// render, then "Export as STL" to get a file your slicer can print.
//
// This model is the design we build up in Chapters 12 and 13: a ridged reamer
// cone sitting in a bowl, with a pour spout and a low strainer wall that holds
// back pulp and seeds. Every size is driven by one measurement: your lemon's
// diameter. Measure a lemon with a ruler, set lemon_d below, and the whole tool
// resizes to fit.
//
// NOTE: the dimensions here match code/squeezer_geometry.py for a 62 mm lemon.
// ---- Parameters you change -------------------------------------------------
lemon_d = 62; // diameter of your lemon, in mm (measure the real fruit)
juice_frac = 0.35; // not used in geometry, kept as a design note
wall = 2.4; // wall thickness, mm (6 lines at a 0.4 mm nozzle)
rib_count = 8; // number of juicing ridges on the reamer cone
strainer_gap = 1.6; // slot width in the strainer, mm (smaller than a seed)
spout = true; // include the pour spout?
$fn = 96; // smoothness of curved surfaces
// ---- Derived dimensions (do not edit; they follow from lemon_d) ------------
reamer_base = 0.70 * lemon_d; // cone base diameter
reamer_h = 0.80 * (lemon_d / 2); // cone height
bowl_id = lemon_d + 6; // bowl inner diameter
bowl_od = bowl_id + 2 * wall; // bowl outer diameter
bowl_h = 0.55 * lemon_d; // bowl height
floor_t = 2.0; // bowl floor thickness
// ---- The ridged reamer cone ------------------------------------------------
module reamer() {
union() {
// central cone
cylinder(h = reamer_h, d1 = reamer_base, d2 = reamer_base * 0.12);
// ridges that tear the pulp, evenly spaced around the cone
for (i = [0 : rib_count - 1]) {
rotate([0, 0, i * 360 / rib_count])
translate([reamer_base * 0.32, 0, 0])
cylinder(h = reamer_h * 0.92,
d1 = reamer_base * 0.16, d2 = 0.6);
}
}
}
// ---- The bowl, with a juice channel and a strainer wall ---------------------
module bowl() {
difference() {
// outer bowl body
cylinder(h = bowl_h, d = bowl_od);
// hollow inside
translate([0, 0, floor_t])
cylinder(h = bowl_h, d = bowl_id);
// pour spout: a notch cut in the rim on one side
if (spout)
translate([bowl_od / 2 - wall, 0, bowl_h - 4])
rotate([0, 35, 0])
cube([8, 10, 8], center = true);
}
// strainer: short vertical fins across the bowl floor that hold back seeds
intersection() {
translate([0, 0, floor_t])
cylinder(h = 6, d = bowl_id - 2);
for (x = [-bowl_id/2 : strainer_gap + 1.2 : bowl_id/2])
translate([x, 0, floor_t])
cube([1.2, bowl_id, 12], center = true);
}
}
// ---- Assemble (reamer centered in the bowl) --------------------------------
module squeezer() {
bowl();
translate([0, 0, floor_t + 4]) reamer();
}
squeezer();
// To print the reamer and bowl separately (often prints cleaner), comment out
// "squeezer();" above and render just one at a time:
// bowl();
// reamer();
Let us walk through it from the top.
The parameters block. The first group of lines is the only part you are meant to edit. lemon_d is your measured lemon diameter and drives everything else. wall is how thick the walls are (2.4 mm, which is six passes of a standard 0.4 mm nozzle, a sturdy choice). rib_count is how many ridges the reamer has. strainer_gap is how wide the slots in the strainer are, kept smaller than a lemon seed so seeds cannot slip through. spout is a true/false switch for whether to include the pour spout. $fn is set to 96 so the curved surfaces render smoothly.
The derived dimensions. The next group calculates sizes from lemon_d and should be left alone. reamer_base and reamer_h set how wide and tall the cone is, as fractions of the lemon. bowl_id (inner diameter) is the lemon diameter plus a little clearance; bowl_od (outer diameter) adds two wall thicknesses on top of that; bowl_h is the bowl height; floor_t is how thick the bottom is. Because every one of these is written in terms of lemon_d, changing that single number at the top reshapes the whole tool. That is the parametric payoff, made concrete.
The reamer() module. This builds the ridged cone. Inside a union() (so the pieces fuse into one solid) it first makes the central cone with cylinder(h, d1, d2), wide at the base and nearly a point at the top. Then a for loop runs once per rib. Each pass rotates by an even slice of 360 degrees, steps outward from the centre with translate, and stands up a small tapered cylinder. The result is a ring of ridges evenly spaced around the cone, the teeth that tear the pulp as you twist the lemon.
The bowl() module. This is the catch basin, and it shows off difference(). The big difference() block starts with the solid outer cylinder (bowl_od across) and then subtracts two things: an inner cylinder lifted up by floor_t, which hollows out the bowl while leaving a floor, and (only if spout is true) a small tilted cube cut into the rim on one side, which forms the pour notch. After that comes a second piece using intersection(): a set of thin upright fins crossing the floor, kept only where they overlap a short cylinder, so the fins sit inside the bowl and stop at its wall. Those fins are the strainer that holds back seeds and pulp while juice runs between them.
The squeezer() module. This assembles the tool. It calls bowl(), then translates the reamer() up by floor_t + 4 so the cone sits centred on the bowl floor. The final line, squeezer();, is what actually draws the whole thing. The comment at the very bottom shows how to render the bowl and reamer separately if you would rather print them as two pieces, which often comes out cleaner.
To use it: set lemon_d to your own measured lemon (measure across the widest part with a ruler), press F5 to preview, press F6 to render, then Export as STL. Take that STL to your slicer and carry on exactly as you would with a Tinkercad export.
What you will see (illustrative)
OpenSCAD's output is a 3D model on your screen, not text, so there is nothing to paste into a box here the way we do with Python output elsewhere in the book. A screenshot would not survive being printed in this text either. So instead, here is a description of what should appear in the preview window, and please treat it as illustrative rather than exact, since the precise look depends on your lemon_d and your viewing angle:
You should see a round, open bowl, a little wider than a lemon, with a low wall. Standing up in the middle of the bowl is a pointed cone wrapped in a ring of small ridges, like a citrus reamer. Across the bowl floor, around the base of the cone, sit a few thin upright fins (the strainer). On one side of the rim there is a small notch cut downward (the pour spout). Spin the view around and you should be able to see daylight through the spout notch and the gaps between the strainer fins. If something looks wrong (the cone floating, the bowl solid with no hollow), that is your cue to re-read the module it came from before you print anything.
This was optional, and both paths meet up ahead
If the code in this chapter felt natural and even a little fun, wonderful: you now have a squeezer you can resize for any citrus by editing one line. If it felt like a detour you did not need, that is completely fine too. The Tinkercad route from Chapter 8 reaches the very same place, a printable STL of a squeezer, and the rest of the book is written so that either route works. Use whichever one you enjoy. The fruit does not care how the model was made.
Takeaways
- OpenSCAD is a free CAD tool where you describe a shape in a small scripting language instead of drawing it; press F5 to preview, F6 to render, then export an STL for your slicer.
- It is parametric: build the model from named variables (like
lemon_d) and one number change resizes the whole thing, which is ideal for fitting different lemons or other citrus. - The core vocabulary is small: primitives (
cube,cylinder,sphere), transforms (translate,rotate,scale), booleans (union,difference,intersection), variables,$fnfor smoothness,forloops, and modules. - A tapered
cylinder(withd1andd2) makes a cone, anddifference()carves hollows and slots: together they build the reamer and the bowl. - OpenSCAD code makes the model/STL; the slicer later makes the G-code. They are different kinds of "code" at opposite ends of the process.
- This whole chapter is optional. The Tinkercad path ends at the same printable STL.
👉 Code or clicks, you now have a way to make the shape. Before we design it for real, let us write down exactly what we are building and how we will know it works: the project brief, in Chapter 11.