/*jslint todo: true, browser: true, continue: true, white: true*/
/*global GCodeToGeometry*/
/**
* Written by Alex Canales for ShopBotTools, Inc.
*/
/**
* This file contains the classes managing the lines. The lines are
* the representation of the G0, G1, G2 and G3 commands.
*/
/**
* Creates an instance of the StraightLine class. This class does the
* computations for the G0 and G1 commands.
*
* @class
* @param {number} index - The line number where this command appears.
* @param {Point} start - The 3D start point.
* @param {ParsedCommand} parsedCommand - The parsed command.
* @param {Settings} settings - The modularity settings.
* @return {StraightLine} An instance of the StraightLine class.
*/
GCodeToGeometry.StraightLine = function(index, start, end, parsedCommand, settings) {
"use strict";
var that = this;
/**
* Returns a line object of type "G0" or "G1" (corresponding to
* parsedCommand).
*
* @function returnLine
* @memberof GCodeToGeometry.StraightLine
* @instance
* @return {Line} The line object.
*/
that.returnLine = function() {
return {
lineNumber : that.index,
type : that.word,
start : that.start,
end : that.end,
feedrate : that.feedrate
};
};
/**
* Returns the size of the line.
*
* @function getSize
* @memberof GCodeToGeometry.StraightLine
* @instance
* @return {Size} The size.
*/
that.getSize = function() {
return {
min : {
x : Math.min(that.start.x, that.end.x),
y : Math.min(that.start.y, that.end.y),
z : Math.min(that.start.z, that.end.z),
}, max : {
x : Math.max(that.start.x, that.end.x),
y : Math.max(that.start.y, that.end.y),
z : Math.max(that.start.z, that.end.z),
}
};
};
function initialize(index, start, parsedCommand, settings) {
that.index = index;
that.word = parsedCommand.type;
that.start = { x : start.x, y : start.y, z : start.z };
that.end = end;
if(parsedCommand.type === "G0") {
that.feedrate = 0;
} else if(parsedCommand.f === undefined) {
that.feedrate = settings.feedrate;
} else {
that.feedrate = GCodeToGeometry.calculateFeedrate(parsedCommand.f,
settings.inMm);
}
}
initialize(index, start, parsedCommand, settings);
};
/**
* Creates an instance of the CurvedLine class. This class does the computations
* for the G2 and G3 commands.
*
* @class
* @param {number} index - The line number where this command appears.
* @param {Point} start - The 3D start point.
* @param {ParsedCommand} parsedCommand - The parsed command.
* @param {Settings} settings - The modularity settings.
* @return {CurvedLine} An instance of the CurvedLine class.
*/
GCodeToGeometry.CurvedLine = function(index, start, end, parsedCommand, settings) {
"use strict";
var that = this;
// Will give 0 if start and end are the same
function getBezierAngle() {
var axes = GCodeToGeometry.findAxes(that.crossAxe);
var cs = { x : that.start[axes.re] - that.center[axes.re],
y : that.start[axes.im] - that.center[axes.im], z : 0};
var ce = { x : that.end[axes.re] - that.center[axes.re],
y : that.end[axes.im] - that.center[axes.im], z : 0};
return GCodeToGeometry.findAngleOrientedVectors2(cs, ce,
that.clockwise === false);
}
function getBezierRadius() {
var axes = GCodeToGeometry.findAxes(that.crossAxe);
var cs = { x : that.start[axes.re] - that.center[axes.re],
y : that.start[axes.im] - that.center[axes.im], z : 0};
return GCodeToGeometry.lengthVector3(cs);
}
//Simple cubic Bézier curve interpolation clockwise on XY plane
//angle in radian included in [0; pi/2]
//radius > 0
//From Richard A DeVeneza's work
function cubBez2DInt(angle, radius) {
var p0 = {}, p1 = {}, p2 ={}, p3 = {};
angle = Math.abs(angle);
if(angle === Math.PI / 2) {
//cos(PI/4) == sin(PI/4) but JavaScript doesn't believe it
p0 = { x : 0.707106781186548, y : 0.707106781186548, z : 0 };
p1 = { x : 1.097631072937817, y : 0.316582489435277, z : 0 };
} else {
p0 = { x : Math.cos(angle/2), y : Math.sin(angle/2), z : 0 };
p1 = {
x : (4 - p0.x) / 3,
y : (1 - p0.x) * (3 - p0.x) / (3 * p0.y),
z : 0
};
}
p0.x *= radius;
p0.y *= radius;
p1.x *= radius;
p1.y *= radius;
p2 = { x : p1.x, y : -p1.y, z : 0 };
p3 = { x : p0.x, y : -p0.y, z : 0 };
return { p0 : p0, p1 : p1, p2 : p2, p3 : p3 };
}
//Transform a 2D cubic Bézier's curve clockwise on XY plane
// to a Bézier's curve in 3D with the right crossAxe and clock direction
// clockwise is bool
// pitch can be positive or negative
function cubBez2DTo3D(curve, clockwise, pitch, crossAxe) {
var height = 0; //height position for p1, p2 and p3
if(clockwise === false) {
GCodeToGeometry.swapObjects(curve.p0, curve.p3);
GCodeToGeometry.swapObjects(curve.p1, curve.p2);
}
//NOTE: maybe this is better:
// b = p*alpha*(r - ax)*(3*r -ax)/(ay*(4*r - ax)*Math.tan(alpha))
//Set the good cross axe and transform into a helical Bézier curve
height = pitch / 3;
if(crossAxe.toLowerCase() === "z") {
curve.p0.z = 0;
curve.p1.z = height;
curve.p2.z = height * 2;
curve.p3.z = height * 3;
} else if(crossAxe.toLowerCase() === "x") {
curve.p0.z = curve.p0.y;
curve.p0.y = curve.p0.x;
curve.p0.x = 0;
curve.p1.z = curve.p1.y;
curve.p1.y = curve.p1.x;
curve.p1.x = height;
curve.p2.z = curve.p2.y;
curve.p2.y = curve.p2.x;
curve.p2.x = height * 2;
curve.p3.z = curve.p3.y;
curve.p3.y = curve.p3.x;
curve.p3.x = height * 3;
} else if(crossAxe.toLowerCase() === "y") {
curve.p0.z = curve.p0.x;
curve.p0.x = curve.p0.y;
curve.p0.y = 0;
curve.p1.z = curve.p1.x;
curve.p1.x = curve.p1.y;
curve.p1.y = height;
curve.p2.z = curve.p2.x;
curve.p2.x = curve.p2.y;
curve.p2.y = height * 2;
curve.p3.z = curve.p3.x;
curve.p3.x = curve.p3.y;
curve.p3.y = height * 3;
}
return curve;
}
function rotAndPlaBez(curve, center, angle, re, im) {
var c = { x : 0, y : 0, z : 0 };
GCodeToGeometry.scaleAndRotation(c,curve.p0,curve.p0, angle, 1, re, im);
GCodeToGeometry.scaleAndRotation(c,curve.p1,curve.p1, angle, 1, re, im);
GCodeToGeometry.scaleAndRotation(c,curve.p2,curve.p2, angle, 1, re, im);
GCodeToGeometry.scaleAndRotation(c,curve.p3,curve.p3, angle, 1, re, im);
GCodeToGeometry.movePoint(curve.p0, center);
GCodeToGeometry.movePoint(curve.p1, center);
GCodeToGeometry.movePoint(curve.p2, center);
GCodeToGeometry.movePoint(curve.p3, center);
}
// The Bézier's curve must be on the good plane
function getFullBezier(num90, bez90, numSmall, bezSmall, pitch90) {
var arcs = [];
var center = GCodeToGeometry.copyObject(that.center);
var axes = GCodeToGeometry.findAxes(that.crossAxe);
var cs = { x : that.start[axes.re] - center[axes.re],
y : that.start[axes.im] - center[axes.im] };
var i = 0, angle = 0, sign = (that.clockwise === true) ? -1 : 1;
if(num90 === 0 && numSmall === 0) {
return arcs;
}
if(num90 > 0) {
angle = GCodeToGeometry.findAngleOrientedVectors2(
{ x : bez90.p0[axes.re], y : bez90.p0[axes.im] }, cs,
that.clockwise === false
);
for(i = 0; i < num90; i++) {
arcs.push(GCodeToGeometry.copyObject(bez90));
rotAndPlaBez(arcs[i], center, angle, axes.re, axes.im);
// angle += Math.PI / 2 * sign;
angle += 1.570796326794897 * sign;
center[that.crossAxe] += pitch90;
}
}
if(numSmall > 0) {
angle = GCodeToGeometry.findAngleOrientedVectors2(
{ x : bezSmall.p0[axes.re], y : bezSmall.p0[axes.im] }, cs,
that.clockwise === false
);
if(num90 !== 0) {
angle += num90 * 1.570796326794897 * sign;
}
arcs.push(GCodeToGeometry.copyObject(bezSmall));
rotAndPlaBez(arcs[i], center, angle, axes.re, axes.im);
}
//To be sure the first point is at the start
arcs[0].p0.x = that.start.x;
arcs[0].p0.y = that.start.y;
arcs[0].p0.z = that.start.z;
//To be sure the last point is at the end
arcs[arcs.length-1].p3.x = that.end.x;
arcs[arcs.length-1].p3.y = that.end.y;
arcs[arcs.length-1].p3.z = that.end.z;
return arcs;
}
function arcToBezier() {
var num90 = 0, numSmall = 1; //Number arc = pi/2 and arc < pi/2
var bez90 = {}, bezSmall = {};
var p90 = 0, pLittle = 0, pAngle = 0; //Pitch of the arcs
var angle = getBezierAngle();
var radius = getBezierRadius();
var absAngle = Math.abs(angle), halfPI = 1.570796326794897;
if(angle === 0 || radius === 0) {
return [];
}
//Find number of diferent sections
if(absAngle > halfPI) {
//Untrustful (as this language) function, should be tested:
num90 = parseInt(absAngle / halfPI, 10);
numSmall = (absAngle % halfPI !== 0) ? 1 : 0;
}
//Find pitches
pAngle = (that.end[that.crossAxe] - that.start[that.crossAxe]) / absAngle;
p90 = halfPI * pAngle;
pLittle = (absAngle - num90 * halfPI) * pAngle;
//Find helical Bézier's curves
if(num90 > 0) {
bez90 = cubBez2DInt(halfPI, radius);
cubBez2DTo3D(bez90, (angle < 0), p90, that.crossAxe);
}
if(numSmall > 0) {
angle = absAngle - num90 * halfPI;
if(that.clockwise === true) {
angle = -angle;
}
bezSmall = cubBez2DInt(angle, radius);
cubBez2DTo3D(bezSmall, (angle < 0), pLittle, that.crossAxe);
}
return getFullBezier(num90, bez90, numSmall, bezSmall, p90);
}
//Cannot use arcToBezier because of calculus of oriented angle
function circleToBezier() {
var bez90 = {};
var bezier = [];
var pitch = 0;
var halfPI = 1.570796326794897;
var sign = (that.clockwise === true) ? -1 : 1;
var rotAngle = sign * Math.PI * 2;
var radius = getBezierRadius();
var i = 0;
var center = GCodeToGeometry.copyObject(that.center);
var axes = GCodeToGeometry.findAxes(that.crossAxe);
if(radius === 0) {
return [];
}
//We cannot just make a full circle without caring of the start and
//end point. Therefore, we need to use the rotation
pitch = (that.end[that.crossAxe] - that.start[that.crossAxe]) / 4;
bez90 = cubBez2DInt(halfPI, radius);
cubBez2DTo3D(bez90, that.clockwise, pitch, that.crossAxe);
for(i = 0; i < 4; i++) {
bezier.push(GCodeToGeometry.copyObject(bez90));
rotAndPlaBez(bezier[i], center, rotAngle, axes.re, axes.im);
rotAngle += halfPI * sign;
center[that.crossAxe] += pitch;
}
return getFullBezier(4, bez90, 0, bez90, pitch);
}
/**
* Returns a line object of type "G2" or "G3" (corresponding to
* parsedCommand).
*
* @function returnLine
* @memberof GCodeToGeometry.CurvedLine
* @instance
* @return {Line|boolean} False if impossible line else the line object.
*/
that.returnLine = function() {
var bez = [];
var axes = GCodeToGeometry.findAxes(that.crossAxe);
if(that.start[axes.re] === that.end[axes.re] &&
that.start[axes.im] === that.end[axes.im]) {
bez = circleToBezier();
} else {
bez = arcToBezier();
}
if(bez.length === 0) {
return false;
}
return {
lineNumber : that.index,
type : that.word,
beziers : bez,
feedrate : that.feedrate
};
};
/**
* Finds the center of the arc. Returns false if impossible.
*
* @param {Point} start The starting point of the arc.
* @param {Point} end The ending point of the arc.
* @param {boolean} clockwise If the arc goes clockwise.
* @param {string} crossAxe The name of the axe given by the cross product
* of the vectors defining the plane.
* @return {object|boolean} The center point or false.
*/
function findCenterWithRadius(start, end, radius, clockwise, crossAxe) {
var se = { x : end.x - start.x, y : end.y - start.y,
z : end.z - start.z
};
var angle = 0, l = 1, lSE = 0, r = Math.abs(radius), aCSCE = 0;
var center = { x : 0, y : 0, z : 0 };
var axes = GCodeToGeometry.findAxes(crossAxe);
lSE = Math.sqrt(se[axes.re] * se[axes.re] + se[axes.im] * se[axes.im]);
if(lSE > Math.abs(radius * 2) || lSE === 0) {
return false;
}
angle = Math.acos(lSE / (2 * r));
l = r / lSE;
GCodeToGeometry.scaleAndRotation(start, end, center, angle, l, axes.re, axes.im);
aCSCE = GCodeToGeometry.findAngleVectors2(
{ x: start[axes.re]-center[axes.re], y: start[axes.im]-center[axes.im] },
{ x: end[axes.re]-center[axes.re], y: end[axes.im]-center[axes.im] }
);
if(clockwise === true) {
if(radius > 0 && -Math.PI <= aCSCE && aCSCE <= 0) {
return center;
}
if(radius < 0 && 0 <= aCSCE && aCSCE <= Math.PI) {
return center;
}
} else {
if(radius > 0 && 0 <= aCSCE && aCSCE <= Math.PI) {
return center;
}
if(radius < 0 && -Math.PI <= aCSCE && aCSCE <= 0) {
return center;
}
}
GCodeToGeometry.scaleAndRotation(start, end, center, -angle, l, axes.re, axes.im);
return center;
}
//radius is positive or negative
function findCenter(start, end, parsedCommand, clockwise, crossAxe, inMm) {
var delta = (inMm === false) ? 1 : GCodeToGeometry.MILLIMETER_TO_INCH;
var center = { x : start.x, y : start.y, z : start.z };
var distCenterStart, distCenterEnd;
var axes = GCodeToGeometry.findAxes(crossAxe);
if(parsedCommand.r === undefined) {
if(parsedCommand.i !== undefined) {
center.x += parsedCommand.i * delta;
}
if(parsedCommand.j !== undefined) {
center.y += parsedCommand.j * delta;
}
if(parsedCommand.k !== undefined) {
center.z += parsedCommand.k * delta;
}
//Check if not impossible
distCenterStart = Math.pow(center[axes.re] - start[axes.re], 2);
distCenterStart += Math.pow(center[axes.im] - start[axes.im], 2);
distCenterEnd = Math.pow(center[axes.re] - end[axes.re], 2);
distCenterEnd += Math.pow(center[axes.im] - end[axes.im], 2);
if(GCodeToGeometry.nearlyEqual(distCenterStart, 0) === true ||
GCodeToGeometry.nearlyEqual(distCenterEnd, 0) === true) {
return false;
}
if(GCodeToGeometry.nearlyEqual(distCenterStart, distCenterEnd) === false) {
return false;
}
} else {
center = findCenterWithRadius(start, end, parsedCommand.r * delta,
clockwise, crossAxe);
if(center === false) {
return false;
}
}
center[crossAxe] = start[crossAxe];
return center;
}
function axeCutArc(reValue, imValue, angleBezier, cs) {
//Find the angle in the same orientation than the Bézier's angle
var a = GCodeToGeometry.findAngleOrientedVectors2(cs,
{ x : reValue, y : imValue }, that.clockwise === false);
return (GCodeToGeometry.isInclude(a, 0, angleBezier) === true);
}
/**
* Returns the size of the line.
*
* @function getSize
* @memberof GCodeToGeometry.CurvedLine
* @instance
* @return {Size} The size.
*/
that.getSize = function() {
var axes = GCodeToGeometry.findAxes(that.crossAxe);
var cs = {
x : that.start[axes.re] - that.center[axes.re],
y : that.start[axes.im] - that.center[axes.im]
};
var radius = getBezierRadius(), aBez = getBezierAngle();
var min = { x : 0 , y : 0, z : 0 }, max = { x : 0 , y : 0, z : 0 };
// Is circle
if(that.start[axes.re] === that.end[axes.re] &&
that.start[axes.im] === that.end[axes.im]) {
min[axes.re] = that.center[axes.re] - radius;
min[axes.im] = that.center[axes.im] - radius;
min[axes.cr] = Math.min(that.start[axes.cr], that.end[axes.cr]);
max[axes.re] = that.center[axes.re] + radius;
max[axes.im] = that.center[axes.im] + radius;
max[axes.cr] = Math.max(that.start[axes.cr], that.end[axes.cr]);
return { min : min, max : max };
}
min.x = Math.min(that.start.x, that.end.x);
min.y = Math.min(that.start.y, that.end.y);
min.z = Math.min(that.start.z, that.end.z);
max.x = Math.max(that.start.x, that.end.x);
max.y = Math.max(that.start.y, that.end.y);
max.z = Math.max(that.start.z, that.end.z);
if(axeCutArc(0, 1, aBez, cs) === true) {
max[axes.im] = that.center[axes.im] + radius;
}
if(axeCutArc(0, -1, aBez, cs) === true) {
min[axes.im] = that.center[axes.im] - radius;
}
if(axeCutArc(1, 0, aBez, cs) === true) {
max[axes.re] = that.center[axes.re] + radius;
}
if(axeCutArc(-1, 0, aBez, cs) === true) {
min[axes.re] = that.center[axes.re] - radius;
}
return { min : min, max : max };
};
function initialize(index, start, parsedCommand, settings) {
that.index = index;
that.word = parsedCommand.type;
that.start = { x : start.x, y : start.y, z : start.z };
that.end = end;
that.clockwise = (parsedCommand.type === "G2");
that.center = findCenter(start, that.end, parsedCommand,
that.clockwise, settings.crossAxe, settings.inMm);
that.crossAxe = settings.crossAxe;
if(parsedCommand.f === undefined) {
that.feedrate = settings.feedrate;
} else {
that.feedrate = GCodeToGeometry.calculateFeedrate(parsedCommand.f,
settings.inMm);
}
}
initialize(index, start, parsedCommand, settings);
};