Maker.js, a Microsoft Garage project, is a JavaScript library for creating and sharing modular line drawings for CNC and laser cutters.

View project on GitHub Star

Advanced drawing

Open vs Closed Geometry

An open geometry is when any path in a drawing is a dead end. A closed geometry is when all path ends meet and there are no dead end paths. A closed geometry forms an enclosed shape.

Examples of Open Geometry:

Examples of Closed Geometry:

Maker.js works with both open and closed geometries. When your desire is to create a three dimensional object, you will probably be using a closed geometry.

Combining with Boolean operations

You can combine models using the makerjs.model.combine function, passing these parameters:
  • first model to combine, we'll call it "modelA"
  • second model to combine, we'll call it "modelB"
  • boolean to include modelA's paths which are inside of modelB
  • boolean to include modelA's paths which are outside of modelB
  • boolean to include modelB's paths which are inside of modelA
  • boolean to include modelB's paths which are outside of modelA
Each model must be a closed geometry, and should not be self-intersecting. The effect of the 4 boolean parameters is shown in these examples:
//combine a rectangle and an oval, several ways

var makerjs = require('makerjs');
    
function example(origin) {
    this.models = {
        rect: new makerjs.models.Rectangle(100, 50),
        oval: makerjs.model.move(new makerjs.models.Oval(100, 50), [50, 25])
    };
    this.origin = origin;
}

var examples = {
    models: {
        x1: new example([0, 0]),
        x2: new example([200, 0]),
        x3: new example([400, 0]),
        x4: new example([500, 0])
    }
};

//save us some typing :)
var x = examples.models;

makerjs.model.combine(x.x2.models.rect, x.x2.models.oval, false, true, false, true);
makerjs.model.combine(x.x3.models.rect, x.x3.models.oval, false, true, true, false);
makerjs.model.combine(x.x4.models.rect, x.x4.models.oval, true, false, true, false);

var svg = makerjs.exporter.toSVG(examples);

document.write(svg);

Instead of remembering the boolean flag combinations, shortcuts are provided for:
//combine a rectangle and an oval, several ways

var makerjs = require('makerjs');

function example(origin) {
    this.models = {
        rect: new makerjs.models.Rectangle(100, 50),
        oval: makerjs.model.move(new makerjs.models.Oval(100, 50), [50, 25])
    };
    this.origin = origin;
}

var examples = {
    models: {
        x1: new example([0, 0]),
        x2: new example([200, 0]),
        x3: new example([400, 0]),
        x4: new example([500, 0])
    }
};

//save us some typing :)
var x = examples.models;

makerjs.model.combineUnion(x.x2.models.rect, x.x2.models.oval);
makerjs.model.combineSubtraction(x.x3.models.rect, x.x3.models.oval);
makerjs.model.combineIntersection(x.x4.models.rect, x.x4.models.oval);

var svg = makerjs.exporter.toSVG(examples);

document.write(svg);
Now it is apparent why we need a closed geometry - because we need to know what is considered the inside of a model.

Return value

These function will return a new model object with 2 child models, "a" and "b" which are aliases for the 2 models you passed in.

Order of Boolean operations

Combining models with boolean operations is a powerful feature but it can be challenging in some scenarios. Re-modeling your drawing may be necessary to acheive certain results. We will explore the order of operations concept with a sample project. Let's first take a look at our desired end goal:


We can start with all of the building blocks of our design: a star, a plus, and a frame:

//the basic skeleton of our project

var makerjs = require('makerjs');

var star = new makerjs.models.Star(28, 25, 20);

var plus = makerjs.model.rotate({
    models: {
        v: makerjs.model.center(new makerjs.models.Rectangle(3, 90)),
        h: makerjs.model.center(new makerjs.models.Rectangle(110, 3))
    }
}, -12.5);

var frame = {
    models: {
        outer: makerjs.model.center(new makerjs.models.RoundRectangle(100, 80, 4)),
        inner: makerjs.model.center(new makerjs.models.Rectangle(90, 70))
    }
};

var model = {
    models: {
        star: star,
        plus: plus,
        frame: frame
    }
};

