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

Intermediate drawing

Zeroing and Centering

To move a model so that its bottom left edge is at [0, 0] use model.zero:

//zero a model

var makerjs = require('makerjs');

var model = {
  models: {

    crosshairs: {
      paths: {
        h: new makerjs.paths.Line([-5, 0], [5, 0]),
        v: new makerjs.paths.Line([0, -5], [0, 5])
      }
    },

    nut: {
      models: {
        polygon: new makerjs.models.Polygon(6, 40)
      },
      paths: {
        inner: new makerjs.paths.Circle(20)
      }
    }

  }
};

makerjs.model.zero(model.models.nut);

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

document.write(svg);

To move a model so that it is centered on [0, 0] use model.center:

//center a couple of models

var makerjs = require('makerjs');

var model = {
  models: {

    crosshairs: {
      paths: {
        h: new makerjs.paths.Line([-5, 0], [5, 0]),
        v: new makerjs.paths.Line([0, -5], [0, 5])
      }
    },

    box: {
      models: {
        outer: new makerjs.models.Rectangle(60, 30),
        inner: new makerjs.models.Oval(45, 15)
      },
    }

  }
};

var shortcut = model.models.box.models;

makerjs.model.center(shortcut.outer);
makerjs.model.center(shortcut.inner);

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

document.write(svg);
edit on GitHub

Originating

A path within a model is referenced relatively to its parent model. There may be times when you want all objects to be within the same coordinate space. Let's create a simple demonstration model:

//render a couple boxes in their own coordinate space

var makerjs = require('makerjs');

function box(origin) {
    this.models = {
        outer: new makerjs.models.RoundRectangle(100, 100, 1),
    };
    this.paths = {
      inner: new makerjs.paths.Circle([50, 50], 25)
    };

    this.origin = origin;
}

var box1 = new box([0, 0]);
var box2 = new box([150, 0]);

var model = {
    models: {
        box1: box1,
        box2: box2
    }
};

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

document.write(svg);

console.log(box1.paths.inner.origin);
console.log(box2.paths.inner.origin);

In this example, both box1.paths.inner.origin and box2.paths.inner.origin have an origin of [50, 50] even though they are not in the same place, because they are located relative to the model that contains them. To make all models and paths occupy a singular coordinate space, we can use makerjs.model.originate:

//render a couple boxes in the same coordinate space

var makerjs = require('makerjs');

function box(origin) {
    this.models = {
        outer: new makerjs.models.RoundRectangle(100, 100, 1),
    };
    this.paths = {
      inner: new makerjs.paths.Circle([50, 50], 25)
    };

    this.origin = origin;
}

var box1 = new box([0, 0]);
var box2 = new box([150, 0]);

var model = {
    models: {
        box1: box1,
        box2: box2
    }
};

//move all path origins into the same space
makerjs.model.originate(model);

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

document.write(svg);

console.log(box1.paths.inner.origin);
console.log(box2.paths.inner.origin);

Now box1.paths.inner.origin and box2.paths.inner.origin have the origins [50, 50] and [200, 50].

edit on GitHub

Scaling

To proportionately scale a simple point, use makerjs.point.scale. To proportionately scale paths and models, use these functions:

Each of these functions return the original object, so that we can "chain" on the same line of code.

Scale path example:

//render a scaled arc

var makerjs = require('makerjs');

var arc1 = new makerjs.paths.Arc([0, 0], 25, 0, 90);
var arc2 = new makerjs.paths.Arc([0, 0], 25, 0, 90);

arc2 = makerjs.path.scale(arc2, 2);

var svg = makerjs.exporter.toSVG({ paths: { arc1: arc1, arc2: arc2 }});

document.write(svg);

Scale model example:

//render a scaled polygon

var makerjs = require('makerjs');

var model = {
  models: {
    inner: new makerjs.models.Polygon(6, 40),
    outer: makerjs.model.scale(new makerjs.models.Polygon(6, 40), 1.7)
  }
};

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

document.write(svg);
edit on GitHub

Rotating

To rotate a single point, see makerjs.point.fromPolar and makerjs.point.rotate depending on what you are trying to achieve.

You can rotate paths and models with these functions:

Each of these functions return the original object, so that we can "chain" on the same line of code.

Rotate path example:

//render a rotated line

var makerjs = require('makerjs');

var line1 = new makerjs.paths.Line([0, 0], [100, 0]);
var line2 = new makerjs.paths.Line([0, 0], [100, 0]);

