<commit_before>// Copyright (c) 2014-16 <NAME>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the The GNU Affero General Public
// License as published by the Free Software Foundation; either
// version 3 of the License, or (at your option) any later version.
//
// You should have received a copy of the GNU Affero General Public
// License along with this library; if not, write to the Free Software
// Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA

// Turtles
const DEFAULTCOLOR = 0;
const DEFAULTVALUE = 50;
const DEFAULTCHROMA = 100;
const DEFAULTSTROKE = 5;
const DEFAULTFONT = 'sans-serif';

// Turtle sprite
const TURTLEBASEPATH = 'images/';

function Turtle (name, turtles, drum) {
    this.name = name;
    this.turtles = turtles;
    this.drum = drum;

    if (drum) {
        console.log('turtle ' + name + ' is a drum.');
    }
    // Is the turtle running?
    this.running = false;

    // In the trash?
    this.trash = false;

    // Things used for drawing the turtle.
    this.container = null;
    this.x = 0;
    this.y = 0;
    this.bitmap = null;
    this.skinChanged = false;  // Should we reskin the turtle on clear?

    // Which start block is assocated with this turtle?
    this.startBlock = null;
    this.decorationBitmap = null;  // Start block decoration.

    // Queue of blocks this turtle is executing.
    this.queue = [];

    // Listeners
    this.listeners = {};

    // Things used for what the turtle draws.
    this.penstrokes = null;
    this.imageContainer = null;
    this.svgOutput = '';
    // Are we currently drawing a path?
    this.svgPath = false;
    this.color = DEFAULTCOLOR;
    this.value = DEFAULTVALUE;
    this.chroma = DEFAULTCHROMA;
    this.stroke = DEFAULTSTROKE;
    this.canvasColor = 'rgba(255,0,49,1)'; // '#ff0031';
    this.canvasAlpha = 1.0;
    this.orientation = 0;
    this.fillState = false;
    this.hollowState = false;
    this.penState = true;
    this.font = DEFAULTFONT;
    this.media = [];  // Media (text, images) we need to remove on clear.
    var canvas = document.getElementById("overlayCanvas");
    var ctx = canvas.getContext("2d");
    // Simulate an arc with line segments since Tinkercad cannot
    // import SVG arcs reliably.
    this._svgArc = function(nsteps, cx, cy, radius, sa, ea) {
        var a = sa;
        if (ea == null) {
            var da = Math.PI / nsteps;
        } else {
            var da = (ea - sa) / nsteps;
        }
        for (var i = 0; i < nsteps; i++) {
            var nx = cx + radius * Math.cos(a);
            var ny = cy + radius * Math.sin(a);
            this.svgOutput += nx + ',' + ny + ' ';
            a += da;
        }
    };

    this.doBezier = function(cp1x, cp1y, cp2x, cp2y, x2, y2) {
        // FIXME: Add SVG output
        if (this.penState && this.hollowState) {
            // Convert from turtle coordinates to screen coordinates.
            var nx = x2;
            var ny = y2;
            var ix = this.turtles.turtleX2screenX(this.x);
            var iy = this.turtles.turtleY2screenY(this.y);
            var fx = this.turtles.turtleX2screenX(x2);
            var fy = this.turtles.turtleY2screenY(y2);
            var cx1 = this.turtles.turtleX2screenX(cp1x);
            var cy1 = this.turtles.turtleY2screenY(cp1y);
            var cx2 = this.turtles.turtleX2screenX(cp2x);
            var cy2 = this.turtles.turtleY2screenY(cp2y);

            // First, we need to close the current SVG path.
            this.closeSVG();

            // Save the current stroke width.
            var savedStroke = this.stroke;
            this.stroke = 1;
            ctx.lineWidth = this.stroke;
            ctx.lineCap = "round";
            // Draw a hollow line.
            if (savedStroke < 3) {
                var step = 0.5;
            } else {
                var step = (savedStroke - 2) / 2.;
            }

            steps = Math.max(Math.floor(savedStroke, 1));

            // We need both the initial and final headings.
            // The initial heading is the angle between (cp1x, cp1y) and (this.x, this.y).
            var degreesInitial = Math.atan2(cp1x - this.x, cp1y - this.y);
            degreesInitial = (180 * degreesInitial / Math.PI);
            if (degreesInitial < 0) { degreesInitial += 360; }
            // The final heading is the angle between (cp2x, cp2y) and (fx, fy).
            var degreesFinal = Math.atan2(nx - cp2x, ny - cp2y);
            degreesFinal = 180 * degreesFinal / Math.PI;
            if (degreesFinal < 0) { degreesFinal += 360; }

            // We also need to calculate the deltas for the "caps" at each end.
            var capAngleRadiansInitial = (degreesInitial - 90) * Math.PI / 180.0;
            var dxi = step * Math.sin(capAngleRadiansInitial);
            var dyi = -step * Math.cos(capAngleRadiansInitial);
            var capAngleRadiansFinal = (degreesFinal - 90) * Math.PI / 180.0;
            var dxf = step * Math.sin(capAngleRadiansFinal);
            var dyf = -step * Math.cos(capAngleRadiansFinal);

            // The four "corners"
            var ax = ix - dxi;
            var ay = iy - dyi;
            var axScaled = ax * this.turtles.scale;
            var ayScaled = ay * this.turtles.scale;
            var bx = fx - dxf;
            var by = fy - dyf;
            var bxScaled = bx * this.turtles.scale;
            var byScaled = by * this.turtles.scale;
            var cx = fx + dxf;
            var cy = fy + dyf;
            var cxScaled = cx * this.turtles.scale;
            var cyScaled = cy * this.turtles.scale;
            var dx = ix + dxi;
            var dy = iy + dyi;
            var dxScaled = dx * this.turtles.scale;
            var dyScaled = dy * this.turtles.scale;

            // Control points scaled for SVG output
            var cx1Scaled = (cx1 + dxi)* this.turtles.scale;
            var cy1Scaled = (cy1 + dyi) * this.turtles.scale;
            var cx2Scaled = (cx2 + dxf) * this.turtles.scale;
            var cy2Scaled = (cy2 + dyf) * this.turtles.scale;

            ctx.moveTo(ax, ay);
            this.svgPath = true;
            this.svgOutput += '<path d="M ' + axScaled + ',' + ayScaled + ' ';

            // Initial arc
            var oAngleRadians = ((180 + degreesInitial) / 180) * Math.PI;
            var arccx = ix;
            var arccy = iy;
            var sa = oAngleRadians - Math.PI;
            var ea = oAngleRadians;
            ctx.arc(arccx, arccy, step, sa, ea, false);
            this._svgArc(steps, arccx * this.turtles.scale, arccy * this.turtles.scale, step * this.turtles.scale, sa, ea);

            // Initial bezier curve
            ctx.bezierCurveTo(cx1 + dxi, cy1 + dyi , cx2 + dxf, cy2 + dyf, cx, cy);
            this.svgOutput += 'C ' + cx1Scaled + ',' + cy1Scaled + ' ' + cx2Scaled + ',' + cy2Scaled + ' ' + cxScaled + ',' + cyScaled + ' ';

            this.svgOutput += 'M ' + cxScaled + ',' + cyScaled + ' ';

            // Final arc
            var oAngleRadians = (degreesFinal / 180) * Math.PI;
            var arccx = fx;
            var arccy = fy;
            var sa = oAngleRadians - Math.PI;
            var ea = oAngleRadians;
            ctx.arc(arccx, arccy, step, sa, ea, false);
            this._svgArc(steps, arccx * this.turtles.scale, arccy * this.turtles.scale, step * this.turtles.scale, sa, ea);

            // Final bezier curve
            ctx.bezierCurveTo(cx2 - dxf, cy2 - dyf, cx1 - dxi, cy1 - dyi, ax, ay);
            this.svgOutput += 'C ' + cx2Scaled + ',' + cy2Scaled + ' ' + cx1Scaled + ',' + cy1Scaled + ' ' + axScaled + ',' + ayScaled + ' ';
            this.closeSVG();

            ctx.stroke();
            if (!this.fillState) {
                ctx.closePath();
            }
            // restore stroke.
            this.stroke = savedStroke;
            ctx.lineWidth = this.stroke;
            ctx.lineCap = "round";
            ctx.moveTo(fx,fy);
            this.x = x2;
            this.y = y2;
        } else if (this.penState) {
            if (this.canvasColor[0] === "#") {
                this.canvasColor = hex2rgb(this.canvasColor.split("#")[1]);
            }
            var subrgb = this.canvasColor.substr(0, this.canvasColor.length-2);
            ctx.strokeStyle = subrgb + this.canvasAlpha + ")";
            ctx.fillStyle = subrgb + this.canvasAlpha + ")";
            ctx.lineWidth = this.stroke;
            ctx.lineCap = "round";
            ctx.beginPath();
            ctx.moveTo(this.container.x, this.container.y);

            // Convert from turtle coordinates to screen coordinates.
            var fx = this.turtles.turtleX2screenX(x2);
            var fy = this.turtles.turtleY2screenY(y2);
            var cx1 = this.turtles.turtleX2screenX(cp1x);
            var cy1 = this.turtles.turtleY2screenY(cp1y);
            var cx2 = this.turtles.turtleX2screenX(cp2x);
            var cy2 = this.turtles.turtleY2screenY(cp2y);

            ctx.bezierCurveTo(cx1, cy1, cx2, cy2, fx, fy);

            if (!this.svgPath) {
                this.svgPath = true;
                var ix = this.turtles.turtleX2screenX(this.x);
                var iy = this.turtles.turtleY2screenY(this.y);
                var ixScaled = ix * this.turtles.scale;
                var iyScaled = iy * this.turtles.scale;
                this.svgOutput += '<path d="M ' + ixScaled + ',' + iyScaled + ' ';
            }

            var cx1Scaled = cx1 * this.turtles.scale;
            var cy1Scaled = cy1 * this.turtles.scale;
            var cx2Scaled = cx2 * this.turtles.scale;
            var cy2Scaled = cy2 * this.turtles.scale;
            var fxScaled = fx * this.turtles.scale;
            var fyScaled = fy * this.turtles.scale;
            this.svgOutput += 'C ' + cx1Scaled + ',' + cy1Scaled + ' ' + cx2Scaled + ',' + cy2Scaled + ' ' + fxScaled + ',' + fyScaled;
            this.x = x2;
            this.y = y2;
            ctx.stroke();
            if (!this.fillState) {
                ctx.closePath();
            }

        } else {
            this.x = x2;
            this.y = y2;
            var fx = this.turtles.turtleX2screenX(x2);
            var fy = this.turtles.turtleY2screenY(y2);
        }

        // Update turtle position on screen.
        this.container.x = fx;
        this.container.y = fy;

        // The new heading is the angle between (cp2x, cp2y) and (nx, ny).
        var degrees = Math.atan2(nx - cp2x, ny - cp2y);
        degrees = 180 * degrees / Math.PI;
        this.doSetHeading(degrees);
    };

    this.move = function(ox, oy, x, y, invert) {
        if (invert) {
            ox = this.turtles.turtleX2screenX(ox);
            oy = this.turtles.turtleY2screenY(oy);
            nx = this.turtles.turtleX2screenX(x);
            ny = this.turtles.turtleY2screenY(y);
        } else {
            nx = x;
            ny = y;
        }

        // Draw a line if the pen is down.
        if (this.penState && this.hollowState) {
            // First, we need to close the current SVG path.
            this.closeSVG();
            this.svgPath = true;
            // Save the current stroke width.
            var savedStroke = this.stroke;
            this.stroke = 1;
            ctx.lineWidth = this.stroke;
            ctx.lineCap = 'round';
            // Draw a hollow line.
            if (savedStroke < 3) {
                var step = 0.5;
            } else {
                var step = (savedStroke - 2) / 2.;
            }

            var capAngleRadians = (this.orientation - 90) * Math.PI / 180.0;
            var dx = step * Math.sin(capAngleRadians);
            var dy = -step * Math.cos(capAngleRadians);

            ctx.moveTo(ox + dx, oy + dy);
            var oxScaled = (ox + dx) * this.turtles.scale;
            var oyScaled = (oy + dy) * this.turtles.scale;
            this.svgOutput += '<path d="M ' + oxScaled + ',' + oyScaled + ' ';

            ctx.lineTo(nx + dx, ny + dy);
            var nxScaled = (nx + dx) * this.turtles.scale;
            var nyScaled = (ny + dy) * this.turtles.scale;
            this.svgOutput += nxScaled + ',' + nyScaled + ' ';

            var capAngleRadians = (this.orientation + 90) * Math.PI / 180.0;
            var dx = step * Math.sin(capAngleRadians);
            var dy = -step * Math.cos(capAngleRadians);

            var oAngleRadians = (this.orientation / 180) * Math.PI;
            var cx = nx;
            var cy = ny;
            var sa = oAngleRadians - Math.PI;
            var ea = oAngleRadians;
            ctx.arc(cx, cy, step, sa, ea, false);

            var nxScaled = (nx + dx) * this.turtles.scale;
            var nyScaled = (ny + dy) * this.turtles.scale;

            var radiusScaled = step * this.turtles.scale;

            // Simulate an arc with line segments since Tinkercad
            // cannot import SVG arcs reliably.
            // Replaces:
            // this.svgOutput += 'A ' + radiusScaled + ',' + radiusScaled + ' 0 0 1 ' + nxScaled + ',' + nyScaled + ' ';
            // this.svgOutput += 'M ' + nxScaled + ',' + nyScaled + ' ';

            steps = Math.max(Math.floor(savedStroke, 1));
            this._svgArc(steps, cx * this.turtles.scale, cy * this.turtles.scale, radiusScaled, sa);
            this.svgOutput += nxScaled + ',' + nyScaled + ' ';

            ctx.lineTo(ox + dx, oy + dy);
            var nxScaled = (ox + dx) * this.turtles.scale;
            var nyScaled = (oy + dy) * this.turtles.scale;
            this.svgOutput += nxScaled + ',' + nyScaled + ' ';

            var capAngleRadians = (this.orientation - 90) * Math.PI / 180.0;
            var dx = step * Math.sin(capAngleRadians);
            var dy = -step * Math.cos(capAngleRadians);

            var oAngleRadians = ((this.orientation + 180) / 180) * Math.PI;
            var cx = ox;
            var cy = oy;
            var sa = oAngleRadians - Math.PI;
            var ea = oAngleRadians;
            ctx.arc(cx, cy, step, sa, ea, false);

            var nxScaled = (ox + dx) * this.turtles.scale;
            var nyScaled = (oy + dy) * this.turtles.scale;

            var radiusScaled = step * this.turtles.scale;
            this._svgArc(steps, cx * this.turtles.scale, cy * this.turtles.scale, radiusScaled, sa);
            this.svgOutput += nxScaled + ',' + nyScaled + ' ';

            this.closeSVG();

            ctx.stroke();
            if (!this.fillState) {
                ctx.closePath();
            }
            // restore stroke.
            this.stroke = savedStroke;
            ctx.lineWidth = this.stroke;
            ctx.lineCap = 'round';
            ctx.moveTo(nx, ny);
        } else if (this.penState) {
            ctx.lineTo(nx, ny);
            if (!this.svgPath) {
                this.svgPath = true;
                var oxScaled = ox * this.turtles.scale;
                var oyScaled = oy * this.turtles.scale;
                this.svgOutput += '<path d="M ' + oxScaled + ',' + oyScaled + ' ';
            }
            var nxScaled = nx * this.turtles.scale;
            var nyScaled = ny * this.turtles.scale;
            this.svgOutput += nxScaled + ',' + nyScaled + ' ';
            ctx.stroke();
            if (!this.fillState) {
                ctx.closePath();
            }
        } else {
            ctx.moveTo(nx, ny);
        }
        this.penstrokes.image = canvas;
        // Update turtle position on screen.
        this.container.x = nx;
        this.container.y = ny;
        if (invert) {
            this.x = x;<commit_msg>don't fill hollow lines<commit_after>// Copyright (c) 2014-16 <NAME>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the The GNU Affero General Public
// License as published by the Free Software Foundation; either
// version 3 of the License, or (at your option) any later version.
//
// You should have received a copy of the GNU Affero General Public
// License along with this library; if not, write to the Free Software
// Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA

// Turtles
const DEFAULTCOLOR = 0;
const DEFAULTVALUE = 50;
const DEFAULTCHROMA = 100;
const DEFAULTSTROKE = 5;
const DEFAULTFONT = 'sans-serif';

// Turtle sprite
const TURTLEBASEPATH = 'images/';

function Turtle (name, turtles, drum) {
    this.name = name;
    this.turtles = turtles;
    this.drum = drum;

    if (drum) {
        console.log('turtle ' + name + ' is a drum.');
    }
    // Is the turtle running?
    this.running = false;

    // In the trash?
    this.trash = false;

    // Things used for drawing the turtle.
    this.container = null;
    this.x = 0;
    this.y = 0;
    this.bitmap = null;
    this.skinChanged = false;  // Should we reskin the turtle on clear?

    // Which start block is assocated with this turtle?
    this.startBlock = null;
    this.decorationBitmap = null;  // Start block decoration.

    // Queue of blocks this turtle is executing.
    this.queue = [];

    // Listeners
    this.listeners = {};

    // Things used for what the turtle draws.
    this.penstrokes = null;
    this.imageContainer = null;
    this.svgOutput = '';
    // Are we currently drawing a path?
    this.svgPath = false;
    this.color = DEFAULTCOLOR;
    this.value = DEFAULTVALUE;
    this.chroma = DEFAULTCHROMA;
    this.stroke = DEFAULTSTROKE;
    this.canvasColor = 'rgba(255,0,49,1)'; // '#ff0031';
    this.canvasAlpha = 1.0;
    this.orientation = 0;
    this.fillState = false;
    this.hollowState = false;
    this.penState = true;
    this.font = DEFAULTFONT;
    this.media = [];  // Media (text, images) we need to remove on clear.
    var canvas = document.getElementById("overlayCanvas");
    var ctx = canvas.getContext("2d");
    // Simulate an arc with line segments since Tinkercad cannot
    // import SVG arcs reliably.
    this._svgArc = function(nsteps, cx, cy, radius, sa, ea) {
        var a = sa;
        if (ea == null) {
            var da = Math.PI / nsteps;
        } else {
            var da = (ea - sa) / nsteps;
        }
        for (var i = 0; i < nsteps; i++) {
            var nx = cx + radius * Math.cos(a);
            var ny = cy + radius * Math.sin(a);
            this.svgOutput += nx + ',' + ny + ' ';
            a += da;
        }
    };

    this.doBezier = function(cp1x, cp1y, cp2x, cp2y, x2, y2) {
        // FIXME: Add SVG output
        if (this.penState && this.hollowState) {
            // Convert from turtle coordinates to screen coordinates.
            var nx = x2;
            var ny = y2;
            var ix = this.turtles.turtleX2screenX(this.x);
            var iy = this.turtles.turtleY2screenY(this.y);
            var fx = this.turtles.turtleX2screenX(x2);
            var fy = this.turtles.turtleY2screenY(y2);
            var cx1 = this.turtles.turtleX2screenX(cp1x);
            var cy1 = this.turtles.turtleY2screenY(cp1y);
            var cx2 = this.turtles.turtleX2screenX(cp2x);
            var cy2 = this.turtles.turtleY2screenY(cp2y);

            // First, we need to close the current SVG path.
            this.closeSVG();

            // Save the current stroke width.
            var savedStroke = this.stroke;
            this.stroke = 1;
            ctx.lineWidth = this.stroke;
            ctx.lineCap = "round";
            // Draw a hollow line.
            if (savedStroke < 3) {
                var step = 0.5;
            } else {
                var step = (savedStroke - 2) / 2.;
            }

            steps = Math.max(Math.floor(savedStroke, 1));

            // We need both the initial and final headings.
            // The initial heading is the angle between (cp1x, cp1y) and (this.x, this.y).
            var degreesInitial = Math.atan2(cp1x - this.x, cp1y - this.y);
            degreesInitial = (180 * degreesInitial / Math.PI);
            if (degreesInitial < 0) { degreesInitial += 360; }
            // The final heading is the angle between (cp2x, cp2y) and (fx, fy).
            var degreesFinal = Math.atan2(nx - cp2x, ny - cp2y);
            degreesFinal = 180 * degreesFinal / Math.PI;
            if (degreesFinal < 0) { degreesFinal += 360; }

            // We also need to calculate the deltas for the "caps" at each end.
            var capAngleRadiansInitial = (degreesInitial - 90) * Math.PI / 180.0;
            var dxi = step * Math.sin(capAngleRadiansInitial);
            var dyi = -step * Math.cos(capAngleRadiansInitial);
            var capAngleRadiansFinal = (degreesFinal - 90) * Math.PI / 180.0;
            var dxf = step * Math.sin(capAngleRadiansFinal);
            var dyf = -step * Math.cos(capAngleRadiansFinal);

            // The four "corners"
            var ax = ix - dxi;
            var ay = iy - dyi;
            var axScaled = ax * this.turtles.scale;
            var ayScaled = ay * this.turtles.scale;
            var bx = fx - dxf;
            var by = fy - dyf;
            var bxScaled = bx * this.turtles.scale;
            var byScaled = by * this.turtles.scale;
            var cx = fx + dxf;
            var cy = fy + dyf;
            var cxScaled = cx * this.turtles.scale;
            var cyScaled = cy * this.turtles.scale;
            var dx = ix + dxi;
            var dy = iy + dyi;
            var dxScaled = dx * this.turtles.scale;
            var dyScaled = dy * this.turtles.scale;

            // Control points scaled for SVG output
            var cx1Scaled = (cx1 + dxi)* this.turtles.scale;
            var cy1Scaled = (cy1 + dyi) * this.turtles.scale;
            var cx2Scaled = (cx2 + dxf) * this.turtles.scale;
            var cy2Scaled = (cy2 + dyf) * this.turtles.scale;

            ctx.moveTo(ax, ay);
            this.svgPath = true;
            this.svgOutput += '<path d="M ' + axScaled + ',' + ayScaled + ' ';

            // Initial arc
            var oAngleRadians = ((180 + degreesInitial) / 180) * Math.PI;
            var arccx = ix;
            var arccy = iy;
            var sa = oAngleRadians - Math.PI;
            var ea = oAngleRadians;
            ctx.arc(arccx, arccy, step, sa, ea, false);
            this._svgArc(steps, arccx * this.turtles.scale, arccy * this.turtles.scale, step * this.turtles.scale, sa, ea);

            // Initial bezier curve
            ctx.bezierCurveTo(cx1 + dxi, cy1 + dyi , cx2 + dxf, cy2 + dyf, cx, cy);
            this.svgOutput += 'C ' + cx1Scaled + ',' + cy1Scaled + ' ' + cx2Scaled + ',' + cy2Scaled + ' ' + cxScaled + ',' + cyScaled + ' ';

            this.svgOutput += 'M ' + cxScaled + ',' + cyScaled + ' ';

            // Final arc
            var oAngleRadians = (degreesFinal / 180) * Math.PI;
            var arccx = fx;
            var arccy = fy;
            var sa = oAngleRadians - Math.PI;
            var ea = oAngleRadians;
            ctx.arc(arccx, arccy, step, sa, ea, false);
            this._svgArc(steps, arccx * this.turtles.scale, arccy * this.turtles.scale, step * this.turtles.scale, sa, ea);

            // Final bezier curve
            ctx.bezierCurveTo(cx2 - dxf, cy2 - dyf, cx1 - dxi, cy1 - dyi, ax, ay);
            this.svgOutput += 'C ' + cx2Scaled + ',' + cy2Scaled + ' ' + cx1Scaled + ',' + cy1Scaled + ' ' + axScaled + ',' + ayScaled + ' ';
            this.closeSVG();

            ctx.stroke();
            ctx.closePath();
            // restore stroke.
            this.stroke = savedStroke;
            ctx.lineWidth = this.stroke;
            ctx.lineCap = "round";
            ctx.moveTo(fx,fy);
            this.x = x2;
            this.y = y2;
        } else if (this.penState) {
            if (this.canvasColor[0] === "#") {
                this.canvasColor = hex2rgb(this.canvasColor.split("#")[1]);
            }
            var subrgb = this.canvasColor.substr(0, this.canvasColor.length-2);
            ctx.strokeStyle = subrgb + this.canvasAlpha + ")";
            ctx.fillStyle = subrgb + this.canvasAlpha + ")";
            ctx.lineWidth = this.stroke;
            ctx.lineCap = "round";
            ctx.beginPath();
            ctx.moveTo(this.container.x, this.container.y);

            // Convert from turtle coordinates to screen coordinates.
            var fx = this.turtles.turtleX2screenX(x2);
            var fy = this.turtles.turtleY2screenY(y2);
            var cx1 = this.turtles.turtleX2screenX(cp1x);
            var cy1 = this.turtles.turtleY2screenY(cp1y);
            var cx2 = this.turtles.turtleX2screenX(cp2x);
            var cy2 = this.turtles.turtleY2screenY(cp2y);

            ctx.bezierCurveTo(cx1, cy1, cx2, cy2, fx, fy);

            if (!this.svgPath) {
                this.svgPath = true;
                var ix = this.turtles.turtleX2screenX(this.x);
                var iy = this.turtles.turtleY2screenY(this.y);
                var ixScaled = ix * this.turtles.scale;
                var iyScaled = iy * this.turtles.scale;
                this.svgOutput += '<path d="M ' + ixScaled + ',' + iyScaled + ' ';
            }

            var cx1Scaled = cx1 * this.turtles.scale;
            var cy1Scaled = cy1 * this.turtles.scale;
            var cx2Scaled = cx2 * this.turtles.scale;
            var cy2Scaled = cy2 * this.turtles.scale;
            var fxScaled = fx * this.turtles.scale;
            var fyScaled = fy * this.turtles.scale;
            this.svgOutput += 'C ' + cx1Scaled + ',' + cy1Scaled + ' ' + cx2Scaled + ',' + cy2Scaled + ' ' + fxScaled + ',' + fyScaled;
            this.x = x2;
            this.y = y2;
            ctx.stroke();
            if (!this.fillState) {
                ctx.closePath();
            }

        } else {
            this.x = x2;
            this.y = y2;
            var fx = this.turtles.turtleX2screenX(x2);
            var fy = this.turtles.turtleY2screenY(y2);
        }

        // Update turtle position on screen.
        this.container.x = fx;
        this.container.y = fy;

        // The new heading is the angle between (cp2x, cp2y) and (nx, ny).
        var degrees = Math.atan2(nx - cp2x, ny - cp2y);
        degrees = 180 * degrees / Math.PI;
        this.doSetHeading(degrees);
    };

    this.move = function(ox, oy, x, y, invert) {
        if (invert) {
            ox = this.turtles.turtleX2screenX(ox);
            oy = this.turtles.turtleY2screenY(oy);
            nx = this.turtles.turtleX2screenX(x);
            ny = this.turtles.turtleY2screenY(y);
        } else {
            nx = x;
            ny = y;
        }

        // Draw a line if the pen is down.
        if (this.penState && this.hollowState) {
            // First, we need to close the current SVG path.
            this.closeSVG();
            this.svgPath = true;
            // Save the current stroke width.
            var savedStroke = this.stroke;
            this.stroke = 1;
            ctx.lineWidth = this.stroke;
            ctx.lineCap = 'round';
            // Draw a hollow line.
            if (savedStroke < 3) {
                var step = 0.5;
            } else {
                var step = (savedStroke - 2) / 2.;
            }

            var capAngleRadians = (this.orientation - 90) * Math.PI / 180.0;
            var dx = step * Math.sin(capAngleRadians);
            var dy = -step * Math.cos(capAngleRadians);

            ctx.moveTo(ox + dx, oy + dy);
            var oxScaled = (ox + dx) * this.turtles.scale;
            var oyScaled = (oy + dy) * this.turtles.scale;
            this.svgOutput += '<path d="M ' + oxScaled + ',' + oyScaled + ' ';

            ctx.lineTo(nx + dx, ny + dy);
            var nxScaled = (nx + dx) * this.turtles.scale;
            var nyScaled = (ny + dy) * this.turtles.scale;
            this.svgOutput += nxScaled + ',' + nyScaled + ' ';

            var capAngleRadians = (this.orientation + 90) * Math.PI / 180.0;
            var dx = step * Math.sin(capAngleRadians);
            var dy = -step * Math.cos(capAngleRadians);

            var oAngleRadians = (this.orientation / 180) * Math.PI;
            var cx = nx;
            var cy = ny;
            var sa = oAngleRadians - Math.PI;
            var ea = oAngleRadians;
            ctx.arc(cx, cy, step, sa, ea, false);

            var nxScaled = (nx + dx) * this.turtles.scale;
            var nyScaled = (ny + dy) * this.turtles.scale;

            var radiusScaled = step * this.turtles.scale;

            // Simulate an arc with line segments since Tinkercad
            // cannot import SVG arcs reliably.
            // Replaces:
            // this.svgOutput += 'A ' + radiusScaled + ',' + radiusScaled + ' 0 0 1 ' + nxScaled + ',' + nyScaled + ' ';
            // this.svgOutput += 'M ' + nxScaled + ',' + nyScaled + ' ';

            steps = Math.max(Math.floor(savedStroke, 1));
            this._svgArc(steps, cx * this.turtles.scale, cy * this.turtles.scale, radiusScaled, sa);
            this.svgOutput += nxScaled + ',' + nyScaled + ' ';

            ctx.lineTo(ox + dx, oy + dy);
            var nxScaled = (ox + dx) * this.turtles.scale;
            var nyScaled = (oy + dy) * this.turtles.scale;
            this.svgOutput += nxScaled + ',' + nyScaled + ' ';

            var capAngleRadians = (this.orientation - 90) * Math.PI / 180.0;
            var dx = step * Math.sin(capAngleRadians);
            var dy = -step * Math.cos(capAngleRadians);

            var oAngleRadians = ((this.orientation + 180) / 180) * Math.PI;
            var cx = ox;
            var cy = oy;
            var sa = oAngleRadians - Math.PI;
            var ea = oAngleRadians;
            ctx.arc(cx, cy, step, sa, ea, false);

            var nxScaled = (ox + dx) * this.turtles.scale;
            var nyScaled = (oy + dy) * this.turtles.scale;

            var radiusScaled = step * this.turtles.scale;
            this._svgArc(steps, cx * this.turtles.scale, cy * this.turtles.scale, radiusScaled, sa);
            this.svgOutput += nxScaled + ',' + nyScaled + ' ';

            this.closeSVG();

            ctx.stroke();
            ctx.closePath();
            // restore stroke.
            this.stroke = savedStroke;
            ctx.lineWidth = this.stroke;
            ctx.lineCap = 'round';
            ctx.moveTo(nx, ny);
        } else if (this.penState) {
            ctx.lineTo(nx, ny);
            if (!this.svgPath) {
                this.svgPath = true;
                var oxScaled = ox * this.turtles.scale;
                var oyScaled = oy * this.turtles.scale;
                this.svgOutput += '<path d="M ' + oxScaled + ',' + oyScaled + ' ';
            }
            var nxScaled = nx * this.turtles.scale;
            var nyScaled = ny * this.turtles.scale;
            this.svgOutput += nxScaled + ',' + nyScaled + ' ';
            ctx.stroke();
            if (!this.fillState) {
                ctx.closePath();
            }
        } else {
            ctx.moveTo(nx, ny);
        }
        this.penstrokes.image = canvas;
        // Update turtle position on screen.
        this.container.x = nx;
        this.container.y = ny;
        if (invert) {
            this.x = x;
{'fraction_non_alphanumeric': 0.12282566679551604, 'fraction_numerical': 0.015848473134905297, 'mean_word_length': 1.6088235294117648, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1488084', 'n_tokens_mistral': 10476, 'n_tokens_neox': 9991, 'n_words': 2518}