<commit_before>/**
 * User: hudbrog (<EMAIL>)
 * Date: 10/20/12
 * Time: 1:36 PM
 * To change this template use File | Settings | File Templates.
 */

GCODE.renderer = (function () {
    // ***** PRIVATE ******
    var canvas;
    var ctx;

    var viewportChanged = true;
    var lineWidthFactor = 1 / 2.8;

    var zoomFactorDelta = 0.4;
    var gridStep = 10;
    var ctxHeight, ctxWidth;
    var prevX = 0,
        prevY = 0;
    var pixelRatio = window.devicePixelRatio || 1;

    var layerNumStore,
        progressStore = {from: 0, to: -1};
    var lastX, lastY;
    var dragStart;
    var scaleFactor = 1.1;
    var model = undefined;
    var modelInfo = undefined;
    var initialized = false;
    var renderOptions = {
        colorGrid: "#bbbbbb",
        bgColorGrid: "#ffffff",
        bgColorOffGrid: "#eeeeee",
        colorLine: ["#000000", "#3333cc", "#cc3333", "#33cc33", "#cc33cc"],
        colorMove: "#00ff00",
        colorRetract: "#ff0000",
        colorRestart: "#0000ff",
        colorHead: "#00ff00",
        colorSegmentStart: "#666666",

        showMoves: true,
        showRetracts: true,
        extrusionWidth: 1 * pixelRatio,
        // #000000", "#45c7ba",  "#a9533a", "#ff44cc", "#dd1177", "#eeee22", "#ffbb55", "#ff5511", "#777788"
        sizeRetractSpot: 2 * pixelRatio,
        sizeHeadSpot: 2 * pixelRatio,
        modelCenter: {x: 0, y: 0},
        differentiateColors: true,
        showNextLayer: false,
        showCurrentLayer: false,
        showPreviousLayer: false,
        showBoundingBox: false,
        showLayerBoundingBox: false,
        showFullSize: false,
        showHead: false,
        showSegmentStarts: false,
        sizeSegmentStart: 2 * pixelRatio,

        moveModel: true,
        zoomInOnModel: false,
        zoomInOnBed: false,
        centerViewport: false,
        invertAxes: {x: false, y: false},

        bed: {x: 200, y: 200},
        container: undefined,

        onInternalOptionChange: undefined,

        onViewportChange: undefined,
        onDragStart: undefined, // Cancellable (return false)
        onDrag: undefined, // Cancellable (return false)
        onDragStop: undefined
    };

    var offsetModelX = 0,
        offsetModelY = 0;
    var offsetBedX = 0,
        offsetBedY = 0;
    var scaleX = 1,
        scaleY = 1;
    var speeds = [];
    var speedsByLayer = {};
    var currentInvertX = false,
        currentInvertY = false;

    function notifyIfViewportChanged() {
        if (viewportChanged) {
            if (renderOptions["onViewportChange"]) {
                renderOptions["onViewportChange"](ctx.getTransform());
            }
            viewportChanged = false;
        }
    }

    var reRender = function () {
        if (!model) return;

        log.debug(
            "Rerendering layer " +
                layerNumStore +
                " of " +
                model.length +
                " with " +
                GCODE.renderer.getLayerNumSegments(layerNumStore) +
                " segments"
        );

        applyOffsets(layerNumStore);
        applyZoom(layerNumStore);

        notifyIfViewportChanged();

        var p1 = ctx.transformedPoint(0, 0);
        var p2 = ctx.transformedPoint(canvas.width, canvas.height);
        ctx.clearRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y);

        drawGrid();
        drawBoundingBox(layerNumStore);
        if (model && model.length) {
            if (layerNumStore < model.length) {
                if (renderOptions["showNextLayer"] && layerNumStore < model.length - 1) {
                    drawLayer(
                        layerNumStore + 1,
                        0,
                        GCODE.renderer.getLayerNumSegments(layerNumStore + 1),
                        true
                    );
                }
                if (renderOptions["showCurrentLayer"] && layerNumStore < model.length) {
                    drawLayer(
                        layerNumStore,
                        0,
                        GCODE.renderer.getLayerNumSegments(layerNumStore),
                        true
                    );
                }
                if (renderOptions["showPreviousLayer"] && layerNumStore > 0) {
                    drawLayer(
                        layerNumStore - 1,
                        0,
                        GCODE.renderer.getLayerNumSegments(layerNumStore - 1),
                        true
                    );
                }
                drawLayer(layerNumStore, progressStore.from, progressStore.to);
            } else {
                console.log("Got request to render non-existent layer");
            }
        }
    };

    function getLayerBounds(layer) {
        if (!model || !model[layer]) return;

        var cmds = model[layer];
        var firstExtrusion;
        var i;

        // find bounds based on x/y moves with extrusion only
        // if you want to change that criterion, this is the place to do it
        var factorIn = function (cmd) {
            return cmd && cmd.extrude && (cmd.x !== undefined || cmd.y !== undefined);
        };

        for (i = 0; i < cmds.length; i++) {
            if (factorIn(cmds[i])) break;
        }

        if (i === cmds.length) return;
        firstExtrusion = i;

        // initialize with guaranteed defined values and cut out a bunch of
        // testing for undefined cases
        var minX = cmds[firstExtrusion].prevX,
            maxX = cmds[firstExtrusion].prevX,
            minY = cmds[firstExtrusion].prevY,
            maxY = cmds[firstExtrusion].prevY;

        for (i = firstExtrusion; i < cmds.length; i++) {
            if (factorIn(cmds[i])) {
                minX = Math.min(minX, cmds[i].prevX);
                maxX = Math.max(maxX, cmds[i].prevX);
                if (cmds[i].x !== undefined) {
                    minX = Math.min(minX, cmds[i].x);
                    maxX = Math.max(maxX, cmds[i].x);
                }
                minY = Math.min(minY, cmds[i].prevY);
                maxY = Math.max(maxY, cmds[i].prevY);
                if (cmds[i].y !== undefined) {
                    minY = Math.min(minY, cmds[i].y);
                    maxY = Math.max(maxY, cmds[i].y);
                }
            }
        }

        return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
    }

    function trackTransforms(ctx) {
        var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        var xform = svg.createSVGMatrix();
        ctx.getTransform = function () {
            return xform;
        };

        var savedTransforms = [];
        var save = ctx.save;
        ctx.save = function () {
            savedTransforms.push(xform.translate(0, 0));
            return save.call(ctx);
        };
        var restore = ctx.restore;
        ctx.restore = function () {
            xform = savedTransforms.pop();
            viewportChanged = true;
            return restore.call(ctx);
        };

        var scale = ctx.scale;
        ctx.scale = function (sx, sy) {
            xform = xform.scaleNonUniform(sx, sy);
            viewportChanged = true;
            return scale.call(ctx, sx, sy);
        };
        var rotate = ctx.rotate;
        ctx.rotate = function (radians) {
            xform = xform.rotate((radians * 180) / Math.PI);
            viewportChanged = true;
            return rotate.call(ctx, radians);
        };
        var translate = ctx.translate;
        ctx.translate = function (dx, dy) {
            xform = xform.translate(dx, dy);
            viewportChanged = true;
            return translate.call(ctx, dx, dy);
        };
        var transform = ctx.transform;
        ctx.transform = function (a, b, c, d, e, f) {
            var m2 = svg.createSVGMatrix();
            m2.a = a;
            m2.b = b;
            m2.c = c;
            m2.d = d;
            m2.e = e;
            m2.f = f;
            xform = xform.multiply(m2);
            viewportChanged = true;
            return transform.call(ctx, a, b, c, d, e, f);
        };
        var setTransform = ctx.setTransform;
        ctx.setTransform = function (a, b, c, d, e, f) {
            xform.a = a;
            xform.b = b;
            xform.c = c;
            xform.d = d;
            xform.e = e;
            xform.f = f;
            viewportChanged = true;
            return setTransform.call(ctx, a, b, c, d, e, f);
        };
        var pt = svg.createSVGPoint();
        ctx.transformedPoint = function (x, y) {
            pt.x = x;
            pt.y = y;
            return pt.matrixTransform(xform.inverse());
        };
    }

    var startCanvas = function () {
        var jqueryCanvas = $(renderOptions["container"]);
        //jqueryCanvas.css("background-color", renderOptions["bgColorOffGrid"]);
        canvas = jqueryCanvas[0];

        ctx = canvas.getContext("2d");
        canvas.style.height = canvas.height + "px";
        canvas.style.width = canvas.width + "px";
        canvas.height = canvas.height * pixelRatio;
        canvas.width = canvas.width * pixelRatio;
        ctxHeight = canvas.height;
        ctxWidth = canvas.width;
        lastX = ctxWidth / 2;
        lastY = ctxHeight / 2;
        ctx.lineWidth = 2 * lineWidthFactor;
        ctx.lineCap = "round";
        trackTransforms(ctx);

        ctx.scale(1, -1); // Invert y-axis

        // dragging => translating
        canvas.addEventListener(
            "mousedown",
            function (event) {
                document.body.style.mozUserSelect = document.body.style.webkitUserSelect = document.body.style.userSelect =
                    "none";

                // remember starting point of dragging gesture
                lastX = (event.offsetX || event.pageX - canvas.offsetLeft) * pixelRatio;
                lastY = (event.offsetY || event.pageY - canvas.offsetTop) * pixelRatio;

                var pt = ctx.transformedPoint(lastX, lastY);
                if (
                    !renderOptions["onDragStart"] ||
                    renderOptions["onDragStart"](pt) !== false
                )
                    dragStart = pt;
            },
            false
        );

        canvas.addEventListener(
            "mousemove",
            function (event) {
                // save current mouse coordinates
                lastX = (event.offsetX || event.pageX - canvas.offsetLeft) * pixelRatio;
                lastY = (event.offsetY || event.pageY - canvas.offsetTop) * pixelRatio;

                // mouse movement => dragged
                if (dragStart !== undefined) {
                    // translate
                    var pt = ctx.transformedPoint(lastX, lastY);

                    if (renderOptions["onDrag"] && renderOptions["onDrag"](pt) === false)
                        return;

                    renderOptions["centerViewport"] = false;
                    renderOptions["zoomInOnModel"] = false;
                    renderOptions["zoomInOnBed"] = false;
                    offsetModelX = 0;
                    offsetModelY = 0;
                    offsetBedX = 0;
                    offsetBedY = 0;
                    scaleX = 1;
                    scaleY = 1;

                    ctx.translate(pt.x - dragStart.x, pt.y - dragStart.y);
                    reRender();

                    if (renderOptions["onInternalOptionChange"] !== undefined) {
                        renderOptions["onInternalOptionChange"]({
                            centerViewport: false,
                            moveModel: false,
                            zoomInOnModel: false,
                            zoomInOnBed: false
                        });
                    }
                }
            },
            false
        );

        canvas.addEventListener(
            "mouseup",
            function (event) {
                // reset dragStart
                dragStart = undefined;

                if (renderOptions["onDragStop"]) {
                    var x =
                        (event.offsetX || event.pageX - canvas.offsetLeft) * pixelRatio;
                    var y =
                        (event.offsetY || event.pageY - canvas.offsetTop) * pixelRatio;
                    renderOptions["onDragStop"](ctx.transformedPoint(x, y));
                }
            },
            false
        );

        // mouse wheel => zooming
        var zoom = function (clicks) {
            // focus on last mouse position prior to zoom
            var pt = ctx.transformedPoint(lastX, lastY);
            ctx.translate(pt.x, pt.y);

            // determine zooming factor and perform zoom
            var factor = Math.pow(scaleFactor, clicks);
            ctx.scale(factor, factor);

            // return to old position
            ctx.translate(-pt.x, -pt.y);

            // disable conflicting options
            renderOptions["zoomInOnModel"] = false;
            renderOptions["zoomInOnBed"] = false;
            offsetModelX = 0;
            offsetModelY = 0;
            offsetBedX = 0;
            offsetBedY = 0;
            scaleX = 1;
            scaleY = 1;

            // render
            reRender();

            if (renderOptions["onInternalOptionChange"] !== undefined) {
                renderOptions["onInternalOptionChange"]({
                    zoomInOnModel: false,
                    zoomInOnBed: false
                });
            }
        };
        var handleScroll = function (event) {
            var delta;

            // determine zoom direction & delta
            if (event.detail < 0 || event.wheelDelta > 0) {
                delta = zoomFactorDelta;
            } else {
                delta = -1 * zoomFactorDelta;
            }
            if (delta) zoom(delta);

            return event.preventDefault() && false;
        };
        canvas.addEventListener("DOMMouseScroll", handleScroll, false);
        canvas.addEventListener("mousewheel", handleScroll, false);
    };

    var drawGrid = function () {
        ctx.translate(offsetBedX, offsetBedY);
        if (renderOptions["bed"]["circular"]) {
            drawCircularGrid();
        } else {
            drawRectangularGrid();
        }
        ctx.translate(-offsetBedX, -offsetBedY);
    };

    var drawRectangularGrid = function () {
        var x, y;
        var width = renderOptions["bed"]["x"];
        var height = renderOptions["bed"]["y"];

        var minX, maxX, minY, maxY;
        if (renderOptions["bed"]["centeredOrigin"]) {
            var halfWidth = width / 2;
            var halfHeight = height / 2;

            minX = -halfWidth;
            maxX = halfWidth;
            minY = -halfHeight;
            maxY = halfHeight;
        } else {
            minX = 0;
            maxX = width;
            minY = 0;
            maxY = height;
        }

        //~ bed outline and origin
        ctx.beginPath();
        ctx.strokeStyle = renderOptions["colorGrid"];
        ctx.fillStyle = "#ffffff";
        ctx.lineWidth = 2 * lineWidthFactor;

        // outline
        ctx.rect(minX, minY, width, height);

        // origin
        ctx.moveTo(minX, 0);
        ctx.lineTo(maxX, 0);
        ctx.moveTo(0, minY);
        ctx.lineTo(0, maxY);

        // draw
        ctx.fill();
        ctx.stroke();

        // draw origin
        ctx.beginPath();
        ctx.arc(0, 0, 2, 0, Math.PI * 2, true);
        ctx.stroke();

        ctx.strokeStyle = renderOptions["colorGrid"];
        ctx.lineWidth = lineWidthFactor;

        //~~ grid starting from origin
        ctx.beginPath();
        for (x = 0; x <= maxX; x += gridStep) {
            ctx.moveTo(x, minY);
            ctx.lineTo(x, maxY);

            if (renderOptions["bed"]["centeredOrigin"]) {
                ctx.moveTo(-1 * x, minY);
                ctx.lineTo(-1 * x, maxY);
            }
        }
        ctx.stroke();

        ctx.beginPath();
        for (y = 0; y <= maxY; y += gridStep) {
            ctx.moveTo(minX, y);
            ctx.lineTo(maxX, y);

            if (renderOptions["bed"]["centeredOrigin"]) {
                ctx.moveTo(minX, -1 * y);
                ctx.lineTo(maxX, -1 * y);
            }
        }
        ctx.stroke();
    };

    var drawCircularGrid = function () {
        var i;

        ctx.strokeStyle = renderOptions["colorGrid"];
        ctx.fillStyle = "#ffffff";
        ctx.lineWidth = 2 * lineWidthFactor;

        //~~ bed outline & origin
        ctx.beginPath();

        // outline
        var r = renderOptions["bed"]["r"];
        ctx.arc(0, 0, r, 0, Math.PI * 2, true);

        // origin
        ctx.moveTo(-1 * r, 0);
        ctx.lineTo(r, 0);
        ctx.moveTo(0, r);
        ctx.lineTo(0, -1 * r);

        // draw
        ctx.fill();
        ctx.stroke();

        // draw origin
        ctx.beginPath();
        ctx.arc(0, 0, 2, 0, Math.PI * 2, true);
        ctx.stroke();

        ctx.strokeStyle = renderOptions["colorGrid"];
        ctx.lineWidth = lineWidthFactor;

        //~~ grid starting from origin
        ctx.beginPath();
        for (i = 0; i <= r; i += gridStep) {
            var x = i;
            var y = Math.sqrt(r * r - x * x);

            ctx.moveTo(x, -1 * y);
            ctx.lineTo(x, y);

            ctx.moveTo(y, -1 * x);
            ctx.lineTo(-1 * y, -1 * x);

            ctx.moveTo(-1 * x, -1 * y);
            ctx.lineTo(-1 * x, y);

            ctx.moveTo(y, x);
            ctx.lineTo(-1 * y, x);
        }
        ctx.stroke();
    };

    var drawBoundingBox = function (layerNum) {
        if (!modelInfo) return;

        var minX, minY, width, height;

        var draw = function (x, y, w, h, c) {
            ctx.beginPath();
            ctx.strokeStyle = c;
            ctx.setLineDash([2, 5]);

            ctx.rect(x, y, w, h);

            ctx.stroke();
        };

        if (renderOptions["showFullSize"]) {
            minX = modelInfo.min.x;
            minY = modelInfo.min.y;
            width = modelInfo.modelSize.x;
            height = modelInfo.modelSize.y;
            draw(minX, minY, width, height, "#0000ff");
        }

        if (renderOptions["showBoundingBox"]) {
            minX = modelInfo.boundingBox.minX;
            minY = modelInfo.boundingBox.minY;
            width = modelInfo.boundingBox.maxX - minX;
            height = modelInfo.boundingBox.maxY - minY;
            draw(minX, minY, width, height, "#ff0000");
        }

        if (renderOptions["showLayerBoundingBox"]) {
            var layerBounds = getLayerBounds(layerNum);
            if (layerBounds) {
                minX = layerBounds.minX;
                minY = layerBounds.minY;
                width = layerBounds.maxX - minX;
                height = layerBounds.maxY - minY;
                draw(minX, minY, width, height, "#00ff00");
            }
        }

        ctx.setLineDash([1, 0]);
    };

    var drawTriangle = function (centerX, centerY, length, up) {
        /*
         *             (cx,cy)
         *                *             ^
         *               / \            |
         *              /   \           |
         *             /     \          |
         *            / (x,y) \         | h
         *           /         \        |
         *          /           \       |
         *         /             \      |
         *        *---------------*     v
         *    (ax,ay)           (bx,by)
         */

        var ax, bx, cx, ay, by, cy;
        var h = Math.sqrt(0.75 * length * length) / 2;

        ax = centerX - length / 2;
        bx = ax + length;
        cx = centerX;

        if (up) {
            ay = centerY - h;
            by = centerY - h;
            cy = centerY + h;
        } else {
            ay = centerY + h;
            by = centerY + h;
            cy = centerY - h;
        }

        var origLineJoin = ctx.lineJoin;
        ctx.lineJoin = "miter";

        ctx.beginPath();
        ctx.moveTo(ax, ay);
        ctx.lineTo(bx, by);
        ctx.moveTo(bx, by);
        ctx.lineTo(cx, cy);
        ctx.lineTo(ax, ay);
        ctx.stroke();
        ctx.fill();

        ctx.lineJoin = origLineJoin;
    };

    var drawCross = function (centerX, centerY, size) {
        var x1, y1, x2, y2;

        var half = size / 2;
        x1 = centerX - half;
        x2 = centerX + half;
        y1 = centerY - half;
        y2 = centerY + half;

        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.moveTo(x1, y2);
        ctx.lineTo(x2, y1);
        ctx.stroke();
    };

    var drawLayer = function (layerNum, fromProgress, toProgress, isNotCurrentLayer) {
        log.trace(
            "Drawing layer " +
                layerNum +
                " from " +
                fromProgress +
                " to " +
                toProgress +
                " (current: " +
                !isNotCurrentLayer +
                ")"
        );

        var i;

        //~~ store current layer values

        isNotCurrentLayer = isNotCurrentLayer !== undefined ? isNotCurrentLayer : false;

        if (!model || !model[layerNum]) return;

        var cmds = model[layerNum];
        var x, y;

        //~~ find our initial prevX/prevY tuple

        if (cmds[0].prevX !== undefined && cmds[0].prevY !== undefined) {
            // command contains prevX/prevY values, use those
            prevX = cmds[0].prevX;
            prevY = cmds[0].prevY;
        } else if (fromProgress > 0) {
            // previous command in same layer exists, use x/y as prevX/prevY
            prevX = cmds[fromProgress - 1].x;
            prevY = cmds[fromProgress - 1].y;
        } else if (model[layerNum - 1]) {
            // previous layer exists, use last x/y as prevX/prevY
            prevX = undefined;
            prevY = undefined;
            var prevModelLayer = model[layerNum - 1];
            for (i = prevModelLayer.length - 1; i >= 0; i--) {
                if (prevX === undefined && prevModelLayer[i].x !== undefined) {
                    prevX = prevModelLayer[i].x;
                    if (prevY !== undefined) break;
                }
                if (prevY === undefined && prevModelLayer[i].y !== undefined) {
                    prevY = prevModelLayer[i].y;
                    if (prevX !== undefined) break;
                }
            }
        }

        // if we did not find prevX or prevY, set it to 0 (might be that we are on the first command of the first layer,
        // or it's just a very weird model...)
        if (prevX === undefined) prevX = 0;
        if (prevY === undefined) prevY = 0;

        //~~ render this layer's commands

        var sizeRetractSpot = renderOptions["sizeRetractSpot"] * lineWidthFactor * 2;
        var sizeSegmentStart = renderOptions["sizeSegmentStart"] * lineWidthFactor * 2;

        // alpha value (100% if current layer is being rendered, 30% otherwise)
        // Note - If showing currently layer as preview - also render it at 30% and draw the progress over the top at 100%
        var alpha =
            (renderOptions["showNextLayer"] ||
                renderOptions["showCurrentLayer"] ||
                renderOptions["showPreviousLayer"]) &&
            isNotCurrentLayer
                ? 0.3
                : 1.0;

        var colorLine = {};
        var colorMove = {};
        var colorRetract = {};
        var colorRestart = {};
        var colorSegmentStart = {};

        function getColorLineForTool(tool) {
            var rv = colorLine[tool];
            if (rv === undefined) {
                var lineColor = renderOptions["colorLine"][tool];
                if (lineColor === undefined) lineColor = renderOptions["colorLine"][0];
                var shade = tool * 0.15;
                rv = colorLine[tool] = pusher
                    .color(lineColor)
                    .shade(shade)
                    .alpha(alpha)
                    .html();
            }
            return rv;
        }

        function getColorMoveForTool(tool) {
            var rv = colorMove[tool];
            if (rv === undefined) {
                var shade = tool * 0.15;
                rv = colorMove[tool] = pusher
                    .color(renderOptions["colorMove"])
                    .shade(shade)
                    .alpha(alpha)
                    .html();
            }
            return rv;
        }

        function getColorRetractForTool(tool) {
            var rv = colorRetract[tool];
            if (rv === undefined) {
                var shade = tool * 0.15;
                rv = colorRetract[tool] = pusher
                    .color(renderOptions["colorRetract"])
                    .shade(shade)
                    .alpha(alpha)
                    .html();
            }
            return rv;
        }

        function getColorRestartForTool(tool) {
            var rv = colorRestart[tool];
            if (rv === undefined) {
                var shade = tool * 0.15;
                rv = colorRestart[tool] = pusher
                    .color(renderOptions["colorRestart"])
                    .shade(shade)
                    .alpha(alpha)
                    .html();
            }
            return rv;
        }

        function getColorSegmentStartForTool(tool) {
            var rv = colorSegmentStart[tool];
            if (rv === undefined) {
                var shade = tool * 0.15;
                rv = colorSegmentStart[tool] = pusher
                    .color(renderOptions["colorSegmentStart"])
                    .shade(shade)
                    .alpha(alpha)
                    .html();
            }
            return rv;
        }

        var prevPathType = "fill";
        function strokePathIfNeeded(newPathType, strokeStyle) {
            if (newPathType != prevPathType || newPathType == "fill") {
                if (prevPathType != "fill") {
                    ctx.stroke();
                }
                prevPathType = newPathType;

                ctx.beginPath();
                if (newPathType != "fill") {
                    ctx.strokeStyle = strokeStyle;
                    ctx.moveTo(prevX, prevY);
                }
            }
        }

        ctx.lineJoin = "round";

        for (i = fromProgress; i <= toProgress; i++) {
            if (typeof cmds[i] === "undefined") continue;
            var cmd = cmds[i];

            if (cmd.prevX !== undefined && cmd.prevY !== undefined) {
                // override new (prevX, prevY)
                prevX = cmd.prevX;
                prevY = cmd.prevY;
            }

            // new x
            if (cmd.x === undefined || isNaN(cmd.x)) {
                x = prevX;
            } else {
                x = cmd.x;
            }

            // new y
            if (cmd.y === undefined || isNaN(cmd.y)) {
                y = prevY;
            } else {
                y = cmd.y;
            }

            // current tool
            var tool = cmd.tool || 0;

            if (!cmd.extrude && !cmd.noMove) {
                // neither extrusion nor move
                if (cmd.retract == -1) {
                    // retract => draw dot if configured to do so
                    if (renderOptions["showRetracts"] && !isNotCurrentLayer) {
                        strokePathIfNeeded("fill");
                        ctx.fillStyle = getColorRetractForTool(tool);
                        ctx.strokeStyle = ctx.fillStyle;
                        drawTriangle(prevX, prevY, sizeRetractSpot, true);
                    }
                }

                strokePathIfNeeded("move", getColorMoveForTool(tool));
                if (renderOptions["showMoves"] && !isNotCurrentLayer) {
                    // move => draw line from (prevX, prevY) to (x, y) in move color
                    ctx.lineWidth = lineWidthFactor;
                    ctx.lineTo(x, y);
                }
            } else if (cmd.extrude) {
                if (cmd.retract == 0) {
                    // no retraction => real extrusion move, use tool color to draw line
                    strokePathIfNeeded("extrude", getColorLineForTool(tool));
                    ctx.lineWidth = renderOptions["extrusionWidth"] * lineWidthFactor;
                    if (cmd.direction !== undefined && cmd.direction != 0) {
                        var di = cmd.i;
                        var dj = cmd.j;
                        var centerX = prevX + di;
                        var centerY = prevY + dj;
                        var startAngle = Math.atan2(prevY - centerY, prevX - centerX);
                        var endAngle = Math.atan2(y - centerY, x - centerX);
                        var radius = Math.sqrt(di * di + dj * dj);
                        ctx.arc(
                            centerX,
                            centerY,
                            radius,
                            startAngle,
                            endAngle,
                            cmd.direction < 0
                        ); // Y-axis is inverted so direction is also inverted
                    } else {<commit_msg>🐛 GCV: Make layer bounds arc aware<commit_after>/**
 * User: hudbrog (<EMAIL>)
 * Date: 10/20/12
 * Time: 1:36 PM
 * To change this template use File | Settings | File Templates.
 */

GCODE.renderer = (function () {
    // ***** PRIVATE ******
    var canvas;
    var ctx;

    var viewportChanged = true;
    var lineWidthFactor = 1 / 2.8;

    var zoomFactorDelta = 0.4;
    var gridStep = 10;
    var ctxHeight, ctxWidth;
    var prevX = 0,
        prevY = 0;
    var pixelRatio = window.devicePixelRatio || 1;

    var layerNumStore,
        progressStore = {from: 0, to: -1};
    var lastX, lastY;
    var dragStart;
    var scaleFactor = 1.1;
    var model = undefined;
    var modelInfo = undefined;
    var initialized = false;
    var renderOptions = {
        colorGrid: "#bbbbbb",
        bgColorGrid: "#ffffff",
        bgColorOffGrid: "#eeeeee",
        colorLine: ["#000000", "#3333cc", "#cc3333", "#33cc33", "#cc33cc"],
        colorMove: "#00ff00",
        colorRetract: "#ff0000",
        colorRestart: "#0000ff",
        colorHead: "#00ff00",
        colorSegmentStart: "#666666",

        showMoves: true,
        showRetracts: true,
        extrusionWidth: 1 * pixelRatio,
        // #000000", "#45c7ba",  "#a9533a", "#ff44cc", "#dd1177", "#eeee22", "#ffbb55", "#ff5511", "#777788"
        sizeRetractSpot: 2 * pixelRatio,
        sizeHeadSpot: 2 * pixelRatio,
        modelCenter: {x: 0, y: 0},
        differentiateColors: true,
        showNextLayer: false,
        showCurrentLayer: false,
        showPreviousLayer: false,
        showBoundingBox: false,
        showLayerBoundingBox: false,
        showFullSize: false,
        showHead: false,
        showSegmentStarts: false,
        sizeSegmentStart: 2 * pixelRatio,

        moveModel: true,
        zoomInOnModel: false,
        zoomInOnBed: false,
        centerViewport: false,
        invertAxes: {x: false, y: false},

        bed: {x: 200, y: 200},
        container: undefined,

        onInternalOptionChange: undefined,

        onViewportChange: undefined,
        onDragStart: undefined, // Cancellable (return false)
        onDrag: undefined, // Cancellable (return false)
        onDragStop: undefined
    };

    var offsetModelX = 0,
        offsetModelY = 0;
    var offsetBedX = 0,
        offsetBedY = 0;
    var scaleX = 1,
        scaleY = 1;
    var speeds = [];
    var speedsByLayer = {};
    var currentInvertX = false,
        currentInvertY = false;

    var deg0 = 0.0;
    var deg90 = Math.PI / 2.0;
    var deg180 = Math.PI;
    var deg270 = Math.PI * 1.5;
    var deg360 = Math.PI * 2.0;

    function notifyIfViewportChanged() {
        if (viewportChanged) {
            if (renderOptions["onViewportChange"]) {
                renderOptions["onViewportChange"](ctx.getTransform());
            }
            viewportChanged = false;
        }
    }

    var reRender = function () {
        if (!model) return;

        log.debug(
            "Rerendering layer " +
                layerNumStore +
                " of " +
                model.length +
                " with " +
                GCODE.renderer.getLayerNumSegments(layerNumStore) +
                " segments"
        );

        applyOffsets(layerNumStore);
        applyZoom(layerNumStore);

        notifyIfViewportChanged();

        var p1 = ctx.transformedPoint(0, 0);
        var p2 = ctx.transformedPoint(canvas.width, canvas.height);
        ctx.clearRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y);

        drawGrid();
        drawBoundingBox(layerNumStore);
        if (model && model.length) {
            if (layerNumStore < model.length) {
                if (renderOptions["showNextLayer"] && layerNumStore < model.length - 1) {
                    drawLayer(
                        layerNumStore + 1,
                        0,
                        GCODE.renderer.getLayerNumSegments(layerNumStore + 1),
                        true
                    );
                }
                if (renderOptions["showCurrentLayer"] && layerNumStore < model.length) {
                    drawLayer(
                        layerNumStore,
                        0,
                        GCODE.renderer.getLayerNumSegments(layerNumStore),
                        true
                    );
                }
                if (renderOptions["showPreviousLayer"] && layerNumStore > 0) {
                    drawLayer(
                        layerNumStore - 1,
                        0,
                        GCODE.renderer.getLayerNumSegments(layerNumStore - 1),
                        true
                    );
                }
                drawLayer(layerNumStore, progressStore.from, progressStore.to);
            } else {
                console.log("Got request to render non-existent layer");
            }
        }
    };

    function getLayerBounds(layer) {
        if (!model || !model[layer]) return;

        var cmds = model[layer];
        var firstExtrusion;
        var i;

        // find bounds based on x/y moves with extrusion only
        // if you want to change that criterion, this is the place to do it
        var factorIn = function (cmd) {
            return cmd && cmd.extrude && (cmd.x !== undefined || cmd.y !== undefined);
        };

        for (i = 0; i < cmds.length; i++) {
            if (factorIn(cmds[i])) break;
        }

        if (i === cmds.length) return;
        firstExtrusion = i;

        // initialize with guaranteed defined values and cut out a bunch of
        // testing for undefined cases
        var minX = cmds[firstExtrusion].prevX,
            maxX = cmds[firstExtrusion].prevX,
            minY = cmds[firstExtrusion].prevY,
            maxY = cmds[firstExtrusion].prevY;

        for (i = firstExtrusion; i < cmds.length; i++) {
            if (factorIn(cmds[i])) {
                minX = Math.min(minX, cmds[i].prevX);
                maxX = Math.max(maxX, cmds[i].prevX);
                if (cmds[i].x !== undefined) {
                    minX = Math.min(minX, cmds[i].x);
                    maxX = Math.max(maxX, cmds[i].x);
                }
                minY = Math.min(minY, cmds[i].prevY);
                maxY = Math.max(maxY, cmds[i].prevY);
                if (cmds[i].y !== undefined) {
                    minY = Math.min(minY, cmds[i].y);
                    maxY = Math.max(maxY, cmds[i].y);
                }
                if (!!cmds[i].direction) {
                    var dir = cmds[i].direction;
                    var arc = getArcParams(cmds[i]);

                    var startAngle, endAngle;
                    if (dir < 0) {
                        // cw: start = start and end = end
                        startAngle = arc.startAngle;
                        endAngle = arc.endAngle;
                    } else {
                        // ccw: start = end and end = start for clockwise
                        startAngle = arc.endAngle;
                        endAngle = arc.startAngle;
                    }

                    if (startAngle < 0) startAngle += deg360;
                    if (endAngle < 0) endAngle += deg360;

                    // from now on we only think in clockwise direction

                    if (endAngle > startAngle) {
                        // arc crosses positive x
                        maxX = Math.max(maxX, arc.x + arc.r);
                    }

                    if (
                        (startAngle >= deg180 &&
                            (endAngle <= deg180 || endAngle > startAngle)) ||
                        (startAngle <= deg180 &&
                            endAngle <= deg180 &&
                            endAngle > startAngle)
                    ) {
                        // arc crosses negative x
                        minX = Math.min(minX, arc.x - arc.r);
                    }

                    if (
                        (startAngle >= deg90 &&
                            (endAngle <= deg90 || endAngle > startAngle)) ||
                        (startAngle <= deg90 &&
                            endAngle <= deg90 &&
                            endAngle > startAngle)
                    ) {
                        // arc crosses positive y
                        maxY = Math.max(maxY, arc.y + arc.r);
                    }

                    if (
                        (startAngle >= deg270 &&
                            (endAngle <= deg270 || endAngle > startAngle)) ||
                        (startAngle <= deg270 &&
                            endAngle <= deg270 &&
                            endAngle > startAngle)
                    ) {
                        // arc crosses negative y
                        minY = Math.min(minY, arc.y - arc.r);
                    }
                }
            }
        }

        return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
    }

    function getArcParams(cmd) {
        var x = cmd.x !== undefined ? cmd.x : cmd.prevX;
        var y = cmd.y !== undefined ? cmd.y : cmd.prevY;

        var centerX = cmd.prevX + cmd.i;
        var centerY = cmd.prevY + cmd.j;
        return {
            x: centerX,
            y: centerY,
            r: Math.sqrt(cmd.i * cmd.i + cmd.j * cmd.j),
            startAngle: Math.atan2(cmd.prevY - centerY, cmd.prevX - centerX),
            endAngle: Math.atan2(y - centerY, x - centerX),
            startX: cmd.prevX,
            startY: cmd.prevY,
            endX: x,
            endY: y
        };
    }

    function trackTransforms(ctx) {
        var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        var xform = svg.createSVGMatrix();
        ctx.getTransform = function () {
            return xform;
        };

        var savedTransforms = [];
        var save = ctx.save;
        ctx.save = function () {
            savedTransforms.push(xform.translate(0, 0));
            return save.call(ctx);
        };
        var restore = ctx.restore;
        ctx.restore = function () {
            xform = savedTransforms.pop();
            viewportChanged = true;
            return restore.call(ctx);
        };

        var scale = ctx.scale;
        ctx.scale = function (sx, sy) {
            xform = xform.scaleNonUniform(sx, sy);
            viewportChanged = true;
            return scale.call(ctx, sx, sy);
        };
        var rotate = ctx.rotate;
        ctx.rotate = function (radians) {
            xform = xform.rotate((radians * 180) / Math.PI);
            viewportChanged = true;
            return rotate.call(ctx, radians);
        };
        var translate = ctx.translate;
        ctx.translate = function (dx, dy) {
            xform = xform.translate(dx, dy);
            viewportChanged = true;
            return translate.call(ctx, dx, dy);
        };
        var transform = ctx.transform;
        ctx.transform = function (a, b, c, d, e, f) {
            var m2 = svg.createSVGMatrix();
            m2.a = a;
            m2.b = b;
            m2.c = c;
            m2.d = d;
            m2.e = e;
            m2.f = f;
            xform = xform.multiply(m2);
            viewportChanged = true;
            return transform.call(ctx, a, b, c, d, e, f);
        };
        var setTransform = ctx.setTransform;
        ctx.setTransform = function (a, b, c, d, e, f) {
            xform.a = a;
            xform.b = b;
            xform.c = c;
            xform.d = d;
            xform.e = e;
            xform.f = f;
            viewportChanged = true;
            return setTransform.call(ctx, a, b, c, d, e, f);
        };
        var pt = svg.createSVGPoint();
        ctx.transformedPoint = function (x, y) {
            pt.x = x;
            pt.y = y;
            return pt.matrixTransform(xform.inverse());
        };
    }

    var startCanvas = function () {
        var jqueryCanvas = $(renderOptions["container"]);
        //jqueryCanvas.css("background-color", renderOptions["bgColorOffGrid"]);
        canvas = jqueryCanvas[0];

        ctx = canvas.getContext("2d");
        canvas.style.height = canvas.height + "px";
        canvas.style.width = canvas.width + "px";
        canvas.height = canvas.height * pixelRatio;
        canvas.width = canvas.width * pixelRatio;
        ctxHeight = canvas.height;
        ctxWidth = canvas.width;
        lastX = ctxWidth / 2;
        lastY = ctxHeight / 2;
        ctx.lineWidth = 2 * lineWidthFactor;
        ctx.lineCap = "round";
        trackTransforms(ctx);

        ctx.scale(1, -1); // Invert y-axis

        // dragging => translating
        canvas.addEventListener(
            "mousedown",
            function (event) {
                document.body.style.mozUserSelect = document.body.style.webkitUserSelect = document.body.style.userSelect =
                    "none";

                // remember starting point of dragging gesture
                lastX = (event.offsetX || event.pageX - canvas.offsetLeft) * pixelRatio;
                lastY = (event.offsetY || event.pageY - canvas.offsetTop) * pixelRatio;

                var pt = ctx.transformedPoint(lastX, lastY);
                if (
                    !renderOptions["onDragStart"] ||
                    renderOptions["onDragStart"](pt) !== false
                )
                    dragStart = pt;
            },
            false
        );

        canvas.addEventListener(
            "mousemove",
            function (event) {
                // save current mouse coordinates
                lastX = (event.offsetX || event.pageX - canvas.offsetLeft) * pixelRatio;
                lastY = (event.offsetY || event.pageY - canvas.offsetTop) * pixelRatio;

                // mouse movement => dragged
                if (dragStart !== undefined) {
                    // translate
                    var pt = ctx.transformedPoint(lastX, lastY);

                    if (renderOptions["onDrag"] && renderOptions["onDrag"](pt) === false)
                        return;

                    renderOptions["centerViewport"] = false;
                    renderOptions["zoomInOnModel"] = false;
                    renderOptions["zoomInOnBed"] = false;
                    offsetModelX = 0;
                    offsetModelY = 0;
                    offsetBedX = 0;
                    offsetBedY = 0;
                    scaleX = 1;
                    scaleY = 1;

                    ctx.translate(pt.x - dragStart.x, pt.y - dragStart.y);
                    reRender();

                    if (renderOptions["onInternalOptionChange"] !== undefined) {
                        renderOptions["onInternalOptionChange"]({
                            centerViewport: false,
                            moveModel: false,
                            zoomInOnModel: false,
                            zoomInOnBed: false
                        });
                    }
                }
            },
            false
        );

        canvas.addEventListener(
            "mouseup",
            function (event) {
                // reset dragStart
                dragStart = undefined;

                if (renderOptions["onDragStop"]) {
                    var x =
                        (event.offsetX || event.pageX - canvas.offsetLeft) * pixelRatio;
                    var y =
                        (event.offsetY || event.pageY - canvas.offsetTop) * pixelRatio;
                    renderOptions["onDragStop"](ctx.transformedPoint(x, y));
                }
            },
            false
        );

        // mouse wheel => zooming
        var zoom = function (clicks) {
            // focus on last mouse position prior to zoom
            var pt = ctx.transformedPoint(lastX, lastY);
            ctx.translate(pt.x, pt.y);

            // determine zooming factor and perform zoom
            var factor = Math.pow(scaleFactor, clicks);
            ctx.scale(factor, factor);

            // return to old position
            ctx.translate(-pt.x, -pt.y);

            // disable conflicting options
            renderOptions["zoomInOnModel"] = false;
            renderOptions["zoomInOnBed"] = false;
            offsetModelX = 0;
            offsetModelY = 0;
            offsetBedX = 0;
            offsetBedY = 0;
            scaleX = 1;
            scaleY = 1;

            // render
            reRender();

            if (renderOptions["onInternalOptionChange"] !== undefined) {
                renderOptions["onInternalOptionChange"]({
                    zoomInOnModel: false,
                    zoomInOnBed: false
                });
            }
        };
        var handleScroll = function (event) {
            var delta;

            // determine zoom direction & delta
            if (event.detail < 0 || event.wheelDelta > 0) {
                delta = zoomFactorDelta;
            } else {
                delta = -1 * zoomFactorDelta;
            }
            if (delta) zoom(delta);

            return event.preventDefault() && false;
        };
        canvas.addEventListener("DOMMouseScroll", handleScroll, false);
        canvas.addEventListener("mousewheel", handleScroll, false);
    };

    var drawGrid = function () {
        ctx.translate(offsetBedX, offsetBedY);
        if (renderOptions["bed"]["circular"]) {
            drawCircularGrid();
        } else {
            drawRectangularGrid();
        }
        ctx.translate(-offsetBedX, -offsetBedY);
    };

    var drawRectangularGrid = function () {
        var x, y;
        var width = renderOptions["bed"]["x"];
        var height = renderOptions["bed"]["y"];

        var minX, maxX, minY, maxY;
        if (renderOptions["bed"]["centeredOrigin"]) {
            var halfWidth = width / 2;
            var halfHeight = height / 2;

            minX = -halfWidth;
            maxX = halfWidth;
            minY = -halfHeight;
            maxY = halfHeight;
        } else {
            minX = 0;
            maxX = width;
            minY = 0;
            maxY = height;
        }

        //~ bed outline and origin
        ctx.beginPath();
        ctx.strokeStyle = renderOptions["colorGrid"];
        ctx.fillStyle = "#ffffff";
        ctx.lineWidth = 2 * lineWidthFactor;

        // outline
        ctx.rect(minX, minY, width, height);

        // origin
        ctx.moveTo(minX, 0);
        ctx.lineTo(maxX, 0);
        ctx.moveTo(0, minY);
        ctx.lineTo(0, maxY);

        // draw
        ctx.fill();
        ctx.stroke();

        // draw origin
        ctx.beginPath();
        ctx.arc(0, 0, 2, 0, Math.PI * 2, true);
        ctx.stroke();

        ctx.strokeStyle = renderOptions["colorGrid"];
        ctx.lineWidth = lineWidthFactor;

        //~~ grid starting from origin
        ctx.beginPath();
        for (x = 0; x <= maxX; x += gridStep) {
            ctx.moveTo(x, minY);
            ctx.lineTo(x, maxY);

            if (renderOptions["bed"]["centeredOrigin"]) {
                ctx.moveTo(-1 * x, minY);
                ctx.lineTo(-1 * x, maxY);
            }
        }
        ctx.stroke();

        ctx.beginPath();
        for (y = 0; y <= maxY; y += gridStep) {
            ctx.moveTo(minX, y);
            ctx.lineTo(maxX, y);

            if (renderOptions["bed"]["centeredOrigin"]) {
                ctx.moveTo(minX, -1 * y);
                ctx.lineTo(maxX, -1 * y);
            }
        }
        ctx.stroke();
    };

    var drawCircularGrid = function () {
        var i;

        ctx.strokeStyle = renderOptions["colorGrid"];
        ctx.fillStyle = "#ffffff";
        ctx.lineWidth = 2 * lineWidthFactor;

        //~~ bed outline & origin
        ctx.beginPath();

        // outline
        var r = renderOptions["bed"]["r"];
        ctx.arc(0, 0, r, 0, Math.PI * 2, true);

        // origin
        ctx.moveTo(-1 * r, 0);
        ctx.lineTo(r, 0);
        ctx.moveTo(0, r);
        ctx.lineTo(0, -1 * r);

        // draw
        ctx.fill();
        ctx.stroke();

        // draw origin
        ctx.beginPath();
        ctx.arc(0, 0, 2, 0, Math.PI * 2, true);
        ctx.stroke();

        ctx.strokeStyle = renderOptions["colorGrid"];
        ctx.lineWidth = lineWidthFactor;

        //~~ grid starting from origin
        ctx.beginPath();
        for (i = 0; i <= r; i += gridStep) {
            var x = i;
            var y = Math.sqrt(r * r - x * x);

            ctx.moveTo(x, -1 * y);
            ctx.lineTo(x, y);

            ctx.moveTo(y, -1 * x);
            ctx.lineTo(-1 * y, -1 * x);

            ctx.moveTo(-1 * x, -1 * y);
            ctx.lineTo(-1 * x, y);

            ctx.moveTo(y, x);
            ctx.lineTo(-1 * y, x);
        }
        ctx.stroke();
    };

    var drawBoundingBox = function (layerNum) {
        if (!modelInfo) return;

        var minX, minY, width, height;

        var draw = function (x, y, w, h, c) {
            ctx.beginPath();
            ctx.strokeStyle = c;
            ctx.setLineDash([2, 5]);

            ctx.rect(x, y, w, h);

            ctx.stroke();
        };

        if (renderOptions["showFullSize"]) {
            minX = modelInfo.min.x;
            minY = modelInfo.min.y;
            width = modelInfo.modelSize.x;
            height = modelInfo.modelSize.y;
            draw(minX, minY, width, height, "#0000ff");
        }

        if (renderOptions["showBoundingBox"]) {
            minX = modelInfo.boundingBox.minX;
            minY = modelInfo.boundingBox.minY;
            width = modelInfo.boundingBox.maxX - minX;
            height = modelInfo.boundingBox.maxY - minY;
            draw(minX, minY, width, height, "#ff0000");
        }

        if (renderOptions["showLayerBoundingBox"]) {
            var layerBounds = getLayerBounds(layerNum);
            if (layerBounds) {
                minX = layerBounds.minX;
                minY = layerBounds.minY;
                width = layerBounds.maxX - minX;
                height = layerBounds.maxY - minY;
                draw(minX, minY, width, height, "#00ff00");
            }
        }

        ctx.setLineDash([1, 0]);
    };

    var drawTriangle = function (centerX, centerY, length, up) {
        /*
         *             (cx,cy)
         *                *             ^
         *               / \            |
         *              /   \           |
         *             /     \          |
         *            / (x,y) \         | h
         *           /         \        |
         *          /           \       |
         *         /             \      |
         *        *---------------*     v
         *    (ax,ay)           (bx,by)
         */

        var ax, bx, cx, ay, by, cy;
        var h = Math.sqrt(0.75 * length * length) / 2;

        ax = centerX - length / 2;
        bx = ax + length;
        cx = centerX;

        if (up) {
            ay = centerY - h;
            by = centerY - h;
            cy = centerY + h;
        } else {
            ay = centerY + h;
            by = centerY + h;
            cy = centerY - h;
        }

        var origLineJoin = ctx.lineJoin;
        ctx.lineJoin = "miter";

        ctx.beginPath();
        ctx.moveTo(ax, ay);
        ctx.lineTo(bx, by);
        ctx.moveTo(bx, by);
        ctx.lineTo(cx, cy);
        ctx.lineTo(ax, ay);
        ctx.stroke();
        ctx.fill();

        ctx.lineJoin = origLineJoin;
    };

    var drawCross = function (centerX, centerY, size) {
        var x1, y1, x2, y2;

        var half = size / 2;
        x1 = centerX - half;
        x2 = centerX + half;
        y1 = centerY - half;
        y2 = centerY + half;

        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.moveTo(x1, y2);
        ctx.lineTo(x2, y1);
        ctx.stroke();
    };

    var drawLayer = function (layerNum, fromProgress, toProgress, isNotCurrentLayer) {
        log.trace(
            "Drawing layer " +
                layerNum +
                " from " +
                fromProgress +
                " to " +
                toProgress +
                " (current: " +
                !isNotCurrentLayer +
                ")"
        );

        var i;

        //~~ store current layer values

        isNotCurrentLayer = isNotCurrentLayer !== undefined ? isNotCurrentLayer : false;

        if (!model || !model[layerNum]) return;

        var cmds = model[layerNum];
        var x, y;

        //~~ find our initial prevX/prevY tuple

        if (cmds[0].prevX !== undefined && cmds[0].prevY !== undefined) {
            // command contains prevX/prevY values, use those
            prevX = cmds[0].prevX;
            prevY = cmds[0].prevY;
        } else if (fromProgress > 0) {
            // previous command in same layer exists, use x/y as prevX/prevY
            prevX = cmds[fromProgress - 1].x;
            prevY = cmds[fromProgress - 1].y;
        } else if (model[layerNum - 1]) {
            // previous layer exists, use last x/y as prevX/prevY
            prevX = undefined;
            prevY = undefined;
            var prevModelLayer = model[layerNum - 1];
            for (i = prevModelLayer.length - 1; i >= 0; i--) {
                if (prevX === undefined && prevModelLayer[i].x !== undefined) {
                    prevX = prevModelLayer[i].x;
                    if (prevY !== undefined) break;
                }
                if (prevY === undefined && prevModelLayer[i].y !== undefined) {
                    prevY = prevModelLayer[i].y;
                    if (prevX !== undefined) break;
                }
            }
        }

        // if we did not find prevX or prevY, set it to 0 (might be that we are on the first command of the first layer,
        // or it's just a very weird model...)
        if (prevX === undefined) prevX = 0;
        if (prevY === undefined) prevY = 0;

        //~~ render this layer's commands

        var sizeRetractSpot = renderOptions["sizeRetractSpot"] * lineWidthFactor * 2;
        var sizeSegmentStart = renderOptions["sizeSegmentStart"] * lineWidthFactor * 2;

        // alpha value (100% if current layer is being rendered, 30% otherwise)
        // Note - If showing currently layer as preview - also render it at 30% and draw the progress over the top at 100%
        var alpha =
            (renderOptions["showNextLayer"] ||
                renderOptions["showCurrentLayer"] ||
                renderOptions["showPreviousLayer"]) &&
            isNotCurrentLayer
                ? 0.3
                : 1.0;

        var colorLine = {};
        var colorMove = {};
        var colorRetract = {};
        var colorRestart = {};
        var colorSegmentStart = {};

        function getColorLineForTool(tool) {
            var rv = colorLine[tool];
            if (rv === undefined) {
                var lineColor = renderOptions["colorLine"][tool];
                if (lineColor === undefined) lineColor = renderOptions["colorLine"][0];
                var shade = tool * 0.15;
                rv = colorLine[tool] = pusher
                    .color(lineColor)
                    .shade(shade)
                    .alpha(alpha)
                    .html();
            }
            return rv;
        }

        function getColorMoveForTool(tool) {
            var rv = colorMove[tool];
            if (rv === undefined) {
                var shade = tool * 0.15;
                rv = colorMove[tool] = pusher
                    .color(renderOptions["colorMove"])
                    .shade(shade)
                    .alpha(alpha)
                    .html();
            }
            return rv;
        }

        function getColorRetractForTool(tool) {
            var rv = colorRetract[tool];
            if (rv === undefined) {
                var shade = tool * 0.15;
                rv = colorRetract[tool] = pusher
                    .color(renderOptions["colorRetract"])
                    .shade(shade)
                    .alpha(alpha)
                    .html();
            }
            return rv;
        }

        function getColorRestartForTool(tool) {
            var rv = colorRestart[tool];
            if (rv === undefined) {
                var shade = tool * 0.15;
                rv = colorRestart[tool] = pusher
                    .color(renderOptions["colorRestart"])
                    .shade(shade)
                    .alpha(alpha)
                    .html();
            }
            return rv;
        }

        function getColorSegmentStartForTool(tool) {
            var rv = colorSegmentStart[tool];
            if (rv === undefined) {
                var shade = tool * 0.15;
                rv = colorSegmentStart[tool] = pusher
                    .color(renderOptions["colorSegmentStart"])
                    .shade(shade)
                    .alpha(alpha)
                    .html();
            }
            return rv;
        }

        var prevPathType = "fill";
        function strokePathIfNeeded(newPathType, strokeStyle) {
            if (newPathType != prevPathType || newPathType == "fill") {
                if (prevPathType != "fill") {
                    ctx.stroke();
                }
                prevPathType = newPathType;

                ctx.beginPath();
                if (newPathType != "fill") {
                    ctx.strokeStyle = strokeStyle;
                    ctx.moveTo(prevX, prevY);
                }
            }
        }

        ctx.lineJoin = "round";

        for (i = fromProgress; i <= toProgress; i++) {
            if (typeof cmds[i] === "undefined") continue;
            var cmd = cmds[i];

            if (cmd.prevX !== undefined && cmd.prevY !== undefined) {
                // override new (prevX, prevY)
                prevX = cmd.prevX;
                prevY = cmd.prevY;
            }

            // new x
            if (cmd.x === undefined || isNaN(cmd.x)) {
                x = prevX;
            } else {
                x = cmd.x;
            }

            // new y
            if (cmd.y === undefined || isNaN(cmd.y)) {
                y = prevY;
            } else {
                y = cmd.y;
            }

            // current tool
            var tool = cmd.tool || 0;

            if (!cmd.extrude && !cmd.noMove) {
                // neither extrusion nor move
                if (cmd.retract == -1) {
                    // retract => draw dot if configured to do so
                    if (renderOptions["showRetracts"] && !isNotCurrentLayer) {
                        strokePathIfNeeded("fill");
                        ctx.fillStyle = getColorRetractForTool(tool);
                        ctx.strokeStyle = ctx.fillStyle;
                        drawTriangle(prevX, prevY, sizeRetractSpot, true);
                    }
                }

                strokePathIfNeeded("move", getColorMoveForTool(tool));
                if (renderOptions["showMoves"] && !isNotCurrentLayer) {
                    // move => draw line from (prevX, prevY) to (x, y) in move color
                    ctx.lineWidth = lineWidthFactor;
                    ctx.lineTo(x, y);
                }
            } else if (cmd.extrude) {
                if (cmd.retract == 0) {
                    // no retraction => real extrusion move, use tool color to draw line
                    strokePathIfNeeded("extrude", getColorLineForTool(tool));
                    ctx.lineWidth = renderOptions["extrusionWidth"] * lineWidthFactor;
                    if (cmd.direction !== undefined && cmd.direction !== 0) {
                        var arc = getArcParams(cmd);
                        ctx.arc(
                            arc.x,
                            arc.y,
                            arc.r,
                            arc.startAngle,
                            arc.endAngle,
                            cmd.direction < 0
                        ); // Y-axis is inverted so direction is also inverted
                    } else {
{'fraction_non_alphanumeric': 0.11465418104156398, 'fraction_numerical': 0.011253491046492524, 'mean_word_length': 1.2509799571037645, 'pattern_counts': {'":': 0, '<': 39, '<?xml version=': 0, '>': 35, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 16, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2368775', 'n_tokens_mistral': 17589, 'n_tokens_neox': 16387, 'n_words': 3925}