├── .gitignore ├── index.js ├── test ├── .DS_Store ├── test.path.js ├── support │ ├── testdriver.js │ └── subsetcanvas.js ├── test.utils.js ├── test.gcode.js └── test.gcanvas.js ├── examples ├── line.js ├── screeny.png ├── example-irl.jpg ├── retractandtop.png ├── lathe-mill-attacks.png ├── rect.js ├── text.js ├── bezier.js ├── quadratic.js ├── clipping.js ├── arc.js ├── filling.js ├── winding.js ├── support │ ├── tomorrow.min.css │ └── highlight.min.js ├── portal2.js └── index.html ├── Makefile ├── lib ├── drivers │ ├── null.js │ └── gcode.js ├── parsefont.js ├── utils.js ├── math │ ├── point.js │ └── matrix.js ├── font.js ├── motion.js ├── subpath.js ├── path.js ├── gcanvas.js └── fonts │ └── helvetiker_regular.typeface.js ├── package.json ├── bin └── gcanvas └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | components 2 | node_modules 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/gcanvas') 2 | -------------------------------------------------------------------------------- /test/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/em/gcanvas/master/test/.DS_Store -------------------------------------------------------------------------------- /examples/line.js: -------------------------------------------------------------------------------- 1 | ctx.moveTo(0,0); 2 | ctx.lineTo(100,100); 3 | ctx.stroke(); 4 | -------------------------------------------------------------------------------- /examples/screeny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/em/gcanvas/master/examples/screeny.png -------------------------------------------------------------------------------- /examples/example-irl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/em/gcanvas/master/examples/example-irl.jpg -------------------------------------------------------------------------------- /examples/retractandtop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/em/gcanvas/master/examples/retractandtop.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @mocha -R list 3 | 4 | clean: 5 | rm -fr build components 6 | 7 | .PHONY: clean test 8 | -------------------------------------------------------------------------------- /examples/lathe-mill-attacks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/em/gcanvas/master/examples/lathe-mill-attacks.png -------------------------------------------------------------------------------- /examples/rect.js: -------------------------------------------------------------------------------- 1 | function main(ctx) { 2 | ctx.rect(0,0,100,100); 3 | ctx.rect(30,0,10,10); 4 | ctx.stroke(); 5 | } 6 | 7 | if(this.example) this.example('filling', main); 8 | -------------------------------------------------------------------------------- /examples/text.js: -------------------------------------------------------------------------------- 1 | function main(ctx) { 2 | ctx.toolDiameter = 5; 3 | ctx.font = "140pt Helvetica"; 4 | ctx.fillText(":)", 20, 150); 5 | } 6 | 7 | if(this.example) this.example('text', main); 8 | -------------------------------------------------------------------------------- /examples/bezier.js: -------------------------------------------------------------------------------- 1 | function main(ctx) { 2 | ctx.moveTo(20,20); 3 | ctx.bezierCurveTo(20,100,180,100,180,20); 4 | ctx.stroke(); 5 | } 6 | 7 | if(this.example) this.example('bezier curves', main); 8 | -------------------------------------------------------------------------------- /examples/quadratic.js: -------------------------------------------------------------------------------- 1 | function main(ctx) { 2 | ctx.moveTo(20,20); 3 | ctx.quadraticCurveTo(20,100,180,20); 4 | ctx.stroke(); 5 | } 6 | 7 | if(this.example) this.example('quadratic curves', main); 8 | -------------------------------------------------------------------------------- /examples/clipping.js: -------------------------------------------------------------------------------- 1 | function main(ctx) { 2 | ctx.toolDiameter = 10; 3 | ctx.rect(20, 20, 160, 160); 4 | ctx.arc(10, 10, 100, 0, Math.PI*2); 5 | ctx.clip(); 6 | 7 | ctx.beginPath(); 8 | ctx.arc(100, 100, 90, 0, Math.PI*2); 9 | ctx.arc(150, 150, 50, 0, Math.PI*2); 10 | ctx.fill(); 11 | } 12 | 13 | if(this.example) this.example('clipping', main); 14 | -------------------------------------------------------------------------------- /test/test.path.js: -------------------------------------------------------------------------------- 1 | describe('Path', function() { 2 | var expect = require('chai').expect 3 | , Path = require('../lib/path'); 4 | 5 | // it('#getLength', function() { 6 | // var path = new Path(); 7 | // path.moveTo(20,20); 8 | // path.lineTo(100,100); 9 | // path.lineTo(100,110); 10 | 11 | // expect(path.getLength()).closeTo(123,1); 12 | // }); 13 | 14 | }); 15 | -------------------------------------------------------------------------------- /examples/arc.js: -------------------------------------------------------------------------------- 1 | function main(ctx) { 2 | ctx.feed = 1000; 3 | ctx.arc(100, 100, 80, 0, Math.PI * 1.5); 4 | ctx.arc(100, 100, 60, 0, Math.PI * 1.5, true); 5 | ctx.arc(100, 100, 40, 0, Math.PI * 1.5, true); 6 | ctx.stroke(); 7 | 8 | // Isolated circle with beginPath 9 | ctx.beginPath(); 10 | ctx.arc(100, 100, 20, 0, Math.PI * 2); 11 | ctx.stroke(); 12 | } 13 | 14 | if(this.example) this.example('arcs', main); 15 | -------------------------------------------------------------------------------- /examples/filling.js: -------------------------------------------------------------------------------- 1 | function main(ctx) { 2 | star(ctx, 100, 100, 90, 5, 0.5); 3 | 4 | function star(ctx, x, y, r, p, m) { 5 | ctx.translate(x, y); 6 | ctx.moveTo(0,0-r); 7 | for (var i = 0; i < p; i++) { 8 | ctx.rotate(Math.PI / p); 9 | ctx.lineTo(0, 0 - (r*m)); 10 | ctx.rotate(Math.PI / p); 11 | ctx.lineTo(0, 0 - r); 12 | } 13 | ctx.fill(); 14 | } 15 | } 16 | 17 | if(this.example) this.example('filling', main); 18 | -------------------------------------------------------------------------------- /lib/drivers/null.js: -------------------------------------------------------------------------------- 1 | module.exports = NullDriver; 2 | 3 | function NullDriver() { 4 | } 5 | 6 | NullDriver.prototype = { 7 | send: function() { 8 | } 9 | , init: function() { 10 | } 11 | , speed: function() { 12 | } 13 | , feed: function() { 14 | } 15 | , coolant: function() { 16 | } 17 | , zero: function() { 18 | } 19 | , atc: function() { 20 | } 21 | , rapid: function() { 22 | } 23 | , linear: function() { 24 | } 25 | , arcCW: function() { 26 | } 27 | , arcCCW: function() { 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /examples/winding.js: -------------------------------------------------------------------------------- 1 | function main(ctx) { 2 | ctx.toolDiameter = 10; 3 | ctx.arc(100, 100, 90, 0, Math.PI*2, true); 4 | star(ctx, 100, 100, 40, 5, 0.5); 5 | // ctx.arc(100, 130, 30, 0, Math.PI*2); 6 | ctx.fill('nonzero'); 7 | } 8 | 9 | function star(ctx, x, y, r, p, m) 10 | { 11 | ctx.save(); 12 | ctx.translate(x, y); 13 | ctx.moveTo(0,0-r); 14 | for (var i = 0; i < p; i++) 15 | { 16 | ctx.rotate(Math.PI / p); 17 | ctx.lineTo(0, 0 - (r*m)); 18 | ctx.rotate(Math.PI / p); 19 | ctx.lineTo(0, 0 - r); 20 | } 21 | ctx.restore(); 22 | } 23 | 24 | 25 | if(this.example) example('winding', main); 26 | -------------------------------------------------------------------------------- /test/support/testdriver.js: -------------------------------------------------------------------------------- 1 | module.exports = TestDriver; 2 | 3 | 4 | function TestDriver() { 5 | this.result = []; 6 | }; 7 | 8 | [ 9 | 'rapid' 10 | , 'linear' 11 | , 'arcCW' 12 | , 'arcCCW' 13 | , 'speed' 14 | , 'feed' 15 | , 'coolant' 16 | , 'send' 17 | ].forEach(function(name) { 18 | TestDriver.prototype[name] = function(params) { 19 | var id = name; 20 | if(typeof params === 'object') { 21 | Object.keys(params).sort().forEach(function(k) { 22 | id += ' ' + k + Number(params[k]).toFixed() 23 | }); 24 | } 25 | else { 26 | id = name + ' ' + params; 27 | } 28 | 29 | this.result.push(id); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gcanvas", 3 | "version": "0.0.37", 4 | "description": "A Canvas API implementation that generates Gcode", 5 | "keywords": [ 6 | "canvas", 7 | "cnc", 8 | "mill", 9 | "gcode" 10 | ], 11 | "scripts": { 12 | "test": "mocha" 13 | }, 14 | "bin": { 15 | "gcanvas": "./bin/gcanvas" 16 | }, 17 | "dependencies": { 18 | "commander": "2.0.0" 19 | }, 20 | "devDependencies": { 21 | "component": "*", 22 | "mocha": "*", 23 | "chai": "*" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/em/gcanvas.git" 28 | }, 29 | "author": "Emery Denuccio", 30 | "license": "MIT" 31 | } 32 | -------------------------------------------------------------------------------- /test/support/subsetcanvas.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This driver passes straight through to another canvas, 3 | * but can disable the presence of certain methods. 4 | * This allows us to test the failover routines with image fixtures. 5 | **/ 6 | 7 | module.exports = TestDriver; 8 | 9 | var Canvas = require('canvas'); 10 | 11 | function TestDriver(resultCanvas) { 12 | GCodeDriver.apply(this, arguments); 13 | this.result = resultCanvas || new Canvas(); 14 | 15 | // Pass all methods through 16 | for(var k in result) { 17 | if(typeof result[k] === 'function') { 18 | this[k] = function() { 19 | this.result[k].apply(this.result, arguments); 20 | } 21 | } 22 | } 23 | }; 24 | 25 | TestDriver.prototype = { 26 | matchesFixture: function(name) { 27 | fs.readFile(__dirname + '../fixtures/'+name+'.png', function(err, file){ 28 | if (err) throw err; 29 | img = new Image; 30 | img.src = squid; 31 | ctx.drawImage(img, 0, 0, img.width, img.height); 32 | }); 33 | } 34 | }; 35 | 36 | TestDriver.prototype.reset = function() { 37 | this.lines = []; 38 | } 39 | -------------------------------------------------------------------------------- /examples/support/tomorrow.min.css: -------------------------------------------------------------------------------- 1 | .tomorrow-comment,pre .comment,pre .title{color:#8e908c}.tomorrow-red,pre .variable,pre .attribute,pre .tag,pre .regexp,pre .ruby .constant,pre .xml .tag .title,pre .xml .pi,pre .xml .doctype,pre .html .doctype,pre .css .id,pre .css .class,pre .css .pseudo{color:#c82829}.tomorrow-orange,pre .number,pre .preprocessor,pre .built_in,pre .literal,pre .params,pre .constant{color:#f5871f}.tomorrow-yellow,pre .class,pre .ruby .class .title,pre .css .rules .attribute{color:#eab700}.tomorrow-green,pre .string,pre .value,pre .inheritance,pre .header,pre .ruby .symbol,pre .xml .cdata{color:#718c00}.tomorrow-aqua,pre .css .hexcolor{color:#3e999f}.tomorrow-blue,pre .function,pre .python .decorator,pre .python .title,pre .ruby .function .title,pre .ruby .title .keyword,pre .perl .sub,pre .javascript .title,pre .coffeescript .title{color:#4271ae}.tomorrow-purple,pre .keyword,pre .javascript .function{color:#8959a8}pre code{display:block;background:white;color:#4d4d4c;padding:.5em}pre .coffeescript .javascript,pre .javascript .xml,pre .tex .formula,pre .xml .javascript,pre .xml .vbscript,pre .xml .css,pre .xml .cdata{opacity:.5} -------------------------------------------------------------------------------- /lib/parsefont.js: -------------------------------------------------------------------------------- 1 | module.export = parseFont; 2 | 3 | // stolen from node-canvas 4 | 5 | /** 6 | * Text baselines. 7 | */ 8 | 9 | var baselines = ['alphabetic', 'top', 'bottom', 'middle', 'ideographic', 'hanging']; 10 | 11 | /** 12 | * Font RegExp helpers. 13 | */ 14 | 15 | var weights = 'normal|bold|bolder|lighter|[1-9]00' 16 | , styles = 'normal|italic|oblique' 17 | , units = 'px|pt|pc|in|cm|mm|%' 18 | , string = '\'([^\']+)\'|"([^"]+)"|[\\w-]+'; 19 | /** 20 | * Font parser RegExp; 21 | */ 22 | 23 | var fontre = new RegExp('^ *' 24 | + '(?:(' + weights + ') *)?' 25 | + '(?:(' + styles + ') *)?' 26 | + '([\\d\\.]+)(' + units + ') *' 27 | + '((?:' + string + ')( *, *(?:' + string + '))*)' 28 | ); 29 | 30 | /** 31 | * Parse font `str`. 32 | * 33 | * @param {String} str 34 | * @return {Object} 35 | * @api private 36 | */ 37 | 38 | var parseFont = module.exports = function(str){ 39 | var font = {} 40 | , captures = fontre.exec(str); 41 | 42 | // Invalid 43 | if (!captures) return; 44 | 45 | // // Cached 46 | // if (cache[str]) return cache[str]; 47 | 48 | // Populate font object 49 | font.weight = captures[1] || 'normal'; 50 | font.style = captures[2] || 'normal'; 51 | font.size = parseFloat(captures[3]); 52 | font.unit = captures[4]; 53 | font.family = captures[5].replace(/["']/g, '').split(',')[0]; 54 | 55 | switch (font.unit) { 56 | case 'pt': 57 | font.size / 72; 58 | break; 59 | case 'in': 60 | font.size *= 96; 61 | break; 62 | case 'mm': 63 | font.size *= 96.0 / 25.4; 64 | break; 65 | case 'cm': 66 | font.size *= 96.0 / 2.54; 67 | break; 68 | } 69 | 70 | return font; 71 | }; 72 | -------------------------------------------------------------------------------- /test/test.utils.js: -------------------------------------------------------------------------------- 1 | describe('utils', function() { 2 | var expect = require('chai').expect 3 | , utils = require('../lib/utils'); 4 | 5 | it('#arcToPoints', function() { 6 | var result = utils.arcToPoints( 7 | 5, 5, // x,y 8 | 0, // start angle 9 | Math.PI, // 180 degrees 10 | 10 // radius 11 | ); 12 | 13 | expect(result.start.x).closeTo(15, 0.000001); 14 | expect(result.start.y).closeTo(5, 0.000001); 15 | expect(result.end.x).closeTo(-5, 0.000001); 16 | expect(result.end.y).closeTo(5, 0.000001); 17 | }); 18 | 19 | it('#pointsToArc', function() { 20 | var result = utils.pointsToArc( 21 | {x: 5, y: 5}, 22 | {x: 15, y: 5}, 23 | {x: -5, y: 5} 24 | ); 25 | 26 | expect(result.start).closeTo(0, 0.000001); 27 | expect(result.end).closeTo(Math.PI, 0.000001); 28 | expect(result.radius).closeTo(10, 0.000001); 29 | }); 30 | 31 | describe('#normalizeAngle', function() { 32 | it('converts 370 degrees to 10 degrees', function() { 33 | var input = Math.PI / 180 * 370; 34 | var output = Math.PI / 180 * 10; 35 | expect(utils.normalizeAngle(input)).closeTo(output, 0.00001); 36 | }); 37 | 38 | it('converts -370 degrees to -10 degrees', function() { 39 | var input = Math.PI / 180 * -370; 40 | var output = Math.PI / 180 * -10; 41 | expect(utils.normalizeAngle(input)).closeTo(output, 0.00001); 42 | }); 43 | 44 | it('converts -370 degrees to 350 degrees', function() { 45 | var input = Math.PI / 180 * -370; 46 | var output = Math.PI / 180 * -10; 47 | expect(utils.normalizeAngle(input)).closeTo(output, 0.00001); 48 | }); 49 | }); 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /test/test.gcode.js: -------------------------------------------------------------------------------- 1 | describe('GcodeDriver', function() { 2 | var expect = require('chai').expect 3 | , GCodeDriver = require('../lib/drivers/gcode'); 4 | 5 | var driver, result; 6 | beforeEach(function() { 7 | result = []; 8 | driver = new GCodeDriver({write: function(str) { 9 | result.push(str); 10 | }}); 11 | }); 12 | 13 | it('#rapid', function() { 14 | driver.rapid({x:10,y:10}); 15 | expect(result).eql(['G0 X10 Y10']); 16 | }); 17 | 18 | it('#linear', function() { 19 | driver.linear({x:10,y:10}); 20 | expect(result).eql(['G1 X10 Y10']); 21 | }); 22 | 23 | it('#arcCW', function() { 24 | driver.arcCW({x:0,y:10,i:10, j:5}); 25 | expect(result).eql(['G2 X0 Y10 I10 J5']); 26 | }); 27 | 28 | it('#arcCCW', function() { 29 | driver.arcCCW({x:0,y:10,i:10, j:5}); 30 | expect(result).eql(['G3 X0 Y10 I10 J5']); 31 | }); 32 | 33 | it('#speed', function() { 34 | driver.speed(100); 35 | expect(result).eql(['S100']); 36 | }); 37 | 38 | it('#feed', function() { 39 | driver.feed(100); 40 | expect(result).eql(['F100']); 41 | }); 42 | 43 | it('#meta', function() { 44 | driver.meta({hello:'world'}); 45 | expect(result).eql(['(hello=world)']); 46 | }); 47 | 48 | describe('#coolant', function() { 49 | it('uses M07 if "mist"', function() { 50 | driver.coolant("mist"); 51 | expect(result).eql(['M07']); 52 | }) 53 | 54 | it('uses M08 if true or "flood"', function() { 55 | driver.coolant(true); 56 | expect(result).eql(['M08']); 57 | }) 58 | 59 | it('sends M09 if false or "off"', function() { 60 | driver.coolant(false); 61 | expect(result).eql(['M09']); 62 | }) 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /lib/drivers/gcode.js: -------------------------------------------------------------------------------- 1 | module.exports = GCodeDriver; 2 | 3 | function GCodeDriver(stream) { 4 | this.stream = stream || { 5 | write: function(str) { 6 | console.log(str); 7 | } 8 | }; 9 | } 10 | 11 | GCodeDriver.prototype = { 12 | send: function(code, params) { 13 | var command = code; 14 | 15 | if(!command) return; 16 | 17 | if(params) { 18 | 'xyzabcijkf'.split('').forEach(function(k) { 19 | if(params[k] === undefined || params[k] === null) 20 | return; 21 | 22 | command += ' ' + k.toUpperCase() + params[k]; 23 | }); 24 | } 25 | 26 | this.stream.write(command); 27 | } 28 | , init: function() { 29 | this.send('G90'); // Absolute mode 30 | } 31 | , unit: function(name) { 32 | name = name.toLowerCase(); 33 | this.send({'inch':'G20','mm':'G21'}[name]); 34 | } 35 | , speed: function(n) { 36 | this.send('S'+n); 37 | } 38 | , feed: function(n) { 39 | this.send('F'+n); 40 | } 41 | , coolant: function(type) { 42 | if(type === 'mist') { 43 | // special 44 | this.send('M07'); 45 | } 46 | else if(type) { 47 | // flood 48 | this.send('M08'); 49 | } 50 | else { 51 | // off 52 | this.send('M09'); 53 | } 54 | } 55 | , zero: function(params) { 56 | this.send('G28.3', params); 57 | } 58 | , atc: function(id) { 59 | this.send('M6', {T: id}); 60 | } 61 | , rapid: function(params) { 62 | this.send('G0', params); 63 | } 64 | , linear: function(params) { 65 | this.send('G1', params); 66 | } 67 | , arcCW: function(params) { 68 | this.send('G2', params); 69 | } 70 | , arcCCW: function(params) { 71 | this.send('G3', params); 72 | } 73 | , meta: function(params) { 74 | var comment = '('; 75 | for(var k in params) { 76 | comment += k+'='+params[k]; 77 | } 78 | comment += ')'; 79 | 80 | this.send(comment); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /examples/portal2.js: -------------------------------------------------------------------------------- 1 | function main(ctx) { 2 | ctx.depth = 100; 3 | ctx.toolDiameter = 1/4*25.4; 4 | portal2(ctx); 5 | 6 | // Good example of a bunch of different canvas features. 7 | // http://www.codeproject.com/Articles/237065/Introduction-to-HTML5-Canvas-Part-2-Example 8 | function portal2(ctx) { 9 | //function to convert deg to radian 10 | var acDegToRad = function(deg){ 11 | return deg* (-(Math.PI / 180.0)); 12 | } 13 | 14 | //save the initial state of the context 15 | ctx.save(); 16 | //set fill color to gray 17 | ctx.fillStyle = "rgb(110,110,110)"; 18 | //save the current state with fillcolor 19 | ctx.save(); 20 | 21 | //draw 2's base rectangle 22 | ctx.fillRect(20,200,120,35); 23 | //bring origin to 2's base 24 | ctx.translate(20,200); 25 | //rotate the canvas 35 deg anti-clockwise 26 | ctx.rotate(acDegToRad(35)); 27 | //draw 2's slant rectangle 28 | ctx.fillRect(0,0,100,35); 29 | //restore the canvas to reset transforms 30 | ctx.restore(); 31 | //set stroke color width and draw the 2's top semi circle 32 | ctx.strokeStyle = "rgb(110,110,110)"; 33 | ctx.lineWidth = 35; 34 | ctx.beginPath(); 35 | ctx.arc(77,135,40,acDegToRad(-40),acDegToRad(180),true); 36 | ctx.stroke(); 37 | 38 | //reset canvas transforms 39 | ctx.restore(); 40 | 41 | //change color to blue 42 | ctx.fillStyle = "rgb(0,160,212)"; 43 | //save current state of canvas 44 | ctx.save(); 45 | //draw long dividing rectangle 46 | ctx.fillRect(162,20,8,300); 47 | //draw player head circle 48 | ctx.beginPath(); 49 | ctx.arc(225,80,35,acDegToRad(0),acDegToRad(360)); 50 | ctx.fill(); 51 | 52 | //start new path for tummy :) 53 | ctx.beginPath(); 54 | ctx.moveTo(170,90); 55 | ctx.lineTo(230,140); 56 | ctx.lineTo(170,210); 57 | ctx.fill(); 58 | 59 | //start new path for hand 60 | //set lineCap and lineJoin to "round", blue color 61 | //for stroke, and width of 25px 62 | ctx.lineWidth = 25; 63 | ctx.lineCap = "round"; 64 | ctx.strokeStyle = "rgb(0,160,212)"; 65 | ctx.lineJoin = "round"; 66 | ctx.beginPath(); 67 | ctx.moveTo(222,150); 68 | ctx.lineTo(230,190); 69 | ctx.lineTo(270,220); 70 | ctx.stroke(); 71 | 72 | ctx.beginPath(); 73 | ctx.moveTo(170, 200); 74 | ctx.lineTo(250, 260); 75 | ctx.lineTo(170,320); 76 | ctx.clip(); 77 | 78 | //begin new path for drawing leg 79 | ctx.beginPath(); 80 | ctx.moveTo(160,210); 81 | ctx.lineTo(195,260); 82 | ctx.lineTo(160,290); 83 | ctx.stroke(); 84 | 85 | //restore the context 86 | //back to its initial state 87 | ctx.restore(); 88 | // body... 89 | } 90 | } 91 | 92 | if(this.example) this.example('portal2', main); 93 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 86 | 87 |
7 | * _ _ 8 | * | a c tx | 9 | * | b d ty | 10 | * |_0 0 1 _| 11 | *12 | * Creates a matrix for 2d affine transformations. 13 | * 14 | * concat, inverse, rotate, scale and translate return new matrices with the 15 | * transformations applied. The matrix is not modified in place. 16 | * 17 | * Returns the identity matrix when called with no arguments. 18 | * @name Matrix 19 | * @param {Number} [a] 20 | * @param {Number} [b] 21 | * @param {Number} [c] 22 | * @param {Number} [d] 23 | * @param {Number} [tx] 24 | * @param {Number} [ty] 25 | * @constructor 26 | */ 27 | function Matrix(a, b, c, d, tx, ty) { 28 | this.a = a !== undefined ? a : 1; 29 | this.b = b || 0; 30 | this.c = c || 0; 31 | this.d = d !== undefined ? d : 1; 32 | this.tx = tx || 0; 33 | this.ty = ty || 0; 34 | } 35 | 36 | Matrix.prototype = { 37 | 38 | clone: function() { 39 | return new Matrix( 40 | this.a, 41 | this.b, 42 | this.c, 43 | this.d, 44 | this.tx, 45 | this.ty 46 | ); 47 | }, 48 | 49 | /** 50 | * Returns the result of this matrix multiplied by another matrix 51 | * combining the geometric effects of the two. In mathematical terms, 52 | * concatenating two matrixes is the same as combining them using matrix multiplication. 53 | * If this matrix is A and the matrix passed in is B, the resulting matrix is A x B 54 | * http://mathworld.wolfram.com/MatrixMultiplication.html 55 | * @name concat 56 | * @methodOf Matrix# 57 | * 58 | * @param {Matrix} matrix The matrix to multiply this matrix by. 59 | * @returns The result of the matrix multiplication, a new matrix. 60 | * @type Matrix 61 | */ 62 | concat: function(matrix) { 63 | return new Matrix( 64 | this.a * matrix.a + this.c * matrix.b, 65 | this.b * matrix.a + this.d * matrix.b, 66 | this.a * matrix.c + this.c * matrix.d, 67 | this.b * matrix.c + this.d * matrix.d, 68 | this.a * matrix.tx + this.c * matrix.ty + this.tx, 69 | this.b * matrix.tx + this.d * matrix.ty + this.ty 70 | ); 71 | }, 72 | 73 | /** 74 | * Given a point in the pretransform coordinate space, returns the coordinates of 75 | * that point after the transformation occurs. Unlike the standard transformation 76 | * applied using the transformnew Point() method, the deltaTransformnew Point() method's 77 | * transformation does not consider the translation parameters tx and ty. 78 | * @name deltaTransformPoint 79 | * @methodOf Matrix# 80 | * @see #transformPoint 81 | * 82 | * @return A new point transformed by this matrix ignoring tx and ty. 83 | * @type Point 84 | */ 85 | deltaTransformPoint: function(point) { 86 | return new Point( 87 | this.a * point.x + this.c * point.y, 88 | this.b * point.x + this.d * point.y 89 | ); 90 | }, 91 | 92 | /** 93 | * Returns the inverse of the matrix. 94 | * http://mathworld.wolfram.com/MatrixInverse.html 95 | * @name inverse 96 | * @methodOf Matrix# 97 | * 98 | * @returns A new matrix that is the inverse of this matrix. 99 | * @type Matrix 100 | */ 101 | inverse: function() { 102 | var determinant = this.a * this.d - this.b * this.c; 103 | return new Matrix( 104 | this.d / determinant, 105 | -this.b / determinant, 106 | -this.c / determinant, 107 | this.a / determinant, 108 | (this.c * this.ty - this.d * this.tx) / determinant, 109 | (this.b * this.tx - this.a * this.ty) / determinant 110 | ); 111 | }, 112 | 113 | /** 114 | * Returns a new matrix that corresponds this matrix multiplied by a 115 | * a rotation matrix. 116 | * @name rotate 117 | * @methodOf Matrix# 118 | * @see Matrix.rotation 119 | * 120 | * @param {Number} theta Amount to rotate in radians. 121 | * @param {Point} [aboutPoint] The point about which this rotation occurs. Defaults to (0,0). 122 | * @returns A new matrix, rotated by the specified amount. 123 | * @type Matrix 124 | */ 125 | rotate: function(theta, aboutPoint) { 126 | return this.concat(Matrix.rotation(theta, aboutPoint)); 127 | }, 128 | 129 | /** 130 | * Returns a new matrix that corresponds this matrix multiplied by a 131 | * a scaling matrix. 132 | * @name scale 133 | * @methodOf Matrix# 134 | * @see Matrix.scale 135 | * 136 | * @param {Number} sx 137 | * @param {Number} [sy] 138 | * @param {Point} [aboutPoint] The point that remains fixed during the scaling 139 | * @type Matrix 140 | */ 141 | scale: function(sx, sy, aboutPoint) { 142 | return this.concat(Matrix.scale(sx, sy, aboutPoint)); 143 | }, 144 | 145 | /** 146 | * Returns the result of applying the geometric transformation represented by the 147 | * Matrix object to the specified point. 148 | * @name transformPoint 149 | * @methodOf Matrix# 150 | * @see #deltaTransformPoint 151 | * 152 | * @returns A new point with the transformation applied. 153 | * @type Point 154 | */ 155 | transformPoint: function(point) { 156 | return new Point( 157 | this.a * point.x + this.c * point.y + this.tx, 158 | this.b * point.x + this.d * point.y + this.ty 159 | ); 160 | }, 161 | 162 | /** 163 | * Translates the matrix along the x and y axes, as specified by the tx and ty parameters. 164 | * @name translate 165 | * @methodOf Matrix# 166 | * @see Matrix.translation 167 | * 168 | * @param {Number} tx The translation along the x axis. 169 | * @param {Number} ty The translation along the y axis. 170 | * @returns A new matrix with the translation applied. 171 | * @type Matrix 172 | */ 173 | translate: function(tx, ty) { 174 | return this.concat(Matrix.translation(tx, ty)); 175 | } 176 | }; 177 | 178 | /** 179 | * Creates a matrix transformation that corresponds to the given rotation, 180 | * around (0,0) or the specified point. 181 | * @see Matrix#rotate 182 | * 183 | * @param {Number} theta Rotation in radians. 184 | * @param {Point} [aboutPoint] The point about which this rotation occurs. Defaults to (0,0). 185 | * @returns 186 | * @type Matrix 187 | */ 188 | Matrix.rotation = function(theta, aboutPoint) { 189 | var rotationMatrix = new Matrix( 190 | Math.cos(theta), 191 | Math.sin(theta), 192 | -Math.sin(theta), 193 | Math.cos(theta) 194 | ); 195 | 196 | if(aboutPoint) { 197 | rotationMatrix = 198 | Matrix.translation(aboutPoint.x, aboutPoint.y).concat( 199 | rotationMatrix 200 | ).concat( 201 | Matrix.translation(-aboutPoint.x, -aboutPoint.y) 202 | ); 203 | } 204 | 205 | return rotationMatrix; 206 | }; 207 | 208 | /** 209 | * Returns a matrix that corresponds to scaling by factors of sx, sy along 210 | * the x and y axis respectively. 211 | * If only one parameter is given the matrix is scaled uniformly along both axis. 212 | * If the optional aboutPoint parameter is given the scaling takes place 213 | * about the given point. 214 | * @see Matrix#scale 215 | * 216 | * @param {Number} sx The amount to scale by along the x axis or uniformly if no sy is given. 217 | * @param {Number} [sy] The amount to scale by along the y axis. 218 | * @param {Point} [aboutPoint] The point about which the scaling occurs. Defaults to (0,0). 219 | * @returns A matrix transformation representing scaling by sx and sy. 220 | * @type Matrix 221 | */ 222 | Matrix.scale = function(sx, sy, aboutPoint) { 223 | sy = sy || sx; 224 | 225 | var scaleMatrix = new Matrix(sx, 0, 0, sy); 226 | 227 | if(aboutPoint) { 228 | scaleMatrix = 229 | Matrix.translation(aboutPoint.x, aboutPoint.y).concat( 230 | scaleMatrix 231 | ).concat( 232 | Matrix.translation(-aboutPoint.x, -aboutPoint.y) 233 | ); 234 | } 235 | 236 | return scaleMatrix; 237 | }; 238 | 239 | /** 240 | * Returns a matrix that corresponds to a translation of tx, ty. 241 | * @see Matrix#translate 242 | * 243 | * @param {Number} tx The amount to translate in the x direction. 244 | * @param {Number} ty The amount to translate in the y direction. 245 | * @return A matrix transformation representing a translation by tx and ty. 246 | * @type Matrix 247 | */ 248 | Matrix.translation = function(tx, ty) { 249 | return new Matrix(1, 0, 0, 1, tx, ty); 250 | }; 251 | 252 | /** 253 | * A constant representing the identity matrix. 254 | * @name IDENTITY 255 | * @fieldOf Matrix 256 | */ 257 | Matrix.IDENTITY = new Matrix(); 258 | /** 259 | * A constant representing the horizontal flip transformation matrix. 260 | * @name HORIZONTAL_FLIP 261 | * @fieldOf Matrix 262 | */ 263 | Matrix.HORIZONTAL_FLIP = new Matrix(-1, 0, 0, 1); 264 | /** 265 | * A constant representing the vertical flip transformation matrix. 266 | * @name VERTICAL_FLIP 267 | * @fieldOf Matrix 268 | */ 269 | Matrix.VERTICAL_FLIP = new Matrix(1, 0, 0, -1); 270 | -------------------------------------------------------------------------------- /lib/motion.js: -------------------------------------------------------------------------------- 1 | module.exports = Motion; 2 | 3 | var Point = require('./math/point') 4 | , Path = require('./path') 5 | , SubPath = require('./subpath') 6 | , utils = require('./utils'); 7 | 8 | /** 9 | * Realtime motion interface 10 | * This actually sends commands to the driver. 11 | * */ 12 | function Motion(ctx) { 13 | this.ctx = ctx; 14 | this.position = new Point(0,0,0); 15 | this.position.a = 0; 16 | } 17 | 18 | Motion.prototype = { 19 | retract: function() { 20 | this.rapid({z:this.ctx.retract}); 21 | } 22 | , plunge: function() { 23 | this.rapid({z:-this.ctx.top}); 24 | } 25 | , zero: function(params) { 26 | this.ctx.driver.zero.call(this.ctx.driver, params); 27 | } 28 | , rapid: function(params) { 29 | var newPosition = this.postProcess(params); 30 | if(!newPosition) return; 31 | 32 | this.ctx.driver.rapid.call(this.ctx.driver, params); 33 | this.position = newPosition; 34 | } 35 | , linear: function(params) { 36 | var newPosition = this.postProcess(params); 37 | if(!newPosition) return; 38 | 39 | // if(params.z - this.position.z > 10) 40 | // debugger; 41 | 42 | this.ctx.driver.linear.call(this.ctx.driver, params); 43 | this.position = newPosition; 44 | } 45 | , arcCW: function(params) { 46 | return this.arc(params,false); 47 | } 48 | , arcCCW: function(params) { 49 | return this.arc(params,true); 50 | } 51 | , arc: function(params,ccw) { 52 | var newPosition = this.postProcess(params); 53 | // Note: Can be cyclic so we don't 54 | // ignore it if the position is the same 55 | // 56 | var cx = this.position.x + params.i; 57 | var cy = this.position.y + params.j; 58 | var arc = utils.pointsToArc({ 59 | x: cx, 60 | y: cy 61 | }, 62 | this.position, { 63 | x: params.x, 64 | y: params.y 65 | }); 66 | 67 | var length = arc.radius * (arc.end-arc.start); 68 | var f = length/(1/this.ctx.feed); 69 | f = Math.round(f * 1000000) / 1000000; 70 | if(f) params.f = Math.abs(f); 71 | 72 | 73 | if(!ccw && this.ctx.driver.arcCW) { 74 | this.ctx.driver.arcCW.call(this.ctx.driver, params); 75 | } 76 | else if(ccw && this.ctx.driver.arcCCW) { 77 | this.ctx.driver.arcCCW.call(this.ctx.driver, params); 78 | } 79 | else { 80 | 81 | this.interpolate('arc',[ 82 | cx, 83 | cy, 84 | arc.radius, 85 | arc.start, 86 | arc.end, 87 | ccw], 88 | params.z||0); 89 | } 90 | 91 | if(newPosition) { 92 | this.position = newPosition; 93 | } 94 | } 95 | , postProcess: function(params) { 96 | // Sync meta 97 | if(this.ctx.driver.unit 98 | && this.ctx.unit != this.currentUnit) { 99 | this.ctx.driver.unit(this.ctx.unit); 100 | this.currentUnit = this.ctx.unit; 101 | } 102 | 103 | // Sync meta 104 | if(this.ctx.driver.meta 105 | && this.ctx.toolDiameter != this.currentToolDiameter) { 106 | this.ctx.driver.meta({ 107 | tooldiameter: this.ctx.toolDiameter 108 | }); 109 | this.currentToolDiameter = this.ctx.toolDiameter; 110 | } 111 | 112 | // Set new spindle atc changed 113 | if(this.ctx.driver.atc 114 | && this.ctx.atc != this.currentAtc) { 115 | this.ctx.driver.atc(this.ctx.atc); 116 | this.currentAtc = this.ctx.atc; 117 | } 118 | 119 | // Set new spindle speed changed 120 | if(this.ctx.driver.speed 121 | && this.ctx.speed != this.currentSpeed) { 122 | this.ctx.driver.speed(this.ctx.speed); 123 | this.currentSpeed = this.ctx.speed; 124 | } 125 | 126 | // Set new feedrate changed 127 | if(this.ctx.driver.feed 128 | && this.ctx.feed != this.currentFeed) { 129 | // Always use inverse time mode 130 | // but we only send a G93 when there is a feedrate. 131 | // This allows backwards compatibility with global 132 | // classic feedrates. 133 | this.ctx.driver.send('G93'); 134 | this.currentFeed = this.ctx.feed; 135 | } 136 | 137 | // Set coolant if changed 138 | if(this.ctx.driver.coolant 139 | && this.ctx.coolant != this.currentCoolant) { 140 | this.ctx.driver.coolant(this.ctx.coolant); 141 | this.currentCoolant = this.ctx.coolant; 142 | } 143 | 144 | var v1 = new Point( 145 | params.x === undefined ? this.position.x : params.x 146 | , params.y === undefined ? this.position.y : params.y 147 | , params.z === undefined ? this.position.z : params.z 148 | , params.a === undefined ? this.position.a : params.a 149 | ); 150 | 151 | // var dist = Point.distance(v1, this.position); 152 | var v2 = this.position; 153 | var dist = Math.sqrt( 154 | Math.pow(v2.x - v1.x, 2) + 155 | Math.pow(v2.y - v1.y, 2) + 156 | Math.pow(v2.z - v1.z, 2)); 157 | 158 | 159 | if(!params.f) { 160 | var f = dist/(1/this.ctx.feed); 161 | f = Math.round(f * 1000000) / 1000000; 162 | if(f) params.f = Math.abs(f); 163 | } 164 | 165 | 166 | if(utils.samePos(this.position, v1)) { 167 | return false; 168 | } 169 | 170 | this.ctx.filters.forEach(function(f) { 171 | var tmp = f.call(this.ctx, params); 172 | 173 | if(tmp) { 174 | for(var k in tmp) { 175 | params[k] = tmp[k]; 176 | } 177 | } 178 | }); 179 | 180 | // Round down the decimal points to 10 nanometers 181 | // Gotta accept that there's no we're that precise. 182 | for(var k in params) { 183 | if(typeof params[k] === 'number') 184 | params[k] = Math.round(params[k] * 100000) / 100000; 185 | } 186 | 187 | 188 | return v1; 189 | } 190 | 191 | , interpolate: function(name, args, zEnd) { 192 | var path = new SubPath(); 193 | path[name].apply(path, args); 194 | 195 | var curLen = 0; 196 | var totalLen = path.getLength(); 197 | var zStart = this.position.z; 198 | 199 | function helix() { 200 | var fullDelta = zEnd - zStart; 201 | var ratio = (curLen / totalLen); 202 | var curDelta = fullDelta * ratio; 203 | return zStart + curDelta; 204 | } 205 | 206 | var pts = path.getPoints(40); 207 | for(var i=0,l=pts.length; i < l; ++i) { 208 | var p=pts[i]; 209 | 210 | var xo = p.x - this.position.x; 211 | var yo = p.y - this.position.y; 212 | curLen += Math.sqrt(xo*xo + yo*yo); 213 | 214 | this.linear({x:p.x, y:p.y, z:helix()}); 215 | } 216 | } 217 | 218 | , followPath: function(path, zEnd) { 219 | if(!path) return false; 220 | 221 | if(path.subPaths) { 222 | path.subPaths.forEach(function(subPath) { 223 | this.followPath(subPath, zEnd); 224 | }, this); 225 | return; 226 | } 227 | 228 | var zStart = this.position.z; 229 | var totalLen = path.getLength(); 230 | var curLen = 0; 231 | var each = {}; 232 | var motion = this; 233 | var driver = this.ctx.driver; 234 | var ctx = this.ctx; 235 | var ramping = path.isClosed() && ctx.ramping != false; 236 | 237 | function helix() { 238 | if(!ramping) { 239 | return zEnd; 240 | } 241 | 242 | // Avoid divide by 0 in case of 243 | // a single moveTo action 244 | if(totalLen === 0) return 0; 245 | 246 | var fullDelta = zEnd - zStart; 247 | var ratio = (curLen / totalLen); 248 | var curDelta = fullDelta * ratio; 249 | 250 | return zStart + curDelta; 251 | } 252 | 253 | function interpolate(name, args) { 254 | var path = new SubPath(); 255 | path.moveTo(motion.position.x, motion.position.y); 256 | path[name].apply(path, args); 257 | 258 | var pts = path.getPoints(40); 259 | for(var i=0,l=pts.length; i < l; ++i) { 260 | var p=pts[i]; 261 | 262 | motion.linear({x:p.x, y:p.y, z:helix()}); 263 | } 264 | } 265 | 266 | each[Path.actions.MOVE_TO] = function(x,y) { 267 | // Optimize out 0 distances moves 268 | var sameXY = (utils.sameFloat(x, this.position.x) && 269 | utils.sameFloat(y, this.position.y)); 270 | 271 | if(ramping && sameXY) return; 272 | 273 | motion.retract(); 274 | motion.rapid({x:x,y:y}); 275 | motion.plunge(); 276 | 277 | if(!ramping) { 278 | motion.linear({z:zEnd}); 279 | } 280 | 281 | zStart = motion.position.z; 282 | 283 | }; 284 | 285 | each[Path.actions.LINE_TO] = function(x,y) { 286 | motion.linear({x:x,y:y,z:helix()}); 287 | }; 288 | 289 | each[Path.actions.ELLIPSE] = function(x, y, rx, ry, 290 | aStart, aEnd, ccw) { 291 | // Detect plain arc 292 | if(utils.sameFloat(rx,ry)) { 293 | var points = utils.arcToPoints(x, y, 294 | aStart, 295 | aEnd, 296 | rx); 297 | var params = { 298 | x: points.end.x, y: points.end.y, 299 | i: x-points.start.x, j: y-points.start.y, 300 | z: helix() 301 | }; 302 | 303 | motion.arc(params, ccw); 304 | 305 | } 306 | else { 307 | interpolate('ellipse', arguments, mx, my); 308 | } 309 | }; 310 | 311 | each[Path.actions.BEZIER_CURVE_TO] = function() { 312 | interpolate('bezierCurveTo', arguments); 313 | }; 314 | 315 | each[Path.actions.QUADRATIC_CURVE_TO] = function() { 316 | interpolate('quadraticCurveTo', arguments); 317 | }; 318 | 319 | for(var i = 0, l = path.actions.length; i < l; ++i) { 320 | item = path.actions[i] 321 | 322 | if(i != 0) { 323 | var x0 = this.position.x; 324 | var y0 = this.position.y; 325 | curLen += path.getActionLength(x0, y0, i); 326 | } 327 | 328 | 329 | // Every action should be plunged except for move 330 | // if(item.action !== Path.actions.MOVE_TO) { 331 | // motion.plunge(); 332 | // } 333 | 334 | each[item.action].apply(this, item.args); 335 | } 336 | } 337 | 338 | }; 339 | -------------------------------------------------------------------------------- /test/test.gcanvas.js: -------------------------------------------------------------------------------- 1 | describe('GCanvas', function() { 2 | var expect = require('chai').expect 3 | var GCanvas = require('../') 4 | var SubPath = require('../lib/subpath') 5 | var TestDriver = require('./support/testdriver') 6 | 7 | var ctx, robot, hand; 8 | beforeEach(function() { 9 | robot = new TestDriver(); 10 | hand = new TestDriver(); 11 | ctx = new GCanvas(robot); 12 | ctx.depth = 0; 13 | ctx.depthOfCut = 0; 14 | }); 15 | 16 | describe('#moveTo', function() { 17 | it('sends #rapid', function() { 18 | ctx.moveTo(10,10); 19 | ctx.stroke(); 20 | hand.rapid({x:10,y:10}); 21 | expect(robot.result).eql(hand.result); 22 | }); 23 | 24 | it('retracts tool', function() { 25 | ctx.depth = 1; 26 | ctx.moveTo(0,0); 27 | ctx.lineTo(10,0); 28 | ctx.moveTo(10,10); 29 | ctx.stroke(); 30 | 31 | hand.linear({z:-1}); // plunge 32 | hand.linear({x:10,y:0,z:-1}); // lineTo 33 | hand.rapid({z:0}); // retract 34 | hand.rapid({x:10,y:10}); // moveTo 35 | 36 | expect(robot.result).eql(hand.result); 37 | }); 38 | 39 | it('optimizes out 0 distance moves', function() { 40 | ctx.moveTo(10,10); 41 | ctx.moveTo(10,10); 42 | ctx.stroke(); 43 | hand.rapid({x:10,y:10}); 44 | expect(robot.result).eql(hand.result); 45 | }); 46 | 47 | 48 | }); 49 | 50 | describe('#lineTo', function() { 51 | it('sends #linear', function() { 52 | ctx.moveTo(0,0); 53 | ctx.lineTo(10,10); 54 | ctx.stroke(); 55 | 56 | hand.linear({x:10,y:10,z:0}); 57 | 58 | expect(robot.result).eql(hand.result); 59 | }); 60 | 61 | it('just moves if no subpath', function() { 62 | ctx.lineTo(10,10); 63 | ctx.stroke(); 64 | 65 | hand.rapid({x:10,y:10}); 66 | 67 | expect(robot.result).eql(hand.result); 68 | }); 69 | 70 | it('plunges tool', function() { 71 | ctx.depth = 1; 72 | ctx.moveTo(0,0); 73 | ctx.lineTo(10,10); 74 | ctx.stroke(); 75 | 76 | hand.linear({z:-1}); 77 | hand.linear({x:10,y:10,z:-1}); 78 | hand.rapid({z:0}); 79 | expect(robot.result).eql(hand.result); 80 | }); 81 | 82 | it('plunges tool (inverted z)', function() { 83 | ctx.depth = -1; 84 | ctx.moveTo(0,0); 85 | ctx.lineTo(10,10); 86 | ctx.stroke(); 87 | 88 | hand.linear({z:1}); 89 | hand.linear({x:10,y:10,z:1}); 90 | hand.rapid({z:0}); 91 | 92 | expect(robot.result).eql(hand.result); 93 | }); 94 | 95 | it('plunges tool (inverted z and +depthOfCut)', function() { 96 | ctx.depth = -1; 97 | ctx.depthOfCut = 1; 98 | ctx.moveTo(0,0); 99 | ctx.lineTo(10,10); 100 | ctx.stroke(); 101 | 102 | hand.linear({z:1}); 103 | hand.linear({x:10,y:10,z:1}); 104 | hand.rapid({z:0}); 105 | 106 | expect(robot.result).eql(hand.result); 107 | }); 108 | }); 109 | 110 | describe('#arc', function() { 111 | it('sends native #arcCW when possible', function() { 112 | ctx.arc(10, 10, 10, 0, Math.PI); 113 | ctx.stroke(); 114 | 115 | hand.rapid({x:20,y:10}); 116 | hand.arcCW({x:0,y:10,z:0,i:-10,j:0}); 117 | 118 | expect(robot.result).eql(hand.result); 119 | }); 120 | 121 | it('sends native #arcCCW when possible', function() { 122 | ctx.arc(10, 10, 10, 0, Math.PI, true); 123 | ctx.stroke(); 124 | 125 | hand.rapid({x:20,y:10}); 126 | hand.arcCCW({x:0,y:10,z:0,i:-10,j:0}); 127 | 128 | expect(robot.result).eql(hand.result); 129 | }); 130 | 131 | it('plunges and retracts semi-circles', function() { 132 | ctx.depth = 1; 133 | ctx.arc(10, 10, 10, 0, Math.PI); 134 | ctx.stroke(); 135 | 136 | hand.rapid({x:20,y:10}); 137 | hand.linear({z:-1}); 138 | hand.arcCW({x:0,y:10,i:-10,j:0,z:-1}); 139 | hand.rapid({z:0}); 140 | 141 | expect(robot.result).eql(hand.result); 142 | }); 143 | 144 | it('spirals through full circles', function() { 145 | ctx.depth = 2; 146 | ctx.depthOfCut=1; 147 | ctx.arc(10, 10, 10, 0, Math.PI*2); 148 | ctx.stroke(); 149 | 150 | hand.rapid({x:20,y:10}); 151 | hand.arcCW({x:20,y:10,z:-1,i:-10,j:0}); 152 | hand.arcCW({x:20,y:10,z:-2,i:-10,j:0}); 153 | hand.arcCW({x:20,y:10,z:-2,i:-10,j:0}); 154 | hand.rapid({z:0}); 155 | 156 | expect(robot.result).eql(hand.result); 157 | }); 158 | }); 159 | 160 | 161 | describe('context.feed', function() { 162 | it('inits inverse time mode and calculates F', function() { 163 | ctx.moveTo(0,0); 164 | ctx.lineTo(10,10); 165 | ctx.feed = 100; 166 | ctx.stroke(); 167 | 168 | hand.send('G93'); 169 | hand.linear({x:10,y:10,z:0,f:1414}); 170 | 171 | expect(robot.result).eql(hand.result); 172 | }); 173 | }); 174 | 175 | describe('context.speed', function() { 176 | it('calls driver.speed() before next move', function() { 177 | ctx.moveTo(0,0); 178 | ctx.lineTo(10,10); 179 | ctx.speed = 100; 180 | ctx.stroke(); 181 | 182 | hand.speed(100); 183 | hand.linear({x:10,y:10,z:0}); 184 | 185 | expect(robot.result).eql(hand.result); 186 | }); 187 | }); 188 | 189 | describe('context.coolant', function() { 190 | it('calls driver.coolant() before next move', function() { 191 | ctx.moveTo(0,0); 192 | ctx.lineTo(10,10); 193 | ctx.coolant = "flood"; 194 | ctx.stroke(); 195 | 196 | hand.coolant("flood"); 197 | hand.linear({x:10,y:10,z:0}); 198 | 199 | expect(robot.result).eql(hand.result); 200 | }); 201 | }); 202 | 203 | describe('#_layer', function() { 204 | it('increments in depthOfCut to depth', function() { 205 | ctx.depth = 2; 206 | ctx.depthOfCut = 1; 207 | ctx.moveTo(0,0); 208 | ctx.lineTo(10,10); 209 | ctx.stroke(); 210 | 211 | // first layer 212 | hand.linear({z:-1}); // plunge 213 | hand.linear({x:10,y:10,z:-1}); // lineTo 214 | 215 | // return to start 216 | hand.rapid({z:0}); // retract 217 | hand.rapid({x:0,y:0}); // moveTo 218 | 219 | // second layer 220 | hand.linear({z:-2}); // plunge 221 | hand.linear({x:10,y:10,z:-2}); // lineTo 222 | 223 | hand.rapid({z:0}); 224 | 225 | expect(robot.result).eql(hand.result); 226 | }); 227 | 228 | it('increments in depthOfCut to depth (-z, +doc)', function() { 229 | ctx.depth = -2; 230 | ctx.depthOfCut = 1; 231 | ctx.moveTo(0,0); 232 | ctx.lineTo(10,10); 233 | ctx.stroke(); 234 | 235 | // first layer 236 | hand.linear({z:1}); // plunge 237 | hand.linear({x:10,y:10,z:1}); // lineTo 238 | 239 | // return to start 240 | hand.rapid({z:0}); // retract 241 | hand.rapid({x:0,y:0}); // moveTo 242 | 243 | // second layer 244 | hand.linear({z:2}); // plunge 245 | hand.linear({x:10,y:10,z:2}); // lineTo 246 | 247 | hand.rapid({z:0}); // retract 248 | expect(robot.result).eql(hand.result); 249 | }); 250 | 251 | it('increments in depthOfCut to depth (-z, -doc)', function() { 252 | ctx.depth = -2; 253 | ctx.depthOfCut = -1; 254 | ctx.moveTo(0,0); 255 | ctx.lineTo(10,10); 256 | ctx.stroke(); 257 | 258 | // first layer 259 | hand.linear({z:1}); // plunge 260 | hand.linear({x:10,y:10,z:1}); // lineTo 261 | 262 | // return to start 263 | hand.rapid({z:0}); // retract 264 | hand.rapid({x:0,y:0}); // moveTo 265 | 266 | // second layer 267 | hand.linear({z:2}); // plunge 268 | hand.linear({x:10,y:10,z:2}); // lineTo 269 | 270 | hand.rapid({z:0}); // retract 271 | expect(robot.result).eql(hand.result); 272 | }); 273 | }); 274 | 275 | describe('#fill', function() { 276 | it('offsets outward from center', function() { 277 | ctx.rect(0,0,10,10); 278 | ctx.toolDiameter = 8; 279 | ctx.fill(); 280 | 281 | hand.rapid({x:6,y:6}); 282 | hand.linear({x:6, y:4, z:0}); 283 | hand.linear({x:4, y:4, z:0}); 284 | hand.linear({x:4, y:6, z:0}); 285 | hand.linear({x:6, y:6, z:0}); 286 | 287 | expect(robot.result).eql(hand.result); 288 | }); 289 | 290 | it('helixes down w/ ramping=true', function() { 291 | ctx.rect(0,0,10,10); 292 | ctx.toolDiameter = 8; 293 | ctx.depth = 10; 294 | ctx.ramping = true; 295 | ctx.fill(); 296 | 297 | // helix 298 | hand.rapid({x:6,y:6}); 299 | hand.linear({x:6, y:4, z:-3}); 300 | hand.linear({x:4, y:4, z:-5}); 301 | hand.linear({x:4, y:6, z:-8}); 302 | hand.linear({x:6, y:6, z:-10}); 303 | 304 | // finishing pass 305 | hand.linear({x:6, y:4, z:-10}); 306 | hand.linear({x:4, y:4, z:-10}); 307 | hand.linear({x:4, y:6, z:-10}); 308 | hand.linear({x:6, y:6, z:-10}); 309 | 310 | // retract 311 | hand.rapid({z:0}); 312 | 313 | expect(robot.result).eql(hand.result); 314 | }); 315 | 316 | it('steps down w/ ramping=false', function() { 317 | ctx.rect(0,0,10,10); 318 | ctx.toolDiameter = 8; 319 | ctx.depthOfCut = 5; 320 | ctx.depth = 10; 321 | ctx.ramping = false; 322 | ctx.fill(); 323 | 324 | // pass 1 325 | hand.rapid({x:6, y:6}); 326 | hand.linear({z:-5}); 327 | hand.linear({x:6, y:4, z:-5}); 328 | hand.linear({x:4, y:4, z:-5}); 329 | hand.linear({x:4, y:6, z:-5}); 330 | hand.linear({x:6, y:6, z:-5}); 331 | 332 | // pass 2 333 | hand.rapid({z:0}); 334 | hand.linear({z:-10}); 335 | hand.linear({x:6, y:4, z:-10}); 336 | hand.linear({x:4, y:4, z:-10}); 337 | hand.linear({x:4, y:6, z:-10}); 338 | hand.linear({x:6, y:6, z:-10}); 339 | 340 | // retract 341 | hand.rapid({z:0}); 342 | 343 | expect(robot.result).eql(hand.result); 344 | }); 345 | 346 | 347 | it('ignores 0 alpha fillStyle', function() { 348 | ctx.rect(0,0,10,10); 349 | ctx.toolDiameter = 8; 350 | ctx.fillStyle='rgba(0,0,0,0)' 351 | ctx.fill(); 352 | 353 | expect(robot.result).eql(hand.result); 354 | }); 355 | }); 356 | 357 | 358 | describe('#stroke', function() { 359 | it('offsets outward from center', function() { 360 | ctx.rect(0,0,10,10); 361 | ctx.toolDiameter = 8; 362 | ctx.stroke(); 363 | 364 | hand.linear({x:10, y:0, z:0}); 365 | hand.linear({x:10, y:10, z:0}); 366 | hand.linear({x:0, y:10, z:0}); 367 | hand.linear({x:0, y:0, z:0}); 368 | 369 | expect(robot.result).eql(hand.result); 370 | }); 371 | 372 | it('ignores 0 alpha strokeStyle', function() { 373 | ctx.rect(0,0,10,10); 374 | ctx.toolDiameter = 8; 375 | ctx.strokeStyle='rgba(0,0,0,0)' 376 | ctx.stroke(); 377 | 378 | expect(robot.result).eql(hand.result); 379 | }); 380 | }); 381 | }); 382 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | NOTICE: I have pretty much abandoned this project. I just don't have the means or incentive to maintain it, because I relocated to another country some time ago and haven't had a CNC machine since. I stick to traditional handtool woodworking these days when I do find the time to make things. 2 | ======== 3 | 4 | Gcanvas 5 | ======== 6 | An HTML5 Canvas implementation that generates Gcode for 4 axis CNC milling. 7 | 8 | ### Installation 9 | First make sure you have [nodejs](http://nodejs.org) installed. 10 | ``` 11 | npm install -g gcanvas 12 | ``` 13 | 14 | ### Example 15 | example.js 16 | ``` 17 | function main(ctx) { 18 | ctx.translate(-90,0); 19 | ctx.toolDiameter = 1/8*25.4; 20 | ctx.depth = 5; 21 | ctx.font = '20pt Helvetiker'; 22 | roundRect(ctx, 0,0,180,60,5); 23 | ctx.text('I < 3 robots', 12, 40); 24 | ctx.fill('evenodd'); 25 | } 26 | 27 | function roundRect(ctx, x, y, w, h, r) { 28 | if (w < 2 * r) r = w / 2; 29 | if (h < 2 * r) r = h / 2; 30 | ctx.moveTo(x+r, y); 31 | ctx.arcTo(x+w, y, x+w, y+h, r); 32 | ctx.arcTo(x+w, y+h, x, y+h, r); 33 | ctx.arcTo(x, y+h, x, y, r); 34 | ctx.arcTo(x, y, x+w, y, r); 35 | ctx.closePath(); 36 | } 37 | ``` 38 | ``` 39 | $ gcanvas example.js | gsim 40 | ``` 41 |  42 | ``` 43 | $ gcanvas example.js | mycnc 44 | ``` 45 |  46 | 47 | ### Non-standard extensions to Canvas 48 | 49 | #### Properties 50 | 51 | * `ctx.toolDiameter` This must be set for fill() to work because it has to calculate tool offsets. stroke() only requires toolDiameter if align is not center. 52 | 53 | * `ctx.depth` Specifies the total depth to cut into the work. If not set the Z axis never changes. 54 | 55 | * `ctx.depthOfCut` Specifies an incrementing depth of cut in layers up to `ctx.depth`. 56 | 57 | * `ctx.top` The Z offset to the top of work surface. When this is set all cuts with plunge down to this depth before spiraling to their full depth. Use cautiously. If the actual work surface is closer this will break tools. I often use this in tandem with facing. First I align the tool to something within 1mm of the lowest part of my material surface, face it down to 1mm, then set ctx.top to 1mm. I then update ctx.top for every tool change. 58 | ``` 59 | function main(ctx) { 60 | step('face', function() { 61 | ctx.depth = 1; 62 | ctx.fillRect(-20,-20,40,40); 63 | }); 64 | 65 | // Outside of a step, applies to all steps after facing 66 | // even if they are run independently. 67 | ctx.top = 1; 68 | 69 | step('10mm hole', function() { 70 | ctx.depth = 20; // 20mm deep 71 | ctx.circle(0,0,10); 72 | ctx.fill(); 73 | }); 74 | } 75 | 76 | ``` 77 | 78 | * `ctx.retract` Set a small distance to retract the tool between rapid moves to overcome imperfect surface tolerances. This is a simpler alternative to ctx.top when the work surface is flat and the tool can be aligned initially to surface zero. i.e. sheet stock. (default: 0) 79 | 80 |  81 | 82 | * `ctx.feed` Sets the effective feedrate. 83 | To support coordinated axial and linear motion Gcanvas always generates inverse time mode feeds 84 | based on the distance of the linear motion, making axial feeds slave to linear. 85 | 86 | * `ctx.speed` Sets the spindle speed by sending a single S command. 87 | 88 | * `ctx.coolant` Can be true, false, "mist" (M07) or "flood" (M08). True defaults to "flood". 89 | 90 | * `ctx.align` Can be 'inner', 'outer', or 'center' (default). Non-center alignment closes the path. 91 | 92 | * `ctx.atc` Auto tool change. Sends `M06 T{ctx.atc}`. Make sure you update toolDiameter. 93 | 94 | * `ctx.unit` (mm|inch) Set unit mode. G20/G21 95 | 96 | * `ctx.ramping` (true|false) Ramping eases into the depth of cut helically. The alternative is to plunge directly down to depth for each cut. It can reduce cutting time to disable this when you use a center-cutting endmill on forgiving material. (default: true) 97 | 98 | 99 | #### Methods 100 | 101 | ##### `ctx.fill([windingRule, depth])` 102 | ##### `ctx.fill([depth])` 103 | Standard canvas fill(), but extended to allow a number for depth. 104 | 105 | * `windingRule`: 'nonzero' or 'evenodd'. (default: 'nonzero') 106 | * `depth`: Full depth to mill the current path to. (default: ctx.depth) 107 | 108 | ##### `ctx.stroke([align, depth])` 109 | ##### `ctx.stroke([depth])` 110 | Standard canvas stroke(), but can also take alignment and/or depth. 111 | 112 | * `align`: 'inner', 'outer', or 'center'. (default: ctx.align or 'center') 113 | * `depth`: Full depth to mill the current path to. (default: ctx.depth) 114 | 115 | ##### `ctx.filter(fn)` 116 | Adds a post-processing function before Gcode generation that allows 117 | you to change the parameters. 118 | * `fn`: function(params) i.e. {x: 10, y:20} 119 | 120 | Example: Cylindrical wrapping by transposing the X axis onto A 121 | ``` 122 | ctx.toolDiameter = 1/4*25.5; 123 | 124 | var diameter = 10; 125 | var circumference = diameter * Math.PI; 126 | 127 | ctx.filter(function(p) { 128 | p.a = (p.x||0)/circumference*360; 129 | p.x = 0; 130 | }); 131 | 132 | ctx.fillRect(0,0,circumference/2,10); // 180 degree slot around cylinder 133 | ``` 134 | 135 | 136 | ##### `ctx.map(axes)` 137 | Adds a filter that maps the standard coordinate system to another one. 138 | * `axes`: A string representing the new coordinate system. 139 | You can also use '-' before any axis to make it negative. 140 | 141 | Example: Make Z- move towards the bed. 142 | ``` 143 | ctx.map('xy-z'); 144 | ``` 145 | 146 | 147 | ##### `ctx.peckDrill(x, y, depth[, peck])` 148 | Drills to depth using a peck drilling strategy. 149 | * `depth`: Full depth to drill to. 150 | * `peck`: Length of each peck. (default: ctx.toolDiameter) 151 | 152 | ##### `ctx.lathe(x, y, attack, pitch, ccw)` 153 | Turning, boring, and facing by controlling XYA to remove the current path as if it were the cross section of a cylinder centered about (0,0). The path is clipped to the bottom right quadrant (x > 0 and y > 0). 154 | 155 | * `attack`: 'inner', 'outer', or 'face' 156 | Use inner for boring, and outer for turning. 157 | 'inner' removes material from the center of a hole outward. 158 | 'outer' removes material from the perimeter of a cylinder inwards. 159 | 'face' removes material from the face of a cylinder through its length. 160 | * `pitch`: Distance to travel per a full rotation. 161 | * `ccw`: Counter-clockwise rotation. (default: false) 162 | 163 | ##### `ctx.latheMill(x, y, attack, pitch, ccw)` 164 | Like lathe but instead of a rotary axis it generates helixes in XYZ. 165 | 166 | * `attack`: 'inner', 'outer', or 'face' 167 | Use inner for boring, and outer for turning. 168 | 'inner' removes material from the center of a hole outward. 169 | 'outer' removes material from the perimeter of a cylinder inwards. 170 | 'face' removes material top down. 171 |  172 | 173 | * `pitch`: Distance to travel per a full rotation. 174 | * `ccw`: Counter-clockwise rotation. (default: false) 175 | 176 | ##### `ctx.thread(x, y, attack, dmin, dmaj, pitch, start, length, ccw)` 177 | Convenience method for turning threads. 178 | Simply lathe() with a rectangular path. 179 | 180 | * `attack`: 'inner' or 'outer' 181 | * `dmin`: Minor diameter of threads. 182 | * `dmaj`: Major diameter of threads. 183 | * `pitch`: Distance between threads. 184 | * `start`: Length to beginning of threads. 185 | * `length`: Full length of threads. 186 | * `ccw`: Counter-clockwise rotation. (default: false) 187 | 188 | ##### `ctx.threadMill(x, y, attack, dmin, dmaj, pitch, start, length, ccw)` 189 | Convenience method for milling threads. 190 | Simply latheMill() with a rectangular path. 191 | 192 | * `attack`: 'inner' or 'outer' 193 | * `dmin`: Minor diameter of threads. 194 | * `dmaj`: Major diameter of threads. 195 | * `pitch`: Distance between threads. 196 | * `start`: Length to beginning of threads. 197 | * `length`: Full length of threads. 198 | * `ccw`: Counter-clockwise rotation. (default: false) 199 | 200 | Example: 201 | ``` 202 | // M8x1.25 inner threads 10mm deep 203 | 204 | // Metric thread standard precise calculation 205 | // http://en.wikipedia.org/wiki/ISO_metric_screw_thread 206 | var dmin = 8-2*5/8*Math.sqrt(3)/2*1.25; 207 | 208 | // Note: Most commerical metric screws I've measured aren't 209 | // even close to the standard. Manufacturers seem to put huge 210 | // tolerances in them so they fit almost anywhere. 211 | 212 | step('base hole', function() { 213 | ctx.toolDiameter = 1/4*25.4; 214 | ctx.fillCircle(0,0,dmin,12); 215 | }); 216 | 217 | step('thread mill', function() { 218 | // Single thread cutter. 219 | ctx.toolDiameter = 1/4*25.4; 220 | ctx.threadMill(0,0,'inner',dmin-0.1,8,1.25,0,10); 221 | // Note: We subtract 0.1 just to afford us tolerance. 222 | // since the true Dmin is determined by the base hole. 223 | }); 224 | ``` 225 | 226 | ##### `ctx.text(text, x, y)` 227 | Adds text to the path. 228 | For some reason standard Canvas only has fillText and strokeText. 229 | I don't know why. That makes it impossible to use winding rules. 230 | 231 | ##### `ctx.circle(x, y, radius)` 232 | Convenience method for full 2pi arc(). 233 | 234 | ##### `ctx.fillRect(x, y, width, height, depth)` 235 | Standard canvas fillRect() with additional depth. 236 | 237 | ##### `ctx.fillCircle(x, y, width, height, depth)` 238 | Convenience method to fill a circle to depth just like fillRect(). 239 | 240 | 241 | ### gcanvas(1) 242 | Gcanvas comes with a command line utility that you can use to write standalone Javascript CNC jobs. Just define a `main(context)` function in the script, and gcanvas will pass it a 243 | pre-built context that outputs to stdout. 244 | 245 | ``` 246 | // helloworld.js 247 | function main(ctx) { 248 | ctx.strokeText("Hello World"); 249 | } 250 | ``` 251 | ``` 252 | $ gcanvas helloworld.js | mycnc 253 | ``` 254 | 255 | #### Setups and tool changes 256 | 257 | gcanvas(1) exposes a global function `step(name, fn)` which prompts for user 258 | intervention and raises the Z axis to 0. 259 | 260 | If the part requires multiple work setups and tool changes, break them into setup blocks: 261 | 262 | ``` 263 | step('1/2" endmill', function(ctx) { 264 | ctx.toolDiameter = 1/2*25.4; 265 | // ... 266 | }); 267 | 268 | step('face down', function(ctx) { 269 | // ... 270 | }); 271 | ``` 272 | 273 | ### As a library 274 | ``` 275 | var Gcanvas = require('gcanvas'); 276 | 277 | var driver = new Gcanvas.GcodeDriver({ 278 | write: function(cmd) { 279 | console.log(cmd); 280 | } 281 | }); 282 | 283 | var ctx = new Gcanvas(driver); 284 | ``` 285 | 286 | ### Why 287 | 1. Most real life machining is either simple 2.5D shapes, 288 | or complex geometry. Both of which are easier to do this way than with traditional CAD/CAM software. It breaks down with moderately complex 3D geometry that is not based on mathematic formulas. 289 | 2. The Canvas API is well documented and prolific. 290 | 3. Especially good for parametric parts. 291 | 4. A good basis for implementing more specific Javascript milling tools. e.g. svg, pcbs. 292 | 293 | 294 | ### Additional Notes 295 | 296 | #### Center cutting and non-center cutting endmills. 297 | The strategy for filling always tries to avoid center cutting 298 | unless it is unavoidable. For instance, if you try to fill a 15mm circle with a 5mm endmill it will start by milling a 10mm circle and finish with a full 15mm circle, which can be done with a non-center cutting endmill. But, if you try to fill an 8mm circle with a 5mm endmill it will assume you have the right tool to do so, and proceed without caution. Just make sure that what you're telling Gcanvas to do is actually possible and you won't have any problems. It always tends to work out that we use the simplest tool for the job - mainly because they are cheaper. Knowing this, Gcanvas avoids a lot of annoying configuration by making the most conservative assumptions. 299 | 300 | #### Climb milling 301 | Gcanvas always tries to climb mill. Since it doesn't know the stock material, it sort of "guesses". But the guesses are pretty reliable. 302 | It bases what the outside or inside of the material is on alignment. If you do an outer stroke() it will always normalize and follow the path clockwise, or counter-clockwise for inner stroke(). 303 | 304 | In the case of lathe()/latheMill() 'inner' and 'outer' attack also 305 | always determine clockwise or counter-clockwise rotational direction. 306 | The physical thread direction is acheived by how the cutter traverses the length, one end to another. 307 | 308 | ### License 309 | 310 | MIT 311 | 312 | [](https://drone.io/github.com/em/gcanvas/latest) 313 | -------------------------------------------------------------------------------- /lib/subpath.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Derived from code originally written by zz85 for three.js 3 | * http://www.lab4games.net/zz85/blog 4 | * Thanks zz85! 5 | **/ 6 | 7 | module.exports = SubPath; 8 | 9 | var Point = require('./math/point') 10 | , ClipperLib = require('./clipper') 11 | , Path = require('./path') 12 | , utils = require('./utils'); 13 | 14 | function SubPath( points ) { 15 | this.actions = []; 16 | this.pointsCache = []; 17 | 18 | if ( points ) { 19 | this.fromPoints( points ); 20 | } 21 | }; 22 | 23 | SubPath.actions = { 24 | MOVE_TO: 'moveTo', 25 | LINE_TO: 'lineTo', 26 | QUADRATIC_CURVE_TO: 'quadraticCurveTo', 27 | BEZIER_CURVE_TO: 'bezierCurveTo', 28 | ELLIPSE: 'ellipse' 29 | }; 30 | 31 | SubPath.prototype = { 32 | clone: function() { 33 | var path = new SubPath(); 34 | path.actions = this.actions.slice(0); 35 | return path; 36 | } 37 | 38 | , isClosed: function() { 39 | var fp = this.firstPoint(); 40 | var lp = this.lastPoint(); 41 | return utils.samePos(fp,lp); 42 | } 43 | 44 | , offset: function(delta) { 45 | var tmp = this.toPath().offset(delta); 46 | if(!tmp) return false; 47 | return tmp.subPaths[0]; 48 | } 49 | 50 | , simplify: function() { 51 | var tmp = this.toPath().simplify(); 52 | if(!tmp) return false; 53 | return tmp.subPaths[0]; 54 | } 55 | 56 | , toPath: function() { 57 | var clone = this.clone(); 58 | var path = new Path(); 59 | path.subPaths.push(clone); 60 | path.current = path.subPaths[path.subPaths.length-1]; 61 | return path; 62 | } 63 | 64 | , addAction: function(action) { 65 | this.actions.push(action); 66 | this.pointsCache = []; 67 | } 68 | 69 | , firstPoint: function() { 70 | var p = new Point(0,0); 71 | var action = this.actions[0]; 72 | var args = action.args; 73 | 74 | switch(action.action) { 75 | case 'ellipse': 76 | p = utils.arcToPoints( 77 | args[0], args[1], args[4], 78 | args[5], args[2]).start 79 | break; 80 | 81 | default: 82 | p.x = args[args.length-2]; 83 | p.y = args[args.length-1]; 84 | break; 85 | } 86 | 87 | return p; 88 | } 89 | 90 | , lastPoint: function() { 91 | var p = new Point(0,0); 92 | var action = this.actions[this. 93 | actions.length-1]; 94 | var args = action.args; 95 | 96 | switch(action.action) { 97 | case 'ellipse': 98 | p = utils.arcToPoints( 99 | args[0], args[1], args[4], 100 | args[5], args[2]).end 101 | break; 102 | 103 | default: 104 | p.x = args[args.length-2]; 105 | p.y = args[args.length-1]; 106 | break; 107 | } 108 | 109 | return p; 110 | } 111 | 112 | , fromPoints: function ( points ) { 113 | this.moveTo( points[ 0 ].x, points[ 0 ].y ); 114 | 115 | for ( var v = 1, vlen = points.length; v < vlen; v ++ ) { 116 | this.lineTo( points[ v ].x, points[ v ].y ); 117 | }; 118 | } 119 | 120 | , getActionLength: function(x0,y0,i) { 121 | var action = this.actions[i], 122 | args = action.args; 123 | 124 | if(action.action == 'ellipse') { 125 | var rad = args[3]; 126 | var astart = args[4]; 127 | var aend = args[5]; 128 | 129 | return (aend-astart)*rad; 130 | } 131 | 132 | var x = args[args.length-2]; 133 | var y = args[args.length-1]; 134 | var xo = x - x0; 135 | var yo = y - y0; 136 | return Math.sqrt(xo*xo + yo*yo); 137 | } 138 | 139 | , getLength: function() { 140 | var args, x1=0, y1=0, x2=0, y2=0, xo=0, yo=0, len=0; 141 | 142 | var first = this.firstPoint(); 143 | x2 = first.x; 144 | y2 = first.y; 145 | 146 | var pts = this.getPoints(10000); 147 | for(var i=1,l=pts.length; i < l; ++i) { 148 | var p=pts[i]; 149 | x1 = x2; 150 | y1 = y2; 151 | x2 = p.x; 152 | y2 = p.y; 153 | xo = x2-x1; 154 | yo = y2-y1; 155 | 156 | len += Math.sqrt(xo*xo + yo*yo); 157 | } 158 | return len; 159 | } 160 | 161 | , nearestPoint: function(p1) { 162 | var p2 = new Point() 163 | , args 164 | , rn 165 | , rp 166 | , rd = Infinity; 167 | 168 | this.actions.forEach(function(action,n) { 169 | args = action.args; 170 | p2.x = args[args.length-2]; 171 | p2.y = args[args.length-1]; 172 | 173 | var d = Point.distance(p1,p2); 174 | if(d < rd) { 175 | rn = n; 176 | rp = p2.clone(); 177 | rd = d; 178 | } 179 | }); 180 | 181 | return { 182 | i: rn 183 | , distance: rd 184 | , point: rp 185 | }; 186 | } 187 | 188 | , pointAt: function(i) { 189 | var p = new Point(); 190 | var action = this.actions[i]; 191 | var args = action.args; 192 | switch(action.action) { 193 | case 'lineTo': 194 | p.x = args[args.length-2]; 195 | p.y = args[args.length-1]; 196 | break; 197 | 198 | } 199 | 200 | return p; 201 | } 202 | 203 | , shiftToNearest: function(x, y) { 204 | var nearest = this.nearestPoint(new Point(x,y)); 205 | return this.shift(nearest.i); 206 | } 207 | 208 | , shift: function(an) { 209 | if(an === 0) return this; 210 | 211 | var result = new SubPath(); 212 | 213 | 214 | result.actions = this.actions.slice(an).concat( 215 | this.actions.slice(0,an) 216 | ); 217 | 218 | result.actions.forEach(function(a) { 219 | a.action = SubPath.actions.LINE_TO; 220 | }); 221 | 222 | result.lineTo.apply(result, result.actions[0].args); 223 | 224 | return result; 225 | } 226 | 227 | , moveTo: function ( x, y ) { 228 | this.addAction( { action: SubPath.actions.MOVE_TO, args: arguments } ); 229 | } 230 | 231 | , lineTo: function ( x, y ) { 232 | this.addAction( { action: SubPath.actions.LINE_TO, args: arguments } ); 233 | } 234 | 235 | , quadraticCurveTo: function( aCPx, aCPy, aX, aY ) { 236 | this.addAction( { action: SubPath.actions.QUADRATIC_CURVE_TO, args: arguments } ); 237 | } 238 | 239 | , bezierCurveTo: function( aCP1x, aCP1y, 240 | aCP2x, aCP2y, 241 | aX, aY ) { 242 | this.addAction( { action: SubPath.actions.BEZIER_CURVE_TO, args: arguments } ); 243 | } 244 | 245 | , arc: function ( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise ) { 246 | this.ellipse(aX, aY, aRadius, aRadius, aStartAngle, aEndAngle, aClockwise); 247 | } 248 | 249 | , ellipse: function ( aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise ) { 250 | this.addAction( { action: SubPath.actions.ELLIPSE, args: arguments } ); 251 | } 252 | 253 | , getPoints: function( divisions ) { 254 | 255 | divisions = divisions || 40; 256 | 257 | if(this.pointsCache[divisions]) { 258 | return this.pointsCache[divisions]; 259 | } 260 | 261 | 262 | var points = []; 263 | 264 | var i, il, item, action, args; 265 | var cpx, cpy, cpx2, cpy2, cpx1, cpy1, cpx0, cpy0, 266 | laste, j, 267 | t, tx, ty; 268 | 269 | for ( i = 0, il = this.actions.length; i < il; i ++ ) { 270 | 271 | item = this.actions[ i ]; 272 | 273 | action = item.action; 274 | args = item.args; 275 | 276 | switch( action ) { 277 | 278 | case SubPath.actions.MOVE_TO: 279 | 280 | points.push( new Point( args[ 0 ], args[ 1 ] ) ); 281 | 282 | break; 283 | 284 | case SubPath.actions.LINE_TO: 285 | 286 | points.push( new Point( args[ 0 ], args[ 1 ] ) ); 287 | 288 | break; 289 | 290 | case SubPath.actions.QUADRATIC_CURVE_TO: 291 | 292 | cpx = args[ 2 ]; 293 | cpy = args[ 3 ]; 294 | 295 | cpx1 = args[ 0 ]; 296 | cpy1 = args[ 1 ]; 297 | 298 | if ( points.length > 0 ) { 299 | 300 | laste = points[ points.length - 1 ]; 301 | 302 | cpx0 = laste.x; 303 | cpy0 = laste.y; 304 | 305 | } else { 306 | 307 | laste = this.actions[ i - 1 ].args; 308 | 309 | cpx0 = laste[ laste.length - 2 ]; 310 | cpy0 = laste[ laste.length - 1 ]; 311 | 312 | } 313 | 314 | for ( j = 1; j <= divisions; j ++ ) { 315 | 316 | t = j / divisions; 317 | 318 | tx = b2( t, cpx0, cpx1, cpx ); 319 | ty = b2( t, cpy0, cpy1, cpy ); 320 | 321 | points.push( new Point( tx, ty ) ); 322 | 323 | } 324 | 325 | break; 326 | 327 | case SubPath.actions.BEZIER_CURVE_TO: 328 | 329 | cpx = args[ 4 ]; 330 | cpy = args[ 5 ]; 331 | 332 | cpx1 = args[ 0 ]; 333 | cpy1 = args[ 1 ]; 334 | 335 | cpx2 = args[ 2 ]; 336 | cpy2 = args[ 3 ]; 337 | 338 | if ( points.length > 0 ) { 339 | 340 | laste = points[ points.length - 1 ]; 341 | 342 | cpx0 = laste.x; 343 | cpy0 = laste.y; 344 | 345 | } else { 346 | 347 | laste = this.actions[ i - 1 ].args; 348 | 349 | cpx0 = laste[ laste.length - 2 ]; 350 | cpy0 = laste[ laste.length - 1 ]; 351 | 352 | } 353 | 354 | for ( j = 1; j <= divisions; j ++ ) { 355 | 356 | t = j / divisions; 357 | 358 | tx = b3( t, cpx0, cpx1, cpx2, cpx ); 359 | ty = b3( t, cpy0, cpy1, cpy2, cpy ); 360 | 361 | points.push( new Point( tx, ty ) ); 362 | 363 | } 364 | 365 | break; 366 | 367 | case SubPath.actions.ELLIPSE: 368 | 369 | var aX = args[ 0 ], aY = args[ 1 ], 370 | xRadius = args[ 2 ], 371 | yRadius = args[ 3 ], 372 | aStartAngle = args[ 4 ], aEndAngle = args[ 5 ], 373 | aClockwise = !!args[ 6 ]; 374 | 375 | var deltaAngle = aEndAngle - aStartAngle; 376 | var angle; 377 | 378 | for ( j = 0; j <= divisions; j ++ ) { 379 | t = j / divisions; 380 | 381 | if(deltaAngle === -Math.PI*2) { 382 | deltaAngle = Math.PI*2; 383 | } 384 | 385 | if(deltaAngle < 0) { 386 | deltaAngle += Math.PI*2; 387 | } 388 | 389 | if(deltaAngle > Math.PI*2) { 390 | deltaAngle -= Math.PI*2; 391 | } 392 | 393 | if ( aClockwise ) { 394 | // sin(pi) and sin(0) are the same 395 | // So we have to special case for full circles 396 | if(deltaAngle === Math.PI*2) { 397 | deltaAngle = 0; 398 | } 399 | 400 | angle = aEndAngle + ( 1 - t ) * ( Math.PI * 2 - deltaAngle ); 401 | } else { 402 | angle = aStartAngle + t * deltaAngle; 403 | } 404 | 405 | var tx = aX + xRadius * Math.cos( angle ); 406 | var ty = aY + yRadius * Math.sin( angle ); 407 | 408 | points.push( new Point( tx, ty ) ); 409 | 410 | } 411 | 412 | break; 413 | 414 | } // end switch 415 | } 416 | 417 | if(this.closed) { 418 | points.push( points[ 0 ] ); 419 | } 420 | 421 | // this.pointsCache[divisions] = points; 422 | return points; 423 | } 424 | , toPoly: function(scale, divisions) { 425 | return this.getPoints(divisions).map(function(p) { 426 | return {X: p.x*scale, Y: p.y*scale}; 427 | }); 428 | } 429 | , fromPoly: function(poly, scale) { 430 | scale = 1/scale; 431 | 432 | this.moveTo(poly[0].X*scale, poly[0].Y*scale); 433 | 434 | for(var i=1,l=poly.length; i < l; ++i) { 435 | this.lineTo(poly[i].X*scale, poly[i].Y*scale); 436 | } 437 | 438 | this.close(); 439 | // todo: close properly (closePath()) 440 | // this.lineTo(poly[0].X*scale, poly[0].Y*scale); 441 | return this; 442 | } 443 | , close: function() { 444 | if(this.isClosed()) return; 445 | 446 | var curStart = this.actions[0].args; 447 | this.lineTo.apply(this, curStart); 448 | } 449 | , reverse: function() { 450 | var result = new SubPath(); 451 | var pts = this.getPoints().reverse(); 452 | if(pts.length == 0) return result; 453 | 454 | result.moveTo(pts[0].x, pts[0].y); 455 | for(var i=1,l=pts.length; i < l; ++i) { 456 | result.lineTo(pts[i].x, pts[i].y); 457 | } 458 | return result; 459 | } 460 | }; 461 | 462 | 463 | // Bezier Curves formulas obtained from 464 | // http://en.wikipedia.org/wiki/B%C3%A9zier_curve 465 | 466 | // Quad Bezier Functions 467 | function b2p0 ( t, p ) { 468 | var k = 1 - t; 469 | return k * k * p; 470 | } 471 | 472 | function b2p1 ( t, p ) { 473 | return 2 * ( 1 - t ) * t * p; 474 | } 475 | 476 | function b2p2 ( t, p ) { 477 | return t * t * p; 478 | } 479 | 480 | function b2 ( t, p0, p1, p2 ) { 481 | return b2p0( t, p0 ) + b2p1( t, p1 ) + b2p2( t, p2 ); 482 | } 483 | 484 | // Cubic Bezier Functions 485 | function b3p0 ( t, p ) { 486 | var k = 1 - t; 487 | return k * k * k * p; 488 | } 489 | 490 | function b3p1 ( t, p ) { 491 | var k = 1 - t; 492 | return 3 * k * k * t * p; 493 | } 494 | 495 | function b3p2 ( t, p ) { 496 | var k = 1 - t; 497 | return 3 * k * t * t * p; 498 | } 499 | 500 | function b3p3 ( t, p ) { 501 | return t * t * t * p; 502 | } 503 | 504 | function b3 ( t, p0, p1, p2, p3 ) { 505 | return b3p0( t, p0 ) + b3p1( t, p1 ) + b3p2( t, p2 ) + b3p3( t, p3 ); 506 | } 507 | -------------------------------------------------------------------------------- /lib/path.js: -------------------------------------------------------------------------------- 1 | module.exports = Path; 2 | 3 | var SubPath = require('./subpath') 4 | , ClipperLib = require('./clipper') 5 | , utils = require('./utils') 6 | , Point = require('./math/point') 7 | 8 | function Path() { 9 | this.subPaths = []; 10 | } 11 | 12 | Path.actions = SubPath.actions; 13 | 14 | Path.prototype = { 15 | clone: function() { 16 | var copy = new Path(); 17 | copy.subPaths = this.subPaths.slice(0); 18 | return copy; 19 | } 20 | , moveTo: function(x,y) { 21 | var subPath = new SubPath(); 22 | subPath.moveTo(x,y); 23 | this.subPaths.push(subPath); 24 | this.current = subPath; 25 | } 26 | , _ensure: function(x,y) { 27 | if(this.subPaths.length === 0) { 28 | this.moveTo(x,y); 29 | } 30 | } 31 | 32 | , close: function() { 33 | if(!this.current) return false; 34 | this.current.close(); 35 | } 36 | 37 | /* 38 | * Pass all curves straight through 39 | * */ 40 | , lineTo: function(x,y) { 41 | this._ensure(x,y); 42 | this.current.lineTo.apply(this.current, arguments); 43 | } 44 | , arc: function(x, y, rad, 45 | astart, aend, ccw) { 46 | this.ellipse(x,y,rad,rad,astart,aend,ccw); 47 | } 48 | , ellipse: function(x, y, xrad, yrad, 49 | astart, aend, ccw) { 50 | 51 | var points = utils.arcToPoints(x, y, 52 | astart, 53 | aend, 54 | xrad); 55 | 56 | // this._ensure(points.start.x, points.start.y); 57 | 58 | if(!this.current || !utils.samePos(this.current.lastPoint(), points.start)) { 59 | this.lineTo(points.start.x, points.start.y); 60 | } 61 | 62 | this.current.ellipse.apply(this.current, arguments); 63 | } 64 | , quadraticCurveTo: function() { 65 | this.current.quadraticCurveTo.apply(this.current, arguments); 66 | } 67 | , bezierCurveTo: function() { 68 | this.current.bezierCurveTo.apply(this.current, arguments); 69 | } 70 | , rect: function(x,y,w,h) { 71 | this.moveTo(x,y); 72 | this.lineTo(x+w,y); 73 | this.lineTo(x+w,y+h); 74 | this.lineTo(x,y+h); 75 | this.lineTo(x,y); 76 | } 77 | 78 | , toPolys: function(scale,divisions) { 79 | if(!scale) throw 'NO SCALE!'; 80 | 81 | return this.subPaths.map(function(subPath) { 82 | return subPath.toPoly(scale,divisions); 83 | }); 84 | } 85 | , fromPolys: function(polygons, scale) { 86 | if(!scale) throw 'NO SCALE!'; 87 | 88 | this.subPaths = []; 89 | 90 | for(var i=0,l=polygons.length; i < l; ++i) { 91 | var subPath = new SubPath(); 92 | subPath.fromPoly(polygons[i], scale); 93 | this.subPaths.push(subPath); 94 | this.current = subPath; 95 | } 96 | 97 | return this; 98 | } 99 | , clip: function(clipRegion, clipType, divisions) { 100 | if(!clipRegion) return this; 101 | 102 | clipType = clipType || 0; 103 | 104 | var scale = 1000; 105 | 106 | // this.close(); 107 | // clipRegion.close(); 108 | 109 | var subjPolys = this.toPolys(scale, divisions); 110 | var clipPolys = clipRegion.toPolys(scale); 111 | 112 | // Clean both 113 | // var subjPolys = ClipperLib.Clipper.CleanPolygons(subjPolys, 1); 114 | // var clipPolys = ClipperLib.Clipper.CleanPolygons(clipPolys, 1); 115 | 116 | // var subjPolys = ClipperLib.Clipper.SimplifyPolygons(subjPolys, ClipperLib.PolyFillType.pftNonZero); 117 | 118 | // var clipPolys = ClipperLib.Clipper.SimplifyPolygons(clipPolys, ClipperLib.PolyFillType.pftNonZero); 119 | 120 | var cpr = new ClipperLib.Clipper(); 121 | // cpr.PreserveCollinear = true; 122 | // cpr.ReverseSolution = true; 123 | 124 | cpr.AddPaths(subjPolys, ClipperLib.PolyType.ptSubject,true); 125 | cpr.AddPaths(clipPolys, ClipperLib.PolyType.ptClip, true); 126 | 127 | var clipped = []; 128 | cpr.Execute(clipType, clipped); 129 | 130 | var tmp; 131 | 132 | var path = new Path(); 133 | path.fromPolys(clipped, scale); 134 | return path; 135 | } 136 | 137 | , translate: function(x,y) { 138 | var result = new Path(); 139 | this.subPaths.forEach(function(subPath) { 140 | var pts = subPath.getPoints(); 141 | result.moveTo(pts[0].x+x, pts[0].y+y); 142 | pts.slice(1).forEach(function(p) { 143 | // p.x += x; 144 | // p.y += y; 145 | result.lineTo(p.x+x, p.y+y); 146 | }); 147 | }); 148 | return result; 149 | } 150 | 151 | , clipToBounds: function(bounds) { 152 | var result = new Path(); 153 | var p0 = new Point(0,0,0); 154 | var p0u = p0.clone(); 155 | var p1u; 156 | 157 | this.subPaths.forEach(function(subPath) { 158 | var pts = subPath.getPoints(); 159 | 160 | pts.forEach(function(p1, i) { 161 | p1 = p1.clone(); 162 | p1u = p1.clone(); 163 | 164 | // if(p1.y < bounds.top && p0.y < bounds.top) { 165 | // return; 166 | // } 167 | // if(p1.x > bounds.right && p0.x > bounds.right) { 168 | // return; 169 | // } 170 | 171 | if(p1.y < bounds.top) { 172 | var m = (p1.x - p0.x) / (p1.y - p0.y); 173 | p1.x += (m * (bounds.top - p1.y)) || 0; 174 | p1.y = bounds.top; 175 | 176 | 177 | } 178 | else if(p0u.y < bounds.top) { 179 | var m = (p1.x - p0u.x) / (p1.y - p0u.y); 180 | var x = (m * (bounds.top - p1.y)) || 0; 181 | 182 | result.moveTo(p1.x+x, bounds.top); 183 | } 184 | 185 | // if(p1.x < bounds.left) { 186 | // var m = (p1.y - p0.y) / (p1.x - p0.x); 187 | // p1.y += m * (bounds.left - p1.x); 188 | // p1.x = bounds.left; 189 | // } 190 | // else if(p0u.x < bounds.left) { 191 | // var m = (p1.y - p0u.y) / (p1.x - p0u.x); 192 | // var y = m * (bounds.left - p1.x); 193 | // // result.moveTo(bounds.left, bounds.top); 194 | // } 195 | 196 | if(p1.x > bounds.right) { 197 | var m = (p1.y - p0.y) / (p1.x - p0.x); 198 | p1.y += m * (bounds.right - p1.x); 199 | p1.x = bounds.right; 200 | 201 | } 202 | else if(p0u.x > bounds.right) { 203 | 204 | var m = (p1.y - p0u.y) / (p1.x - p0u.x); 205 | var y = m * (bounds.right - p1.x); 206 | 207 | // result.moveTo(bounds.right, p1.y-y); 208 | } 209 | 210 | 211 | if(i === 0) 212 | result.moveTo(p1.x, p1.y); 213 | else 214 | result.lineTo(p1.x, p1.y); 215 | 216 | p0 = p1; 217 | p0u = p1u; 218 | }); 219 | }); 220 | 221 | return result; 222 | } 223 | 224 | , simplify: function(windingRule, divisions) { 225 | 226 | // Special case for single ellipse 227 | // just change the radius. 228 | // if(this.is('ellipse')) { 229 | // var result = new Path(); 230 | // var args = this.subPaths[0].actions[1].args; 231 | 232 | // result.ellipse( 233 | // args[0], 234 | // args[1], 235 | // args[2], 236 | // args[3], 237 | // args[4], 238 | // args[5], 239 | // args[6] 240 | // ); 241 | 242 | // return result; 243 | // } 244 | 245 | 246 | var scale = 1000; 247 | var polys = this.toPolys(scale, divisions); 248 | var type = ClipperLib.PolyFillType.pftNonZero; 249 | 250 | if(windingRule === 'evenodd') { 251 | type = ClipperLib.PolyFillType.pftEvenOdd; 252 | } 253 | 254 | polys = ClipperLib.Clipper.SimplifyPolygons(polys, type); 255 | 256 | var result = new Path(); 257 | result.fromPolys(polys, scale); 258 | 259 | return result; 260 | } 261 | 262 | , is: function(action) { 263 | if(this.subPaths.length == 1 264 | && this.subPaths[0].actions.length == 2 265 | && this.subPaths[0].actions[1].action === action) { 266 | return true; 267 | } 268 | 269 | return false; 270 | } 271 | 272 | , offset: function(delta, divisions) { 273 | if(delta === 0) { 274 | return this; 275 | } 276 | 277 | // Special case for single ellipse 278 | // just change the radius. 279 | if(this.is('ellipse')) { 280 | var result = new Path(); 281 | var args = this.subPaths[0].actions[1].args; 282 | 283 | if(args[2] + delta < 0) 284 | return false; 285 | 286 | result.ellipse( 287 | args[0], 288 | args[1], 289 | args[2] + delta, 290 | args[3] + delta, 291 | args[4], 292 | args[5], 293 | args[6] 294 | ); 295 | 296 | return result; 297 | } 298 | 299 | var scale = 1000; 300 | var cleandelta = 0.1; 301 | 302 | var polygons = this.toPolys(scale, divisions); 303 | 304 | // offset 305 | var miterLimit = 1000*scale; 306 | 307 | var co = new ClipperLib.ClipperOffset(); 308 | // co.PreserveCollinear = true; 309 | // co.ReverseSolution = true; 310 | 311 | co.AddPaths(polygons, 312 | ClipperLib.JoinType.jtMiter, 313 | ClipperLib.EndType.etClosedPolygon); 314 | 315 | var solution = []; 316 | 317 | try { 318 | co.Execute(solution, delta*scale); 319 | } 320 | catch(err) { 321 | return false; 322 | } 323 | 324 | 325 | if(!solution || solution.length === 0 326 | || solution[0].length === 0) return false; 327 | 328 | var result = new Path(); 329 | result.fromPolys(solution, scale); 330 | 331 | result.close(); // Not sure why I need to do this now 332 | return result; 333 | } 334 | 335 | , ramp: function(depth) { 336 | } 337 | 338 | , addPath: function(path2) { 339 | this.subPaths = this.subPaths.concat(path2.subPaths); 340 | } 341 | 342 | , estimateMaxOffset: function(divisions) { 343 | var bounds = this.getBounds(); 344 | var width = Math.abs(bounds.right - bounds.left) 345 | var height = Math.abs(bounds.bottom - bounds.top) 346 | var lt = Math.min(width, height) / 2; 347 | 348 | var gt = 0; 349 | 350 | for(var i = 0; i < 5; ++i) { 351 | var test = gt+(lt-gt)/2; 352 | var offset = this.offset(-test,3); 353 | 354 | if(offset) { 355 | gt = test 356 | } 357 | else { 358 | lt = test; 359 | } 360 | } 361 | 362 | return {lt: lt, gt: gt}; 363 | } 364 | 365 | , fillPath: function(diameter, divisions) { 366 | var result = new Path(); 367 | var overlap = Math.sin(Math.PI/4); 368 | 369 | // this.subPaths.forEach(function(sp) { 370 | // var path = sp.toPath(); 371 | var path = this; 372 | 373 | var max = path.estimateMaxOffset(5).lt; 374 | max -= diameter/2; 375 | 376 | for(var i = -max; i < -diameter/2; i += diameter*overlap) { 377 | var offsetPath = path.offset(i, divisions); 378 | if(!offsetPath) break; 379 | offsetPath = offsetPath.reverse(); 380 | result.addPath(offsetPath); 381 | } 382 | 383 | // Finishing pass 384 | var finish = path.offset( -diameter/2, divisions ); 385 | if(finish) 386 | result.addPath( finish.reverse() ); 387 | // }); 388 | 389 | return result; 390 | } 391 | 392 | , connectEnds: function(diameter) { 393 | for(var i=this.subPaths.length-1; i > 0; --i) { 394 | var sp1 = this.subPaths[i-1]; 395 | var sp2 = this.subPaths[i]; 396 | 397 | var p1 = sp1.lastPoint(); 398 | var nearest = sp2.nearestPoint(p1); 399 | var p2 = nearest.point; 400 | 401 | if(nearest.distance < diameter*2) { 402 | sp2 = sp2.shift(nearest.i); 403 | sp1.lineTo(p2.x, p2.y); 404 | sp2.actions[0].action = Path.actions.LINE_TO; 405 | sp1.actions = sp1.actions.concat( sp2.actions ); 406 | this.subPaths.splice(i,1); 407 | } 408 | } 409 | 410 | return this; 411 | } 412 | 413 | , reverse: function() { 414 | if(this.is('ellipse')) { 415 | var result = new Path(); 416 | var args = this.subPaths[0].actions[1].args; 417 | 418 | result.ellipse( 419 | args[0], 420 | args[1], 421 | args[2], 422 | args[3], 423 | args[5], // end as start 424 | args[4], // start as end 425 | !args[6] // invert ccw 426 | ); 427 | 428 | return result; 429 | } 430 | 431 | var result = new Path(); 432 | 433 | result.subPaths = this.subPaths.map(function(sp) { 434 | return sp.reverse(); 435 | }).reverse(); 436 | 437 | return result; 438 | } 439 | 440 | , sort: function() { 441 | if(this.subPaths.length === 0) return this; 442 | 443 | var copy = new Path(); 444 | 445 | var p0 = this.subPaths[0].lastPoint(); 446 | 447 | copy.subPaths = this.subPaths.sort(function(a, b) { 448 | var p1 = a.lastPoint(); 449 | var p2 = b.firstPoint(); 450 | 451 | var d1 = Point.distance(p1,p0); 452 | var d2 = Point.distance(p2,p0); 453 | 454 | // Moving target 455 | p0 = b.lastPoint(); 456 | 457 | if(d1 < d2) return -1; 458 | if(d1 > d2) return 1; 459 | 460 | return 0; 461 | }); 462 | 463 | return copy; 464 | } 465 | 466 | , firstPoint: function() { 467 | if(!this.current) return false; 468 | return this.subPaths[0].firstPoint(); 469 | } 470 | 471 | , lastPoint: function() { 472 | if(!this.current) return false; 473 | return this.subPaths[this.subPaths.length-1].lastPoint(); 474 | } 475 | 476 | , getPoints: function(divisions) { 477 | var pts = []; 478 | this.subPaths.forEach(function(sp) { 479 | pts.push.apply(pts, sp.getPoints(divisions)); 480 | }); 481 | return pts; 482 | } 483 | 484 | , getBounds: function() { 485 | var pts = this.getPoints(); 486 | var p0 = this.firstPoint(); 487 | var res = { 488 | left: p0.x, 489 | top: p0.y, 490 | right: p0.x, 491 | bottom: p0.y 492 | }; 493 | 494 | pts.forEach(function(p) { 495 | res.left = Math.min(res.left, p.x); 496 | res.top = Math.min(res.top, p.y); 497 | res.right = Math.max(res.right, p.x); 498 | res.bottom = Math.max(res.bottom, p.y); 499 | }); 500 | 501 | return res; 502 | } 503 | } 504 | 505 | var NON_ZERO = ClipperLib.PolyFillType.pftNonZero; 506 | var EVEN_ODD = ClipperLib.PolyFillType.pftEvenOdd; 507 | -------------------------------------------------------------------------------- /examples/support/highlight.min.js: -------------------------------------------------------------------------------- 1 | var hljs=new function(){function l(o){return o.replace(/&/gm,"&").replace(//gm,">")}function b(p){for(var o=p.firstChild;o;o=o.nextSibling){if(o.nodeName=="CODE"){return o}if(!(o.nodeType==3&&o.nodeValue.match(/\s+/))){break}}}function h(p,o){return Array.prototype.map.call(p.childNodes,function(q){if(q.nodeType==3){return o?q.nodeValue.replace(/\n/g,""):q.nodeValue}if(q.nodeName=="BR"){return"\n"}return h(q,o)}).join("")}function a(q){var p=(q.className+" "+q.parentNode.className).split(/\s+/);p=p.map(function(r){return r.replace(/^language-/,"")});for(var o=0;o