Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

The µcad programming language is 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 a 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:

BannerMeaning
okOk
okOk (with warnings)
fail_okFails 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.

BannerMeaning
not_todoMarked as todo but is ok
not_todo_failMarked 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.

BannerMeaning
failFails with errors
fail_wrongFails with wrong errors or warnings
ok_failIs ok but was meant to fail
parse_failFails early while parsing

The following banners occur if tests are marked as todo and so are not running successful.

BannerMeaning
todoWork in progress
todo_failWork in progress (should fail)

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:

StatementPurposeExample
expressioncalculate valuesx * 5;
assignmentstore valuesy = x;
functionseparate calculationsfn f() { }
workbenchbuild or transform 2D sketches and 3D partspart P() { }
modulemodularization of complex codemod m { }
ifprocess conditionsif x > 1 { }
useuse elements from other modulesuse m;
calluse functions and workbenchesf();
commentfor 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.

test

// simply draw a circle
std::geo2d::Circle(radius = 1cm);

test

// 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:

test

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 mod keyword (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:

test

// 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 are defined with the mod keyword followed by a name and a body as the following example shows:

test

mod my_module {
    // statements
}

Module names are always written in lower_snake_case.

The following statements can be listed in a module:

StatementPurposeExample
const assignmentname constantsconst y = x;
pub y = x;
functionseparate calculationsfn f() { }
workbenchbuild or transform 2D sketches and 3D partspart P() { }
modulemodularization of complex codemod m { }
ifprocess conditionsif x > 1 { }
useuse elements from other modulesuse m;
calluse functions and workbenchesf();
commentfor 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.

test

mod second;
second::f(); 

test

// 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.

Use Statements

When including code from other modules or other files fully qualified names of symbols (e.g. std:geo3d::cube) often 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.

Internally every use statement builds one or more aliases, each with an identifier and a target symbol it points to.

The following example which uses two parts of geo3d shows the problem:

test

std::geo3d::Sphere(radius = 40mm);
std::geo3d::Cube(size = 40mm);

Use Statement

With use it first seems not shorter, but if we would use sphere and cube more often this would shorten things a lot:

test

use std::geo2d::Circle;
use std::geo2d::Rect;

Circle(radius = 4mm);
Rect(size = 40mm);

You may also use whole the module if the names you are using already exist as a symbol:

test

use std::geo2d;

geo2d::Circle(radius = 40mm);

Use As Statement

Another way to be explicit when name conflicts exist is to use use as where you can locally rename the target symbol:

test

use std::geo2d::Circle as Disk;

Disk(radius = 4mm);

Or you may use use as with a module:

test

use std::geo2d as geo;

geo::Circle(radius = 4mm);

Use All Statement

The shortest way to use many symbols from one module is to put an * at the end. The following example aliases all symbols of std::geo3d into the current scope.

test

use std::geo3d::*;

Sphere(radius = 4mm);
Cube(size = 40mm);

Public Use Statement

This statement does not only make the target symbol visible on the current scope but in the symbol table where outside code might use it too.

Sphere and Cube will be made available for using them outside of module my in the following example:

test

mod my {
    pub use std::geo2d::*;
}

my::Circle(radius = 4mm);
my::Rect(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.

test

// 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");

Functions may be declared within source files, modules or workbenches.

Declaration

A function declaration starts with the keyword fn, followed by an identifier, a parameter list, and a function body. Functions can also return a value as result:

test

fn pow( x: Scalar, n: Integer ) {
    if n > 1 {
        return x * pow(x, n-1); // return recursive product
    } else if n == 1 {
        return x;   // return x
    }
}

std::debug::assert_eq([ pow(8.0,2), 64.0 ]);

Returning a value twice is not allowed.

test

fn pow( x: Scalar, n: Integer ) {
    if n > 1 {
        return x * pow(x, n-1); // return recursive product
    }
    return x;
}

std::debug::assert_eq([ pow(8.0,2), 64.0 ]);

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.

test

// module math
mod math {
    // pow cannot be called from outside math
    fn pow( x: Scalar, n: Integer ) {
        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) {
        // 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 that are accessible within the module only.

Here is an example which generates a punched disk of a given radius using a function inner():

test

sketch PunchedDisk(radius: Length) {
    use std::geo2d::Circle;

    // calculate inner from radius in a method
    fn inner() { return radius/2; }

    // generate donut (and call inner)
    Circle(radius) - Circle(radius = inner());
}

PunchedDisk(1cm);

Restrictions

There are some restrictions for workbench functions:

Trying to make them public with the keyword pub will result into an error:

test

part PunchedDisk(radius: Length) {
    pub fn inner() { return radius/2; }   // error: cant use pub fn inside workbench
}

PunchedDisk(4.0mm);

You cannot create workbench properties within the code body.

test

part PunchedDisk(radius: Length) {
    fn inner() { 
        prop hole = radius/2;  // error: prop not allowed in function
        return hole;
    }
    inner();
}

PunchedDisk(1cm);

Workbenches

Workbench Types

Workbenches are µcad code constructs used to:

Essentially, a workbench is initialized with a set of parameters and possibly some initialization code, then executes code that generates 2D and 3D objects.

Workbench Declaration

Workbench Elements

A workbench consists of the following elements:

  • A leading keyword: part, sketch, or op,
  • an identifier that names the workbench,
  • a building plan defined by a parameter list following the identifier,
  • optional init code, which is executed before any initializer,
  • optional initializers, offering alternative ways to initialize the building plan,
  • optional functions, acting as 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 most of these elements:

test

// Sketch with a `radius` as building plan.
// Which will automatically become a property.
sketch Wheel(radius: Length) {
    
    // Init 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]);

d - r;

test

Building Plan

The building plan is defined by a parameter list that follows the workbench’s identifier. All parameters in that list become properties of the workbench when it is invoked. These properties can be accessed within the building code, inside functions, or externally.

test

// sketch with a `radius` as building plan
sketch Wheel(radius: Length) {
    // access property `radius` from the building plan
    std::geo2d::Circle(radius);
}

std::debug::assert_eq([Wheel(5cm).radius, 5cm]);

Initializers

Initializers are defined with the keyword init and a following parameter list. One may define multiple initializers which must have different parameter lists.

test

sketch Wheel(radius: Length) {
    init( radius: Length ) {} // error: same parameters as in building plan
    std::geo2d::Circle(1mm);
}

Wheel(radius = 1.0mm);

However, if an initializer is used, all properties from the building plan must be initialized (except those with default values).

test

sketch Wheel(radius: Length, thickness: Length) {
    // initializer with diameter
    init( diameter: Length, thickness: Length ) {
        // must set `radius` in code 
        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
    std::geo2d::Circle(radius) - std::geo2d::Circle(radius = radius - thickness);
}

// call with building plan
Wheel(radius=5cm, thickness=1cm);
// call with initializer
Wheel(diameter=10cm, thickness=1cm);

If the building plan is not fully initialized by an initializer you will get an error:

test

sketch Wheel(radius: Length) {
    init( width: Length ) { _ = width; } // error: misses to set `radius` from building plan
}

Wheel(width = 1.0mm);

Init Code

If you use initializers you might write some init code which must be placed on top of the workbench’s body (before any initializers).

The init code is just allowed to define some constants which then can be used in all following code (including code within initializers and functions).

test

sketch Wheel(radius: Length) {
    // init code
    const FACTOR = 2.0;

    // initializer
    init(diameter: Length) { radius = into_radius(diameter); }

    // function
    fn into_radius( diameter: Length ) {
        // use constant FACTOR from init code
        return diameter / FACTOR;
    }

    // set property diameter and use FACTOR from init code
    prop diameter = radius * FACTOR;
    
    // code body
    std::geo2d::Circle(radius);
}

__builtin::debug::assert(Wheel(radius = 5cm).radius == 5cm);
__builtin::debug::assert(Wheel(radius = 5cm).diameter == 10cm);
__builtin::debug::assert(Wheel(diameter = 10cm).diameter == 10cm);

Init Code Rules

It’s not allowed to write any code between initializers.

test

sketch Wheel(radius: Length) {
    init( width:Length ) { radius = width / 2; }
    
    radius = 1; // error: code between initializers not allowed

    init( height:Length ) { radius = height / 2; }
}

Wheel(radius = 1.0mm);

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.

test

sketch Wheel(radius: Length) {
    // building code starts here
    std::geo2d::Circle(radius);
}

Wheel(radius = 1.0mm)

If initializers were defined the building code starts below them.

test

sketch Wheel(radius: Length) {
    // initializer
    init( diameter: Length ) { radius = diameter / 2; }

    // building code starts here
    std::geo2d::Circle(radius);
}

Building Code Rules

It’s not allowed to use the sketch, part, op, return nor mod statements within workbench code:

test

sketch Wheel(radius: Length) {
    sketch Axis(length: Length) {}  // error
    std::geo2d::Circle(radius);
}

Wheel(radius = 1.0mm);

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:

test

// `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 and maybe read from 
    // outside this workbench
    prop inner = outer / 2;

    // generate wheel (and use property inner)
    Circle(r = outer) - Circle(r = 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}");

If you remove the prop keyword you will fail at accessing inner:

test

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(r = outer) - Circle(r = inner);
}

t = Wheel(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

Sketches

Sketches are similar to parts but in two dimensions only. They can be extruded into three-dimensional parts.

test

use std::geo2d::*;
sketch MySketch( radius: Length) {
    Circle(radius) - Rect(size = radius);
}

MySketch(1cm);

The output is a 2D sketch:

test

If you generate a 3D model within a sketch you will get an error:

test

use std::geo3d::*;
sketch MySketch( radius: Length) {
    Sphere(radius) - Cube(size = radius);  // error
}

MySketch(1cm);

Parts

Parts are workbenches that are used to create 3D objects. If you want to create 2D objects, use sketches.

test

part MyPart() {}

Operations

Operations process 2D or 3D geometries into 2D or 3D geometries.

Actual operations are workbenches that process or transform their child object nodes to generate a new geometry.

So this would be a neutral operation:

test

// define operation nop without parameters
op nop() { @input }

// use operation nop on a circle
std::geo2d::Circle(radius = 1cm).nop();

@input

@input is a placeholder to tell where child nodes are nested. It can also be used to retrieve information about the tree structure In the above example @input will result in a std::geo2d::Circle(radius = 1cm).

An operation can have multiple children like in this example:

test

// define operation which takes multiple items
op punched_disk() { 
    // check number of children
    if @input.count() == 2 {
        // make hole
        @input.subtract(); 
    } else {
        std::error("punched_disk must get 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:

test

// 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 by combining multiple other expressions. The simplest form of an expression are literals:

test

5;        // Scalar literal
4.0mm;    // Quantity literal
"Hello";  // String literal

However — as mentioned before — literals can be combined into larger expressions. For example, we can multiply the value 5 by the quantity 4.0mm:

test

5 * 4.0mm;

The result of this expression would be 20mm.
In this form, the computed value is not yet used for anything, so the examples above will not produce any visible output or effect.

Things change when an expression generates a model instead of just a value. We call this a model expression:

test

std::geo2d::Rect(1cm);

This too is an expression — one that contains an element, specifically a call to Rect, which draws a rectangle that will actually be rendered into this:

output

If statements can also be used as an expression, evaluating to the value from the chosen branch.

test

use std::ops::*;
use std::math::*;
use std::geo2d::*;

sketch MySketch(a: Integer) {
    outer = if a == 1 {
        Circle(r = 1cm)
    } else if a == 2 {
        Rect(1cm)
    } else {
        Hexagon(1cm)
    };
    outer - Circle(r = 3mm)
}

MySketch([1,2,3]).align(X, 1cm);

output

Literals

Literals are the simplest form of expressions. In this section, we will look at some literals.

Integer Literals

Integer literals contain a whole number with a sign (but without a unit). Here are a few examples:

test

50;
1350;
-6

Scalar Literals

Scalar literals contain a floating point number with a sign (but without a unit).

test

1.23;
0.3252;
.4534;
1.;
-1200.0;
12.0E+12;
50%    // = 0.5

Boolean Literals

Booleans contain either the value true or false:

test

true;
false

String Literals

Strings are texts enclosed in quotation marks:

test

"Hello, World!"

Quantity Literals

Quantities are like scalars but with a unit and are widely used in microcad if you wanna draw something.

test

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

There are several operators which can be used to combine expressions with each other:

OperatorTypeInput typesResult TypeDescription
-unaryInteger, Scalar, ArrayInteger, Scalar, ArrayNegation
+binaryInteger, Scalar, ArrayInteger, Scalar, ArrayAddition
-binaryInteger, Scalar, ArrayInteger, Scalar, ArraySubtraction
*binaryInteger, Scalar, ArrayInteger, Scalar, ArrayMultiplication
/binaryInteger, Scalar, ArrayInteger, Scalar, ArrayDivision
^binaryInteger, ScalarInteger, ScalarPower
&binaryBooleanBooleanLogical AND
|binaryBooleanBooleanLogical OR
>binaryInteger, ScalarBooleanGreater than
>=binaryInteger, ScalarBooleanGreater or equal
<binaryInteger, ScalarBooleanLess than
<=binaryInteger, ScalarBooleanLess or equal
==binaryInteger, ScalarBooleanEqual
!=binaryInteger, ScalarBooleanNot equal

Here are some examples of each operator:

test

use std::debug::assert; // used for testing

-5;                                             // Negation
assert(  5 + 6        == 11                  ); // Addition
assert(  5 - 6        == -1                  ); // Subtraction
assert(  5 * 6        == 30                  ); // Multiplication
assert(  5 / 6        == 0.83333333333333333 ); // Division
assert(  5 ^ 6        == 15625               ); // Power
assert(  true & false == false               ); // Logical AND
assert(  true | false == true                ); // Logical OR
assert(  5 > 6        == false               ); // Greater than
assert(  5 >= 6       == false               ); // Greater or equal
assert(  5 < 6        == true                ); // Less than
assert(  5 <= 6       == true                ); // Less or equal
assert(  5 == 6       == false               ); // Equal
assert(  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.

test

use std::debug::assert; // used for testing

assert(  -[1, 2, 3, 4]              == [-1, -2, -3, -4]     ); // Negation
assert(  [1, 2, 3, 4] + 5           == [6, 7, 8, 9]         ); // Addition
assert(  [1, 2, 3, 4] - 5           == [-4, -3, -2, -1]     ); // Subtraction
assert(  [1, 2, 3, 4] * 5           == [5, 10, 15, 20]      ); // Multiplication
assert(  [1.0, 2.0, 3.0, 4.0] / 5   == [0.2, 0.4, 0.6, 0.8] ); // Division

Assignments

Assignments

Whenever you use a more complex expression, it is often worthwhile to store it in a variable so that it can be used once or multiple times elsewhere. In µcad, variables are always immutable which means that once they are set, their value cannot be reset in the same context. Therefore, they differ from the variables known in other programming languages.

A value assignment stores a value by a name on the evaluation stack.

The following example defines the variable a which from then is a reserved name within the scope in which it was defined.

test

a = 5;
b = a * 2;
std::debug::assert_eq([a,5]);
std::debug::assert_eq([b,10]);

Another assignment of a variable with the same name is not allowed.

test

a = 5; // warning: unused local
a = a * 2; // error: a already defined in this scope

Value assignments may only be used within code bodies (scopes) - and not within modules or in init code for example:

test

mod my_module {
    a = 1; // error
}

sketch MySketch() {
    a = 1;   // error
    init(_x : Scalar) {}
}

MySketch();

Prefix Underscore

If a value name starts with an underscore (like _this_name) that suppresses any warning about if it is not in use.

Constant Assignments

Unlike value assignments, constant assignments are not stored on the evaluation stack but in the symbol table. Constant assignments can be placed directly within a module — which is not allowed for value assignments — and they can also be declared as public.

test

const TEXT = "Hello";

mod my_module {
    pub const TEXT = "Hello";
}

std::print(TEXT);
std::print(my_module::TEXT);

Additionally, constant assignments are permitted in the init code of a workbench, where value assignments are likewise prohibited.

test

sketch MySketch(text: String) {
    const TEXT = "Hello";

    init() {
        text = TEXT;
    }

    std::print(text);
}

MySketch();

Using pub is not allowed in workbenches:

test

sketch MySketch() {
    pub const TEXT = "Hello";  // error
    std::print(TEXT);
}

MySketch();

Property Assignments

Program Flow

Microcad 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.

Conditionals

Conditions lead to different executions paths for different cases.

If statements in workbenches

Inside a workbench block, an if statement can be used to select different shapes or constructions depending on input parameters.

test

use std::ops::*;
use std::math::*;
use std::geo2d::*;

sketch MySketch(a: Integer) {
    if a == 1 {
        Circle(r = 1cm)
    } else {
        Rect(s = 1cm)
    }
}

MySketch([1,2]).align(X, 1cm);

Multiple conditions can be chained, allowing more than two alternatives:

output

test

use std::ops::*;
use std::math::*;
use std::geo2d::*;

sketch MySketch(a: Integer) {
    if a == 1 {
        Circle(r = 1cm)
    } else if a == 2 {
        Rect(s = 1cm)
    } else {
        Hexagon(r = 1cm)
    }
}

MySketch([1,2,3]).align(X, 1cm);

output

If Statement for functions

test

fn f(x: Integer) {
    if x == 5 or x == 4 {
        std::print("match");
    } else if x > 0 and x < 4 {
        std::print("no match");
    } else {
        std::print("invalid");
    }
}

f(5);  // prints "match"
f(1);  // prints "no match"
f(-1); // prints "invalid"
f(6);  // prints "invalid"

Calls

Calling Functions

A call of a function consists of just the identifier and an argument list. and the result is a value:

test

// function definition
fn square(x: Scalar) { return x * x; }

// call function square with parameter 2 and store result in s
s = square(x = 2);

// check value
std::debug::assert_eq( [s, 4] );

Calling Workbenches

Workbenches can be called in the same way as functions except that the result is a object node.

test

// definition of a sketch workbench
sketch Square(size: Length) { 
    std::geo2d::Rect(size);
}

// call square with a size and store object node in s
s = Square(size=2cm);

// translate object s
s.std::ops::translate(x = 1cm);

Calling Operations

Operations are called differently.

Parameters

Arguments

Function Calls

A function is simply called by appending a parameter list to it’s name:

test

// function definition
fn f() {}

// function call
f();

test

// function definition
fn f() {
    // return statement
    return 1; 
}

// function call (and result check)
std::debug::assert_eq([ f(), 1 ]);

test

// function definition with parameter
fn f(n: Integer) {
    return n * 2; 
}

// function calls with parameter (and result checks)
std::debug::assert_eq([ f(1), 2 ]);
std::debug::assert_eq([ f(2), 4 ]);

Workbench Calls

Method Calls

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
MatchesExample ParametersExample Arguments
Empty ListEmpty arguments with empty parameters()()
IdentifierMatch argument identifier with parameter identifier(x: Scalar)(x=1)
Shortened IdentifierMatch argument identifier with shortened parameter identifier(max_height: Scalar)(m_h=1)
TypeMatch argument type with parameter type(x: Length)(1mm)
Compatible TypeMatch argument type with compatible parameter type(x: Scalar)f(1)
DefaultMatch parameter defaults(x=1mm)()

Match Empty List

Matches when both the arguments and parameters are empty.

test

fn f() {}   // no parameters

f();        // no arguments

Match Identifier

The following example demonstrates calling a function f with each argument specified by name:

test

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 (_).

test

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:

IdentifierShort Identifier
parameterp
my_parameterm_p
my_very_long_parameter_namem_v_l_p_n
my_Parameterm_P
MyParameterM
myParameterm

Match Type

Nameless values can be used if all parameter types of the called function (or workbench) are distinct.

test

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.

test

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.

test

fn f( a = 1mm ) {}  // warning: unused a
// a has default
f();

Mix’em all

You can combine all these methods.

test

fn f( a: Scalar, b: Length, c=2cm, d: Area ) -> Volume { } // warning: unused a,b,c,d

// `a` is the only Scalar and `b` is named, so `c` and `d` do not need a name.
f(1, b=2cm, 2cm², 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.

test

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:

test

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`

test

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.

Tuple Argument Matching

The concept behind named tuple argument matching is to allow functions to accept subsets of parameters in a bundle. This makes it easy to pre-configure parts of arguments:

test

// Function with three parameters: x, y, and z
fn f( x: Length, y: Length, z: Length ) {}

// Since we do not want to change x and y in the following statements,
// we prepare a tuple named plane:
plane = (x=1cm, y=2cm);

// Then we pass plane to f() three times with different z values
f( plane, z=3cm);
f( plane, z=6cm);
f( plane, z=9cm);

The same function can be called in various ways using named tuples:

test

fn f( x: Length, y: Length, z: Length ) {}

// Every parameter given by name
f( x=1cm, y=2cm, z=3cm);

// Parameters given by named tuple
f( (x=1cm, y=2cm, z=3cm) );

// Parameters given by named tuple variable
p = (x=1cm, y=2cm, z=3cm);
f( p );

// Parameters given by named tuple and a single value
f( (x=1cm, y=2cm), z=3cm );
f( y=2cm, (x=1cm, z=3cm) );

// Parameters given by named tuple variable and a single value
q = (x=1cm, y=2cm);
f( q, z=3cm );

As you can see, the possibilities are endless.

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:

test

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:

test

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 2 nested for loops, which are not available in µcad.

Another example uses an array of tuples and produces the same output:

test

r = std::geo2d::Rect(width = 2mm, height = 2mm);

r.std::ops::translate([(x=-4mm, y=-4mm), (x=-4mm, y=4mm), (x=4mm, y=-4mm), (x=4mm, y=4mm)]);

Matching Errors

If you do not provide all parameters, you will get an error:

test

fn f( x: Length, y: Length, z: Length ) {}
f( x=1cm, z=3cm); // error: y is missing here

When you provide all parameters but some are redundant, you will get a error too:

test

fn f( x: Length, y: Length, z: Length ) {}
f( x=1cm, y=2cm, v=5cm, z=3cm);  // error: Unexpected argument v

Types

The µcad type system consists of a group built-in types. The type system is static, which means a declared variable has a fixed type that cannot be changed.

These classes of built-in types are supported:

Built-in typeDescriptionExample
QuantityNumeric values with an optional unit.a: Length = 4mm
BoolA boolean value.b: Bool = true
IntegerAn integer value without a unit.c: Integer = 4
StringA string.d: String = "Hello World"
MatrixMatrix types for affine transforms, for internal usage.
ArrayA list of values with a common type.e: [Integer] = [1,2,3]
TupleA list of values with a distinct types.f: (Length, Scalar) = (4mm, 4.0)
Named tupleA sorted list of key-value pairs with distinct types.g: (x: Scalar, y: Length) = (x = 4.0, y = 4mm)
ModelsNodes in the model tree.h: Models = { 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:

test

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:

test

x: Length;         // parse_error

However, for parameter lists in functions and workbenches, you can declare the type only but also pass a default value:

test

fn f(x = 4mm) {}        // use unit (with default)
fn f(x: Length) {}     // use type (without default)

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

An array is an ordered collection of values with a common type.

Arrays as list: [1, 2, 3]

test

[
    // First element
    1,

    // Second element
    2
];

You can count the number of elements in an array using std::count:

test

std::debug::assert_eq([std::count([1,2,3]), 3]);

You can get the head and tail of an array using std::head and std::tail:

test

std::debug::assert_eq([std::head([1,2,3]), 1]);
std::debug::assert_eq([std::tail([1,2,3]), [2,3]]);

Arrays as range: [1..5]

You can generate an array via range expressions: [1..5].

A µcad range includes both of it’s end points.

test

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:

test

[6..1];  // error
[2..-2];  // error

Only Integer can be used as endpoint:

test

[1.0..2.0];  // parse_error

Array operations

Unit bundling

Array support unit bundling, which means the you can write the unit after the [] brackets.

test

std::debug::assert_eq([ [1mm, 2mm, 3mm], [1, 2, 3]mm ]);

Addition +

Adding a quantity

test

std::debug::assert_eq([ [1,2]mm + 2mm, [3,4]mm ]);

Subtraction -

Subtracting a quantity

test

std::debug::assert_eq([ [1,2]mm - 2mm, [-1,0]mm ]);

Multiplication *

Scaling an array

test

std::debug::assert_eq([ [-0.5mm,0.5mm]*2, [-1,1]mm ]);

Division /

Dividing an array by a value

test

std::debug::assert_eq([ [-1.0mm,1.0mm]/2, [-0.5, 0.5]mm ]);

Negation -

test

std::debug::assert_eq([ [-1.0mm,1.0mm], -[1.0mm, -1.0mm] ]);

Tuples

A tuple is a collection of values, each of which can be of different types. Typically, each value is paired with an identifier, allowing a tuple to function similarly to a key-value store.

test

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 Unnamed 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.

test

tuple = (10cm, 10cm², 1l);

Otherwise, they would be indistinguishable since the values in a tuple do not adhere to a specific order.

test

// these tuples are equal
std::debug::assert_eq([(1l, 10cm, 10cm²), (10cm, 10cm², 1l)]);

A partially or fully unnamed tuple can only be utilized through argument matching or tuple assignment.

Tuple Assignments

Tuple syntax is also employed on the left side of tuple assignments.

test

(width, height) = (1m,2m);
// check values of width and height
assert_eq([width,1m]);
assert_eq([height,2m]);

Occasionally, it’s practical to group units together, but this cannot be done directly with a tuple. Instead, you must use an array, which will be converted into a tuple during the assignment.

test

(width, height) = [1,2]m;
assert_eq([width,1m]);
assert_eq([height,2m]);

This method can generally be used to convert an array into a tuple:

test

array = [1,2]m;
(width, height) = array;
tuple = (width, height);

assert_eq([tuple,(1m,2m)]);
assert_eq([tuple.width,1m]);
assert_eq([tuple.height,2m]);

Tuple operators

Addition *

Adding two tuples of the same type

test

a = (x = 1.0mm, y = 2.0mm);
b = (x = 3.0mm, y = 4.0mm);
std::debug::assert_eq([a + b, (x = 4.0mm, y = 6.0mm)]);

test

a = (x = 1.0mm, y = 2.0mm);
b = (x = 3.0mm, z = 4.0mm);
c = a + b; // error: Tuple type mismatch for +: lhs=(x: Length, y: Length), rhs=(x: Length, z: Length)
std::debug::assert_eq([c, (x = 4.0mm, y = 6.0mm)]); // error: Array elements have different types: [<INVALID TYPE>, (x: Length, y: Length)]

Subtraction -

Subtracting a quantity

test

a = (x = 2.0mm, y = 3.0mm);
b = (x = 1.0mm, y = 4.0mm);
std::debug::assert_eq([a - b, (x = 1.0mm, y = -1.0mm)]);

Multiplication *

Scaling a tuple

test

v = (x = 1.0mm, y = 2.0mm);
std::debug::assert_eq([v*2, (x = 2.0mm, y = 4.0mm)]);

Division /

Dividing a tuple by a value

test

v = (x = 1.0mm, y = 2.0mm);
std::debug::assert_eq([v/2, (x = 0.5mm, y = 1.0mm)]);

Negation -

test

v = (x = 1.0mm, y = 2.0mm);
std::debug::assert_eq([-v, (x = -1.0mm, y = -2.0mm)]);

Type 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.

test

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.

test

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.

test

v: Vec3 = (x = 2.0, y = 3.0, z = 4.0);

std::debug::assert(v.x + v.y + v.z == 9.0);

Quantities

Quantities are numeric values coupled with a unit. Each unit refers to example one quantity type.

The following quantity types are supported:

TypeMetric UnitsImperial Units
Scalar-, %-
Lengthµm, mm, cm, min or ", ft or ', yd
Angle° or deg, grad, turn,rad
Areaµm²,mm²,cm²,in², ft² , yd²
Volumeµm³, mm³,cm³,,ml,cl,l, µlin³, ft³ , yd³
Densityg/mm³-
Weightg, kglb, oz

Note: More units may be implemented.

Literals

Quantities can be declared by literals. This means that your will automatically get the following type if you use the beside units:

test

// 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;

Examples

Scalar

The type Scalar contains a floating number (without any unit) and must be written with at least one decimal place (or in percent).

test

zero = 0;
pi =  3.1415;
percent = 55%;

Length

Length is used to describe a concrete length.

test

millimeters = 1000mm;
centimeters = 100cm;
meters = 1m;
inches = 39.37007874015748in;

std::debug::assert( [millimeters, centimeters, meters, inches].all_equal() );

Angle

Angles are used with rotations and in constrains when proving measures.

test

pi = std::math::PI;
radian = 1rad * pi;
degree = 180°;
degree_ = 180deg;
grad = 200grad;
turn = 0.5turns;

std::debug::assert( [degree, degree_, grad, turn, radian].all_equal() );

Area

An Area is a two-dimensional quantity. It is the result when multiplying two Lengths.

test

a = 3cm;
b = 2cm;
area = a * b;
std::debug::assert(area == 6cm²);

Here is an example of how to use different areal units:

test

square_millimeter = 100000mm²;
square_centimeter = 1000cm²;
square_meter = 0.1m²;
square_inch = 155in²;

std::debug::assert(square_millimeter == 0.1m²);
std::debug::assert(square_centimeter == 0.1m²);

Volume

A Volume is a three-dimensional quantity. It is the result when multiplying three Lengths.

test

a = 3mm;
b = 2mm;
c = 4mm;

volume = a * b * c;

std::debug::assert(volume == 24mm³);

Here is an example for units:

test

cubic_millimeter = 1000000.0mm³;
cubic_centimeter = 100.0cl;
cubic_meter = 0.001m³;
cubic_inch = 61.0237in³;
liter = 1.0l;
centiliter = 100.0cl;
milliliter = 1000.0ml;

std::debug::assert(cubic_millimeter == 1.0l);
std::debug::assert(cubic_centimeter == 1.0l);
std::debug::assert(cubic_meter == 1.0l);
std::debug::assert(centiliter == 1.0l);
std::debug::assert(milliliter == 1.0l);

Density

TODO

Weight

Weights can be calculated by applying volumes to materials.

test

gram = 1000.0g;
kilogram = 1.0kg;
pound = 2.204623lb;

std::debug::assert([gram, kilogram].all_equal());

Arithmetic

Quantity types can use operators:

test

use std::debug::assert;

a = 2.0mm;
assert([a, 2mm, 2.0mm].all_equal());
b = 2 * a;
assert([b, 4mm, 4.0mm].all_equal());
c = a / 2;
assert([c, 1mm, 1.0mm].all_equal());
d = a + 2.0mm;
assert([d, 4mm, 4.0mm].all_equal());
e = a - 2.0mm;
assert([e, 0mm, 0.0mm].all_equal());

Primitive Types

Bool

Boolean is the result type of boolean expressions which may just be true or false.

test

std::debug::assert(true != false);

Boolean values can be combined with or and and operators:

test

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 a natural number.

test

i = 3;

String

Text can be used to logging or to render text.

test

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.

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:

test

__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.

MeasureOutput QuantityDescription
area(..)Areaarea
circum(..)Lengthcircumference
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.

MeasureOutput QuantityDescription
area(..)Areasurface 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(..)Volumevolume

Documentation

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:

test

#[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.

test

#[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.

test

#[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:

test

#[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.

test

#[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:

test

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

Builtin Library

The Builtin library is the bridge between µcad and Rust and covers all tasks that are not supposed to be in the µcad language itself (like drawing things and more complex calculations). It handles the communications with compiler and the geometry backend during evaluation.

  • 2D geometric primitives: geo2d
  • 3D geometric primitives: geo3d
  • geometric operations: ops
  • mathematics: math
  • debugging: debug

Standard Library

The Standard Library is written in µcad and covers builtin functionality and adds more sophisticated interfaces to the core functionalities of the Builtin Library. So generically spoken you will find all functionalities of __builtin within std but in a handier form.

Modules

The main module of the µcad standard library std consists of these modules which group different kinds of functionalities together:

  • geo2d: 2D parts (e.g. circle, rect)
  • geo3d: 3D parts (e.g. sphere, cube)
  • ops: Algorithms to manipulate 2D and 3D parts (e.g. translate, difference)
  • math: Mathematical solutions (e.g. abs, pi)
  • debug: Debugging functions

Functions

std::print()

Print is a alias to __builtin::print() which is printing stuff to the output console to be read by the user.

Plugins

(not yet available)

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.

test

std::debug::assert(true, "You won't see this message");

test

std::debug::assert(false, "this assertion fails"); // error

Error

test

std::log::error("this should not have happened"); // error

Todo

todo() is like error() but aims at reminding you to finish code later.

test

a = 0;

if a == 0 {
    std::log::info("a is zero");
} else {
    std::log::todo("print proper message");
}

Appendix

Coding Style

Names

ElementExampleFormat
typeMyTypepascal case
constconst MY_CONST = 1upper case snake case
valuesmy_var = 1snake case
modulemod my_lib {}snake case
functionfn my_func() {}snake case
sketchsketch MySketch() {}pascal case
partpart MyPart () {}pascal case
operationop my_op() {}snake case

Test List

The following table lists all tests included in this documentation.

177 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.

ResultSourceName
testalign_2d
testalign_2d_multiplicity
testalign_3d
testargument_match_auto
testargument_match_auto_err
testargument_match_default
testargument_match_empty
testargument_match_id
testargument_match_mix
testargument_match_short
testargument_match_single_identifier
testargument_match_type
testargument_match_type_compatible
testarray_add
testarray_div
testarray_expressions
testarray_expressions_head_tail
testarray_mul_scale
testarray_neg
testarray_sub
testarray_unit_bundling
testarrays_and_comments
testassignment
testassignment_immutable
testassignment_module
testattributes_color
testattributes_export
testattributes_precision
testattributes_simple_example
testboolean
testboolean_literal
testbuilding_plan
testbuiltin_debug_assert_false
testbuiltin_debug_assert_true
testbuiltin_extrude
testbuiltin_print
testbuiltin_revolve
testbuiltin_subtract_2d
testbuiltin_subtract_3d
testbuiltin_translate
testbuiltin_translate_twice
testcall_function
testcall_match
testcall_workbench
testchained_if_statement
testcode
testcode_between_initializers
testcode_post_init
testconst_assignment_mod
testconst_assignment_workbench
testconst_assignment_workbench_pub
testdifference_alt_operator
testdifference_operator
testdistribute_grid_2d
testdistribute_grid_3d
testexample
testexport_attributes
testexpression_literals
testexpression_model
testexpression_multiply
testexternal_modules_main
testexternal_modules_second
testextrude
testfunction_call
testfunction_param_return
testfunction_return
testhull_multiple
testhull_single
testif_expression
testif_functions
testif_statement
testillegal_workbench_statement
testinit_property
testinitializers
testinput
testinteger_literal
testinternal_mod
testintersection_alt_operator
testintersection_operator
testmath_abs
testmath_trigonometric
testmeasure
testmirror_3d
testmissed_property
testmod
testmod_example
testmultiplicity_arrays
testmultiplicity_tuple_array
testnamed_tuple_access
testno_multiplicity
testnone
testop_example
testoperator_array
testoperator_examples
testorient_3d
testparameters
testpart_basic
testpart_declaration
testpre_init_code
testproperty
testproperty_wrong
testquantity_literal
testquantity_types_number_literals
testrange_expressions
testrange_expressions_bad_order
testrange_expressions_bad_type
testreturn
testreturn_twice
testrevolve
testrotate_2d
testrotate_3d
testscalar_literal
testscale_3d
testscale_uniform
testsector
testsketch_3d
testsketch_basic
testsource_file_2D
testsource_file_3D
testsource_file_mixed
teststd_geo2d_buffer
teststd_geo2d_involute_gear_profile
teststd_geo2d_rounded_rect
teststd_geo2d_sector
teststd_geo2d_text
teststring_literal
testtext
testtoml_import
testtranslate_2d
testtuple_add_different
testtuple_add_same
testtuple_assignment
testtuple_assignment_bundle
testtuple_assignment_convert
testtuple_div
testtuple_match
testtuple_match_errors
testtuple_match_variants
testtuple_match_warnings
testtuple_mul_scale
testtuple_neg
testtuple_sub
testtypes_bundles_functions
testtypes_def_vs_decl
testtypes_named_tuple_color
testtypes_named_tuple_vec2
testtypes_named_tuple_vec3
testtypes_no_declaration
testtypes_primitive_bool
testtypes_primitive_integer
testtypes_primitive_string
testtypes_quantity_angle
testtypes_quantity_area
testtypes_quantity_area_units
testtypes_quantity_arithmetic
testtypes_quantity_length
testtypes_quantity_scalar
testtypes_quantity_volume
testtypes_quantity_volume_units
testtypes_quantity_weight
testunion_alt_operator
testunion_operator
testunnamed_tuple
testunnamed_tuple_order
testuse
testuse_all
testuse_as
testuse_as_module
testuse_module
testuse_statement_pub
testverify_assert
testverify_assert_fail
testverify_error
testverify_todo
testworkbench_example
testworkbench_fn_prop
testworkbench_pub