var svg = makerjs.exporter.toSVG(model);

document.write(svg);

The first step is to combine the vertical and horizontal bars of the plus:

//combine the plus

var makerjs = require('makerjs');

var star = new makerjs.models.Star(28, 25, 20);

var plus = makerjs.model.rotate({
    models: {
        v: makerjs.model.center(new makerjs.models.Rectangle(3, 90)),
        h: makerjs.model.center(new makerjs.models.Rectangle(110, 3))
    }
}, -12.5);

//make a union from the vertical and horizontal
makerjs.model.combineUnion(plus.models.v, plus.models.h);

var frame = {
    models: {
        outer: makerjs.model.center(new makerjs.models.RoundRectangle(100, 80, 4)),
        inner: makerjs.model.center(new makerjs.models.Rectangle(90, 70))
    }
};

var model = {
    models: {
        star: star,
        plus: plus,
        frame: frame
    }
};

var svg = makerjs.exporter.toSVG(model);

document.write(svg);

Next we will combine the star and the plus:

//combine the star and plus

var makerjs = require('makerjs');

var star = new makerjs.models.Star(28, 25, 20);

var plus = makerjs.model.rotate({
    models: {
        v: makerjs.model.center(new makerjs.models.Rectangle(3, 90)),
        h: makerjs.model.center(new makerjs.models.Rectangle(110, 3))
    }
}, -12.5);

makerjs.model.combineUnion(plus.models.v, plus.models.h);

//make a union from the star and the plus:
makerjs.model.combineUnion(star, plus);

var frame = {
    models: {
        outer: makerjs.model.center(new makerjs.models.RoundRectangle(100, 80, 4)),
        inner: makerjs.model.center(new makerjs.models.Rectangle(90, 70))
    }
};

var model = {
    models: {
        star: star,
        plus: plus,
        frame: frame
    }
};

var svg = makerjs.exporter.toSVG(model);

document.write(svg);

Let's pause and consider what the plus looks like by itself, after our union operation with the star:

And the star by itself:

They have become open geometries. We cannot call the combine function with an open geometry. But since we combined them, they are a closed geometry when they are together. So, we should create a new model for them together:

//remodel the star and plus

var makerjs = require('makerjs');

var star = new makerjs.models.Star(28, 25, 20);

var plus = makerjs.model.rotate({
    models: {
        v: makerjs.model.center(new makerjs.models.Rectangle(3, 90)),
        h: makerjs.model.center(new makerjs.models.Rectangle(110, 3))
    }
}, -12.5);

makerjs.model.combineUnion(plus.models.v, plus.models.h);

makerjs.model.combineUnion(star, plus);

//make a new model with the star and plus together
var starplus = {
    models: {
        star: star,
        plus: plus
    }
};

var frame = {
    models: {
        outer: makerjs.model.center(new makerjs.models.RoundRectangle(100, 80, 4)),
        inner: makerjs.model.center(new makerjs.models.Rectangle(90, 70))
    }
};

var model = {
    models: {
        //re-modeling: reference the starplus instead of star and plus separately
        starplus: starplus,
        frame: frame
    }
};

var svg = makerjs.exporter.toSVG(model);

document.write(svg);

Now we can continue, with a subtraction operation. Notice that we should not subtract the starplus from the frame (try that on your own to see what happens) but only from the inner frame:

//subtract the starplus

var makerjs = require('makerjs');

var star = new makerjs.models.Star(28, 25, 20);

var plus = makerjs.model.rotate({
    models: {
        v: makerjs.model.center(new makerjs.models.Rectangle(3, 90)),
        h: makerjs.model.center(new makerjs.models.Rectangle(110, 3))
    }
}, -12.5);

makerjs.model.combineUnion(plus.models.v, plus.models.h);

makerjs.model.combineUnion(star, plus);

var starplus = {
    models: {
        star: star,
        plus: plus
    }
};

var frame = {
    models: {
        outer: makerjs.model.center(new makerjs.models.RoundRectangle(100, 80, 4)),
        inner: makerjs.model.center(new makerjs.models.Rectangle(90, 70))
    }
};

//subtract from the inner frame only
makerjs.model.combineSubtraction(frame.models.inner, starplus);

var model = {
    models: {
        starplus: starplus,
        frame: frame
    }
};

var svg = makerjs.exporter.toSVG(model);

document.write(svg);

Expanding paths

Paths can be expanded to produce a closed geometry model which surrounds them perfectly.

//show each path type

var makerjs = require('makerjs');

var model = {
  paths: {
    p1: new makerjs.paths.Line([0, 2], [10, 2]),
    p2: new makerjs.paths.Arc([20, 0], 5, 0, 180),
    p3: new makerjs.paths.Circle([35, 2], 5)
  }
};

var svg = makerjs.exporter.toSVG(model);

document.write(svg);

Pass a path and a distance to makerjs.path.expand, this will return a new model:

//expand around each path type

var makerjs = require('makerjs');

var model = {
  paths: {
    p1: new makerjs.paths.Line([0, 2], [10, 2]),
    p2: new makerjs.paths.Arc([20, 0], 5, 0, 180),
    p3: new makerjs.paths.Circle([35, 2], 5)
  }
};

model.models = {
    x1: makerjs.path.expand(model.paths.p1, 2),
    x2: makerjs.path.expand(model.paths.p2, 2),
    x3: makerjs.path.expand(model.paths.p3, 2)
};

var svg = makerjs.exporter.toSVG(model);

document.write(svg);
//show only expansions

var makerjs = require('makerjs');

var temp = {
  paths: {
    p1: new makerjs.paths.Line([0, 2], [10, 2]),
    p2: new makerjs.paths.Arc([20, 0], 5, 0, 180),
    p3: new makerjs.paths.Circle([35, 2], 5)
  }
};

var model = {
    models: {
        x1: makerjs.path.expand(temp.paths.p1, 2),
        x2: makerjs.path.expand(temp.paths.p2, 2),
        x3: makerjs.path.expand(temp.paths.p3, 2)
    }
};

var svg = makerjs.exporter.toSVG(model);

document.write(svg);

You can also expand all the paths in a model by calling makerjs.model.expandPaths:

//expand a star model

var m = require('makerjs');

var star = m.model.rotate(new m.models.Star(5, 100), 18);
var expanded = m.model.expandPaths(star, 10);

var model = {
    models: {
        star: star,
        outline: expanded
    }
};

var svg = m.exporter.toSVG(model);

document.write(svg);

Beveling joints

A third parameter can be passed to makerjs.model.expandPaths to specify the number of corners to apply to each joint and end cap:

  • 0 (default) - no corners (rounded)
  • 1 - one corner (pointed)
  • 2 - two corners (beveled)

//expand and bevel

var m = require('makerjs');

var star = m.model.rotate(new m.models.Star(5, 100), 18);

var rounded = m.model.expandPaths(star, 10, 0);

var pointed = m.model.expandPaths(star, 10, 1);

var beveled = m.model.expandPaths(star, 10, 2);

var model = {
    models: {
        star: star,
        rounded: m.model.move(rounded, [240, 0]),
        pointed: m.model.move(pointed, [480, 0]),
        beveled: m.model.move(beveled, [720, 0])

    }
};

var svg = m.exporter.toSVG(model);

document.write(svg);

Outlining a model

Expanding a model's path will surround every path, which sometimes can mean there is an inner and an outer surrounding chain. If you only want the outer surrounding chain, use makerjs.model.outline:

//outline a star model

var m = require('makerjs');

var star = m.model.rotate(new m.models.Star(5, 100), 18);
var outline = m.model.outline(star, 10);

var model = {
    models: {
        star: star,
        outline: outline
    }
};

var svg = m.exporter.toSVG(model);

document.write(svg);

Wireframe technique

Creating a wireframe and using expansion may save you a lot of work. We will demonstrate by creating a wireframe of a truss:

//create a simple truss

var m = require('makerjs');

