grid-apps-cmms/js/kiri-print.js
2017-04-18 17:09:33 -04:00

765 lines
25 KiB
JavaScript

/** Copyright 2014-2017 Stewart Allen -- All Rights Reserved */
"use strict";
var gs_kiri_print = exports;
(function() {
if (!self.kiri) self.kiri = {};
var KIRI = self.kiri,
DRIVERS = KIRI.driver,
CAM = DRIVERS.CAM,
FDM = DRIVERS.FDM,
LASER = DRIVERS.LASER,
BASE = self.base,
UTIL = BASE.util,
DBUG = BASE.debug,
POLY = BASE.polygons,
SQRT = Math.sqrt,
PI = Math.PI,
PROTO = Print.prototype,
Polygon = BASE.Polygon,
newPoint = BASE.newPoint;
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);
this.settings = settings;
this.widgets = widgets;
this.group = new THREE.Group();
this.layerView = [];
this.time = 0;
this.lines = 0;
this.bytes = 0;
this.output = [];
this.distance = 0;
this.bounds = null;
}
PROTO.newOut = newOut;
PROTO.tip2tipEmit = tip2tipEmit;
PROTO.extrudePerMM = extrudePerMM;
PROTO.constReplace = constReplace;
PROTO.poly2polyEmit = poly2polyEmit;
PROTO.addPrintPoints = addPrintPoints;
PROTO.poly2polyDepthFirstEmit = poly2polyDepthFirstEmit;
PROTO.parseGCode = function(gcode, offset) {
var lines = gcode
.toUpperCase()
.replace("X", " X")
.replace("Y", " Y")
.replace("Z", " Z")
.replace("E", " E")
.replace("F", " F")
.replace(" ", " ")
.split("\n");
var scope = this,
output = scope.output = [],
bounds = scope.bounds = {
max: { x:-Infinity, y:-Infinity, z:-Infinity},
min: { x:Infinity, y:Infinity, z:Infinity}
},
seq = [],
move = false,
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
};
lines.forEach(function(line) {
line = line.split(" ");
if (line.length < 2) return;
switch (line.shift()) {
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;
}
seq.push(newOut(
{x:pos.X + off.x, y:pos.Y + off.y, z:pos.Z + off.z},
!move,
pos.F
));
break;
case 'M6':
break;
}
move = false;
pos.E = 0.0;
LZ = pos.Z;
});
G0();
scope.lines = lines.length;
scope.bytes = gcode.length;
};
PROTO.setup = function(remote, onupdate, ondone) {
var scope = this,
settings = scope.settings,
mode = settings.mode;
if (remote) {
KIRI.work.printSetup(settings, function(reply) {
if (reply.done) {
scope.output = reply.output;
ondone();
} else {
onupdate(reply.update, reply.updateStatus)
}
});
} else {
var driver = KIRI.driver[mode];
if (driver) driver.printSetup(scope, onupdate);
else console.log({missing_print_driver: mode});
ondone();
}
};
PROTO.exportGCode = function(remote, ondone, online) {
var scope = this,
settings = scope.settings,
mode = settings.mode;
if (remote) {
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;
} else {
var driver = KIRI.driver[mode];
if (driver && driver.printExport) {
ondone(driver.printExport(scope, online));
} else {
console.log({missing_export_driver: mode});
ondone(null);
}
}
};
PROTO.exportLaserGCode = function() {
return KIRI.driver.LASER.exportGCode(this);
};
PROTO.exportSVG = function() {
return KIRI.driver.LASER.exportSVG(this);
};
PROTO.exportDXF = function() {
return KIRI.driver.LASER.exportDXF(this);
};
PROTO.encodeOutput = function() {
var newout = [], newlayer;
this.output.forEach(function(layerout) {
newlayer = [];
newout.push(newlayer);
layerout.forEach(function(out) {
if (out.point) newlayer.push({emit:out.emit, point:{x:out.point.x, y:out.point.y, z:out.point.z}});
});
});
return newout;
};
PROTO.render = function() {
var scope = this,
mode = scope.settings.mode;
switch (mode) {
case 'CAM':
case 'FDM':
scope.renderMoves(true, 0x0088aa);
break;
case 'LASER':
scope.renderMoves(false, 0x0088aa);
break;
}
};
PROTO.renderMoves = function(showMoves, moveColor) {
var scope = this, last, view;
// render layered output
scope.lines = 0;
scope.output.forEach(function(layerout) {
var move = [], print = [], z;
layerout.forEach(function(out) {
if (last) {
if (UTIL.distSq(last, out.point) < 0.001 && out.point.z === last.z) {
return;
}
if (out.emit > 0) {
print.push(last);
print.push(out.point);
} else {
move.push(last);
move.push(out.point);
}
} else {
if (out.emit) DBUG.log("first point is emit");
z = out.point.z;
}
last = out.point;
});
view = KIRI.newLayer(scope.group);
scope.layerView.push(view);
if (showMoves) view.lines(move, moveColor);
view.lines(print, 0x5566aa);
view.render();
scope.lines += print.length;
});
}
PROTO.getLayerCount = function() {
return this.output.length;
}
PROTO.hide = function() {
this.layerView.forEach(function(layer) {
layer.setVisible(false);
})
};
PROTO.showLayer = function(index, show) {
if (this.layerView[index]) this.layerView[index].setVisible(show);
};
/**
* @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;
}
/**
* @param {Point} point
* @param {number} emit (0=move, !0=filament emit/laser on/cut mode)
* @param {number} [speed] speed
* @param {number} [tool] tool
*/
function newOut(point, emit, speed, tool) {
return new Output(point, emit, speed, tool);
}
/**
*
* @param {Polygon} poly
* @param {Point} startPoint
* @param {Array} output
* @param {number} [extrude] multiplier
* @param {number} [extrude] wipe distance
* @return {Point} last output point
*/
PROTO.polyPrintPath = function(poly, startPoint, output, extrude, wipe) {
// poly.setClosed();
poly.setClockwise();
var mindist = Infinity,
closest = poly.findClosestPointTo(startPoint),
dist,
first = true,
settings = this.settings,
shellMult = extrude || settings.process.outputShellMult;
poly.forEachPoint(function(point) {
if (first) {
// move from startPoint to point
output.push(newOut(point, 0));
first = false;
} else {
output.push(newOut(point, shellMult));
}
}, true, closest.index);
if (wipe) {
wipe = Math.min(wipe, poly.perimeter());
poly.forEachSegment(function(point, next) {
// exit loop when no more wipe
if (!(wipe && wipe > 0.1)) return true;
dist = point.distTo2D(next);
if (dist <= wipe) {
output.push(newOut(next, 0));
wipe -= dist;
} else {
output.push(newOut(point.followTo(next, wipe), 0));
return true;
}
}, false, closest.index);
}
return output[output.length - 1].point;
};
/**
* create 3d print output path for this slice
*
* @parma {Slice} slice
* @param {Point} startPoint start as close as possible to startPoint
* @param {THREE.Vector3} offset
* @param {Point[]} output points
* @param {boolean} isFDM controls whether we emit wipe or not
* @return {Point} last output point
*/
PROTO.slicePrintPath = function(slice, startPoint, offset, output, isFDM) {
var i,
preout = [],
scope = this,
settings = this.settings,
process = settings.process,
minSeek = settings.device.nozzleSize * 1.5,
wipeDist = process.outputWipeDistance, // todo disable for laser mode
fillMult = process.outputFillMult,
seekMult = fillMult * 0.5,
origin = startPoint.add(offset),
extrude = process.outputShellMult || (process.laserSliceHeight >= 0 ? 1 : 0),
z = slice.z;
function outputTraces(poly, extrude, last) {
if (!poly) return;
if (Array.isArray(poly)) {
outputOrderClosest(poly, function(next) {
outputTraces(next, extrude, last);
});
} else {
startPoint = scope.polyPrintPath(poly, startPoint, preout, extrude, isFDM && last ? wipeDist : 0);
}
}
/**
* @param {Polygon[]} polys
*/
function outputSparse(polys) {
if (!polys) return;
var lines = [], p1, p2, iter;
polys.forEach(function(poly) {
poly.forEachSegment(function(p1,p2) {
lines.push(p1.clone());
lines.push(p2.clone());
}, true);
});
outputFills(lines);
}
function outputFills(lines, extrude) {
var mindist, p1, p2, dist, point, find,
printMult = extrude || fillMult;
while (lines) {
find = null;
mindist = Infinity;
for (i=0; i<lines.length; i++) {
point = lines[i];
if (point.del) continue;
dist = SQRT(startPoint.distToSq2D(point));
if (dist < mindist) {
find = {i:i, p:point};
mindist = dist;
}
}
if (find) {
if (find.i % 2 === 0) {
p1 = find.p;
p2 = lines[find.i + 1];
} else {
p1 = find.p;
p2 = lines[find.i - 1];
}
p1.del = true;
p2.del = true;
preout.push(newOut(p1, SQRT(startPoint.distToSq2D(p1)) < minSeek ? seekMult : 0));
preout.push(newOut(p2, printMult));
startPoint = p2;
} else {
break;
}
}
// 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
* @param {Array} array of Polygon or Polygon wrappers
* @param {Function} fn
* @param {Function} fnp convert 'next' object into a Polygon
*/
function outputOrderClosest(array, fn, fnp) {
array = array.slice();
var closest, find, next, poly;
for (;;) {
closest = null;
for (i=0; i<array.length; i++) {
next = array[i];
if (!next) continue;
poly = fnp ? fnp(next) : next;
find = poly.findClosestPointTo(startPoint);
if (!closest || find.distance < closest.distance) {
closest = find;
closest.i = i;
closest.next = next;
}
}
if (!closest) return;
array[closest.i] = null;
fn(closest.next);
}
}
var all = [].appendAll(slice.supports || []).appendAll(slice.tops || []);
outputOrderClosest(all || [], function(next) {
if (next instanceof Polygon) {
// support polygon
next.setZ(z);
outputTraces(next, extrude);
outputTraces(next.inner, extrude);
if (next.fills) {
next.fills.forEach(function(p) { p.z = z });
outputFills(next.fills, extrude);
}
} else {
// top object
outputTraces(next.traces, extrude);
outputTraces(next.innerTraces(), extrude);
outputFills(next.fill_lines);
outputSparse(next.fill_sparse);
}
}, function(obj) {
return obj instanceof Polygon ? obj : obj.poly;
});
// offset print points
for (i=0; i<preout.length; i++) {
preout[i].point = preout[i].point.add(offset);
}
// add offset points to total print
addPrintPoints(preout, output, origin);
return startPoint.add(offset);
};
/**
*
* @param {Output[]} input
* @param {Point[]} output
* @param {Point} [startPoint]
*/
function addPrintPoints(input, output, startPoint) {
if (startPoint && input.length > 0) {
output.push(newOut(startPoint, 0));
}
output.appendAll(input);
}
/**
* emit each element in an array based on
* the next closest endpoint.
* todo replace outputFills() with this
*/
function tip2tipEmit(array, startPoint, emitter) {
var mindist, dist, found, count = 0;
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;
startPoint = found.last;
emitter(found.el, found.first, ++count);
} else {
break;
}
}
return startPoint;
}
/**
* like tip2tipEmit but accepts an array of
* polygons and the next closest point can
* be anywhere in the adjacent polygon
*/
function poly2polyEmit(array, startPoint, emitter) {
var mindist, dist, found, count = 0;
for (;;) {
found = null;
mindist = Infinity;
array.forEach(function(poly) {
if (poly.delete) return;
if (poly.isOpen()) {
const d2f = startPoint.distTo2D(poly.first());
const d2l = startPoint.distTo2D(poly.first());
if (d2f > mindist && d2l > mindist) return;
if (d2l < mindist && d2l < d2f) {
poly.reverse();
found = {poly:poly, index:0, point:poly.first()};
} else if (d2f < mindist) {
found = {poly:poly, index:0, point:poly.first()};
}
return;
}
poly.forEachPoint(function(point, index) {
dist = startPoint.distTo3D(point);
if (dist < mindist) {
found = {poly:poly, index:index, point:point};
mindist = dist;
}
});
});
if (found) {
found.poly.delete = true;
startPoint = emitter(found.poly, found.index, ++count, startPoint) || found.point;
} else {
break;
}
}
// undo delete marks
array.forEach(function(poly) { poly.delete = false });
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) {
var layers = [],
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
var search = poly.parent;
// 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 (var i=0; i<poolsAbove.length; i++) {
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);
poolPoly.poolsDown.forEach(function(downPool) {
emitPool(downPool);
});
};
// 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) {
pools.forEach(function(poolPoly) {
emitPool(poolPoly);
});
})
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) {
var mindist = Infinity;
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;
}
/**
* @param noz nozzle diameter
* @param fil filament diameter
* @param slice height in mm
* @returns filament extruded per mm
*/
function extrudePerMM(noz, fil, slice) {
return ((PI * UTIL.sqr(noz/2)) /
(PI * UTIL.sqr(fil/2))) *
(slice / noz);
}
function constOp(tok, consts, opch, op) {
var pos, v1, v2;
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) {
var cs = str.indexOf("{", start || 0),
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;
}
}
})();