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

688 lines
24 KiB
JavaScript

/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: geo.base
// dep: geo.point
// dep: geo.polygon
// dep: geo.paths
// dep: kiri.consts
gapp.register("kiri.print", [], (root, evets) => {
const { base, kiri } = self;
const { paths, util, newPoint } = base;
const { arcToPath } = paths;
const { numOrDefault } = util;
const { beltfact } = kiri.consts;
const XAXIS = new THREE.Vector3(1,0,0);
const DEG2RAD = Math.PI / 180;
class Print {
constructor(settings, widgets, id) {
this.id = id || new Date().getTime().toString(36);
this.settings = settings;
this.widgets = widgets;
this.lastPoint = null;
this.lastPoly = null;
this.lastEmit = null;
this.lastOut = null;
this.lastPos = null;
this.tools = {};
// set to 1 to enable flow rate analysis (console)
this.debugE = settings ? (settings.controller.devel ? 1 : 0) : 0;
}
setType(type) {
this.nextType = type;
}
// allows for gcode object id annotations enabling
// discrete object cancellation during print (bambu)
setWidget(widget) {
this.widget = widget;
}
/**
* addOutput - add a new point to the output gcode array
* @param {any[]} array - the output gcode array
* @param {Point} point - the new point
* @param {number} emit - the extrusion value
* @param {number} speed - the feed rate
* @param {string} tool - the tool id
* @param {"lerp"|string} opts.type - the output type
* @param {Point} opts.center - the center of the arc
* @param {Point[]} opts.arcPoints - point based approximation of arc used for rendering
* @param {unknown} opts.retract - the retraction value used for FDM
* @return {Output} - the new output object
*/
addOutput(array, point, emit, speed, tool, opts) {
const { type, retract, center, arcPoints} = opts ?? {};
let { lastPoint, lastEmit, lastOut } = this;
let arc = emit == 2 || emit == 3;
// drop duplicates (usually intruced by FDM bisections)
if (lastPoint && point && !arc && type !== 'lerp') {
// nested due to uglify confusing browser
const { x, y, z } = lastPoint;
if (point.x == x && point.y == y && point.z == z && lastEmit == emit) {
return lastOut;
}
}
// if (emit && emit < 1) console.log(emit);
this.lastPoint = point;
this.lastEmit = emit;
this.lastOut = lastOut = new Output(point, emit, speed, tool, {
type: type ?? this.nextType,
center,
arcPoints,
});
if (tool !== undefined) {
this.tools[tool] = true;
}
lastOut.retract = retract;
lastOut.widget = this.widget;
array.push(lastOut);
// console.log("addOutput Called", structuredClone({lastOut,array}))
this.nextType = undefined;
return lastOut;
}
addPrintPoints(input, output, startPoint, tool) {
if (this.startPoint && input.length > 0) {
this.lastPoint = this.startPoint;
// TODO: revisit seek to origin as the first move
// addOutput(output, startPoint, 0, undefined, tool);
}
output.appendAll(input);
}
/**
* Prints a polygon to a given output array, possibly with a given extrude factor,
* and starting from a given point. The last point is returned.
* used for FDM and laser
* @param {Polygon} poly - the polygon to print
* @param {Point} startPoint - the point to start printing from
* @param {Array} output - the array to print to
* @param {Object} [options] - optional parameters
* @param {boolean} [options.ccw] - set the polygon to be counter-clockwise
* @param {number} [options.extrude] - extrude factor for the polygon
* @param {number} [options.rate] - print speed in mm/s
* @param {number} [options.coast] - distance to coast at the end of the polygon
* @param {number} [options.simple] - if true, use the first point of the polygon
* @param {number} [options.open] - if true, don't close the polygon
* @param {number} [options.tool] - the tool to use
* @param {function} [options.onfirst] - called with the first point of the polygon
* @param {function} [options.onfirstout] - called with the first output point
* @returns {Point} the last point of the polygon
*/
polyPrintPath(poly, startPoint, output, options = {}) {
if (options.ccw) {
poly.setCounterClockwise();
} else {
poly.setClockwise();
}
const scope = this;
const { settings } = scope;
const { process } = settings;
let shortDist = process.outputShortDistance,
shellMult = numOrDefault(options.extrude, process.outputShellMult),
printSpeed = options.rate || process.outputFeedrate,
moveSpeed = process.outputSeekrate,
minSpeed = process.outputMinSpeed,
coastDist = options.coast || 0,
closest = options.simple ? poly.first() : poly.findClosestPointTo(startPoint),
perimeter = poly.perimeter(),
close = !options.open,
tool = options.tool,
last = startPoint,
first = true;
// if short, use calculated print speed based on sliding scale
if (perimeter < process.outputShortPoly) {
printSpeed = minSpeed + (printSpeed - minSpeed) * (perimeter / process.outputShortPoly);
}
poly.forEachPoint((point, pos, points, count) => {
if (first) {
if (options.onfirst) {
options.onfirst(point);
}
// move to first output point on poly
let out = scope.addOutput(output, point, 0, moveSpeed, tool);
if (options.onfirstout) {
options.onfirstout(out);
}
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)
scope.addOutput(output, offPoint, shellMult, printSpeed, tool);
shellMult = 0;
}
perimeter -= seglen;
scope.addOutput(output, point, shellMult, printSpeed, tool);
}
last = point;
}, close, closest.index);
this.lastPoly = poly;
return output.last().point;
}
constReplace(str, consts, start, pad, short) {
let cs = str.indexOf("{", start || 0),
ce = str.indexOf("}", cs),
tok, nutok, nustr;
if (cs >=0 && ce > cs) {
tok = str.substring(cs+1,ce);
let eva = [];
for (let [k,v] of Object.entries(consts)) {
switch (typeof v) {
case 'object':
eva.push(`let ${k} = ${JSON.stringify(v)};`);
break;
case 'number':
case 'boolean':
eva.push(`let ${k} = ${v};`);
break;
default:
if (v === undefined) v = '';
eva.push(`let ${k} = "${v.replace(/\"/g,"\\\"")}";`);
break;
}
}
eva.push(`function range(a,b) { return (a + (layer / layers) * (b-a)).round(4) }`);
eva.push(`try {( ${tok} )} catch (e) {console.log(e);0}`);
let scr = eva.join('');
let evl = eval(`{ ${scr} }`);
nutok = evl;
if (pad === 666) {
return evl;
}
if (pad) {
nutok = nutok.toString();
let oldln = ce-cs+1;
let tokln = nutok.length;
if (tokln < oldln) {
short = (short || 1) + (oldln - tokln);
}
}
nustr = str.replace("{"+tok+"}",nutok);
return this.constReplace(nustr, consts, ce+1+(nustr.length-str.length), pad, short);
} else {
// insert compensating spaces for accumulated replace string shortages
if (short) {
let si = str.indexOf(';');
if (si > 0) {
str = str.replace(';', ';'.padStart(short,' '));
}
}
return str;
}
}
parseSVG(code, offset) {
let scope = this,
svg = new DOMParser().parseFromString(code, 'text/xml'),
lines = [...svg.getElementsByTagName('polyline')],
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);
const { x, y, z } = point; // SVGPoint is not serializable
scope.addOutput(seq, { x, y, z }, seq.length > 0);
});
output.push(seq);
});
scope.imported = code;
scope.lines = lines.length;
scope.bytes = code.length;
return scope.output;
};
parseGCode(gcode, offset, progress, done, opts = {}) {
const fdm = opts.fdm;
const cam = opts.cam;
const belt = opts.belt;
const lines = gcode
.toUpperCase()
.replaceAll("X", " X")
.replaceAll("Y", " Y")
.replaceAll("Z", " Z")
.replaceAll("A", " A")
.replaceAll("E", " E")
.replaceAll("F", " F")
.replaceAll("G", " G")
.replaceAll("I", " I")
.replaceAll("J", " J")
.replaceAll(" ", " ")
.split("\n");
const scope = this,
// morph = false,
morph = true,
bounds = scope.bounds = {
max: { x:-Infinity, y:-Infinity, z:-Infinity},
min: { x:Infinity, y:Infinity, z:Infinity}
},
pos = {
X: 0.0,
Y: 0.0,
Z: 0.0,
A: 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
};
let dz = 0,
abs = true,
absE = true,
defh = 0,
height = 0,
factor = 1,
tool = 0,
time = 0,
minf = Infinity,
maxf = 0,
seq = [],
autolayer = true,
newlayer = false,
hasmoved = false,
lastG = 'G1';
const output = scope.output = [ seq ];
const beltaxis = { X: "X", Y: "Z", Z: "Y", E: "E", F: "F" };
function LOG() {
console.log(...[...arguments].map(o => Object.clone(o)));
}
/**
* @function processLine
* @description parses a line of g-code into individual axis movements
* @param {string[]} line - the line of g-code as an array of strings,
* each representing a single axis movement
* @param {Object} axes - an object to store the axis values
* @returns {Object} an object containing the current and previous points
*/
function processLine(line, axes) {
const prevPoint = newPoint(
factor * pos.X ,
factor * pos.Y ,
factor * pos.Z + dz
)
.add(xoff)
.add(off);
// apply origin offset
// for (let layer of output) {
// for (let rec of layer) {
// let point = rec.point;
// point.x += off.X;
// point.y += off.Y;
// point.z += off.Z;
// }
// }
const point = prevPoint.clone()
line.forEach(tok => {
let axis = tok.charAt(0).toUpperCase();
if (morph && belt) {
axis = beltaxis[axis];
}
// console.log("position updated",structuredClone(pos))
let val = parseFloat(tok.substring(1));
axes[axis] = val;
// if( axis == 'I' || axis == "J") return
if (abs) {
pos[axis] = val;
if (axis == "X") point.x = factor * pos.X + xoff.x + off.x
else if (axis == "Y") point.y = factor * pos.Y + xoff.y + off.y
else if (axis == "Z") point.z = factor * pos.Z + xoff.z + off.z + dz
} else {
mov[axis] = val;
pos[axis] += val;
}
// console.log("position updated",structuredClone(pos))
});
let center;
if(axes.I !== undefined && axes.J !== undefined) {
center = newPoint(
factor* axes.I+ xoff.x,
factor* axes.J+ xoff.y,
0,
);
}else if(axes.R !== undefined) {
center = newPoint(
factor* Math.cos(axes.R * DEG2RAD),
factor* Math.sin(axes.R * DEG2RAD),
0,
);
}
if(center){
center = center.add(prevPoint);
center.setZ((prevPoint.z+point.z)/2+dz);
}
return {
center,
point,
prevPoint
};
}
function outputPoint(point,lastP,emit,{center,arcPoints,retract}) {
// non-move in a new plane means burp out
// the old sequence and start a new one
if (newlayer || (autolayer && seq.z != point.z)) {
newlayer = false;
let dz = point.z - seq.z;
let nh = dz > 0 ? dz : defh;
seq = [];
seq.height = height = nh;
if (fdm) dz = -height / 2;
output.push(seq);
}
if (!hasmoved) {
seq.height = seq.z = pos.Z;
hasmoved = true;
}
// debug extrusion rate
const lastPos = scope.lastPos;
if (scope.debugE && fdm && lastPos && pos.E) {
// extruder move
let dE = (absE ? pos.E - scope.lastPosE : pos.E);
// distance moved in XY
let dV = point.distTo2D(lastP);
// debug print time
time += (dV * pos.F) / 1000;
// filament per mm
let dR = (dE / dV);
if (dV > 2 && dE > 0.001) {
let lab = (absE ? 'aA' : 'rR')[scope.debugE++ % 2];
console.log(lab, height.toFixed(2), dV.toFixed(2), dE.toFixed(3), dR.toFixed(4), pos.F.toFixed(0));
}
}
// add point to current sequence
scope.addOutput(seq, point, emit, pos.F, tool,{retract,arcPoints});
scope.lastPos = Object.assign({}, pos);
scope.lastPosE = pos.E;
}
/**
* Handles G2 and G3 arcs, which are circular arcs.
* @param {boolean} g2 - Whether this is a G2 or G3 arc. G2 is a clockwise arc, G3 is a counter-clockwise arc.
* @param {string[]} line - The line of the g-code file that contains the G2 or G3 command.
* @param {number} index - The line number of the g-code file that contains the G2 or G3 command.
*/
function G2G3(g2, line, index) {
const axes = {};
const {point, prevPoint, center} = processLine(line,axes);
// console.log(structuredClone({point,prevPoint,center}));
let arcPoints = arcToPath( prevPoint, point, 64,{ clockwise:g2,center}) ?? []
let emit = g2 ? 2 : 3;
// console.log("clone point",structuredClone({point,prevPoint,center,arcPoints,emit}));
// console.log("pointer point",{point,prevPoint,center,arcPoints,emit});
outputPoint(point,prevPoint,emit,{center,arcPoints});
// scope.addOutput(seq, point, emit, pos.F, tool,{center,arcPoints});
}
function G0G1(g0, line) {
const mov = {};
const axes = {};
lastG = g0 ? 'G0' : 'G1';
const {point, prevPoint} = processLine(line,axes);
if (morph && belt) {
point.y -= point.z * beltfact;
point.z *= beltfact;
}
if (pos.A) {
let ip = new THREE.Vector3(pos.X, pos.Y, pos.Z)
.applyAxisAngle(XAXIS, -pos.A * DEG2RAD);
point.x = ip.x;
point.y = ip.y;
point.z = ip.z;
}
const retract = (fdm && pos.E < 0) || undefined;
const moving = g0 || (fdm && (pos.E <= 0 || !(axes.X || axes.Y || axes.Z)));
if (!moving && point.x) bounds.min.x = Math.min(bounds.min.x, point.x);
if (!moving && point.x) bounds.max.x = Math.max(bounds.max.x, point.x);
if (!moving && point.y) bounds.min.y = Math.min(bounds.min.y, point.y);
if (!moving && point.y) bounds.max.y = Math.max(bounds.max.y, point.y);
if (!moving && point.z) bounds.min.z = Math.min(bounds.min.z, point.z);
if (!moving && point.z) bounds.max.z = Math.max(bounds.max.z, point.z);
// update max speed
if (pos.F) minf = Math.min(minf, pos.F);
maxf = Math.max(maxf, pos.F);
// always add moves to the current sequence
if (moving) {
// console.log("move",structuredClone(point))
scope.addOutput(seq, point, 0, pos.F, tool,{retract})
scope.lastPos = Object.assign({}, pos);
return;
}
if (seq.z === undefined) {
seq.z = point.z;
}
if (fdm && height === 0) {
seq.height = defh = height = pos.Z;
}
outputPoint(point,prevPoint,1,{retract})
}
const linemod = cam ? Math.ceil(lines.length / 2500) : 0;
lines.forEach((line, idx) => {
if (linemod && idx % linemod === 0) {
newlayer = true;
autolayer = false;
}
if (line.indexOf(';LAYER:') === 0) {
newlayer = true;
autolayer = false;
}
if (line.indexOf('- LAYER ') > 0) {
seq.height = defh;
const hd = line.replace('(','').replace(')','').split(' ');
defh = parseFloat(hd[4]);
if (fdm) dz = -defh / 2;
newlayer = true;
autolayer = false;
}
// if (["X","Y","Z"].indexOf(line.charAt(0)) >= 0) {
// line = `G0${line}`;
// }
line = line.trim().split(";")[0].split(" ").filter(v => v);
if (!line.length) return;
const c0 = line[0].charAt(0);
let cmd = ["X","Y","Z"].indexOf(c0) >= 0 ? lastG : line.shift();
if (!cmd) return;
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;
}
}
pos.E = 0.0;
switch (cmd) {
case 'M82':
absE = true;
break;
case 'M83':
absE = false;
break;
case 'G20':
factor = 25.4;
break;
case 'G21':
factor = 1;
break;
case 'G90':
// absolute positioning
abs = true;
break;
case 'G91':
// relative positioning
abs = false;
break;
case 'G92':
line.forEach(tok => {
pos[tok.charAt(0)] = parseFloat(tok.substring(1));
});
break;
case 'G10':
if (seq && seq.length) {
seq.last().retract = true;
}
break;
case 'G11':
break;
case 'G0':
G0G1(1, line);
break;
case 'G1':
G0G1(0, line);
break;
case 'G2':
// turn arc into a series of points
G2G3(true, line, idx)
break;
case 'G3':
// turn arc into a series of points
G2G3(false, line, idx);
break;
case 'M6':
tool = parseInt(line[0].substring(1));
break;
}
});
scope.imported = gcode;
scope.lines = lines.length;
scope.bytes = gcode.length;
scope.minSpeed = Math.floor(minf / 60);
scope.maxSpeed = Math.floor(maxf / 60);
scope.belt = belt;
if (scope.debugE) {
console.log({ bounds, print_time: time.round(2) });
}
console.log(scope.output)
done({ output: scope.output });
}
}
class Output {
/**
* Construct a new output element.
*
* in cam, emit is the G code number (G0, G1, G2, G3)
*
* @param {Point} point point to emit, with x, y, and z properties
* @param {number} emit emit (feed for printers, power for lasers, cut for cam)
* @param {number} speed speed in mm/min
* @param {number} tool tool id
* @param {Object} options options object
* @param {string} [options.type] type of point
* @param {Point} [options.center] the center of the arc
* @param {Point[]} [options.arcPoints] point based approximation of arc
*/
constructor(point, emit, speed, tool, options) {
const { type, center, arcPoints } = (options ?? {});
//speed, tool, type, center, arcPoints
this.point = point;
this.emit = Number(emit); //convert bools into 0/1
this.speed = speed;
this.tool = tool;
this.type = type;
this.center = center;
this.arcPoints = arcPoints;
// this.where = new Error().stack.split("\n");
}
clone(z) {
let o = new Output(
this.point.clone(),
this.emit,
this.speed,
this.tool,
this.type
);
if (z !== undefined) {
o.point.setZ(z);
}
return o;
}
set_retract() {
this.retract = true;
return this;
}
}
function newPrint(settings, widgets, id) {
return new Print(settings, widgets, id);
};
gapp.overlay(kiri, {
Print,
newPrint
});
});