function trussWireframe(w, h) {

  this.models = {
    frame: new m.models.ConnectTheDots(true, [ [0, h], [w, 0], [0, 0] ])
  };

  var angled = this.models.frame.paths.ShapeLine1;

  var bracepoints = [
    [0, 0],
    m.point.middle(angled, 1/3),
    [w/2 , 0],
    m.point.middle(angled, 2/3)
  ];

  this.models.brace = new m.models.ConnectTheDots(false, bracepoints);
}

var truss = new trussWireframe(200, 50);

var svg = m.exporter.toSVG(truss);

document.write(svg);

Next we will expand the paths:

//expand a truss wireframe

var m = require('makerjs');

function trussWireframe(w, h) {

  this.models = {
    frame: new m.models.ConnectTheDots(true, [ [0, h], [w, 0], [0, 0] ])
  };

  var angled = this.models.frame.paths.ShapeLine1;

  var bracepoints = [
    [0, 0],
    m.point.middle(angled, 1/3),
    [w/2 , 0],
    m.point.middle(angled, 2/3)
  ];

  this.models.brace = new m.models.ConnectTheDots(false, bracepoints);
}

var truss = new trussWireframe(200, 50);
var expansion = m.model.expandPaths(truss, 3, 1);

var svg = m.exporter.toSVG(expansion);

document.write(svg);

Simplifying paths

If you Play the wireframe example above, and click on 'show path names' you will see that many lines have been created as a result of the expansion. This is an artefact of all of the boolean operations with combine. The outmost chain for example, should be able to represented with only four lines. To remedy this, there is makerjs.model.simplify - however there is an important caveat: your model must be originated before you can call the simplify function. This is to make sure that all of the segmented paths share the same coordinate space.

//expand a truss wireframe

var m = require('makerjs');

function trussWireframe(w, h) {

  this.models = {
    frame: new m.models.ConnectTheDots(true, [ [0, h], [w, 0], [0, 0] ])
  };

  var angled = this.models.frame.paths.ShapeLine1;

  var bracepoints = [
    [0, 0],
    m.point.middle(angled, 1/3),
    [w/2 , 0],
    m.point.middle(angled, 2/3)
  ];

  this.models.brace = new m.models.ConnectTheDots(false, bracepoints);
}

var truss = new trussWireframe(200, 50);
var expansion = m.model.expandPaths(truss, 3, 1);

//call originate before calling simplify:
m.model.originate(expansion);
m.model.simplify(expansion);

var svg = m.exporter.toSVG(expansion);

document.write(svg);

Be sure to play this example, and click 'show path names' for comparison.

Bezier curves

Bezier curves are a fascinating and complex topic too large to cover here, it is recommended that you visit A Primer on Bezier Curves by Mike Kamermans (aka Pomax). Maker.js depends on Pomax's Bezier.js which is a vital site to visit for understanding Bezier curve functionality in depth.

It is important to understand how Maker.js manages the complexity of these mathematical wonders. For this explanation, we will start at the end and work our way backwards to the beginning of the process.

The final representation of a Bezier curve in Maker.js is a model containing a series of circular arc paths which closely approximate the curve. Closer approximation means more calculation time and more arcs.

Prior to generating the arcs, the curve is broken down into a series of sub-curves. It is from the sub-curves that the arcs are generated. Each sub-curve is guaranteed to not have an "S" shape so that it more closely resembles a circular arc. The sub-curves are also broken at their rectangular "boundary box" points so that we are guaranteed that the boundary box tangent points are truly points on the curve and not approximations. In the Bezier.js terminology, these breaking points are known as extrema.

Now we are at the beginning of the process, where you call makerjs.models.BezierCurve with the new operator. You can create both quadratic and cubic Bezier curves. For either type, you may optionally pass the accuracy - the maximum distance between the true curve and the arc approximations. The default accuracy coefficient in Maker.js will produce an accurate and visually smooth curve in a reasonable calculation timeframe.


Create a quadratic Bezier curve in by passing an array of three points - an origin point, a control point, and an end point:

//create a quadratic Bezier curve

var m = require('makerjs');

var points = [[0, 100], [0, 0], [100, 0]];

var curve1 = new m.models.BezierCurve(points);
curve1.origin = [20, 20];