var paths = [line1, makerjs.path.rotate(line2, -30, [100, 0])];

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

document.write(svg);

Rotate model example:

//render a rotated rectangle

var makerjs = require('makerjs');

var rect1 = new makerjs.models.Rectangle(40, 80);

makerjs.model.rotate(rect1, 45, [0, 0]);

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

document.write(svg);
edit on GitHub

Cloning

Models and paths are simple JavaScript objects, so they are easy to clone is a way that is standard to JavaScript. Maker.js provides a few functions for cloning:

Cloning is useful in many situations. For example, if you need many copies of a model for rotation:

//clone and rotate

var makerjs = require('makerjs');

function sawtooth(numTeeth, r1, rd, offset) {
    var a = 360 / numTeeth;
    var a1 = 90 - a / 2;
    var r2 = r1 + rd;
    var p1 = makerjs.point.fromPolar(makerjs.angle.toRadians(a1), r1);
    var p2 = makerjs.point.rotate(p1, a, [0, 0]);

    var p3 = [-offset, r2];

    this.paths = {
        outer: new makerjs.paths.Arc(p1, p3, r2 / 4, false, false),
        inner: new makerjs.paths.Arc(p2, p3, r1 / 4, false, false)
    };
}

var wheel = { models: {} };
var numTeeth = 30;
var tooth = new sawtooth(numTeeth, 100, 20, 10);

for (var i = 0; i < numTeeth; i++ ) {
    var clone = makerjs.cloneObject(tooth);
    var a = 360 / numTeeth;
    makerjs.model.rotate(clone, a * i, [0, 0]);
    wheel.models[i] = clone;
}

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

document.write(svg);
edit on GitHub

Mirroring

Use makerjs.angle.mirror to get a mirror of an angle, and makerjs.point.mirror to get a mirror of a simple point.

You can create a mirrored copy of paths and models with the following functions. The mirroring can occur on the x axis, the y axis, or both.

Each of these functions returns a new object and does not modify the original.

Mirror path example:

//render a line mirrored in the x dimension

var makerjs = require('makerjs');

var line1 = new makerjs.paths.Line([0, 0], [100, 100]);
var line2 = makerjs.path.mirror(line1, true, false);

var paths = [line1, line2];

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

document.write(svg);

Mirror model example:

//render a model mirrored in the y dimension

var makerjs = require('makerjs');

var ovalArc1 = new makerjs.models.OvalArc(45, 135, 50, 10);

var model = {
    models: {
        ovalArc1: ovalArc1,
        ovalArc2: makerjs.model.mirror(ovalArc1, false, true)
    }
};

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

document.write(svg);

Hint: When creating symmetrical models, it may be easier to create one half, and then use mirror to generate the other half.

edit on GitHub

Intersection

You can find the point(s) of intersection between two paths using makerjs.path.intersection. If the paths do not intersect, this function will return null. Otherwise, it will return an object with a property named intersectionPoints which is an array of points. Additionally, if either path was an arc or circle, this object will contain the angles at which an intersection occurred.

Intersection examples:

//line-line intersection

var makerjs = require('makerjs');

var model = {
    paths: {
        line1: new makerjs.paths.Line([0, 0], [20, 10]),
        line2: new makerjs.paths.Line([2, 10], [50, 2])
    }
}

var int = makerjs.path.intersection(model.paths.line1, model.paths.line2);

if (int) {
    var p = int.intersectionPoints[0];
    var id = JSON.stringify(makerjs.point.rounded(p, .01));
    model.paths[id] = new makerjs.paths.Circle(p, 1);
}

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

document.write(svg);
//circle-circle intersection

var makerjs = require('makerjs');

var model = {
    paths: {
        circle1: new makerjs.paths.Circle([0, 10], 20),
        circle2: new makerjs.paths.Circle([20, 0], 20)
    }
}

var int = makerjs.path.intersection(model.paths.circle1, model.paths.circle2);

if (int) {
    int.intersectionPoints.forEach(
        function(p, i) {
            var id = JSON.stringify(makerjs.point.rounded(p, .01)) + ' intersects circle1 at ' + makerjs.round(int.path1Angles[i], .01) + ' circle2 at ' + makerjs.round(int.path2Angles[i], .01);
            model.paths[id] = new makerjs.paths.Circle(p, 1);
        }
    );
}

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

document.write(svg);
//line-arc intersection

var makerjs = require('makerjs');

