Source: util.js

/*jslint todo: true, browser: true, continue: true, white: true*/

/**
 * Written by Alex Canales for ShopBotTools, Inc.
 */

/**
 * A 3D point.
 *
 * @typedef {object} Point
 * @property {number} x - The x coordinate.
 * @property {number} y - The y coordinate.
 * @property {number} z - The z coordinate.
 */

/**
 * A helper for finding axes according to the chosen plane.
 *
 * @typedef {object} Axes
 * @property {string} re - The axis for REal numbers.
 * @property {string} im - The axis for IMaginary numbers.
 * @property {string} cr - The CRoss axis.
 */

/**
 * An object defining a cubic Bézier curve.
 *
 * @typedef {object} Bezier
 * @property {Point} p0 - The first control point.
 * @property {Point} p1 - The second control point.
 * @property {Point} p2 - The third control point.
 * @property {Point} p3 - The fourth control point.
 */

/**
 * An object defining a line.
 *
 * @typedef {object} Line
 * @property {number} lineNumber - The line number in the G-Code file
 * corresponding to the line definition.
 * @property {string} type - The G-Code command.
 * @property {number} feedrate - The feed rate for doing the path defined by
 * the line.
 * @property {Point} [start] - The starting point of the line if type "G0" or
 * "G1".
 * @property {Point} [end] - The ending point of the line if type "G0" or "G1".
 * @property {Bezier[]} [bez] - The bezier curves defining the point if type
 * "G2" or G3".
 */

/**
 * Defines the settings of the G-Code. It changes constantly according to the
 * G-Code commands used.
 *
 * @typedef {object} Settings
 * @property {string} [crossAxe="z"] - The cross axe.
 * @property {number} [feedrate=0] - The feed rate.
 * @property {boolean} [inMm=false] - If the units are in millimeters.
 * @property {Point} [position={x:0, y:0, z:0}] - The last position of the bit.
 * @property {string} [previousMoveCommand=""] - The previous move command
 * ("G0", "G1", "G2", "G3").
 * @property {boolean} [relative=false] - If the coordinates are relative.
*/

/**
 * Defines a single command parsed by the G-Code syntax parser. The definition
 * is not exhaustive.
 *
 * @typedef {object} ParsedCommand
 * @property {string} type - The command type.
 * @property {number} [x] - The X argument.
 * @property {number} [y] - The Y argument.
 * @property {number} [z] - The Z argument.
 * @property {number} [f] - The F argument.
 * @property {number} [r] - The R argument.
 * @property {number} [i] - The I argument.
 * @property {number} [j] - The J argument.
 * @property {number} [k] - The K argument.
 */

/**
 * An object defining the size.
 *
 * @typedef {object} Size
 * @property {Point} min - The lowest values in x, y and z coordinates.
 * @property {Point} max - The highest values in x, y and z coordinates.
 */

/**
 * Errors can happen in G-Code files. It can be simple warning where code is
 * parsed but can have a different behaviour depending on the machine, or it
 * can be a real error and the command is skipped.
 *
 * @typedef {object} Error
 * @property {number} line - The line number where the error occurs.
 * @property {string} message - The message explaining the error.
 * @property {boolean} isSkipped - If the command is skipped.
 */

/**
 * An object defining the parsed G-Code. This is what that should be used by
 * the developper using this library.
 *
 * @typedef {object} ParsedGCode
 * @property {string[]} gcode - The original G-Code, each cell contains a
 * single command.
 * @property {Lines[]} lines - The lines defining the path the bit will take.
 * @property {Size} size - The size the job will take.
 * @property {boolean} displayInInch - If the job shoud be display in inches.
 * @property {Error} errorList - The error the G-Code contains.
 */

/**
 * This file contains useful scripts for different purposes (geometry, object
 * operations...). It also create the GCodeToGeometry namespace.
 */

/**
 * Namespace for the library.
 *
 * @namespace
 */
