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

Working with chains

Chain theory

When 2 or more paths connect end to end, we call this a chain. Here are 3 lines that connect end to end, forming a chain with 3 links; each line path is considered a link in the chain:

When the links do not have any loose ends and connect to each other, we call this an endless chain. Frequently, endless chains are used to represent a closed geometry. Here is an endless chain made up of 2 lines and an arc:

A circle is a closed geometry by nature. In Maker.js, a single circle comprises an endless chain with only one link.

A chain may contain other chains, recursively. A chain may only contain others if it is an endless chain itself. Here are some examples of one chain containing another:

Here is a model which does not have any chains. Although the lines overlap, they do not connect end to end.

Chains are implicit

You do not explicitly define chains in your drawing, chains are something that Maker.js finds in your model(s).

Finding

Call one of these two functions to find chains, which will return one or more Chain objects:

Chain object type

  • contains: - (optional) Chains that are contained within this chain. Populated when chains are found with the 'contain' option
  • endless: boolean - Flag if this chain forms a loop end to end.
  • links: - The links in this chain.
  • pathLength: number - Total length of all paths in the chain.

Find a single chain

Let's start with a drawing of a rectangle. A rectangle is a model, but we also implicitly know that a rectangle comprises a chain of 4 paths which connect end to end. Let's find this chain now using makerjs.model.findSingleChain(model):
//from a rectangle, find a single chain

var makerjs = require('makerjs');

var model = new makerjs.models.Rectangle(100, 50);

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

document.write(svg);

//now find the chain
var chain = makerjs.model.findSingleChain(model);

document.write('found a chain with ' + chain.links.length + ' links and endless=' + chain.endless);
Now, let's combine two rectangle models in a union. Notice that a chain will continue unconstrained by the fact that the two rectangle models are independent:
//combine 2 rectangles

var makerjs = require('makerjs');

var drawing = {
    models: {
        r1: new makerjs.models.Rectangle(100, 50),
        r2: makerjs.model.move(new makerjs.models.Rectangle(100, 50), [50, 25])
    }
};

makerjs.model.combineUnion(drawing.models.r1, drawing.models.r2);

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

document.write(svg);

//now find the chain
var chain = makerjs.model.findSingleChain(drawing);

document.write('found a chain with ' + chain.links.length + ' links and endless=' + chain.endless);

Chain links

Each path in the chain is represented by a ChainLink wrapper object in the links array. This ChainLink wrapper tells us how the path relates to the rest of the chain. Each ChainLink array element is connected to the next and previous element. If the chain is endless, then the last array element is connected to the first, and vice-versa.

ChainLink object

  • endPoints: - The endpoints of the path, in absolute coords.
  • pathLength: number - Length of the path.
  • reversed: boolean - Path flows forwards or reverse.
  • walkedPath: WalkPath - Reference to the path.
The path itself can be found in the walkedPath property which is a WalkPath object, the same type of object used in walking a model tree.

Natural path flow

The three types of paths in Maker.js are line, arc and circle. A circle has no end points, and therefore cannot connect to other paths to form a chain. Lines and arcs however, may connect to other lines or arcs at their end points to form chains. In context of a chain, 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.
The reversed property of a ChainLink denotes that the link's path flows in the opposite direction of its natural flow to connect to its neighboring links.

Order of chain links

You may have already noticed that we have not specified the order of the links array. For example, given a chain with 3 links A, B, C - the order may also be C, B, A. So, what is the order of the links in a chain? The answer is: it is quite arbitrary. There is no guarantee that the order will be the same each time across JavaScript runtime environments.

Reverse a chain

If you have deduced that your chain needs to be reversed, you can call makerjs.chain.reverse(chain).

Beginning of a chain

Another issue with endless chains is, which link is at the beginning of the links array? The answer once again, is that it is unpredictable. If you need to specify which link is at the beginning of an endless chain, you have 2 functions at your disposal:

Clockwise