var model = {
    paths: {
        line1: new makerjs.paths.Line([0, 0], [20, 10]),
        arc1: new makerjs.paths.Arc([12, 0], 10, 45,215)
    }
}

var int = makerjs.path.intersection(model.paths.line1, model.paths.arc1);

if (int) {
    int.intersectionPoints.forEach(
        function(p, i) {
            var id = JSON.stringify(makerjs.point.rounded(p, .01)) + ' arc1 at ' + makerjs.round(int.path2Angles[i], .01);
            model.paths[id] = new makerjs.paths.Circle(p, 1);
        }
    );
}

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

document.write(svg);
edit on GitHub

Converging lines

To make lines meet at their slope intersection point, use makerjs.path.converge. This function will only work with lines, it will not work with arcs.

The converge function will try to use the end of the line that is closest to the convergence point. If you need to specify which ends of your lines should be converged, pass two additional boolean values. The boolean value is true to use the line's origin, false to use the end.

Converge example:

//converge lines

var makerjs = require('makerjs');

var model = {
  origin: [0, 0],
  paths: {
    line1: new makerjs.paths.Line([0, 0], [10, 5]),
    line2: new makerjs.paths.Line([0, 10], [10, 4]),
    line3: new makerjs.paths.Line([1, 0], [5, -2])
  }
}

var clone1 = makerjs.cloneObject(model);
clone1.origin = [10, 0];

var clone2 = makerjs.cloneObject(model);
clone2.origin = [20, 0];

makerjs.path.converge(clone1.paths.line1, clone1.paths.line2);
makerjs.path.converge(clone1.paths.line1, clone1.paths.line3);

makerjs.path.converge(clone2.paths.line1, clone2.paths.line2, false, true);
makerjs.path.converge(clone2.paths.line1, clone2.paths.line3, true, false);

var svg = makerjs.exporter.toSVG({ models: { before: clone1, after: model, x: clone2 } });

document.write(svg);
edit on GitHub

Modifying models

We know that models are relatively simple objects with a well known recursive structure. This allows us to modify them for different purposes. Let's modify and combine two different models in one drawing.

For this example we will use ovals to make an oval L shape. We begin by creating a model function that has two ovals:

//render two ovals which overlap

var makerjs = require('makerjs');

function ovalL(width, height, thickness) {
    var ovalH = new makerjs.models.Oval(width, thickness);
    var ovalV = new makerjs.models.Oval(thickness, height);

    this.models = {
        h: ovalH, v: ovalV
    };
}

var svg = makerjs.exporter.toSVG(new ovalL(100, 100, 37));

document.write(svg);

There are overlapping arcs in the lower left corner. We can remove them if we know their id and position in the heirarchy. There are several ways we can inspect this model, here are a few:

  • Look at the code which created it. This may involve deep lookups. For example, the Oval source code references the RoundRectangle source code.
  • Use the browser's console, or JavaScript debugger to set a breakpoint in your model.
  • Use the browser's DOM inspector to traverse the rendered SVG.
  • Output the raw JSON or SVG on screen.
  • Use the Playground app and click "show path names".

By looking at the source code we know that an Oval is a RoundRectangle and that the ids for arcs are BottomLeft, BottomRight, TopLeft and TopRight. The ids for the sides are Left, Right, Top and Bottom. Also, we need to note the orientation of these lines so we know which are origin and end points.

To remove a path we use the JavaScript delete keyword:

//render two ovals which overlap

var makerjs = require('makerjs');

function ovalL(width, height, thickness) {
    var ovalH = new makerjs.models.Oval(width, thickness);
    var ovalV = new makerjs.models.Oval(thickness, height);

    //delete the lower arcs from the vertical oval
    delete ovalV.paths.BottomLeft;
    delete ovalV.paths.BottomRight;

    //delete the inside arc of the horizontal
    delete ovalH.paths.TopLeft;

    this.models = {
        h: ovalH, v: ovalV
    };
}

var svg = makerjs.exporter.toSVG(new ovalL(100, 100, 37));

document.write(svg);

The next step is to eliminate the overlap in the lines. Here are two approaches to do this:

Adjust only the x or y component of the point:

//render an L shape, modifying points by their x and y

var makerjs = require('makerjs');

