Parametric design in OpenSCAD: build models that resize cleanly
Parametric design means building a model from named dimensions and the rules between them, so changing one number regenerates a correct part. In OpenSCAD every size is already a variable; this guide shows the structure that keeps a model correct when you, a slider or the CAD Agent change those values.
- Every dimension is a named parameter at the top, with a label comment above it.
- Derive dependent values from parameters; never type the same number twice.
- One module per feature, one top-level call to build the part.
- Guard limits with
assert(), then test every slider at its min and max.
Why parametric design pays off
- A bracket with
leg_lengthis both a 30 mm corner bracket and a 100 mm shelf bracket. - A cable clip with
cable_dfits a USB-C lead and a Cat 6 cable without a rewrite. - A gear with
teethandgear_modbecomes a whole gear family. - When a test print is 0.3 mm too tight, you change one number instead of redrawing.
The parametric workflow, step by step
Click each step as you finish it. Every step ends with something you can check.
- Write the requirements as numbers. Measure what the part attaches to and what must fit in it, and note which sizes will change. Check: you have a list of sizes in millimetres, with the variable ones marked.
- Put the parameters at the top. One
name = value;line per dimension, each with a label comment above it. Check: sliders appear in the editor's Adjust tab (Params in Advanced mode). - Load libraries after the parameters.
includeanduselines go below the last parameter. Check: the sliders are still there. - Derive everything else. Hole positions, gusset sizes and inner dimensions are expressions of the parameters. Check: no number in the geometry repeats a parameter's value.
- Give each feature its own module. Use descriptive names and put a
// @feature: nametag above each call. Check: the editor shows one chip per feature. - Build the part from one top-level call. Check: the file ends with a single call such as
bracket();, so it exports as one solid. - Add assert() guards. Protect minimum walls and impossible combinations. Check: a bad value stops the render with a clear message.
- Drag every slider to its minimum and maximum. Check: the part renders at every extreme and holes never break through an edge.
- Print a fit test. Print just the critical hole, slot or snap before the whole part. Check: the coupon fits, or you know which parameter to adjust.
Parameters that become sliders
// Box length (mm) [20:1:150]
box_length = 60; // [20:1:150]
// Box width (mm) [20:1:100]
box_width = 40; // [20:1:100]
// Box height (mm) [10:1:80]
box_height = 30; // [10:1:80]
// Wall thickness (mm) [1.2:0.2:4]
wall = 2; // [1.2:0.2:4]
difference() {
cube([box_length, box_width, box_height]);
translate([wall, wall, wall])
cube([box_length - 2 * wall, box_width - 2 * wall, box_height]);
}
In the PrintPal editor these lines become sliders in the Adjust tab (Simple mode) or the Params tab (Advanced mode). Dragging one re-renders instantly without an AI call. Desktop OpenSCAD's Customizer reads a slightly different format, which is why each line carries the range twice:
| PrintPal editor | Desktop Customizer | |
|---|---|---|
| Label | The comment line above the variable | The comment line above the variable |
| Range | [min:step:max] or [min:max] in the comment above | A trailing comment on the same line, e.g. // [10:1:100] |
| Value types | Numbers, true/false, strings and lists of numbers | Literals, and vectors of up to 4 numbers |
| Stops reading at | The first include, use, module, function or shape line | The first { |
| Hiding a value | Write it as an expression, or define it after the first module | Put it after a /* [Hidden] */ comment |
An include on line 1 means no sliders. The PrintPal editor stops reading parameters at the first include or use. Libraries like BOSL2 go below the parameter block; see OpenSCAD libraries.
Naming parameters
- Spell names out:
plate_widthbeatspw, and it is what the slider shows if there is no label comment. - Put the unit in the label:
// Wall thickness (mm). OpenSCAD has no units; the convention for printing is millimetres. - Counts:
hole_count,tooth_count. Switches:has_lid,add_gusset. - Hardware: name the standard, as in
m3_clearance_d = 3.4. - Short names like
d,randhare fine as module arguments, where the context is obvious.
Derive values instead of typing them twice
If two values move together, write the dependent one as an expression of the other:
plate_w = 40;
hole_d = 5;
hole_x = plate_w / 2; // always centred, even after plate_w changes
difference() {
cube([plate_w, 20, 3]);
translate([hole_x, 10, -1]) cylinder(h=5, d=hole_d, $fn=32);
}
hole_x = 20; is right today and wrong as soon as plate_w changes.
hole_x = plate_w / 2; states the intent: the hole is always centred.
A file has plate_w = 40; and hole_x = 20;. You drag plate_w to 60. Where is the hole now?
A typed number never follows other parameters. Writing hole_x = plate_w / 2; keeps it centred, and as an expression it also stays out of the sliders.
One module per feature, tagged at the call
Wrap each feature in a named module, even ones used once, and assemble them in one place. You can then read, reuse or replace a feature without touching the rest. The modules and functions page covers the syntax.
In the PrintPal editor, a // @feature: name comment goes on the line above
the module call, not above the definition. Each tag becomes a chip: click it to
highlight that feature in 3D and add @feature[name] to your message, so the
CAD Agent knows exactly which part you mean. The full example below shows the layout.
Defaults with overrides
Give reusable modules sensible defaults and let each call override only what differs:
module m3_hole(depth=10, d=3.4) {
translate([0, 0, -1]) cylinder(h=depth + 2, d=d, $fn=32);
}
difference() {
cube([50, 15, 12]);
translate([10, 7.5, 0]) m3_hole(); // defaults
translate([25, 7.5, 0]) m3_hole(depth=20); // override one
translate([40, 7.5, 0]) m3_hole(d=3.0, depth=6); // tighter pilot hole, override both
}
Guard limits with assert()
assert(condition, "message") stops the render when a combination makes no
sense, instead of quietly producing a broken part. This is what a failing guard looks like:
thickness = 1;
assert(thickness >= 1.2, "Walls under 1.2 mm print poorly with a 0.4 mm nozzle.");
cube(10);
ERROR: Assertion '(thickness >= 1.2)' failed: "Walls under 1.2 mm print poorly with a 0.4 mm nozzle."
Good guards protect minimum walls, holes that would break through an edge, and features that collide. With a 0.4 mm nozzle, 1.2 mm (about three perimeters) is a safe minimum wall, and load-bearing walls are better at 2 mm or more; the 3D printing design guide has the details.
Echo a summary for yourself
An echo() at the end of the file prints key sizes on every render. In the
PrintPal editor it appears in the Console tab next to the Code tab in
Advanced mode. It is for you: the CAD Agent does not read echo output. It checks each
render's measured size and a preview image instead.
Full example: a parametric L-bracket
Everything above in one file: labelled parameters, derived values, guards, one module per
feature with @feature tags at the calls, a single entry point and an echo.
Drag leg_length from 40 to 100 and the holes and gusset follow.
// L-bracket: parametric corner mount for M3 screws
// === Parameters (each literal below becomes a slider) =========
// Leg length (mm) [15:1:120]
leg_length = 40; // [15:1:120]
// Bracket width (mm) [10:1:60]
width = 20; // [10:1:60]
// Wall thickness (mm) [2:0.5:8]
thickness = 4; // [2:0.5:8]
// Screw clearance hole (mm) [2:0.1:8]
hole_d = 3.4; // [2:0.1:8]
// Hole distance from the leg end (mm) [4:0.5:30]
hole_inset = 8; // [4:0.5:30]
// Corner gusset?
add_gusset = true;
$fn = 48;
// === Derived values (expressions never become sliders) ========
gusset_size = (leg_length - thickness) / 2;
gusset_thickness = thickness;
// === Guards ===================================================
assert(thickness >= 1.2, "Walls under 1.2 mm print poorly with a 0.4 mm nozzle.");
assert(hole_inset * 2 < leg_length, "hole_inset is too large for this leg_length.");
assert(hole_d < width - 2, "The hole is wider than the leg.");
// === Features =================================================
module horizontal_leg() { cube([leg_length, width, thickness]); }
module vertical_leg() { cube([thickness, width, leg_length]); }
module gusset() {
translate([thickness, (width + gusset_thickness) / 2, thickness])
rotate([90, 0, 0])
linear_extrude(height=gusset_thickness)
polygon([[0, 0], [gusset_size, 0], [0, gusset_size]]);
}
module mounting_holes() {
translate([leg_length - hole_inset, width / 2, -1])
cylinder(h=thickness + 2, d=hole_d);
translate([-1, width / 2, leg_length - hole_inset])
rotate([0, 90, 0]) cylinder(h=thickness + 2, d=hole_d);
}
// === Assembly =================================================
module bracket() {
difference() {
union() {
// @feature: horizontal_leg
horizontal_leg();
// @feature: vertical_leg
vertical_leg();
// @feature: gusset
if (add_gusset) gusset();
}
// @feature: mounting_holes
mounting_holes();
}
}
bracket();
echo(str("Envelope: ", leg_length, " × ", width, " × ", leg_length, " mm"));
Test the extremes, then print a fit test
- Every slider renders at its minimum and its maximum.
- Walls stay at 1.2 mm or more at every setting, and assert() enforces it.
- Holes and slots never break through an edge at the smallest size.
- Mating parts have clearance: start around 0.2 mm per side for a snug fit and tune from a test print.
- A small coupon of the critical fit has been printed and checked.
Anti-patterns
| Avoid | Because |
|---|---|
| Magic numbers in the geometry | You cannot change a size you cannot find. Name it. |
One 400-line difference() | Hard to read, impossible to reuse, and there is nothing to tag with @feature. |
Names like part1() or thing() | Future you and the CAD Agent both need to know what a module makes. |
include lines above the parameters | The PrintPal editor finds no sliders. |
| A range only in a trailing comment | Desktop OpenSCAD reads it, but the PrintPal editor only reads the comment above. |
| Renaming features mid-conversation | Earlier @feature[…] references in the chat stop matching. Add a new feature instead. |
Working with the CAD Agent
The CAD Agent already writes parameters at the top with a label comment above each and
puts @feature tags above module calls. To keep a model easy to edit over many
turns:
- Keep the
@featuretags when you edit by hand; they are how you point at parts in chat. - Ask for the parameters you care about: "make the hole spacing and wall thickness sliders".
- Comment intent, not mechanics: "rib must clear the M3 boss above" helps far more than "adds a 3 × 10 mm cube".
The AI CAD prompting guide has more ways to phrase requests.
Design a parametric L-bracket for M4 screws: 50 mm legs, 25 mm wide, 5 mm thick, with a triangular gusset and two 4.5 mm holes per leg. Put every dimension at the top as a labelled slider with a sensible range, add assert() checks for minimum wall thickness and hole edge distance, and tag each feature with @feature.
Make a parametric cable clip that screws to a desk edge. Parameters: cable diameter 4 to 12 mm, number of cable slots 1 to 6, clip width 10 mm, and one 3.5 mm countersunk screw hole. Keep 0.3 mm of clearance around each cable and walls at least 1.6 mm thick.
Frequently asked questions
What is parametric design?
Parametric design builds a model from named parameters and the relationships between them, so changing one value, such as a length or hole size, regenerates the whole part correctly. OpenSCAD is parametric by nature because every shape is defined in code.
How do I add sliders to an OpenSCAD model?
Put each parameter at the top of the file as a plain assignment, with a comment line above it holding a label and a [min:step:max] range. The PrintPal editor reads that comment. Desktop OpenSCAD's Customizer reads the range from a trailing comment on the same line, so write both to support both.
Why don't my parameters show up as sliders?
The PrintPal editor stops reading parameters at the first include, use, module or shape line, so anything below an include is missed. Values written as expressions, such as plate_width / 2, never become sliders either. Move plain value assignments to the very top of the file.
What minimum wall thickness should a parametric part allow?
With a 0.4 mm nozzle, 1.2 mm (about three perimeters) is a safe minimum for most walls, and load-bearing walls are better at 2 mm or more. Enforce the minimum with assert() so no slider setting can go below it.
How do I keep a variable out of the sliders?
In the PrintPal editor, only plain values at the top of the file become sliders, so write the variable as an expression or define it after the first module. Desktop OpenSCAD hides everything after a /* [Hidden] */ comment, which the PrintPal editor does not read.
Sources & further reading
- OpenSCAD User Manual — Customizer (description and range comments, tabs, [Hidden])
- OpenSCAD User Manual — User-defined functions and modules
Tools for thisAll tools
Watch it doneVideo library
Thanks! We read every note and use them to decide what to update next.