Skip to content
Sign in

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.

PPSCAD
// 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.

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.

PPSCAD
param name: type = default [min:max:step] "Label" "Description"
PPSCAD
// 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"

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.

PPSCAD
let outer_r = teeth * module / 2
let offset = [10, 0, -5]
let part = cube(20, 10, 5)

Solids
CallMakes
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

A 2D shape becomes a solid through extrude or revolve; on its own it draws nothing.

Shapes
CallMakes
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
PPSCAD
// 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)

Solids combine by constructive solid geometry. Each takes a block of children.

PPSCAD
// 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)
}

A transform moves, turns, scales or mirrors its child: one statement or a block.

PPSCAD
// 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)

PPSCAD
// 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)

PPSCAD
// 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)
}

A function computes a value; a module makes geometry you can call by name.

PPSCAD
// 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)

Every operation is also a method on a shape, so a chain reads left to right.

PPSCAD
// 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 half

Trigonometry takes and gives degrees, as OpenSCAD does.

Functions
FunctionGives
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
Constants
NameValue
PI3.14159…
TAU6.28318…, twice PI
E2.71828…
INFInfinity

PPSCAD
// Arithmetic (+ also joins strings)
+  -  *  /  %  ^     // add, subtract, multiply, divide, remainder, power

// Comparison
==  !=  <  <=  >  >=

// Logic
&&  ||  !            // and, or, not

// Conditional expression
condition ? when_true : when_false

Primitives and transforms take their arguments by position, by name, or both.

PPSCAD
// 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)

PPSCAD
// One line

/* Several
   lines */

Every length is in millimetres, in the code and in the STL.

Parameters, a function, a loop, transforms, a condition and a boolean together.

PPSCAD
// 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)
        }
      }
}