function ovalL(width, height, thickness) {
    var ovalH = new makerjs.models.Oval(width, thickness);
    var ovalV = new makerjs.models.Oval(thickness, height);

    delete ovalV.paths.BottomLeft;
    delete ovalV.paths.BottomRight;
    delete ovalH.paths.TopLeft;

    //move the x of the horizontal's top
    ovalH.paths.Top.end[0] = thickness;

    //move the y of the vertical's right
    ovalV.paths.Right.origin[1] = thickness;

    this.models = {
        h: ovalH, v: ovalV
    };
}

var svg = makerjs.exporter.toSVG(new ovalL(100, 100, 37));

document.write(svg);

Share a point on both lines:

//render an L shape, sharing a point

var makerjs = require('makerjs');

function ovalL(width, height, thickness) {
    var ovalH = new makerjs.models.Oval(width, thickness);
    var ovalV = new makerjs.models.Oval(thickness, height);

    delete ovalV.paths.BottomLeft;
    delete ovalV.paths.BottomRight;
    delete ovalH.paths.TopLeft;

    //set to the same point
    ovalH.paths.Top.end =
        ovalV.paths.Right.origin =
            [thickness, thickness];

    this.models = {
        h: ovalH, v: ovalV
    };
}

var svg = makerjs.exporter.toSVG(new ovalL(100, 100, 37));

document.write(svg);

Let's progress this example further, by modifying an L shape into a C shape. Create a new model function for C, and immediately create an L within it. The C may create a new models object for itself, and nest the L inside; alternatively, C can just assume L's models object:

//render an L with an oval over it

var makerjs = require('makerjs');

function ovalL(width, height, thickness) {
    var ovalH = new makerjs.models.Oval(width, thickness);
    var ovalV = new makerjs.models.Oval(thickness, height);
    delete ovalV.paths.BottomLeft;
    delete ovalV.paths.BottomRight;
    delete ovalH.paths.TopLeft;
    ovalH.paths.Top.end =
        ovalV.paths.Right.origin =
            [thickness, thickness];
    this.models = { h: ovalH, v: ovalV };
}

function ovalC(width, height, thickness) {

    //assume the same models as L
    this.models = new ovalL(width, height, thickness).models;

    //add another oval
    this.models.h2 = new makerjs.models.Oval(width, thickness);

    //move it to the top
    this.models.h2.origin = [0, height - thickness];
}

//using C instead of L
var svg = makerjs.exporter.toSVG(new ovalC(100, 100, 37));

document.write(svg);

Just as before, we need to delete the overlapping paths using the delete keyword. Let us also make a short alias for this.models to save us some keystrokes:

//render an L and form a C

var makerjs = require('makerjs');

function ovalL(width, height, thickness) {
    var ovalH = new makerjs.models.Oval(width, thickness);
    var ovalV = new makerjs.models.Oval(thickness, height);
    delete ovalV.paths.BottomLeft;
    delete ovalV.paths.BottomRight;
    delete ovalH.paths.TopLeft;
    ovalH.paths.Top.end =
        ovalV.paths.Right.origin =
            [thickness, thickness];
    this.models = { h: ovalH, v: ovalV };
}

function ovalC(width, height, thickness) {

    //set local var m for easy typing
    var m =
        this.models =
            new ovalL(width, height, thickness).models;

    m.h2 = new makerjs.models.Oval(width, thickness);
    m.h2.origin = [0, height - thickness];

    //delete overlapping arcs again
    delete m.h2.paths.TopLeft;
    delete m.h2.paths.BottomLeft;
    delete m.v.paths.TopRight;
}

var svg = makerjs.exporter.toSVG(new ovalC(100, 100, 37));

document.write(svg);

Lastly, we need our overlapping lines to meet at a common point. Notice that the new oval h2 has a different origin the the previous ovals. So, we must originate for all of the ovals to share the same coordinate space. Afterwards, we can assign the common point to both lines.

In the Play editor, try removing the call to originate to see the results without it.

//render a C shape

var makerjs = require('makerjs');

function ovalL(width, height, thickness) {
    var ovalH = new makerjs.models.Oval(width, thickness);
    var ovalV = new makerjs.models.Oval(thickness, height);
    delete ovalV.paths.BottomLeft;
    delete ovalV.paths.BottomRight;
    delete ovalH.paths.TopLeft;
    ovalH.paths.Top.end = 
        ovalV.paths.Right.origin = 
            [thickness, thickness];
    this.models = { h: ovalH, v: ovalV };
}

