// LIVEPRINTER - a livecoding system for live CNC manufacturing
//-------------------------------------------------------------
/**
* Basic properties, settings and functions for the physical printer like speeds, dimensions, extrusion.
* Uses a function passed in to send messages (strings of G Code), usually a websockets one.
* @version 0.8
* @example <caption>Log GCode to console:</caption>
* let printer = new Printer(msg => console.log(msg));
* @license
* Copyright 2018 Evan Raskob
* Licensed under the GNU Affero 3.0 License (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* https://www.gnu.org/licenses/gpl-3.0.en.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
class Printer {
///////
// Printer API /////////////////
///////
// FUTURE NOTE: make this not a class but use object inheritance and prototyping
/**
* Create new instance, passing a function for sending messages
* @constructor
* @param {Function} _messageSendFunc function to pass in that will send messages to the server/physical printer
*/
constructor(_messageSendFunc = null) {
/**
* the function (Websockets or other) that this object will use to send gcode to the printer
* @type {Function}
*/
this.send = _messageSendFunc;
if (this.send === null) {
this.send = msg => console.log(msg);
}
// TODO: not sure about this being valid - maybe check for max speed?
this._printSpeed = Printer.defaultPrintSpeed;
this.travelSpeed = Printer.maxTravelSpeed[this._model];
this._model = Printer.UM2plus; // default
this.layerHeight = 0.2; // thickness of a 3d printed extrudion, mm by default
this.minPosition = new Vector({
x: 0, // x position in mm
y: 0,// y position in mm
z: 0, // z position in mm
e: -99999
});
this.maxPosition = new Vector({
x: Printer.bedSize[this.model]["x"], // x position in mm
y: Printer.bedSize[this.model]["y"], // y position in mm
z: Printer.bedSize[this.model]["z"], // z position in mm
e: 999999
});
this.position = new Vector({
x: this.minPosition.axes.x, // x position in mm
y: this.minPosition.axes.y, // y position in mm
z: this.minPosition.axes.z, // z position in mm
e: 0
});
this.lastSpeed = -1.0;
////////////////////////////////////////////
// these are used in in the go() function
this._heading = 0; // current angle of movement (xy) in radians
this._elevation = 0; // current angle of elevated movement (z) in radians
this._distance = 0; // next distance to move
this._waitTime = 0;
////////////////////////////////////////////
this.totalMoveTime = 0; // time spent moving/extruding
this.maxFilamentPerOperation = 30; // safety check to keep from using all filament, in mm
this.maxTimePerOperation = 10; // prevent very long operations, by accident - this is in seconds
// NOTE: disabled for now to use hardware retraction settings
this.currentRetraction = 0; // length currently retracted
this.retractLength = 6.5; // in mm - amount to retract after extrusion. This is high because most moves are slow...
this.retractSpeed = 1000; //mm/min
this.firmwareRetract = true; // use Marlin or printer for retraction
this.extraUnretract = 1; // extra amount to unretract each time (recovery filament) in mm
this.unretractZHop = 2; //little z-direction hop on retracting to avoid blobs, in mm
/**
* What to do when movement or extrusion commands are out of machine bounds.
* Can be clip (keep printing inside edges), bounce (bounce off edges), stop
*/
this.boundaryMode = "stop";
this.maxMovePerCycle = 200; // max mm to move per calculation (see _extrude method)
this.queuedMessages = []; // messages queued to be sent by this.send(...)
}
get x() { return this.position.axes.x; }
get y() { return this.position.axes.y; }
get z() { return this.position.axes.z; }
get e() { return this.position.axes.e; }
set x(val) { this.position.axes.x = val; }
set y(val) { this.position.axes.y = val; }
set z(val) { this.position.axes.z = val; }
set e(val) { this.position.axes.e = val; }
/**
* readonly total movetime
*/
get time() { return this.totalMoveTime; }
//
// set printer model - should be one definined in this class!
//
set model(m) {
// TODO: check valid model
this._model = m;
// if invalid, throw exception
}
get model() { return this._model; }
set printSpeed(s) {
let maxs = Printer.maxPrintSpeed[this._model];
this._printSpeed = Math.min(parseFloat(s), parseFloat(maxs.x)); // pick in x direction...
}
get maxSpeed() { return Printer.maxPrintSpeed[this._model]; } // in mm/s
get printSpeed() { return this._printSpeed; }
get extents() {
return this.maxPosition.axes;
}
/**
* Get the center horizontal (x) position on the bed
*/
get cx() {
return (this.maxPosition.axes.x - this.minPosition.axes.x) / 2;
}
/**
* Get the center vertical (y) position on the bed,
*/
get cy() {
return (this.maxPosition.axes.y - this.minPosition.axes.y) / 2;
}
/// maximum values
get minx() {
return this.minPosition.axes.x;
}
get miny() {
return this.minPosition.axes.y;
}
get minz() {
return this.minPosition.axes.z;
}
/// maximum values
set minx(v) {
this.minPosition.axes.x = v;
}
set miny(v) {
this.minPosition.axes.y = v;
}
set minz(v) {
this.minPosition.axes.z = v;
}
// maximum values
get maxx() {
return this.maxPosition.axes.x;
}
get maxy() {
return this.maxPosition.axes.y;
}
get maxz() {
return this.maxPosition.axes.z;
}
set maxx(v) {
this.maxPosition.axes.x = v;
}
set maxy(v) {
this.maxPosition.axes.y = v;
}
set maxz(v) {
this.maxPosition.axes.z = v;
}
/**
* Set the extrusion thickness (in mm)
* @param {float} val thickness of the extruded line in mm
* @returns {Printer} reference to this object for chaining
*/
thick(val) {
this.thickness = val;
return this;
}
/**
* Set the overall speed of the extrusion in mm/s
* @param {float} val Speed for the extrusion in mm/s
* @returns {Printer} reference to this object for chaining
*/
speed(val) {
this.printSpeed = val;
return this;
}
/**
* Send the current retract settings to the printer (useful when updating the retraction settings locally)
* @returns {Printer} reference to this object for chaining
*/
sendFirmwareRetractSettings() {
// update firmware retract settings
this.send("M207 S" + this.retractLength + " F" + this.retractSpeed + " Z" + this.unretractZHop);
//set retract recover
this.send("M208 S" + this.extraUnretract + "F" + this.retractSpeed);
return this;
}
/**
* Immediately perform a "retract" which is a shortcut for just moving the filament back up at a speed. Sets the internal retract variables to those passed in.
* @param {Number} len Length of filament to retract. Set to 0 to use current setting (or leave out)
* @param {Number} speed (optional) Speed of retraction. Will be clipped to max filament feed speed for printer model.
* @returns {Printer} reference to this object for chaining
* @example
* Custom retraction:
* lp.extrude({x:40,y:80, retract:false}).retract(6,30);
* // next move, will unretract that amount too
*
* // or extrude an angle/distance and then force retract
* lp.firmwareRetract = false; // turn off automatic retraction in firmware
* lp.angle(45).dist(50).go(1).retract(6,30);
*
* // retract again with same settings
* lp.angle(45).dist(50).go(1).retract();
*/
retract(len = this.retractLength, speed) {
if (len < 0) throw new Error("retract length can't be less than 0: " + len);
this.retractLength = len;
if (speed !== undefined) {
if (speed <= 0) throw new Error("retract speed can't be 0 or less: " + speed);
// set speed safely!
if (speed > Printer.maxPrintSpeed["e"]) throw new Error("retract speed to high: " + speed);
// convert to mm/s
this.retractSpeed = speed * 60;
this.sendFirmwareRetractSettings();
}
// RETRACT
this.currentRetraction += this.retractLength;
this.e -= this.retractLength;
const fixedE = this.e.toFixed(4);
this.send("G1 " + "E" + fixedE + " F" + this.retractSpeed.toFixed(4));
this.e = parseFloat(fixedE); // make sure e is actually e even with rounding errors!
return this;
}
/**
* Immediately perform an "unretract" which is a shortcut for just extruding the filament out at a speed. Sets the internal retract variables to those passed in.
* @param {Number} len Length of filament to unretract. Set to 0 to use current setting (or leave out)
* @param {Number} speed (optional) Speed of unretraction. Will be clipped to max filament feed speed for printer model.
* @returns {Printer} reference to this object for chaining
* @example
* Custom unretraction:
* lp.extrude({x:40,y:80, retract:false}).retract(6,30);
* lp.unretract();
*
* // next move, will unretract that amount too
*
* // or extrude an angle/distance and then force retract
* lp.firmwareRetract = false; // turn off automatic retraction in firmware
* lp.angle(45).dist(50).go(1).retract(6,30);
* lp.unretract(8,30); // extract a little more to get it going
*/
unretract(len = this.currentRetraction, speed) {
if (len < 0) throw new Error("retract length can't be less than 0: " + len);
if (len !== this.currentRetraction) this.retractLength = len; // set new retract length if specified
if (speed !== undefined) {
if (speed <= 0) throw new Error("retract speed can't be 0 or less: " + speed);
// set speed safely!
if (speed > Printer.maxPrintSpeed["e"]) throw new Error("retract speed to high: " + speed);
// convert to mm/s
this.retractSpeed = speed * 60;
this.sendFirmwareRetractSettings();
}
// UNRETRACT
this.e += len;
const fixedE = this.e.toFixed(4);
this.send("G1 " + "E" + fixedE + " F" + this.retractSpeed.toFixed(4));
this.e = parseFloat(fixedE); // make sure e is actually e even with rounding errors!
this.currentRetraction = 0;
return this;
}
/**
* Performs a quick startup by resetting the axes and moving the head
* to printing position (layerheight).
* @param {float} temp is the temperature to start warming up to
* @returns {Printer} reference to this object for chaining
*/
start(temp = "190") {
this.send("G28");
this.send("M104 S" + temp);
this.sendFirmwareRetractSettings();
this.moveto({ x: this.cx, y: this.cy, z: this.layerHeight, speed: Printer.defaultPrintSpeed });
this.send("M106 S100"); // set fan to full
return this;
}
/**
* Set temperature, don't block other operation.
* to printing position (layerheight).
* @param {float} temp is the temperature to start warming up to
* @returns {Printer} reference to this object for chaining
*/
temp(temp = "190") {
this.send("M104 S" + temp);
return this;
}
/**
* Set fan speed.
* @param {float} speed is the speed from 0-100
* @returns {Printer} reference to this object for chaining
*/
fan(speed = "100") {
this.send("M106 S" + speed);
return this;
}
/**
* clip object's x,y,z properties to printer bounds and return it
* @param {object} position: object with x,y,z properties clip
* @returns {object} position clipped object
*/
clipToPrinterBounds(position) {
position.x = Math.min(position.x, this.maxx);
position.y = Math.min(position.y, this.maxy);
position.z = Math.min(position.z, this.maxz);
// stop at min edges
position.x = Math.max(position.x, this.minx);
position.y = Math.max(position.y, this.miny);
position.z = Math.max(position.z, this.minz);
return position;
}
/**
* Perform current operations (extrusion) based on direction/elevation/distance.
* @param {Boolean} extruding Whether to extrude whilst moving (true if yes, false if not)
* @param {Boolean} retract Whether to retract at end (usually true). Set to 0 if executing a few moves in a row
* @returns {Printer} reference to this object for chaining
*/
go(extruding = false, retract = true) {
// wait, if necessary
if (this._waitTime > 0) {
return this.wait();
}
else {
const _x = this._distance * Math.cos(this._heading);
const _y = this._distance * Math.sin(this._heading);
const _z = this._distance * Math.sin(this._elevation);
const _e = extruding ? undefined : 0; // no filament extrusion
// debugging
let _div = Math.sqrt(_x * _x + _y * _y);
let _normx = _x / _div;
let _normy = _y / _div;
/* for debugging -- test if start and end are same
console.log("[go] end position:" + (this.x + _x) + "," + (this.y + _y) + "," + (this.z + _z) + "," + _e);
console.log("[go] move vec:" + _normx + ", " + _normy);
*/
// reset distance to 0 because we've traveled
this._distance = 0;
return this.extrude({ x: _x, y: _y, z: _z, e: _e, 'retract': (retract && extruding) }); // don't retract if not extruding!
}
// never reached
return this;
}
/**
* Set layer height safely and easily
*
* @param {float} height layer height in mm
* @returns {Printer} Reference to this object for chaining
*/
lh(height) {
this.layerHeight = Math.max(Printer.MinLayerHeight, height);
return this;
}
/**
* Set the direction of movement for the next operation.
* @param {float} ang Angle of movement (in xy plane)
* @param {Boolean} radians use radians or not
* @returns {Printer} Reference to this object for chaining
*/
angle(ang, radians = false) {
let a = ang;
if (!radians) {
a = this.d2r(ang);
}
this._heading = a;
return this;
}
/**
* Run a set of commands specified in a grammar (experimental.)
* @param {String} strings commands to run - M(move),E(extrude),L(left turn),R(right turn)
* @returns {Printer} Reference to this object for chaining
*/
run(strings) {
const mvChar = "M";
const exChar = "E";
const ltChar = "L";
const rtChar = "R";
// Match whole command
const cmdRegExp = /([a-zA-Z][0-9]+\.?[0-9]*)/gim;
const subCmdRegExp = /([a-zA-Z])([0-9]+\.?[0-9]*)/;
//console.log(strings.raw);
//console.log(strings.raw[0]);
for (let rawstring of strings.raw) {
//console.log("strings: " + rawstring);
let found = rawstring.match(cmdRegExp);
//console.log(found);
for (let cmd of found) {
//console.log(cmd);
let matches = cmd.match(subCmdRegExp);
if (matches.length !== 3) throw new Error("Error in command string: " + found);
let cmdChar = matches[1];
let value = parseFloat(matches[2]);
//console.log(matches);
switch (cmdChar) {
case mvChar: this.distance(value).go();
break;
case exChar: this.distance(value).go(1);
break;
case ltChar: this.turn(value);
break;
case rtChar: this.turn(-value);
break;
default:
throw new Error("Error in command - unknown command char: " + cmdChar);
}
}
}
return this;
}
/**
* Move up quickly! (in mm)
* @param {Number} d distance in mm to move up
* @returns {Printer} Reference to this object for chaining
*/
up(d) {
return this.move({ 'z': d, 'speed': this.travelSpeed });
}
/**
* Set the direction of movement for the next operation.
* TODO: This doesn't work with other commands. Need to implement roll, pitch, yaw?
* @param {float} angle elevation angle (in z direction, in degrees) for next movement
* @param {Boolean} radians use radians or not
* @returns {Printer} reference to this object for chaining
*/
elevation(angle, radians = false) {
if (!radians) {
a = this.d2r(angle);
}
this._elevation = a;
return this;
}
/**
* Shortcut for elevation.
* @see elevation
* @param {any} _elev elevation
* @returns {Printer} reference to this object for chaining
*/
elev(_elev) {
return this.elevation(_elev);
}
/**
* Set the distance of movement for the next operation.
* @param {float} d distance to move next time
* @returns {Printer} reference to this object for chaining
*/
distance(d) {
this._distance = d;
return this;
}
/**
* Shortcut to distance()
* @param {float} d distance to move next time
* @returns {Printer} reference to this object for chaining
*/
dist(d) {
return this.distance(d);
}
/**
* Set firmware retraction on or off (for after every move).
* @param {Boolean} state True if on, false if off
* @returns {Printer} this printer object for chaining
*/
fwretract(state) {
this.firmwareRetract = state;
return this;
}
/**
* Send all the queued command messages via the send function (probably websockets)
* @returns {Printer} reference to this object for chaining
*/
sendQueued() {
for (let msg of this.queuedMessages) {
this.send(msg);
}
return this;
}
/**
* Extrude a circle starting at the current point on the curve
* @param {any} r radius
* @param {any} segs segments (more means more perfect circle)
*/
circle(r, segs = 10) {
// law of cosines
const r2x2 = r * r * 2;
const segAngle = Math.PI * 2 / segs;
const arc = Math.sqrt(r2x2 - r2x2 * Math.cos(segAngle));
this.turn(Math.PI / 2);
// we're in the middle of segment
this.turn(-segAngle / 2);
for (let i = 0; i < segs; i++) {
this.turn(segAngle);
// print without retraction
this.dist(arc).go(1, false);
}
}
/**
* Extrude a rectangle with the current point as its centre
* @param {any} w width
* @param {any} h height
* @returns {Printer} reference to this object for chaining
*/
rect(w, h) {
for (let i = 0; i < 2; i++) {
this.dist(w).go(1, false);
this.turn(90);
this.dist(h).go(1, false);
this.turn(90);
}
return this;
}
/**
* Extrude plastic from the printer head to specific coordinates, within printer bounds
* @param {Object} params Parameters dictionary containing either x,y,z keys or direction/angle (radians) keys and retract setting (true/false).
* Optional bounce (Boolean) key if movement should bounce off sides.
* @returns {Printer} reference to this object for chaining
*/
extrudeto(params) {
let extrusionSpecified = (params.e !== undefined);
let retract = (params.retract === undefined) ? !extrusionSpecified : params.retract; // don't retract if given e value alone, no matter what
let __x = (params.x !== undefined) ? parseFloat(params.x) : this.x;
let __y = (params.y !== undefined) ? parseFloat(params.y) : this.y;
let __z = (params.z !== undefined) ? parseFloat(params.z) : this.z;
let __e = (extrusionSpecified) ? parseFloat(params.e) : this.e;
let newPosition = new Vector({ x: __x, y: __y, z: __z, e: __e });
let _speed = parseFloat((params.speed !== undefined) ? params.speed : this.printSpeed);
this.layerHeight = parseFloat((params.thickness !== undefined) ? params.thickness : this.layerHeight);
//////////////////////////////////////
/// START CALCULATIONS //////////
//////////////////////////////////////
let distanceVec = Vector.sub(newPosition, this.position);
let distanceMag = 1; // calculated later
// FYI:
// nozzle_speed{mm/s} = (radius_filament^2) * PI * filament_speed{mm/s} / layer_height^2
// filament_speed{mm/s} = layer_height^2 * nozzle_speed{mm/s}/(radius_filament^2)*PI
if (!extrusionSpecified) {
// distance is purely 3D movement, not filament movement
distanceMag = Math.sqrt(distanceVec.axes.x * distanceVec.axes.x + distanceVec.axes.y * distanceVec.axes.y + distanceVec.axes.z * distanceVec.axes.z);
// otherwise, calculate filament length needed based on layerheight, etc.
const filamentRadius = Printer.filamentDiameter[this.model] / 2;
// for extrusion into free space
// apparently, some printers take the filament into account (so this is in mm3)
// this was helpful: https://github.com/Ultimaker/GCodeGenJS/blob/master/js/gcode.js
const filamentLength = distanceMag * this.layerHeight * this.layerHeight;//(Math.PI*filamentRadius*filamentRadius);
//
// safety check:
//
if (filamentLength > this.maxFilamentPerOperation) {
throw Error("Too much filament in move:" + filamentLength);
}
if (!Printer.extrusionInmm3[this.model]) {
filamentLength /= (filamentRadius * filamentRadius * Math.PI);
}
//console.log("filament speed: " + filamentSpeed);
//console.log("filament distance : " + filamentLength + "/" + dist);
distanceVec.axes.e = filamentLength;
newPosition.axes.e = this.e + distanceVec.axes.e;
}
else {
// distance is 3D movement PLUS filament movement
distanceMag = distanceVec.mag();
}
// note: velocity in 'e' direction is always layerHeight^2
const velocity = Vector.div(distanceVec, distanceMag);
const moveTime = distanceMag / _speed; // in sec, doesn't matter that new 'e' not taken into account because it's not in firmware
this.totalMoveTime += moveTime; // update total movement time for the printer
//this._elevation = Math.asin(velocity.axes.z); // removed because it was non-intuitive
//console.log("time: " + moveTime + " / dist:" + distanceMag);
//
// BREAK AT LARGE MOVES
//
if (moveTime > this.maxTimePerOperation) {
throw Error("move time too long:" + moveTime);
}
const nozzleSpeed = Vector.div(distanceVec, moveTime);
//
// safety checks
//
if (nozzleSpeed.axes.x > this.maxSpeed["x"]) {
throw Error("X travel too fast:" + nozzleSpeed.axes.x);
}
if (nozzleSpeed.axes.y > this.maxSpeed["y"]) {
throw Error("Y travel too fast:" + nozzleSpeed.axes.y);
}
if (nozzleSpeed.axes.z > this.maxSpeed["z"]) {
throw Error("Z travel too fast:" + nozzleSpeed.axes.z);
}
if (nozzleSpeed.axes.e > this.maxSpeed["e"]) {
throw Error("E travel too fast:" + nozzleSpeed.axes.z);
}
// Handle movements outside printer boundaries if there's a need.
// Tail recursive.
//
this._extrude(_speed, velocity, distanceMag, retract);
return this;
} // end extrudeto
/**
* Send movement update GCode to printer based on current position (this.x,y,z).
* @param {Int} speed print speed in mm/s
* @param {boolean} retract if true (default) add GCode for retraction/unretraction. Will use either hardware or software retraction if set in Printer object
* */
sendExtrusionGCode(speed, retract = true) {
if (retract && this.currentRetraction > 0) {
//unretract manually first if needed
if (!this.firmwareRetract) {
this.e += this.currentRetraction;
// account for previous retraction
this.send("G1 " + "E" + this.e.toFixed(4) + " F" + this.retractSpeed.toFixed(4));
this.currentRetraction = 0;
} else
// unretract via firmware otherwise
this.send("G11");
this.currentRetraction = 0;
}
// G1 - Coordinated Movement X Y Z E
let moveCode = ["G1"];
moveCode.push("X" + this.x.toFixed(4));
moveCode.push("Y" + this.y.toFixed(4));
moveCode.push("Z" + this.z.toFixed(4));
moveCode.push("E" + this.e.toFixed(4));
moveCode.push("F" + (speed * 60).toFixed(4)); // mm/s to mm/min
this.send(moveCode.join(" "));
// RETRACT
if (retract && this.retractLength > 0) {
if (this.firmwareRetract) {
this.send("G10");
this.currentRetraction = this.retractLength; // this is handled in hardware
} else {
this.currentRetraction = this.retractLength;
this.e -= this.currentRetraction;
this.send("G1 " + "E" + this.e.toFixed(4) + " F" + this.retractSpeed.toFixed(4));
}
}
} // end sendExtrusionGCode
// TODO: have this chop up moves and call a callback function each time,
// like in _extrude
//
// call movement callback function with this lp object
// if(that.moveCallback)
// that.moveCallback(that);
/**
* Extrude plastic from the printer head, relative to the current print head position, within printer bounds
* @param {Object} params Parameters dictionary containing either x,y,z keys or direction/angle (radians) keys and retract setting (true/false).
* @returns {Printer} reference to this object for chaining
*/
extrude(params) {
// first, handle distance/angle mode
if (params.dist !== undefined) {
params.dist = parseFloat(params.dist);
if (params.angle === undefined) {
params.angle = this._heading; // use current heading angle
}
else {
params.angle = parseFloat(params.angle);
}
params.x = params.dist * Math.cos(params.angle);
params.y = params.dist * Math.sin(params.angle);
if (params.elev === undefined) {
params.elev = this.elevation; // use current elevation angle
}
params.z = params.dist * Math.sin(parseFloat(params.elev));
params.e = (params.e !== undefined) ? parseFloat(params.e) + this.e : undefined;
}
//otherwise, handle cartesian coordinates mode
else {
params.x = (params.x !== undefined) ? parseFloat(params.x) + this.x : this.x;
params.y = (params.y !== undefined) ? parseFloat(params.y) + this.y : this.y;
params.z = (params.z !== undefined) ? parseFloat(params.z) + this.z : this.z;
params.e = (params.e !== undefined) ? parseFloat(params.e) + this.e : undefined;
}
// extrude using absolute cartesian coords
return this.extrudeto(params);
} // end extrude
/**
* Relative movement.
* @param {any} params Can be specified as x,y,z,e or dist (distance), angle (xy plane), elev (z dir). All in mm.
* @returns {Printer} reference to this object for chaining
*/
move(params) {
params.e = 0; // no filament extrusion
params.retract = false;
params.speed = (params.speed === undefined) ? this.travelSpeed : parseFloat(params.speed);
return this.extrude(params);
}
/**
* Absolute movement.
* @param {any} params Can be specified as x,y,z,e. All in mm.
* @returns {Printer} reference to this object for chaining
*/
moveto(params) {
params.e = this.e; // keep filament at current position
params.retract = false;
params.speed = (params.speed === undefined) ? this.travelSpeed : parseFloat(params.speed);
return this.extrudeto(params);
}
/**
* Turn (clockwise positive, CCW negative)
* @param {Number} angle in degrees by default
* @param {Boolean} radians use radians if true
* @returns {Printer} reference to this object for chaining
* @example
* Turn 45 degrees twice (so 90 total) and extrude 40 mm in that direction:
* lp.turn(45).turn(45).distance(40).go(1);
*/
turn(angle, radians = false) {
let a = angle;
if (!radians) {
a = this.d2r(angle);
}
this._heading += a;
return this;
}
/**
* Fill a rectagular area.
* @param {Number} w width
* @param {Number} h height
* @param {Number} gap gap between fills
* @param {Boolean} retract retract when finished
*/
fillDirection(w, h, gap, retract = true) {
if (gap === undefined) gap = 1.5 * this.layerHeight;
this.unretract();
for (let i = 0; i < h / gap; i++) {
let m = (i % 2 === 0) ? -1 : 1;
this.turn(-90 * m);
this.dist(w).go(1, false);
this.turn(90 * m); //turn back
this.dist(gap).go(1, false);
}
if (retract !== undefined && retract) lp.retract();
}
/**
* Degrees to radians conversion.
* @param {float} angle in degrees
* @returns {float} angle in radians
*/
d2r(angle) {
return Math.PI * angle / 180;
}
/**
* Radians to degrees conversion.
* @param {float} angle in radians
* @returns {float} angle in degrees
*/
r2d(angle) {
return angle * 180 / Math.PI;
}
/**
* Convert MIDI notes and duration into direction and angle for future movement.
* Low notes below 10 are treated a pauses.
* @param {float} note as midi note
* @param {float} time in ms
* @param {string} axes move direction as x,y,z (default "x")
* @returns {Printer} reference to this object for chaining
* @example
* Play MIDI note 41 for 400ms on the x & y axes
* lp.note(41, 400, "xy").go();
*/
note(note = 40, time = 200, axes = "x") {
const a = [];
a.push(...axes); // turn into array of axes
// total movement
let totalSpeed = 0;
let yangle = 0, xangle = 0, zangle = 0;
for (const axis of a) {
// low notes below 10 are treated as pauses
if (note < 10) {
// set the next movement as a wait
this._waitTime = time;
break;
}
else {
let _speed = this.midi2speed(note, axis); // mm/s
totalSpeed += _speed * _speed;
if (axis === "x") {
if (this._heading < Math.PI / 2 && this._heading > -Math.PI / 2) xangle = -90;
else xangle = 90;
} else if (axis === "y") {
if (this._heading > 0 && this._heading < Math.PI) yangle = 90;
else yangle = -90;
}
else if (axis === "z") {
if (this._elevation > 0) zangle = 90;
else zangle = -90;
}
}
}
// combine all separate distances and speeds into one
this._heading = Math.atan2(yangle, xangle);
this._elevation = zangle;
this.printSpeed = Math.sqrt(totalSpeed);
this._distance = this.printSpeed * time / 1000; // time in ms
return this;
}
/**
* Fills an area based on layerHeight (as thickness of each line)
* @param {float} w width of the area in mm
* @param {float} h height of the area in mm
* @param {float} lh the layerheight (or gap, if larger)
* @returns {Printer} reference to this object for chaining
*/
fill(w, h, lh = this.layerHeight) {
let inc = lh * Math.PI; // not totally sure why this works, but experimentally it does
for (var i = 0, y = 0; y < h; i++ , y += inc) {
let m = (i % 2 === 0) ? 1 : -1;
this.move({ y: inc });
this.extrude({ x: m * w });
}
return this;
}
/**
* @param {number} note as midi note
* @param {string} axis of movement: x,y,z
* @returns {float} speed in mm/s
*/
midi2speed(note, axis) {
// MIDI note 69 = A4(440Hz)
// 2 to the power (69-69) / 12 * 440 = A4 440Hz
// 2 to the power (64-69) / 12 * 440 = E4 329.627Hz
// Ultimaker:
// 47.069852, 47.069852, 160.0,
//freq_xyz[j] = Math.pow(2.0, (note-69)/12.0)*440.0
let freq = Math.pow(2.0, (note - 69) / 12.0) * 440.0;
let speed = freq / parseFloat(this.speedScale()[axis]);
return speed;
}
/**
* Calculate the tool speed in mm/s based on midi note
* @param {float} note midi note
* @param {string} axis axis (x,y,z,e) of movement
* @returns {float} tool speed in mm/s
*/
m2s(note, axis) {
return this.midi2speed(note, axis);
}
/**
* Convenience function for getting speed scales for midi notes from printer model.
* @returns {object} x,y,z speed scales
*/
speedScale() {
let bs = Printer.speedScale[this.model];
return { "x": bs["x"], "y": bs["y"], "z": bs["z"] };
}
/**
* Causes the printer to wait for a number of milliseconds
* @param {float} ms to wait
* @returns {Printer} reference to this object for chaining
*/
wait(ms = this._waitTime) {
this.send("G4 P" + ms);
this._waitTime = 0;
return this;
}
/**
* Temporarily pause the printer: move the head up, turn off fan & temp
* @returns {Printer} reference to this object for chaining
*/
pause() {
// retract filament, turn off fan and heater wait
this.extrude({ e: -16, speed: 250 });
this.move({ z: -3 });
this.send("M104 S0"); // turn off temp
this.send("M107 S0"); // turn off fan
return this;
}
/**
* Resume the printer printing: turn on fan & temp
* @param {float} temp target temp
* @returns {Printer} reference to this object for chaining
*/
resume(temp = "190") {
this.send("M109 S" + temp); // turn on temp, but wait until full temp reached
this.send("M106 S100"); // turn on fan
this.extrude({ e: 16, speed: 250 });
return this;
}
/**
* Print paths
* @param {Array} paths List of paths (lists of coordinates in x,y) to print
* @param {Object} settings Settings for the scaling, etc. of this object. useaspect means respect aspect ratio (width/height). A width or height
* of 0 means to use the original paths' width/height.
* @returns {Printer} reference to this object for chaining
* @test const p = [
[[20,20],
[30,30],
[50,30]]
];
lp.printPaths({paths:p,minZ:0.2,passes:10});
*/
printPaths({ paths = [[]], minY = 0, minX = 0, minZ = 0, width = 0, height = 0, useaspect = true, passes = 1, safeZ = 0 }) {
safeZ = safeZ || (this.layerHeight * passes + 10); // safe z for traveling
// total bounds
let boundsMinX = Infinity,
boundsMinY = Infinity,
boundsMaxX = -Infinity,
boundsMaxY = -Infinity;
let idx = paths.length;
while (idx--) {
let subidx = paths[idx].length;
let bounds = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity, area: 0 };
// find lower and upper bounds
while (subidx--) {
boundsMinX = Math.min(paths[idx][subidx][0], boundsMinX);
boundsMinY = Math.min(paths[idx][subidx][1], boundsMinY);
boundsMaxX = Math.max(paths[idx][subidx][0], boundsMaxX);
boundsMaxY = Math.max(paths[idx][subidx][1], boundsMaxY);
if (paths[idx][subidx][0] < bounds.x) {
bounds.x = paths[idx][subidx][0];
}
if (paths[idx][subidx][1] < bounds.y) {
bounds.y = paths[idx][subidx][0];
}
if (paths[idx][subidx][0] > bounds.x2) {
bounds.x2 = paths[idx][subidx][0];
}
if (paths[idx][subidx][1] > bounds.y2) {
bounds.y2 = paths[idx][subidx][0];
}
}
// calculate area
bounds.area = (1 + bounds.x2 - bounds.x) * (1 + bounds.y2 - bounds.y);
paths[idx].bounds = bounds;
}
// make range mapping functions for scaling - see util.js
const boundsW = boundsMaxX - boundsMinX;
const boundsH = boundsMaxY - boundsMinY;
const useBoth = width && height;
const useOne = width || height;
if (!useBoth) {
if (useOne) {
if (width > 0) {
const ratio = boundsH / boundsW;
height = width * ratio;
} else {
const ratio = boundsW / boundsH;
width = height * ratio;
}
} else {
width = boundsW;
height = boundsH;
}
}
const xmapping = makeMapping([boundsMinX, boundsMaxX], [minX, minX + width]);
const ymapping = makeMapping([boundsMinY, boundsMaxY], [minY, minY + height]);
// print the inside parts first
paths.sort(function (a, b) {
// sort by area
return (a.bounds.area < b.bounds.area) ? -1 : 1;
});
/*
paths.sort(function (a, b) {
// sort by horizontal position
return (a.bounds.x < b.bounds.x) ? -1 : 1;
});
*/
for (let pathIdx = 0, pathLength = paths.length; pathIdx < pathLength; pathIdx++) {
const path = paths[pathIdx];
for (let i = 1; i <= passes; i++) {
const currentHeight = i * this.layerHeight + minZ;
this.moveto({ 'x': xmapping(path[0][0]), 'y': ymapping(path[0][1]) });
this.moveto({ 'z': currentHeight });
this.unretract(); // makes sense to do this every time
// print each segment, one by one
for (let segmentIdx = 0, segmentLength = path.length; segmentIdx < segmentLength; segmentIdx++) {
const segment = path[segmentIdx];
this.extrudeto({
'x': xmapping(segment[0]),
'y': ymapping(segment[1]),
'retract': false
});
}
if (i < passes) {
paths[pathIdx].reverse(); //save time, do it backwards
}
else {
// path finished, retract and raise up head
this.retract();
this.moveto({ 'z': safeZ });
}
}
}
return this;
}
// end Printer class
}
// defined outside class because we have to
/**
* Tail-recursive extrusion function. Don't call this directly. Uses {@link https://github.com/glathoud/fext fext}
* See [extrudeto()]{@link Printer#extrudeto}
* @function
* @param {Vector} moveVector
* @param {Number} leftToMove
* @returns {Boolean} false when done
* @memberof Printer
*/
Printer.prototype._extrude = meth("_extrude", function (that, speed, moveVector, leftToMove, retract) {
// if there's nowhere to move, return
//console.log(that);
//console.log("left to move:" + leftToMove);
//console.log(moveVector);
if (isNaN(leftToMove) || leftToMove < 0.01) {
//console.log("(extrude) end position:" + that.x + ", " + that.y + ", " + that.z + ", " + that.e);
return false;
}
let amountMoved = Math.min(leftToMove, that.maxMovePerCycle);
// calculate next position
let nextPosition = Vector.add(that.position, Vector.mult(moveVector, amountMoved));
//console.log("VECTOR:");
//console.log(moveVector);
//console.log("CURRENT:");
//console.log(that.position);
//console.log("NEXT:");
//console.log(nextPosition);
if (that.boundaryMode === "bounce") {
let moved = new Vector();
let outsideBounds = false;
// calculate movement time per axis, based on printer bounds
for (const axis in nextPosition.axes) {
// TODO:
// for each axis, see where it intersects the printer bounds
// then, using velocity, get other axes positions at that point
// if any of them are over, skip to next axis
if (axis !== "e") {
if (nextPosition.axes[axis] > that.maxPosition.axes[axis]) {
// hit - calculate up to min position
moved.axes[axis] = (that.maxPosition.axes[axis] - that.position.axes[axis]) / moveVector.axes[axis];
outsideBounds = true;
} else if (nextPosition.axes[axis] < that.minPosition.axes[axis]) {
// hit - calculate up to min position
moved.axes[axis] = (that.minPosition.axes[axis] - that.position.axes[axis]) / moveVector.axes[axis];
outsideBounds = true;
}
} //else {
// moved.axes[axis] = nextPosition.axes[axis] - that.position.axes[axis];
//}
}
//console.log("moved:");
//console.log(moved);
if (outsideBounds) {
//console.log("outside");
let shortestAxisTime = 99999;
let shortestAxes = [];
// find shortest time before an axis was hit
// if it hits two (or more?) at the same time, mark both
for (const axis in moved.axes) {
if (moved.axes[axis] === shortestAxisTime) {
shortestAxes.push(axis);
} else if (moved.axes[axis] < shortestAxisTime) {
shortestAxes = [axis];
shortestAxisTime = moved.axes[axis];
}
}
//console.log("shortest axis:");
//console.log(shortestAxes);
//console.log("shortest axis TIME:");
//console.log(shortestAxisTime);
const amountMovedVec = Vector.mult(moveVector, shortestAxisTime);
amountMoved = amountMovedVec.mag();
//console.log("amt moved:" + amountMoved + " / " + leftToMove);
//console.log("next:");
//console.log(nextPosition);
nextPosition.axes = that.clipToPrinterBounds(Vector.add(that.position, amountMovedVec).axes);
//console.log(nextPosition);
// reverse velocity if axis bounds hit, for shortest axis
for (const axis of shortestAxes) {
moveVector.axes[axis] = moveVector.axes[axis] * -1;
}
}
} else {
that.clipToPrinterBounds(nextPosition.axes);
}
leftToMove -= amountMoved;
// update current position
//console.log("current pos:")
//console.log(that.position);
// DON'T DO THIS ANYMORE... counter-intuitive!
//that._elevation = Math.asin(moveVector.axes.z);
that.position.set(nextPosition);
//console.log("next pos:");
//console.log(nextPosition);
//console.log(that.position);
//console.log(that);
that.sendExtrusionGCode(speed, retract);
// handle cases where velocity is 0 (might be movement up or down)
//console.log("prev heading:" + this._heading);
//console.log("move vec:" + moveVector.axes.x + ", " + moveVector.axes.y);
let _test = moveVector.axes.y * moveVector.axes.y + moveVector.axes.x * moveVector.axes.x;
if (_test > Number.EPSILON) {
//console.log("not not going nowhere __" + that._heading);
let newHeading = Math.atan2(moveVector.axes.y, moveVector.axes.x);
if (!isNaN(newHeading)) that._heading = newHeading;
//console.log("new heading:" + that._heading);
}
// Tail recursive, until target x,y,z is hit
return mret(that._extrude, speed, moveVector, leftToMove, retract);
//return false;
} // end _extrude
);
// TODO: this is dumb. SHould be in another data model class called "printer model"
// supported printers
Printer.UM2 = "UM2";
Printer.UM2plus = "UM2plus";
Printer.UM2plusExt = "UM2plusExt";
Printer.UM3 = "UM3";
Printer.REPRAP = "REP";
Printer.PRINTERS = [Printer.UM2, Printer.UM3, Printer.REPRAP];
// dictionary of first GCODE sent to printer at start
Printer.GCODE_HEADERS = {};
Printer.GCODE_HEADERS[Printer.UM2] = [
";FLAVOR:UltiGCode",
";TIME:1",
";MATERIAL:1"
];
Printer.GCODE_HEADERS[Printer.UM2plus] = [
";FLAVOR:UltiGCode",
";TIME:1",
";MATERIAL:1"
];
Printer.GCODE_HEADERS[Printer.UM3] = [
";START_OF_HEADER",
";HEADER_VERSION:0.1",
";FLAVOR:Griffin",
";GENERATOR.NAME:GCodeGenJS",
";GENERATOR.VERSION:?",
";GENERATOR.BUILD_DATE:2016-11-26",
";TARGET_MACHINE.NAME:Ultimaker Jedi",
";EXTRUDER_TRAIN.0.INITIAL_TEMPERATURE:200",
";EXTRUDER_TRAIN.0.MATERIAL.VOLUME_USED:1",
";EXTRUDER_TRAIN.0.NOZZLE.DIAMETER:0.4",
";BUILD_PLATE.INITIAL_TEMPERATURE:0",
";PRINT.TIME:1",
";PRINT.SIZE.MIN.X:0",
";PRINT.SIZE.MIN.Y:0",
";PRINT.SIZE.MIN.Z:0",
";PRINT.SIZE.MAX.X:215",
";PRINT.SIZE.MAX.Y:215",
";PRINT.SIZE.MAX.Z:200",
";END_OF_HEADER",
"G92 E0"
];
Printer.GCODE_HEADERS[Printer.REPRAP] = [
";RepRap target",
"G28",
"G92 E0"
];
Printer.MinLayerHeight = 0.05; // in mm
Printer.filamentDiameter = {};
Printer.filamentDiameter[Printer.UM2] = Printer.filamentDiameter[Printer.UM2plus] =
Printer.filamentDiameter[Printer.REPRAP] = 2.85;
Printer.extrusionInmm3 = {};
Printer.extrusionInmm3[Printer.UM2] = Printer.extrusionInmm3[Printer.REPRAP] = false;
Printer.extrusionInmm3[Printer.UM2plus] = Printer.extrusionInmm3[Printer.UM3] = true;
// TODO: FIX THESE!
// https://ultimaker.com/en/products/ultimaker-2-plus/specifications
// TODO: check these: there are max speeds for each motor (x,y,z,e)
Printer.maxTravelSpeed = {};
Printer.maxTravelSpeed[Printer.UM3] =
Printer.maxTravelSpeed[Printer.UM2plus] =
Printer.maxTravelSpeed[Printer.UM2] = { 'x': 300, 'y': 300, 'z': 80, 'e': 45 };
Printer.maxTravelSpeed[Printer.REPRAP] = { 'x': 300, 'y': 300, 'z': 80, 'e': 45 };
Printer.maxPrintSpeed = {};
Printer.maxPrintSpeed[Printer.UM2] =
Printer.maxPrintSpeed[Printer.REPRAP] = { 'x': 150, 'y': 150, 'z': 80, 'e': 45 };
Printer.maxPrintSpeed[Printer.UM3] = Printer.maxPrintSpeed[Printer.UM2plus] = { 'x': 150, 'y': 150, 'z': 80, 'e': 45 };
Printer.bedSize = {};
Printer.bedSize[Printer.UM2plus] = Printer.bedSize[Printer.UM2]
= Printer.bedSize[Printer.UM3] = { 'x': 223, 'y': 223, 'z': 205 };
Printer.bedSize[Printer.UM2plusExt] = { 'x': 223, 'y': 223, 'z': 305 };
Printer.bedSize[Printer.REPRAP] = { 'x': 150, 'y': 150, 'z': 80 };
Printer.defaultPrintSpeed = 50; // mm/s
Printer.speedScale = {};
Printer.speedScale[Printer.UM2] = { 'x': 47.069852, 'y': 47.069852, 'z': 160.0 };
Printer.speedScale[Printer.UM2plus] = { 'x': 47.069852, 'y': 47.069852, 'z': 160.0 };
//////////////////////////////////////////////////////////