gcanvas
Version:
A Canvas API implementation that generates Gcode
507 lines (394 loc) • 11.7 kB
JavaScript
module.exports = Path;
var SubPath = require('./subpath')
, ClipperLib = require('./clipper')
, utils = require('./utils')
, Point = require('./math/point')
function Path() {
this.subPaths = [];
}
Path.actions = SubPath.actions;
Path.prototype = {
clone: function() {
var copy = new Path();
copy.subPaths = this.subPaths.slice(0);
return copy;
}
, moveTo: function(x,y) {
var subPath = new SubPath();
subPath.moveTo(x,y);
this.subPaths.push(subPath);
this.current = subPath;
}
, _ensure: function(x,y) {
if(this.subPaths.length === 0) {
this.moveTo(x,y);
}
}
, close: function() {
if(!this.current) return false;
this.current.close();
}
/*
* Pass all curves straight through
* */
, lineTo: function(x,y) {
this._ensure(x,y);
this.current.lineTo.apply(this.current, arguments);
}
, arc: function(x, y, rad,
astart, aend, ccw) {
this.ellipse(x,y,rad,rad,astart,aend,ccw);
}
, ellipse: function(x, y, xrad, yrad,
astart, aend, ccw) {
var points = utils.arcToPoints(x, y,
astart,
aend,
xrad);
// this._ensure(points.start.x, points.start.y);
if(!this.current || !utils.samePos(this.current.lastPoint(), points.start)) {
this.lineTo(points.start.x, points.start.y);
}
this.current.ellipse.apply(this.current, arguments);
}
, quadraticCurveTo: function() {
this.current.quadraticCurveTo.apply(this.current, arguments);
}
, bezierCurveTo: function() {
this.current.bezierCurveTo.apply(this.current, arguments);
}
, 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.lineTo(x,y);
}
, toPolys: function(scale,divisions) {
if(!scale) throw 'NO SCALE!';
return this.subPaths.map(function(subPath) {
return subPath.toPoly(scale,divisions);
});
}
, fromPolys: function(polygons, scale) {
if(!scale) throw 'NO SCALE!';
this.subPaths = [];
for(var i=0,l=polygons.length; i < l; ++i) {
var subPath = new SubPath();
subPath.fromPoly(polygons[i], scale);
this.subPaths.push(subPath);
this.current = subPath;
}
return this;
}
, clip: function(clipRegion, clipType, divisions) {
if(!clipRegion) return this;
clipType = clipType || 0;
var scale = 1000;
// this.close();
// clipRegion.close();
var subjPolys = this.toPolys(scale, divisions);
var clipPolys = clipRegion.toPolys(scale);
// Clean both
// var subjPolys = ClipperLib.Clipper.CleanPolygons(subjPolys, 1);
// var clipPolys = ClipperLib.Clipper.CleanPolygons(clipPolys, 1);
// var subjPolys = ClipperLib.Clipper.SimplifyPolygons(subjPolys, ClipperLib.PolyFillType.pftNonZero);
// var clipPolys = ClipperLib.Clipper.SimplifyPolygons(clipPolys, ClipperLib.PolyFillType.pftNonZero);
var cpr = new ClipperLib.Clipper();
// cpr.PreserveCollinear = true;
// cpr.ReverseSolution = true;
cpr.AddPaths(subjPolys, ClipperLib.PolyType.ptSubject,true);
cpr.AddPaths(clipPolys, ClipperLib.PolyType.ptClip, true);
var clipped = [];
cpr.Execute(clipType, clipped);
var tmp;
var path = new Path();
path.fromPolys(clipped, scale);
return path;
}
, translate: function(x,y) {
var result = new Path();
this.subPaths.forEach(function(subPath) {
var pts = subPath.getPoints();
result.moveTo(pts[0].x+x, pts[0].y+y);
pts.slice(1).forEach(function(p) {
// p.x += x;
// p.y += y;
result.lineTo(p.x+x, p.y+y);
});
});
return result;
}
, clipToBounds: function(bounds) {
var result = new Path();
var p0 = new Point(0,0,0);
var p0u = p0.clone();
var p1u;
this.subPaths.forEach(function(subPath) {
var pts = subPath.getPoints();
pts.forEach(function(p1, i) {
p1 = p1.clone();
p1u = p1.clone();
// if(p1.y < bounds.top && p0.y < bounds.top) {
// return;
// }
// if(p1.x > bounds.right && p0.x > bounds.right) {
// return;
// }
if(p1.y < bounds.top) {
var m = (p1.x - p0.x) / (p1.y - p0.y);
p1.x += (m * (bounds.top - p1.y)) || 0;
p1.y = bounds.top;
}
else if(p0u.y < bounds.top) {
var m = (p1.x - p0u.x) / (p1.y - p0u.y);
var x = (m * (bounds.top - p1.y)) || 0;
result.moveTo(p1.x+x, bounds.top);
}
// if(p1.x < bounds.left) {
// var m = (p1.y - p0.y) / (p1.x - p0.x);
// p1.y += m * (bounds.left - p1.x);
// p1.x = bounds.left;
// }
// else if(p0u.x < bounds.left) {
// var m = (p1.y - p0u.y) / (p1.x - p0u.x);
// var y = m * (bounds.left - p1.x);
// // result.moveTo(bounds.left, bounds.top);
// }
if(p1.x > bounds.right) {
var m = (p1.y - p0.y) / (p1.x - p0.x);
p1.y += m * (bounds.right - p1.x);
p1.x = bounds.right;
}
else if(p0u.x > bounds.right) {
var m = (p1.y - p0u.y) / (p1.x - p0u.x);
var y = m * (bounds.right - p1.x);
// result.moveTo(bounds.right, p1.y-y);
}
if(i === 0)
result.moveTo(p1.x, p1.y);
else
result.lineTo(p1.x, p1.y);
p0 = p1;
p0u = p1u;
});
});
return result;
}
, simplify: function(windingRule, divisions) {
// Special case for single ellipse
// just change the radius.
// if(this.is('ellipse')) {
// var result = new Path();
// var args = this.subPaths[0].actions[1].args;
// result.ellipse(
// args[0],
// args[1],
// args[2],
// args[3],
// args[4],
// args[5],
// args[6]
// );
// return result;
// }
var scale = 1000;
var polys = this.toPolys(scale, divisions);
var type = ClipperLib.PolyFillType.pftNonZero;
if(windingRule === 'evenodd') {
type = ClipperLib.PolyFillType.pftEvenOdd;
}
polys = ClipperLib.Clipper.SimplifyPolygons(polys, type);
var result = new Path();
result.fromPolys(polys, scale);
return result;
}
, is: function(action) {
if(this.subPaths.length == 1
&& this.subPaths[0].actions.length == 2
&& this.subPaths[0].actions[1].action === action) {
return true;
}
return false;
}
, offset: function(delta, divisions) {
if(delta === 0) {
return this;
}
// Special case for single ellipse
// just change the radius.
if(this.is('ellipse')) {
var result = new Path();
var args = this.subPaths[0].actions[1].args;
if(args[2] + delta < 0)
return false;
result.ellipse(
args[0],
args[1],
args[2] + delta,
args[3] + delta,
args[4],
args[5],
args[6]
);
return result;
}
var scale = 1000;
var cleandelta = 0.1;
var polygons = this.toPolys(scale, divisions);
// offset
var miterLimit = 1000*scale;
var co = new ClipperLib.ClipperOffset();
// co.PreserveCollinear = true;
// co.ReverseSolution = true;
co.AddPaths(polygons,
ClipperLib.JoinType.jtMiter,
ClipperLib.EndType.etClosedPolygon);
var solution = [];
try {
co.Execute(solution, delta*scale);
}
catch(err) {
return false;
}
if(!solution || solution.length === 0
|| solution[0].length === 0) return false;
var result = new Path();
result.fromPolys(solution, scale);
result.close(); // Not sure why I need to do this now
return result;
}
, ramp: function(depth) {
}
, addPath: function(path2) {
this.subPaths = this.subPaths.concat(path2.subPaths);
}
, estimateMaxOffset: function(divisions) {
var bounds = this.getBounds();
var width = Math.abs(bounds.right - bounds.left)
var height = Math.abs(bounds.bottom - bounds.top)
var lt = Math.min(width, height) / 2;
var gt = 0;
for(var i = 0; i < 5; ++i) {
var test = gt+(lt-gt)/2;
var offset = this.offset(-test,3);
if(offset) {
gt = test
}
else {
lt = test;
}
}
return {lt: lt, gt: gt};
}
, fillPath: function(diameter, divisions) {
var result = new Path();
var overlap = Math.sin(Math.PI/4);
// this.subPaths.forEach(function(sp) {
// var path = sp.toPath();
var path = this;
var max = path.estimateMaxOffset(5).lt;
max -= diameter/2;
for(var i = -max; i < -diameter/2; i += diameter*overlap) {
var offsetPath = path.offset(i, divisions);
if(!offsetPath) break;
offsetPath = offsetPath.reverse();
result.addPath(offsetPath);
}
// Finishing pass
var finish = path.offset( -diameter/2, divisions );
if(finish)
result.addPath( finish.reverse() );
// });
return result;
}
, connectEnds: function(diameter) {
for(var i=this.subPaths.length-1; i > 0; --i) {
var sp1 = this.subPaths[i-1];
var sp2 = this.subPaths[i];
var p1 = sp1.lastPoint();
var nearest = sp2.nearestPoint(p1);
var p2 = nearest.point;
if(nearest.distance < diameter*2) {
sp2 = sp2.shift(nearest.i);
sp1.lineTo(p2.x, p2.y);
sp2.actions[0].action = Path.actions.LINE_TO;
sp1.actions = sp1.actions.concat( sp2.actions );
this.subPaths.splice(i,1);
}
}
return this;
}
, reverse: function() {
if(this.is('ellipse')) {
var result = new Path();
var args = this.subPaths[0].actions[1].args;
result.ellipse(
args[0],
args[1],
args[2],
args[3],
args[5], // end as start
args[4], // start as end
!args[6] // invert ccw
);
return result;
}
var result = new Path();
result.subPaths = this.subPaths.map(function(sp) {
return sp.reverse();
}).reverse();
return result;
}
, sort: function() {
if(this.subPaths.length === 0) return this;
var copy = new Path();
var p0 = this.subPaths[0].lastPoint();
copy.subPaths = this.subPaths.sort(function(a, b) {
var p1 = a.lastPoint();
var p2 = b.firstPoint();
var d1 = Point.distance(p1,p0);
var d2 = Point.distance(p2,p0);
// Moving target
p0 = b.lastPoint();
if(d1 < d2) return -1;
if(d1 > d2) return 1;
return 0;
});
return copy;
}
, firstPoint: function() {
if(!this.current) return false;
return this.subPaths[0].firstPoint();
}
, lastPoint: function() {
if(!this.current) return false;
return this.subPaths[this.subPaths.length-1].lastPoint();
}
, getPoints: function(divisions) {
var pts = [];
this.subPaths.forEach(function(sp) {
pts.push.apply(pts, sp.getPoints(divisions));
});
return pts;
}
, getBounds: function() {
var pts = this.getPoints();
var p0 = this.firstPoint();
var res = {
left: p0.x,
top: p0.y,
right: p0.x,
bottom: p0.y
};
pts.forEach(function(p) {
res.left = Math.min(res.left, p.x);
res.top = Math.min(res.top, p.y);
res.right = Math.max(res.right, p.x);
res.bottom = Math.max(res.bottom, p.y);
});
return res;
}
}
var NON_ZERO = ClipperLib.PolyFillType.pftNonZero;
var EVEN_ODD = ClipperLib.PolyFillType.pftEvenOdd;