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.

edit on GitHub

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.

edit on GitHub

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);
edit on GitHub

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);
edit on GitHub

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);
edit on GitHub

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);
edit on GitHub

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.

edit on GitHub

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('/maker.js/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('Hello', font);

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.

edit on GitHub

Layers

Layers are a way of logically grouping your paths or models as you see fit. Simply add a layer property to any path or model object, with the name of the layer. Every path within a model will automatically inherit its parent model's layer, unless it has its own layer property. As you can see in this example, a layer can transcend the logical grouping boundaries of models:

//render a round rectangle with arcs in their own layer

var makerjs = require('makerjs');

var roundRect = new makerjs.models.RoundRectangle(100, 50, 10);
roundRect.layer = "layer1";

roundRect.paths["BottomLeft"].layer = "layer2";
roundRect.paths["BottomRight"].layer = "layer2";
roundRect.paths["TopRight"].layer = "layer2";
roundRect.paths["TopLeft"].layer = "layer2";

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

document.write(svg);
Layers are not visible in this example but they logically exist to separate arcs from straight lines.

Layers will be output during the export process in these formats:

  • DXF - paths will be assigned to a DXF layer.
  • SVG - in continuous mode, a new <path> element will be created for each layer.

edit on GitHub

Chains

Chains are an ordered collection of paths that connect end-to-end. A chain may be endless which means the chain ends at its beginning (and is most likely forming a closed geometry). The paths in a chain might be contained in more than one model. To illustrate this, we will look at the example from the combine operations above:

//a rectangle and an oval

var m = require('makerjs');

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

var x = new example();

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

document.write(svg);

Above we see two chains: all of the lines in the rectangle form an endless chain, as well as all of the paths of the oval.

Now, let's combine these in a union:

//combine a rectangle and an oval

var m = require('makerjs');

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

var x = new example();

m.model.combineUnion(x.models.rect, x.models.oval);

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

document.write(svg);

We have combined the rectangle and the oval, and they now form one chain. Yet they are still two models. Next we will add a few more paths to the example:

//combine a rectangle and an oval, add some other paths

var m = require('makerjs');

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

var x = new example();

m.model.combineUnion(x.models.rect, x.models.oval);

//add a couple more paths into the scene
x.paths = {
    line1: new m.paths.Line([150, 10], [220, 10]),
    line2: new m.paths.Line([220, 50], [220, 10]),
    line3: new m.paths.Line([220, 75], [260, 35]),
    circle: new m.paths.Circle([185, 50], 15)
};

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

document.write(svg);

A circle is a closed geometry. In Maker.js, a circle is also an endless chain. Line1 and line2 form a chain. The diagonal line3 is not part of a chain.

Now that we have these concepts illustrated, let's call makerjs.model.findChains, passing our model, and a function to collect what we found. This function will be passed these three parameters:

  • chains: an array of chains that were found. (both endless and non-endless)
  • loose: an array of paths that did not connect in a chain.
  • layer: the layer name containing the above.
This function will get called once for each logical layer. Since our example has no layers (logically it's all one "null" layer), our function will only get called once.

//combine a rectangle and an oval, add some other paths

var m = require('makerjs');

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

var x = new example();

m.model.combineUnion(x.models.rect, x.models.oval);

x.paths = {
    line1: new m.paths.Line([150, 10], [220, 10]),
    line2: new m.paths.Line([220, 50], [220, 10]),
    line3: new m.paths.Line([220, 75], [260, 35]),
    circle: new m.paths.Circle([185, 50], 15)
};

//find chains and output the results, without even rendering visually

m.model.findChains(x, function(chains, loose, layer) {
    document.write('found ' + chains.length + ' chain(s) and ' + loose.length + ' loose path(s) on layer ' + layer);
});

chains array
As expected, we found 3 chains. But what is a chain? A chain is an object with these two properties:
  • endless: boolean.
  • links: array of chain link objects.
A chain link has these properties:
  • reversed: boolean, explained below.
  • walkedPath: a walkPath object, which contains one path. We will explain this type of object in the next section.
For any chain, it is quite arbitrary which is the first link and which is the last link. It is still the same chain if it were to begin at the last link and end at the first - but if it did, then the links would also be reversed. So this is what it means for a link to be reversed, but this also means that the path would "flow" in reverse. Here is how paths flow normally:
  • 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.
Therefore, if the reversed flag is true, then the path in the chain link was found to flow opposite of the norm.
loose paths array
This is an array of walkPath objects, explained in the next section.

edit on GitHub

Walking a model tree

A model is a tree structure which may contain paths, and it may also contain other models in a heirachy. You can traverse the entire tree by calling makerjs.model.walk with your model and an object with these optional properties:

  • onPath: function(walkPath object) - called for every path (in every model) in your tree.
  • beforeChildWalk: function(walkModel) - called for every model in your tree, prior to diving deeper down the tree. Return false if you wish to not dive deeper.
  • afterChildWalk: function(walkModel) - called for every model in your tree, after returning from a deep dive down the tree.

walkPath object

A walkPath object has these properties:

  • layer: the layer name (if any) containing this path.
  • modelContext: the model containing this path.
  • offset: the absolute coordinates from [0, 0] where this path is located.
  • pathContext: the path itself.
  • pathId: the id of this path in its parent model.paths container.
  • route: array of property names to locate this path from the root of the tree.
  • routeKey: a string representation of the route which may safely be used as a unique key identifier for this path.

walkModel object

A walkModel object has these properties:

  • childId: the id of this model in its parent model.models container.
  • childModel: the model itself
  • layer: the layer name (if any) containing this path.
  • offset: the absolute coordinates from [0, 0] where this model is located.
  • parentModel: the model containing this model.
  • route: array of property names to locate this model from the root of the tree.
  • routeKey: a string representation of the route which may safely be used as a unique key identifier for this model.

edit on GitHub