function ovalC(width, height, thickness) {
    var m = 
        this.models = 
            new ovalL(width, height, thickness).models;
    m.h2 = new makerjs.models.Oval(width, thickness);
    m.h2.origin = [0, height - thickness];
    delete m.h2.paths.TopLeft;
    delete m.h2.paths.BottomLeft;
    delete m.v.paths.TopRight;
    
    //h2 has paths relative to h2 origin, 
    //we need to originate to share the point
    makerjs.model.originate(this);
    
    //share the point
    m.h2.paths.Bottom.origin = 
        m.v.paths.Right.end =
            [thickness, height - thickness];
}

var svg = makerjs.exporter.toSVG(new ovalC(100, 100, 37));

document.write(svg);
edit on GitHub

Breaking paths

You can break paths into two pieces if you have a point that lies on the path (from an intersection, for example) by using makerjs.path.breakAtPoint. This function will change the path that you pass it, so that it is broken at that point, and it will return a new path object which is the other broken piece:

//break a path in two

var makerjs = require('makerjs');

var model = {
  paths: {
    arc: new makerjs.paths.Arc([0, 0], 50, 0, 180)
  }
}

var arc2 = makerjs.path.breakAtPoint(model.paths.arc, [0, 50]);
makerjs.model.moveRelative(arc2, [-10, 0]);

model.paths.arc2 = arc2;

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

document.write(svg);

For Circle, the original path will be converted in place to an Arc, and null is returned.

edit on GitHub

Fillets

Fillets are round corners where two paths meet. Rounding a corner can add strength to your part, as well as make it faster to print. Maker.js provides two types of fillets: traditional fillets and dogbone fillets.

Traditional fillet

Using makerjs.path.fillet you can round a corner at the junction between two lines, two arcs, or a line and an arc. This function will clip the two paths that you pass it, and will return a new arc path which fits between the clipped ends. The paths must meet at one point, this is how it determines which ends of the paths to clip. You also provide a radius of the fillet. If the fillet cannot be created this function will return null.

//fillet between lines

var makerjs = require('makerjs');

var model = {
  paths: {
    line1: new makerjs.paths.Line([0, 20], [30, 10]),
    line2: new makerjs.paths.Line([10, 0], [30, 10])
  }
}

//create a fillet
var arc = makerjs.path.fillet(model.paths.line1, model.paths.line2, 2);

//add the fillet to the model
model.paths.arc = arc;

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

document.write(svg);
//fillet between arcs

var makerjs = require('makerjs');

var model = {
  paths: {
    arc1: new makerjs.paths.Arc([0, 50], 50, 270, 0),
    arc2: new makerjs.paths.Arc([100, 50], 50, 180, 270)
  }
}

//create a fillet
var arc = makerjs.path.fillet(model.paths.arc1, model.paths.arc2, 2);

//add the fillet to the model
model.paths.arc = arc;

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

document.write(svg);
//fillet between line and arc (or arc and line!)

var makerjs = require('makerjs');

var model = {
  paths: {
    arc: new makerjs.paths.Arc([0, 50], 50, 270, 0),
    line: new makerjs.paths.Line([50, 50], [50, 0])
  }
}

//create a fillet
var arc2 = makerjs.path.fillet(model.paths.arc, model.paths.line, 2);

//add the fillet to the model
model.paths.arc2 = arc2;

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

document.write(svg);

Dogbone fillet

Many CNC tools are not able to cut a sharp interior corner. The way to clear the apex of an interior corner is by encompassing the corner with a circular cut known as a dogbone fillet. Use makerjs.path.dogbone to round a corner at the junction between two lines. This function will only work for two lines which must meet at one point. It will clip the two lines that you pass it, and will return a new arc path which clears the corner where the lines meet. It will return null if a dogbone fillet cannot be created at the radius you specify.

//dogbone fillet between lines.

var makerjs = require('makerjs');

var model = {
  paths: {
    line1: new makerjs.paths.Line([0, 0], [0, 5]),
    line2: new makerjs.paths.Line([0, 5], [10, 5]),
    line3: new makerjs.paths.Line([10, 5], [10, 0])
  }
}

//create dogbone fillets
var arc1 = makerjs.path.dogbone(model.paths.line1, model.paths.line2, 1);

var arc2 = makerjs.path.dogbone(model.paths.line2, model.paths.line3, 1);

//add the fillets to the model
model.paths.arc1 = arc1;
model.paths.arc2 = arc2;

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

document.write(svg);
edit on GitHub

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, .001);

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, .001);

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.

edit on GitHub

Next: learn more in Advanced Drawing.