If you have an endless chain, you also have the option to see if your links flow in a clockwise direction. Call makerjs.measure.isChainClockwise(chain) which returns a boolean, unless your chain has one link which is a circle - in which case it will return null.

Find multiple chains

You can find multiple chains by calling makerjs.model.findChains(model), which will return an array of chains, sorted by largest to smallest on the pathLength property. We can find 2 chains in this drawing with 2 rectangles:
//2 concentric rectangles

var makerjs = require('makerjs');

var model = {
    models: {
        outer: makerjs.model.center(new makerjs.models.Rectangle(60, 30)),
        inner: makerjs.model.center(new makerjs.models.Rectangle(45, 15))
    }
};

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

document.write(svg);

//now find the chains
var chains = makerjs.model.findChains(model);

document.write('found ' + chains.length + ' chains');

Containment

Instead of a "flat" array, we can see the containment of chains by also passing an { contain: true } object to makerjs.model.findChains(model, options):
//2 concentric rectangles

var makerjs = require('makerjs');

var model = {
    models: {
        outer: makerjs.model.center(new makerjs.models.Rectangle(60, 30)),
        inner: makerjs.model.center(new makerjs.models.Rectangle(45, 15))
    }
};

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

document.write(svg);

//now find the contained chains
var chains = makerjs.model.findChains(model, { contain: true });

document.write('found ' + chains.length + ' chain(s) ');
document.write('which contains ' + chains[0].contains.length + ' chain(s)');

Alternating flow directions

There are scenarios where you may need contained chains to flow in the opposite direction of their containing chain. This will require extra computation on each chain to test its direction. If you need this, use { contain: { alternateDirection: true } } in your options. In the returned chains array, the outmost chains will flow clockwise:
//2 concentric rectangles

var makerjs = require('makerjs');

var model = {
    models: {
        outer: makerjs.model.center(new makerjs.models.Rectangle(60, 30)),
        inner: makerjs.model.center(new makerjs.models.Rectangle(45, 15))
    }
};

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

document.write(svg);

//now find the contained chains with alternating direction
var chains = makerjs.model.findChains(model, { contain: { alternateDirection: true } });

document.write('found ' + chains.length + ' chain(s)<br/>');
document.write('which contains ' + chains[0].contains.length + ' chain(s)<br/>');
document.write('outer is clockwise:' + makerjs.measure.isChainClockwise(chains[0]) + '<br/>');
document.write('inner is clockwise:' + makerjs.measure.isChainClockwise(chains[0].contains[0]));

Isolating within layers

You can find chains within layers by passing { byLayers: true } in your options. This will not return an array, but it will return an object map with keys being the layer names, and values being the array of chains for that layer:
//find chains on layers

var makerjs = require('makerjs');

var c1 = new makerjs.paths.Circle(1);
var c2 = new makerjs.paths.Circle(1);

c2.origin = [3, 0];

c1.layer = 'red';
c2.layer = 'blue';

var model = { paths: { c1: c1, c2: c2 } };

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

document.write(svg);

//now find the chains by layer
var chains = makerjs.model.findChains(model, { byLayers: true });

document.write('found ' + chains['red'].length + ' chain(s) on red layer<br/>');
document.write('found ' + chains['blue'].length + ' chain(s) on blue layer');

Finding loose paths

You may also wish to find paths that are not part of a chain. This will require you to pass a callback function which 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)
};

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

document.write(svg);

//find chains and output the results

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

Chain to key points

If you want a "low poly" representation of a chain, call makerjs.model.toKeyPoints(chain, [optional] maxArcFacet) passing your chain, and the maximum length of facets on arcs & circles:
//convert a round rectangle to key points

var makerjs = require('makerjs');

var rect = new makerjs.models.RoundRectangle(100, 50, 10);

var chain = makerjs.model.findSingleChain(rect);
var keyPoints = makerjs.chain.toKeyPoints(chain, 5);