//more accurate
var curve2 = new m.models.BezierCurve(points, 0.01);

var model = {
  models: {
    c1: curve1, c2: curve2
  }
};

var svg = m.exporter.toSVG(model);

document.write(svg);

Create a cubic Bezier curve in by passing an array of four points - an origin point, a first control point (relating to the origin point), a second control point (relating to the end point), and an end point:

//create a cubic Bezier curve

var m = require('makerjs');

var points = [[0, 0], [50, 50], [50, -50], [100, 0]];

var curve1 = new m.models.BezierCurve(points);
curve1.origin = [0, 20];

//more accurate
var curve2 = new m.models.BezierCurve(points, 0.1);

var model = {
  models: {
    c1: curve1, c2: curve2
  }
};

var svg = m.exporter.toSVG(model);

document.write(svg);

Be sure to Play these examples, then click "show path names" to see the arcs representing the curve.

Fonts and text

To create models based on fonts, use makerjs.models.Text with the new operator. Pass a font object, your text, and a font size. Each character of your text string will become a child model containing the paths for that character.

Maker.js uses Opentype.js by Frederik De Bleser to read TrueType and OpenType fonts. Please visit the Opentype.js GitHub website for details on its API. You will need to know how to load font files before you can use them in Maker.js.


Loading fonts in the browser

Use opentype.load(url, callback) to load a font from a URL. Since this method goes out the network, it is asynchronous. The callback gets (err, font) where font is a Font object. Check if the err is null before using the font.

Previously, all of our examples ran synchronously and we could use document.write to output a result. But now we will need to wait for a font file to download. You will have to take this in consideration in your application. In the Maker.js Playground we can call playgroundRender(). Here on this page we will insert our SVG into a div in this document:

var makerjs = require('makerjs');

//load a font asynchronously
opentype.load('/fonts/stardosstencil/StardosStencil-Bold.ttf', function (err, font) {

    if (err) {
        document.getElementById('render-text').innerText = 'the font could not be loaded :(';
    } else {

        var textModel = new makerjs.models.Text(font, 'Hello', 100);

        var svg = makerjs.exporter.toSVG(textModel);

        document.getElementById('render-text').innerHTML = svg;
    }
});
...waiting for font to download...

Loading fonts in Node.js

Use opentype.loadSync(url) to load a font from a file and return a Font object. Throws an error if the font could not be parsed. This only works in Node.js.

var makerjs = require('makerjs');
var opentype = require('opentype.js');

var font = opentype.loadSync('./fonts/stardosstencil/StardosStencil-Regular.ttf');

var textModel = new makerjs.models.Text(font, 'Hello', 100);

console.log(makerjs.exporter.toSVG(textModel));

Finally, a phenomenon to be aware of is that fonts aren't always perfect. You may encounter cases where paths within a character are self-intersecting or otherwise not forming closed geometries. This is not common, but it is something to be aware of, especially during combine operations.

Layout on a path

You can use a path as a layout guide for a row of child models within a model. Call makerjs.layout.childrenOnPath(parentModel: Model, onPath: Path), the x-axis will be projected onto your onPath:
//render a row of squares on a path

var makerjs = require('makerjs');

var square = new makerjs.models.Square(10);

var row = makerjs.layout.cloneToRow(square, 10, 10);

var arc = new makerjs.paths.Arc([0, 0], 150, 45, 135);

makerjs.layout.childrenOnPath(row, arc);

var svg = makerjs.exporter.toSVG(row);

document.write(svg);
To better see how layout is performed, let's show the arc in red and add a triangle to the first square:
//render a row of squares on a path

var makerjs = require('makerjs');

var square = new makerjs.models.Square(10);

var row = makerjs.layout.cloneToRow(square, 10, 10);

//add a triangle to the first model
row.models[0].models = { triangle: new makerjs.models.ConnectTheDots(true, [ [5, 8], [2, 2], [8, 2] ]) };

var arc = new makerjs.paths.Arc([0, 0], 150, 45, 135);

makerjs.layout.childrenOnPath(row, arc);

