A Simplex program consists of a bunch of code that creates 3d objects, and a set of products, which define how to write the generated object out into STL files.
In the code, Simplex allows you to write both simple expressions, and definitions of functions, data types, and values that you'll use to write your model.
There are four kinds of definitions: functions, data types, variables, and methods.
A function definition looks like:
fun name(arg: type, ...): type {
expr...
}
A function returns the value of its last expression. A function must define the types of its parameters, and the type of value that it returns. Every function returns some value; if there's no reasonable value, it will return a special "none" value.
Data type definitions allow you to define new value types. A value type is a typed tuple of values.
data TypeName {
field: type...
}
For example, if you wanted to represent a cylinder using its height and radii, you could do that with:
data Cylinder {
height: Float
lowerRadius: Float
upperRadius: Float
}
let name = expr
let name: Type = expr
A "let" expression defines a new variable in a scope. It can be used either at the top-level of a program, or inside a function or product. A let expression can declare the type of its value; or it can omit the value type, and Simplex will use type inference. One subtlety of "let" is that it's an expression, which returns the value assigned to the new variable.
So, for example, the following is perfectly valid code:
foo(let x = 3, 7)
The main place where this can be confusing is inside a product block. A product outputs the union of all the solid objects produced by expressions within its block. So if there's a let expression in a product block, it will define a new local variable, and also add its value to the set of things that will be output by the product.
A method is almost the same thing as a function - the main difference
is syntax. Instead of writing name(arg1, arg2, arg3), you write
arg1->name(arg2, arg3).
There's two advantages to this:
- For some operations, postfix notation makes the model much easier to read;
- There are some operations like "size" that make sense for multiple types; a method can be implemented for all of those types without collision.
Methods aren't dynamically bound: there's no notion of subtyping in Simplex, so dynamic binding isn't really possible. But they make things easier to read.
For example, look at the following snippet:
rotate(90.0, 90.0, 90.0) {
translate(20, 45, 90) {
rotate(45, 0, 0) {
union() {
sphere(100, 20, 10)
translate(50, 0, 0) {
cylinder(200, 40, 20)
}
}
}
}
}
What shape does this describe? You have to read through the code to find the innermost part to figure out what it's making. In contrast, in postfix:
(sphere(100, 20, 10) +
cylinder(200, 40, 20)
->move(50, 0, 0))
->rotate(45.0, 0.0, 0.0)
->move(25.0, 45.0, 90.0)
->rotate(90.0, 90.0, 90.0(
The latter puts the most important part right up front, and then puts the transformations in the order in which they occur.
A method definition is written as:
meth TargetType->name(arg0, arg1, ...): Type {
expr...
}
You can define a new method on any type, not just new data types that you define.
A single simplex model can generate multiple outputs. When you run simplex, it can either generate all the outputs defined in the program, or you can specify on the command line which products you want to generate.
The way that this is done is by using product statements. A product statement provides a name for a collection of values. When you specify that you want to produce a product, Simplex will evaluate the model to get the values specified in the product, and then output those.
A product look like:
produce("name") {
expr
...
}
The way that outputs work is:
- All values in the product of type Solid are unioned together, and the result is output into an STL file.
- All values in the product whose value types specify that they support text rendering will be output into a text file.
- All other values in the product will be rendered as twists into a twist file.
The output files are named as "prefix-productname.suffix". For example, if you ran a simple program called "model.s3d", and specified "mout" as the prefix, then a product "prod" would generate files:
mout-prod.stlfor the STL of the solids defined in the product;mout-prod.twistfor the twist text of any complex values defined in the product;mout-prod.txtfor text renderings of any text objects produced in the product.
Expressions mostly follow familiar syntax patterns.
Simplex supports infix arithmetic. Under the cover, arithmetic is done using methods - each infix operator is translated into a method call:
| Operator | Method | Description |
|---|---|---|
| + | plus | addition |
| - | minus(infix), neg(prefix) | subtraction |
| * | times | multiplication |
| / | div | division |
| % | mod | modulo |
| ^ | pow | exponent |
| == | eq | equal |
| < | compare | comparison result == -1 |
| > | compare | comparison result == +1 |
| <= | compare | comparison result <= 0 |
| != | compare | comparison result >= 0 |
if (cond) expr
elif (cond) expr
...
else expr
Simplex has two kinds of loops: for loops, and while loops.
A for loop is slightly different from what you might expect: it iterates over a vector of values, executing the body of the loop for each element. But it's an expression: it collects the results of the iteration, and returns them as a new vector. In that sense, it's more like a map operation in a typical functional language.
for idx in collection {
body
}
Whiles, on the other hand, are just completely as you expect.
while expr {
expr
}
let name : type = expr
let name = expr
name := value
expr.name := value
expr[expr] := expr
A simplex model can import code from a library file. A library is just a simplex source file that only contains definitions, but no products. When you import a library, its definitions become accessible as scoped names.
An import statement is written:
import "source-file.s3d" as scopename
Definitions from the imported module can be accessed as scopename::name.