var model = {
  models: {
    rect: rect,
    dots: new makerjs.models.Holes(1, keyPoints)
  }
};

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

document.write(svg);

Chain to points

To get points consistently spaced along a chains, call makerjs.model.toPoints(chain, distance) passing your chain, and the distance between points:
//convert a round rectangle to points

var makerjs = require('makerjs');

var rect = new makerjs.models.RoundRectangle(100, 50, 10);

var chain = makerjs.model.findSingleChain(rect);
var spacing = 10;
var keyPoints = makerjs.chain.toPoints(chain, spacing);

var model = {
  models: {
    rect: rect,
    dots: new makerjs.models.Holes(1, keyPoints)
  }
};

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

document.write(svg);
Hint: you can use the pathLength property of the chain to make sure your distance divides equally on the entire chain:
//convert a round rectangle to points

var makerjs = require('makerjs');

var rect = new makerjs.models.RoundRectangle(100, 50, 10);

var chain = makerjs.model.findSingleChain(rect);
var minimumSpacing = 10;
var divisions = Math.floor(chain.pathLength / minimumSpacing);
var spacing = chain.pathLength / divisions;

console.log(spacing);

var keyPoints = makerjs.chain.toPoints(chain, spacing);

var model = {
  models: {
    rect: rect,
    dots: new makerjs.models.Holes(1, keyPoints)
  }
};

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

document.write(svg);

Chain fillet

A fillet can be added between all paths in a chain by calling makerjs.chain.fillet with these parameters:
  • chainToFillet: the chain containing paths which will be modified to have fillets at their joints.
  • filletRadius: radius of the fillets.
This will modify all of the chain's paths to accomodate an arc between each other, and it will return a new model containing all of the fillets which fit. This new model should be added into your tree.
Basic example
Let's draw a few lines that we know will form a chain:
//render a model with paths that form a chain

var makerjs = require('makerjs');

var model = {

  paths: {
    "0": new makerjs.paths.Line([0, 0], [100, 0]),
    "1": new makerjs.paths.Line([100, 0], [100, 100]),
    "2": new makerjs.paths.Line([100, 100], [200, 100])
  }

};

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

document.write(svg);
Next we will find all of the chains in our model. We are expecting that there will only be one chain, so we will just take chains[0]. Then we will add fillets to that chain:
//render a model with paths that form a chain

var makerjs = require('makerjs');

var model = {

  paths: {
    "0": new makerjs.paths.Line([0, 0], [100, 0]),
    "1": new makerjs.paths.Line([100, 0], [100, 100]),
    "2": new makerjs.paths.Line([100, 100], [200, 100])
  },

  //create a placeholder in the tree for more models
  models: {}

};

//find the chain
var chain = makerjs.model.findSingleChain(model);

//add fillets to the chain
var filletsModel = makerjs.chain.fillet(chain, 10);

//put the fillets in the tree
model.models.fillets = filletsModel;

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

document.write(svg);
Advanced example
We can improve upon the design of the truss example by adding fillets to the interior shapes. Let's review the truss design:
//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);
We know that there are 5 chains in this drawing. When we find chains, the array of found chains will be sorted by pathLength (the total length of all paths in each chain), so we know that the first chain represents the outermost perimeter of the drawing. Therefore we will ignore chains[0] and create a for...loop beginning at chains[1]:
//fillet all interior chains in the 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 expansion = m.model.expandPaths(truss, 3, 1);

m.model.originate(expansion);
m.model.simplify(expansion);

//find chains
var chains = m.model.findChains(expansion);

//start at 1 - ignore the longest chain which is the perimeter
for (var i = 1; i < chains.length; i++) {

  //save the fillets in the model tree 
  expansion.models['fillets' + i] = m.chain.fillet(chains[i], 2);
}

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

document.write(svg);

Chain dogbone

