ShapeScript

Literals

Literals are values that you type in your ShapeScript file. They could be numbers, text or something else. Literals can be passed as parameters to options, functions or commands, or used in expressions. Here are some examples:

5 // an integer literal

37.2 // a decimal literal

false // a boolean literal

"hello" // a string literal

1 0 0 // a vector literal

#FF0000 // a hex color literal

Strings

Strings are how human-readable text is represented in ShapeScript. These are used for a variety of purposes, including specifying file names for imports or textures, logging debug information to the console, or even rendering 3D text.

String literals are delimited by double quotes (") to prevent ambiguity if the text matches a keyword or symbol, or contains spaces or other punctuation. If you want to use a double quote inside the text itself then it must be escaped using a backslash (\) character:

"hello \"Bob\" (if that's your real name)"

A line containing a string literal without a closing " is treated as an error, so if you need to include line-breaks in your string then these must also be escaped. Use the escape sequence \n (short for “new line”) to indicate a line-break:

"first line\nsecond line"

Because \ is used as the escape character, it must also be escaped if you want it to appear literally in the string. Use a double \\ in this case (there is no need to escape forward slashes (/) however):

"back slash: \\"
"forward slash: /"

Vectors and Tuples

A sequence of values separated by spaces defines a tuple. A tuple of numeric values is also known as a vector.

Vectors are used in ShapeScript to represent positions, colors, sizes and rotations. Many commands in ShapeScript accept a vector argument, and you can pass a tuple to these commands directly:

translate 1 0 0

You can also define a tuple value to use later:

define size 1 1 0.5

A tuple defined in this way doesn’t know if it’s going to be used as a vector - it’s just a sequence of numbers. We might guess from the name “size” that it will be used to set the size of something, but there’s nothing preventing you from using it as, say, a color value:

define size 1 1 0.5
color size // sets color to yellow

Tuples are’t limited to numbers. They can be comprised of any type of value (including other tuples), or a mix of different types:

define size 1 2 3
define myTuple2 "hello" 5.3 size

Tuple values can be accessed by index using ordinal members or subscripting:

define size 1 2 3

print size.second // prints 2
print size[0] // prints 1

To check if a tuple contains a particular value, you can use the in operator:

define values 1 2 3

if 2 in values {
    print "values includes the number 2"
}

You can also enumerate the values in a tuple using a for loop:

define values 1 2 3

for value in values {
    print value // prints 1 2 3
}

Structured Data

As well as simple values like numbers and text, it can sometimes be useful to group together sets of related data. Tuples can be nested arbitrarily to create complex data structures:

define matrix (1 2 3) (4 5 6) (7 8 9)

Parentheses are used here to indicate that this is a tuple of three nested tuples, and not a single tuple of nine numbers. To make this more readable, you can wrap the data over multiple lines inside outer parentheses, and even use comments if you wish:

define matrix (
    (1 2 3) // position
    (4 5 6) // scale
    (7 8 9) // orientation
)

Note: The line breaks have no semantic meaning here, they only serve to make the code more readable. Only the parentheses are used to determine the grouping.

Since no built-in commands in ShapeScript consume structured data like this, you need a way to access individual elements. You can do this in two ways:

To extract individual values from a tuple, you can use member syntax. If the tuple is numeric and shaped like a vector, size, rotation or color then you can use the x/y/z, width/height/depth, roll/yaw/pitch or red/green/blue/alpha members respectively:

define pos 1 2
print pos.x // prints 1
print pos.y // prints 2
print pos.z // prints 0

define size 3 4 5
print size.width // prints 3
print size.height // prints 4
print size.depth // prints 5

define rotation 0.5 0.25 0
print rotation.roll // prints 0.5
print rotation.yaw // prints 0.25
print rotation.pitch // prints 0

define col 1 0 0
print col.red // prints 1
print col.green // prints 0
print col.blue // prints 0
print col.alpha // prints 1

For more abstract data, you can use the ordinal members (first, second, third, … last) to access members by index:

define data (
    "cube" // name
    (1 2 3) // position
    #ff0000 // color
)

print data.count // prints 3
print data.first // name
print data.second // position
print data.last // color

Member expressions can be chained, so something like this will also work:

print data.second.x // x component of the position

You can split up lists of data using the allButFirst and allButLast members:

define data (1 2 3 4 5)

print data.allButFirst // prints 2 3 4 5
print data.allButLast // prints 1 2 3 4

For list-like data, you can use a for loop to loop over the top-level values:

define positions (
    (1 1 2)
    (2 1 2)
    (3 1 2)
)

for p in positions {
    cube {
        position p 
    }
}

You can even use nested loops to access sub-elements:

define matrix (
    (1 2 3)
    (4 5 6)
    (7 8 9)
)

for row in matrix {
    for column in row {
        print column // prints 1 to 9
    }
}

You can also access elements using a computed index via subscripting:

print matrix[0][2] // prints 3 (first row, third column)

Note: There is no requirement that rows in such a structure are the same type or length:

define data (
    (1 2 3)
    (10) // single element
    () // empty tuple
    ("hello" "world") // non-numeric
)

for row in data {
    for element in row {
        print element // prints 1, 2, 3, 10, "hello", "world"
    }
}

Objects

For more complex structured data you can use the object command to create an object with arbitrary, named members:

define data object {
    type "box"
    width 5
    height 10
    depth 15
}

To access the members of an object you can either use the dot operator or subscripting:

print data.height // prints 10
print data["height"] // also prints 10

Objects can be nested inside each other:

define data object {
    type "box"
    position 1 2 0
    dimensions object {
        width 5
        height 10
        depth 15
    }
}

print data.dimensions.height // prints 10
print data["dimensions"]["height"] // also prints 10

Attempting to access a non-existent member of an object will cause an error. To check if an object contains a particular member before you try to access it, you can use the in operator:

define axes object {
    x (1 0 0)
    y (0 1 0)
    z (0 0 1)
}

if "x" in axes {
    print "axes object contains an x component"
}

To enumerate all the members of an object, you can use a for loop. Each member is returned as a tuple of the key (member name) and value:

for row in data {
    print "key: " row.first ", value: " row.second
}

Note: object members are unordered, meaning that the order in which they are defined has no special significance. When looping through members of an object, the order will be alphabetical rather than reflecting the order in which the members were defined.


Index | Next: Symbols