Introduction
The µcad programming language is a purely declarative so-called markup programming language, which means that a µcad program can be evaluated (like a mathematical equation) resulting in a graphical output.
Purpose of this document
This document is meant as an explaining reference of all features of microcad. So chapter-wise this book will explain them around some example code. This example code is automatically tested and gives us a bit of a guarantee, that those features we explain here, work like we say.
Contribute
You might directly contribute your changes or new texts to this book via our git repository or if you are not so familiar with git, just leave an issue at codeberg.org so we can implement your ideas.
❤️ Donate
If you like this project, you can help us spend more time on it by donating:
Preface
In the 2010s, 3D printing suddenly became accessible to almost everyone — whether in maker spaces or even at home. People began building their own DIY printers and writing software to control them. Naturally, these developers needed a platform to design 3D models that could be printed.
Around that time, someone came up with an idea: a programming language made specifically for people who prefer working with a keyboard rather than a mouse or graphics tablet. That language is called OpenSCAD. It turned out to be a great success, inspiring countless impressive projects.
We loved it too and created many “thingies” with it. However, as experienced software engineers, we also had a few points of critique. While OpenSCAD is easy to learn and has a syntax reminiscent of C, we felt the language could be improved in several ways:
- more specialization for creating graphics,
- better support for modular programming,
- strict typing and unit handling,
- a syntax closer to Rust than to C,
- a solid library system,
- plugin support for other programming languages,
- and a more powerful visualization concept.
Over the past few years, as we became more familiar with Rust, we started microcad as a fun side project. In 2025, we were fortunate to receive funding to develop a visualization plugin.
After more than a year of work, we’ve developed and partially implemented several concepts. There’s still plenty to do and many more ideas to explore — but we believe we’ve now reached a point where it’s time to share our work with others.
Automatic Tested Code Examples
Within this document all code examples are automatically tested.
Test result banners
Each code example has a banner nearby which indicates the current testing status of the code. If you click one of these banners you will jump directly into a specific test report.
The following banners indicate that the code is tested ok:
| Banner | Meaning |
|---|---|
| Ok | |
| Ok (with warnings) | |
| Fails intentionally |
The following banners occur if a test is still marked as todo but is ok already. This can be corrected by changing the documentation.
| Banner | Meaning |
|---|---|
| Marked as todo but is ok | |
| Marked as todo but fails intentionally |
If you see one of the following banners, we did something wrong. Either the example may be wrong or the µcad interpreter might have a bug.
| Banner | Meaning |
|---|---|
| Fails with errors | |
| Fails with wrong errors or warnings | |
| Is ok but was meant to fail | |
| Is ok but with wrong warnings | |
| Fails early while parsing |
The following banners occur if tests are marked as todo and so are not running successful.
| Banner | Meaning |
|---|---|
| Work in progress | |
| Work in progress (should fail) | |
| Work in progress (fails with wrong warnings) |
Test list
See this section for a list of tests within this document.
Program Structure
A µcad program primary consists of the following core elements:
This section just explains the core elements but each may contain various statements.
Source Files
Source files are simply files which contain µcad code.
Such files might have the extension .µcad, .mcad or .ucad.
A source file can include the following types of statements which we will all discuss within this book:
| Statement | Purpose | Example |
|---|---|---|
| expression | calculate values | x * 5; |
| assignment | store values | y = x; |
| const assignment | naming constants | const y = 1; |
| pub assignment | exporting constants | pub y = 1; |
| function | separate calculations | fn f() { } |
| workbench | build or transform 2D sketches and 3D parts | part P() { } |
| module | modularization of complex code | mod m { } |
| if | process conditions | if x > 1 { } |
| use | use elements from other modules | use m; |
| call | use functions and workbenches | f(); |
| comment | for documentation | // comment |
In its simplest form, a µcad program consists of a single file containing one or more of the above statements.
A source file can serve as both a module and a workbench.
You can use it to provide structure (for example, by organizing submodules) or
as a kind of workbench where you add statements to generate outputs—such as a
2D graphic or a 3D model.
The workbench section of the file is only evaluated if it is in the main file
(the one that microcad was called with).
The following examples illustrate this: a circle and a sphere are created, each with a radius of one centimeter.
// simply draw a circle
std::geo2d::Circle(radius = 1cm);
// simply draw a sphere
std::geo3d::Sphere(radius = 1cm);
Statements within a source file can represent either a 2D or a 3D model — but not both at the same time. Mixing 2D and 3D statements in the same file will result in an error:
std::geo2d::Circle(radius = 1cm);
std::geo3d::Sphere(radius = 1cm); // error: can't mix 2D and 3D
However, µcad programs can also be split across multiple files.
To include other files, the mod
statement is used…
Modules
A module is a way to organize and group code into logical units.
Modules help manage scope and control visibility (e.g., using pub to make
items public available).
In short:
- Modules define a namespace for your code.
- They can be nested to create hierarchies,
- declared with the
modkeyword (internal modules) - or in separate files (external modules).
- Control what is exposed to the outside world using
pub.
Example
The following example shows two nested internal modules carrying a public constant and a public function which then are used from outside the modules:
// define private module
mod my {
// define public module
pub mod math {
// define PI as a public constant value
pub const PI = 3.14159;
// define some public calculation function
pub fn abs(x: Scalar) -> Scalar {
if x < 0 { return -x; } else { return x; }
}
}
}
// use the function and the constant
x = my::math::abs(-1.0) * my::math::PI;
Internal Modules
Internal modules are modules which have been defined with the mod keyword
followed by a name and a body as the following example shows:
mod my_module {
// e.g. define a value
pub VALUE = 1;
}
Module names are always written in
lower_snake_case.
The following statements can be listed in a module:
| Statement | Purpose | Example |
|---|---|---|
| const assignment | naming constants | const y = 1; |
| pub assignment | exporting constants | pub y = 1; |
| function | separate calculations | fn f() { } |
| workbench | build or transform 2D sketches and 3D parts | part P() { } |
| module | modularization of complex code | mod m { } |
| if | process conditions | if x > 1 { } |
| use | use elements from other modules | use m; |
| call | use functions and workbenches | f(); |
| comment | for documentation | // comment |
External Modules
External modules are external source files.
For example if you put a second file beside your main source code file, you can easily import this second file.
mod second;
second::f();
// file: second.µcad
pub fn f() {}
By using mod second, in the first source file, microcad searches for either a
file called second.µcad or second/mod.µcad and loads it into a module.
Hint: Because external modules are source files, they may contain statements that are not allowed in internal modules. These statements (such as calls, expressions, or value assignments) will not be processed when including an external module.
Use Statements
When including code from other modules or
other source files the use of
fully qualified names (e.g. std:geo3d::Sphere or std:geo3d::Cube) may
produce much boiler plate code when using them often.
The powerful use statement solves this problem and gives you an elegant method
to import code from other sources.
The following example which uses two parts of std::geo3d shows the problem
with long names:
std::geo3d::Sphere(radius = 40mm);
std::geo3d::Cube(size = 40mm);
There are several ways to solve this with use:
Use Statement
Looking at the example below using use does not seem any shorter, but if we would use Sphere and Cube
more often this would shorten things quite a lot:
use std::geo3d::Sphere;
use std::geo3d::Cube;
Sphere(radius = 4mm);
Cube(size = 40mm);
Alternatively you can use the whole module geo3d at once and would get rid
of the std:: part within the names:
use std::geo3d;
geo3d::Sphere(radius = 40mm);
Use As Statement
Internally every use statement defines an aliases with an identifier
(e.g. Sphere) and a target symbol it points to (e.g. std::geo3d::Sphere).
If name conflicts occur a way to deal with this is to explicitly name the
identifier with use as:
use std::geo3d::Sphere as Disk;
Disk(radius = 4mm);
Of course you can use use as with a whole module:
use std::geo3d as space;
space::Sphere(radius = 4mm);
Use All Statement
The shortest way to use many symbols from one module is to use * as target.
The following example defines aliases for all symbols of std::geo3d.
use std::geo3d::*;
Sphere(radius = 4mm);
Cube(size = 40mm);
Torus(major_radius = 40mm, minor_radius = 20mm);
The disadvantage of using * is that it increases the risk of name conflicts
between your code and the aliased symbols, some of which you might not even use.
Public Use Statement
The pub use statement does not only make the target symbol visible within
the module in which it is defined, but from the outside too.
mod my {
pub use std::geo3d::Sphere;
pub use std::geo3d::Cube;
}
my::Sphere(radius = 4mm);
my::Cube(size = 40mm);
Functions
Functions provide a way to encapsulate frequently used code into sub-routines. These sub-routines can then be called to execute their code with a specific set of parameters.
The function definition starts with the keyword fn, followed by an
identifier, a parameter list, and a function body.
// define function print_error with text as parameter of type String
fn print_error( text: String ) {
// code body
std::print("ERROR: {text}");
}
print_error("first");
print_error("second");
- Output
-
ERROR: first ERROR: second
Default Parameters
Parameters can include default values. Default values are specified without an explicit type annotation, but their type is inferred from the unit of the provided value.
fn f(x: Scalar, y=1mm) -> Length {
x * y
}
std::debug::assert_eq([ f(2), 2mm ]);
std::debug::assert_eq([ f(2, 2mm), 4mm ]);
Functions may be declared within source files, modules or workbenches.
Function Results
Of course, functions can also return a value.
To do so, you need to define the return type (using ->) as shown in the following example:
fn square( x: Scalar ) -> Scalar {
x * x
}
std::debug::assert_eq([ square(8.0), 64.0 ]);
You may also use if to decide between different results.
fn pow( x: Scalar, n: Integer ) -> Scalar {
if n > 1 {
x * pow(x, n-1)
} else if n < 1 {
1.0 / x / pow(x, -n)
} else {
1.0
}
}
std::debug::assert_eq([ pow(8.0,2), 64.0 ]);
std::debug::assert_eq([ pow(8.0,-2), 0.015625 ]);
Of course returning a value twice is not allowed.
fn pow( x: Scalar, n: Integer ) -> Scalar {
if n > 1 {
x * pow(x, n-1)
} else if n < 1 {
1.0 / x / pow(x, -n)
}
1.0 // error: without else this line would return a second value
}
std::debug::assert_eq([ pow(8.0,2), 64.0 ]);
std::debug::assert_eq([ pow(8.0,-2), 0.015625 ]);
Early Return
It is also possible to implement an early return pattern with the return
statement.
fn pow( x: Scalar, n: Integer ) -> Scalar {
if n > 1 {
return x * pow(x, n-1);
}
if n < 1 {
return 1.0 / x / pow(x, -n);
}
1.0
}
std::debug::assert_eq([ pow(8.0,2), 64.0 ]);
std::debug::assert_eq([ pow(8.0,-2), 0.015625 ]);
Module Functions
A module can contain functions that are accessible within
the module.
By declaring a function as public using the keyword pub, it becomes
available for use outside the module.
// module math
mod math {
// pow cannot be called from outside math
fn pow( x: Scalar, n: Integer ) -> Scalar {
if n == 1 {
x // return x
} else {
x * pow(x, n-1) // return recursive product
}
}
// square is callable from outside math
pub fn square(x: Scalar) -> Scalar {
// call internal pow
pow(x, 2)
}
}
// call square in math
math::square(2.0);
math::pow(2.0, 5); // error: pow is private
Workbench Functions
A workbench can contain functions. These functions can be used within the workbench, but not from outside it.
Here is an example which generates a punched disk of a given radius using a
function inner():
sketch PunchedDisk(radius: Length) {
use std::geo2d::Circle;
// function to calculate inner from radius
fn inner() { radius/2 }
// generate donut (and call inner)
Circle(radius) - Circle(radius = inner());
}
PunchedDisk(1cm);
Restrictions
There are some restrictions for workbench functions:
No public workbench functions
Trying to make them public with the keyword pub will result into an error:
sketch PunchedDisk(radius: Length) {
use std::geo2d::Circle;
pub fn inner() { radius/2 } // error: cant use pub fn inside workbench
Circle(radius) - Circle(radius = inner());
}
PunchedDisk(1cm);
No prop in workbench functions nor initializers
You cannot create workbench properties in function bodies.
sketch PunchedDisk(radius: Length) {
use std::geo2d::Circle;
fn inner() {
prop hole = radius/2; // error: prop not allowed in function
hole
}
prop hole = radius/2; // correct prop definition
Circle(radius) - Circle(radius = inner());
}
PunchedDisk(1cm);
Also the prop keyword is not allowed in initializers.
Instead, the properties of the building plan must be set directly, without
using the prop keyword.
sketch PunchedDisk(radius: Length) {
use std::geo2d::Circle;
init(diameter: Length) {
prop radius = diameter/2; // error: prop not allowed in init
}
init(d: Length) {
radius = d/2; // correct way to set radius
}
// Accessing property in a function is ok
fn inner() { radius/2 }
Circle(radius) - Circle(radius = inner());
}
PunchedDisk(diameter=1cm);
PunchedDisk(d=1cm);
Workbenches
Workbenches are essential in µcad for creating and constructing 2D sketches, 3D parts, or performing operations on them. Every workbench is initialized with a building plan from which it generates 2D or 3D models.
In the following pages, you will first learn about the elements that compose a workbench of any type. Then, we will explore the different types of workbenches.
Workbench Elements
A workbench in general consists of the following elements:
- A leading keyword:
part,sketch, orop, - an identifier that names the workbench,
- a building plan defined by a parameter list following the identifier,
- optional initialization code, which is placed and executed before any initializer,
- optional initializers, offering alternative ways to initialize the building plan,
- optional functions, acting as local subroutines with their own parameters and code bodies,
- optional properties, accessible from outside and set from the inside,
- and typically some building code, which runs after all initialization steps and generates the final objects.
The following code demonstrates (on the basis of a sketch) most of these elements which we will discuss in the following pages in detail.
// Sketch with a `radius` as building plan.
// Which will automatically become a property.
sketch Wheel(radius: Length) {
// Initialization code...
const FACTOR = 2;
// Initializer #1
init(diameter: Length) {
// must set `radius`
radius = diameter / FACTOR;
}
// No code in between!
// Initializer #2
init(r: Length) {
// must set `radius`
radius = r;
}
// Function (sub routine)
fn into_diameter(r: Length) {
return r * FACTOR;
}
// Building code...
// Set a property which can be seen from outside.
prop diameter = into_diameter(radius);
// Local variable `r`
r = radius;
// Create a circle.
std::geo2d::Circle(r);
}
use std::debug::*;
// Call sketch with diameter.
d = Wheel(diameter = 2cm);
// Check radius property.
assert_eq([d.radius, 1cm]);
// Call sketch with radius.
r = Wheel(radius = 2cm);
// Check diameter property.
assert_eq([r.diameter, 4cm]);
r - d;
Building Plan
The building plan is defined by a parameter list that follows the workbench’s identifier. All parameters in that list will automatically become properties of the workbench when it is invoked. These properties can be accessed within the building code, inside functions, or externally.
The following code demonstrates this using a sketch with a single parameter
in the building plan, called radius, of type Length:
// sketch with a radius as building plan
sketch Wheel(radius: Length, thickness = 5mm) {
use std::geo2d::Circle;
// access property radius from the building plan
Circle(radius = radius + thickness) - Circle(radius)
}
// access property radius of a Wheel
w = Wheel(1cm);
// render Wheel
w;
// check if r is 5cm an thickness equals the default (1cm)
std::debug::assert_eq( [w.radius, 1cm] );
std::debug::assert_eq( [w.thickness, 5mm] );
Initializers
Initializers are a way to define alternative parameters to create the
building plan.
An Initializer is defined with the keyword init and a following parameter list.
One may define multiple initializers which must have different parameter lists.
However, if an initializer is used, all properties from the building plan must be initialized (except those with default values).
sketch Wheel(radius: Length, thickness = 5mm) {
use std::geo2d::Circle;
// initializer with diameter
init( diameter: Length, thickness = 5mm ) {
// must set property `radius` from building plan
radius = diameter / 2;
// `thickness` (from the building plan) does not need
// to be set, because it was automatically set by
// parameter of this initializer
}
// Now radius and thickness can be used
Circle(radius = radius + thickness) - Circle(radius)
}
// call with building plan
Wheel(radius=1.5cm, thickness=2mm);
// call with initializer and use default thickness
Wheel(diameter=1.5cm);
Restrictions
Building plan must be initialized
If the building plan is not fully initialized by an initializer you will get an error:
sketch Wheel(radius: Length, thickness = 5mm) {
use std::geo2d::Circle;
init( thickness: Length ) { } // error: misses to set radius from building plan
Circle(radius = radius + thickness) - Circle(radius) // error: radius is missing
}
Wheel(thickness = 1cm);
Building plan properties with default values
Parameters of a workbench’s building plan which have a default value do not need to be set in the initializers.
sketch Wheel(radius: Length, thickness = 5mm) {
use std::geo2d::Circle;
init(diameter: Length) {
radius = diameter / 2;
// thickness has been set automatically by the default in the building plan
}
Circle(radius = radius + thickness) - Circle(radius)
}
Wheel(diameter = 1cm);
Building plan cannot be accessed within initializers
You cannot read building plan items from within initializers.
sketch Wheel(radius: Length, thickness = 5mm) {
use std::geo2d::Circle;
init( diameter: Length, thickness = 5mm ) {
_ = radius; // error: cannot read radius here
radius = diameter / 2; // instead you need to set it
}
Circle(radius = radius + thickness) - Circle(radius)
}
Wheel(diameter = 1cm);
Initializers with parameters from building plan
If you use parameter names in an initializer which already are used in the building plan, they will automatically become properties and cannot be set second time.
sketch Wheel(radius: Length, thickness: Length) {
use std::geo2d::Circle;
init( radius: Length ) {
// radius property has already been set by building plan
radius = radius * 2; // error: it cannot be set a second time
thickness = 5mm;
}
Circle(radius = radius + thickness) - Circle(radius)
}
// Use initializer
Wheel(radius = 1cm);
Types must match when using a name from building plan in initializer parameters.
sketch Wheel(radius: Length, thickness: Length) {
use std::geo2d::Circle;
init( radius: Scalar, outer: Length ) { // error: radius is already a Length in building plan
thickness = outer - (radius * 1mm);
}
Circle(radius = radius + thickness) - Circle(radius)
}
// Use initializer
Wheel(radius = 1.0, outer = 1cm);
No code between initializers
It’s not allowed to write any code between initializers.
sketch Wheel(radius: Length, thickness = 5mm) {
use std::geo2d::Circle;
init( width: Length, thickness = 5mm ) { radius = width / 2; }
radius = 1; // error: code between initializers not allowed
init( height: Length, thickness = 5mm ) { radius = height / 2; }
Circle(radius = radius + thickness) - Circle(radius)
}
Wheel(radius = 1cm);
Initialization Code
If you use initializers you might place some initialization code on top of the workbench’s body (before the first initializer).
The initialization code is just allowed to define some constants which then can be used in all following code (including code within initializers and functions).
sketch Wheel(radius: Length) {
// init code
const FACTOR = 2.0;
// initializer
init(diameter: Length) { into_radius(diameter) }
// function
fn into_radius( diameter: Length ) {
// use constant FACTOR from init code
diameter / FACTOR
}
// set property diameter and use FACTOR from init code
prop diameter = radius * FACTOR;
// code body
std::geo2d::Circle(radius);
}
std::debug::assert_eq([ Wheel(radius = 5cm).radius, 5cm ]);
std::debug::assert_eq([ Wheel(radius = 5cm).diameter, 10cm ]);
If there are no initializers, the initialization code is just part of the building code.
Restrictions
Cannot access building plan in initialization code
sketch Wheel(radius: Length, thickness = 5mm) {
use std::geo2d::Circle;
const _ = radius * 2; // error: cannot use radius from building plan
init( diameter: Length ) { radius = diameter / 2; }
Circle(radius + thickness) - Circle(radius)
}
Wheel(radius = 1cm);
Building Code
The building code is executed after any initialization. Usually it produces one or many 2D or 3D objects on base of the given building plan.
sketch Wheel(radius: Length, thickness = 5mm) {
// building code starts here
use std::geo2d::Circle;
Circle(radius = radius + thickness) - Circle(radius)
}
Wheel(radius = 1cm);
If initializers were defined the building code starts below them.
sketch Wheel(radius: Length, thickness = 5mm) {
// initializer code
use std::geo2d::Circle;
// initializer
init( diameter: Length ) { radius = diameter / 2; }
// building code starts here
std::geo2d::Circle(radius);
}
Wheel(radius = 1cm);
Restrictions
Illegal statements within workbenches
It’s not allowed to use the sketch, part, op, return nor mod statements within workbench code:
sketch Wheel(radius: Length) {
sketch A() {} // error
part B() {} // error
op C() {} // error
}
sketch Wheel(radius: Length) {
mod m {} // error
}
sketch Wheel(radius: Length) {
return; // error
}
Properties
There are two ways to declare Properties:
- as parameter of the building plan or
- in an assignment within the build code by using the keyword
prop.
In the following example we declare a building plan which consists of a radius which will automatically be a property:
// `outer` will automatically become a property because
// it is declared in the building plan:
sketch Wheel(outer: Length) {
use std::geo2d::Circle;
// `inner` is declared as property
prop inner = outer / 2;
// generate wheel (and use property `outer` and `inner`)
Circle(radius = outer) - Circle(radius = inner);
}
// evaluate wheel
t = Wheel(1cm);
// extract and display `outer` and `inner` from generated wheel
std::print("outer: {t.outer}");
std::print("inner: {t.inner}");
- Output
-
outer: 10mm inner: 5mm
If you remove the prop keyword you will fail at accessing inner:
sketch Wheel(outer: Length) {
use std::geo2d::Circle;
// `inner` is declared as variable and may not be read
// from outside this workbench
inner = outer / 2;
Circle(radius = outer) - Circle(radius = inner);
}
t = Wheel(outer = 1cm);
// you can still extract and display `outer`
std::print("outer: {t.outer}");
// but you cannot access `inner` anymore
std::print("inner: {t.inner}"); // error
Restrictions
No prop within initializer
You may not define a property within an initializer.
sketch Wheel(radius: Length, thickness = 5cm) {
init(radius: Length, inner: Length) {
thickness = radius - inner;
prop center = radius - inner; // error: do not use prop here
}
prop center = radius - thickness / 2; // here it's ok
}
center = Wheel(radius = 1cm, inner = 0.5cm).center;
std::debug::assert_eq([ center, 1.75cm ] );
No prop within initialization code
Also you may not use prop within initialization code.
sketch Wheel(outer: Length) {
prop max = 100; // error: do not use prop here
init(inner: Length) {
outer = inner * 2;
}
}
Wheel(inner = 0.5cm);
Workbench Types
Workbenches come in three flavors:
| Type | Keyword | Input Model | Output Model |
|---|---|---|---|
| parts | part | none | 3D |
| sketches | sketch | none | 2D |
| operations | op | 2D or 3D | 2D or 3D |
Mostly you may start directly with part or with a sketch which you then
operate (with an op) into a part.
Parts
Parts are workbenches that are used to create 3D models.
They are named in PascalCase:
part MyPart( radius: Length ) {
use std::geo3d::*;
Sphere(radius) - Cube(radius)
}
MyPart(1cm);
Like all workbenches parts can have several workbench elements.
Restrictions
Parts cannot generate 2D models
You will get an error if you generate a 2D model with a part:
sketch MyPart( radius: Length) {
use std::geo2d::*;
Circle(radius) - Rect(radius); // error: Circle and Rect are 2D
}
MyPart(1cm);
Sketches
Sketches are similar to parts but in two dimensions only.
They may be extruded into three-dimensional parts by using
operations.
Sketches are named in PascalCase:
sketch MySketch( radius: Length) {
use std::geo2d::*;
Circle(radius) - Rect(size = radius);
}
MySketch(1cm);
The output is a 2D sketch:
Restrictions
Sketches cannot generate 3D models
You will get an error if you generate a 3D model with a sketch:
sketch MySketch( radius: Length) {
use std::geo3d::*;
Sphere(radius) - Cube(size = radius); // error: Sphere and Cube are 3D
}
MySketch(1cm);
Operations
Note
Do not confuse operations with operators
Operations process 2D or 3D geometries into 2D or 3D geometries.
Unlike sketches or parts they are named in snake_case.
Actual operations are workbenches that process input models into output models.
So the following nop operation would be a neutral operation which just
passes-through the original input model:
// define operation nop without parameters
op nop() { @input }
// use operation `nop` on a circle results in the same circle
std::geo2d::Circle(radius = 1cm).nop();
@input
@input is a placeholder to tell where the input nodes of the operation shall
be inserted.
An operation can have multiple children when they are bunched together in a
group.
In the following example punshed_disk awaits a group of exactly two children.
// define operation which takes multiple items
op punched_disk() {
// check number of input models
if @input.count() == 2 {
// make hole by subtracting both inputs
@input.subtract();
} else {
std::error("punched_disk must get exactly two objects");
}
}
// use operation punch_disk with two circles
{
std::geo2d::Circle(radius = 1cm);
std::geo2d::Circle(radius = 2cm);
}.punched_disk();
Like other workbenches operations can have parameters too:
// define operation which takes multiple items
op punch_disk(radius: Length) {
if @input.count() == 1 {
{
@input;
std::geo2d::Circle(radius);
}.subtract();
} else {
std::error("punch_disk must get one object");
}
}
// use operation punch_disk on a circle
{
std::geo2d::Circle(radius = 2cm);
}.punch_disk(radius = 1cm);
Expressions
An expression defines a value simply by a literal or by
combining multiple other expressions.
For example, we can multiply a quantity of 4 millimeters with a factor 5 and
assign it to a constant v with the following code:
v = 5 * 4.0mm;
std::debug::assert_eq([ v, 20mm ]);
The result of this expression would be 20mm like the test (see assert_eq)
proves.
In this form, the computed value is not yet used for anything, so the examples
above will not produce any visible output or effect.
Expression result types
In the above example v is of type Length because the expression 5 * 4.0mm
is the multiplication of a factor (without unit) and a length (in mm).
Scalar & Quantity results
In general units of quantities are calculated according to the operator and the units of the operands in an expression like the following examples show.
use std::debug::assert_eq;
// an integer multiplied with another one remains a integer
assert_eq([ 5 * 4, 20 ]);
// a scalar multiplied with another scalar or integer results in a scalar
assert_eq([ 5.5 * 4.5, 24.75 ]);
assert_eq([ 5.1 * 4, 20.4 ]);
assert_eq([ 5 * 4.1, 20.5 ]);
// a scalar multiplied with a length is a length
assert_eq([ 5 * 4mm, 20mm ]);
// two length multiplied with each another is an area
assert_eq([ 5mm * 4mm, 0.2cm² ]);
// dividing an area by a length is a length
assert_eq([ 20mm² / 4mm, 5mm ]);
Boolean results
Boolean operations (e.g. !, >, == or &&) lead to a boolean result.
// Using logical operators lead to a boolean result
std::debug::assert_eq([ 5mm > 4mm, true ]);
Even when using them with models:
// Using logical operators between models too
std::debug::assert_eq([ std::geo2d::Circle(1cm) == std::geo2d::Circle(10mm), true ]);
Only Boolean expressions (expressions with a boolean result) can be used to define conditions (see if statement).
Literals
Literals are the simplest form of expressions. In this section, we will look at some literals.
5; // Scalar literal
4.0mm; // Quantity literal
"Hello"; // String literal
There are several types of literals:
| Name | Encoding | Description |
|---|---|---|
| Integer | 64 bit1 integer | signed integer |
| Scalar | 64 bit1 float | signed floating point |
| Boolean | 1 bit bool | boolean |
| String | UTF-8 | Text |
| Quantities | 64 bit1 float | signed floating point (including type) |
Integer Literals
Integer literals contain a whole number with a sign (but without a unit). Here are a few examples:
50;
1350;
-6
Scalar Literals
Scalar literals contain a floating point number with a sign (but without a unit).
1.23;
0.3252;
.4534;
1.;
-1200.0;
12.0E+12;
50% // = 0.5
Boolean Literals
Booleans contain either the value true or false:
true;
false
String Literals
Strings are texts enclosed in quotation marks:
"Hello, World!"
Quantity Literals
Quantities are like scalars but with a unit and are widely used in microcad if you wanna draw something.
4.0mm; // 4 millimeters
5l; // 5 liters
4m²; // 4 square meters
4m2; // also 4 square meters
10°; // 10 degree angle
10deg // also 10 degree angle
You will find more details about quantities and units in this section.
Operators
Note
Do not confuse operators with operations
There are several operators which can be used to combine expressions with each other:
| Operator | Type | 1st Operand1 | 2nd Operand | Result Type | Description |
|---|---|---|---|---|---|
! | unary | B | - | same | Inversion |
- | unary | I Q A T | - | same | Negation |
+ | binary | I Q A T | compatible | same | Addition |
- | binary | I Q A T | compatible | same | Subtraction |
* | binary | I Q A T | compatible | same | Multiplication |
/ | binary | I Q A T | compatible | same | Division |
^ | binary | I Q | Integer | like 1st | Power |
& | binary | B | Boolean | Boolean | Logical AND |
| | binary | B | Boolean | Boolean | Logical OR |
> | binary | I Q | compatible | Boolean | Greater than |
>= | binary | I Q | compatible | Boolean | Greater or equal |
< | binary | I Q | compatible | Boolean | Less than |
<= | binary | I Q | compatible | Boolean | Less or equal |
== | binary | I Q A T | compatible | Boolean | Equal |
!= | binary | I Q A T | compatible | Boolean | Not equal |
Here are some examples of each operator:
use std::debug::assert_eq; // used for testing
assert_eq([ -5, 0 - 5 ]); // Negation
assert_eq([ 5 + 6, 11 ]); // Addition
assert_eq([ 5 - 6, -1 ]); // Subtraction
assert_eq([ 5 * 6, 30 ]); // Multiplication
assert_eq([ 5 / 6, 0.83333333333333333 ]); // Division
assert_eq([ 5 ^ 6, 15625 ]); // Power
assert_eq([ true & false, false ]); // Logical AND
assert_eq([ true | false, true ]); // Logical OR
assert_eq([ 5 > 6, false ]); // Greater than
assert_eq([ 5 >= 6, false ]); // Greater or equal
assert_eq([ 5 < 6, true ]); // Less than
assert_eq([ 5 <= 6, true ]); // Less or equal
assert_eq([ 5 == 6, false ]); // Equal
assert_eq([ 5 != 6, true ]); // Not equal
Operators & Arrays
Some of the operators listed above can be used with arrays too. There result then is a new array with each value processed with the operator and the second operand.
See Array Operators for more information.
-
B= Boolean,I= Integer,Q= Quantity,A= Array,T= Tuple ↩
Model Expressions
Things change when an expression consists of models instead of just values. We call this a model expression:
std::geo2d::Rect(1cm) - std::geo2d::Circle(radius = 3mm);
In this expression which consists of a subtraction operation of the results of
two calls to Rect and Circle.
Building a group (using curly braces) of both operands and applying the
builtin method subtract to it is equivalent to the above code:
use __builtin::ops::subtract;
{
std::geo2d::Rect(1cm);
std::geo2d::Circle(radius = 3mm);
}.subtract();
The following operations can be applied to 2D or 3D models:
| Operator | Builtin Operation | Description |
|---|---|---|
- | __builtin::ops::subtract | Geometrical difference |
| | __builtin::ops::union | Geometrical union |
& | __builtin::ops::intersect | Geometrical intersection |
Assignments
Whenever you use a more complex expression, it is often useful to store it behind a name so that it can be used once or multiple times elsewhere.
In µcad, stored values are always immutable, meaning that once a value has been stored behind a name, it cannot be reset in the same context. This is different from the variables known in other programming languages.
use std::math::sqrt;
a = 2cm;
b = 5cm;
c = sqrt(a*a + b*b);
std::print("{c}");
Tip
If a name starts with an underscore (like
_this_name) that suppresses any warning about if it is not in use.
There are several kinds of assignments for different purposes. A property for example is always related to a workbench so it can only be defined within one. Here is a complete list of all assignment types available in µcad and where they can be defined (✅) and where they are prohibited (❌):
| Target | Key-word | Source File | Module | Building Code | Func-tion | Initial-ization | Initial-izers |
|---|---|---|---|---|---|---|---|
| Value | - | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ |
| Model | - | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| Constant | const | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ |
| Public | pub | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Property | prop | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| Model | - | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ |
Value Assignments
A value assignment stores a value by a name on the stack. They can be placed in source files, building code, module functions or workbench functions.
The following example defines the variable a which from then is a reserved
name within the scope in which it was defined (locality).
In this case the source code file itself:
// source code file is the scope of a and b
use std::debug::*;
a = 5;
b = a * 2;
assert_eq([ a, 5 ]);
assert_eq([ b, 10 ]);
Locality
If you place a value assignment into a scope (using brackets {}), the defined
value is only available within that specific scope:
// source code file is the topmost scope #0
use std::debug::*;
// define a within scope #0
a = 5;
assert_eq([ a, 5 ]);
// scope #1
{
// define b within scope #1
b = a * 2;
// of course b is available at this point
assert_valid( b );
// scope #2
{
// b is available in scope #2 because #2 is within #1
assert_valid( b );
}
}
// a is still available
assert_valid(a);
// b not known here anymore
assert_invalid(b);
Restrictions
No Shadowing
So-called “shadowing” (reusing a name) is prohibited. This restriction is highly intentional because µcad follows a concept of strict immutability1 of all value definitions.
a = 5;
{
a = a * 2; // this works because we are in a new scope
std::debug::assert_eq([ a, 10 ]);
}
Another assignment of a variable with the same name without an additional scope is prohibited.
a = 5; // warning: unused local
a = a * 2; // error: a already defined in this scope
Not in modules
Value assignments are not available in modules:
mod my_module {
a = 1; // error
}
Not in initialization code
Value assignments are not available in workbenches’ initialization code:
sketch MySketch() {
a = 1; // error
init(_x : Scalar) {}
}
MySketch();
-
Reusing names would undercut the ability to connect identifiers to values (e.g. when displaying). ↩
Constant Assignments
Unlike values, constants are not stored on the stack but in the symbol table. For example this allows them to be accessed from within functions or workbenches in the same module where the constant is defined. Constants can be placed in source files, modules or initialization code.
const TEXT = "Hello";
mod my_module {
// constant assignment
const TEXT = "Hello my_module";
// public function
pub fn f() -> String {
TEXT
}
// public workbench
pub sketch MySketch(text: String) {
std::debug::assert_eq([ TEXT, text ]);
}
}
my_module::MySketch("Hello my_module");
std::debug::assert_eq([ my_module::f(), "Hello my_module" ]);
std::debug::assert_eq([ TEXT, "Hello" ]);
Additionally, constant assignments are permitted in the init code of a workbench, where value assignments are likewise prohibited.
sketch MySketch(text: String) {
// constant assignment in initialization code
const TEXT = "Hello";
init() {
text = TEXT;
}
std::print(text);
}
MySketch();
Restrictions
Uppercase naming
Constants are always written in UPPER_CASE.
const A = 1; // ok
const a = 1; // warning
const MyValue = 1; // warning
const MY_VALUE = 1; // ok
Ambiguous Names
A constant cannot be defined within the same module or workbench twice.
mod module {
const A = 5;
const A = 1; // error: A already defined in this module
pub mod another_module {
const A = 5; // ok
pub fn a() -> Integer { A }
}
pub sketch Sketch() {
const A = 5; // error: A is ambiguous
const A = 5; // error: A already defined in this workbench
}
}
std::debug::assert_eq([ module::another_module::a(), 5 ]);
module::Sketch();
Not in building code
Constant assignments cannot be used in building code (the code below any initializers).
sketch MySketch() {
init(_: Integer) {}
const MY_CONST = 1; // error: not allowed in building code
}
MySketch();
Constant assignments must be on top of the workbench code if initializers are not used.
sketch MySketch() {
const MY_CONST = 1; // allowed if no initializers
}
MySketch();
They cannot be placed below non constant assignments within in a workbench.
sketch MySketch() {
_i = 5; // any non const code
const MY_CONST = 1; // error: const not allowed then
}
MySketch();
Not in function
Constant assignments cannot be used in functions.
fn f() {
const MY_CONST = 1; // error: not allowed in functions
}
f();
sketch MySketch() {
fn f() {
const MY_CONST = 1; // error: not allowed in workbench functions
}
f();
}
MySketch();
Not in initializers
Constant assignments cannot be used in initializers.
sketch MySketch() {
init(_: Integer) {
const MY_CONST = 1; // error: not allowed in initializers
}
}
MySketch();
Public Assignments
Public assignments provide a value to the inner and the outer of a module.
mod my_module {
pub mod sub_module {
pub TEXT = "Hello";
}
}
std::print(my_module::sub_module::TEXT);
Restrictions
Not in workbenches
Using pub is not allowed within workbenches:
sketch MySketch() {
pub TEXT = "Hello"; // error
std::print(TEXT);
}
MySketch();
Not in functions
fn f() {
const MY_CONST = 1; // error: not allowed in functions
}
f();
Property Assignments
Property assignments define additional1properties of a workbench. The may appear anywhere within the building code of a workbench and can then be read from outside.
sketch MySketch(radius: Length) {
prop diameter = radius * 2;
std::geo2d::Circle(radius);
}
std::debug::assert_eq([ MySketch(5cm).diameter, 10cm ])
Restrictions
Not in source files
prop diameter = radius * 2; // error: not in source file
Not in functions
fn f() {
prop diameter = radius * 2; // error: not in functions
}
f();
Not in initialization code
sketch MySketch(radius: Length) {
prop diameter = radius * 2; // error: not in initialization code
init() { radius = 1; }
std::geo2d::Circle(radius);
}
std::debug::assert_eq([ MySketch(5cm).diameter, 10cm ])
Not in initializers
sketch MySketch(radius: Length) {
init() {
radius = 1;
prop diameter = radius * 2; // error: not in initializer
}
std::geo2d::Circle(radius);
}
MySketch(5cm)
-
Additional to properties which are automatically generated from a workbench’s building plan. ↩
Model Assignments
Model assignments look like value assignments but instead having a value on the right side they store a model.
m = std::geo2d::Circle(radius = 10mm); // assign the model of a circle into m
std::debug::assert_eq([ m.radius, 10mm ]); // access property radius of m
Using a model as a value or vice versa does not work without further operations.
m = std::geo2d::Circle(radius = 10mm); // assign the model of a circle into m
std::geo2d::Circle(radius = m); // error: cannot use m as value
Restrictions
No Shadowing
Like with value assignments so-called “shadowing” (reusing a name) is prohibited. Another assignment of a variable with the same name without an additional scope is prohibited.
Not in modules
Model assignments are not available in modules:
mod my_module {
a = std::geo2d::Circle(radius = 1mm); // error
}
Not in initialization code
Model assignments are not available in workbenches’ initialization code:
sketch MySketch() {
a = std::geo2d::Circle(radius = 1mm); // error
init(_x : Scalar) {}
}
MySketch();
Program Flow
µcad is a programming language designed for generating 2D and 3D models. It deliberately avoids common programming constructs such as loops or mutable variables, but instead offers an alternative concept we call Multiplicity.
This chapter explains how program flow in µcad works.
Start Code
A µcad program consists of one or multiple files.
Files can be added by the mod statement
or by adding library paths of external modules with command line options.
Currently1 every µcad program starts with the original input file
that was given at running microcad.
The top level code within this file is where microcad starts processing.
// start code begins here
use std::geo2d::*;
mod my_inner {
pub RADIUS = 10mm;
}
Circle( radius = my_inner::RADIUS );
// start ends begins here
-
In future µcad will get a package management and will have projects and toml files. ↩
Conditionals
The if statement can control the program flow by the result of conditions.
In general an id statement consists of the following elements in fixed order:
- an initial
iffollowed by the condition in form of an boolean expression - a block of code (in
{ .. }) which is processed if the condition is true - maybe one or more
else ifstatements with alternative conditions and code blocks - and last maybe an
elsestatement followed by a code block which got executed when no other condition wastrue.
Conditions lead to different executions paths for different cases.
Here is a simple example:
use std::geo2d::*;
x = 0; // output changes if you change that value
if x > 0 {
Circle(radius = 5mm);
} else if x < 0 {
Rect(10mm);
} else {
Hexagon(5mm);
}
if in workbenches
Inside a workbench block, an if statement can be used to select different shapes
or constructions depending on input parameters.
So in the following example all possible geometries are generated with parameter multiplicity
and put side by side with the operation std::ops::align.
use std::ops::*;
use std::math::*;
use std::geo2d::*;
sketch MySketch(x: Integer) {
if x > 0 {
Circle(radius = 5mm)
} else if x < 0 {
Rect(10mm)
} else {
Hexagon(5mm)
}
}
MySketch([-1,0,2]).align(X, 5mm);
if in expressions
If statements can also be used as an expression, evaluating to the value from the chosen branch.
use std::ops::*;
use std::math::*;
use std::geo2d::*;
sketch MySketch(x: Integer) {
outer = if x > 0 {
Circle(radius = 5mm)
} else if x < 0 {
Rect(10mm)
} else {
Hexagon(5mm)
};
outer - Circle(radius = 3mm)
}
MySketch([-1,0,1]).align(X, 5mm);
Calls
Workbenches and functions can get called, which just means there inner code gets executed. There are several types of calls which have some different effects or usage.
| Call… | Example | Input(s) | Output |
|---|---|---|---|
| function | value = my_function(..); | parameter list | Value |
| workbench | model = MySketch(..); | parameter list | Model1 |
| operation | new_model = model.my_operation(..); | Model1 & parameter list | Model1 |
Calling Functions
A call of a function consists of just the identifier and an argument list. and the result is a value:
// call function -2 and store result in x
x = std::math::abs(-2);
// check value
std::debug::assert_eq( [x, 2] );
Calling Workbenches
Workbenches can be called in the same way as functions except that the result is a model.
// call sketch Circle with a size and store object node in s
s = std::geo2d::Circle(diameter = 2cm);
std::debug::assert_eq([ s.radius, 1cm ]);
Calling Operations
Operations are called in a different way because they are always attached to a model which come out of workbenches.
// call square with a size and store object node in s
s = std::geo2d::Circle(radius = 2cm);
// translate object s
s.std::ops::translate(x = 1cm);
Surely this looks better when using thw use statement.
// use translate
use std::ops::translate;
// call square with a size and store object node in s
s = std::geo2d::Circle(radius = 2cm);
// translate object s
s.translate(x = 1cm);
Parameters & Arguments
Parameters
Important
Parameters in µcad are not positional (which means their order is irrelevant)!
Parameters in µcad always have a name which we often call identifier and a type.
// function with two parameters (`one` and `another`)
fn f( one: Integer, another: Length ) {
std::print("{one} {another}");
}
// call function with two parameters in arbitrary order
f(another = 2m, one = 1);
Arguments
Arguments are the values which are given in call. Each argument consists these elements:
- an optional identifier in
lower_snake_case - a value (e.g.
42,3.1415,"Hello") - a type (e.g.
Integer,Scalar,Length) - an optional unit that suits the type (e.g.
mmforLength,m²forArea)
Parameter can have defaults. Then the notation changes and the type is deduced from the default value.
// function with two parameters (`one`, `another`, and `one_more`)
fn f( one: Integer, another = 2m, one_more: Area ) {
std::print("{one} {another} {one_more}");
}
// call function with two arguments.
// One matches by name another by type and one_more by default.
f(one = 1, 5m²);
There are several ways in which parameters can match arguments.
Argument Matching
To match call arguments with function or workbench parameters, µcad employs a process known as argument matching.
Important
Parameters in µcad are not positional (which means their order is irrelevant)!
Instead of having so-called positional arguments, µcad has named arguments, which means
that every parameter and every argument must have an identifier.
Like x is in the following example:
fn f(x: Length) -> Length { x*2 }
std::debug::assert_eq([ f( x = 10m ), 20m ]);
Fortunately there are some facilities for your convenience, like:
All those are created to feel natural when using them together. If some explanations in the following sections may sound complicated, you might just go with the examples and “get the feeling”.
Match Priorities
A single parameter can match an argument in several ways, each with a defined priority. These priorities become important when calling workbenches which support overloaded initialization.
| Priority ⭣ high to low | Matches | Example Parameters | Example Arguments |
|---|---|---|---|
| Empty List | Empty arguments with empty parameters | () | () |
| Identifier | Match argument identifier with parameter identifier | (x: Scalar) | (x=1) |
| Shortened Identifier | Match argument identifier with shortened parameter identifier | (max_height: Scalar) | (m_h=1) |
| Type | Match argument type with parameter type | (x: Length) | (1mm) |
| Compatible Type | Match argument type with compatible parameter type | (x: Scalar) | f(1) |
| Default | Match parameter defaults | (x=1mm) | () |
The match strategy is to try all priorities in order from highest (Empty) to
lowest (Default) until all arguments match a parameter.
Match Empty List
Matches when both the arguments and parameters are empty.
fn f() {} // no parameters
f(); // no arguments
Match Identifier
The following example demonstrates calling a function f with each argument specified by name:
fn f( width: Length, height: Length ) -> Area { width * height }
x = f(height = 2cm, width = 1cm); // call f() with parameters in arbitrary order
std::debug::assert_eq([ x, 2cm² ]);
Match Short Identifier
A parameter can also be matched using it’s short identifier.
The short form consists of the first letter of each word separated by underscores (_).
fn f( width: Length, height: Length ) -> Area { width * height }
// use short identifiers
std::debug::assert_eq([ f(w = 1cm, h = 2cm), 2cm² ]);
// can be mixed
std::debug::assert_eq([ f(w = 1cm, height = 2cm), 2cm² ]);
Here are some usual examples of short identifiers:
| Identifier | Short Identifier |
|---|---|
parameter | p |
my_parameter | m_p |
my_very_long_parameter_name | m_v_l_p_n |
my_Parameter | m_P |
MyParameter | M |
myParameter | m |
Match Type
Nameless values can be used if all parameter types of the called function (or workbench) are distinct.
fn f( a: Scalar, b: Length, c: Area ) {} // warning: unused a,b,c
// Who needs names?
f(1.0, 2cm, 3cm²);
Match Compatible Type
Nameless arguments can also be compatible with parameter types, even if they are not identical.
fn f( a: Scalar, b: Length, c: Area ) {} // warning: unused a,b,c
// giving an integer `1` to a `Scalar` parameter `a`
f(1, 2cm, 3cm²);
Match Default
If an argument is not provided and its parameter has a default value defined, the default will be used.
fn f( a = 1mm ) {} // warning: unused a
// a has default
f();
Mix’em all
You can combine all these methods.
fn f( a: Scalar, b: Length, c=2cm, d: Length) -> Volume {} // warning: unused a,b,c,d
// `a` gets the Integer (1) which is compatible to Scalar (1.0)
// `b` is named
// `c` gets it's default
// `d` does not need a name because `b` has one
f(b=2cm, 1, 3cm);
Inline Identifiers
In some cases, the parameter name is already included in the argument expression. If there is only one (or multiple identical) identifier(s) within an expression and it matches a parameter of the same type, that parameter will be automatically matched.
fn f(x: Integer, y: Integer) -> Integer { x*y }
x = 1;
f(x, y=2); // matches because argument `x` matches the name of parameter `x`
Even when using a more complex expression a unique identifier can match an argument:
fn f(x: Integer, y: Integer) -> Integer { x*y }
x = 1;
y = 2;
f(x * 2, y * y); // matches because `x` and `y` match parameter names `x` and `y`
fn f(x: Integer, y: Integer) -> Integer { x*y }
x = 1;
y = 2;
f(x * y, y * x); // error: cannot be matched because arguments are not unique.
Multiplicity
A core concept of µcad is called Argument Multiplicity which replace loops like they are known from other languages.
When working with multiplicities, each argument can be provided as an array of elements of a parameter’s type. Each list element will then be evaluated for each of the array’s values. This way, we can intuitively express a call that is executed for each argument variant.
The following example will produce 4 rectangles at different positions:
r = std::geo2d::Rect(width = 2mm, height = 2mm);
r.std::ops::translate(x = [-4mm, 4mm], y = [-4mm, 4mm]);
Because in the above example x and y have two values each, the result
are four (2×2) calls:
r = std::geo2d::Rect(width = 2mm, height = 2mm);
use std::ops::translate;
r.translate(x = -4mm, y = -4mm);
r.translate(x = -4mm, y = 4mm);
r.translate(x = 4mm, y = -4mm);
r.translate(x = 4mm, y = 4mm);
Normally, this would require two nested for loops. As you may imagine, multiplicity can be a powerful tool.
Match Errors
Missing Arguments
If you do not provide all parameters, you will get an error:
fn f( x: Length, y: Length, z: Length ) {}
f( x=1cm, z=3cm); // error: y is missing here
Too Many Arguments
When you provide all parameters but some are redundant, you will get a error too:
fn f( x: Length, y: Length, z: Length ) {}
f( x=1cm, y=2cm, v=5cm, z=3cm); // error: Unexpected argument v
Ambiguous Arguments
If some arguments cannot be matched unambiguously to any of the parameters you will get an error.
fn f( x: Length, y: Length, z: Length ) {}
f( x=1cm, 5cm, 3cm); // error: Missing arguments y and z
Types
The µcad type system consists of a group of builtin types. The type system is static, which means a every value has a fixed type which cannot be changed or overwritten.
Here is a complete list of the builtin types:
| Type | Description | Type Declarations | Example Values |
|---|---|---|---|
| Boolean | Boolean value | Bool | true, false |
| Integer | Integer value without unit | Integer | 4, -1 |
| Scalar | Floating point value without unit | Scalar | 0.5, 50%, -1.23e10 |
| Quantity | Floating point value with unit | Length, Area, Volume, Density, Angle, Weight | -4mm, 1.3m2, 2cm², 23.0e5deg |
| String | UTF-8 text string | String | "Hello, World!" |
| Array | List of values with common type | [Integer] | [1,2,3], [1m,2cm,3µm] |
| Tuple | List of named values or distinct types | (Length,Scalar,Bool), (x:Scalar,y:Length), (x:Scalar,Length) | (4mm,4.0,true), (x=4.0,y=4mm), (x=4.0,4mm) |
| Model | Geometric 2D or 3D model | Model | std::geo3d::Cube(2mm) |
Declaration
The examples in the table above declare the type explicitly. However, we can use units to declare the implicitly. Using units is recommended and what you get in return is that declarations are quite handy:
x: Length = 4mm; // explicit type declaration
y = 4mm; // implicit type declaration via units.
Declarations without any initializations are not allowed in µcad. Hence, the following example will fail:
x: Length; // parse_error
However, for parameter lists in functions and workbenches, you can declare the type only but also pass a default value:
fn f(x = 4mm) {} // use unit (with default)
fn f(x: Length) {} // use type (without default)
Note
Find out more about what types are used for in the sections about argument matching and assignments.
Collections
Collections put many items into one type:
- Arrays store multiple values of the same type
- Tuples can store multiple values of different types which may or may not have names.
Arrays
Note
Arrays are quite involved in the multiplicity concept so you might want to read about this too.
Declaration
An array is an ordered collection of values with a common type.
a : [Integer] = [ 1, 2, 3, 4, 5 ];
b : [Scalar] = [ 1.42, 2.3, 3.1, 4.42, 5.23 ];
c : [Length] = [ 1.42mm, 2.3m, 3.1cm, 4.42cm, 5.23m ];
d : [String] = [ "one", "two", "three", "four", "five" ];
e : [Model] = [ std::geo2d::Circle(radius = 1cm),
std::geo3d::Sphere(radius = 1cm)
];
Of course these type declarations can be skipped (e.g. a = [ 1, 2, 3, 4, 5 ]).
Unit bundling
Array support unit bundling, which means the you can write a unit behind the brackets.
std::debug::assert_eq([ [1mm, 2mm, 3mm], [1, 2, 3]mm ]);
Single elements of the array can have special units of the same type.
std::debug::assert_eq([ [1mm, 2m, 3mm], [1, 2m, 3]mm ]);
Range Arrays
Alternatively a range expression can be used to generate an array with consecutive values.
Important
µcad ranges include start and end point! This is different in many other programming languages. So in µcad
[1..3]results in[1,2,3].
std::debug::assert_eq([[1..5], [1,2,3,4,5]]);
std::debug::assert_eq([[-2..2], [-2,-1,0,1,2]]);
The order of the endpoints of a range is important.
[6..1]; // error
[2..-2]; // error
Only Integer can be used as endpoint.
[1.0..2.0]; // parse_error
Array Operators
Arrays support the following operators.
| Operator | Description | Example |
|---|---|---|
+ | add value to every element | [1, 2] + 2 |
- | subtract value from every element | [1, 2] - 2 |
* | multiply every element with a value | [-1.0, 2.0] * 2.0 |
/ | divide every element by a value | [1.0, 2.0] / 2.0 |
- | negate every element | -[ 1, 2 ] |
! | invert every element | ![ true, false ] |
use std::debug::assert_eq;
assert_eq([ [1, 2] + 2, [3, 4] ]);
assert_eq([ [1, 2] - 2, [-1, 0] ]);
assert_eq([ [-1.0, 2.0] * 2.0, [-2.0, 4.0] ]);
assert_eq([ [1.0, 2.0] / 2.0, [0.5, 1.0] ]);
assert_eq([ -[-1.0, 1.0], [1.0, -1.0] ]);
std::debug::assert_eq([ ![true, false], [false, true] ]);
Tuples
A tuple is a collection of values, each of which can be of different type. Typically, each value is paired with an identifier, allowing a tuple to function similarly to a key-value store.
use std::debug::assert_eq;
tuple = (width=10cm, depth=10cm, volume=1l);
assert_eq([ tuple.width, 10cm ]);
assert_eq([ tuple.depth, 10cm ]);
assert_eq([ tuple.volume, 1l ]);
assert_eq([ tuple, (width=10cm, depth=10cm, volume=1l) ]);
Partially Named Tuples
A tuple may lack identifiers for some or even all of its values. In such cases, these unnamed values within the tuple must all be of different types.
(10cm, 10cm², 1l);
Otherwise, they would be indistinguishable since the values in a tuple do not adhere to a specific order.
Arbitrary Order
The order of values have no consequences for equality.
// these tuples are equal
std::debug::assert_eq([ (1l, 10cm, 10cm²), (10cm, 10cm², 1l) ]);
Arbitrary Units
Different units of values have no consequences for equality.
// these tuples are equal
std::debug::assert_eq([ (1000cm3, 100mm, 0.01m²), (10cm, 100cm², 1l) ]);
Ambiguous Elements
Either names or types must be unique in a tuple.
(10cm, 10mm, 1m); // error: ambiguous type Length
Tuple Aliases
Some named tuples are used frequently and thus they have aliases: Color, Vec2 and Vec3.
This feature is also called named-tuple duck-typing.
Color
A named tuple with the fields r, g, b and a will be treated as a color with red, green, blue and alpha channel.
You can use the type alias Color to deal with color values.
color: Color = (r = 100%, g = 50%, b = 25%, a = 100%);
std::debug::assert(color.r == 100%);
Vec2
A named tuple with the fields x and y will be treated as a two-dimensional vector.
You can use the type alias Vec2.
v: Vec2 = (x = 2.0, y = 3.0);
std::debug::assert(v.x + v.y == 5.0);
Vec3
A named tuple with the fields x, y and z will be treated as a three-dimensional vector.
You can use the type alias Vec3.
v: Vec3 = (x = 2.0, y = 3.0, z = 4.0);
std::debug::assert(v.x + v.y + v.z == 9.0);
Tuple Operators
Tuples support the following operators.
| Operator | Description | Example |
|---|---|---|
+ | add each element | (x=1, y=2) + (x=3, y=4) |
- | subtract each element | (x=1, y=2) - (x=3, y=4) |
* | multiply each element | (x=1, y=2) * (x=3, y=4) |
/ | divide each element | (x=1, y=2) / (x=3, y=4) |
- | negation of each element | -(x=1, y=2) |
! | inversion of each element | !( true, false ) |
std::debug::assert_eq([ (x=1, y=2) + (x=3, y=4), (x=4, y=6) ]);
std::debug::assert_eq([ (x=2, y=3) - (x=1, y=4), (x=1, y=-1) ]);
std::debug::assert_eq([ (x=1.0, y=2.0) * 2, (x=2.0, y=4.0) ]);
std::debug::assert_eq([ (x = 1.0, y = 2.0) / 2, (x = 0.5, y = 1.0)]);
std::debug::assert_eq([ -(x = 1.0, y = 2.0), (x = -1.0, y = -2.0)]);
Tuple Mismatch
Names or types must match like the element count.
(x=1, y=2) + (x=3, z=4); // error: mismatch (x, y) + (x, z)
(x=1, y=2) + (x=3mm, y=4mm); // error: mismatch (Integer, Integer) + (Length, Length)
(x=1, y=2) + (x=3, y=4, z=5); // error: mismatch unexpected z
Quantities
The term quantities bundles a set of quantity types which are basically floating point1 values with physical units (e.g. Meter, Liters,…) attached.
// declare variable `height` of type `Length` to 1.4 Meters
height = 1.4m;
// use as *default* value in parameter list
fn f( height = 1m ) {}
// calculate a `Length` called `width` by multiplying the
// `height` with `Scalar` `2` and add ten centimeters
width = height * 2 + 10cm;
Quantity Types
The following quantity types are currently supported in µcad:
| Type | Metric Units | Imperial Units |
|---|---|---|
Scalar | % or none | - |
Length | µm, mm, cm, m | in or ", ft or ', yd |
Angle | ° or deg, grad, turn,rad | |
Area | µm²,mm²,cm²,m² | in², ft² , yd² |
Volume | µm³, mm³,cm³,m³,ml,cl,l, µl | in³, ft³ , yd³ |
Density | g/mm³ | - |
Weight | g, kg | lb, oz |
Tip
Special characters like
µ,°,², and³have ASCII1 replacements you may use if your keyboard misses these letters (see this appendix for more information).
Scalar
The type Scalar contains a floating number (without any unit) and must be written with at least one decimal place (or in percent).
zero = 0;
pi = 3.1415;
percent = 55%;
Length
Length is used to describe a concrete length.
millimeters = 1000mm;
centimeters = 100cm;
meters = 1m;
inches = 39.37007874015748in;
std::debug::assert_eq([ millimeters, centimeters, meters, inches ]);
Angle
Angles are used with rotations and in constrains when proving measures.
pi = std::math::PI;
radian = 1rad * pi;
degree = 180°;
degree_ = 180deg;
grad = 200grad;
turn = 0.5turns;
std::debug::assert_eq([ degree, degree_, grad, turn, radian ]);
Area
An Area is a two-dimensional quantity. It is the result when multiplying two Lengths.
a = 3cm;
b = 2cm;
area = a * b;
std::debug::assert(area == 6cm²);
Here is an example of how to use different areal units:
square_millimeter = 100000mm²;
square_centimeter = 1000cm2;
square_meter = 0.1m²;
square_inch = 155in²;
std::debug::assert_eq([ square_millimeter, square_centimeter ]);
Volume
A Volume is a three-dimensional quantity. It is the result when multiplying three Lengths.
a = 3mm;
b = 2mm;
c = 4mm;
volume = a * b * c;
std::debug::assert(volume == 24mm³);
Here is an example for units:
cubic_millimeter = 1000000.0mm³;
cubic_centimeter = 100.0cl;
cubic_meter = 0.001m3;
cubic_inch = 61.0237in³;
liter = 1.0l;
centiliter = 100.0cl;
milliliter = 1000.0ml;
std::debug::assert_eq([ cubic_millimeter,
cubic_centimeter,
cubic_meter,
centiliter,
milliliter
]);
Density
Currently Density has not specific use in µcad and only can use unit g/mm³.
gramm_per_square_centimeters = 19.302g/mm³;
Weight
Weights can be calculated by applying volumes to materials.
gram = 1000.0g;
kilogram = 1.0kg;
pound = 2.204623lb;
std::debug::assert_eq([ gram, kilogram ]);
Quantity Operators
Quantity types support most operators which primitive types do. See section operators in expressions for a complete list
The only difference about quantity operators is, that they calculate units too, where units remain flexible.
use std::debug::assert_eq;
assert_eq([ 6cm * 2cm, 12cm² ]);
assert_eq([ 6cm / 2cm, 3 ]);
assert_eq([ 6cm + 2cm, 80mm ]);
assert_eq([ 6cm - 2cm, 0.04m ]);
Primitive Types
Bool
Boolean is the result type of boolean expressions which may just be true or false.
std::debug::assert(true != false);
Boolean values can be combined with or and and operators:
std::debug::assert_eq([true or false, true]);
std::debug::assert_eq([true and false, false]);
std::debug::assert_eq([4 == 4, true]);
std::debug::assert_eq([4 == 5, false]);
std::debug::assert_eq([4 == 5 or 4 == 4, true]);
std::debug::assert_eq([4 == 5 and 4 == 4, false]);
Integer
The type Integer contains an integer number.
i = 3;
String
Strings are mostly used for rendering text.
text = "Hello µcad!";
std::debug::assert_eq([std::count(text), 11]);
// logging
std::print(text);
Matrix
Matrix types are built-in types and can be defined as:
Matrix2: A 2x2 matrix, commonly used for 2D rotations.Matrix3: A 3x3 matrix, commonly used for rotations.Matrix4: A 4x4 matrix, commonly used for affine transformations.
Note: Currently, matrices are used only internally and cannot be instantiated from µcad code.
Format Strings
Format strings are strings which include a special notation to insert expressions into a text.
std::debug::assert_eq([ "{2+5}", "7" ]);
Usually they are used to insert parameters into text:
fn print_length( length: Length ) {
std::print("{length}");
}
print_length(7mm);
Bad Expression in Format String
If a format string expression cannot be solved you will get an error.
fn print_length( length: Length ) { // warning: unused length
std::print("{size}"); // error: size is not known
}
print_length(7mm);
Models
Builtin Measures
Measures are builtin methods you can use with 2D or 3D objects.
The following example calculates the area of a circle by using the measure area:
__builtin::debug::assert_eq([
// use measure area() on a circle
std::geo2d::Circle(radius=10mm).area(),
// circle area formula for comparison
10mm * 10mm * std::math::PI
]);
Currently it is not possible to declare measures in µcad.
2D Measures
Builtin measures that can be used on 2D objects.
| Measure | Output Quantity | Description |
|---|---|---|
area(..) | Area | area |
circum(..) | Length | circumference |
center(..) | (x:Length, y:Length) | geometric center |
size(..) | (width:Length, height:Length) | extents |
bounds(..) | (left:Length, right:Length, top:Length, bottom:Length) | bound (from center) |
3D Measures
Builtin measures that can be used on 3D objects.
| Measure | Output Quantity | Description |
|---|---|---|
area(..) | Area | surface area |
center(..) | (x:Length, y:Length, z:Length) | geometric center |
size(..) | (depth:Length, width:Length, height=Length) | extents |
bounds(..) | (front: Length, back: Length, left:Length, right:Length, top:Length, bottom:Length) | bound (from center) |
volume(..) | Volume | volume |
Documentation
Code Comments
By using // or /* and */ you may insert comments within the code.
// This is a line comment...
/* This is a block comment */
/* Block comments my have...
...multiple lines.
*/
std::print("Hello, "); // Line comments can be appended to a line
std::print( /* Block comments can be placed almost anywhere */ "world!");
Doc Comments
You may also use comments to attribute your code with documentation.
By placing a comment with /// above a symbol definition you can attribute
your code with documentation.
Markdown may be used to shape sections or format text.
/// A function which returns what it gets.
///
/// It simply returns the **same** value...
///
/// ...as it got from the *parameter*.
///
/// ## Arguments
///
/// - `n`: input value
///
/// ## Returns
///
/// Output value.
fn f( n: Integer ) -> Integer { n }
// usual comment for non-symbols
f(1);
Till the first empty line text will be interpreted as a summary for documentation, all other lines will build the detailed description.
The above function f will be documented with the following markdown output:
A function which returns what it gets.
It simply returns the same value…
…as it got from the parameter.
Arguments
n: input valueReturns
Output value.
Attributes
Attributes are syntax elements that can be used to attach arbitrary data to model nodes.
The attributes will not change the model geometry itself, but might change its appearance when if they are used for viewers or exporters. There can be multiple attributes for a node, but each attribute needs to have a unique ID.
Simple example
Let’s define a model node c.
When viewed or exported, model node c will have a red color, because the color attribute will be set:
#[color = "#FF0000"]
c = std::geo2d::Circle(r = 42.0mm);
std::debug::assert_eq([c#color, (r = 1.0, g = 0.0, b = 0.0, a = 1.0)]);
Syntax
Syntactically, an attribute consists of # prefix and an item.
-
Name-value pair:
#[color = "#FF00FF"],#[resolution = 200%]. Store and retrieve arbitrary values. The name has to be unique. -
Calls:
#[svg("style = fill:none")]. Control the output for a specific exporter. -
Commands:
#[export = "test.svg"],#[measure = width, height]. A certain command to execute on a model, e.g. for export and measurement. Multiple commands are possible.
Color attribute
The color attribute attaches a color to a model node.
In viewer and when exported, the model will be drawn in the specified color.
#[color = "#FFFFFF"]
c = std::geo2d::Circle(r = 42.0mm);
std::debug::assert_eq([c#color, (r = 1.0, g = 1.0, b = 1.0, a = 1.0)]);
Resolution attribute
The resolution attribute sets the rendering resolution of the model.
The model will be rendered in with 200% resolution than the default resolution of 0.1mm.
This means the circle will be rendered with a resolution 0.05mm.
#[resolution = 200%]
c = std::geo2d::Circle(r = 42.0mm);
std::debug::assert_eq([c#resolution, 200%]);
Export command
If you have created a part or a sketch and want to export it to a specific file, you can add an export command:
#[export = "circle.svg"]
c = std::geo2d::Circle(r = 42.0mm);
The exporter is detected automatically depending on the file extension.
See export for more information.
Export nodes
When a µcad file is processed by the interpreter, you can export the resulting nodes in a specific file format.
Export via CLI
To export a µcad file via the CLI, the most simple form is:
µcad export myfile.µcad # -> myfile.svg
You can also give an optional custom name with a specific format:
microcad-cli export myfile.µcad custom_name.svg # -> custom_name.svg
Default formats
The output format depends on the kind of node:
- SVG is the default format for sketches.
- STL is the default format for parts.
- If the content is mixed, there will be an error, unless you mark different nodes with export attributes (see next section).
If you call the command line:
microcad-cli export my_sketch.µcad # This is a sketch and will output `my_sketch.svg`
microcad-cli export my_part.µcad # This is a part and will output `my_part.stl`
Export specific nodes via attributes
Assuming, you have two sketches and want to export each in a specific file. You assign an export attribute with a filename to each sketch. If you omit the file extension, the default export format will be picked automatically.
#[export = "rect.svg"] // Will be exported to `rect.svg`
std::geo2d::Rect(42mm);
#[export = "circle.svg"] // Will be exported to `circle.svg`
std::geo2d::Circle(r = 42mm);
In the CLI, you can select the node specifically:
microcad-cli export myfile.µcad --list # List all exports in this file: `rect, circle.svg`.
microcad-cli export myfile.µcad --target rect # Export rectangle node to `rect.svg`
Import data
Use can import data via std::import function.
Note: This WIP. Currently, only tuples from a TOML file can be imported.
TOML import
Assuming, you have the following data in a TOML file example.toml:
# file: example.toml
[M6]
diameter = 6.0
pitch = 1.0
[M10]
diameter = 10.0
pitch = 1.5
You can then load the TOML file by using std::import and access its values:
data = std::import("example.toml");
std::debug::assert_eq([data.M10.diameter, 10.0]);
std::debug::assert_eq([data.M6.pitch, 1.0]);
Included Libraries
With microcad come two out of the box libraries.
µcad Standard Library
The µcad standard library is written in µcad and provides a convenient interface to the µcad builtin library.
The µcad standard library is available in the global module std and is self
documented in a reference book.
use std::print;
print("Hello, µcad standard library!");
µcad Builtin Library
The µcad builtin library is written in Rust (and still a little C) and brings mathematical calculation functions, model processing and rendering capabilities into µcad.
The µcad builtin library is available in the global module __builtin and is self
documented in a reference book.
use __builtin::print;
print("Hello, µcad builtin library!");
Debugging
Verification
µcad provides several builtin functions that help you to avoid bad input parameters.
Assert
Assertions define constrains of parameters or cases and they bring any rendering to fail immediately.
one form of assertion is a function which gets an expression.
If the expression computes to false a compile error will occur at
that point.
std::debug::assert(true, "You won't see this message");
std::debug::assert(false, "this assertion fails"); // error
Error
std::log::error("this should not have happened"); // error
Todo
todo() is like error() but aims at reminding you to finish code later.
a = 0;
if a == 0 {
std::log::info("a is zero");
} else {
std::log::todo("print proper message");
}
Appendix
Coding Style
The µcad language has a styling convention which shall be used.
Naming convention
| Element | Example | Format |
|---|---|---|
| Constant | const MY_CONST = 1; | UPPER_SNAKE_CASE |
| Type | x: Length = 1; | PascalCase |
| Sketch | sketch MySketch() {} | PascalCase |
| Part | part MyPart() {} | PascalCase |
| Operation | op my_op() {} | snake_case |
| Value | my_var = 1; | snake_case |
| Public | pub my_var = 1; | snake_case |
| Module | mod my_lib {} | snake_case |
| Function | fn my_func() {} | snake_case |
| Model | my_model | snake_case |
| Property | my_model.my_prop | snake_case |
| Attribute | #[my_attribute] | snake_case |
Keywords
The following keywords are reserved and cannot be used as identifier.
| Keyword | Description |
|---|---|
__builtin | builtin module |
__plugin | (reserved) |
_ | underscore identifier |
and | logical and |
Angle | angle quantity type |
Area | area quantity type |
as | part of use-as |
assembly | (reserved) |
Bool | boolean type |
Color | color type |
const | constant definition prefix |
Density | density quantity type |
else | part of if-else |
enum | (reserved) |
false | boolean constant |
fn | function definition prefix |
if | part of if-else |
init | workbench initializer |
Integer | integer type |
Length | length quantity type |
match | (reserved) |
material | (reserved) |
Matrix | matrix type |
mod | module definition prefix |
Model | model type |
op | operation definition prefix |
or | logical or |
part | 3D workbench |
prop | property definition prefix |
pub | public visibility prefix |
return | return statement |
Scalar | floating point type |
Size2 | 2D size vector |
sketch | 2D workbench |
String | text string |
struct | (reserved) |
true | boolean constant |
unit | (reserved) |
use | use statement |
Vec2 | 2D vector |
Vec3 | 3D vector |
Volume | volume quantity type |
Weight | weight quantity type |
xor | logical xor |
Special Characters
µcad is already using a Unicode character in it’s name and it allows some more to improve readability of scientific unit notations within the source code.
Sadly the majority of keyboards do not support some of them. In this case you may easily replace the special characters like shown in the following table.
| Original Character | Replacement | Example | Equivalent | Usage |
|---|---|---|---|---|
µ | m, u | µcad | mcad,ucad | file extension, markdown testing |
µ | u | µm, µl | um, ul | micro prefix in metric units |
° | deg | 3° | 3deg | angle unit |
² | 2 | 3m² | 3m2 | area units |
³ | 3 | 3m³ | 3m3 | volume units |
Test List
The following table lists all tests included in this documentation.
175 tests have been evaluated with version 0.2.20 of microcad.
Click on the test names to jump to file with the test or click the buttons to get the logs.