diff --git a/software/config.js b/software/config.js new file mode 100644 index 0000000..fc2a48c --- /dev/null +++ b/software/config.js @@ -0,0 +1,55 @@ +var config = {} + +//Side of end effector +//~~Do not touch~~ +config.e = 34.64101615137754; // Math.sqrt(3) * 10 * 2 + +//Side of top triangle +//~~Do not touch~~ +config.f = 110.85125168440814; // Math.sqrt(3) * 32 * 2 + +//Length of parallelogram joint +//~~Do not touch~~ +config.re = 153.5; // 145 + 8.5 + +//Length of upper joint +//~~Do not touch~~ +config.rf = 52.690131903421914; // Math.sqrt(52**2 + 8.5**2) + +//Input ranges for servos +//~~Do not touch~~ +config.servo1 = {in_min: 0, in_max: 90}; +config.servo2 = {in_min: 0, in_max: 90}; +config.servo3 = {in_min: 0, in_max: 90}; + +//Default output ranges for servos +//CHANGE THESE +config.servo1.out_min = 12; +config.servo1.out_max = 93; +config.servo2.out_min = 8; +config.servo2.out_max = 90; +config.servo3.out_min = 14; +config.servo3.out_max = 96; + +//Dimensions of the base plate +config.baseHeight = 95; +config.baseWidth = 80; + +//Default Z-Level of the pen +config.penHeight = -140; + +//Default drawing height of the pen +config.drawHeight = -152.75; + +//Delay for commands in SVGReader +//Note that some commands will take longer than this +//Default value is 150 +config.delay = 200; + +//The default easing type to be used +//When no easing is specified, this is the type that will be used +//"none" means that if no easing is specified, do not ease +//For a list of possible easing types, look in motion.js +config.defaultEaseType = "linear"; + +module.exports = config; \ No newline at end of file diff --git a/software/package.json b/software/package.json index 0463414..d5b7665 100755 --- a/software/package.json +++ b/software/package.json @@ -16,7 +16,9 @@ "engines":{ "node":"0.x.x" }, - "dependencies":{ - "johnny-five":"0.7.x" + "dependencies": { + "johnny-five": "0.8.x", + "svg-path-parser": "^1.0.1", + "xml2js": "^0.4.9" } } diff --git a/software/recorder/index.html b/software/recorder/index.html new file mode 100644 index 0000000..4602082 --- /dev/null +++ b/software/recorder/index.html @@ -0,0 +1,167 @@ + + + + + +Tracing a line with d3.js + + + + +
+
+ + + + + + + + + + diff --git a/software/recorder/simplify.js b/software/recorder/simplify.js new file mode 100644 index 0000000..fa86746 --- /dev/null +++ b/software/recorder/simplify.js @@ -0,0 +1,133 @@ +/* + (c) 2013, Vladimir Agafonkin + Simplify.js, a high-performance JS polyline simplification library + mourner.github.io/simplify-js +*/ + +(function () { 'use strict'; + +// to suit your point format, run search/replace for '.x' and '.y'; +// for 3D version, see 3d branch (configurability would draw significant performance overhead) + +// square distance between 2 points +function getSqDist(p1, p2) { + + var dx = p1.x - p2.x, + dy = p1.y - p2.y; + + return dx * dx + dy * dy; +} + +// square distance from a point to a segment +function getSqSegDist(p, p1, p2) { + + var x = p1.x, + y = p1.y, + dx = p2.x - x, + dy = p2.y - y; + + if (dx !== 0 || dy !== 0) { + + var t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy); + + if (t > 1) { + x = p2.x; + y = p2.y; + + } else if (t > 0) { + x += dx * t; + y += dy * t; + } + } + + dx = p.x - x; + dy = p.y - y; + + return dx * dx + dy * dy; +} +// rest of the code doesn't care about point format + +// basic distance-based simplification +function simplifyRadialDist(points, sqTolerance) { + + var prevPoint = points[0], + newPoints = [prevPoint], + point; + + for (var i = 1, len = points.length; i < len; i++) { + point = points[i]; + + if (getSqDist(point, prevPoint) > sqTolerance) { + newPoints.push(point); + prevPoint = point; + } + } + + if (prevPoint !== point) newPoints.push(point); + + return newPoints; +} + +// simplification using optimized Douglas-Peucker algorithm with recursion elimination +function simplifyDouglasPeucker(points, sqTolerance) { + + var len = points.length, + MarkerArray = typeof Uint8Array !== 'undefined' ? Uint8Array : Array, + markers = new MarkerArray(len), + first = 0, + last = len - 1, + stack = [], + newPoints = [], + i, maxSqDist, sqDist, index; + + markers[first] = markers[last] = 1; + + while (last) { + + maxSqDist = 0; + + for (i = first + 1; i < last; i++) { + sqDist = getSqSegDist(points[i], points[first], points[last]); + + if (sqDist > maxSqDist) { + index = i; + maxSqDist = sqDist; + } + } + + if (maxSqDist > sqTolerance) { + markers[index] = 1; + stack.push(first, index, index, last); + } + + last = stack.pop(); + first = stack.pop(); + } + + for (i = 0; i < len; i++) { + if (markers[i]) newPoints.push(points[i]); + } + + return newPoints; +} + +// both algorithms combined for awesome performance +function simplify(points, tolerance, highestQuality) { + + if (points.length <= 1) return points; + + var sqTolerance = tolerance !== undefined ? tolerance * tolerance : 1; + + points = highestQuality ? points : simplifyRadialDist(points, sqTolerance); + points = simplifyDouglasPeucker(points, sqTolerance); + + return points; +} + +// export as AMD module / Node module / browser or worker variable +if (typeof define === 'function' && define.amd) define(function() { return simplify; }); +else if (typeof module !== 'undefined') module.exports = simplify; +else if (typeof self !== 'undefined') self.simplify = simplify; +else window.simplify = simplify; + +})(); diff --git a/software/src/SVGReader.js b/software/src/SVGReader.js new file mode 100644 index 0000000..52e2fdc --- /dev/null +++ b/software/src/SVGReader.js @@ -0,0 +1,901 @@ +//Draws stuff from SVG files +//Built with InkScape in mind, but should support: +//http://svg-edit.googlecode.com/svn/branches/stable/editor/svg-editor.html +//> To test: +//> svgRead.drawSVG(filepath) + +var parse = require('svg-path-parser'); +var fs = require('fs'); +var draw = require("./draw"); +var parseString = require('xml2js').parseString; + +function SVGReader(args) { + this.baseWidth = 80; + this.baseHeight = 95; + this.drawHeight = -140; + this.delay = 150; + this.defaultEaseType = "linear"; + + if (args) { + var keys = Object.keys(args) + keys.forEach(function(key){ + this[key] = args[key] + }, this) + } + objRef = this; + defaultEaseType = this.defaultEaseType; +} + +//Draws from an SVG image specified by filepath +//> Usage: +//> drawSVG("C:/Projects/Tapsterbot/software/src/drawing.svg"); +//Note: filePath can be relative +//connect is a special flag that indicates that each path should be drawn connected to each other +//It is really only used for drawing in cursive and does not need to be specified otherwise +SVGReader.prototype.drawSVG = function(filePath, connect) { + resetTimer(); + var parsed; + + if (connect) + connected = connect; + else + connected = null; + + objRef = this; + + //Create a JSON string out of the SVG image data + //parseString strips away the XML data + try { + parseString(fs.readFileSync(filePath, "utf8"), function(err, result) { + parsed = JSON.stringify(result, null, 1); + }); + } catch (e) { + if (e.code === "ENOENT") + console.log("File not found."); + else + throw e; + + return; //If the file is not found stop execution + } + + //Parse the JSON string into an array + objArr = JSON.parse(parsed); + + //Extract width and height data from the drawing + var svgDimensions = dimensionConversion(objArr.svg.$.width, objArr.svg.$.height); + width = svgDimensions.width; + height = svgDimensions.height; + + //Check for translation and account for it + //Commented out because going to stop supporting transformations + /* + transformX = 0; + transformY = 0; + + if (objArr.svg.g[0].$ && objArr.svg.g[0].$.transform) { //Done in multiple checks to avoid errors being thrown + var transString = objArr.svg.g[0].$.transform; + var subX = transString.indexOf("("); + transformX = parseInt(transString.substring(subX + 1)); + var subY = transString.indexOf(","); + transformY = parseInt(transString.substring(subY + 1)); + } + */ + + var phoneWidth = this.baseWidth; + var phoneHeight = this.baseHeight; + penHeight = this.drawHeight; + + widthRatio = width / phoneWidth; + heightRatio = height / phoneHeight; + + halfway = {x:width / 2, y:height / 2}; + currentPoint = {x:halfway.x, y:halfway.y}; //Start at the center of the canvas, which corresponds to (0,0) on the Tapster + + if (objArr.svg.g[0].g && objArr.svg.g[0].g[0].path) { //If there are multiple groups. Additional check to make sure that there is actually path data + for (var i = 0; i < objArr.svg.g[0].g.length; i++) { + pathArray = objArr.svg.g[0].g[i].path; + drawImage(pathArray); + } + } + + else if (objArr.svg.g.length > 0) { //Depending on how the paths are grouped, it is possible that this value can be greater than zero + for (var i = 0; i < objArr.svg.g.length; i++) { //If this is the case, loop through the array to get the path data + pathArray = objArr.svg.g[i].path; + drawImage(pathArray); + } + } + + else if (objArr.svg.g[0].path) { //If there is only one group + pathArray = objArr.svg.g[0].path; + drawImage(pathArray); + } + + doSetTimeout(0, 48, -130, delay); + //setTimeout(function() { resetTimer() }, timer + 5); +} + +drawImage = function(pathArray) { + var d = ""; + firstMove = null; //After a group is done being drawn, firstMove is reset as there has not yet been a first move made in the next group + for (var i = 0; i < pathArray.length; i++) { //When drawing multiple lines, there are multiple paths + firstPoint = null; + d = pathArray[i].$.d; + var commands = parse(d); + objRef.interpretCommands(commands); + /* if (i < (pathArray.length - 1)) { //Smooth transition to the next path + doSetTimeout(mapX(currentPoint.x), mapY(currentPoint.y), penHeight + 10, 300); //Moves the pen up and over so no line is drawn between the two + doSetTimeout(mapX(parse(pathArray[i+1].$.d)[0].x), mapY(parse(pathArray[i+1].$.d)[0].y), penHeight + 10, 300); + doSetTimeout(mapX(parse(pathArray[i+1].$.d)[0].x), mapY(parse(pathArray[i+1].$.d)[0].y), penHeight, 300); + } */ + } + doSetTimeout(mapX(currentPoint.x), mapY(currentPoint.y), penHeight + 10, delay); +} + +//Move from one point to (x, y) +move = function(x, y) { + var ptArray = []; + if (!connected) { //If the paths should not be connected, lift up the pen and move over so that a line is not drawn + doSetTimeout(mapX(currentPoint.x), mapY(currentPoint.y), penHeight + 10, delay, "none"); + doSetTimeout(mapX(x), mapY(y), penHeight + 10, delay, "none"); + doSetTimeout(mapX(x), mapY(y), penHeight, delay, "none"); + //svg.drawSVG("C:/Projects/Tapsterbot/software/src/hello/helloChF.svg") + //ptArray.push({x:mapX(currentPoint.x), y:mapY(currentPoint.y), z:penHeight + 10}); + //ptArray.push({x:mapX(x), y:mapY(y), z:penHeight + 10}); + //ptArray.push({x:mapX(x), y:mapY(y), z:penHeight}); + } + else if (connected && !firstMove) { //If the paths should be connected and a move has not been made, lift up the pen and move to the first point + doSetTimeout(mapX(currentPoint.x), mapY(currentPoint.y), penHeight + 10, delay, "none"); + doSetTimeout(mapX(x), mapY(y), penHeight + 10, delay, "none"); + doSetTimeout(mapX(x), mapY(y), penHeight, delay, "none"); + //ptArray.push({x:mapX(currentPoint.x), y:mapY(currentPoint.y), z:penHeight + 10}); + //ptArray.push({x:mapX(x), y:mapY(y), z:penHeight + 10}); + //ptArray.push({x:mapX(x), y:mapY(y), z:penHeight}); + } + else //If the paths should be connected and a move has been made, just draw a line between the two paths + doSetTimeout(mapX(x), mapY(y), penHeight, delay); + //ptArray.push({x:mapX(x), y:mapY(y), z:penHeight}); + + currentPoint = {x:x, y:y}; //Update the current point (done every time an SVG command is called) + + if (!firstMove) //Keeps track of if a move has been made or not. + firstMove = true; + + if (!firstPoint) //Keeps track of the first point, for use with the Z/z command + firstPoint = {x:currentPoint.x, y:currentPoint.y}; //Since the first command of a path is always to Move, this check only occurs here + + //return ptArray; +} + +//Move from one point to that that point + x, y +relMove = function(x, y) { + x = currentPoint.x + x; + y = currentPoint.y + y; + + //return move(x, y); + move(x, y); +} + +//Draw a line from one point to (x, y) +line = function(x, y) { + var ptArray = []; + doSetTimeout(mapX(x), mapY(y), penHeight, delay, "linear"); + //ptArray.push({x:mapX(x), y:mapY(y), z:penHeight}); + currentPoint = {x:x, y:y}; + +} + +//Draw a line from one point to that point + x, y +relLine = function(x, y) { + x = currentPoint.x + x; + y = currentPoint.y + y; + + //return line(x, y); + line(x, y); +} + +//Draws a cubic Bezier curve. +//(x1,y1) is the first control point +//(x2, y2) is the second +//(x, y) is the end point +cubicCurve = function(x1, y1, x2, y2, x, y) { + + //Function for calculating the coordinates of points on the curve + //Calculates t+1 points + b = function(x1, y1, x2, y2, x, y, t) { + var ptArray = new Array(); + for (var i = 0; i <= t; i++) { + var newI = i/t; //Converts i to a decimal, to satisfy 0 <= i <= 1 + var ptX = (Math.pow((1-newI), 3) * currentPoint.x) + (3 * Math.pow((1-newI), 2) * newI * x1) //From https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves + + (3 * (1-newI) * Math.pow(newI, 2) * x2) + (Math.pow(newI, 3) * x); + var ptY = (Math.pow((1-newI), 3) * currentPoint.y) + (3 * Math.pow((1-newI), 2) * newI * y1) + + (3 * (1-newI) * Math.pow(newI, 2) * y2) + (Math.pow(newI, 3) * y); + var newPt = {x:ptX, y:ptY, z:penHeight}; + ptArray.push(newPt); //Populates the array with points + } + currentPoint = {x:ptArray[t].x, y:ptArray[t].y}; + return ptArray; + } + + var curvePts = new Array(); + curvePts = b(x1, y1, x2, y2, x, y, 5); //Arbitrarily-chosen value. It creates a smooth-looking curve without calculating too many points + for (var i = 0;i < curvePts.length; i++) + doSetTimeout(mapX(curvePts[i].x), mapY(curvePts[i].y), penHeight, delay*2 / curvePts.length, "none"); //The cubic curve command takes 2*delay ms to complete so that an accurate curve is created + //return b(x1, y1, x2, y2, x, y, 8); +} + +//Draws a relative cubic Bezier curve +relCubicCurve = function(x1, y1, x2, y2, x, y) +{ + var tempX = currentPoint.x; + var tempY = currentPoint.y; + //return cubicCurve(tempX + x1, tempY + y1, tempX + x2, tempY + y2, tempX + x, tempY + y); + cubicCurve(tempX + x1, tempY + y1, tempX + x2, tempY + y2, tempX + x, tempY + y); +} + +//Draws a quadratic Bezier curve +//(x1, y1) is the control point +//(x, y) is the end point +quadraticCurve = function(x1, y1, x, y) { + + //Helper function for generating the points + q = function(x1, y1, x, y, t) { + var ptArray = new Array(); + for (var i = 0; i <= t; i++) { + var newI = i/t; //Converts i to a decimal, to satisfy 0 <= i <= 1 + var ptX = Math.pow((1-newI), 2)*currentPoint.x + (2 * (1-newI) * newI * x1) + (Math.pow(newI, 2) * x); //From https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B.C3.A9zier_curves + var ptY = Math.pow((1-newI), 2)*currentPoint.y + (2 * (1-newI) * newI * y1) + (Math.pow(newI, 2) * y); + var newPt = {x:ptX, y:ptY, z:penHeight}; + ptArray.push(newPt); + } + currentPoint = {x:ptArray[t].x, y:ptArray[t].y}; + return ptArray; + } + + var curvePts = new Array(); + curvePts = q(x1, y1, x, y, 5); + for (var i = 0; i < curvePts.length; i++) + doSetTimeout(mapX(curvePts[i].x), mapY(curvePts[i].y), penHeight, delay*2 / curvePts.length, "none"); + //return q(x1, y1, x, y, 8); +} + +//Draws a relative quadratic Bezier curve +relQuadraticCurve = function(x1, y1, x, y) { + var tempX = currentPoint.x; + var tempY = currentPoint.y; + //return quadraticCurve(tempX + x1, tempY + y1, tempX + x, tempY + y); + quadraticCurve(tempX + x1, tempY + y1, tempX + x, tempY + y); +} + +//Draws an elliptical arc +//rx and ry are the radii +//rotation is the angle (in degrees) between the rotated x-axis and the original x-axis +//largeArc is a flag that determines if the arc is greater than or less than or equal to 180 degrees +//sweep is a flag that determines if the arc is drawn in a positive direction or a negative direction +//(x, y) is the final point of the arc +//From: http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes +arc = function(rx, ry, rotation, largeArc, sweep, x, y) { + + //Helper function for calculating the points + a = function(rx, ry, largeArc, sweep, x, y, t) { + var ptArray = new Array(); + + for (var i = 0; i <= t; i++) { + var newI = i / t; + var angle = startAngle + sweepAngle * newI; + var newPt = {x: cx + rx * cos(angle), y: cy + ry * sin(angle), z:penHeight}; + ptArray.push(newPt); + } + + currentPoint = {x: ptArray[t].x, y: ptArray[t].y}; + return ptArray; + } + + //Helper function for calculating the angle between two vectors + angleBetween = function(v1, v2) { + var p = v1.x*v2.x + v1.y*v2.y; + var n = Math.sqrt((Math.pow(v1.x, 2)+Math.pow(v1.y, 2)) * (Math.pow(v2.x, 2)+Math.pow(v2.y, 2))); + var sign = v1.x*v2.y - v1.y*v2.x < 0 ? -1 : 1; + var angle = sign*Math.acos(p/n) * 180 / Math.PI; + + return angle; + } + + tempX = currentPoint.x; + tempY = currentPoint.y; + + var xPrime = (cos(rotation) * ((tempX - x) / 2)) + (sin(rotation) * ((tempY - y) / 2)); + var yPrime = (-sin(rotation) * ((tempX - x) / 2)) + (cos(rotation) * ((tempY - y) / 2)); + + //Checks to ensure radii are as they should be + rx = Math.abs(rx); //Ensures they are non-zero and positive + ry = Math.abs(ry); + + var lambda = (Math.pow(xPrime, 2) / Math.pow(rx, 2)) + (Math.pow(yPrime, 2) / Math.pow(ry, 2)); + + if (lambda > 1) { //Ensures they are large enough + rx = Math.sqrt(lambda) * rx; + ry = Math.sqrt(lambda) * ry; + } + + var sign = 1; + + if (largeArc == sweep) //If they are equal, cPrime is negative + sign = -1; + + //For some reason this would occasionally result in NaN + //Implemented this check at the suggestion of: + //http://users.ecs.soton.ac.uk/rfp07r/interactive-svg-examples/arc.html + var cPrimeNumerator = ((Math.pow(rx, 2) * Math.pow(ry, 2)) - (Math.pow(rx, 2) * Math.pow(yPrime, 2)) - (Math.pow(ry, 2) * Math.pow(xPrime, 2))); + var cPrimeDenom = ((Math.pow(rx, 2) * Math.pow(yPrime, 2)) + (Math.pow(ry, 2) * Math.pow(xPrime, 2))); + + if ((cPrimeNumerator / cPrimeDenom) < 1e-7) + cPrime = 0; + else + cPrime = Math.sqrt(cPrimeNumerator / cPrimeDenom); + + //Calculates the transformed center + var cxPrime = sign * cPrime * ((rx * yPrime) / ry); + var cyPrime = sign * cPrime * (-(ry * xPrime) / rx); + + //Calculates the original center + var cx = ((cos(rotation) * cxPrime) + (-sin(rotation) * cyPrime)) + ((tempX + x) / 2); + var cy = ((sin(rotation) * cxPrime) + (cos(rotation) * cyPrime)) + ((tempY + y) / 2); + + //Calculates the start angle of the arc and the total change in the angle + var startVector = {x: (xPrime - cxPrime) / rx, y: (yPrime - cyPrime) / ry}; + var startAngle = angleBetween({x:1, y:0}, startVector); + var endVector = {x: (-xPrime - cxPrime) / rx, y: (-yPrime - cyPrime) / ry}; + var sweepAngle = angleBetween(startVector, endVector); + + if (!sweep && sweepAngle > 0) { + sweepAngle -= 360; + } + + else if (sweep && sweepAngle < 0) { + sweepAngle += 360; + } + + sweepAngle %= 360; + + var ptArray = a(rx, ry, largeArc, sweep, x, y, 5); + + for (var i = 0; i < ptArray.length; i++) { + doSetTimeout(mapX(ptArray[i].x), mapY(ptArray[i].y), penHeight, delay*2 / ptArray.length, "none"); + } + + //return a(rx, ry, largeArc, sweep, x, y, 10); +} + +//Function for drawing a relative elliptical arc +relArc = function(rx, ry, rotation, largeArc, sweep, x, y) { + x = currentPoint.x + x; + y = currentPoint.y + y; + + //return arc(rx, ry, rotation, largeArc, sweep, x, y); + arc(rx, ry, rotation, largeArc, sweep, x, y); +} + +//A function for setting the penHeight from the command line +setPenHeight = function(penHeight) { + penHeight = penHeight; +} + +//Reflects the point (x,y) across the point (x1, y1) +//For use with smooth curves +reflect = function(x, y, x1, y1) { + var tempX = x; + var tempY = y; + + tempX = x1 - (tempX - x1); + tempY = y1 - (tempY - y1); + + var point = {x:tempX, y:tempY}; + + return point; +} + +//Convert points pixel coordinates to Tapster coordinates +//Done in two methods for ease of use +mapX = function(x) { + var newX = x; + newX = (newX - halfway.x) / widthRatio; //The center of the canvas corresponds to (0, 0) on the Tapster + return newX; +}; + +mapY = function(y) { + var newY = y; + newY = (halfway.y - newY) / heightRatio; + return newY; +} + +// A sine function for working with degrees, not radians +sin = function(degree) { + return Math.sin(Math.PI * (degree/180)); +} + +// A cosine function for working with degrees, not radians +cos = function(degree) { + return Math.cos(Math.PI * (degree/180)); +} + +//Function for converting Inkscape dimensions into Tapster-friendly pixels +//Should switch to switch statements +dimensionConversion = function(width, height) { + width = String(width); + if (width.search("mm") != -1) { + var dimension = {width: parseInt(width) * 3.779527559, height: parseInt(height) * 3.779527559}; //The px:mm ratio is ~ 3.8:1 + } + else if (width.search("in") != -1) { + var dimension = {width: parseInt(width) * 96, height: parseInt(height) * 96}; //The px:in ratio is 96:1 + } + else if (width.search("ft") != -1) { + var dimension = {width: parseInt(width) * 96 * 12, height: parseInt(height) * 96 * 12}; //The px:ft ratio is 96*12:1 + } + else if (width.search("m") != -1) { + var dimension = {width: parseInt(width) * 3.779527559 * 1000, height: parseInt(height) * 3.779527559 * 1000}; //The px:m ratio is ~3.8*1000:1 + } + else if (width.search("cm") != -1) { + var dimension = {width: parseInt(width) * 3.779527559 * 100, height: parseInt(height) * 3.779527559 * 100}; //The px:cm ratio is ~3.8*100:1 + } + else if (width.search("pt") != -1) { + var dimension = {width: parseInt(width) * 1.3333, height: parseInt(height) * 1.3333}; //The px:pt ratio is ~1.3:1 + } + else if (width.search("pc") != -1) { + var dimension = {width: parseInt(width) * 16, height: parseInt(height) * 16}; //The px:pc ratio is 16:1 + } + else //No unit specified == pixels + var dimension = {width: width, height: height}; + + return dimension; +} + +//Goes through the list of commands an generated by the SVG-Path-Parser and calls the corresponding functions +//Should switch to switch statements +SVGReader.prototype.interpretCommands = function(commands) { + delay = this.delay; + for (var i = 0; i < commands.length; i++) { + var cmdCode = commands[i].code; + switch (cmdCode) { + case 'M': + move(commands[i].x, commands[i].y); + break; + + case 'm': + relMove(commands[i].x, commands[i].y); + break; + + case 'L': + line(commands[i].x, commands[i].y); + break; + + case 'l': + relLine(commands[i].x, commands[i].y); + break; + + case 'V': + line(currentPoint.x, commands[i].y); + break; + + case 'v': + relLine(currentPoint.x, commands[i].y); + break; + + case 'H': + line(commands[i].x, currentPoint.y); + break; + + case 'h': + relLine(commands[i].x, currentPoint.y); + break; + + case 'C': + cubicCurve(commands[i].x1, commands[i].y1, commands[i].x2, commands[i].y2, commands[i].x, commands[i].y); + break; + + case 'c': + relCubicCurve(commands[i].x1, commands[i].y1, commands[i].x2, commands[i].y2, commands[i].x, commands[i].y); + break; + + //Smooth cubic curve + case 'S': + if (i > 1 && (commands[i-1].code == 's' || commands[i-1].code == 'c' || commands[i-1].code == 'C' || commands[i-1].code == 'S')) { + var reflected = reflect(commands[i].x, commands[i].y, commands[i-1].x, commands[i-1].y); + var ctrl = {x:reflected.x, y:reflected.y}; + } + else + var ctrl = {x:currentPoint.x, y:currentPoint.y}; + + cubicCurve(ctrl.x, ctrl.y, commands[i].x2, commands[i].y2, commands[i].x, commands[i].y); + break; + + //Smooth relative cubic curve + case 's': + if (i > 1 && (commands[i-1].code == 's' || commands[i-1].code == 'c' || commands[i-1].code == 'C' || commands[i-1].code == 'S')) { + var reflected = reflect(commands[i].x, commands[i].y, commands[i-1].x, commands[i-1].y); + var ctrl = {x:reflect(commands[i-1].x2).x, y:reflect(commands[i-1].y2).y}; + } + else + var ctrl = {x:currentPoint.x, y:currentPoint.y}; + + relCubicCurve(ctrl.x, ctrl.y, commands[i].x2, commands[i].y2, commands[i].x, commands[i].y); + break; + + case 'Q': + quadraticCurve(commands[i].x1, commands[i].y1, commands[i].x, commands[i].y); + break; + + case 'q': + relQuadraticCurve(commands[i].x1, commands[i].y1, commands[i].x, commands[i].y); + break; + + //Smooth quadratic curve + case 'T': + if (i > 1 && (commands[i-1].code == 't' || commands[i-1].code == 'q' || commands[i-1].code == 'Q' || commands[i-1].code == 'T')) { + var reflected = reflect(commands[i].x, commands[i].y, commands[i-1].x, commands[i-1].y); + var ctrl = {x:reflect(commands[i-1].x1).x, y:reflect(commands[i-1].y1).y}; + } + else + var ctrl = {x:currentPoint.x, y:currentPoint.y}; + + quadraticCurve(ctrl.x, ctrl.y, commands[i].x, commands[i].y); + break; + + //Smooth relative quadratic curve + case 't': + if (i > 1 && (commands[i-1].code == 't' || commands[i-1].code == 'q' || commands[i-1].code == 'Q' || commands[i-1].code == 'T')) { + var reflected = reflect(commands[i].x, commands[i].y, commands[i-1].x, commands[i-1].y); + var ctrl = {x:reflect(commands[i-1].x1).x, y:reflect(commands[i-1].y1).y}; + } + else + var ctrl = {x:currentPoint.x, y:currentPoint.y}; + + relQuadraticCurve(ctrl.x, ctrl.y, commands[i].x, commands[i].y); + break; + + case 'A': + arc(commands[i].rx, commands[i].ry, commands[i].xAxisRotation, commands[i].largeArc, commands[i].sweep, commands[i].x, commands[i].y); + break; + + case 'a': + relArc(commands[i].rx, commands[i].ry, commands[i].xAxisRotation, commands[i].largeArc, commands[i].sweep, commands[i].x, commands[i].y); + break; + + case 'Z': + line(firstPoint.x, firstPoint.y); + firstPoint = null; + break; + + case 'z': + line(firstPoint.x, firstPoint.y); + firstPoint = null; + break; + } + } + } + +//Creates a working clock +SVGReader.prototype.clock = function() { + var dimensions = dimensionConversion("80mm", "95mm"); //Since no dimensions are specified, assume the default + //To-do: Pull this from a config file + width = dimensions.width; + height = dimensions.height; + + objRef = this; + + resetTimer(); + connected = false; + + //Used to access the erase functions + var drawing = new draw.Draw({ + baseWidth: objRef.baseWidth, + baseHeight: objRef.baseHeight, + drawHeight: objRef.drawHeight + }); + + var phoneWidth = this.baseWidth; + var phoneHeight = this.baseHeight; + + widthRatio = width / phoneWidth; + heightRatio = height / phoneHeight; + + penHeight = this.drawHeight; + halfway = {x:width / 2, y:height / 2}; + currentPoint = {x:halfway.x, y:halfway.y}; //Start at the center of the canvas, which corresponds to (0,0) on the Tapster + + //Currently hardcoded path data + var zero = "M 40.890286,241.44557 30.687127,245.57254 23.885021,257.95342 20.483968,278.58823 20.483968,290.96912 23.885021,311.60393 30.687127,323.98482 40.890286,328.11178 47.692392,328.11178 57.895551,323.98482 64.697657,311.60393 68.09871,290.96912 68.09871,278.58823 64.697657,257.95342 57.895551,245.57254 47.692392,241.44557 40.890286,241.44557"; + var one = "M 50.289453,243.07635 50.289453,329.52761"; + var two = "M 27.618803,262.08031 27.618803,257.95271 30.945521,249.69749 34.27224,245.56989 40.925676,241.44228 54.23255,241.44228 60.885986,245.56989 64.212704,249.69749 67.539423,257.95271 67.539423,266.20792 64.212704,274.46313 57.559268,286.84595 24.292085,328.12202 70.866141,328.12202"; + var three = "M 33.809127,248.8729 C 45.779077,241.87747 46.887418,243.5556 53.186196,243.05913 61.663816,242.39092 65.286579,246.55715 64.812967,252.72615 64.140416,261.48642 55.207358,274.28536 40.185777,283.01419 L 52.623884,281.94331 59.295192,285.47169 62.630846,289.00007 65.9665,299.58523 65.9665,306.64193 62.630846,317.22713 55.959538,324.28383 45.952576,327.81223 35.945614,327.81223 25.938652,324.28383 22.602998,320.75553 19.267344,313.69873"; + var four = "M 54.845427,241.51709 22.803997,300.19076 70.866142,300.19076 M 54.845427,241.51709 54.845427,329.5276"; + var five = "M 61.700462,246.77512 26.654546,246.77512 23.149954,281.56734 26.654546,277.70154 37.168321,273.83574 47.682096,273.83574 58.19587,277.70154 65.205054,285.43315 68.709645,297.03055 68.709645,304.76216 65.205054,316.35956 58.19587,324.09117 47.682096,327.95697 37.168321,327.95697 26.654546,324.09117 23.149954,320.22537 19.645363,312.49376"; + var six = "M 64.294901,253.7887 60.65789,245.53396 49.746856,241.40659 42.472833,241.40659 31.561799,245.53396 24.287776,257.91607 20.650765,278.55291 20.650765,299.18976 24.287776,315.69923 31.561799,323.95397 42.472833,328.08134 46.109844,328.08134 57.020878,323.95397 64.294901,315.69923 67.931912,303.31713 67.931912,299.18976 64.294901,286.80765 57.020878,278.55291 46.109844,274.42554 42.472833,274.42554 31.561799,278.55291 24.287776,286.80765 20.650765,299.18976"; + var seven = "M 68.262659,241.73033 32.158284,328.90301 M 17.716535,241.73033 68.262659,241.73033"; + var eight = "M 37.489233,241.44557 27.286074,245.57254 23.885021,253.82646 23.885021,262.08039 27.286074,270.33431 34.08818,274.46127 47.692392,278.58823 57.895551,282.7152 64.697657,290.96912 68.09871,299.22304 68.09871,311.60393 64.697657,319.85785 61.296604,323.98482 51.093445,328.11178 37.489233,328.11178 27.286074,323.98482 23.885021,319.85785 20.483968,311.60393 20.483968,299.22304 23.885021,290.96912 30.687127,282.7152 40.890286,278.58823 54.494498,274.46127 61.296604,270.33431 64.697657,262.08039 64.697657,253.82646 61.296604,245.57254 51.093445,241.44557 37.489233,241.44557"; + var nine = "M 67.931912,270.29818 64.2949,282.68028 57.020878,290.93502 46.109844,295.06239 42.472833,295.06239 31.561799,290.93502 24.287776,282.68028 20.650765,270.29818 20.650765,266.17081 24.287776,253.7887 31.561799,245.53396 42.472833,241.40659 46.109844,241.40659 57.020878,245.53396 64.2949,253.7887 67.931912,270.29818 67.931912,290.93502 64.2949,311.57186 57.020878,323.95397 46.109844,328.08134 38.835821,328.08134 27.924787,323.95397 24.287776,315.69923"; + + pathData = [zero, one, two, three, four, five, six, seven, eight, nine]; + + var colon = "M 165.73227,300.21546 Z M 165.73227,253.34648 Z"; + var arrayTime = new Array(); + + objRef = this; + + //Simple function for converting units in millimeters to units in pixels + //Based on the fixed ratio between mm and px + toPixels = function(mm) { + return mm * 3.779527559; + } + + //Draws the time + //Takes an array of path data and a callback function + drawTime = function(arrayOfPaths, callback) { + for (var i = 0; i < arrayOfPaths.length; i++) { + objRef.interpretCommands(arrayOfPaths[i]); //Interprets the path data and draws the number + + firstPoint = null; + + if (i == 1) //Inserts the colon between the second and third number + objRef.interpretCommands(parse(colon)); + } + doSetTimeout(0, 0, -140, delay); + setTimeout(function() { endTime = new Date().getTime() }, timer + 1); //Gets the time after drawTime finishes executing + //Because of the way doSetTimeout works, timer + 1 occurs (is meant to occur) a millisecond after the doSetTimeout call + setTimeout(function() { difference = endTime - startTime }, timer + 3); + setTimeout(function() { callback() }, timer + 4); + } + + //Gets the curent time and converts it into an array of four digits + //The first two digits are the hours, the last two are the minutes + getTheTime = function() { + var currentTime = new Date() + var hours = String(currentTime.getHours()); + var minutes = String(currentTime.getMinutes()); + + //Converts from 24 hour time to 12 hour time + if (hours > 12) + hours -= 12; + else if (hours === 0) + hours = 12; + + //Adds a placeholder zero to ensure that there are four digits in the time + //The zero is not actually drawn + if (hours < 10) + hours = "0" + hours; + + if (minutes < 10) + minutes = "0" + minutes; + + var timeArray = new Array(); + for (var i = 0; i < hours.length; i++) { + timeArray.push(hours.charAt(i)); + } + + for (var i = 0; i < minutes.length; i++) { + timeArray.push(minutes.charAt(i)); + } + arrayTime = timeArray; //Stores the array in another variable so it can be accessed elsewhere without calling the function again + return timeArray; + } + + //Converts the digits in the timeArray into path data + convertToPath = function(timeArray) { + var pathArray = new Array(); + var offset = toPixels(3); //The first digit is drawn three mm from the left side + var commandArray = new Array(); + //Loops through the digits in timeArray + for (var i = 0; i < timeArray.length; i++) { + if (i == 0 && timeArray[i] == 0) { //If the first digit is 0 (if the hours < 10) AND the loop is on the first digit + offset += toPixels(5); //Changes the offset so that the three digits that will be drawn will be centered + commandArray.push(["Z"]); //The Z command, without a firstPoint value, will not draw anything, but can still be interpreted without errors + colon = "M 118.73227,307.21546 Z M 118.73227,253.34648 Z"; //Changes the path data of the colon so that it will still be in the correct location + } + else { + var data = pathData[timeArray[i]]; + var commands = parse(data); + + //Loops through the commands in the array and adds the offset to each x value + for (var x = 0; x < commands.length; x++) { + if (commands[x].code != 'Z' || commands[x].code != 'z') { //Every command but the close path commands have an x value + commands[x].x += offset; + if (commands[x].x1) //Some commands (curves and arcs) have an x1 value + commands[x].x1 += offset; + if (commands[x].x2) //Some commands (cubic curves) have an x2 value + commands[x].x2 += offset; + } + } + + commandArray.push(commands); + + if (timeArray[0] != 0) + colon = "M 165.73227,300.21546 Z M 165.73227,253.34648 Z"; //Ensures that the colon has the correct path data + //Without this check, once the hours changed from single to double digits, the colon would be in the incorrect place + + offset += toPixels(18); //Adds an offset to each digit + //Each digit should be 15mm wide, with 3mm space between each digit + if (i == 1) + offset += toPixels(4); //Adds an extra 4mm of space to account for the colon + } + } + + return commandArray; + } + + //Draws a circle to indicate the amount of time left in the minute + timeCircle = function() { + resetTimer(); + + //Draws a circle, given an array of points and the amount of delay in between each point + circle = function(array, timeDelay) { + for (var i=0; i -2; degree--) { + var radians = (degree + 90) * Math.PI/180; //Add 90 to degree so that the circle starts at the top, not at the right + var x = centerX + radius * Math.cos(radians); + var y = centerY + radius * Math.sin(radians); + points.push({x:x,y:y}); + } + + setTimeout(function() { circle(points, calcDelay(calcTimeLeft(), points.length)) }, 0); + } + + //Tells the time + tellTime = function() { + resetTimer(); + + startTime = new Date().getTime(); + + drawing.erase(function() { + drawTime(convertToPath(getTheTime()), function() { + timeCircle(); + }); + }); + + //drawTime(convertToPath([0, 2, 4, 2]), console.log); + //setTimeout(function() { drawTime(convertToPath(getTheTime())) }, 12500); + //setTimeout(function() { drawTime(convertToPath([0, 8, 5, 8]))}, 12500); + //setTimeout(function() { timeCircle() }, 12501); + } + + tellTime(); + clockTimer = setInterval(function() { tellTime() }, 60000); //Repeat every 60 seconds +} + +//A function to stop the clock +//The clock will finish erasing, drawing, and moving the arms in a circle, but it will not repeat +SVGReader.prototype.clearClock = function() { + clearInterval(clockTimer); + console.log("Stopping clock."); +} + +//Says 'hello' in multiple languages +SVGReader.prototype.hello = function() { + resetTimer(); + var fileArray = fs.readdirSync("./hello"); + var fileNum; + objRef = this; + + //Used to access the erase functions + var drawing = new draw.Draw({ + baseWidth: objRef.baseWidth, + baseHeight: objRef.baseHeight, + drawHeight: objRef.drawHeight + }); + + //Picks a file at random from a folder of 'hello's + pickFile = function() { + do { + fileNum = Math.floor(Math.random() * fileArray.length); + } + while (fileNum == lastNum1 || fileNum == lastNum2); //Generates numbers until a number that has not been used in the past two calls is picked + //A language can only be used every third time + + lastNum2 = lastNum1; //Adjusts the last two numbers used + lastNum1 = fileNum; + + //Checks to see if the characters should be drawn connected or not + //Specified in file name + if (fileArray[fileNum].charAt(7) === 'T') + connect = true; + else + connect = false; + + return "./hello/" + fileArray[fileNum]; + } + + //Says hello. + sayHello = function() { + resetTimer(); + var fileChoice = pickFile(); + drawing.erase(function() { + setTimeout(function() { objRef.drawSVG(fileChoice, connect) }, 10000); + }); + } + + sayHello(); + helloTimer = setInterval(function() { sayHello() }, 60000); //Repeat every minute +} + +//A function to cancel saying hello +//The robot will finish writing/erasing if it has already started but after that it will stop +SVGReader.prototype.sayGoodbye = function() { + clearInterval(helloTimer); + console.log("Goodbye!"); +} + +//Cycles through all the languages rather than picking one at random +SVGReader.prototype.helloCycle = function() { + resetTimer(); + var fileArray = fs.readdirSync("./hello"); + objRef = this; + var fileNum = 0; + + //Used to access the erase functions + var drawing = new draw.Draw({ + baseWidth: objRef.baseWidth, + baseHeight: objRef.baseHeight, + drawHeight: objRef.drawHeight + }); + + //Function to actually write hello + sayHello = function() { + var fileChoice = "./hello/" + fileArray[fileNum]; + + if (fileChoice.charAt(15) === 'T') + connect = true; + else + connect = false; + + //drawing.erase(function() { + // setTimeout(function() { objRef.drawSVG(fileChoice, connect) }, 10000); + //}); + + fileNum++; + + if (fileNum >= fileArray.length) //Once the end of the list is reached, stop writing + clearInterval(helloTimer); + } + + sayHello(); + helloTimer = setInterval(function() { sayHello() }, 60000); + +} + +//Saves the coordinates of the first point drawn +var firstPoint; + +var objArr; +var pathArray; + +var widthRatio; +var heightRatio; +var halfway; +var currentPoint; +var penHeight; +var delay, loaded, fontFile; +var transformX, transformY, objRef, connected, startTime, endTime, difference, clockTimer, firstMove, lastNum1, lastNum2, connect, baseWidth, baseHeight, defaultEaseType; + +module.exports.SVGReader = SVGReader; diff --git a/software/src/bot.js b/software/src/bot.js index f313d6a..4c0cb8e 100755 --- a/software/src/bot.js +++ b/software/src/bot.js @@ -1,23 +1,115 @@ five = require("johnny-five"); -ik = require("./ik"); +kinematics = require("./kinematics"); +svgRead = require("./SVGReader"); +drawing = require("./draw"); +motion = require("./motion"); + +//If a filepath is specified, load that config +//Otherwise, resort to the default config +//> Usage: +//> node bot.js "C:\Projects\Tapsterbot\software\config.js" +if (process.argv[2]) { + try { + var config = require(process.argv[2]); + console.log("Config found and loaded."); + } catch (e) { + console.log("Config not found. Loading default."); + var config = require("../config.js"); + } +} +else { + console.log("Config not specified. Loading default."); + var config = require("../config.js"); +} + +//Alternate config loading code +//If a Tapster version is specified, load that config +//Otherwise resort to the default config +//> Usage: +//> node bot.js "Tapster-2-plus" + +/*if (process.argv[2]) { + try { + var config = require("../" + process.argv[2] + ".js"); + console.log("Config found and loaded."); + } catch (e) { + console.log("Config not found. Loading default."); + var config = require("../config.js"); + } +} +else { + console.log("Config not specified. Loading default."); + var config = require("../config.js"); +} */ + +k = new kinematics.Kinematics({ + e: config.e, + f: config.f, + re: config.re, + rf: config.rf +}); + +svg = new svgRead.SVGReader({ + baseWidth: config.baseWidth, + baseHeight: config.baseHeight, + drawHeight: config.drawHeight, + delay: config.delay, + defaultEaseType: config.defaultEaseType +}); + +draw = new drawing.Draw({ + baseWidth: config.baseWidth, + baseHeight: config.baseHeight, + drawHeight: config.drawHeight, + defaultEaseType: config.defaultEaseType +}); + board = new five.Board({ debug: false }); +var steps = 5; +var delay = config.delay / steps; +var defaultEaseType = config.defaultEaseType; + +current = [0, 0, -140]; +timer = 0; + board.on("ready", function() { // Setup - servo1 = five.Servo({ - pin: 9, - range: [0,90] + /*servo1 = five.Servo({ + address: 0x40, + controller: "PCA9685", + pin: 0, + range: [35, 145] //Too high of a minimum input will cause issues with the forward kinematics }); servo2 = five.Servo({ - pin: 10, - range: [0,90] + address: 0x40, + controller: "PCA9685", + pin: 1, + range: [35, 145] }); servo3 = five.Servo({ - pin: 11, - range: [0, 90] - }); + address: 0x40, + controller: "PCA9685", + pin: 2, + range: [35, 145] + }); */ + + servo1 = five.Servo({ + pin: 9, + range: [0, 100] + }); + + servo2 = five.Servo({ + pin: 10, + range: [0, 100] + }); + + servo3 = five.Servo({ + pin: 11, + range: [0, 100] + }); servo1.on("error", function() { console.log(arguments); @@ -42,10 +134,11 @@ board.on("ready", function() { var max = 15; var min = 5; var range = max - min; - servo1.to(min); - servo2.to(min); - servo3.to(min); + servo1.to(15); + servo2.to(15); + servo3.to(15); + /* var dance = function() { servo1.to(parseInt((Math.random() * range) + min, 10)); servo2.to(parseInt((Math.random() * range) + min, 10)); @@ -66,14 +159,13 @@ board.on("ready", function() { } board.repl.inject({ - dance: start_dance, + dance: start_dance, chill: stop_dance - }); + }); */ }); - Number.prototype.map = function ( in_min , in_max , out_min , out_max ) { return ( this - in_min ) * ( out_max - out_min ) / ( in_max - in_min ) + out_min; } @@ -100,23 +192,57 @@ sin = function(degree) { // A cosine function for working with degrees, not radians cos = function(degree) { - return Math.cos(Math.PI * (degree/180)); + return Math.cos(Math.PI * (degree/180)); } - -// TODO: pull out map values to config file or some other solution. -go = function(x, y, z) { +moveServosTo = function(x, y, z) { + current = [x, y, z] reflected = reflect(x,y); rotated = rotate(reflected[0],reflected[1]); - - angles = ik.inverse(rotated[0], rotated[1], z); - servo1.to((angles[1]).map( 0 , 90 , 8 , 90 )); - servo2.to((angles[2]).map( 0 , 90 , 8 , 90 )); - servo3.to((angles[3]).map( 0 , 90 , 8 , 90 )); + + angles = k.inverse(rotated[0], rotated[1], z); + + servo1.to((angles[1]).map(config.servo1.in_min, config.servo1.in_max, config.servo1.out_min, config.servo1.out_max)); + servo2.to((angles[2]).map(config.servo2.in_min, config.servo2.in_max, config.servo2.out_min, config.servo2.out_max)); + servo3.to((angles[3]).map(config.servo3.in_min, config.servo3.in_max, config.servo3.out_min, config.servo3.out_max)); console.log(angles); } +go = function(x, y, z, easeType) { + var pointB = [x, y, z]; + if (easeType == "none") { + moveServosTo(pointB[0], pointB[1], pointB[2]); + return; //Ensures that it doesn't move twice + } + + else if (!easeType) + easeType = defaultEaseType //If no easeType is specified, go with default (specified in config.js) + + //motion.move(current, pointB, steps, easeType, delay); + var points = motion.getPoints(current, pointB, steps, easeType); + + for (var i = 0; i < points.length; i++) { + setTimeout( function(point) { moveServosTo(point[0], point[1], point[2]) }, i * delay, points[i]); + } +} + +//Returns the coordinates of the end effector, based on the angles +//Using the map function messes up these values +//Simply passing in the original angles will return the correct coordinates position = function() { - return ik.forward(servo1.last.degrees, servo2.last.degrees, servo3.last.degrees); + return k.forward(servo1.last.degrees, servo2.last.degrees, servo3.last.degrees); } +//A separate setTimeout method so that delays work properly +doSetTimeout = function(x, y, z, timeDelay, easing) { + if (!easing) + easing = defaultEaseType; + + setTimeout(function() { go(x, y, z, easing) }, timer); + timer = timer + timeDelay; +}; + + +resetTimer = function() { + timer = 0; +} \ No newline at end of file diff --git a/software/src/data.json b/software/src/data.json new file mode 100644 index 0000000..d77a6af --- /dev/null +++ b/software/src/data.json @@ -0,0 +1,240 @@ +[[ + { + "x": 2.013888359069824, + "y": -0.9861116409301758 + }, + { + "x": 0.013888359069824219, + "y": -0.9861116409301758 + }, + { + "x": 0.013888359069824219, + "y": 4.013888359069824 + }, + { + "x": 9.013888359069824, + "y": 23.01388931274414 + }, + { + "x": 24.01388931274414, + "y": 36.01388931274414 + }, + { + "x": 35.01388931274414, + "y": 48.01388931274414 + }, + { + "x": 44.01388931274414, + "y": 65.01388549804688 + }, + { + "x": 47.01388931274414, + "y": 76.01388549804688 + }, + { + "x": 55.01388931274414, + "y": 93.01388549804688 + }, + { + "x": 67.01388549804688, + "y": 104.01388549804688 + }, + { + "x": 85.01388549804688, + "y": 117.01388549804688 + }, + { + "x": 106.01388549804688, + "y": 149.01388549804688 + }, + { + "x": 107.01388549804688, + "y": 154.01388549804688 + }, + { + "x": 114.01388549804688, + "y": 165.01388549804688 + }, + { + "x": 123.01388549804688, + "y": 173.01388549804688 + }, + { + "x": 132.01388549804688, + "y": 178.01388549804688 + }, + { + "x": 163.01388549804688, + "y": 214.01388549804688 + }, + { + "x": 187.01388549804688, + "y": 233.01388549804688 + }, + { + "x": 226.01388549804688, + "y": 276.0138854980469 + }, + { + "x": 252.01388549804688, + "y": 290.0138854980469 + }, + { + "x": 255.01388549804688, + "y": 294.0138854980469 + }, + { + "x": 260.0138854980469, + "y": 312.0138854980469 + }, + { + "x": 262.0138854980469, + "y": 313.0138854980469 + }, + { + "x": 266.0138854980469, + "y": 323.0138854980469 + }, + { + "x": 270.0138854980469, + "y": 326.0138854980469 + }, + { + "x": 279.0138854980469, + "y": 329.0138854980469 + }, + { + "x": 287.0138854980469, + "y": 337.0138854980469 + } +] +, [ + { + "x": 294.0138854980469, + "y": 6.013888359069824 + }, + { + "x": 255.01388549804688, + "y": 49.01388931274414 + }, + { + "x": 253.01388549804688, + "y": 53.01388931274414 + }, + { + "x": 245.01388549804688, + "y": 59.01388931274414 + }, + { + "x": 242.01388549804688, + "y": 68.01388549804688 + }, + { + "x": 239.01388549804688, + "y": 70.01388549804688 + }, + { + "x": 235.01388549804688, + "y": 82.01388549804688 + }, + { + "x": 225.01388549804688, + "y": 92.01388549804688 + }, + { + "x": 216.01388549804688, + "y": 97.01388549804688 + }, + { + "x": 209.01388549804688, + "y": 106.01388549804688 + }, + { + "x": 203.01388549804688, + "y": 123.01388549804688 + }, + { + "x": 195.01388549804688, + "y": 134.01388549804688 + }, + { + "x": 178.01388549804688, + "y": 145.01388549804688 + }, + { + "x": 169.01388549804688, + "y": 154.01388549804688 + }, + { + "x": 156.01388549804688, + "y": 176.01388549804688 + }, + { + "x": 153.01388549804688, + "y": 185.01388549804688 + }, + { + "x": 126.01388549804688, + "y": 229.01388549804688 + }, + { + "x": 115.01388549804688, + "y": 244.01388549804688 + }, + { + "x": 102.01388549804688, + "y": 258.0138854980469 + }, + { + "x": 88.01388549804688, + "y": 269.0138854980469 + }, + { + "x": 87.01388549804688, + "y": 272.0138854980469 + }, + { + "x": 68.01388549804688, + "y": 282.0138854980469 + }, + { + "x": 54.01388931274414, + "y": 287.0138854980469 + }, + { + "x": 50.01388931274414, + "y": 292.0138854980469 + }, + { + "x": 47.01388931274414, + "y": 305.0138854980469 + }, + { + "x": 43.01388931274414, + "y": 310.0138854980469 + }, + { + "x": 35.01388931274414, + "y": 315.0138854980469 + }, + { + "x": 30.01388931274414, + "y": 315.0138854980469 + }, + { + "x": 20.01388931274414, + "y": 320.0138854980469 + }, + { + "x": 20.01388931274414, + "y": 322.0138854980469 + }, + { + "x": 16.01388931274414, + "y": 326.0138854980469 + }, + { + "x": 13.013888359069824, + "y": 337.0138854980469 + } +]] diff --git a/software/src/demo/angry-birds/angrybirds.js b/software/src/demo/angry-birds/angrybirds.js index 697d9ac..86643ec 100644 --- a/software/src/demo/angry-birds/angrybirds.js +++ b/software/src/demo/angry-birds/angrybirds.js @@ -1,24 +1,217 @@ +var five = require("johnny-five") +var kin = require("./../../kinematics.js"); +var config = require("./../../../config.js"); + +drawHeight = config.drawHeight; + +board = new five.Board({ + debug: false +}); + +k = new kin.Kinematics({ + e: config.e, + f: config.f, + re: config.re, + rf: config.rf +}) + +board.on("ready", function() { + // Setup + /*servo1 = five.Servo({ + address: 0x40, + controller: "PCA9685", + pin: 0, + range: [35, 145] //Too high of a minimum input will cause issues with the forward kinematics + }); + servo2 = five.Servo({ + address: 0x40, + controller: "PCA9685", + pin: 1, + range: [35, 145] + }); + servo3 = five.Servo({ + address: 0x40, + controller: "PCA9685", + pin: 2, + range: [35, 145] + }); */ + + servo1 = five.Servo({ + pin: 9, + range: [0, 100] + }); + + servo2 = five.Servo({ + pin: 10, + range: [0, 100] + }); + + servo3 = five.Servo({ + pin: 11, + range: [0, 100] + }); + + servo1.on("error", function() { + console.log(arguments); + }) + servo2.on("error", function() { + console.log(arguments); + }) + servo3.on("error", function() { + console.log(arguments); + }) + + board.repl.inject({ + servo1: servo1, + s1: servo1, + servo2: servo2, + s2: servo2, + servo3: servo3, + s3: servo3, + }); + + // Move to starting point + var max = 15; + var min = 5; + var range = max - min; + servo1.to(15); + servo2.to(15); + servo3.to(15); + + /* + var dance = function() { + servo1.to(parseInt((Math.random() * range) + min, 10)); + servo2.to(parseInt((Math.random() * range) + min, 10)); + servo3.to(parseInt((Math.random() * range) + min, 10)); + }; + + var dancer; + + start_dance = function() { + if (!dancer) dancer = setInterval(dance, 250); + } + + stop_dance = function() { + if (dancer) { + clearInterval(dancer); + dancer = null; + } + } + + board.repl.inject({ + dance: start_dance, + chill: stop_dance + }); */ + + +}); + +rotate = function(x,y) { + var theta = -60 * Math.PI / 180; + x1 = x * Math.cos(theta) - y * Math.sin(theta); + y1 = y * Math.cos(theta) + x * Math.sin(theta); + return [x1,y1] +} + +reflect = function(x,y) { + var theta = 0; + x1 = x; + y1 = x * Math.sin(2*theta) - y * Math.cos(2*theta); + return [x1,y1] +} + +Number.prototype.map = function ( in_min , in_max , out_min , out_max ) { + return ( this - in_min ) * ( out_max - out_min ) / ( in_max - in_min ) + out_min; +} + +moveServosTo = function(x, y, z) { + reflected = reflect(x,y); + rotated = rotate(reflected[0],reflected[1]); + + angles = k.inverse(rotated[0], rotated[1], z); + + servo1.to((angles[1]).map(config.servo1.in_min, config.servo1.in_max, config.servo1.out_min, config.servo1.out_max)); + servo2.to((angles[2]).map(config.servo2.in_min, config.servo2.in_max, config.servo2.out_min, config.servo2.out_max)); + servo3.to((angles[3]).map(config.servo3.in_min, config.servo3.in_max, config.servo3.out_min, config.servo3.out_max)); + console.log(angles); +} + move = function(x,y,z, when) { - setTimeout(function(){ go(x,y,z);}, when); + setTimeout(function(){ moveServosTo(x,y,z) }, moveTimer); + moveTimer += when; +} + +resetMoveTimer = function() { + moveTimer = 0; +} + +var moveTimer = 0; + +playLevelOne = function(){ + resetMoveTimer(); + move(-20, -3, drawHeight + 10, 0); + move(-20, -3, drawHeight, 500); + move(-35,-6, drawHeight,300); + move(-35,-6, drawHeight + 10,400); + move(-30,0, drawHeight + 10,400); + + move(42,-18, drawHeight + 10, 2800); + move(42,-18, drawHeight - 3, 500); + move(42, -18, drawHeight + 10, 500); + + move(9,-13, drawHeight + 10, 3000); + move(9,-13, drawHeight - 2, 300); + move(9, -13, drawHeight + 10,300); + move(0,0, drawHeight + 10,300); } -play = function(){ - move(-10,-20,-140,0); - move(-10,-20,-149,200); - move(-25,-28,-148,500); - move(-25,-28,-140,800); - move(0,0,-120,1200); +playLevelTwo = function() { + resetMoveTimer(); + move(-20, -3, drawHeight + 10, 500); + move(-20, -3, drawHeight, 200); + move(-35, -2, drawHeight, 300); + move(-35, -2, drawHeight + 10, 300); + move(-20, -3, drawHeight + 10, 15000); + move(-20, -3, drawHeight - 1, 300); + move(-35, -8, drawHeight, 400); + move(-35, -8, drawHeight + 10, 300); - move(34,-36,-140, 4000); - move(34,-36,-150, 4500); - move(34,-36,-140, 5000); + move(42, -18, drawHeight + 10, 6000); + move(42, -18, drawHeight - 3, 500); + move(42, -18, drawHeight + 10, 500); - move(4,-32,-140,0+8000); - move(4,-32,-149,300+8000); - move(4,-32,-140,600+8000); - move(0,0,-120,900+8000); + move(9, -13, drawHeight + 10, 3000); + move(9, -13, drawHeight - 2, 300); + move(9, -13, drawHeight + 10, 300); + move(0, 0, drawHeight + 10, 400); } +playLevelThree = function() { + resetMoveTimer(); + move(-20, -3, drawHeight + 10, 1000); + move(-20, -3, drawHeight, 200); + move(-25, -10, drawHeight, 350); + move(-23, -6.5, drawHeight, 350); + move(-23, -6.5, drawHeight + 10, 350); + + move(42, -18, drawHeight + 10, 8000); + move(42, -18, drawHeight - 3, 500); + move(42, -18, drawHeight + 10, 500); +}; + +goToLevelOne = function() { + resetMoveTimer(); + move(-14, -12, drawHeight + 10, 250); + move(-14, -12, drawHeight - 2, 250); + move(-14, -12, drawHeight + 10, 250); + move(-48, 23, drawHeight + 10, 1000); + move(-46, 23, drawHeight - 2, 1000); + move(-48, 23, drawHeight + 10, 500); + move(45, -18, drawHeight + 10, 500); + move(41, -18, drawHeight - 4, 500); + move(45, -18, drawHeight + 10, 500); + move(0, 0, drawHeight + 10, 250); +} repeat = function(){ move(4,-32,-140,0); @@ -27,10 +220,25 @@ repeat = function(){ move(0,0,-120,900); } +playLevels = function() { + resetMoveTimer(); + var objRef = this; + setTimeout(objRef.playLevelOne, 0); + setTimeout(objRef.playLevelTwo, 15000); + setTimeout(objRef.playLevelThree, 47500); + setTimeout(goToLevelOne, 65000); +} + play_forever = function(){ + var objRef = this; console.log("Now playing forever...") - play(); - interval = setInterval(play,13000); + this.playLevels(); + interval = setInterval(objRef.playLevels, 70000); return interval; +} + +stop_playing = function() { + clearInterval(interval); + console.log("No longer playing forever."); } \ No newline at end of file diff --git a/software/src/demo/hi.js b/software/src/demo/hi.js new file mode 100644 index 0000000..768f5bd --- /dev/null +++ b/software/src/demo/hi.js @@ -0,0 +1,70 @@ +five = require("johnny-five"); +bot = require("bot"); +board = new five.Board({ + debug: false +}); + +board.on("ready", function() { + // Setup + servo1 = five.Servo({ + pin: 9, + range: [0,90] + }); + servo2 = five.Servo({ + pin: 10, + range: [0,90] + }); + servo3 = five.Servo({ + pin: 11, + range: [0, 90] + }); + + servo1.on("error", function() { + console.log(arguments); + }) + servo2.on("error", function() { + console.log(arguments); + }) + servo3.on("error", function() { + console.log(arguments); + }) + + board.repl.inject({ + servo1: servo1, + s1: servo1, + servo2: servo2, + s2: servo2, + servo3: servo3, + s3: servo3, + }); + + bot.go(0, 0, -140); + }); + +hi = function() { + setTimeout(function() { go(-20, 20, -150) }, 0); + setTimeout(function() { go(-20, -20, -150) }, 250); + setTimeout(function() { go(-20, 0, -150) }, 500); + setTimeout(function() { go(-10, 0, -150) }, 750) + setTimeout(function() { go(-10, 20, -150) }, 1000); + setTimeout(function() { go(-10, -20, -150) }, 1250); + + setTimeout(function() { go(-10, -20, -140) }, 1500); + setTimeout(function() { go(-5, -20, -140) }, 1750) + setTimeout(function() { go(-5, -20, -150) }, 2000); + setTimeout(function() { go(-5, 0, -150) }, 2250); + setTimeout(function() { go(-5, 0, -140) }, 2500); + setTimeout(function() { go(-5, 10, -140) }, 2750); + setTimeout(function() { go(-5, 10, -150) }, 3000); + setTimeout(function() { go(-5, 15, -150) }, 3250); + + setTimeout(function() { go(-5, 20, -140) }, 3500); + setTimeout(function() { go(5, 20, -140) }, 3750); + setTimeout(function() { go(5, 20, -150) }, 4000); + setTimeout(function() { go(5, -10, -150) }, 4250); + setTimeout(function() { go(5, -10, -140) }, 4500); + setTimeout(function() { go(5, -15, -140) }, 4750); + setTimeout(function() { go(5, -15, -150) }, 5000); + setTimeout(function() { go(5, -20, -150) }, 5250); + setTimeout(function() { go(5, 0, -140) }, 5500); + } diff --git a/software/src/draw.js b/software/src/draw.js new file mode 100644 index 0000000..066eb05 --- /dev/null +++ b/software/src/draw.js @@ -0,0 +1,345 @@ +//Draws lines and shapes. + +var fs = require('fs'); +var objRef, defaultEaseType; +var calculated, spiralPts; + +function Draw(args) { + this.baseWidth = 80; + this.baseHeight = 95; + this.drawHeight = -140; + this.defaultEaseType = "linear"; + + if (args) { + var keys = Object.keys(args); + keys.forEach(function(key){ + this[key] = args[key]; + }, this) + } + penHeight = this.drawHeight; + defaultEaseType = this.defaultEaseType; + objRef = this; +} + +//Maps point from canvas to the Tapster coordinate plane +//The conversion is based on the canvas size and the size of the Tapster base +//These can be changed as needed +mapPoints = function(x, y) { + var newX = x; + var newY = y; + + newX = (newX - halfway.x) / widthRatio; + newY = (halfway.y - newY) / heightRatio; + return {x:newX, y:newY}; +}; + +//Adds delays while the Tapster is writing +//The specific delay can be changed if the bot has to go slower or faster for a particular segment +/*doSetTimeout = function(x, y, z, delay, easing) { + if (!easing) + easing = defaultEaseType; + setTimeout(function() { go(x, y, z, easing) }, timer); + currentPoint = {x: x, y: y, z: z}; + timer = timer + delay; +}; */ + +//Initialized here so that they are accessible from the mapPoints function +var baseHeight, baseWidth, canvasHeight, canvasWidth, heightRatio, widthRatio, halfway; + +var currentPoint = {x: 0, y: 0, z: -140}; +var penHeight = this.drawHeight; + +//Set the penHeight from the command line +Draw.prototype.setPenHeight = function(height) { + penHeight = height; + console.log("The pen is now set at: " + height); +} + +//Draws an image from a JSON file of coordinates +//The SVGReader drawSVG method is preferred +Draw.prototype.drawFromCoordinates = function() { + + baseHeight = this.baseHeight; + baseWidth = this.baseWidth; + canvasHeight = baseHeight * 3.779527559; //Set ratio of ~1:3.8 mm:px + canvasWidth = baseWidth * 3.779527559; + + //The ratio between the sizes of the canvas and robot. + //It will always be 3.779527559 because the canvas size is set + //according to that ratio + heightRatio = 3.779527559; + widthRatio = 3.779527559; + + //The center of the canvas + halfway = {x:canvasWidth / 2, y:canvasHeight / 2}; + + + var jFile = fs.readFileSync('.//data.json', 'utf8'); //Reads data from a JSON file of coordinates + var objArr = JSON.parse(jFile); //Creates an array out of the data + + //Loops through the JSON array + for (var i = 0; i < objArr.length; i++) { + var x = 0; + if (objArr[i].length > 0) { //If there are multiple lines + var point = objArr[i][x]; + var transMap = mapPoints(point.x, point.y); + doSetTimeout(transMap.x, transMap.y, penHeight + 20, 200); //Moves the arm vertically so that it does not draw a line between the last point of one line + //and the first point of another + + for (x = 0; x < objArr[i].length; x++) { + point = objArr[i][x]; + var mapped = mapPoints(point.x, point.y); + doSetTimeout(mapped.x, mapped.y, penHeight, 100); + } + transMap = mapPoints(point.x, point.y); + doSetTimeout(transMap.x, transMap.y, penHeight + 20, 200); + } + + else { //Only one line to be drawn + point = objArr[i]; + var mapped = mapPoints(point.x, point.y); + doSetTimeout(mapped.x, mapped.y, penHeight, 100); + } + } +}; + +//Draws a square in order to ensure that everything is working properly +//Optional args: +//sideLength: the length of each side (default: 20) +//n: draws every nth point (default: 2) +Draw.prototype.drawSquare = function(args) { + + //Default values + this.sideLength = 20; + this.n = 2; + + if (args) { + var keys = Object.keys(args) + keys.forEach(function(key){ + this[key] = args[key] + }, this) + } + + resetTimer(); //Reset the timer so that there isn't unnecessary delay when calling the function multiple times + + var halfSide = this.sideLength / 2; + var points = this.sideLength / this.n; + + doSetTimeout(-halfSide, halfSide, penHeight + 10, 0) + doSetTimeout(-halfSide, halfSide, penHeight, 500); //Top left corner + + for (var i = 0; i < points; i++) { //To bottom left + doSetTimeout(-halfSide, halfSide - (this.n * i), penHeight, i * 5); + } + + for (var i = 0; i < points; i++) { //To bottom right + doSetTimeout(-halfSide + (this.n * i), -halfSide, penHeight, i * 5); + } + + for (var i = 0; i < points; i++) { //To top right + doSetTimeout(halfSide, -halfSide + (this.n * i), penHeight, i * 5); + } + + for (var i = 0; i < points; i++) { //To top left + doSetTimeout(halfSide - (this.n * i), halfSide, penHeight, i * 5); + } + + doSetTimeout(0, 0, -140, timer + 100); + +}; + +//Draws a star to test that the Tapster bot is working properly +Draw.prototype.drawStar = function() { + resetTimer(); + doSetTimeout(-20, -20, penHeight, 1000); + doSetTimeout(0, 30, penHeight, 1000); + doSetTimeout(20, -20, penHeight, 1000); + doSetTimeout(-30, 10, penHeight, 1000); + doSetTimeout(30, 10, penHeight, 1000); + doSetTimeout(-20, -20, penHeight, 1000); + + //-20, -20, 0, 30, 20, -20 +}; + + +Draw.prototype.drawTriangle = function(x, y, x1, y1, x2, y2) { + resetTimer(); + doSetTimeout(x, y, penHeight, 1000); + doSetTimeout(x1, y1, penHeight, 1000); + doSetTimeout(x2, y2, penHeight, 1000); + doSetTimeout(x, y, penHeight, 1000); +} + +//Draws a circle +//Optional args: +//centerX: the x coordinate of the center (default: 0) +//centerY: the y coordinate of the center (default: 0) +//radius: the radius of the center (default: 20) +Draw.prototype.drawCircle = function(args) { + resetTimer(); + + this.centerX = 0; + this.centerY = 0; + this.radius = 20; + + if (args) { + var keys = Object.keys(args) + keys.forEach(function(key){ + this[key] = args[key] + }, this) + } + + // an array to save your points + var points=[]; + + // populate array with points along a circle + //Goes slightly over so that the circle is actually completed + for (var degree=0; degree < 395; degree++) { + var radians = (degree + 90) * Math.PI/180; + var x = this.centerX + this.radius * Math.cos(radians); + var y = this.centerY + this.radius * Math.sin(radians); + points.push({x:x,y:y}); + } + + circle = function() { + doSetTimeout(0, 0, -120, 50, "none"); + doSetTimeout(points[0].x, points[0].y, penHeight + 10, 150, "none"); + doSetTimeout(points[0].x, points[0].y, penHeight, 150, "none"); + for (var i=0; i + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/software/src/example/textTutorial.svg b/software/src/example/textTutorial.svg new file mode 100644 index 0000000..d816475 --- /dev/null +++ b/software/src/example/textTutorial.svg @@ -0,0 +1,74 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/software/src/example/tutorial.svg b/software/src/example/tutorial.svg new file mode 100644 index 0000000..70ed47d --- /dev/null +++ b/software/src/example/tutorial.svg @@ -0,0 +1,77 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/software/src/hello/helloChF.svg b/software/src/hello/helloChF.svg new file mode 100644 index 0000000..2fe5d96 --- /dev/null +++ b/software/src/hello/helloChF.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/software/src/hello/helloEnT.svg b/software/src/hello/helloEnT.svg new file mode 100644 index 0000000..c5fa472 --- /dev/null +++ b/software/src/hello/helloEnT.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/software/src/hello/helloFrF.svg b/software/src/hello/helloFrF.svg new file mode 100644 index 0000000..e6997af --- /dev/null +++ b/software/src/hello/helloFrF.svg @@ -0,0 +1,101 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/software/src/hello/helloItF.svg b/software/src/hello/helloItF.svg new file mode 100644 index 0000000..b7dac35 --- /dev/null +++ b/software/src/hello/helloItF.svg @@ -0,0 +1,80 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/software/src/hello/helloJpF.svg b/software/src/hello/helloJpF.svg new file mode 100644 index 0000000..aff0c36 --- /dev/null +++ b/software/src/hello/helloJpF.svg @@ -0,0 +1,126 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/software/src/hello/helloRuF.svg b/software/src/hello/helloRuF.svg new file mode 100644 index 0000000..d3d732e --- /dev/null +++ b/software/src/hello/helloRuF.svg @@ -0,0 +1,90 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/software/src/hello/helloSpT.svg b/software/src/hello/helloSpT.svg new file mode 100644 index 0000000..ae19b57 --- /dev/null +++ b/software/src/hello/helloSpT.svg @@ -0,0 +1,74 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/software/src/ik.js b/software/src/ik.js deleted file mode 100755 index e129746..0000000 --- a/software/src/ik.js +++ /dev/null @@ -1,134 +0,0 @@ -// Original code from -// http://forums.trossenrobotics.com/tutorials/introduction-129/delta-robot-kinematics-3276/ - -(function(exports) { - - // Specific geometry for Tapster: - var e = 34.64101615137754; // Math.sqrt(3) * 10 * 2 - var f = 110.85125168440814; // Math.sqrt(3) * 32 * 2 - var re = 153.5; // 145 + 8.5 - var rf = 52.690131903421914; // Math.sqrt(52**2 + 8.5**2) - - exports.updateSize = function(parameters) { - e = parameters[0]; - f = parameters[1]; - re = parameters[2]; - rf = parameters[3]; - exports.e = e; - exports.f = f; - exports.re = re; - exports.rf = rf; - }; - - exports.getSize = function() { - return new Array(e, f, re, rf); - }; - - // Trigonometric constants - var s = 165 * 2; - var sqrt3 = Math.sqrt(3.0); - var pi = 3.141592653; - var sin120 = sqrt3 / 2.0; - var cos120 = -0.5; - var tan60 = sqrt3; - var sin30 = 0.5; - var tan30 = 1.0 / sqrt3; - - // Forward kinematics: (theta1, theta2, theta3) -> (x0, y0, z0) - // Returned {error code,theta1,theta2,theta3} - exports.forward = function(theta1, theta2, theta3) { - var x0 = 0.0; - var y0 = 0.0; - var z0 = 0.0; - - var t = (f - e) * tan30 / 2.0; - var dtr = pi / 180.0; - - theta1 *= dtr; - theta2 *= dtr; - theta3 *= dtr; - - var y1 = -(t + rf * Math.cos(theta1)); - var z1 = -rf * Math.sin(theta1); - - var y2 = (t + rf * Math.cos(theta2)) * sin30; - var x2 = y2 * tan60; - var z2 = -rf * Math.sin(theta2); - - var y3 = (t + rf * Math.cos(theta3)) * sin30; - var x3 = -y3 * tan60; - var z3 = -rf * Math.sin(theta3); - - var dnm = (y2 - y1) * x3 - (y3 - y1) * x2; - - var w1 = y1 * y1 + z1 * z1; - var w2 = x2 * x2 + y2 * y2 + z2 * z2; - var w3 = x3 * x3 + y3 * y3 + z3 * z3; - - // x = (a1*z + b1)/dnm - var a1 = (z2 - z1) * (y3 - y1) - (z3 - z1) * (y2 - y1); - var b1 = -((w2 - w1) * (y3 - y1) - (w3 - w1) * (y2 - y1)) / 2.0; - - // y = (a2*z + b2)/dnm; - var a2 = -(z2 - z1) * x3 + (z3 - z1) * x2; - var b2 = ((w2 - w1) * x3 - (w3 - w1) * x2) / 2.0; - - // a*z^2 + b*z + c = 0 - var a = a1 * a1 + a2 * a2 + dnm * dnm; - var b = 2.0 * (a1 * b1 + a2 * (b2 - y1 * dnm) - z1 * dnm * dnm); - var c = (b2 - y1 * dnm) * (b2 - y1 * dnm) + b1 * b1 + dnm * dnm * (z1 * z1 - re * re); - - // discriminant - var d = b * b - 4.0 * a * c; - if (d < 0.0) { - return new Array(1, 0, 0, 0); // non-existing povar. return error,x,y,z - } - - z0 = -0.5 * (b + Math.sqrt(d)) / a; - x0 = (a1 * z0 + b1) / dnm; - y0 = (a2 * z0 + b2) / dnm; - - return new Array(0, x0, y0, z0); - }; - - // Inverse kinematics - // Helper functions, calculates angle theta1 (for YZ-pane) - var delta_calcAngleYZ = function(x0, y0, z0) { - var y1 = -0.5 * 0.57735 * f; // f/2 * tg 30 - y0 -= 0.5 * 0.57735 * e; // shift center to edge - // z = a + b*y - var a = (x0 * x0 + y0 * y0 + z0 * z0 + rf * rf - re * re - y1 * y1) / (2.0 * z0); - var b = (y1 - y0) / z0; - - // discriminant - var d = -(a + b * y1) * (a + b * y1) + rf * (b * b * rf + rf); - if (d < 0) { - return new Array(1, 0); // non-existing povar. return error, theta - } - - var yj = (y1 - a * b - Math.sqrt(d)) / (b * b + 1); // choosing outer povar - var zj = a + b * yj; - var theta = Math.atan(-zj / (y1 - yj)) * 180.0 / pi + ((yj > y1) ? 180.0 : 0.0); - - return new Array(0, theta); // return error, theta - }; - - exports.inverse = function(x0, y0, z0) { - var theta1 = 0; - var theta2 = 0; - var theta3 = 0; - var status = delta_calcAngleYZ(x0, y0, z0); - - if (status[0] === 0) { - theta1 = status[1]; - status = delta_calcAngleYZ(x0 * cos120 + y0 * sin120, y0 * cos120 - x0 * sin120, z0, theta2); - } - if (status[0] === 0) { - theta2 = status[1]; - status = delta_calcAngleYZ(x0 * cos120 - y0 * sin120, y0 * cos120 + x0 * sin120, z0, theta3); - } - theta3 = status[1]; - - return new Array(status[0], theta1, theta2, theta3); - }; -}(typeof exports === 'undefined' ? this.ik = {} : exports)); diff --git a/software/src/kinematics.js b/software/src/kinematics.js new file mode 100644 index 0000000..400eeb1 --- /dev/null +++ b/software/src/kinematics.js @@ -0,0 +1,135 @@ +// Trigonometric constants +var s = 165 * 2; +var sqrt3 = Math.sqrt(3.0); +var pi = 3.141592653; +var sin120 = sqrt3 / 2.0; +var cos120 = -0.5; +var tan60 = sqrt3; +var sin30 = 0.5; +var tan30 = 1.0 / sqrt3; + +function Kinematics(args) { + //Side of end effector + this.e = 0; + + //Side of top triangle + this.f = 0; + + //Length of parallelogram joint + this.re = 0; + + //Length of upper joint + this.rf = 0; + + if (args) { + var keys = Object.keys(args) + keys.forEach(function(key){ + this[key] = args[key] + }, this) + } +} + +// Forward kinematics: (theta1, theta2, theta3) -> (x0, y0, z0) +// Returned {error code,theta1,theta2,theta3} +Kinematics.prototype.forward = function(theta1, theta2, theta3) { + var x0 = 0.0; + var y0 = 0.0; + var z0 = 0.0; + + var t = (this.f - this.e) * tan30 / 2.0; + var dtr = pi / 180.0; + + theta1 *= dtr; + theta2 *= dtr; + theta3 *= dtr; + + var y1 = -(t + this.rf * Math.cos(theta1)); + var z1 = -this.rf * Math.sin(theta1); + + var y2 = (t + this.rf * Math.cos(theta2)) * sin30; + var x2 = y2 * tan60; + var z2 = -this.rf * Math.sin(theta2); + + var y3 = (t + this.rf * Math.cos(theta3)) * sin30; + var x3 = -y3 * tan60; + var z3 = -this.rf * Math.sin(theta3); + + var dnm = (y2 - y1) * x3 - (y3 - y1) * x2; + + var w1 = y1 * y1 + z1 * z1; + var w2 = x2 * x2 + y2 * y2 + z2 * z2; + var w3 = x3 * x3 + y3 * y3 + z3 * z3; + + // x = (a1*z + b1)/dnm + var a1 = (z2 - z1) * (y3 - y1) - (z3 - z1) * (y2 - y1); + var b1 = -((w2 - w1) * (y3 - y1) - (w3 - w1) * (y2 - y1)) / 2.0; + + // y = (a2*z + b2)/dnm; + var a2 = -(z2 - z1) * x3 + (z3 - z1) * x2; + var b2 = ((w2 - w1) * x3 - (w3 - w1) * x2) / 2.0; + + // a*z^2 + b*z + c = 0 + var a = a1 * a1 + a2 * a2 + dnm * dnm; + var b = 2.0 * (a1 * b1 + a2 * (b2 - y1 * dnm) - z1 * dnm * dnm); + var c = (b2 - y1 * dnm) * (b2 - y1 * dnm) + b1 * b1 + dnm * dnm * (z1 * z1 - this.re * this.re); + + // discriminant + var d = b * b - 4.0 * a * c; + if (d < 0.0) { + return new Array(1, 0, 0, 0); // non-existing povar. return error,x,y,z + } + + z0 = -0.5 * (b + Math.sqrt(d)) / a; + x0 = (a1 * z0 + b1) / dnm; + y0 = (a2 * z0 + b2) / dnm; + + return new Array(0, x0, y0, z0); +}; + + + + +// Inverse kinematics + +// Helper functions, calculates angle theta1 (for YZ-pane) +Kinematics.prototype.delta_calcAngleYZ = function(x0, y0, z0) { + var y1 = -0.5 * 0.57735 * this.f; // f/2 * tg 30 + y0 -= 0.5 * 0.57735 * this.e; // shift center to edge + // z = a + b*y + var a = (x0 * x0 + y0 * y0 + z0 * z0 + this.rf * this.rf - this.re * this.re - y1 * y1) / (2.0 * z0); + var b = (y1 - y0) / z0; + + // discriminant + var d = -(a + b * y1) * (a + b * y1) + this.rf * (b * b * this.rf + this.rf); + if (d < 0) { + return new Array(1, 0); // non-existing povar. return error, theta + } + + var yj = (y1 - a * b - Math.sqrt(d)) / (b * b + 1); // choosing outer povar + var zj = a + b * yj; + var theta = Math.atan(-zj / (y1 - yj)) * 180.0 / pi + ((yj > y1) ? 180.0 : 0.0); + + return new Array(0, theta); // return error, theta + }; + + +Kinematics.prototype.inverse = function(x0, y0, z0) { + var theta1 = 0; + var theta2 = 0; + var theta3 = 0; + var status = this.delta_calcAngleYZ(x0, y0, z0); + + if (status[0] === 0) { + theta1 = status[1]; + status = this.delta_calcAngleYZ(x0 * cos120 + y0 * sin120, y0 * cos120 - x0 * sin120, z0, theta2); + } + if (status[0] === 0) { + theta2 = status[1]; + status = this.delta_calcAngleYZ(x0 * cos120 - y0 * sin120, y0 * cos120 + x0 * sin120, z0, theta3); + } + theta3 = status[1]; + + return new Array(status[0], theta1, theta2, theta3); +}; + +module.exports.Kinematics = Kinematics; \ No newline at end of file diff --git a/software/src/motion.js b/software/src/motion.js new file mode 100644 index 0000000..1f62398 --- /dev/null +++ b/software/src/motion.js @@ -0,0 +1,90 @@ +/* + +// Usage: +// motion.move(pointA, pointB, numberOfSteps, easingType, timeDeltaInMilliseconds) +// Example +$ node bot.js +>> motion = require('./motion') +>> motion.move([0,0,-140],[-20,20,-140], 20, 'easeInOutCubic', 500) +>> motion.move([-20,60,-165],[-20,-60,-165], 30, 'easeInOutCubic', 20) +>> motion.move([-20,-60,-165],[-20,60,-165], 30, 'easeInOutCubic', 20) + + +Usage: +> motion = require('./motion') +> motion.directionVector([0,0,-100], [5,10,-150]) +[ 5, 10, -50 ] + +> var A = [1,6,3]; +> var B = [8,2,7]; +> motion.directionVector(A, B); +[ 7, -4, 4 ] + + +Reference: +Line between two points in 3D space: http://mathcentral.uregina.ca/QQ/database/QQ.09.01/murray2.html +Easing functions: https://gist.github.com/gre/1650294 + +*/ +var EasingFunctions = { + linear: function (t) { return t }, + easeInQuad: function (t) { return t*t }, + easeOutQuad: function (t) { return t*(2-t) }, + easeInOutQuad: function (t) { return t<.5 ? 2*t*t : -1+(4-2*t)*t }, + easeInCubic: function (t) { return t*t*t }, + easeOutCubic: function (t) { return (--t)*t*t+1 }, + easeInOutCubic: function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 }, + easeInQuart: function (t) { return t*t*t*t }, + easeOutQuart: function (t) { return 1-(--t)*t*t*t }, + easeInOutQuart: function (t) { return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t }, + easeInQuint: function (t) { return t*t*t*t*t }, + easeOutQuint: function (t) { return 1+(--t)*t*t*t*t }, + easeInOutQuint: function (t) { return t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t } +}; + +(function(exports) { + var directionVector = function(pointA, pointB) { + var vector = [ pointB[0] - pointA[0], + pointB[1] - pointA[1], + pointB[2] - pointA[2] ]; + return vector; + } + + // (x,y,z) = (1,6,3) + t(7,-4,4) = (1 + 7t, 6 - 4t, 3 + 4t). + var parametricEquation = function(pointA, pointB) { + var dv = directionVector(pointA, pointB); + var equation = function(t) { + return [ pointA[0] + dv[0]*t, + pointA[1] + dv[1]*t, + pointA[2] + dv[2]*t ]; + } + return equation; + } + + // get an array of points between (and including) two end points + // numberOfSteps and easingType are required + var getPoints = function(pointA, pointB, numberOfSteps, easingType) { + var points = []; + var point = parametricEquation(pointA, pointB); + var easingFunction = EasingFunctions[easingType]; + + for (var i = 0; i <= numberOfSteps; i++) { + t = easingFunction(i/numberOfSteps); + points.push(point(t)) + } + return points; + } + + var move = function(pointA, pointB, numberOfSteps, easingType, timeDelta) { + var points = getPoints(pointA, pointB, numberOfSteps, easingType); + for (var i=0; i + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + +