624 lines
21 KiB
JavaScript
624 lines
21 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, Polygon } = base;
|
|
const { tip2tipEmit } = 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;
|
|
}
|
|
|
|
addOutput(array, point, emit, speed, tool, type) {
|
|
let { lastPoint, lastEmit, lastOut } = this;
|
|
// drop duplicates (usually intruced by FDM bisections)
|
|
if (lastPoint && point) {
|
|
// 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 || this.nextType);
|
|
if (tool !== undefined) {
|
|
this.tools[tool] = true;
|
|
}
|
|
array.push(lastOut);
|
|
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);
|
|
}
|
|
|
|
// fdm & laser
|
|
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,
|
|
minf = Infinity,
|
|
maxf = 0,
|
|
seq = [],
|
|
autolayer = true,
|
|
newlayer = false,
|
|
arcdivs = Math.PI / 24,
|
|
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 G2G3(g2, line, index) {
|
|
const rec = {};
|
|
|
|
line.forEach(tok => {
|
|
rec[tok.charAt(0)] = parseFloat(tok.substring(1));
|
|
});
|
|
|
|
let center = { x:0, y:0, r:0 };
|
|
|
|
if (rec.X === undefined && rec.X === rec.Y) {
|
|
// bambu generates loop z or wipe loop arcs in place
|
|
// console.log({ skip_empty_arc: rec });
|
|
return;
|
|
}
|
|
// G0G1(false, [`X${rec.X}`, `Y${rec.Y}`, `E1`]);return;
|
|
|
|
if (rec.I !== undefined && rec.J !== undefined) {
|
|
center.x = pos.X + rec.I;
|
|
center.y = pos.Y + rec.J;
|
|
center.r = Math.sqrt(rec.I*rec.I + rec.J*rec.J);
|
|
} else if (rec.R !== undefined) {
|
|
let pd = { x: rec.X - pos.X, y: rec.Y - pos.Y };
|
|
let dst = Math.sqrt(pd.x * pd.x + pd.y * pd.y) / 2;
|
|
let pr2;
|
|
if (Math.abs(dst - rec.R) < 0.001) {
|
|
// center point radius
|
|
pr2 = { x: (rec.X + pos.X) / 2, y: (rec.Y + pos.Y) / 2};
|
|
} else {
|
|
// triangulate
|
|
pr2 = base.util.center2pr({
|
|
x: pos.X,
|
|
y: pos.Y
|
|
}, {
|
|
x: rec.X,
|
|
y: rec.Y
|
|
}, rec.R, g2);
|
|
}
|
|
center.x = pr2.x;
|
|
center.y = pr2.y;
|
|
center.r = rec.R;
|
|
} else {
|
|
console.log({malfomed_arc: line});
|
|
}
|
|
|
|
// line angles
|
|
let a1 = Math.atan2(center.y - pos.Y, center.x - pos.X) + Math.PI;
|
|
let a2 = Math.atan2(center.y - rec.Y, center.x - rec.X) + Math.PI;
|
|
let ad = base.util.thetaDiff(a1, a2, g2);
|
|
let steps = Math.max(Math.floor(Math.abs(ad) / arcdivs), 3);
|
|
let step = (Math.abs(ad) > 0.001 ? ad : Math.PI * 2) / steps;
|
|
let rot = a1 + step;
|
|
|
|
let da = Math.abs(a1 - a2);
|
|
let dx = pos.X - rec.X;
|
|
let dy = pos.Y - rec.Y;
|
|
let dd = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
// LOG({index, da, dd, first: pos, last: rec, center, a1, a2, ad, step, steps, rot, line});
|
|
// G0G1(false, [`X${center.x}`, `Y${center.y}`, `E1`]);
|
|
|
|
// under 1 degree arc and 5mm, convert to straight line
|
|
if (da < 0.005 && dd < 5) {
|
|
G0G1(false, [`X${rec.X}`, `Y${rec.Y}`, `E1`]);
|
|
return;
|
|
}
|
|
|
|
let pc = { X: pos.X, Y: pos.Y };
|
|
for (let i=0; i<=steps-2; i++) {
|
|
let np = {
|
|
X: center.x + Math.cos(rot) * center.r,
|
|
Y: center.y + Math.sin(rot) * center.r
|
|
};
|
|
rot += step;
|
|
G0G1(false, [`X${np.X}`, `Y${np.Y}`, `E1`]);
|
|
}
|
|
|
|
G0G1(false, [`X${rec.X}`, `Y${rec.Y}`, `E1`]);
|
|
|
|
pos.X = rec.X;
|
|
pos.Y = rec.Y;
|
|
}
|
|
|
|
function G0G1(g0, line) {
|
|
const mov = {};
|
|
const axes = {};
|
|
|
|
lastG = g0 ? 'G0' : 'G1';
|
|
|
|
line.forEach(tok => {
|
|
let axis = tok.charAt(0);
|
|
if (morph && belt) {
|
|
axis = beltaxis[axis];
|
|
}
|
|
let val = parseFloat(tok.substring(1));
|
|
axes[axis] = val;
|
|
if (abs) {
|
|
pos[axis] = val;
|
|
} else {
|
|
mov[axis] = val;
|
|
pos[axis] += val;
|
|
}
|
|
});
|
|
|
|
const point = newPoint(
|
|
factor * pos.X + xoff.X,
|
|
factor * pos.Y + xoff.Y,
|
|
factor * pos.Z + xoff.Z + dz
|
|
);
|
|
|
|
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) {
|
|
scope.addOutput(seq, point, false, pos.F, tool).retract = retract;
|
|
scope.lastPos = Object.assign({}, pos);
|
|
return;
|
|
}
|
|
|
|
if (seq.Z === undefined) {
|
|
seq.Z = pos.Z;
|
|
}
|
|
|
|
if (fdm && height === 0) {
|
|
seq.height = defh = height = pos.Z;
|
|
}
|
|
|
|
// non-move in a new plane means burp out
|
|
// the old sequence and start a new one
|
|
if (newlayer || (autolayer && seq.Z != pos.Z)) {
|
|
newlayer = false;
|
|
let dz = pos.Z - seq.Z;
|
|
let nh = dz > 0 ? dz : defh;
|
|
seq = [];
|
|
seq.height = height = nh;
|
|
if (fdm) dz = -height / 2;
|
|
output.push(seq);
|
|
}
|
|
|
|
if (!hasmoved && !moving) {
|
|
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 = Math.sqrt(
|
|
(Math.pow(pos.X - lastPos.X, 2)) +
|
|
(Math.pow(pos.Y - lastPos.Y, 2))
|
|
);
|
|
// 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, true, pos.F, tool).retract = retract;
|
|
scope.lastPos = Object.assign({}, pos);
|
|
scope.lastPosE = pos.E;
|
|
}
|
|
|
|
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(true, line);
|
|
break;
|
|
case 'G1':
|
|
G0G1(false, 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;
|
|
}
|
|
});
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
done({ output: scope.output });
|
|
}
|
|
}
|
|
|
|
class Output {
|
|
constructor(point, emit, speed, tool, type) {
|
|
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;
|
|
this.type = type;
|
|
}
|
|
|
|
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
|
|
});
|
|
|
|
});
|