gcanvas
Version:
A Canvas API implementation that generates Gcode
791 lines (674 loc) • 20 kB
JavaScript
module.exports = GCanvas;
var Path = require('./path')
, Motion = require('./motion')
, GcodeDriver = require('./drivers/gcode')
, NullDriver = require('./drivers/null')
, Point = require('./math/point')
, Matrix = require('./math/matrix')
, Font = require('./font')
, parseFont = require('./parsefont')
, utils = require('./utils');
function GCanvas(driver, width, height) {
var self = this;
this.canvas = {
width: width,
height: height,
getContext: function() {
return self;
}
};
if(driver === null) {
driver = new NullDriver();
}
this.font = "7pt Helvetiker";
this.matrix = new Matrix();
this.rotation = 0;
this.depth = 0;
this.depthOfCut = 0;
this.top = 0;
this.retract = 0;
this.align = 'center';
this.driver = driver || new GcodeDriver();
this.driver.src = this;
this.stack = [];
this.motion = new Motion(this);
this.filters = [];
this.precision = 20;
this.ramping = true;
this.strokeStyle = '#000000';
this.fillStyle = '#000000';
if(this.driver.init)
this.driver.init();
this.beginPath();
}
GCanvas.prototype = {
save: function() {
this.stack.push({
matrix: this.matrix.clone(),
font: this.font,
speed: this.speed,
feed: this.feed,
depth: this.depth,
depthOfCut: this.depthOfCut,
toolDiameter: this.toolDiameter,
align: this.align,
top: this.top,
filters: this.filters.slice(),
strokeStyle: this.strokeStyle,
fillStyle: this.fillStyle
});
}
, restore: function() {
var prev = this.stack.pop();
for(var k in prev) {
if(prev.hasOwnProperty(k)) {
this[k] = prev[k];
}
}
}
, filter: function(fn) {
this.filters.push(fn);
}
, map: function(from, to) {
if(arguments.length == 1) {
to = from;
from = 'xyza';
}
from = from.split('');
to = to.split('');
this.filter(function(p) {
var tmp = {};
var negative = false;
var i = 0;
to.forEach(function(d) {
if(d == '-') {
negative = true;
}
else {
tmp[d] = p[ from[i] ];
if(tmp[d] != undefined && negative) {
tmp[d] = -tmp[d];
}
negative = false;
i++;
}
});
return tmp;
});
}
, beginPath: function() {
// this.prevsubPaths = this.subPaths;
this.path = new Path();
// this.subPaths = [this.path];
}
, _restorePath: function() {
this.subPaths = this.prevsubPaths;
this.path = this.subPaths[this.subPaths.length-1] || new Path();
}
, transform: function(a, b, c, d, e, f) {
this.matrix = this.matrix.concat(
new Matrix(a, b, c, d, e, f)
);
}
, setTransform: function(a, b, c, d, e, f) {
this.matrix = new Matrix(a, b, c, d, e, f);
}
, resetTransform: function() {
this.matrix = new Matrix();
}
, rotate: function(angle) {
this.matrix = this.matrix.rotate(angle);
}
, translate: function(x,y) {
this.matrix = this.matrix.translate(x,y);
}
, scale: function(x,y) {
this.matrix = this.matrix.scale(x,y);
}
// TODO: clean up
, _transformPoint: function(a, i) {
i = i || 0;
if(a.length) {
var v = new Point(a[i], a[i+1]);
v = this.matrix.transformPoint(v);
a[i] = v.x;
a[i+1] = v.y;
}
else if(a.x !== undefined) {
var v = new Point(a.x, a.y);
v = this.matrix.transformPoint(v);
a.x = v.x;
a.y = v.y;
}
}
, _ensurePath: function(x,y) {
if(this.path.subPaths.length === 0) {
this.path.moveTo(x,y);
}
}
, moveTo: function(x,y) {
this._transformPoint(arguments);
this.path.moveTo(x,y);
}
, lineTo: function(x,y) {
this._transformPoint(arguments);
this._ensurePath(x,y);
this.path.lineTo(x,y);
}
, arcTo: function(x1, y1, x2, y2, radius) {
this._transformPoint(arguments,0);
this._transformPoint(arguments,2);
this._ensurePath(x1,y1);
var p0 = this.path.lastPoint();
var p1 = new Point(x1,y1);
var p2 = new Point(x2,y2);
var v01 = p0.sub(p1);
var v21 = p2.sub(p1);
// sin(A - B) = sin(A) * cos(B) - sin(B) * cos(A)
var cross = v01.x * v21.y - v01.y * v21.x;
if (Math.abs(cross) < 1E-10) {
// on one line
this.lineTo(x1,y1);
return;
}
var d01 = v01.magnitude();
var d21 = v21.magnitude();
var angle = (Math.PI - Math.abs(Math.asin(cross / (d01 * d21)))) / 2;
var span = radius * Math.tan(angle);
var rate = span / d01;
var startPoint = new Point(
p1.x + v01.x * rate,
p1.y + v01.y * rate
);
rate = span / d21;
var endPoint = new Point(
p1.x + v21.x * rate,
p1.y + v21.y * rate
);
var midPoint = new Point(
(startPoint.x + endPoint.x) / 2,
(startPoint.y + endPoint.y) / 2
);
var vm1 = midPoint.sub(p1);
var dm1 = vm1.magnitude();
var d = Math.sqrt(radius*radius + span*span);
var centerPoint = new Point();
rate = d / dm1;
centerPoint.x = p1.x + vm1.x * rate;
centerPoint.y = p1.y + vm1.y * rate;
var arc = utils.pointsToArc(centerPoint, startPoint, endPoint);
this.path.lineTo(startPoint.x, startPoint.y);
this.path.arc(centerPoint.x, centerPoint.y, arc.radius, arc.start, arc.end, cross > 0);
}
, arc: function (x, y, radius,
aStartAngle,
aEndAngle,
aClockwise ) {
// In the conversion to points we lose the distinction
// between 0 and pi2 so we must optimize out 0 here
// or else they will be treated as full circles.
if(aStartAngle - aEndAngle === 0) {
return;
}
// See portal2 example
if(aEndAngle-aStartAngle === -Math.PI*2)
aEndAngle = Math.PI*2;
var center = new Point(x, y);
var points = utils.arcToPoints(x, y,
aStartAngle,
aEndAngle,
radius);
this._transformPoint(center);
this._transformPoint(points.start);
this._transformPoint(points.end);
var res = utils.pointsToArc(center,
points.start,
points.end);
// this._ensurePath(points.start.x, points.start.y);
this.path.arc(center.x, center.y, res.radius, res.start, res.end, aClockwise);
// var tmp = new Path();
// tmp.moveTo(points.start.x, points.start.y);
// tmp.arc(center.x, center.y, radius, res.start, res.end, aClockwise);
// tmp.getPoints(40).forEach(function(p) {
// this.lineTo(p.x,p.y);
// },this);
}
, circle: function(x, y, rad, ccw) {
this.arc(x, y, rad, 0, Math.PI*2, ccw);
}
, bezierCurveTo: function( aCP1x, aCP1y,
aCP2x, aCP2y,
aX, aY ) {
this._transformPoint(arguments, 0);
this._transformPoint(arguments, 2);
this._transformPoint(arguments, 4);
this.path.bezierCurveTo.apply(this.path, arguments);
}
, quadraticCurveTo: function( aCPx, aCPy, aX, aY ) {
this._transformPoint(arguments, 0);
this._transformPoint(arguments, 2);
this.path.quadraticCurveTo.apply(this.path, arguments);
}
, clip: function() {
this.clipRegion = this.path;
}
, rect: function(x,y,w,h) {
this.moveTo(x,y);
this.lineTo(x+w,y);
this.lineTo(x+w,y+h);
this.lineTo(x,y+h);
this.closePath();
}
, fillRect: function(x,y,w,h,depth) {
this.save();
this.beginPath();
this.depth = depth || this.depth;
this.rect(x,y,w,h);
this.fill();
this.restore();
}
, fillCircle: function(x,y,rad,depth) {
this.save();
this.beginPath();
this.depth = depth || this.depth;
this.circle(x,y,rad);
this.fill();
this.restore();
}
, clone: function(driver) {
var copy = new GCanvas(driver);
this.save();
copy.stack[0] = this.stack[0];
copy.restore();
return copy;
}
, measureText: function(text) {
var copy = this.clone(null);
copy.text(text);
var b = copy.path.getBounds();
b.width = b.right - b.left;
b.height = b.bottom - b.top;
return b;
}
, _isOpaque: function(color) {
if(color == 'transparent') return false;
if(color == 'none') return false;
if(typeof color == 'string' &&
color.match(/rgba\((?:.*,){3}[0\.]*\)/)) {
return false;
}
return true;
}
, stroke: function(align, depth) {
if(!this._isOpaque(this.strokeStyle)) return;
this.save();
if(typeof align === 'number') {
depth = align;
align = undefined;
}
if(depth) {
this.depth = depth;
}
var offset = 0;
align = align || this.align;
if(align === 'outer') {
offset = this.toolDiameter/2;
}
if(align === 'inner') {
offset = -this.toolDiameter/2;
}
var path = this.path;
if(align != 'center') {
path = path.simplify('evenodd', this.precision);
path = path.offset(offset);
}
if(path.subPaths)
path.subPaths.forEach(function(subPath) {
// Climb milling
if(align == 'inner') {
subPath = subPath.reverse();
}
this._layer(subPath, function(z) {
this.motion.followPath(subPath,z);
});
}, this);
this.motion.retract();
this.restore();
}
, fill: function(windingRule, depth) {
if(!this._isOpaque(this.fillStyle)) return;
this.save();
if(typeof windingRule === 'number') {
depth = windingRule;
windingRule = 'nonzero';
}
if(depth) {
this.depth = depth;
}
if(!this.toolDiameter) {
throw 'You must set context.toolDiameter to use fill()'
}
var path = this.path;
path = path.simplify(windingRule, this.precision);
path = path.clip(this.clipRegion, 0, this.precision);
path = path.fillPath(this.toolDiameter, this.precision);
var motion = this.motion;
if(path.subPaths)
path.subPaths.forEach(function(subPath) {
this._layer(subPath, function(z) {
this.motion.followPath(subPath, z);
});
}, this);
this.motion.retract();
this.restore();
}
// , outerThread: function(dmin, dmaj, depth, pitch, ccw) {
// this.align = 'outer';
// this.beginPath();
// if(ccw) {
// this.moveTo(dmin/2,depth);
// this.lineTo(dmaj/2,depth);
// this.lineTo(dmaj/2,0);
// }
// else {
// this.moveTo(dmaj/2,0);
// this.lineTo(dmaj/2,depth);
// this.lineTo(dmin/2,depth);
// }
// this.spiral(pitch, ccw);
// }
, thread: function(x, y, attack, dmin, dmaj, pitch, start, length, ccw, virtual) {
this.save();
this.beginPath();
this.rect(dmin/2,start,dmaj/2-dmin/2,length);
this.lathe(x,y,attack, pitch, ccw, virtual);
this.restore();
}
, threadMill: function(x, y, attack, dmin, dmaj, pitch, start, length, ccw) {
return this.thread(x, y, attack, dmin, dmaj, pitch, start, length, ccw, true);
}
, latheMill: function(x, y, attack, pitch, ccw) {
return this.lathe(x, y, attack, pitch, ccw, true);
}
, lathe: function(x, y, attack, pitch, ccw, virtual) {
this.save();
this.filter(function(p) {
if(p.x != undefined)
p.x += x;
if(p.y != undefined)
p.y += y;
});
var inPath = this.path;
var toolR = this.toolDiameter/2 || 0;
if(virtual && toolR) {
if(attack == 'inner') {
inPath = inPath.translate(-toolR,0);
}
if(attack == 'outer') {
inPath = inPath.translate(toolR,0);
}
}
var bounds = inPath.getBounds(); // Find bounds
var height = bounds.bottom-bounds.top;
var width = bounds.right-bounds.left;
inPath = inPath.simplify();
var quad = new Path();
quad.rect(0,0,bounds.right,bounds.bottom);
inPath = inPath.clip(quad);
var path = new Path();
var s = this.depthOfCut || 1000;
var top = virtual ? this.top : 0;
// Build waterline path
// for given attack.
// Each subpath is a layer.
if(attack === 'face') {
for(var i=s; i <= bounds.bottom; i += s) {
var clip = new Path();
clip.rect(0, top, width, i);
var layer = inPath.clip(clip,0);
layer.subPaths = layer.subPaths.map(function(sp) {
sp = sp.shiftToNearest(0,0);
return sp;
});
path.addPath(layer);
}
}
else if(attack === 'inner') {
for(var i=bounds.left; i <= bounds.right; i += s) {
var clip = new Path();
clip.rect(0,top,i,height-top);
var layer = inPath.clip(clip,0);
layer.subPaths = layer.subPaths.map(function(sp) {
sp = sp.shiftToNearest(0,0);
return sp;
});
path.addPath(layer);
}
}
else if(attack === 'outer') {
for(var i=bounds.right;; i -= s) {
i = Math.max(i, bounds.left);
var clip = new Path();
clip.rect(i,top,bounds.right+10,height-top);
var layer = inPath.clip(clip,0);
layer.subPaths = layer.subPaths.map(function(sp) {
sp = sp.shiftToNearest(bounds.right,0);
return sp;
});
path.addPath(layer);
if(i == bounds.left) break;
}
}
// path.addPath(inPath);
var inner = this.align == 'inner';
pitch = pitch || this.toolDiameter || 1;
var motion = this.motion;
var driver = this.driver;
// var range = (attack == 'face') ? height : width;
// var depthOfCut = this.depthOfCut || range;
// var offset = range;
var a = 0;
path.subPaths.forEach(function(subPath) {
var spiralAngle = 0;
driver.zero({a:0});
var a = 0;
var pts = subPath.getPoints();
if(!ccw) {
// CCW simply makes the path bottom-out.
// The physical circular motion is
// always determined by the attack
// to ensure climb milling.
pts = pts.reverse();
}
var p0u = motion.position;
var p0 = p0u.clone();
pts.forEach(function(p1,i) {
p1 = p1.clone();
p1u = p1.clone();
var xo = p1u.x-p0u.x;
var yo = p1u.y-p0u.y;
var dist = Math.sqrt(xo*xo + yo*yo);
if(virtual) {
var r0 = p0.x;
var z0 = p0.y;
var r1 = p1.x;
var z1 = p1.y;
var loops = dist/pitch;
// Entering and exiting are a part of the path
// touching the bounds as a consequence
// of the waterline clipping.
// We just have to detect them and
// behave a little differently.
if(attack == 'inner' && p0.x <= bounds.left) {
// Inner Entering
var x = (r1) * Math.sin(spiralAngle);
var y = (r1) * Math.cos(spiralAngle);
if(i === 0)
motion.rapid({x: x, y: y});
else
motion.linear({x: x, y: y});
motion.rapid({z: z1});
}
else if(attack == 'inner' && p1.x <= bounds.left) {
// Inner Exiting
var x = (r1) * Math.sin(spiralAngle);
var y = (r1) * Math.cos(spiralAngle);
motion.rapid({x: x, y: y});
motion.rapid({z: z1});
}
else if(attack == 'outer' && p0.x >= bounds.right) {
// Outer Entering
var x = (r1) * Math.cos(spiralAngle);
var y = (r1) * Math.sin(spiralAngle);
motion.linear({x: x, y: y});
}
else if(attack == 'outer' && p1.x >= bounds.right) {
// Outer Exiting
var x = (r1) * Math.cos(spiralAngle);
var y = (r1) * Math.sin(spiralAngle);
motion.rapid({x: x, y: y});
motion.rapid({z: z1});
}
else if(attack == 'face' && p0.y <= bounds.top) {
// Face Entering
var x = (r1) * Math.cos(spiralAngle);
var y = (r1) * Math.sin(spiralAngle);
motion.rapid({x: x, y: y});
motion.linear({z: z1});
}
else if(attack == 'face' && p1.y <= bounds.top) {
// Outer Exiting
var x = (r1) * Math.cos(spiralAngle);
var y = (r1) * Math.sin(spiralAngle);
motion.rapid({z: z1});
motion.rapid({x: x, y: y});
}
else {
spiralAngle = utils.spiral(this.precision, r0, r1, loops,
spiralAngle, attack=='inner',
function(x,y,t) {
var z = z0+(z1-z0)*t;
motion.linear({
x: x,
y: y,
z: z
});
});
}
}
else {
if(attack == 'inner' && p1u.x <= bounds.left) {
motion.rapid({x: p1.x, y: p1.y});
}
else if(attack == 'outer' && p1.x >= bounds.right) {
motion.rapid({x: p1.x, y: p1.y});
}
else if(attack == 'face' && p1.y <= bounds.top) {
motion.rapid({x: p1.x, y: p1.y});
}
else {
a += dist/pitch*360;
motion.linear({x: p1.x, y: p1.y, a: a});
}
}
p0 = p1.clone();
p0u = p1u.clone();
}, this);
// This is important to be done after
// every subpath, for ctx.top
if(virtual) {
motion.rapid({z: 0});
}
}, this);
driver.zero({a:0});
// Always finish with an arc?
// motion.arcCW({
// x: 0,
// y: 0,
// i:-p1.x,
// j:-p1.y
// });
this.restore();
}
, peckDrill: function(x, y, depth, peck) {
this._transformPoint(arguments);
if(arguments.length <= 2) {
depth = arguments[0];
peck = arguments[1];
x = 0;
y = 0;
}
var prevZ = this.top;
var mtn = this.motion;
peck = peck || this.toolDiameter;
// We need a tad bit of play
// to avoid rapid plunging into
// the previous peck's bur
// and to pull out a little further
// than the surface in case it started
// out too close
var tad = peck/2 || depth/10;
mtn.rapid({x:x,y:y});
for(var z=this.top+peck; prevZ < depth; z += peck) {
z = Math.min(z, depth); // Cap to exact depth
mtn.rapid({z:prevZ-tad}); // Rapid to prev depth - tad
mtn.linear({z:z}); // Drill a bit further
mtn.retract();
// mtn.rapid({z:-tad}); // Rapid all the way out
prevZ = z;
}
mtn.retract();
}
, _layer: function(subPath, fn) {
var depthOfCut = this.depthOfCut || this.depth;
if(depthOfCut === 0) {
fn.call(this, -this.top);
return;
}
var invertedZ = this.depth < 0;
if (invertedZ && depthOfCut > 0) {
depthOfCut = -depthOfCut;
}
var steps = Math.ceil(Math.abs(this.depth/depthOfCut));
var offset = -this.top;
while(steps--) {
offset -= depthOfCut;
// Clip to actual depth
if (invertedZ) {
offset = Math.max(offset, this.top+this.depth);
} else {
offset = Math.max(offset, -this.top-this.depth);
}
// Remove the material at this depth
fn.call(this, offset);
}
// Finishing pass
if(this.ramping && subPath.isClosed()) {
fn.call(this, offset);
}
}
, text: function(text, x, y, params) {
var fontProps = parseFont(this.font);
var font = new Font(fontProps);
// this.beginPath();
this.save();
this.translate(x, y);
font.drawText(this, text);
this.restore();
}
, fillText: function(text, x, y, params) {
this.text(text, x, y, params);
this.fill();
}
, strokeText: function(text, x, y, params) {
this.text(text, x, y, params);
this.stroke();
}
, clearRect: function() {}
, closePath: function() {
this.path.close();
}
};
GCanvas.GcodeDriver = GcodeDriver;
var helvetiker = require('./fonts/helvetiker_regular.typeface');
Font.load(helvetiker);