Rather than pre-calculating the sizes and positions of your shapes, you can get ShapeScript to compute the values for you using expressions.
Expressions are formed by combining literal values, symbols or functions with operators.
Operators are used in conjunction with individual values to perform calculations:
5 + 3 * 4
ShapeScript supports all the standard infix arithmetic operators:
| Symbol | Name | Function |
|---|---|---|
| + | plus | Adds the left and right values |
| ‐ | minus | Subtracts the right value from the left value |
| * | times | Multiplies the left value by the right value |
| / | divide | Divides the left value by the right value |
| % | modulo | Remainder of dividing the left value by the right value |
Unary + and - are also supported:
-5 * +7
Operator precedence follows the standard BODMAS convention, and you can use parentheses to override the order of evaluation:
(5 + 3) * 4
Because spaces are used as delimiters in vector literals, you need to take care with the spacing around operators to avoid ambiguity. Specifically, unary + and - must not have a space after them, and ordinary infix operators should have balanced space around them.
For example, these expressions would both evaluate to a single number with the value 4:
5 - 1
5-1
Whereas this expression would be interpreted as a 2D vector of 5 and -1:
5 -1
ShapeScript includes the following equality and comparison operators, which can be used in conditional logic:
| Symbol | Name | Function |
|---|---|---|
| = | equal | Compares two values and returns true if they are equal |
| <> | not equal | Compares two values and returns false if they are equal |
| < | less than | Returns true if the left value is less than the value on the right |
| <= | less than or equal | Returns true if the left value is less than or equal to the right |
| > | greater than | Returns true if the left value is greater than the value on the right |
| >= | greater than or equal | Returns true if the left value is greater than or equal to the right |
Note: You may have used other languages where = is written as ==. This is generally because in such languages the = operator is used for assignment, and re-using the same symbol would cause ambiguity. This is not a problem in ShapeScript.
While these operators are typically used with numeric inputs, the output is a boolean value (true or false). These values are most commonly used in conjunction with with the if/else control flow statement. For example:
if rnd > 0.5 {
print "heads"
} else {
print "tails"
}
But they can also be assigned to a symbol and passed around:
define averageColor (color.red + color.green + color.blue) / 3
define isBrightColor averageColor >= 0.5
print isBrightColor // prints true or false
As well as operating on individual numbers, some operators can be used with vectors or tuples. To multiply or divide a tuple of numbers by a scalar value you can use:
define numbers (1 2 3 -4)
print numbers * 2 // prints 2 4 6 -8
You can also multiply two tuples:
define left (1 2 3)
define right (1 -2 3)
print left * right // prints 1 -4 9
Note that is a simple member-wise multiplication of the numbers. For other types of vector multiplication such as the dot or cross product see the functions section.
If the tuples have different lengths, the result will be truncated to the shorter of the two:
define left (1 2 3)
define right (1 -2)
print left * right // prints 1 -4
Rotations can also be multiplied. Multiplying two rotations composes them in order, which is useful when you want to build an orientation from separate rotation values:
define turn (rotation 0.5 1 0 0)
define tilt (rotation 0.5 0 1 0)
orientation turn * tilt
Multiplying or dividing a rotation by a number scales the rotation angle instead:
define halfTurn (rotation 0.5 0 1 0) / 2
orientation halfTurn
You can also add or subtract two lists of numbers together:
define left (1 2 3)
define right (1 -2 3)
print left + right // prints 2 0 6
Unlike with multiplication or division, adding or subtracting a shorter tuple from a longer one will preserve the length of the left side:
define left (1 2 3)
define right (1 -2)
print left + right // prints 2 0 3
Adding a longer tuple to a shorter one will not widen the result however:
define left (1 2 3)
define right (1 -2 3 4)
print left + right // prints 2 0 3
Along with the standard arithmetic operators, ShapeScript also has boolean operators for implementing logical operations.
Not to be confused with the boolean geometry functions for working with 3D solids, ShapeScript’s boolean operators work with true or false values, and are predominantly used in conjunction with if/else control flow statements.
ShapeScript supports the common boolean operators:
| Operator | Function |
|---|---|
| and | Compares two values and returns true if they are both true |
| or | Compares two values and returns true if either one is true |
| not | Returns false if the expression to the right is true, and true if it’s false |
Unlike some languages, ShapeScript’s boolean operators are implemented as keywords rather than symbols like && or ||, so control flow statements read more like sentences:
if a and b {
print "both a and b were true"
}
These can be combined into more complex expressions, and used in conjunction with parentheses for disambiguation:
if (not a) and (b or c) {
print "a was false and either b or c were true"
}
Another type of expression you can create is a range expression. This consists of two numeric values separated by a to keyword:
1 to 5
Ranges are mostly used in for loops:
for i in 1 to 5 {
print i
}
But they can also be assigned to a symbol using the define command, and then used later:
define loops 1 to 5
for i in loops {
print i // prints 1, 2, 3, 4, 5
}
You can use subscripting for indexing and slicing ranges. The in operator is used to check if a range contains a particular value:
define range 1 to 5
if 2.5 in range {
print "range contains 2.5"
}
Note: Ranges are inclusive of both the start and end values, so a loop from 0 to 5 would loop 6 times and not 5 as you might expect.
Range values can be fractional and/or negative:
for i in 0.2 to 2.2 {
print i // prints 0.2, 1.2, 2.2
}
for i in -3 to -1 {
print i // prints -3, -2, -1
}
Applying step to a range produces a sequence that defines how the range will be enumerated:
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
}
You can apply step to an existing range or sequence to update the step value:
define loops 1 to 5 step 3
for i in loops {
print i // prints 1, 4
}
for i in loops step 2 {
print i // prints 1, 3, 5
}
A negative step can be used to create a backwards loop:
for i in 5 to 1 step -1 {
print i // prints 5, 4, 3, 2, 1
}
You can create a partial range using the from command. Partial ranges have a start value but no end:
define range from 5
print 4 in range // prints false
print 5 in range // prints true
print 5000 in range // prints true
Applying step to a partial range also produces a sequence:
define range from 5 step -1
print 0 in range // prints true
print 5 in range // prints true
print 6 in range // prints false
print -5000 in range // prints true
A sequence defines an enumeration over a range, path, list, or object. For loops automatically convert their input to a sequence. The step operator creates a sequence from a numeric range or a path without modifying the original value. Applying step again changes the increment, retaining the original source.
For numeric sequences, membership checks with in and switch patterns match values that fall within the range and align with its step increment:
define samples 1 to 6 step 2
print 3 in samples // true
print 4 in samples // false
Path sequences sample points along a path or set of subpaths, interpolating the point positions and orientations. A path sequence can be converted back to a path, so step can be used to increase or decrease the detail level of an existing path. Closed path sequences retain their closing point, but looping over a closed path or sequence skips the duplicate end point:
define samples square step 0.7
path { samples } // Connects the sampled points and closes the path
path { for point in samples { point } } // Leaves the sampled path open
Compound values like vectors and tuples and objects can be decomposed by using the dot operator to access individual components or members:
define vector 0.5 0.2 0.4
define yComponent vector.y
print yComponent 0.2
Like other operators, the dot operator can be used as part of a larger expression:
color 1 0.5 0.2
define averageColor (color.red + color.green + color.blue) / 3
print averageColor // prints 0.5667
To access members by index instead of name, you can use the ordinal members (first, second, third, … last):
define vector 0.5 0.2 0.4
print vector.first // prints 0.5
print vector.second // prints 0.2
print vector.last // prints 0.4
For strings, you can use the lines, words and characters members:
define sentence "The quick brown fox"
for word in sentence {
print word // prints each word on a new line
}
For paths you can access the bounds and points members. For each point you can access the position, isCurved and color:
// Print the points in a circle
for point in circle.points {
print "position: " point.position ", isCurved: " point.isCurved
}
For meshes you can access the name, bounds, volume, polygons, triangles and material members:
print cube.bounds.size // prints 1 1 1
print cube.volume // prints 1
print cube.polygons.count // prints 6
print cube.triangles.count // prints 12
For polygons you can get the bounds or center, use triangles to get the triangles that make up the polygon, or use points to access the individual vertices. For points you can access the position and color:
define triangle polygon {
color red
point 0 0
color green
point 1 0
color blue
point 1 1
}
// Print the vertex positions and colors
for point in triangle.points {
print "position: " point.position ", color: " point.color
}
Ranges have start, end and count members. Partial ranges only have start:
define limits 1 to 6
print limits.start // 1
print limits.end // 6
print limits.count // 6
Numeric [sequences(#sequences) have the same properties as ranges, plus a step member:
define samples limits step 2
print samples.start // 1
print samples.end // 6
print samples.step // 2
print samples.count // 3 (the values are 1, 3 and 5)
To access members via a computed name or index, see the subscripting section below. For more information about the members that are available to access on various data types, see structured data.
As discussed in the members and structured data sections, you can decompose a tuple or other compound value using the dot operator followed by the name or ordinal index of the element you wish to retrieve.
But it is often useful to be able to access a tuple’s members using an index that is computed at runtime rather than a hard-coded offset. ShapeScript supports this via a mechanism called subscripting.
Using square brackets ([ and ]) you can use a string value to access a named member of a value:
define vector 1 2 3
print vector["y"] // prints 2 - equivalent to vector.y
To access a member by its ordinal position, you can use a numeric subscript value instead of a string:
define vector 1 2 3
print vector[0] // prints 1 - equivalent to vector.first
Note: subscript indices start at zero, not one. This means that the last available member in a tuple will have an index of count - 1:
define foo 1 2 3 4
// print all the elements of foo
for i in 0 to foo.count - 1 {
print foo[i]
}
To access elements relative to the end of the tuple, you can use negative indices. An index of -1 is shorthand for count - 1:
define foo 1 2 3 4
print foo[-1] // prints 4
print foo[-2] // prints 3
Ranges and finite sequences also support indexing and slicing. Indices refer to the enumerated values: ranges use increments of one, while sequences use their specified step.
define limits 1 to 6
print limits[1] // 2
print limits[-1] // 6
print limits[1 to 3] // 2 3 4
define samples limits step 2
print samples[1] // 3
print samples[-1] // 5
print samples[0 to 1] // 1 3
Trying to access tuple elements outside the range -count to count - 1 will result in an error, as will attempting to access a named member that does not exist. To avoid the error you can check if a given member exists by using the in operator, as follows:
define purple 0.6 0 1
if "red" in purple {
print "the red value of purple is " purple["red"] // prints 0.6
}
You can also use a range or partial range of indices to access a subset of a tuple’s values’:
define foo 1 2 3 4 5
print foo[0 to 2] // prints 1 2 3
print foo[1 to 3] // prints 2 3 4
print foo[from 2] // prints 3 4 5
Geometry values support lookup of elements named with the name option, using either the dot operator or a string subscript:
define assembly group {
cube { name "body" }
circle { name "front wheel" }
}
assembly.body
assembly["front wheel"]
Use subscripts for names containing spaces or punctuation, or when the name needs to be computed at runtime:
define part "front wheel"
assembly[part]
Lookup searches all descendants of the target geometry. The returned geometry retains its inherited transform and materials. An unknown or ambiguous/duplicate name produces an error.