Documentation
PPSCAD language reference
Every statement, primitive, transform, operation, function and operator of the language.
This page is the quick reference. The language is defined by the PPSCAD 1.0 specification in its open-source repository, next to its grammar, its tests and its interpreter; where the two differ, the specification is right.
Quick start
// A box with a hole through it
param width: float = 30 [10:100] "Width (mm)"
param height: float = 20 [5:50] "Height (mm)"
param hole_r: float = 4 [1:15] "Hole radius (mm)"
difference {
cube(width, height, 10)
translate(width / 2, height / 2, -1)
cylinder(h: 12, r: hole_r)
}The parameters become sliders beside the viewer, and the model follows a slider as it moves. Every length is a millimetre; every angle a degree.
Parameters
A parameter is declared at the top level of the code with param. The range in brackets is min:max or min:max:step; the first string is the label, the second a description.
param name: type = default [min:max:step] "Label" "Description"// A whole number with a slider
param teeth: int = 20 [6:200:1] "Number of teeth"
// A decimal with its own step
param module: float = 2.0 [0.5:10:0.01] "Module (mm)"
// A switch
param chamfer: bool = true "Add a chamfer"
// A choice from a list
param style: choice = round ["round", "square", "hex"] "Head style"
// Text
param label: string = "PP" "Engraved text"Variables
let names a value: a number, a boolean, a string, a list, or a shape. A name lives in the block it is declared in: the body of a for, a transform, a boolean, a function or a module. An if is the exception: a let inside it is still there after it, which is how a value is replaced on a condition. A list is walked with for; it has no index.
let outer_r = teeth * module / 2
let offset = [10, 0, -5]
let part = cube(20, 10, 5)3D primitives
| Call | Makes |
|---|---|
| cube(size) | A cube with equal sides |
| cube(x, y, z) | A box |
| cube(size: [x, y, z], center: true) | A box centred on the origin, by named arguments |
| cylinder(h, r) | A cylinder of a height and a radius |
| cylinder(h: 20, r1: 5, r2: 3) | A cone, with a radius at each end |
| cylinder(h: 20, d: 10) | A cylinder by diameter |
| sphere(r) | A sphere |
| sphere(d: 20, segments: 64) | A sphere by diameter, with more facets |
2D shapes
A 2D shape becomes a solid through extrude or revolve; on its own it draws nothing.
| Call | Makes |
|---|---|
| circle(r) | A circle |
| square(w, h) | A rectangle |
| square(size, center: true) | A square centred on the origin |
| polygon([[x, y], …]) | A shape from points, listed counter-clockwise |
| polygon([outer, hole, …]) | A shape with holes, from several contours; a hole runs clockwise |
| text("ABC", size) | Text in the built-in font; extrude it for a label |
// A profile from points
polygon([[0,0], [20,0], [20,10], [10,10], [10,20], [0,20]])
// A polygon with a hole: the outside runs counter-clockwise,
// the hole clockwise
polygon([
[[0,0], [30,0], [30,20], [0,20]],
[[5,5], [5,15], [25,15], [25,5]]
])
// 3D text: 2D text, extruded
extrude(2) text("HELLO", 10)
// Text from a parameter
param label: string = "Part A" "Label"
extrude(1.5) text(label, 8)Booleans
Solids combine by constructive solid geometry. Each takes a block of children.
// Union: every child joined into one solid
union {
cube(10)
translate(5, 5, 0) sphere(8)
}
// Difference: the first child minus every later one
difference {
cube(20) // the base
translate(10, 10, -1) // what is taken away
cylinder(h: 22, r: 5)
}
// Intersection: only where every child overlaps
intersection {
cube(20, center: true)
sphere(14)
}Transforms
A transform moves, turns, scales or mirrors its child: one statement or a block.
// Move by x, y, z
translate(10, 0, 5) cube(10)
// Turn around the x, y and z axes, in degrees
rotate(0, 0, 45) cube(10)
// Scale the same on every axis, or per axis
scale(2) sphere(5)
scale(2, 1, 0.5) cube(10)
// Mirror across an axis
mirror(1, 0, 0) {
translate(5, 0, 0) cube(10)
}
// Chained: each transform applies to the next statement
translate(20, 0, 0)
rotate(0, 0, 45)
cube(10)Extrude and revolve
// A circle extruded into a cylinder
extrude(20) circle(5)
// With a twist and more slices
extrude(h: 30, twist: 90, slices: 50)
square(10, 10)
// A shape revolved around the Y axis
revolve()
translate(15, 0) circle(3)
// Part of a turn
revolve(angle: 180)
translate(10, 0) square(5, 8)Conditions and loops
// Geometry on a condition
if (add_holes) {
translate(10, 10, -1) cylinder(h: 22, r: 3)
}
// If and else
if (style == "round") {
sphere(10)
} else {
cube(16, center: true)
}
// A loop over a range [start:end], both ends included
for (i in [0:4]) {
translate(i * 15, 0, 0) cube(10)
}
// A range with a step [start:step:end]
for (angle in [0:30:330]) {
rotate(0, 0, angle)
translate(25, 0, 0)
cylinder(h: 10, r: 3)
}
// A loop over a list
for (size in [5, 10, 15, 20]) {
translate(size * 2, 0, 0) cube(size)
}Functions and modules
A function computes a value; a module makes geometry you can call by name.
// A function returns a value
fn gear_radius(teeth, module) = teeth * module / 2
// A function with a body
fn clamp(val, lo, hi) {
if (val < lo) { return lo }
if (val > hi) { return hi }
return val
}
let r = gear_radius(20, 2.5)
// A module makes geometry
mod rounded_box(w, h, d, r) {
hull {
translate(r, r, 0) cylinder(h: d, r: r)
translate(w - r, r, 0) cylinder(h: d, r: r)
translate(r, h - r, 0) cylinder(h: d, r: r)
translate(w - r, h - r, 0) cylinder(h: d, r: r)
}
}
rounded_box(40, 20, 5, 3)Methods
Every operation is also a method on a shape, so a chain reads left to right.
// Transforms as methods
cube(20, 10, 5)
.translate(5, 0, 0)
.rotate(0, 0, 45)
// Booleans as methods
let base = cube(30, 20, 10)
let hole = cylinder(h: 12, r: 4).translate(15, 10, -1)
base.subtract(hole)
// 2D to 3D as a method
circle(5).extrude(20)
square(10, 5).translate(15, 0).revolve()
// Offset: grow or shrink a 2D shape
circle(10).offset(2) // 2 mm outward
square(20, 10).offset(-1) // 1 mm inward
square(20, 10).offset(2, joinType: "miter") // sharp corners; round is the default
// Slice: the 2D cross-section of a solid at a height
let profile = sphere(10).slice(5)
// Trim: cut a solid by a plane; the side the normal points to stays
cube(20, center: true).trimByPlane(0, 0, 1, 0) // keeps the upper halfMaths
Trigonometry takes and gives degrees, as OpenSCAD does.
| Function | Gives |
|---|---|
| sin(deg), cos(deg), tan(deg) | Trigonometry, in degrees |
| asin(x), acos(x), atan(x) | The inverse, in degrees |
| atan2(y, x) | The angle of a point, in degrees |
| abs(x) | The absolute value |
| ceil(x), floor(x), round(x) | Rounding |
| sqrt(x) | The square root |
| pow(x, y) | A power; x ^ y does the same |
| ln(x), log(x) | The natural and the base-10 logarithm |
| exp(x) | e to the x |
| min(a, b, …), max(a, b, …) | The smallest and the largest |
| len(list) | The length of a list or a string |
| str(values…) | Values as one string |
| concat(a, b, …) | Strings joined |
| chr(code), ord(char) | A character from a code, and back |
| echo(values…) | A line in the console |
| Name | Value |
|---|---|
| PI | 3.14159… |
| TAU | 6.28318…, twice PI |
| E | 2.71828… |
| INF | Infinity |
Operators
// Arithmetic (+ also joins strings)
+ - * / % ^ // add, subtract, multiply, divide, remainder, power
// Comparison
== != < <= > >=
// Logic
&& || ! // and, or, not
// Conditional expression
condition ? when_true : when_falseNamed arguments
Primitives and transforms take their arguments by position, by name, or both.
// The same cylinder three ways
cylinder(20, 5)
cylinder(h: 20, r: 5)
cylinder(r: 5, h: 20)
// Positional and named together
cylinder(20, r: 5)Comments and units
// One line
/* Several
lines */Every length is in millimetres, in the code and in the STL.
A complete example
Parameters, a function, a loop, transforms, a condition and a boolean together.
// A twisted tower: layers that turn and taper
param layers: int = 20 [4:60] "Number of layers"
param size: float = 25 [10:50] "Base size (mm)"
param height: float = 80 [20:200] "Total height (mm)"
param twist: float = 90 [0:720:5] "Total twist (degrees)"
param taper: float = 0.5 [0.2:1.0:0.05] "Top to bottom ratio"
param hollow: bool = true "Hollow"
param wall: float = 2 [1:5:0.5] "Wall thickness (mm)"
fn lerp(a, b, t) = a + (b - a) * t
let layer_h = height / layers
for (i in [0:layers - 1]) {
let t = i / max(layers - 1, 1)
let s = lerp(size, size * taper, t)
let angle = twist * t
translate(0, 0, i * layer_h)
rotate(0, 0, angle)
translate(-s / 2, -s / 2, 0) {
if (hollow) {
difference {
cube(s, s, layer_h + 0.1)
translate(wall, wall, -0.05)
cube(s - wall * 2, s - wall * 2, layer_h + 0.2)
}
} else {
cube(s, s, layer_h + 0.1)
}
}
}