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:
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 chainvarmakerjs=require('makerjs');varmodel=newmakerjs.models.Rectangle(100,50);varsvg=makerjs.exporter.toSVG(model);document.write(svg);//now find the chainvarchain=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 rectanglesvarmakerjs=require('makerjs');vardrawing={models:{r1:newmakerjs.models.Rectangle(100,50),r2:makerjs.model.move(newmakerjs.models.Rectangle(100,50),[50,25])}};makerjs.model.combineUnion(drawing.models.r1,drawing.models.r2);varsvg=makerjs.exporter.toSVG(drawing);document.write(svg);//now find the chainvarchain=makerjs.model.findSingleChain(drawing);document.write('found a chain with '+chain.links.length+' links and endless='+chain.endless);
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
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.
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.
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:
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.
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 rectanglesvarmakerjs=require('makerjs');varmodel={models:{outer:makerjs.model.center(newmakerjs.models.Rectangle(60,30)),inner:makerjs.model.center(newmakerjs.models.Rectangle(45,15))}};varsvg=makerjs.exporter.toSVG(model);document.write(svg);//now find the chainsvarchains=makerjs.model.findChains(model);document.write('found '+chains.length+' chains');
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 rectanglesvarmakerjs=require('makerjs');varmodel={models:{outer:makerjs.model.center(newmakerjs.models.Rectangle(60,30)),inner:makerjs.model.center(newmakerjs.models.Rectangle(45,15))}};varsvg=makerjs.exporter.toSVG(model);document.write(svg);//now find the contained chains with alternating directionvarchains=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 layersvarmakerjs=require('makerjs');varc1=newmakerjs.paths.Circle(1);varc2=newmakerjs.paths.Circle(1);c2.origin=[3,0];c1.layer='red';c2.layer='blue';varmodel={paths:{c1:c1,c2:c2}};varsvg=makerjs.exporter.toSVG(model);document.write(svg);//now find the chains by layervarchains=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');
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 pathsvarm=require('makerjs');functionexample(origin){this.models={rect:newm.models.Rectangle(100,50),oval:m.model.move(newm.models.Oval(100,50),[50,25])};this.origin=origin;}varx=newexample();m.model.combineUnion(x.models.rect,x.models.oval);x.paths={line1:newm.paths.Line([150,10],[220,10]),line2:newm.paths.Line([220,50],[220,10]),line3:newm.paths.Line([220,75],[260,35]),circle:newm.paths.Circle([185,50],15)};varsvg=m.exporter.toSVG(x);document.write(svg);//find chains and output the resultsm.model.findChains(x,function(chains,loose,layer){document.write('found '+chains.length+' chain(s) and '+loose.length+' loose path(s) on layer '+layer);});
//convert a round rectangle to key pointsvarmakerjs=require('makerjs');varrect=newmakerjs.models.RoundRectangle(100,50,10);varchain=makerjs.model.findSingleChain(rect);varkeyPoints=makerjs.chain.toKeyPoints(chain,5);varmodel={models:{rect:rect,dots:newmakerjs.models.Holes(1,keyPoints)}};varsvg=makerjs.exporter.toSVG(model);document.write(svg);
//convert a round rectangle to pointsvarmakerjs=require('makerjs');varrect=newmakerjs.models.RoundRectangle(100,50,10);varchain=makerjs.model.findSingleChain(rect);varspacing=10;varkeyPoints=makerjs.chain.toPoints(chain,spacing);varmodel={models:{rect:rect,dots:newmakerjs.models.Holes(1,keyPoints)}};varsvg=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 pointsvarmakerjs=require('makerjs');varrect=newmakerjs.models.RoundRectangle(100,50,10);varchain=makerjs.model.findSingleChain(rect);varminimumSpacing=10;vardivisions=Math.floor(chain.pathLength/minimumSpacing);varspacing=chain.pathLength/divisions;console.log(spacing);varkeyPoints=makerjs.chain.toPoints(chain,spacing);varmodel={models:{rect:rect,dots:newmakerjs.models.Holes(1,keyPoints)}};varsvg=makerjs.exporter.toSVG(model);document.write(svg);
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 chainvarmakerjs=require('makerjs');varmodel={paths:{"0":newmakerjs.paths.Line([0,0],[100,0]),"1":newmakerjs.paths.Line([100,0],[100,100]),"2":newmakerjs.paths.Line([100,100],[200,100])}};varsvg=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 chainvarmakerjs=require('makerjs');varmodel={paths:{"0":newmakerjs.paths.Line([0,0],[100,0]),"1":newmakerjs.paths.Line([100,0],[100,100]),"2":newmakerjs.paths.Line([100,100],[200,100])},//create a placeholder in the tree for more modelsmodels:{}};//find the chainvarchain=makerjs.model.findSingleChain(model);//add fillets to the chainvarfilletsModel=makerjs.chain.fillet(chain,10);//put the fillets in the treemodel.models.fillets=filletsModel;varsvg=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 wireframevarm=require('makerjs');functiontrussWireframe(w,h){this.models={frame:newm.models.ConnectTheDots(true,[[0,h],[w,0],[0,0]])};varangled=this.models.frame.paths.ShapeLine1;varbracepoints=[[0,0],m.point.middle(angled,1/3),[w/2,0],m.point.middle(angled,2/3)];this.models.brace=newm.models.ConnectTheDots(false,bracepoints);}vartruss=newtrussWireframe(200,50);varexpansion=m.model.expandPaths(truss,3,1);//call originate before calling simplify:m.model.originate(expansion);m.model.simplify(expansion);varsvg=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 trussvarm=require('makerjs');functiontrussWireframe(w,h){this.models={frame:newm.models.ConnectTheDots(true,[[0,h],[w,0],[0,0]])};varangled=this.models.frame.paths.ShapeLine1;varbracepoints=[[0,0],m.point.middle(angled,1/3),[w/2,0],m.point.middle(angled,2/3)];this.models.brace=newm.models.ConnectTheDots(false,bracepoints);}vartruss=newtrussWireframe(200,50);varexpansion=m.model.expandPaths(truss,3,1);m.model.originate(expansion);m.model.simplify(expansion);//find chainsvarchains=m.model.findChains(expansion);//start at 1 - ignore the longest chain which is the perimeterfor(vari=1;i<chains.length;i++){//save the fillets in the model tree expansion.models['fillets'+i]=m.chain.fillet(chains[i],2);}varsvg=m.exporter.toSVG(expansion);document.write(svg);
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 squarevarmakerjs=require('makerjs');varplus=makerjs.model.combineUnion(makerjs.model.center(newmakerjs.models.Rectangle(50,100)),makerjs.model.center(newmakerjs.models.Rectangle(100,50)));varplus2=makerjs.cloneObject(plus);plus2.origin=[150,0];varouter=makerjs.model.center(newmakerjs.models.Rectangle(150,150));varmodel={models:{plus,plus2,outer}//using Shorthand property names :)}varsvg=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 squarevarmakerjs=require('makerjs');varplus1=makerjs.model.combineUnion(makerjs.model.center(newmakerjs.models.Rectangle(50,100)),makerjs.model.center(newmakerjs.models.Rectangle(100,50)));varplus2=makerjs.cloneObject(plus1);plus2.origin=[150,0];varsquare=makerjs.model.center(newmakerjs.models.Square(150));//find chains for each plusvarchain1=makerjs.model.findSingleChain(plus1);varchain2=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 turnsvardogbones1=makerjs.chain.dogbone(chain1,{left:5});vardogbones2=makerjs.chain.dogbone(chain2,{right:5});varmodel={models:{plus1,plus2,square,dogbones1,dogbones2}}varsvg=makerjs.exporter.toSVG(model);document.write(svg);
//render a row of squares on a chainvarmakerjs=require('makerjs');varsquare=newmakerjs.models.Square(5);varrow=makerjs.layout.cloneToRow(square,10,10);varcurve=newmakerjs.models.BezierCurve([0,0],[33,25],[66,-25],[100,0]);varchain=makerjs.model.findSingleChain(curve);makerjs.layout.childrenOnChain(row,chain,0.5,false,true);varmodel={models:{curve:curve,row:row}};curve.layer="red";varsvg=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.
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