grid-apps-cmms/js/kiri-print.js

1203 lines
41 KiB
JavaScript
Raw Normal View History

2019-01-21 10:57:11 -05:00
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
2017-04-18 17:09:33 -04:00
"use strict";
let gs_kiri_print = exports;
2017-04-18 17:09:33 -04:00
(function() {
if (!self.kiri) self.kiri = {};
let KIRI = self.kiri,
2017-04-18 17:09:33 -04:00
DRIVERS = KIRI.driver,
LASER = DRIVERS.LASER,
2017-04-18 17:09:33 -04:00
CAM = DRIVERS.CAM,
FDM = DRIVERS.FDM,
BASE = self.base,
UTIL = BASE.util,
DBUG = BASE.debug,
POLY = BASE.polygons,
SQRT = Math.sqrt,
SQR = UTIL.sqr,
2017-04-18 17:09:33 -04:00
PI = Math.PI,
2017-10-28 13:30:08 -04:00
PRO = Print.prototype,
2017-04-18 17:09:33 -04:00
Polygon = BASE.Polygon,
2017-11-11 15:37:59 -05:00
newPoint = BASE.newPoint,
lastPoint = null,
lastEmit = null;
2017-04-18 17:09:33 -04:00
KIRI.newPrint = function(settings, widgets, id) { return new Print(settings, widgets, id) };
/**
* @param {Object} settings
* @param {Widget[]} widgets
* @constructor
*/
function Print(settings, widgets, id) {
this.id = id || new Date().getTime().toString(36);
2017-04-18 17:09:33 -04:00
this.settings = settings;
this.widgets = widgets;
this.group = new THREE.Group();
this.printView = [];
this.movesView = [];
2017-04-18 17:09:33 -04:00
this.time = 0;
this.lines = 0;
this.bytes = 0;
this.output = [];
this.distance = 0;
this.bounds = null;
this.imported = null;
2017-04-18 17:09:33 -04:00
}
2017-11-11 15:37:59 -05:00
PRO.addOutput = addOutput;
2017-10-28 13:30:08 -04:00
PRO.tip2tipEmit = tip2tipEmit;
PRO.extrudePerMM = extrudePerMM;
PRO.constReplace = constReplace;
PRO.poly2polyEmit = poly2polyEmit;
PRO.addPrintPoints = addPrintPoints;
PRO.poly2polyDepthFirstEmit = poly2polyDepthFirstEmit;
2017-04-18 17:09:33 -04:00
2019-09-17 08:36:20 -04:00
PRO.parseSVG = function(code, offset) {
let scope = this,
svg = new DOMParser().parseFromString(code, 'text/xml'),
lines = [...svg.getElementsByTagName('polyline')],
2019-09-17 08:36:20 -04:00
output = scope.output = [],
bounds = scope.bounds = {
max: { x:-Infinity, y:-Infinity, z:-Infinity},
min: { x:Infinity, y:Infinity, z:Infinity}
};
lines.forEach(line => {
let seq = [];
let points = [...line.points];
points.forEach(point => {
if (offset) {
point.x += offset.x;
point.y += offset.y;
}
if (point.x) bounds.min.x = Math.min(bounds.min.x, point.x);
if (point.x) bounds.max.x = Math.max(bounds.max.x, point.x);
if (point.y) bounds.min.y = Math.min(bounds.min.y, point.y);
if (point.y) bounds.max.y = Math.max(bounds.max.y, point.y);
if (point.z) bounds.min.z = Math.min(bounds.min.z, point.z);
if (point.z) bounds.max.z = Math.max(bounds.max.z, point.z);
addOutput(seq, point, seq.length > 0);
});
output.push(seq);
});
scope.imported = code;
2019-09-17 08:36:20 -04:00
scope.lines = lines.length;
scope.bytes = code.length;
2019-09-17 08:36:20 -04:00
};
2017-10-28 13:30:08 -04:00
PRO.parseGCode = function(gcode, offset) {
let lines = gcode
2017-04-18 17:09:33 -04:00
.toUpperCase()
.replace("X", " X")
.replace("Y", " Y")
.replace("Z", " Z")
.replace("E", " E")
.replace("F", " F")
.replace(" ", " ")
.split("\n");
let scope = this,
2017-04-18 17:09:33 -04:00
output = scope.output = [],
bounds = scope.bounds = {
max: { x:-Infinity, y:-Infinity, z:-Infinity},
min: { x:Infinity, y:Infinity, z:Infinity}
},
seq = [],
abs = true,
2017-04-18 17:09:33 -04:00
move = false,
2020-04-09 22:42:40 -04:00
tool = 0,
2017-04-18 17:09:33 -04:00
E0G0 = false,
G0 = function() {
move = true;
if (seq.length > 0) {
output.push(seq);
seq = [];
}
},
LZ = 0.0,
pos = {
X: 0.0,
Y: 0.0,
Z: 0.0,
F: 0.0,
E: 0.0
},
off = {
X: offset ? offset.x || 0 : 0,
Y: offset ? offset.y || 0 : 0,
Z: offset ? offset.z || 0 : 0
},
xoff = {
X: 0,
Y: 0,
Z: 0
2017-04-18 17:09:33 -04:00
};
lines.forEach(function(line) {
line = line.split(";")[0].split(" ");
let cmd = line.shift();
if (cmd.charAt(0) === 'T') {
let ext = scope.settings.device.extruders;
let pos = parseInt(cmd.charAt(1));
if (ext && ext[pos]) {
xoff.X = -ext[pos].extOffsetX;
xoff.Y = -ext[pos].extOffsetY;
}
}
switch (cmd) {
case 'G90':
// absolute positioning
abs = true;
break;
case 'G91':
// relative positioning
abs = false;
break;
2017-04-18 17:09:33 -04:00
case 'G0':
G0();
case 'G1':
line.forEach(function(tok) {
pos[tok.charAt(0)] = parseFloat(tok.substring(1));
});
if (pos.X) bounds.min.x = Math.min(bounds.min.x, pos.X);
if (pos.X) bounds.max.x = Math.max(bounds.max.x, pos.X);
if (pos.Y) bounds.min.y = Math.min(bounds.min.y, pos.Y);
if (pos.Y) bounds.max.y = Math.max(bounds.max.y, pos.Y);
if (pos.Z) bounds.min.z = Math.min(bounds.min.z, pos.Z);
if (pos.Z) bounds.max.z = Math.max(bounds.max.z, pos.Z);
if (pos.E) E0G0 = true;
if (E0G0 && pos.E === 0.0) {
if (LZ != pos.Z) G0();
else move = true;
}
2017-11-11 15:37:59 -05:00
addOutput(
seq,
{
x:pos.X + off.X + xoff.X,
y:pos.Y + off.Y + xoff.Y,
z:pos.Z + off.Z + xoff.Z
},
2017-04-18 17:09:33 -04:00
!move,
2020-04-09 22:42:40 -04:00
pos.F,
tool
2017-11-11 15:37:59 -05:00
);
2017-04-18 17:09:33 -04:00
break;
case 'M6':
break;
}
move = false;
pos.E = 0.0;
LZ = pos.Z;
});
G0();
scope.imported = gcode;
2017-04-18 17:09:33 -04:00
scope.lines = lines.length;
scope.bytes = gcode.length;
};
2017-10-28 13:30:08 -04:00
PRO.setup = function(remote, onupdate, ondone) {
let scope = this,
2017-04-18 17:09:33 -04:00
settings = scope.settings,
mode = settings.mode;
lastPoint = null;
lastEmit = null;
2017-04-18 17:09:33 -04:00
if (remote) {
2017-11-17 13:11:13 -05:00
// executed from kiri.js
2017-04-18 17:09:33 -04:00
KIRI.work.printSetup(settings, function(reply) {
if (reply.done) {
scope.output = reply.output;
ondone();
} else {
onupdate(reply.update, reply.updateStatus)
}
});
2017-11-17 13:11:13 -05:00
2017-04-18 17:09:33 -04:00
} else {
2017-11-17 13:11:13 -05:00
// executed from kiri-worker.js
let driver = KIRI.driver[mode];
2017-04-18 17:09:33 -04:00
if (driver) driver.printSetup(scope, onupdate);
else console.log({missing_print_driver: mode});
ondone();
2017-11-17 13:11:13 -05:00
2017-04-18 17:09:33 -04:00
}
};
2020-04-14 09:26:59 -04:00
// more generic. only used by SLA at the moment
PRO.export = function(remote, online, ondone) {
let driver = KIRI.driver[this.settings.mode];
if (!(driver && driver.printExport)) {
console.log({missing_export_driver: mode});
ondone(null);
return;
}
if (remote) {
// executed from kiri.js
KIRI.work.printExport(this.settings, online, ondone);
} else {
// executed from kiri-worker.js
driver.printExport(this, online, ondone);
}
};
2017-10-28 13:30:08 -04:00
PRO.exportGCode = function(remote, ondone, online) {
let scope = this,
2017-04-18 17:09:33 -04:00
settings = scope.settings,
mode = settings.mode;
if (scope.imported) {
return ondone(scope.imported);
}
2017-04-18 17:09:33 -04:00
if (remote) {
2017-11-17 13:11:13 -05:00
// executed from kiri.js
2017-04-18 17:09:33 -04:00
KIRI.work.printGCode(function(reply) {
scope.lines = reply.lines;
scope.bytes = reply.bytes;
scope.bounds = reply.bounds;
scope.distance = reply.distance;
scope.time = reply.time;
ondone(reply.gcode);
});
return;
2017-11-17 13:11:13 -05:00
2017-04-18 17:09:33 -04:00
} else {
2017-11-17 13:11:13 -05:00
// executed from kiri-worker.js
let driver = KIRI.driver[mode];
2017-04-18 17:09:33 -04:00
if (driver && driver.printExport) {
ondone(driver.printExport(scope, online));
} else {
console.log({missing_export_driver: mode});
ondone(null);
}
2017-11-17 13:11:13 -05:00
2017-04-18 17:09:33 -04:00
}
};
2017-10-28 13:30:08 -04:00
PRO.exportLaserGCode = function() {
2017-04-18 17:09:33 -04:00
return KIRI.driver.LASER.exportGCode(this);
};
2017-12-06 14:09:29 -05:00
PRO.exportSVG = function(cut_color) {
return KIRI.driver.LASER.exportSVG(this, cut_color);
2017-04-18 17:09:33 -04:00
};
2017-10-28 13:30:08 -04:00
PRO.exportDXF = function() {
2017-04-18 17:09:33 -04:00
return KIRI.driver.LASER.exportDXF(this);
};
2017-10-28 13:30:08 -04:00
PRO.encodeOutput = function() {
let newout = [], newlayer;
2017-04-18 17:09:33 -04:00
this.output.forEach(function(layerout) {
newlayer = [];
newout.push(newlayer);
layerout.forEach(function(out) {
if (out.point) {
2018-01-15 22:17:56 -05:00
// used for renderMoves client side. can drop non-essential
// data to speed up worker -> browser transfer. perhaps a
// more compact encoding (arrays, etc)
newlayer.push({
emit: out.emit,
speed: out.speed * 60,
2018-01-15 22:17:56 -05:00
retract: out.retract,
point: {x: out.point.x, y: out.point.y, z: out.point.z}
});
}
2017-04-18 17:09:33 -04:00
});
});
return newout;
};
2017-10-28 13:30:08 -04:00
PRO.render = function() {
let scope = this,
2020-04-13 21:52:05 -04:00
mode = scope.settings.mode,
driver = KIRI.driver[mode];
2017-04-18 17:09:33 -04:00
switch (mode) {
2020-04-13 21:52:05 -04:00
case 'SLA':
driver.printRender(scope);
break;
2017-04-18 17:09:33 -04:00
case 'CAM':
case 'FDM':
2019-04-08 20:03:50 -04:00
scope.renderMoves(true, 0x888888);
2017-04-18 17:09:33 -04:00
break;
case 'LASER':
scope.renderMoves(false, 0x0088aa);
break;
}
};
function pref(a,b) {
return a !== undefined ? a : b;
}
2020-04-13 17:42:40 -04:00
function rgb2hsv(ir, ig, ib) {
let H = 0,
S = 0,
V = 0,
r = ir / 255,
g = ig / 255,
b = ib / 255;
let minRGB = Math.min(r, Math.min(g, b)),
maxRGB = Math.max(r, Math.max(g, b));
// Black-gray-white
if (minRGB == maxRGB) {
V = minRGB;
return [0, 0, V];
}
// Colors other than black-gray-white:
let d = (r == minRGB) ? g - b : ((b == minRGB) ? r - g : b - r),
h = (r == minRGB) ? 3 : ((b == minRGB) ? 1 : 5);
H = 60 * (h - d / (maxRGB - minRGB));
S = (maxRGB - minRGB) / maxRGB;
V = maxRGB;
return [H, S, V];
}
2017-12-13 08:15:37 -05:00
// hsv values all = 0 to 1
function hsv2rgb(hsv) {
2020-04-13 17:42:40 -04:00
let seg = Math.floor(hsv.h * 6),
rem = hsv.h - (seg * (1/6)),
p = hsv.v * (1.0 - (hsv.s)),
q = hsv.v * (1.0 - (hsv.s * rem)),
t = hsv.v * (1.0 - (hsv.s * (1.0 - rem))),
out = {};
2017-12-13 08:15:37 -05:00
switch (seg) {
case 0:
out.r = hsv.v;
out.g = t;
out.b = p;
break;
case 1:
out.r = q;
out.g = hsv.v;
out.b = p;
break;
case 2:
out.r = p;
out.g = hsv.v;
out.b = t;
break;
case 3:
out.r = p;
out.g = q;
out.b = hsv.v;
break;
case 4:
out.r = t;
out.g = p;
out.b = hsv.v;
break;
case 5:
out.r = hsv.v;
out.g = p;
out.b = q;
break;
}
return out;
}
2017-10-28 13:30:08 -04:00
PRO.renderMoves = function(showMoves, moveColor) {
let debug = KIRI.api.const.LOCAL;
let scope = this, last, emits, moves;
2017-04-18 17:09:33 -04:00
// render layered output
scope.lines = 0;
scope.output.forEach(function(layerout) {
let move = [], print = {}, z;
layerout.forEach(function(out, index) {
let point = out.point;
2017-04-18 17:09:33 -04:00
if (last) {
if (UTIL.distSq(last, point) < 0.001 && point.z === last.z) {
2017-04-18 17:09:33 -04:00
return;
}
if (out.emit > 0) {
let spd = out.speed || 4000;
let arr = print[spd] || [];
print[spd] = arr;
arr.push(last);
arr.push(point);
2017-04-18 17:09:33 -04:00
} else {
move.push(last);
move.push(point);
2017-04-18 17:09:33 -04:00
}
if (debug && showMoves && last.z == point.z) {
let rs = BASE.newSlope(
{x: point.x, y: point.y},
{x: last.x, y: last.y}
);
let ao1 = BASE.newSlopeFromAngle(rs.angle + 25);
let ao2 = BASE.newSlopeFromAngle(rs.angle - 25);
let sp = BASE.newPoint(point.x, point.y, point.z);
move.push(sp);
move.push(sp.projectOnSlope(ao1, 0.5));
move.push(sp);
move.push(sp.projectOnSlope(ao2, 0.5));
}
2017-04-18 17:09:33 -04:00
} else {
z = point.z;
2017-04-18 17:09:33 -04:00
}
last = point;
2017-04-18 17:09:33 -04:00
});
emits = KIRI.newLayer(scope.group);
scope.printView.push(emits);
if (showMoves) {
moves = KIRI.newLayer(scope.group);
moves.lines(move, moveColor);
scope.movesView.push(moves);
moves.render();
}
for (let speed in print) {
let sint = Math.min(6000, parseInt(speed));
let rgb = hsv2rgb({h:sint/6000, s:1, v:0.6});
emits.lines(print[speed],
2017-12-13 08:15:37 -05:00
((rgb.r * 0xff) << 16) |
((rgb.g * 0xff) << 8) |
((rgb.b * 0xff) << 0)
);
}
emits.render();
2017-04-18 17:09:33 -04:00
scope.lines += print.length;
});
}
2017-10-28 13:30:08 -04:00
PRO.getLayerCount = function() {
2017-04-18 17:09:33 -04:00
return this.output.length;
}
2017-10-28 13:30:08 -04:00
PRO.hide = function() {
this.printView.forEach(function(layer) {
layer.setVisible(false);
})
this.movesView.forEach(function(layer) {
2017-04-18 17:09:33 -04:00
layer.setVisible(false);
})
};
PRO.showLayer = function(index, show, moves) {
if (this.printView[index]) this.printView[index].setVisible(show);
if (this.movesView[index]) this.movesView[index].setVisible(show && moves);
2017-04-18 17:09:33 -04:00
};
/**
* @constructor
*/
function Output(point, emit, speed, tool) {
this.point = point; // point to emit
this.emit = emit; // emit (feed for printers, power for lasers, cut for cam)
this.speed = speed;
this.tool = tool;
}
/**
2017-11-11 15:37:59 -05:00
* @param {Point[]} array of points
2017-04-18 17:09:33 -04:00
* @param {Point} point
* @param {number} emit (0=move, !0=filament emit/laser on/cut mode)
* @param {number} [speed] speed
* @param {number} [tool] tool # or nozzle #
2017-04-18 17:09:33 -04:00
*/
2017-11-11 15:37:59 -05:00
function addOutput(array, point, emit, speed, tool) {
2017-12-13 08:15:37 -05:00
// drop duplicates (usually intruced by FDM bisections)
if (lastPoint && point) {
// nested due to uglify confusing browser
if (point.x == lastPoint.x && point.y == lastPoint.y && point.z == lastPoint.z && lastEmit == emit) {
return;
}
2017-11-11 15:37:59 -05:00
}
lastPoint = point;
lastEmit = emit;
array.push(new Output(point, emit, speed, tool));
2017-04-18 17:09:33 -04:00
}
2020-04-09 22:42:40 -04:00
// function segmentedOutput(output, p1, p2, s1, s2, steps, mult) {
// let sd = (s2 - s1) / (steps + 1);
// let dd = p1.distTo2D(p2) / steps;
// let dist = dd;
// let spd = s1;
// while (steps-- > 0) {
// spd += sd;
// p1 = p1.offsetPointTo(p2, dd);
// addOutput(output, p1, mult, spd);
// }
// }
2017-04-18 17:09:33 -04:00
/**
2018-03-25 19:21:07 -04:00
* FDM & Laser. add points in polygon to an output array (print path)
2017-04-18 17:09:33 -04:00
*
* @param {Polygon} poly
* @param {Point} startPoint
* @param {Array} output
* @param {number} [extrude] multiplier
* @param {Function} [onfirst] optional fn to call on first point
2017-04-18 17:09:33 -04:00
* @return {Point} last output point
*/
PRO.polyPrintPath = function(poly, startPoint, output, opt) {
2017-04-18 17:09:33 -04:00
poly.setClockwise();
let options = opt || {},
process = this.settings.process,
shortDist = process.outputShortDistance,
shellMult = pref(options.extrude, process.outputShellMult),
printSpeed = options.rate || process.outputFeedrate,
moveSpeed = process.outputSeekrate,
2019-05-04 09:56:21 -04:00
minSpeed = process.outputMinSpeed,
closest = options.simple ? poly.first() : poly.findClosestPointTo(startPoint),
2018-01-11 08:57:46 -05:00
perimeter = poly.perimeter(),
2017-04-18 17:09:33 -04:00
first = true,
close = !options.open,
last = startPoint,
wipeDist = options.wipe || 0,
2020-04-09 22:42:40 -04:00
coastDist = options.coast || 0,
tool = options.tool;
2017-04-18 17:09:33 -04:00
// if short, use calculated print speed based on sliding scale
if (perimeter < process.outputShortPoly) {
2019-05-04 09:56:21 -04:00
printSpeed = minSpeed + (printSpeed - minSpeed) * (perimeter / process.outputShortPoly);
2018-01-11 08:57:46 -05:00
}
poly.forEachPoint(function(point, pos, points, count) {
2017-04-18 17:09:33 -04:00
if (first) {
if (options.onfirst) {
options.onfirst(point);
}
2017-04-18 17:09:33 -04:00
// move from startPoint to point
2020-04-09 22:42:40 -04:00
addOutput(output, point, 0, moveSpeed, tool);
2017-04-18 17:09:33 -04:00
first = false;
} else {
let seglen = last.distTo2D(point);
if (coastDist && shellMult && perimeter - seglen <= coastDist) {
let delta = perimeter - coastDist;
let offset = seglen - delta;
let offPoint = last.offsetPointFrom(point, offset)
2020-04-09 22:42:40 -04:00
addOutput(output, offPoint, shellMult, printSpeed, tool);
shellMult = 0;
}
perimeter -= seglen;
2020-04-09 22:42:40 -04:00
addOutput(output, point, shellMult, printSpeed, tool);
2017-04-18 17:09:33 -04:00
}
last = point;
}, close, closest.index);
2017-04-18 17:09:33 -04:00
return output.last().point;
2017-04-18 17:09:33 -04:00
};
/**
2017-12-13 08:15:37 -05:00
* FDM only. create 3d print output path for this slice
2017-04-18 17:09:33 -04:00
*
* @parma {Slice} slice
* @param {Point} startPoint start as close as possible to startPoint
* @param {THREE.Vector3} offset
* @param {Point[]} output points
2018-01-14 18:43:43 -05:00
* @param {Object} [options] object
2017-04-18 17:09:33 -04:00
* @return {Point} last output point
*/
2018-01-14 18:43:43 -05:00
PRO.slicePrintPath = function(slice, startPoint, offset, output, options) {
2020-04-09 22:42:40 -04:00
// console.log({slicePrintPath: slice.index, ext:slice.extruder});
let i,
2018-01-14 18:43:43 -05:00
opt = options || {},
2017-04-18 17:09:33 -04:00
preout = [],
scope = this,
settings = this.settings,
process = settings.process,
2020-04-09 22:42:40 -04:00
extruder = slice.extruder || 0,
nozzleSize = settings.device.extruders[extruder].extNozzle,
2018-01-14 21:49:45 -05:00
firstLayer = opt.first || false,
2020-04-09 22:42:40 -04:00
minSeek = nozzleSize * (opt.minSeek || 1.5),
thinWall = nozzleSize * (opt.thinWall || 1.75),
2018-01-15 22:17:56 -05:00
retractDist = opt.retractOver || 2,
2018-01-14 18:43:43 -05:00
fillMult = opt.mult || process.outputFillMult,
shellMult = opt.mult || process.outputShellMult || (process.laserSliceHeight >= 0 ? 1 : 0),
sparseMult = process.outputSparseMult,
coastDist = process.outputCoastDist || 0,
2018-01-14 18:43:43 -05:00
finishSpeed = opt.speed || process.outputFinishrate,
2018-01-18 21:53:46 -05:00
firstShellSpeed = process.firstLayerRate,
firstFillSpeed = process.firstLayerFillRate,
firstPrintMult = process.firstLayerPrintMult,
2018-01-18 21:53:46 -05:00
printSpeed = opt.speed || (firstLayer ? firstShellSpeed : process.outputFeedrate),
fillSpeed = opt.speed || opt.fillSpeed || (firstLayer ? firstFillSpeed || firstShellSpeed : process.outputFeedrate),
moveSpeed = process.outputSeekrate,
2017-04-18 17:09:33 -04:00
origin = startPoint.add(offset),
2018-02-28 10:10:57 -05:00
zhop = process.zHopDistance || 0,
antiBacklash = process.antiBacklash,
2017-04-18 17:09:33 -04:00
z = slice.z;
// apply first layer extrusion multipliers
if (firstLayer) {
fillMult *= firstPrintMult;
shellMult *= firstPrintMult;
sparseMult *= firstPrintMult;
}
2017-12-09 21:55:25 -05:00
function retract() {
if (preout.length) preout.last().retract = true;
}
2018-01-16 16:01:53 -05:00
function intersectsTop(p1, p2) {
let int = false;
2018-01-30 20:01:34 -05:00
POLY.flatten(slice.gatherTopPolys([])).forEach(function(poly) {
if (!int) poly.forEachSegment(function(s1, s2) {
2018-01-16 16:01:53 -05:00
if (UTIL.intersect(p1,p2,s1,s2,BASE.key.SEGINT)) {
int = true;
return int;
}
});
});
return int;
}
function outputTraces(poly, extrude) {
2017-04-18 17:09:33 -04:00
if (!poly) return;
if (Array.isArray(poly)) {
outputOrderClosest(poly, function(next) {
outputTraces(next, extrude);
2018-01-15 22:17:56 -05:00
}, null);
2017-04-18 17:09:33 -04:00
} else {
let finishShell = poly.depth === 0 && !firstLayer;
startPoint = scope.polyPrintPath(poly, startPoint, preout, {
2020-04-09 22:42:40 -04:00
tool: extruder,
2018-01-14 18:43:43 -05:00
rate: finishShell ? finishSpeed : printSpeed,
accel: finishShell,
wipe: process.outputWipeDistance || 0,
coast: firstLayer ? 0 : coastDist,
extrude: pref(extrude, shellMult),
onfirst: function(firstPoint) {
2019-03-04 18:12:00 -05:00
if (startPoint.distTo2D(firstPoint) > retractDist) {
retract();
}
2017-12-09 21:55:25 -05:00
}
});
2017-04-18 17:09:33 -04:00
}
}
/**
* @param {Polygon[]} polys
*/
2020-03-05 22:26:33 -05:00
function outputSparse(polys, extrude, speed) {
2017-04-18 17:09:33 -04:00
if (!polys) return;
let proxy = polys.map(function(poly) {
return {poly: poly, first: poly.first(), last: poly.last()};
});
let lp = startPoint;
startPoint = tip2tipEmit(proxy, startPoint, function(el, point, count) {
let poly = el.poly;
if (poly.last() === point) {
poly.reverse();
}
poly.forEachPoint(function(p, i) {
2018-01-16 16:01:53 -05:00
// retract if dist trigger and crosses a slice top polygon
2018-01-30 20:01:34 -05:00
if (i === 0 && lp && lp.distTo2D(p) > retractDist && intersectsTop(lp,p)) {
2018-01-16 16:01:53 -05:00
retract();
}
2020-04-09 22:42:40 -04:00
addOutput(preout, p, i === 0 ? 0 : extrude, speed || printSpeed, extruder);
lp = p;
});
return lp;
2017-04-18 17:09:33 -04:00
});
}
function outputFills(lines, options) {
let p, p1, p2, dist, len, found, group, mindist, t1, t2,
2018-02-11 16:18:46 -05:00
marked = 0,
2018-02-11 22:59:15 -05:00
start = 0,
skip = false,
lastIndex = -1,
opt = options || {},
fast = opt.fast || false,
fill = opt.fill >= 0 ? opt.fill : fillMult;
2018-02-11 22:59:15 -05:00
while (lines && marked < lines.length) {
found = false;
group = null;
2017-04-18 17:09:33 -04:00
mindist = Infinity;
2018-02-11 22:59:15 -05:00
2017-12-09 21:55:25 -05:00
// order all points by distance to last point
2018-02-11 22:59:15 -05:00
for (i=start; i<lines.length; i += 2) {
p = lines[i];
if (p.del) continue;
if (group === null && p.index > lastIndex) {
group = p.index;
}
if (group !== null) {
if (p.index !== group) break;
if (p.index % 2 === 0) {
t1 = lines[i];
t2 = lines[i+1];
} else {
t2 = lines[i];
t1 = lines[i+1];
}
dist = Math.min(t1.distTo2D(startPoint), t2.distTo2D(startPoint));
if (dist < mindist) {
p1 = t1;
p2 = t2;
mindist = dist;
}
start = i;
found = true;
}
2018-02-11 16:18:46 -05:00
}
2018-02-11 22:59:15 -05:00
// go back to start and try again
if (!found) {
start = 0;
lastIndex = -1;
2018-02-11 16:18:46 -05:00
continue;
2017-04-18 17:09:33 -04:00
}
2017-12-08 13:27:15 -05:00
2018-02-11 22:59:15 -05:00
dist = startPoint.distTo2D(p1);
len = p1.distTo2D(p2);
// go back to start when dist > retractDist
2018-06-20 13:24:17 -04:00
if (!fast && !skip && dist > retractDist) {
skip = true;
start = 0;
lastIndex = -1;
continue;
}
skip = false;
2018-02-13 14:46:37 -05:00
// mark as used (temporarily)
p1.del = true;
p2.del = true;
marked += 2;
lastIndex = p1.index;
2018-02-11 22:59:15 -05:00
// if dist to new segment is less than thinWall
// and segment length is less than thinWall then
// just extrude to midpoint of next segment. this is
// to avoid shaking the printer to death.
if (dist <= thinWall && len <= thinWall) {
p2 = p1.midPointTo(p2);
2020-04-09 22:42:40 -04:00
addOutput(preout, p2, fill * (dist / thinWall), fillSpeed, extruder);
2018-02-11 22:59:15 -05:00
} else {
// retract if dist trigger or crosses a slice top polygon
if (!fast && dist > retractDist && (zhop || intersectsTop(startPoint, p1))) {
2018-02-11 22:59:15 -05:00
retract();
}
2017-10-29 16:51:32 -04:00
2018-02-11 22:59:15 -05:00
// anti-backlash on longer move
if (!fast && antiBacklash && dist > retractDist) {
2020-04-09 22:42:40 -04:00
addOutput(preout, p1.add({x:antiBacklash,y:-antiBacklash,z:0}), 0, moveSpeed, extruder);
2018-02-11 22:59:15 -05:00
}
// bridge ends of fill when they're close together
if (dist < thinWall) {
2020-04-09 22:42:40 -04:00
addOutput(preout, p1, fill, fillSpeed, extruder);
2018-02-11 22:59:15 -05:00
} else {
2020-04-09 22:42:40 -04:00
addOutput(preout, p1, 0, moveSpeed, extruder);
2018-02-11 22:59:15 -05:00
}
2020-04-09 22:42:40 -04:00
addOutput(preout, p2, fill, fillSpeed, extruder);
2017-04-18 17:09:33 -04:00
}
2018-02-11 22:59:15 -05:00
startPoint = p2;
2017-04-18 17:09:33 -04:00
}
2017-04-18 17:09:33 -04:00
// clear delete marks so we can re-print later
if (lines) lines.forEach(function(p) { p.del = false });
}
/**
* given array of polygons, emit them in next closest order with
* the special exception that depth is considered into distance
* so that inner polygons are emitted first.
*
2017-04-18 17:09:33 -04:00
* @param {Array} array of Polygon or Polygon wrappers
* @param {Function} fn
* @param {Function} fnp convert 'next' object into a Polygon
*/
2017-12-15 09:31:53 -05:00
function outputOrderClosest(array, fn, fnp, newTop) {
2017-12-14 20:47:50 -05:00
if (array.length === 1) {
return fn(array[0]);
2017-12-14 20:47:50 -05:00
}
2017-04-18 17:09:33 -04:00
array = array.slice();
let closest, find, next, order, poly, lastDepth = 0;
2017-04-18 17:09:33 -04:00
for (;;) {
order = [];
2017-04-18 17:09:33 -04:00
closest = null;
for (i=0; i<array.length; i++) {
next = array[i];
if (!next) continue;
poly = fnp ? fnp(next) : next;
find = poly.findClosestPointTo(startPoint);
order.push({
i: i,
n: next,
d: find.distance - (poly.depth * thinWall),
});
2017-04-18 17:09:33 -04:00
}
2017-12-15 09:31:53 -05:00
newTop = false;
if (order.length === 0) {
return;
}
order.sort(function(a,b) {
return a.d - b.d;
});
array[order[0].i] = null;
fn(order[0].n);
2017-04-18 17:09:33 -04:00
}
}
let all = [].appendAll(slice.supports || []).appendAll(slice.tops || []);
let lastTop = null;
2017-04-18 17:09:33 -04:00
outputOrderClosest(all || [], function(next) {
if (next instanceof Polygon) {
// support polygon
next.setZ(z);
2017-11-01 16:33:06 -04:00
outputTraces([next].appendAll(next.inner || []));
2017-04-18 17:09:33 -04:00
if (next.fills) {
next.fills.forEach(function(p) { p.z = z });
outputFills(next.fills, {fast: true});
2017-04-18 17:09:33 -04:00
}
} else {
// top object
let bounds = POLY.flatten(next.gatherOuter([]));
2019-03-04 18:12:00 -05:00
let dir = -1; // 1 == inside out, -1 == outside-in
// output inner polygons
if (dir === 1)
outputTraces([].appendAll(next.innerTraces() || []));
// sort perimeter polygon by length to go out-to-in or in-to-out
2019-03-04 18:12:00 -05:00
(next.traces || []).sort(function(a,b) {
return a.perimeter() > b.perimeter() ? dir : -dir;
2019-03-04 18:12:00 -05:00
}).forEach(function(poly, index) {
outputTraces(poly);
2019-03-04 18:12:00 -05:00
});
// output inner polygons
if (dir === -1)
outputTraces([].appendAll(next.innerTraces() || []));
2019-03-04 18:12:00 -05:00
// then output solid and sparse fill
outputFills(next.fill_lines);
outputSparse(next.fill_sparse, sparseMult);
lastTop = next;
2017-04-18 17:09:33 -04:00
}
}, function(obj) {
return obj instanceof Polygon ? obj : obj.poly;
});
// produce polishing paths when present
2020-03-05 18:36:12 -05:00
if (slice.tops.length && slice.tops[0].polish) {
let {x,y} = slice.tops[0].polish;
if (x) {
2020-03-05 22:26:33 -05:00
outputSparse(x, 0, process.polishSpeed);
2020-03-05 18:36:12 -05:00
}
if (y) {
2020-03-05 22:26:33 -05:00
outputSparse(y, 0, process.polishSpeed);
}
}
2017-04-18 17:09:33 -04:00
// offset print points
for (i=0; i<preout.length; i++) {
preout[i].point = preout[i].point.add(offset);
}
// add offset points to total print
2020-04-09 22:42:40 -04:00
addPrintPoints(preout, output, origin, extruder);
2017-04-18 17:09:33 -04:00
return startPoint.add(offset);
};
/**
*
* @param {Output[]} input
* @param {Point[]} output
* @param {Point} [startPoint]
*/
2020-04-09 22:42:40 -04:00
function addPrintPoints(input, output, startPoint, tool) {
2017-04-18 17:09:33 -04:00
if (startPoint && input.length > 0) {
2020-04-09 22:42:40 -04:00
addOutput(output, startPoint, 0, undefined, tool);
2017-04-18 17:09:33 -04:00
}
output.appendAll(input);
}
/**
* emit each element in an array based on
* the next closest endpoint.
*/
function tip2tipEmit(array, startPoint, emitter) {
let mindist, dist, found, count = 0;
2017-04-18 17:09:33 -04:00
for (;;) {
found = null;
mindist = Infinity;
array.forEach(function(el) {
if (el.delete) return;
dist = startPoint.distTo3D(el.first);
if (dist < mindist) {
found = {el:el, first:el.first, last:el.last};
mindist = dist;
}
dist = startPoint.distTo3D(el.last);
if (dist < mindist) {
found = {el:el, first:el.last, last:el.first};
mindist = dist;
}
});
if (found) {
found.el.delete = true;
2020-03-11 13:24:09 -04:00
// startPoint = found.last;
startPoint = emitter(found.el, found.first, ++count);
2017-04-18 17:09:33 -04:00
} else {
break;
}
}
return startPoint;
}
/**
* like tip2tipEmit but accepts an array of polygons and the next closest
* point can be anywhere in the adjacent polygon. should be re-written
* to be more like outputOrderClosest() and have the option to account for
* depth in determining distance
2017-04-18 17:09:33 -04:00
*/
function poly2polyEmit(array, startPoint, emitter, mark) {
let mindist, dist, found, count = 0, marker = mark || 'delete';
2017-04-18 17:09:33 -04:00
for (;;) {
found = null;
mindist = Infinity;
array.forEach(function(poly) {
if (poly[marker]) {
return;
}
2017-04-18 17:09:33 -04:00
if (poly.isOpen()) {
const d2f = startPoint.distTo2D(poly.first());
const d2l = startPoint.distTo2D(poly.last());
if (d2f > mindist && d2l > mindist) {
return;
}
2017-04-18 17:09:33 -04:00
if (d2l < mindist && d2l < d2f) {
poly.reverse();
2017-04-18 17:09:33 -04:00
found = {poly:poly, index:0, point:poly.first()};
} else if (d2f < mindist) {
found = {poly:poly, index:0, point:poly.first()};
}
return;
}
let area = poly.area();
2017-04-18 17:09:33 -04:00
poly.forEachPoint(function(point, index) {
dist = startPoint.distTo3D(point) * area * area;
2017-04-18 17:09:33 -04:00
if (dist < mindist) {
found = {poly:poly, index:index, point:point};
mindist = dist;
}
});
});
if (found) {
found.poly[marker] = true;
2017-04-18 17:09:33 -04:00
startPoint = emitter(found.poly, found.index, ++count, startPoint) || found.point;
} else {
break;
}
}
// undo delete marks
array.forEach(function(poly) { poly[marker] = false });
2017-04-18 17:09:33 -04:00
return startPoint;
}
/**
* @param {Polygon[][]} array of array of polygons representing each layer (top down)
* @param {Point} startPoint entry point for algorithm
* @param {Function} emitter called to emit each polygon
* @param {number} offset tool diameter used for this depth-first cut
*
* used for CAM depth first layer output
*/
function poly2polyDepthFirstEmit(array, startPoint, emitter, offset) {
let layers = [],
2017-04-18 17:09:33 -04:00
pools;
array.forEach(function(layerPolys, layerIndex) {
pools = [];
layers.push(pools);
// flattening but preserving inner relationships
// allows iterating over all layer polys to determine
// if they deserve their own pool
flattenPolygons(POLY.nest(layerPolys, true, true)).sort(function(p1,p2) {
// sort by area descending
return p2.area() - p1.area();
}).forEach(function (poly) {
// a polygon should be made into a pool if:
// - it is open
// - it has more than one sibling
// - it has no parent (top/outer most)
// - it is offset from its parent by more than diameter
if (poly.isOpen() || !poly.parent || poly.parent.innerCount() > 1 || !polygonWithinOffset(poly, poly.parent, offset)) {
pools.push(poly);
poly.pool = [];
poly.poolsDown = [];
} else {
// otherwise walk up the parent tree to find a pool to join
let search = poly.parent;
2017-04-18 17:09:33 -04:00
// walk up until pool found
while (search && !search.pool) {
search = search.parent;
}
// open polygons can be unparented and without a pool
if (!search) {
console.log({orphan:poly});
return;
}
// add to pool
search.pool.push(poly);
}
});
// sort pools increasing in size to aid fitting from below
pools.sort(function (p1, p2) {
return p1.area() - p2.area();
});
// add add pools to smallest enclosing pool in layer above
const poolsAbove = layers[layerIndex - 1];
if (layerIndex > 0)
pools.forEach(function(pool) {
for (let i=0; i<poolsAbove.length; i++) {
2017-04-18 17:09:33 -04:00
const above = poolsAbove[i];
// can only add open polys to open polys
if (above.isOpen() && pool.isClosed()) {
// console.log({skip_open_above:above});
continue;
}
// if pool fits into smallest above pool, add it and break
if (polygonFitsIn(pool, above, 0.1)) {
above.poolsDown.push(pool);
return;
}
}
});
});
const emitPool = function(poolPoly) {
if (poolPoly.mark) return;
poolPoly.mark = true;
const polys = poolPoly.pool.slice().append(poolPoly);
startPoint = poly2polyEmit(polys, startPoint, emitter);
startPoint = poly2polyEmit(poolPoly.poolsDown, startPoint, emitPool, "del_pdown");
return startPoint;
2017-04-18 17:09:33 -04:00
};
// from the top layer, iterate and descend through all connected pools
// pools are sorted smallest to largest. pools are polygons with an
// attached 'pool' array of polygons
layers.forEach(function(pools) {
startPoint = poly2polyEmit(pools, startPoint, emitPool, "del_ptop");
2017-04-18 17:09:33 -04:00
})
return startPoint;
}
/**
* flatten deeply nested polygons preserving inner arrays
*
* @param {Polygon | Polygon[]} poly or array to flatten
* @param {Polygon[]} to
* @returns {Polygon[]}
*/
function flattenPolygons(poly, to) {
if (!poly) return;
if (!to) to = [];
if (Array.isArray(poly)) {
poly.forEach(function(p) {
flattenPolygons(p, to);
})
} else {
to.push(poly);
flattenPolygons(poly.inner, to);
}
return to;
}
function polygonFitsIn(inside, outside, tolerance) {
return inside.isInside(outside, tolerance);
// return inside.area() <= outside.area() + tolerance &&
// (polygonWithinOffset(inside, outside, tolerance) || inside.isInside(outside, tolerance));
}
function polygonWithinOffset(poly1, poly2, offset) {
return polygonMinOffset(poly1, poly2, offset) <= offset;
}
function polygonMinOffset(poly1, poly2, offset) {
let mindist = Infinity;
2017-04-18 17:09:33 -04:00
poly1.forEachPoint(function(p) {
const nextdist = p.distToPolySegments(poly2, offset);
mindist = Math.min(mindist, nextdist);
// returning true terminates forEachPoint()
if (mindist <= offset) return true;
});
return mindist;
}
/**
* calculate mm of filament required for a given extrusion length and layer height.
*
2017-04-18 17:09:33 -04:00
* @param noz nozzle diameter
* @param fil filament diameter
* @param slice height in mm
* @returns mm of filament extruded per mm of length on the layer
2017-04-18 17:09:33 -04:00
*/
function extrudePerMM(noz, fil, slice) {
return ((PI * SQR(noz/2)) / (PI * SQR(fil/2))) * (slice / noz);
2017-04-18 17:09:33 -04:00
}
function constOp(tok, consts, opch, op) {
let pos, v1, v2;
2017-04-18 17:09:33 -04:00
if ((pos = tok.indexOf(opch)) > 0) {
v1 = consts[tok.substring(0,pos)] || 0;
v2 = parseInt(tok.substring(pos+1)) || 0;
return op(v1,v2);
} else {
return null;
}
}
function constReplace(str, consts, start) {
let cs = str.indexOf("{", start || 0),
2017-04-18 17:09:33 -04:00
ce = str.indexOf("}", cs),
tok, nutok, nustr;
if (cs >=0 && ce > cs) {
tok = str.substring(cs+1,ce);
nutok =
constOp(tok, consts, "-", function(v1,v2) { return v1-v2 }) ||
constOp(tok, consts, "+", function(v1,v2) { return v1+v2 }) ||
constOp(tok, consts, "/", function(v1,v2) { return v1/v2 }) ||
constOp(tok, consts, "*", function(v1,v2) { return v1*v2 }) ||
consts[tok] || 0;
nustr = str.replace("{"+tok+"}",nutok);
return constReplace(nustr, consts, ce+1+(nustr.length-str.length));
} else {
return str;
}
}
})();