//show the arc in red
arc.layer = "red";
row.paths = { arc: arc };

var svg = makerjs.exporter.toSVG(row);

document.write(svg);
You may be surprised to see that the first model is upside down and on the right! This is because the x-axis of the row has been projected onto the arc. The arc starts at 45 degrees and ends at 135 degrees - increasing to the left. The x-axis increases to the right, therefore it appears upside down. Fortunately, there are additional optional parameters to this makerjs.layout.childrenOnPath which let you control this behavior:
  • baseline: number [default: 0]
  • reversed: boolean [default: false]
  • contain: boolean [default: false]
  • rotate: boolean [default: true]

baseline

This is a number, ususally between 0 and 1, to determine where to place each model "vertically" on the layout path. This is a ratio of the parentModel's total height above the x-axis. You may also use a negative number or greater than 1 for interesting effects. Use 0.5 to place a model at the y-center:
//render a row of squares on a path

var makerjs = require('makerjs');

var square = new makerjs.models.Square(10);

var row = makerjs.layout.cloneToRow(square, 10, 10);

row.models[0].models = { triangle: new makerjs.models.ConnectTheDots(true, [ [5, 8], [2, 2], [8, 2] ]) };

var arc = new makerjs.paths.Arc([0, 0], 150, 45, 135);

//layout on the y-center
makerjs.layout.childrenOnPath(row, arc, 0.5);

arc.layer = "red";
row.paths = { arc: arc };

var svg = makerjs.exporter.toSVG(row);

document.write(svg);

reversed

This option will not work for a circle. Lines and arcs each have a concept of a directional flow:
  • line - a line flows from its origin to its end.
  • arc - an arc flows from its startAngle to its endAngle, in the polar (counter-clockwise) direction.
If you want to plot the opposite direction, set reversed to true:
//render a row of squares on a path

var makerjs = require('makerjs');

var square = new makerjs.models.Square(10);

var row = makerjs.layout.cloneToRow(square, 10, 10);

row.models[0].models = { triangle: new makerjs.models.ConnectTheDots(true, [ [5, 8], [2, 2], [8, 2] ]) };

var arc = new makerjs.paths.Arc([0, 0], 150, 45, 135);

//layout on the y-center, reversed
makerjs.layout.childrenOnPath(row, arc, 0.5, true);

arc.layer = "red";
row.paths = { arc: arc };

var svg = makerjs.exporter.toSVG(row);

document.write(svg);

contain

You may notice that the red arc's endpoints are in the x-center of the first and last children. To contain the children within the span, set contain to true:
//render a row of squares on a path

var makerjs = require('makerjs');

var square = new makerjs.models.Square(10);

var row = makerjs.layout.cloneToRow(square, 10, 10);

row.models[0].models = { triangle: new makerjs.models.ConnectTheDots(true, [ [5, 8], [2, 2], [8, 2] ]) };

var arc = new makerjs.paths.Arc([0, 0], 150, 45, 135);

//layout on the y-center, reversed, contained
makerjs.layout.childrenOnPath(row, arc, 0.5, true, true);

arc.layer = "red";
row.paths = { arc: arc };

var svg = makerjs.exporter.toSVG(row);

document.write(svg);

rotate

If you wish each child to be placed on the path but not rotated, set rotate to false:
//render a row of squares on a path

var makerjs = require('makerjs');

var square = new makerjs.models.Square(10);

var row = makerjs.layout.cloneToRow(square, 10, 10);

row.models[0].models = { triangle: new makerjs.models.ConnectTheDots(true, [ [5, 8], [2, 2], [8, 2] ]) };

var arc = new makerjs.paths.Arc([0, 0], 150, 45, 135);

//layout on the y-center, reversed, not contained, not rotated
makerjs.layout.childrenOnPath(row, arc, 0.5, true, false, false);

arc.layer = "red";
row.paths = { arc: arc };

var svg = makerjs.exporter.toSVG(row);

document.write(svg);

Laying out text

Layout on a path works well with fonts and text. See an example here.

Solvers

Maker.js provides a solvers module for common trigonometry equations, such as solving triangles.

Next: Model trees.