Language Specification
This page documents features of Penrose Domain schemas and the Substance and Style languages not written up in the SIGGRAPH '20 paper, as well as changes made to these languages since the publication of our paper.
Subtype relationships can now be expressed using the following syntax:
type T
type S <: T -- declares that S is a subtype of T
The constructor syntax has been changed; it now looks like this:
constructor MyConstructor( Type1 x1, Type2 x2, ... ) -> ConstructedType
Strings have type string
and string literals are delimited by double quotes. Strings can be concatenated using the +
operator. For instance, to put parentheses around the label associated of x
, write
string fancyLabel = “(“ + x.label + “)”
A local variable is not attached to a Substance object, and so cannot be referred to by any other block that brings a Substance object in scope. Variables only live in the scope of the block and can have names reused in following blocks.
Within the scope of a block, local variables are immutable references (like const in JS) and can't be redefined.
Local variables can refer to anything in scope, e.g. things in other namespaces. Example:
Colors {
color red = rgba(1, 0, 0, 1) —- local var in namespace
}
forall Set X {
scalar pi = 3.14 —- local var in block
scalar pi = 2 * pi —- yields a compiler error
color myRed = lighten(Colors.red) // this is fine
scalar X.x = 2
scalar X.x = 3 —- this assignment is valid, since fields and properties are mutable references
shape X.shape = Circle {
x: X.x
r: pi * 100
color: myRed
}
}
forall Set `A` {
—- `A`.pi does not exist, so it doesn't make sense to write "override `A`.pi = ..."
scalar pi = 6.28; —- can define its own
...
override `A`.shape.r = pi * 100 —- Yields a different value
}
Style supports dense n-dimensional vector and matrix types, and standard operations on these types. These types behave largely like small, dense matrix types found in other languages (such as GLSL), with some specific differences noted below. Note that these types are meant largely for manipulating small 2D, 3D, and 4D vectors/matrices in the context of standard graphics operations (transformations, perspective projection, etc.), and may not perform well for larger matrix manipulations. Like all other objects in Style, the value of any vector or matrix entry can be declared as unknown (?
) and determined automatically via optimization by the layout engine.
Style is designed to support n-dimensional dense vectors of type vecN
, and square n-dimensional matrices of type matNxN
, where in both cases N
is an integer greater than or equal to 2. E.g., types commonly used for diagramming are vec2
and mat3x3
. The compiler may or may not accept other declarations (e.g., rectangular matrices); however, behavior on these types is currently undefined and unsupported. Some library functions may be available only for vectors or matrices of a specific size (e.g., the function cross(u,v)
, which computes a 3D cross product, assumes that both u
and v
have type vec3
).
A vector is constructed by specifying its components. For instance,
vec2 u = (1.23,4.56)
constructs a 2-dimension vector with x-component 1.23
and y-component 4.56
. As noted above, unknown values can be used as components, e.g.,
vec2 p = (?, 0.0)
specifies a point p
that sits on the x-axis
, with an undetermined x-coordinate. This coordinate will be determined by the optimizer, according to any constraints and objectives involving p
. More advanced initializers (e.g., initializing a 3-vector from a 2-vector and a scalar) are currently not supported, but are planned for future language versions. In most cases, the same functionality can currently be emulated by directly referencing components of a vector, e.g.,
vec3 a = ( b[0], b[1], 1.0 )
A matrix is constructed by specifying a list of vectors. Each vector corresponds to a row (not a column) of the matrix. For instance,
mat2x2 A = ((1,2),(3,4))
initializes a 2x2 matrix where the top row has entries 1, 2 and the bottom row has entries 3, 4. Rows can also reference existing vectors, e.g.,
vec2 a1 = (1, 2)
vec2 a2 = (3, 4)
mat2x2 A = (a1,a2)
builds the same matrix as above. As with vectors, matrix entries can be undetermined. E.g.,
scalar d = ?
mat3x3 D = ((d,0,0),(0,d,0),(0,0,d))
describes a 3x3 diagonal matrix, where all three diagonal entries take the same, undetermined value d
.
Individual elements of a vecN
can be accessed using square brackets, and an index i
between 0
and N-1
(inclusive). For instance,
vec3 u = (1,2,3)
scalar y = u[1]
will extract the y-coordinate of u
(i.e., y=2
). Matrix entries can likewise be accessed using double square brackets, e.g.,
mat2x2 M = ((?,?),(?,?))
scalar trM = M[0][0] + M[1][1]
constructs an expression for the trace of M
. In this case, since M
is not given explicitly in the Style program, the value of the trace will depend on the entry values of the optimized matrix.
As with any other variables in Style, assignment of vector or matrix entries is done in a functional manner (i.e. immutably in the computational graph).
The Style language supports the operations listed below on scalars, vectors, and matrices—here we will assume that c
has type scalar
, u
and v
have type vectorN
, and A
and B
have type matrixNxN
(all for the same N
).
Note that elementwise multiplication .*
and division ./
get applied independently to each entry of a vector or matrix. For instance, if u = (6,8,9)
and v = (3,2,3)
, then the elementwise division operation u ./ v
yields the vector (2,4,3)
(i.e., six divided by three, eight divided by two, and nine divided by three).
-
c * v
— scalesv
byc
(from the left) -
v * c
— scalesv
byc
(from the right) -
v / c
— dividesv
byc
-
c * A
— scalesA
byc
(from the left) -
A * c
— scalesA
byc
(from the right) -
A / c
— dividesA
byc
-
u + v
— sum ofu
andv
-
u - v
— difference ofu
andv
-
u .* v
— elementwise product ofu
andv
-
u ./ v
— elementwise quotient ofu
andv
-
A*u
— matrix-vector product Au -
u*A
— matrix vector product uᵀA
-
A * B
— matrix-matrix product AB -
A + B
— sum ofA
andB
-
A - B
— difference ofA
andB
-
A .* B
— elementwise product ofA
andB
-
A ./ B
— elementwise quotient ofA
andB
-
A'
— matrix transpose Aᵀ
A variety of standard vector and matrix functions are also available in Style. Note that this list may not be up to date; see the Style Functions documentation documentation for an automatically-generated list of all available functions.
-
dot(u,v)
— dot product of two n-vectors -
norm(u)
— norm of an n-vector -
normsq(u)
— squared norm of an n-vector, equal todot(u,u)
-
unit(u)
— unit vector parallel to u -
rot90(u)
— counter-clockwise 90-degree rotation of a 2D vector -
rotateBy(u,θ)
— rotates the 2D vector u by θ radians in the counter-clockwise direction -
cross2D(u,v)
— cross product of 2D vectors, giving the scalar u₁v₂ − u₂v₁ -
cross(u,v)
— cross product of 3D vectors -
outer(u,v)
— outer product of two n-vectors, giving the matrix uvᵀ -
angleBetween(u,v)
— in 2D, unsigned angle between vectors u and v, in radians -
angleFrom(u,v)
— in 2D, signed angle from u to v, in radians -
angleOf(u)
— in 2D, angle made by u with the positive x-axis
2D vectors are also be used natively as the representation for shapes' coordinates (start
, end
, and center
), with the appropriate getters and setters (e.g. A.shape.end[0]
). Example:
forall VectorSpace U { -- LinearAlgebra.sty
scalar axisSize = 1.0 -- this is a local var
vec2 U.origin = (?, ?) -- this is a globally-accessible var
vec2 o = U.origin
shape U.xAxis = Arrow { -- draw an arrow along the x-axis
start : (o.x - axisSize, o.y)
end : (o.x + axisSize, o.y)
}
}
Style supports optional type annotations on local variables and paths, e.g.
scalar v = 1.2
or
shape U.dot = Circle { ... }
.
These type annotations are not typechecked, they're for readability only. (We hope to do real typechecking in the future.)
The list of supported Style types is:
scalar
int
bool
string
path
color
file
style
shape
-
vec2
,vec3
,vec4
-
mat2x2
,mat3x3
,mat4x4
function
objective
constraint
It's an error if you name a variable/path something that conflicts with a type name (e.g. U.shape
is no longer allowed).
If you want to declare a variable as varying and initialize it at a constant float value, you can do it with VARYING_INIT(i)
(where i
is the float value you want). Example use:
forall Vertex V {
scalar V.val = VARYING_INIT(100.) —- Will be initialized at 100., then optimized
shape V.icon = Rectangle {
center: (V.val, VARYING_INIT(-200.)) —- Second coordinate will be initialized at -200., then optimized
}
This can only be used by itself in either fields or vectors, not in expressions (e.g. VARYING_INIT(i) + VARYING_INIT(j)
is not allowed).
In SIGGRAPH '20 the dimensions of the output SVG canvas were hardcoded to 800x700 pixels. The canvas width and height are now configurable by specifying numeric literal width
and height
values in the canvas
namespace like so:
canvas {
width = 800
height = 700
}
It is now possible to check whether a Substance object has been assigned a label in a Style selector. This kind of selector can be useful for, e.g., conditionally drawing label text. For instance, you can write a Style rule of the form
forall Point p
where p has label {
shape p.labelText = Text {
string: p.label
center: (?,?)
}
shape p.labelBox = Rectangle {
center: p.labelText.center
}
}
This rule will display the label and draw a rectangle around it, but only if p
was assigned a label in the .sub
program.
Label selectors can be refined by checking whether a math or text label was specified in the Substance program. Such selectors can be useful for conditionally typesetting labels using either an Equation
shape, which typesets TeX formulas via, MathJax, or a Text
shape, which typesets text via an SVG text element. A math label is a TeX string delimited by dollar signs, e.g.,
Label p $p_0$
whereas a text label is a plain-text string delimited by double quotes, e.g.,
Label p "a point"
In Style, these two different label types can be matched using the selectors
where p has math label
and
where p has text label
respectively.
Canvas dimensions must be defined in Style or else the Penrose compiler will throw an error.
Other than these fields' effects on the output canvas's dimensions and their restriction to numeric literals, the canvas
namespace acts like any other namespace: you can define the namespace anywhere in the program; you can override the fields; and you can add other fields to the namespace without any special restrictions.
More concretely, the following Style programs are valid:
...my cool Style code...
canvas {
width = 200
height = 30
}
...more of my awesome Style code...
canvas {
width = 800
height = 700
}
canvas {
override height = 800
}
canvas {
width = 800
height = 700
foo = ?
bar = (1., 1.)
}
The following Style programs are not valid:
...my cool style program but canvas dimensions are not defined!...
Error: Canvas properties are not defined. Try adding:
canvas {
width = <my desired width>
height = <my desired height>
}
canvas {
width = 100
}
Error: Canvas height is not defined.
Try adding: canvas { height = <my desired height> }
canvas {
width = Circle {}
height = 100
}
Error: Canvas width must be a numeric literal, but it is a shape.
canvas {
width = ?
height = 100
}
Error: Canvas width must be a numeric literal, but it is an expression or uninitialized.
canvas {
width = (1.0, 1.0)
height = 100
}
Error: Canvas width must be a numeric literal, but it has type Vector.
In SIGGRAPH '20, maxSize
implicitly used the minimum of the canvas's width and height to compute the maximum size of objects constrained by maxSize
. Since the canvas
update described above, the interface is now maxSize(variable, bound)
. The old functionality can be achieved by writing maxSize(variable, min(canvas.width, canvas.height))
.
Elements of a Style block (including shapes, constraints, and penalties) can be deleted in a later, cascading block. This feature can be helpful for, e.g., removing visual elements for a subtype. For instance,
-- by default, draw a circle for all instances of type T
forall T x {
x.widget = Circle { ... }
}
-- but don't draw this circle for instances of a subtype S <: T
forall S x {
delete x.widget
}
Note that one must be careful not to reference deleted attributes in a later generic block. For instance, the following block will produce an error if invoked for an instance of S:
forall T x {
shape x.newWidget = Circle {
center : x.widget.center -- not defined for instances of S
}
}
Found a problem or got a suggestion? Please open a GitHub issue and tag it with documentation
!