var GCodeToGeometry = {};

/**
 * Constant for converting inches values into millimeters values.
 */
GCodeToGeometry.INCH_TO_MILLIMETER = 25.4;

/**
 * Constant for converting millimeters values into inches values.
 */
GCodeToGeometry.MILLIMETER_TO_INCH = 0.03937008;

/*
 * Precision constant for comparing floats. Used in GCodeToGeometry.nearlyEqual.
 */
GCodeToGeometry.FLOAT_PRECISION = 0.001;

/*
 * Converts the feedrate in inches according to the types of unit used.
 *
 * @param {number} feedrate - The given feedrate.
 * @param {number} inMm - If the feedrate is in millimeters.
 * Returns the feedrate in inches.
 */
GCodeToGeometry.calculateFeedrate = function(feedrate, inMm) {
    return (inMm === false) ? feedrate : feedrate * GCodeToGeometry.MILLIMETER_TO_INCH;
};

/**
 * Checks if two numbers are nearly equal. This function is used to avoid
 * to have too much precision when checking values between floating-point
 * numbers.
 *
 * @param {number} a - Number A.
 * @param {number} b - Number B.
 * @param {number} [precision=GCodeToGeometry.FLOAT_PRECISION] - The precision
 * of the comparaison.
 * @return {boolean} True if the two values are nearly equal.
 */
GCodeToGeometry.nearlyEqual = function(a, b, precision) {
    var p = (precision === undefined) ? GCodeToGeometry.FLOAT_PRECISION : precision;
    return Math.abs(b - a) <= p;
};

/**
 * Swaps two objects. It has to be the same objects, too bad if it's not.
 *
 * @param {object} obj1 - The first object.
 * @param {object} obj2 - The second object.
*/
GCodeToGeometry.swapObjects = function(obj1, obj2) {
    function swapSingleField(objA, objB, key) {
        var temp;
        temp = objA[key];
        objA[key] = objB[key];
        objB[key] = temp;
    }
    var keys = Object.keys(obj1);
    var i = 0;

    for(i = 0; i < keys.length; i++) {
        if(typeof obj1[keys[i]] === "object") {
            GCodeToGeometry.swapObjects(obj1[keys[i]], obj2[keys[i]]);
        } else {
            swapSingleField(obj1, obj2, keys[i]);
        }
    }
};

/**
 * Returns the copy of the object.
 *
 * @param {object} object - The object.
 * @return {object} The copy of the object.
*/
GCodeToGeometry.copyObject = function(object) {
    var keys = Object.keys(object);
    var i = 0;
    var copy = {};
    for(i = 0; i < keys.length; i++) {
        if(typeof object[keys[i]] === "object") {
            copy[keys[i]] = GCodeToGeometry.copyObject(object[keys[i]]);
        } else {
            copy[keys[i]] = object[keys[i]];
        }
    }
    return copy;
};

/**
 * Moves the point according to the vector.
 *
 * @param {Point} point - The point to move.
 * @param {Point} vector - The vector.
 */
GCodeToGeometry.movePoint = function(point, vector) {
    var keys = Object.keys(vector);
    var i = 0;
    for(i = 0; i < keys.length; i++) {
        if(point[keys[i]] !== undefined) {
            point[keys[i]] += vector[keys[i]];
        }
    }
};

/**
 * Does a 2D dot product.
 *
 * @param {Point} v1 - The first vector.
 * @param {Point} v2 - The second vector.
 * @return {number} The result.
 */
GCodeToGeometry.dotProduct2 = function(v1, v2) {
    return v1.x * v2.x + v1.y * v2.y;
};

/**
 * Does a 2D cross product.
 *
 * @param {Point} v1 - The first vector.
 * @param {Point} v2 - The second vector.
 * @return {number} The result on the Z axis.
 */
GCodeToGeometry.crossProduct2 = function(v1, v2) {
    return v1.x * v2.y - v2.x * v1.y;
};