A dogbone fillet can be added between all line paths in a chain by calling makerjs.chain.dogbone with these parameters:
  • chainToFillet: the chain containing paths which will be modified to have dogbone fillets at their joints.
  • filletRadiusOrFilletRadii: Either of:
    • a number, specifying the radius of the dogbone fillets at every link junction.
    • an object, with these optional properties:
      • left: radius of the dogbone fillets at every left-turning link junction.
      • right: radius of the dogbone fillets at every right-turning link junction.
This will modify all of the chain's line paths to accomodate an arc between each other, and it will return a new model containing all of the dogbone fillets which fit. This new model should be added into your tree.
Left turn and right turn example
The direction of turns are in context of which direction the chain is "flowing". An endless chain might flow either clockwise or counter-clockwise. Let's decide to make our chain clockwise. Now when we follow the chain's links in a clockwise direction, right turns will be on the "outside" corners of the shape, and left turns will be on the "inside" corners of the shape. Let's make a shape that is a cutout to represent both the inside and outside of a cut:
//make a plus that is cut out from a square

var makerjs = require('makerjs');

var plus = makerjs.model.combineUnion(
  makerjs.model.center(new makerjs.models.Rectangle(50, 100)),
  makerjs.model.center(new makerjs.models.Rectangle(100, 50))
);

var plus2 = makerjs.cloneObject(plus);
plus2.origin = [150, 0];

var outer = makerjs.model.center(new makerjs.models.Rectangle(150, 150));

var model = {
  models: { plus, plus2, outer }  //using Shorthand property names :)
}

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

document.write(svg);
Next, lets find the chains for each plus, and ensure they are clockwise. Then we can add dogbones to the "outside" corners of the plus that is contained within the square, and to the "inside" corners of the plus that is apart:
//make a plus that is cut out from a square

var makerjs = require('makerjs');

var plus1 = makerjs.model.combineUnion(
  makerjs.model.center(new makerjs.models.Rectangle(50, 100)),
  makerjs.model.center(new makerjs.models.Rectangle(100, 50))
);

var plus2 = makerjs.cloneObject(plus1);
plus2.origin = [150, 0];

var square = makerjs.model.center(new makerjs.models.Square(150));

//find chains for each plus
var chain1 = makerjs.model.findSingleChain(plus1);
var chain2 = makerjs.model.findSingleChain(plus2);

//make sure our chains are clockwise
[chain1, chain2].forEach(chain => {
  if (makerjs.measure.isChainClockwise(chain)) makerjs.chain.reverse(chain);
});

//add dogbones for left and right turns
var dogbones1 = makerjs.chain.dogbone(chain1, { left: 5});
var dogbones2 = makerjs.chain.dogbone(chain2, { right: 5});

var model = {
  models: { plus1, plus2, square, dogbones1, dogbones2 }
}

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

document.write(svg);

Chain to new model

Once you have a chain, you can also convert it to a model, so that you can return to using the familiar model API with your shapes. Call makerjs.chain.toNewModel(chain, detachFromOldModel: boolean).

Layout on a chain

Similar to layout on a path, you can use a chain as a layout guide for a row of child models within a model. Call makerjs.layout.childrenOnChain(parentModel: Model, onChain: chain), the x-axis will be projected onto your onChain:
//render a row of squares on a chain

var makerjs = require('makerjs');

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

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

var curve = new makerjs.models.BezierCurve([0, 0], [33, 25], [66, -25], [100, 0]);

var chain = makerjs.model.findSingleChain(curve);

makerjs.layout.childrenOnChain(row, chain, 0.5, false, true);

var model = {
    models: {
        curve: curve,
        row: row
    }
};

curve.layer = "red";

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

document.write(svg);
There are additional optional parameters to this makerjs.layout.childrenOnChain:
  • baseline: number [default: 0]
  • reversed: boolean [default: false]
  • contain: boolean [default: false]
  • rotate: boolean [default: true]
These behave the same as when laying out on a path. See layout on a path for explanation.

Laying out text

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