ShapeScript

Control Flow

Loops

To repeat an instruction (or sequence of instructions) you can use a for loop. The simplest form of the for loop takes a numeric range, and a block of instructions inside braces. The following loop creates a circle of 5 points (you might use this inside a path):

for 1 to 5 {
    point 0 1
    rotate 2 / 5
}

The range 1 to 5 is inclusive of both the start and end values. A range of 0 to 5 would therefore loop 6 times and not 5 as you might expect.

The loop range does not have to be a literal value, you can use a previously defined symbol or expression instead:

define count 5

for 1 to count {
    point 0 1
    rotate 2 / count
}

Loop Index

If you have used similar loops in other programming languages, you might be wondering why we don’t need to use an index variable of some kind to keep track of the loop iteration?

Symbols defined inside the { ... } block will not persist between loops (see scope for details), but changes to the world transform will, which is why the rotate command doesn’t need to reference the index - its effect is cumulative.

If you do need to reference the index inside your loop for some reason, you can define a loop index symbol like this:

for i in 1 to count {
    point 0 i
}

This defines a symbol called i with the value of the current loop iteration. The i symbol only exists within the loop body itself and can’t be referenced after the loop has ended.

Note: The index symbol does not need to be called i, it can be any valid symbol name that you choose.

If you want to loop in increments greater or less than 1, you can use the optional step property:

for i in 1 to 5 step 2 {
    print i // prints 1, 3, 5
}

for i in 0 to 1 step 0.2 {
    print i // prints 0, 0.2, 0.4, 0.6, 1
}

For numeric ranges, the step value defaults to 1.

Looping Backwards

If the end value of a loop is less than the start value, the loop body will normally be skipped, but if you do wish to loop backwards you can achieve this by using a negative step value:

for i in 5 to 1 step -1 {
    print i // prints 5, 4, 3, 2, 1
}

Looping Over Values

As well as looping over a numeric range, you can also loop over a tuple of values:

define values 1 5 7 9

for i in values {
    print i // prints 1 5 7 9
}

The values can be non-numeric, or even a mix of different types:

define values "Mambo" "No." 5

for i in values {
    print i // prints Mambo No. 5
}

Note: To use a list of values directly in the loop definition, they must be placed in parentheses:

for i in ("parentheses" "are" "required") {
    print i
}

Lists and tuples can be used with a step increment to enumerate every nth element:

define values 1 2 3 4 5

for i in values step 2 {
    print i // prints 1 3 5
}

A negative step selects elements in reverse order:

for i in values step -2 {
    print i // prints 5 3 1
}

Looping Over Paths

You can loop over the points in a path as follows:

for point in square {
    print point.position // 4 points
}

When looping over a closed path, the duplicate closing point is omitted. To iterate over all stored points, including closing points, use for point in path.points instead:

for point in square.points {
    print point.position // 5 points
}

Depending on how the path was produced, the control points might be widely or unevenly spaced, which may not produce the effect you want if you are using the points to position other geometry. To solve this, you can use step to interpolate new, evenly-spaced points along the path:

for point in square step 0.1 {
    print point.position // 40 points
}

The following example places small circles every 0.05 units along a pawn profile, then uses lathe to turn each circle into a ring:

detail 32

define pawn path {
    curve 0 0.78
    curve -0.15 0.7
    curve -0.15 0.5
    point -0.07 0.45
    curve -0.12 0.2
    point -0.25 0.1
    point -0.25 0
    point 0 0
}

lathe for point in pawn step 0.05 {
    circle {
        size 0.02
        position point.position
    }
}

Rings spaced along a pawn profile

You can use path subscripting to shift where the loop begins along the path. This could be used for animating features such as the caterpillar tracks in the tank example:

for point in square[from 0.025 step 0.1] {
    print point.position // 40 points, starting at distance 0.025 along the square
}

You can also select a section of path and then skip some of its sampled points, as follows:

for point in square[0.5 to 1.5 step 0.1].points step 2 {
    print point.position
}

See subscripting for distance ranges, negative steps and resampling behavior.

If-Else

Sometimes instead of repeating an action multiple times you need to perform it just once, but conditionally, based on some programmatic criteria. For example, your scene might have multiple configurations that you can switch between by setting constants at the top of the file.

To execute code conditionally, you can use an if statement:

define showCube true

if showCube {
    cube
}

The showCube constant here is a boolean that can have the value true or false. The condition for an if statement must always be a boolean expression. The body of the if statement will only be executed if the condition is true.

To perform an alternative action for when the condition is false, you can add an else clause:

if showCube {
    cube
} else {
    sphere
}

You can chain multiple conditional statements using the else if construct:

if showCube {
    cube
} else if showSphere {
    sphere
} else if showCone {
    cone
} else {
    print "Nothing to see here!"
}

For single-line if statements, you can omit the braces:

if showCube cube else sphere

This single-line form also works for commands with arguments:

if showText print "Visible"

Switch-Case

To select between multiple mutually-exclusive cases based on some value, you can use an else if chain, but it can be a bit cumbersome:

if value = 1 {
    // Do something
} else if value = 2 {
    // Do something else
} else if value = 3 {
    // Do something different
} else {
    // And so on ...
}

Instead, you can use a switch statement. This takes an input and performs one of several cases depending on its value:

switch value {
case 1
    // Do something
case 2
    // Do something else
case 3
    // Do something different
else
    // And so on ...
}

Note: If you are familiar with the C family of languages you’ll probably recognize this, but you should note that there are a few differences between the ShapeScript and C switch semantics:

Although there is no way to fall through from one case to another, you can group cases together using tuples to avoid repeating the case body:

switch value {
case 1 2
    print("Value was 1 or 2")
case 3
    print("Value was 3")
}

Conditional Defines

Something you might want to do with an if or switch statement is to conditionally define a constant value, for example:

define highlighted true

if highlighted {
    define cubeColor red
} else {
    define cubeColor white
}

cube cubeColor

Unfortunately this won’t work, due to the scope rules. The cubeColor symbol is only defined inside the if statement blocks themselves, and can’t be accessed outside. The solution is to move the if statement into the define itself:

define highlighted true

define cubeColor if highlighted {
    red
} else {
    white
}

cube { color cubeColor }

Control-Flow Expressions

if, switch, and for can be used anywhere a value is expected. An if or switch expression evaluates to the value produced by the selected branch, and a for expression evaluates to a tuple of the values produced by each iteration. This allows you to compute values conditionally, or build lists from loops without resorting to temporary symbols:

define dozen 12 + if isBaker { 1 } else { 0 }

define length switch type {
case "small"
    1
case "large"
    3
else
    2
}

define scales for i in 1 to 3 {
    i / 3
}

Index | Next: Blocks