Source: gcodetogeometry.js

/*jslint todo: true, browser: true, continue: true, white: true*/
/*global GCodeToGeometry, GParser*/

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

/**
 * Parses the GCode into a series of lines and curves and checks if errors.
 *
 * @param {string} code - The GCode.
 * @returns {ParsedGCode} The parsed GCode.
 */
GCodeToGeometry.parse = function(code) {
    "use strict";

    var unitIsSet = false;
    var setInInch = true;

    /**
     * Removes the comments and spaces.
     * @param  {string}  command  The command to parse
     * @return  {string}  The command without the commands and spaces.
     */
    function removeCommentsAndSpaces(command) {
        var s = command.split('(')[0].split(';')[0]; //No need to use regex
        return s.split(/\s/).join('').trim();
    }

    /**
     * Parses the result of GParser.parse.
     * @param  {array}  Result of GParser.parse
     * @return  {array}  Array of object.
     */
    function parseParsedGCode(parsed) {
        var obj = {};
        var i = 0;
        var letter = "", number = "";
        var tab = [];
        var emptyObj = true;

        for(i=0; i < parsed.words.length; i++) {
            letter = parsed.words[i][0];
            number = parsed.words[i][1];
            if(letter === "G" || letter === "M") {
                //Make sure multiple commands in one line are interpreted as
                //multiple commands:
                if(emptyObj === false) {
                    tab.push(obj);
                    obj = {};
                }
                obj.type = letter + number;
                emptyObj = false;
            } else  {
                obj[letter.toLowerCase()] = parseFloat(number, 10);
            }
        }
        tab.push(obj);
        return tab;
    }

    /**
     * Checks if there is a wrong parameter.
     * @param  {array}  acceptedParameters  Array of accepted parameters (should
     *                                      not include the type of the command)
     * @param  {array}  parameters          The current given parameters
     * @return  {bool}  True if there is a wrong parameter.
     */
    function checkWrongParameter(acceptedParameters, parameters) {
        var i = 0, j = 0;
        var accepted = true;

        for(j = parameters.length - 1; j >= 0; j--) {
            for(i = acceptedParameters.length - 1; i >= 0; i--) {
                accepted = false;
                if(parameters[j].toUpperCase() === acceptedParameters[i].toUpperCase()) {
                    accepted = true;
                    acceptedParameters.splice(i, 1);
                    break;
                }
            }
            if(accepted === false) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks and modifies the total size.
     * @param  {object}  totalSize  The the whole operation size (modified)
     * @param  {object}  size       The added operation size
     */
    function checkTotalSize(totalSize, size) {
        var keys = ["x", "y", "z"];
        var i = 0;
        for(i = keys.length - 1; i >= 0; i--) {
            if(totalSize.min[keys[i]] > size.min[keys[i]]) {
                totalSize.min[keys[i]] = size.min[keys[i]];
            }
            if(totalSize.max[keys[i]] < size.max[keys[i]]) {
                totalSize.max[keys[i]] = size.max[keys[i]];
            }
        }
    }

    /**
     * Creates an error object.
     *
     * @param {number} line The line number.
     * @param {string} message The message.
     * @param {boolean} isSkipped If the command is skipped.
     * @return {Error} The error object.
     */
    function createError(line, message, isSkipped) {
        return { line : line, message : message, isSkipped : isSkipped };
    }

    /**
     * Checks if there is an error due to the feed rate configuration.
     * @param  {object}  command    The command (the feed rate can be changed)
     * @param  {object}  errorList  The error list
     * @param  {number}  line       The line number
     * @param  {object}  settings   The modularity settings
     * @return {bool}  True if the command is skipped (error), else false if the
     *                 feedrate is correct or emits only a warning
     */
    function checkErrorFeedrate(command, errorList, line, settings) {
        var c = command;
        var consideredFeedrate = (c.f === undefined) ? settings.feedrate : c.f;

        if(c.type !== undefined && c.type !== "G1" && c.type !== "G2" &&
                c.type !== "G3") {
            return false;
        }

        if(consideredFeedrate > 0) {
            return false;
        }

        if(consideredFeedrate < 0) {
            errorList.push(createError(
                line,
                "(warning) Cannot use a negative feed rate " +
                          "(the absolute value is used).",
                false
            ));
            c.f = Math.abs(consideredFeedrate);
            return false;
        }

        errorList.push(createError(
            line, "(error) Cannot use a null feed rate (skipped).", true
        ));
        settings.feedrate = 0;

        return true;
    }

    /**
     * Sets the command type if not set and if a previous move command was set.
     * @param  {object}  parsedCommand        The command (is modified)
     * @param  {string}  previousMoveCommand  The type of the previous move
     *                                        command
     */
    function setGoodType(parsedCommand, previousMoveCommand) {
        if(parsedCommand.type !== undefined) {
            return;
        }
        if(previousMoveCommand !== "") {
            parsedCommand.type = previousMoveCommand;
        }
    }

    /**
     * Finds the next position according to the x, y and z contained or not in
     * the command parameters.
     *
     * @param {object} start The 3D start point.
     * @param {object} parameters The command parameters.
     * @param {boolean} relative If the point in the parameters is a relative
     * point.
     * @param {boolean} inMm If the values are in inches.
     * @return {object} The point.
    */
    function findPosition (start, parameters, relative, inMm) {
        console.log(parameters);
        var pos = { x : start.x, y : start.y, z : start.z };
        var d = (inMm === false) ? 1 : GCodeToGeometry.MILLIMETER_TO_INCH;
        if(relative === true) {
            if(parameters.x !== undefined) { pos.x += parameters.x * d; }
            if(parameters.y !== undefined) { pos.y += parameters.y * d; }
            if(parameters.z !== undefined) { pos.z += parameters.z * d; }
        } else {
            if(parameters.x !== undefined) { pos.x = parameters.x * d; }
            if(parameters.y !== undefined) { pos.y = parameters.y * d; }
            if(parameters.z !== undefined) { pos.z = parameters.z * d; }
        }

        return pos;
    }

    /**
     * Checks a G0 command.
     * @param  {object}  command    The command
     * @param  {array}   errorList  The error list
     * @param  {number}  line       The line number
     * @return  {bool}   Returns true if the command is done, false if skipped
     */
    function checkG0(command, errorList, line) {
        var acceptedParameters = [ "X", "Y", "Z" ];
        var parameters = Object.keys(command);
        parameters.splice(parameters.indexOf("type"), 1);

        if(checkWrongParameter(acceptedParameters, parameters) === true) {
            errorList.push(createError(
                line, "(warning) Some parameters are wrong.", false
            ));
        }
        return true;
    }

    /**
     * Checks a G1 command.
     * @param  {object}  command           The command
     * @param  {array}   errorList         The error list
     * @param  {number}  line              The line number
     * @param  {number}  previousFeedrate  The previous feedrate
     * @return  {bool}   Returns true if the command is done, false if skipped
     */
    function checkG1(command, errorList, line, previousFeedrate) {
        var acceptedParameters = [ "X", "Y", "Z", "F" ];
        var parameters = Object.keys(command);
        parameters.splice(parameters.indexOf("type"), 1);

        if(checkWrongParameter(acceptedParameters, parameters) === true) {
            errorList.push(createError(
                line, "(warning) Some parameters are wrong.", false
            ));
        }

        return !checkErrorFeedrate(command, errorList, line, previousFeedrate);
    }

    /**
     * Checks a G2 or G3 command.
     * @param  {object}  command           The command
     * @param  {array}   errorList         The error list
     * @param  {number}  line              The line number
     * @param  {number}  previousFeedrate  The previous feedrate
     * @return  {bool}   Returns true if the command is done, false if skipped
     */
    function checkG2G3(command, errorList, line, previousFeedrate) {
        var acceptedParameters = [ "X", "Y", "Z", "F", "I", "J", "K", "R" ];
        var parameters = Object.keys(command);
        parameters.splice(parameters.indexOf("type"), 1);

        if(checkWrongParameter(acceptedParameters, parameters) === true) {
            errorList.push(createError(
                line, "(warning) Some parameters are wrong.", false
            ));
        }

        if(command.r === undefined && command.i === undefined &&
                command.j === undefined && command.k === undefined) {
            errorList.push(createError(
                line, "(error) No parameter R, I, J or K.", true
            ));
            return false;
        }

        if(command.r !== undefined && (command.i !== undefined ||
            command.j !== undefined || command.k !== undefined)) {
            errorList.push(createError(
                line,
                "(error) Cannot use R and I, J or K at the same time.",
                true
            ));
            return false;
        }

        return !checkErrorFeedrate(command, errorList, line, previousFeedrate);
    }

    /**
     * Manages a 60 or G1 command.
     * @param  {object}  command    The command
     * @param  {object}  settings   The modularity settings
     * @param  {object}  totalSize  The the whole operation size (modified)
     * @param  {array}   lines      The array containing the lines
     * @param  {number}  lineNumber The line number
     * @param  {object}  errorList  The error list
     */
    function manageG0G1(command, settings, lineNumber, lines, totalSize) {
        console.log(settings);
        var nextPosition = findPosition(settings.position, command,
            settings.relative, settings.inMm);
        var line = new GCodeToGeometry.StraightLine(lineNumber,
            settings.position, nextPosition, command, settings);
        settings.previousMoveCommand = command.type;
        checkTotalSize(totalSize, line.getSize());
        lines.push(line.returnLine());
        settings.position = GCodeToGeometry.copyObject(line.end);
        if(command.f !== undefined) {
            settings.feedrate = command.f;
        }
    }

    /**
     * Manages a G2 or G3 command.
     * @param  {object}  command    The command
     * @param  {object}  settings   The modularity settings
     * @param  {number}  lineNumber The line number
     * @param  {array}   lines      The array containing the lines
     * @param  {object}  totalSize  The the whole operation size (modified)
     * @param  {object}  errorList  The error list
     */
    function manageG2G3(command, settings, lineNumber, lines, totalSize,
            errorList) {
        var nextPosition = findPosition(settings.position, command,
            settings.relative, settings.inMm);
        var line = new GCodeToGeometry.CurvedLine(lineNumber, settings.position,
            nextPosition, command, settings);
        if(line.center !== false) {
            var temp = line.returnLine();
            if(temp === false) {
                errorList.push(createError(
                    lineNumber, "(error) Impossible to create arc.", true
                ));
                return;
            }
            settings.feedrate = line.feedrate;
            settings.previousMoveCommand = command.type;
            checkTotalSize(totalSize, line.getSize());
            lines.push(temp);
            settings.position = GCodeToGeometry.copyObject(line.end);
        } else {
            errorList.push(createError(
                lineNumber,
                "(error) Physically impossible to do with those values.",
                true
            ));
        }
    }

    /**
     * Manages a command (check it, create geometrical line, change setting...).
     * @param  {object}  command    The command
     * @param  {object}  settings   The modularity settings
     * @param  {number}  lineNumber The line number
     * @param  {array}   lines      The array containing the lines
     * @param  {object}  totalSize  The the whole operation size (modified)
     * @param  {object}  errorList  The error list
     * @return {bool}  Returns true if have to continue, else false
     */
    function manageCommand(command, settings, lineNumber, lines, totalSize,
            errorList) {
        //Empty line
        if(command.type === undefined && Object.keys(command).length === 0) {
            return true;
        }

        setGoodType(command, settings.previousMoveCommand);

        if(command.type === undefined) {
            if(command.f !== undefined) {
                checkErrorFeedrate(command, errorList, lineNumber,
                        settings.feedrate);
                settings.feedrate = command.f;
            }
        } else if(command.type === "G0" &&
                checkG0(command, errorList, lineNumber) === true)
        {
            manageG0G1(command, settings, lineNumber, lines, totalSize);
        } else if (command.type === "G1" &&
            checkG1(command, errorList, lineNumber, settings) === true)
        {
            manageG0G1(command, settings, lineNumber, lines, totalSize);
        } else if((command.type === "G2" || command.type === "G3") &&
                checkG2G3(command, errorList, lineNumber, settings) === true)
        {
            manageG2G3(command, settings, lineNumber, lines, totalSize, errorList);
        } else if(command.type === "G17") {
            settings.crossAxe = "z";
        } else if(command.type === "G18") {
            settings.crossAxe = "y";
        } else if(command.type === "G19") {
            settings.crossAxe = "x";
        } else if(command.type === "G20") {
            settings.inMm = false;
            if(unitIsSet === false) {
                setInInch = true;
                unitIsSet = true;
            }
        } else if(command.type === "G21") {
            settings.inMm = true;
            if(unitIsSet === false) {
                setInInch = false;
                unitIsSet = true;
            }
        } else if(command.type === "G90") {
            settings.relative = false;
        } else if(command.type === "G91") {
            settings.relative = true;
        } else if(command.type === "M2") {
            return false;
        }

        return true;
    }

    var totalSize = {
        min : { x: 0, y : 0, z : 0 },
        max : { x: 0, y : 0, z : 0 }
    };
    var i = 0, j = 0;
    var tabRes = [];
    var parsing = true;
    var lines = [];
    var errorList = [];

    var settings = {
        feedrate : 0,
        previousMoveCommand : "",
        crossAxe : "z",
        inMm : false,
        relative : false,
        position : { x : 0, y : 0, z : 0 }
    };

    if(typeof code !== "string" || code  === "") {
        return {
            gcode : [],
            lines : [],
            size : totalSize,
            displayInInch : setInInch,
            errorList : [ { line : 0, message : "(error) No command." } ]
        };
    }
    var gcode = code.split('\n');

    i = 0;
    while(i < gcode.length && parsing === true) {
        //Sorry for not being really readable :'(
        tabRes = parseParsedGCode(
            GParser.parse(
                removeCommentsAndSpaces(gcode[i]).toUpperCase()
            )
        );

        j = 0;
        while(j < tabRes.length && parsing === true) {
            parsing = manageCommand(tabRes[j], settings, i+1, lines, totalSize,
                    errorList);
            j++;
        }
        i++;
    }

    if(i < gcode.length) {
        errorList.push(createError(
            i + 1, "(warning) The next code is not executed.", false
        ));
    }

    return {
        gcode : gcode,
        lines : lines,
        size : totalSize,
        displayInInch : setInInch,
        errorList : errorList
    };
};