grid-apps-cmms/src2/kiri-mode/fdm/prepare.js
2025-07-05 17:06:38 -04:00

1504 lines
56 KiB
JavaScript

/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { base, util } from '../../geo/base.js';
import { poly2polyEmit, tip2tipEmit } from '../../geo/paths.js';
import { newBounds } from '../../geo/bounds.js';
import { newPoint } from '../../geo/point.js';
import { newPolygon, Polygon } from '../../geo/polygon.js';
import { polygons as POLY, fillArea } from '../../geo/polygons.js';
import { newPrint } from '../../kiri/print.js';
import { newSlice } from '../../kiri/slice.js';
import { newWidget } from '../../kiri/widget.js';
import { render } from '../../kiri/render.js';
import { getRangeParameters } from './driver.js';
const { numOrDefault } = util;
/**
* DRIVER PRINT CONTRACT
*
* @param {Function} update progress callback
* @returns {Object[]} returns array of render objects
*/
export async function fdm_prepare(widgets, settings, update) {
// filter ignored widgets
widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled);
let { device, process, controller, bounds } = settings,
{ sliceHeight, firstSliceHeight, firstLayerRate } = process,
{ outputSeekrate, outputDraftShield, outputPurgeTower } = process,
{ bedWidth, bedDepth } = device,
{ lineType } = controller,
printPoint = newPoint(0,0,0),
print = self.kiri_worker.current.print = newPrint(settings, widgets),
nozzle = process.sliceLineWidth || device.extruders[0].extNozzle,
isBelt = device.bedBelt,
isThin = lineType === "line",
isFlat = lineType === "flat",
useRaft = process.outputRaft || false,
firstLayerHeight = isBelt ? sliceHeight : firstSliceHeight || sliceHeight,
layerno = 0,
zneg = 0,
zoff = 0,
zmin = 0,
shield,
output = [],
layerout = [];
let lastLayerStart = null;
// compute bounds if missing
if (!bounds) {
bounds = new THREE.Box3();
for (let widget of widgets) {
let wp = widget.track.pos;
let wb = widget.bounds.clone();
wb.min.x += wp.x;
wb.max.x += wp.x;
wb.min.y += wp.y;
wb.max.y += wp.y;
bounds.union(wb);
}
settings.bounds = bounds;
}
// TODO pick a widget with a slice on the first layer and use that nozzle
// create brim, skirt, raft if specificed in FDM mode (code shared by laser)
if (!isBelt && (process.outputBrimCount || useRaft)) {
let brims = [],
offset = process.outputBrimOffset || (useRaft ? 4 : 0);
// compute first brim
widgets.filter(w => w.slices.length).forEach(function(widget) {
let tops = [];
let slices = [];
if (outputDraftShield) {
slices.appendAll(widget.slices);
} else {
slices.push(widget.slices[0]);
}
for (let slice of slices) {
// collect top outer polygons
if (slice.tops)
slice.tops.forEach(function(top) {
tops.push(top.poly.clone());
});
// collect support polygons
if (slice.supports)
slice.supports.forEach(function(support) {
tops.push(support.clone());
});
}
if (outputDraftShield) {
tops = POLY.union(tops,1,true);
}
// nest and offset tops
POLY.nest(tops).forEach(function(poly) {
let off = poly.offset(-offset + nozzle / 2);
if (off) off.forEach(function(brim) {
brim.move(widget.track.pos);
brims.push(brim);
});
});
});
// merge brims
brims = POLY.union(brims, undefined, true);
// if brim is offset, over-expand then shrink to induce brims to merge
if (brims.length > 1 && offset >= nozzle) {
let extra = process.sliceSupportExtra + 2;
let zheight = brims[0].getZ();
brims = POLY.expand(brims, extra, zheight, null, 1);
brims = POLY.expand(brims, -extra, zheight, null, 1);
}
// if raft is specified
if (useRaft) {
let offset = newPoint(0,0,0),
height = nozzle,
angle = process.sliceFillAngle,
rate1 = firstLayerRate / 3,
rate2 = process.outputFeedrate,
rafts = process.outputBrimCount ?
POLY.expand(brims, nozzle * 4, 0, null, 1) : brims;
function raft(height, angle, spacing, speed, extrude, outline) {
let slice = newSlice(zoff + height / 2);
for (let brim of rafts) {
// use first point of first brim as start point
if (printPoint === null) printPoint = brim.first();
let t = slice.addTop(brim);
if (outline) t.traces = [ brim ];
t.inner = POLY.expand([ brim ], nozzle * -0.8, 0, null, 1);
t.fill_lines = fillArea(t.inner, angle, spacing, []);
t.isRaft = true;
}
offset.z = slice.z;
printPoint = slicePrintPath(print, slice, printPoint, offset, layerout, {
speed,
mult: extrude,
});
layerout.z = zoff + height;
layerout.height = height;
output.append(layerout);
layerout = [];
zoff += height;
zneg = height / 2;
};
// cause first point of raft to be used
printPoint = null;
// emit raft layers
raft(nozzle * 1.5, angle + 0, nozzle * 6.00, rate1, 2.50, true);
raft(nozzle * 0.5, angle + 90, nozzle * 2.00, rate2, 1.50, true);
raft(nozzle * 0.5, angle + 0, nozzle * 1.00, rate2, 1.00);
raft(nozzle * 0.5, angle + 90, nozzle * 0.75, rate2, 0.75);
// raise first layer off raft slightly to lessen adhesion
firstLayerHeight += process.outputRaftSpacing || 0;
zoff += process.outputRaftSpacing || 0;
// retract after last raft layer
output.last().last().retract = true;
}
// if using brim or skirt
if (process.outputBrimCount) {
let polys = [],
preout = [];
// offset specified # of brims
POLY.offset(brims, nozzle, {
outs: polys,
flat: true,
count: process.outputBrimCount,
z: firstLayerHeight / 2 + zoff - zneg
});
print.setType('brim');
shield = POLY.nest(polys.clone(), true).clone();
// output brim points
let brimStart = offset < nozzle * 2 ? newPoint(-bedWidth, -bedDepth, 0) : printPoint;
printPoint = poly2polyEmit(polys, brimStart, (poly, index, count, startPoint) => {
return print.polyPrintPath(poly, startPoint, preout, {
rate: firstLayerRate,
onfirst: function(point) {
if (preout.length && point.distTo2D(startPoint) > 2) {
// retract between brims
preout.last().retract = true;
}
}
});
});
print.addPrintPoints(preout, layerout, null);
if (preout.length) {
// retract between brims and print
preout.last().retract = true;
}
}
// recompute bounds for purge block offsets
let bbounds = newBounds();
brims.forEach(brim => {
bbounds.merge(brim.bounds);
});
bounds.min.x = Math.min(bounds.min.x, bbounds.minx);
bounds.min.y = Math.min(bounds.min.y, bbounds.miny);
bounds.max.x = Math.max(bounds.max.x, bbounds.maxx);
bounds.max.y = Math.max(bounds.max.y, bbounds.maxy);
}
// synthesize support widgets when needed
// so that they can use a separate extruder
// compute zmin for belt purge towers
for (let widget of widgets.slice()) {
let sslices = [];
if (!widget.slices) {
console.log('invalid widget', widget);
continue;
}
for (let slice of widget.slices) {
zmin = Math.min(zmin, slice.z);
if (!slice.supports) {
continue;
}
let sslice = newSlice(slice.z);
sslice.extruder = process.sliceSupportNozzle;
sslice.supports = slice.supports.slice();
sslice.height = slice.height;
sslices.push(sslice);
}
if (sslices.length) {
let swidget = newWidget(null,widget.group);
swidget.slices = sslices;
swidget.support = true;
swidget.belt = widget.belt;
swidget.track = Object.clone(widget.track);
swidget.mesh = { widget: swidget, position: swidget.track.pos };
widget.anno.extruder = process.sliceSupportNozzle;
widgets.push(swidget);
}
}
let lastPoly;
let lastLayer;
let lastOffset;
let extruders = print.extruders = [];
let extcount = 0;
// find max layers (for updates)
// generate list of used extruders for purge blocks
for (let widget of widgets) {
let extruder = widget.anno.extruder ?? 0;
if (!extruders[extruder]) {
extruders[extruder] = {};
extcount++;
}
}
// determine size/location of purge blocks
let blokw = Math.sqrt(outputPurgeTower || 0);
let blokh = blokw;
let blokpos, walkpos, blok;
function mkblok(w,h) {
let count = extcount - 1;
let gap = nozzle;
if (isBelt) {
let step = w + gap;
let mp = (bounds.max.y + bounds.min.y) / 2; // part y midpoint
let sp = (bounds.max.y - bounds.min.y); // part y span
let ts = (w * count) + (gap * (count - 1)); // tower y span
let ty = -ts + w/2 - gap/2; // tower start y
blokpos = { y:ty, x: bounds.max.x + 2 + h / 2}; // first block pos
walkpos = { y:step, x:0 };
blok = { x:h, y:w };
} else if (bounds.min.x < bounds.min.y) {
let step = h + gap;
let mp = (bounds.max.x + bounds.min.x) / 2; // part y midpoint
let sp = (bounds.max.x - bounds.min.x); // part y span
let ts = (h * count) + (gap * (count - 1)); // tower y span
let tx = mp - ts / 2 + step / 2; // tower start y
blokpos = { x:tx, y: bounds.max.y + 2 + w / 2}; // first block pos
walkpos = { x:step, y:0 };
blok = { x:w, y:h };
} else {
let step = w + gap;
let mp = (bounds.max.y + bounds.min.y) / 2; // part y midpoint
let sp = (bounds.max.y - bounds.min.y); // part y span
let ts = (w * count) + (gap * (count - 1)); // tower y span
let ty = mp - ts / 2 + step / 2; // tower start y
blokpos = { y:ty, x: bounds.max.x + 2 + h / 2}; // first block pos
walkpos = { y:step, x:0 };
blok = { x:w, y:h };
}
}
function mkrec(i, angle = 45, thin = 6) {
let exi = device.extruders[i],
noz = exi.extNozzle,
pos = {x:blokpos.x + walkpos.x * i, y:blokpos.y + walkpos.y * i, z:0},
rect = newPolygon().centerRectangle(pos, blok.x, blok.y),
full = linesToPoly(fillArea([
newPolygon().centerRectangle(pos, blok.x - noz, blok.y - noz)
], angle, noz)),
sparse = rect.area() > 10 ? linesToPoly(fillArea([
newPolygon().centerRectangle(pos, blok.x - noz, blok.y - noz)
], angle + 90, noz * thin)) : newPolygon(),
rec = {
extruder: i,
diameter: exi.extNozzle,
rect,
full,
sparse,
pause: newPoint(pos.x + walkpos.y, pos.y + walkpos.x, pos.z)
};
return rec;
}
mkblok(blokw, blokh);
// allocate tower space for extruders-1 locations
let towers = extruders.slice(1).map((ext,i) => {
return ext ? mkrec(i) : ext;
});
function linesToPoly(points) {
let poly = newPolygon().setOpen();
let ping = 0;
for (let i=0; i<points.length; i += 2) {
let p1 = points[i];
let p2 = points[i+1];
if (ping++ % 2 === 0) {
poly.push(p1);
poly.push(p2);
} else {
poly.push(p2);
poly.push(p1);
}
}
return poly;
}
let purgedFirst = false;
let lastPurgeTool;
// generate purge block for given nozzle
function purge(nozzle, track, layer, start, z, using, offset) {
if (!outputPurgeTower || extcount < 2) {
return start;
}
let rec = track.shift();
let thin = using >= 0 || lastPurgeTool === nozzle;
let tool = using >= 0 ? using : nozzle;
let first = isBelt ? !purgedFirst && layer.slice.index >= 0 : layer.slice.index === 0;
let rate = first ? process.firstLayerRate : process.outputFeedrate;
let wipe = true;
lastPurgeTool = tool;
if (rec) {
print.setType('purge tower');
if (layer.last()) {
layer.last().retract = true;
}
purgedFirst = purgedFirst || first;
let purgeOn = first || !thin;
if (isBelt && z < 0) {
let scale = 1 - ((z / zmin));
mkblok(blokw * scale, blokh);
rec = mkrec(rec.extruder, 0, 10);
rate = (process.outputFeedrate - process.firstLayerRate) * scale + process.firstLayerRate;
wipe = false;
}
let box = rec.rect.clone().setZ(z);
let pause = rec.pause.clone().setZ(z);
let fill = (z >= 0 && purgeOn ? rec.full : rec.sparse).clone().setZ(z);
if (isBelt) {
let bmove = {x:0, y:z, z:0};
box.move(bmove);
pause.move(bmove);
if (fill) {
fill.move(bmove);
}
if (offset) {
let bo = { x:0, y:offset.y, z:offset.z };
box.move(bo);
pause.move(bo);
if (fill) fill.move(bo);
}
}
start = print.polyPrintPath(box, start, layer, {
tool,
rate,
simple: true,
open: false,
onfirstout: (out => out.overate = (isBelt ? rate : 0))
});
if (fill && fill.length) {
// for pings, split path at 20mm
start = print.polyPrintPath(fill, start, layer, {
tool,
rate,
simple: true,
open: true,
onfirst: (point) => { point.purgeOn = purgeOn ? pause : undefined }
});
}
layer.last().retract = true;
// experimental post-retract wipe
if (wipe) start = print.polyPrintPath(box, start, layer, {
tool,
rate,
simple: true,
open: false,
extrude: 0
});
layer.last().overate = 0;
return start;
} else {
console.log({no_purge_tower_for: nozzle, using, track, layer});
return start;
}
}
// establish offsets
for (let widget of widgets) {
let { belt } = widget;
let offset = Object.clone(widget.track.pos);
if (isBelt) {
offset = {
x: belt.xpos,
y: belt.ypos * belt.cosf,
z: belt.ypos * belt.sinf
};
} else {
// when rafts used this is non-zero
offset.z = zoff;
}
widget.offset = offset;
}
// create shuffled slice cake by z offset (slice.z + offset.z)
let cake = [];
let zrec = {};
for (let widget of widgets) {
// skip synthesized support widget(s)
if (!widget.mesh) {
continue;
}
for (let slice of widget.slices) {
slice.widget = widget;
let z = (slice.z + widget.offset.z).round(2);
let rec = zrec[z] = zrec[z] || {z, slices:[]};
if (rec.slices.length === 0) {
cake.push(rec);
}
rec.slices.push(slice);
}
}
cake.sort((a, b) => {
return a.z - b.z;
});
let firstTool;
let lastWidget;
let lastExt;
let lastOut;
// walk cake layers bottom up
for (let layer of cake) {
// track purge blocks generated for each layer
let track = towers.slice();
// iterate over layer slices, find closest widget, print, eliminate
for (;;) {
let order = [];
// select slices of the same extruder type first then distance
for (let slice of layer.slices) {
if (slice.prep) {
continue;
}
let offset = lastOffset = slice.widget.offset;
let find = slice.findClosestPointTo(printPoint.sub(offset));
if (find) {
let ext = slice.extruder;
let lex = lastExt;
let dst = Math.abs(find.distance);
// penalize extruder swaps
if (ext !== lex) {
dst *= 10000;
}
// for first object, penalize extruders other than first
if (lastExt === undefined && ext > 0) {
dst *= 10000;
}
order.push({dst, slice, offset, z: layer.z});
}
}
if (order.length === 0) {
break;
}
order.sort((a,b) => {
return a.dst - b.dst;
});
let { z, slice, offset } = order[0];
if (firstTool === undefined) {
firstTool = slice.extruder;
}
// when layers switch between widgets, force retraction
let forceRetract = lastOut && lastOut.widget !== slice.widget;
if (forceRetract && output.length) {
// selecet last output of last layer
output.last().last().retract = true;
}
let params = getRangeParameters(process, slice.index);
slice.prep = true;
// retract between widgets or layers (when set)
if (layerout.length && slice.widget !== lastWidget) {
layerout.last().retract = true;
}
lastWidget = slice.widget;
layerout.z = z + slice.height / 2;
layerout.height = layerout.height || slice.height;
layerout.slice = slice;
layerout.params = params;
// mark layer as anchor if slice is belt and flag set
layerout.anchor = slice.belt && slice.belt.anchor;
// detect extruder change and print purge block
if (!lastOut || lastOut.extruder !== slice.extruder) {
if (slice.extruder >= 0)
printPoint = purge(slice.extruder, track, layerout, printPoint, slice.z, undefined, offset);
}
let wtb = slice.widget.track.box;
let beltStart = slice.belt && slice.belt.touch;// && (widgets.length === 1);
// output seek to start point between mesh slices if previous data
print.setType('layer');
print.setWidget(lastWidget);
printPoint = slicePrintPath(
print,
slice,
beltStart ? newPoint(-5000, 5000, 0) : printPoint.sub(offset),
offset,
layerout,
{
routeAround: process.outputAvoidGaps,
seedPoint: printPoint.sub(offset),
params, // range parameters
first: slice.index === 0,
support: slice.widget.support,
onBelt: beltStart,
pretract: (wipeDist) => {
if (!(lastLayer && lastLayer.length)) {
return;
}
let lastOut = lastLayer.last().set_retract();
if (wipeDist && lastPoly && lastOut.point) {
let center = lastPoly.center(true).add(offset);
let maxDist = lastOut.point.distTo2D(center);
let useDist = Math.min(wipeDist, maxDist);
let endpoint = lastOut.point.followTo(center, useDist);
if (endpoint.inPolygon(lastPoly)) {
print.addOutput(lastLayer, endpoint, null, null, lastOut.tool);
}
}
}
}
);
print.setWidget(null);
lastOut = slice;
lastExt = lastOut.extruder;
lastPoly = slice.lastPoly;
lastLayer = layerout;
if (params.outputLayerRetract && layerout.length) {
layerout.last().retract = true;
}
}
// clear slice.prep so it can be re-previewed in a different mode
for (let widget of widgets) {
// skip synthesized support widget(s)
if (!widget.mesh) {
continue;
}
for (let slice of widget.slices) {
slice.prep = false;
}
}
// draft shield
if (layerno > 0 && shield && outputDraftShield) {
print.setType('shield');
shield = POLY.setZ(shield.clone(), printPoint.z);
let preout = [];
printPoint = poly2polyEmit(shield, printPoint, (poly, index, count, startPoint) => {
return print.polyPrintPath(poly, startPoint, preout, {
onfirst: function(point) {
if (preout.length && point.distTo2D(startPoint) > 2) {
// retract between part and shield
preout.last().retract = true;
}
}
});
});
preout.last().retract = true;
layerout.appendAll(preout);
}
// if a declared extruder isn't used in a layer, use selected
// extruder to fill the relevant purge blocks for later support
if (lastOut) track.slice().forEach(ext => {
printPoint = purge(ext.extruder, track, layerout, printPoint, lastOut.z, lastExt, lastOffset);
});
// if layer produced output, append to output array
if (layerout.length) {
output.append(layerout);
}
// retract after last layer
if (layerno === cake.length - 1 && layerout.length) {
layerout.last().retract = true;
}
// notify progress
layerout.layer = layerno++;
update((layerno / cake.length) * 0.5, "prepare");
layerout = [];
}
print.output = output;
print.firstTool = firstTool;
// post-process for base extrusions (touching the bed)
if (isBelt) {
// all widgets should have the same belt scaling constants
let belt = widgets[0].belt;
// tune base threshold
let thresh = Infinity;
for (let layer of output) {
for (let rec of layer) {
let point = rec.point;
thresh = Math.min(thresh, (belt.slope * point.z) - point.y);
}
}
// store this offset to be removed from Y values in export
print.belty = thresh;
thresh = Math.max(0, thresh) + firstLayerHeight * belt.slope;
// track last layer out b/c it might be connected to the current
// layer first point, in which case the injected brim will cause a long print line
let lastLayerOut;
// iterate over layers, find extrusion on belt and
// apply corrections and add brim when specified
for (let layer of output) {
let params = getRangeParameters(process, layer.layer || 0);
let brimHalf = params.firstLayerBrim < 0;
let firstLayerBrim = Math.abs(params.firstLayerBrim);
let firstLayerBrimIn = params.firstLayerBrimIn || 0;
let firstLayerBrimTrig = params.firstLayerBrimTrig || 0;
let firstLayerBrimGap = params.firstLayerBrimGap || 0;
let lastout, first = false;
let minz = Infinity, maxy = -Infinity, minx = Infinity, maxx = -Infinity;
let mins = Infinity;
let miny = Infinity;
let pads = [];
let overate = 0;
for (let rec of layer) {
overate = rec.overate >= 0 ? rec.overate : overate;
let brate = params.firstLayerRate || firstLayerRate;
let bmult = params.firstLayerPrintMult || params.firstLayerPrintMult;
let point = rec.point;
let belty = rec.belty = -point.y + (point.z * belt.slope);
let lowrate = belty <= thresh ? brate : overate || brate;
miny = Math.min(miny, belty);
if (layer.anchor) {
// apply base rate to entire anchor (including bump)
rec.speed = Math.min(rec.speed, lowrate);
}
if (rec.emit && belty <= thresh && lastout && Math.abs(lastout.belty - belty) < 0.01) {
// apply base speed to segments touching belt
rec.speed = params.firstLayerRate || Math.min(rec.speed, lowrate);
rec.emit *= bmult;
rec.fan = params.firstLayerFanSpeed;
minx = Math.min(minx, point.x, lastout.point.x);
maxx = Math.max(maxx, point.x, lastout.point.x);
maxy = Math.max(maxy, point.y);
minz = Math.min(minz, point.z);
first = rec;
// find length of shortest bed-facing segment
mins = Math.min(mins, lastout.point.distTo2D(rec.point));
// add to pads list if > 1mm long
if (point.x - lastout.point.x > 1) {
pads.push([lastout.point.x, point.x]);
}
}
lastout = rec;
}
// do not add brims to anchor layers
if (!first || layer.anchor) {
continue;
}
let tmpout = [];
let trigmet = firstLayerBrimTrig === 0 || (firstLayerBrimTrig && mins <= firstLayerBrimTrig);
let brimax = Math.max(firstLayerBrim, firstLayerBrimIn);
// add brim when all conditions met
if (brimax && trigmet) {
let { emit, tool } = first;
let y = maxy;
let z = minz;
let g = firstLayerBrimGap || 0;
let b = Math.max(firstLayerBrim, 1) + g;
let bi = Math.max(firstLayerBrimIn, 1) + g;
let lastLayerOut = layer.last();
lastLayerOut.retract = true;
// if routaround selected, move to belt before moving to brim start
if (process.outputAvoidGaps) {
let beltPoint = lastLayerOut.point.clone();
beltPoint.y = y;
print.addOutput(tmpout, beltPoint, 0, outputSeekrate, tool);
}
// outside brim
if (firstLayerBrim && !brimHalf) {
print.addOutput(tmpout, newPoint(maxx + b, y, z), 0, outputSeekrate, tool);
print.addOutput(tmpout, newPoint(maxx + g, y, z), emit, firstLayerRate, tool,{retract: true})
}
// inside brim
if (firstLayerBrimIn && pads.length > 1) {
let gaps = [];
let lpad = pads[0];
for (let pad of pads.slice(1)) {
let x0 = lpad[1];
let x1 = pad[0];
lpad = pad;
if (x1 - x0 > bi * 2) {
// over 2x brim so emit two segments
print.addOutput(tmpout, newPoint(x1 - g, y, z), 0, outputSeekrate, tool);
print.addOutput(tmpout, newPoint(x1 - bi, y, z), emit, firstLayerRate, tool, {retract: true});
print.addOutput(tmpout, newPoint(x0 + bi, y, z), 0, outputSeekrate, tool);
print.addOutput(tmpout, newPoint(x0 + g, y, z), emit, firstLayerRate, tool, {retract: true});
} else if (x1 - x0 > bi / 3) {
// over 1/3rd brim length emit single segment
print.addOutput(tmpout, newPoint(x1 - g, y, z), 0, outputSeekrate, tool);
print.addOutput(tmpout, newPoint(x0 + g, y, z), emit, firstLayerRate, tool, {retract: true});
}
}
}
// outside brim
if (firstLayerBrim) {
print.addOutput(tmpout, newPoint(minx - b, y, z), 0, outputSeekrate, tool);
print.addOutput(tmpout, newPoint(minx - g, y, z), emit, firstLayerRate, tool, {retract: true});
}
// when there is a printing move between layers
// inject non-printing move to after brim to prevent erroneous extrusion
if (layer[0].emit && lastLayerOut) {
print.addOutput(tmpout, layer[0].point.clone(), 0, outputSeekrate, tool);
}
layer.splice(0,0,...tmpout);
} else {
// for any layer touching belt, ensure start point is nearest origin
// print.addOutput(tmpout, newPoint(minx, maxy, minz), 0, outputSeekrate, first.tool);
// print.lastPoint = newPoint(minx, maxy, minz);
}
lastLayerOut = layer.peek();
}
}
// render if not explicitly disabled
if (settings.render !== false) {
await render.path(output, (progress, layer) => {
update(0.5 + progress * 0.5, "render", layer);
}, {
lineWidth: settings.process.sliceLineWidth,
toolMode: settings.pmode === 2,
tools: device.extruders,
thin: isThin,
flat: isFlat,
fdm: true
});
}
};
function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
const { settings } = print;
const { device } = settings;
const { bedWidth, bedDepth, bedRound } = device;
let preout = [],
process = opt.params || settings.process,
originCenter = device.originCenter || bedRound,
extruder = parseInt(slice.extruder || 0),
nozzleSize = process.sliceLineWidth || device.extruders[extruder].extNozzle,
firstLayer = (opt.first || false) && !opt.support,
thinWall = nozzleSize * (opt.thinWall || 1.75),
retractDist = opt.retractOver || (nozzleSize * 5),
fillMult = opt.mult || process.outputFillMult,
shellMult = opt.mult || process.outputShellMult || (process.ctSliceHeight >= 0 ? 1 : 0),
shellOrder = {"out-in":-1,"in-out":1,"alternate":-2}[process.sliceShellOrder] || -1,
sparseMult = process.outputSparseMult,
coastDist = process.outputCoastDist || 0,
finishSpeed = opt.speed || process.outputFinishrate,
firstShellSpeed = process.firstLayerRate,
firstFillSpeed = process.firstLayerFillRate,
firstPrintMult = process.firstLayerPrintMult,
printSpeed = opt.speed || (firstLayer ? firstShellSpeed : process.outputFeedrate),
fillSpeed = opt.speed || opt.fillSpeed || (firstLayer ? firstFillSpeed || firstShellSpeed : process.outputFeedrate),
infillSpeed = process.sliceFillRate || opt.infillSpeed || fillSpeed || printSpeed,
moveSpeed = process.outputSeekrate,
origin = startPoint.add(offset),
zhop = process.zHopDistance || 0,
antiBacklash = process.antiBacklash,
wipeDist = process.outputRetractWipe || 0,
isBelt = device.bedBelt,
bedOffset = originCenter ? {
x: 0,
y: 0,
z: 0
} : {
x: bedWidth/2,
y: isBelt ? 0 : bedDepth/2,
z: 0
},
beltFirst = process.outputBeltFirst || false,
startClone = startPoint.clone(),
seedPoint = opt.seedPoint || startPoint,
switchTop = print.lastPoly && print.lastPoly.perimeter() < process.outputShortPoly,
z = slice.z,
lastPoly;
// support alternating shell order
if (Math.abs(shellOrder) > 1 && slice.index % 2 === 1) {
shellOrder = -shellOrder;
}
if (slice.finishSolids) {
fillSpeed = process.sliceSolidRate || finishSpeed;
}
// apply first layer extrusion multipliers
if (firstLayer) {
fillMult *= firstPrintMult;
shellMult *= firstPrintMult;
sparseMult *= firstPrintMult;
}
function retract() {
let array = preout.length ? preout : output;
if (array.length) {
let last = array.last();
last.retract = true;
if (wipeDist && lastPoly && last.point) {
let center = lastPoly.center(true);
let maxDist = last.point.distTo2D(center);
let useDist = Math.min(wipeDist, maxDist);
let endpoint = last.point.followTo(center, useDist);
if (endpoint.inPolygon(lastPoly)) {
print.addOutput(array, endpoint, null, null, last.tool);
}
}
} else if (opt.pretract) {
opt.pretract(wipeDist);
} else {
console.log('unable to retract. no preout or output');
}
}
// return true if move path from p1 to p2 intersects a
// top and a path around the top (inside print) was not found
function intersectsTop(p1, p2) {
if (slice.index < 0) {
return false;
}
let int = false;
slice.topPolysFlat().forEach((poly) => {
if (!int) poly.forEachSegment((s1, s2) => {
if (util.intersect(p1,p2,s1,s2,base.key.SEGINT)) {
return int = true;
}
});
});
// if intersecting, look for a route around
if (int && opt.routeAround) {
return !routeAround(p1, p2);
}
return int;
}
// returns true if routed around or no retract requried
function routeAround(p1, p2) {
const dbug = false;
if (dbug === slice.index) console.log(slice.index, {p1, p2, d: p1.distTo2D(p2)});
let ints = [];
let tops = slice.topRouteFlat();
for (let poly of tops) {
poly.forEachSegment((s1, s2) => {
let ip = util.intersect(p1,p2,s1,s2,base.key.SEGINT);
if (ip) {
ints.push({ip, poly});
}
});
}
// no intersections
if (ints.length === 0) {
if (dbug === slice.index) console.log(slice.index, 'no ints');
return false;
}
// odd # of intersections ?!? do retraction
if (ints.length && ints.length % 2 !== 0) {
if (dbug === slice.index) console.log(slice.index, {odd_intersects: ints});
return false;
}
// sort by distance
ints.sort((a, b) => {
return a.ip.dist - b.ip.dist;
});
if (dbug === slice.index) console.log(slice.index, {ints});
// check pairs. eliminate too close points.
// pairs must intersect same poly or retract.
for (let i=0; i<ints.length; i += 2) {
let i1 = ints[i];
let i2 = ints[i+1];
// different poly. force retract
if (i1.poly !== i2.poly) {
if (dbug === slice.index) console.log(slice.index, {int_diff_poly: ints, i});
return false;
}
// mark invalid intersect pairs (low or zero dist, etc)
// TODO: only if this is the outer pair and there are closer inner pairs
if (i1.ip.distTo2D(i2.ip) < retractDist) {
if (dbug === slice.index) console.log(slice.index, {int_dist_too_small: i1.ip.distTo2D(i2.ip), retractDist});
ints[i] = undefined;
ints[i+1] = undefined;
}
}
// filter out invalid intersection pairs
ints = ints.filter(i => i);
if (ints.length > 2) {
if (dbug === slice.index) console.log(slice.index, {complex_route: ints.length});
return false;
}
if (ints.length === 2) {
// can route around intersected top polys
for (let i=0; i<ints.length; i += 2) {
let i1 = ints[0];
let i2 = ints[1];
// output first point
print.addOutput(preout, i1.ip, 0, moveSpeed, extruder);
// create two loops around poly
// find shortest of two paths and emit poly points
let poly = i1.poly;
let isCW = poly.isClockwise();
let points = poly.points;
let p1p = isCW ? points : points.slice().reverse(); // CW
let p2p = isCW ? points.slice().reverse() : points; // CCW
let r1s = p1p.indexOf(isCW ? i1.ip.p2 : i1.ip.p1);
let r1e = p1p.indexOf(isCW ? i2.ip.p1 : i2.ip.p2);
let r1 = r1s === r1e ?
[ p1p[r1s] ] : r1s < r1e ?
[ ...p1p.slice(r1s,r1e+1) ] :
[ ...p1p.slice(r1s), ...p1p.slice(0,r1e+1) ];
let r1d = 0;
for (let i=1; i<r1.length; i++) {
r1d += r1[i-1].distTo2D(r1[i]);
}
let r2s = p2p.indexOf(isCW ? i1.ip.p1 : i1.ip.p2);
let r2e = p2p.indexOf(isCW ? i2.ip.p2 : i2.ip.p1);
let r2 = r2s === r2e ?
[ p2p[r2s] ] : r2s < r2e ?
[ ...p2p.slice(r2s,r2e+1) ] :
[ ...p2p.slice(r2s), ...p2p.slice(0,r2e+1) ];
let r2d = 0;
for (let i=1; i<r2.length; i++) {
r2d += r2[i-1].distTo2D(r2[i]);
}
let route = r1d <= r2d ? r1 : r2;
if (dbug === slice.index) console.log(slice.index, {
ints: ints.map(i=>i.ip.dist),
i1, i2, same: i1.poly === i2.poly,
route,
p1, p2, dist: p1.distTo2D(p2),
r1, r1d, r1s, r1e,
r2, r2d, r2s, r2e,
isCW});
for (let p of route) {
print.addOutput(preout, p, 0, moveSpeed, extruder);
}
// output last point
print.addOutput(preout, i2.ip, 0, moveSpeed, extruder);
}
return true;
}
return false;
}
// solid infill
function outputTraces(poly, opt = {}) {
if (!poly) return;
if (Array.isArray(poly)) {
if (opt.sort) {
let polys = poly.slice().sort((a,b) => {
return (a.perimeter() - b.perimeter()) * opt.sort;
});
let debug = polys.length > 3;
let last;
while (polys.length) {
let next;
for (let p of polys) {
if (!last) {
next = p;
break;
}
if (opt.sort > 0) {
// in-out
if (last.isInside(p)) {
next = p;
break;
}
} else {
// out-in
if (p.isInside(last)) {
next = p;
break;
}
}
}
if (next) {
last = next;
polys.remove(next);
outputTraces(next, opt);
} else {
last = null;
}
}
} else {
outputOrderClosest(poly, function(next) {
outputTraces(next, opt);
}, null);
}
} else {
let finishShell = poly.depth === 0 && !firstLayer;
startPoint = print.polyPrintPath(poly, startPoint, preout, {
ccw: opt.shell && process.outputAlternating && slice.index % 2,
tool: extruder,
rate: finishShell ? finishSpeed : printSpeed,
accel: finishShell,
wipe: process.outputWipeDistance || 0,
coast: firstLayer ? 0 : coastDist,
extrude: numOrDefault(opt.extrude, shellMult),
onfirst: function(firstPoint) {
let from = seedPoint || startPoint;
if (from.distTo2D(firstPoint) > retractDist) {
if (intersectsTop(from, firstPoint)) {
retract();
}
}
seedPoint = null;
}
});
lastPoly = slice.lastPoly = poly;
}
}
/**
* output sparse infill
* @param {Polygon[]} polys
*/
function outputSparse(polys, extrude, speed) {
if (!polys) return;
let proxy = polys.map((poly) => {
return {poly: poly, first: poly.first(), last: poly.last()};
});
let lp = startPoint;
startPoint = tip2tipEmit(proxy, startPoint, (el, point, count) => {
let poly = el.poly;
if (poly.last() === point) {
poly.reverse();
}
poly.forEachPoint((p, i) => {
let dist = lp.distTo2D(p);
let rdst = dist > retractDist;
let itop = rdst && intersectsTop(lp,p);
let emit = extrude;
// retract if dist trigger and crosses a slice top polygon
if (i === 0) {
if (itop) {
retract();
emit = 0;
} else if (dist > nozzleSize) {
emit = 0;
}
}
// let emit = i === 0 ? 0 : extrude;
// handle shallow cloned infill
if (poly.z !== undefined) {
p = p.clone().setZ(poly.z);
}
print.addOutput(preout, p, emit, speed || printSpeed, extruder);
lp = p;
}, !poly.open);
return lp;
});
}
/**
* convert cross-hatch thin-fill into lines by connecting short segment mid-points
*/
function outputThin(lines) {
if (!lines) {
return;
}
let points = lines.group(2).map(grp => {
let [ p1, p2 ] = grp;
return newPoint(
(p1.x + p2.x) / 2,
(p1.y + p2.y) / 2,
(p1.z + p2.z) / 2
)
});
let order = util.orderClosest(points, (p1, p2) => p1.distTo2D(p2));
if (order.length === 0) {
return;
}
let first = points[0];
let last = first;
if (startPoint && startPoint.distTo2D(first) > thinWall && intersectsTop(startPoint, first)) {
retract();
}
print.addOutput(preout, first, 0, moveSpeed, extruder);
for (let p of order) {
let dist = last ? last.distTo2D(p) : 0;
if (dist > thinWall) {
retract();
print.addOutput(preout, p, 0, moveSpeed, extruder);
}
print.addOutput(preout, p, 1, fillSpeed, extruder);
startPoint = last = p;
}
// close a circle
if (last && last.distTo2D(first) <= thinWall) {
print.addOutput(preout, first, 1, fillSpeed, extruder);
startPoint = first;
}
}
/**
* output solid infill
* @param {*} lines
* @param {*} opt
*/
function outputFills(lines, opt = {}) {
if (!lines || lines.length === 0) {
return;
}
let p, p1, p2, dist, len, found, group, mindist, t1, t2,
marked = 0,
start = 0,
skip = false,
lastIndex = -1,
raft = opt.raft || false,
flow = opt.flow || 1,
near = opt.near || (antiBacklash ? false : true),
fast = opt.fast || false, // support infill only!
fill = (opt.fill >= 0 ? opt.fill : fillMult) * flow,
thinDist = near ? thinWall : thinWall;
// continue until all lines in array are "marked" as used
while (lines && marked < lines.length) {
group = null;
found = false;
mindist = Infinity;
// use next nearest line endpoint strategy
if (near)
for (let i=0; i<lines.length; i += 2) {
t1 = lines[i];
if (t1.del) {
continue;
}
t2 = lines[i+1];
let d1 = t1.distToSq2D(startPoint);
let d2 = t2.distToSq2D(startPoint);
// penalize next nearest point if it's not on an adjacent line
let idiff = startPoint.index ? Math.abs(startPoint.index - t1.index) : 1;
let min = Math.min(d1, d2) * (idiff !== 1 ? 10 : 1);
if (min < mindist) {
if (d2 < d1) {
p2 = t1;
p1 = t2;
} else {
p1 = t1;
p2 = t2;
}
mindist = min;
}
}
// use next line index strategy (perpendicular offset from origin)
if (!near)
for (let 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;
}
}
// go back to start and try again
if (!near && !found) {
if (start === 0 && lastIndex === -1) {
console.log('infinite loop', lines, {
marked, i, group, start, lastIndex,
points: lines.map(p => p.index).join(', ')
});
break;
}
start = 0;
lastIndex = -1;
continue;
}
dist = startPoint.distToSq2D(p1);
len = p1.distToSq2D(p2);
// go back to start when dist > retractDist
if (!near && !fast && !skip && dist > retractDist) {
skip = true;
start = 0;
lastIndex = -1;
continue;
}
skip = false;
// re-seek a new start index within fill array
let restart = lastIndex < 0;
// mark as used
p1.del = true;
p2.del = true;
marked += 2;
lastIndex = p1.index;
// 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 <= thinDist && len <= thinDist) {
p2 = p1.midPointTo(p2);
// this.addOutput(preout, p2, fill * (dist / thinWall), fillSpeed, extruder);
print.addOutput(preout, p2, fill, fillSpeed, extruder);
} else {
// retract if dist trigger or crosses a slice top polygon
if (!fast && dist > retractDist && (zhop || intersectsTop(startPoint, p1))) {
retract();
}
// anti-backlash on longer move
if (!fast && antiBacklash && dist > retractDist) {
print.addOutput(preout, p1.add({x:antiBacklash,y:-antiBacklash,z:0}), 0, moveSpeed, extruder);
}
// bridge ends of fill when they're close together
if (dist < thinDist) {
print.addOutput(preout, p1, fill, fillSpeed, extruder);
} else if (raft && !restart) {
// connect raft lines unless it's a restart
print.addOutput(preout, p1, fill, fillSpeed, extruder);
} else {
print.addOutput(preout, p1, 0, moveSpeed, extruder);
}
print.addOutput(preout, p2, fill, fillSpeed, extruder);
}
startPoint = p2;
}
// clear delete marks so we can re-print later
if (lines) lines.forEach(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.
*
* @param {Array} array of Polygons or Polygon wrappers (tops)
* @param {Function} fn call to emit next candidate
* @param {Function} fnp convert 'next' object into a Polygon for closeness
*/
function outputOrderClosest(array, fn, fnp) {
if (array.length === 1) {
return fn(array[0]);
}
array = array.slice();
let closest, find, next, order, poly, lastDepth = 0;
for (;;) {
order = [];
closest = null;
for (let 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),
});
}
if (order.length === 0) {
return;
}
// when previous top is under a certain length
// seek furthest top on layer switch
if (switchTop) {
switchTop = false;
order.sort((a,b) => {
return b.d - a.d;
});
} else {
order.sort((a,b) => {
return a.d - b.d;
});
}
array[order[0].i] = null;
fn(order[0].n);
}
}
let out = [];
if (slice.tops) {
out.appendAll(slice.tops);
}
if (opt.support && slice.supports) {
out.appendAll(slice.supports);
}
let lastTop = null;
outputOrderClosest(out, function(next) {
if (next instanceof Polygon) {
print.setType('support');
// support polygon
next.setZ(z);
if (process.sliceSupportOutline !== false) {
outputTraces([next].appendAll(next.inner || []));
}
if (next.fill) {
next.fill.forEach(p => { p.z = z });
outputFills(next.fill, {fast: true, near: true});
}
} else {
// solid infill
let top = next;
let isRaft = top.isRaft;
print.setType('shells');
if (lastTop && lastTop !== next) {
retract();
}
// control of layer start point
switch (process.sliceLayerStart) {
case "last":
break;
case "random":
let bounds = top.poly.bounds;
let center = bounds.center();
let radius = Math.max(bounds.width(), bounds.height());
let angle = Math.random()*Math.PI*2;
let x = center.x + Math.cos(angle)*radius;
let y = center.y + Math.sin(angle)*radius;
startPoint = newPoint(x,y,startPoint.z);
break;
case "center":
startPoint = newPoint(0,0,startPoint.z);
break;
case "origin":
startPoint = newPoint(-bedOffset.x, -bedOffset.y, startPoint.z);
break;
case "position":
startPoint = newPoint(-bedOffset.x+process.sliceLayerStartX, -bedOffset.y+process.sliceLayerStartY, startPoint.z);
break;
}
// optimize start point on belt for tops touching belt
// and enforce optimal shell order (outer first)
if (isBelt && opt.onBelt) {
startPoint = startClone;
if (beltFirst) {
shellOrder = -1;
}
}
// raft
if (top.traces) outputTraces(top.traces);
// innermost shells
let inner = next.innerShells() || [];
let shells = next.shells || [];
// alternating winding option
// if (process.outputAlternating || slice.index % 2) {
// console.log({alternating: slice, shells, inner});
// POLY.setWinding(inner, false);
// POLY.setWinding(shells, false);
// }
// output inner polygons
if (shellOrder > 0) outputTraces(inner, { sort: shellOrder, shell: true });
outputTraces(shells, { sort: shellOrder, shell: true });
// output outer polygons
if (shellOrder < 0) outputTraces(inner, { sort: shellOrder, shell: true });
// output thin fill
print.setType('thin fill');
outputThin(next.thin_fill);
// then output solid and sparse fill
print.setType('solid fill');
outputFills(next.fill_lines, { flow: fillMult, raft: isRaft });
print.setType('sparse infill');
outputSparse(next.fill_sparse, sparseMult, infillSpeed);
lastTop = next;
}
}, function(obj) {
// for tops, return the wrapped poly
return obj instanceof Polygon ? obj : obj.poly;
});
// produce polishing paths when present
if (slice.tops && slice.tops.length && slice.tops[0].polish) {
let {x,y} = slice.tops[0].polish;
if (x) {
outputSparse(x, 0, process.polishSpeed);
}
if (y) {
outputSparse(y, 0, process.polishSpeed);
}
}
// offset print points
for (let i=0; i<preout.length; i++) {
preout[i].point = preout[i].point.add(offset);
}
// add offset points to total print
print.addPrintPoints(preout, output, origin, extruder);
return startPoint.add(offset);
}