/**
 * Calculates the length of a 3D vector.
 *
 * @param {Point} v - The vector.
 * @return {number} The vector length.
 */
GCodeToGeometry.lengthVector3 = function(v) {
    return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
};

/**
 * Returns object of 3 axes:
 *  re is the axes for REal numbers;
 *  im for the IMaginary numbers;
 *  cr for the CRoss axis
 *
 * @param {string} crossAxe The name of the axis given by the cross product of
 * the vectors defining the plane. Should be "x", "y" or "z", considered "z" if
 * not "x" or "y".
 * @return {Axes} The object defining the real, imaginary and cross axis.
 */
GCodeToGeometry.findAxes = function(crossAxe) {
    if(crossAxe.toLowerCase() === "x") {
        return { re : "y", im : "z", cr : "x"};
    }
    if(crossAxe.toLowerCase() === "y") {
        return { re : "z", im : "x", cr : "y"};
    }
    return { re : "x", im : "y", cr : "z"};
};

/**
 * Does a rotation and scale of point according to center. Stores the result in
 * newPoint.
 *
 * @param {Point} center - The center of the rotation and scale.
 * @param {Point} point - The point to modify.
 * @param {Point} newPoint - The point storying the result.
 * @param {number} angle - The angle in radians.
 * @param {number} length - The scale ratio.
 * @param {string} re - The real axis.
 * @param {string} im - The imaginary axis.
 */
GCodeToGeometry.scaleAndRotation = function(
    center, point, newPoint, angle, length, re, im
) {
    var c = center, p = point, nP = newPoint;
    var l = length, cA = Math.cos(angle), sA = Math.sin(angle);
    var pRe = p[re], pIm = p[im], cRe = c[re], cIm = c[im];

    nP[re] = l * ((pRe - cRe) * cA - (pIm - cIm) * sA) + cRe;
    nP[im] = l * ((pIm - cIm) * cA + (pRe - cRe) * sA) + cIm;
};

/**
 * Returns the signed angle in radian in 2D (between -PI and PI).
 *
 * @param {Point} v1 - The first vector.
 * @param {Point} v2 - The second vector.
 * @return {number} The angle in radian.
 */
GCodeToGeometry.findAngleVectors2 = function(v1, v2) {
    var sign = (GCodeToGeometry.crossProduct2(v1, v2) < 0) ? -1 : 1;
    var dot = GCodeToGeometry.dotProduct2(v1, v2);
    var lV1 = Math.sqrt(v1.x * v1.x + v1.y * v1.y);
    var lV2 = Math.sqrt(v2.x * v2.x + v2.y * v2.y);

    if(lV1 === 0 || lV2 === 0) {
        return 0;
    }

    return sign * Math.acos(dot / (lV1 * lV2));
};

/**
 * Returns the signed angle in radian in 2d (between -2pi and 2pi).
 *
 * @param {Point} v1 - The first vector.
 * @param {Point} v2 - The second vector.
 * @param {boolean} positive - If the oriented angle goes counter-clockwise.
 * @return {number} The angle in radian.
 */
GCodeToGeometry.findAngleOrientedVectors2 = function(v1, v2, positive) {
    var angle =  GCodeToGeometry.findAngleVectors2(v1, v2);

    if(positive === false && angle > 0) {
        return angle - Math.PI * 2;
    }
    if(positive === true && angle < 0) {
        return Math.PI * 2 + angle;
    }

    return angle;
};

/**
 * Checks if the value is include between the value a (include) and b (include).
 * Order between a and b does not matter.
 *
 * @param {number} value - The value.
 * @param {number} a - The first boundary.
 * @param {number} b - The second boundary.
 * @return {boolean} The result.
 */
GCodeToGeometry.isInclude = function(value, a, b) {
    return (b < a) ? (b <= value && value <= a) : (a <= value && value <= b);
};