1074 lines
36 KiB
JavaScript
1074 lines
36 KiB
JavaScript
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
|
|
|
import { tip2tipEmit, poly2polyEmit } from '../../../../geo/paths.js';
|
|
import { newPoint } from '../../../../geo/point.js';
|
|
import { polygons as POLY } from '../../../../geo/polygons.js';
|
|
import { render } from '../../../core/render.js';
|
|
import { newPrint } from '../../../core/print.js';
|
|
import { Tool } from '../core/tool.js';
|
|
import { newPolygon } from '../../../../geo/polygon.js';
|
|
|
|
const debug = false;
|
|
const debug_push = false;
|
|
const CLOSEST_TO_PP = -999;
|
|
|
|
/**
|
|
* DRIVER PRINT CONTRACT
|
|
*
|
|
* @param {Object} print state object
|
|
* @param {Function} update incremental callback
|
|
* @param {Number} [index] into widget array
|
|
* @param {Object} [firstPoint] starting point
|
|
*/
|
|
export async function cam_prepare(widgets, settings, update) {
|
|
const active = widgets
|
|
.filter(w => !w.isSynth() && !w.track.ignore && !w.meta.disabled)
|
|
.filter(w => w?.camops.length)
|
|
;
|
|
const count = active.length;
|
|
const weight = 1 / count;
|
|
const print = self.kiri_worker.current.print = newPrint(settings, active);
|
|
const { order, origin } = settings;
|
|
|
|
// wait for safe eval setup
|
|
await print.ready();
|
|
|
|
// cam-specific storage
|
|
print.output = [];
|
|
|
|
// sort output by distance to origin
|
|
if (order) {
|
|
active.sort((a,b) => {
|
|
return (order[a.id] ?? Infinity) - (order[b.id] ?? Infinity);
|
|
});
|
|
} else if (origin) {
|
|
let point = newPoint().move(origin);
|
|
active.sort((w0,w1) =>
|
|
newPoint().move(w0.track.pos).distTo2D(point) -
|
|
newPoint().move(w1.track.pos).distTo2D(point)
|
|
);
|
|
}
|
|
|
|
let index = 0;
|
|
let startPoint;
|
|
for (let widget of active) {
|
|
startPoint = await prepare_one(widget, settings, print, startPoint, (progress, msg) => {
|
|
update((index * weight + progress * weight) * 0.75, msg || "prepare");
|
|
});
|
|
index++;
|
|
}
|
|
|
|
// prune empty levels
|
|
const output = print.output.filter(level => Array.isArray(level));
|
|
|
|
// compute path display
|
|
return render.path(
|
|
output,
|
|
(progress, layer) => {
|
|
update(0.75 + progress * 0.25, "render", layer);
|
|
}, {
|
|
thin: true,
|
|
print: 0,
|
|
move: 0x557799,
|
|
speed: false,
|
|
moves: true,
|
|
other: "moving",
|
|
action: "milling",
|
|
// maxspeed: settings.process.camFastFeed || 6000
|
|
}
|
|
);
|
|
};
|
|
|
|
// process `prepare` paths for a single widget
|
|
export async function prepare_one(widget, settings, print, firstPoint, update) {
|
|
|
|
let { device, process, stock: set_stock } = settings,
|
|
{ center } = set_stock,
|
|
{ alignTop } = settings.controller,
|
|
{ camArcEnabled, camArcResolution, camArcTolerance } = process,
|
|
{ camDepthFirst, camEaseAngle, camEaseDown } = process,
|
|
{ camFastFeed, camFastFeedZ, camZTop } = process,
|
|
{ camStockX, camStockY, camStockZ, camStockIndexed, camStockOffset } = process,
|
|
{ camForceZMax, camFullEngage, camInnerFirst, camOriginCenter } = process,
|
|
{ camOriginOffX, camOriginOffY, camOriginOffZ, camZClearance } = process,
|
|
bounds = widget.getBoundingBox(),
|
|
stock = camStockOffset ? {
|
|
x: bounds.dim.x + camStockX,
|
|
y: bounds.dim.y + camStockY,
|
|
z: bounds.dim.z + camStockZ,
|
|
} : {
|
|
x: camStockX,
|
|
y: camStockY,
|
|
z: camStockZ
|
|
},
|
|
stockZ = stock.z * (camStockIndexed ? 0.5 : 1),
|
|
stockZClear = stockZ + camZClearance,
|
|
widgetTrackTop = widget.track.top,
|
|
widgetTopToStock = stockZ - widgetTrackTop,
|
|
boundsZ = camStockIndexed ? stock.z / 2 : bounds.max.z + widgetTopToStock,
|
|
wmpos = widget.track.pos,
|
|
wmx = wmpos.x,
|
|
wmy = wmpos.y,
|
|
wmz = !camStockIndexed ? stock.z - boundsZ : alignTop ? 0 : 0,
|
|
zSafe = Math.max(camZTop, camStockIndexed ? Math.hypot(stock.y, stock.z) / 2 + camZClearance : stockZClear),
|
|
originx = (camOriginCenter ? 0 : -stock.x / 2) + (camOriginOffX || 0),
|
|
originy = (camOriginCenter ? 0 : -stock.y / 2) + (camOriginOffY || 0),
|
|
origin = newPoint(originx, originy, zSafe),
|
|
coastline,
|
|
contouring = false,
|
|
currentOp,
|
|
drillDown = 0,
|
|
drillLift = 0,
|
|
drillDwell = 0,
|
|
feedRate,
|
|
isLathe,
|
|
isIndex,
|
|
layerOut = [],
|
|
lasering = false,
|
|
laserPower = 0,
|
|
lastOp,
|
|
lastTool,
|
|
lastTravelBounds,
|
|
newOutput = print.output || [],
|
|
nextIsMove = true,
|
|
nextIsNewOp = false,
|
|
plungeRate = camFastFeedZ,
|
|
printPoint,
|
|
tool,
|
|
toolType,
|
|
toolDiam,
|
|
toolDiamMove,
|
|
travelBounds,
|
|
spindle = 0,
|
|
spindleMax = device.spindleMax,
|
|
tolerance = 0,
|
|
easeThrottle = (90 - Math.min(90, camEaseAngle)) / 180,
|
|
easeDzPerMm = Math.tan(camEaseAngle * Math.PI / 180);
|
|
|
|
if (debug) console.log({ zSafe, wmx, wmy, wmz });
|
|
|
|
// function d(o) {
|
|
// console.log('<-------------------');
|
|
// for (let [k,v] of Object.entries(o)) console.log(k,v);
|
|
// console.log('------------------->');
|
|
// }
|
|
|
|
// d({
|
|
// stock,
|
|
// sstck: set_stock,
|
|
// center,
|
|
// origin,
|
|
// wmx,
|
|
// wmy
|
|
// });
|
|
|
|
function newLayer(op) {
|
|
if (layerOut.length || layerOut.mode) {
|
|
newOutput.push(layerOut);
|
|
}
|
|
layerOut = [];
|
|
layerOut.mode = op || currentOp;
|
|
layerOut.spindle = spindle;
|
|
}
|
|
|
|
function addGCode(text) {
|
|
if (!(text && text.length)) {
|
|
return;
|
|
}
|
|
if (!Array.isArray(text)) {
|
|
text = text.trim().split('\n');
|
|
}
|
|
newOutput.push([{ gcode: text }]);
|
|
if (layerOut.length) {
|
|
layerOut = [];
|
|
layerOut.mode = currentOp;
|
|
layerOut.spindle = spindle;
|
|
}
|
|
}
|
|
|
|
function setContouring(bool, step, coast) {
|
|
coastline = coast;
|
|
contouring = bool;
|
|
toolDiamMove = step ?? tool.getStepSize(currentOp.step) * 2;
|
|
if (bool) setTravelBoundary();
|
|
}
|
|
|
|
function setSpindle(speed) {
|
|
spindle = Math.min(speed, spindleMax);
|
|
}
|
|
|
|
function setTolerance(dist) {
|
|
tolerance = dist;
|
|
}
|
|
|
|
function getTool() {
|
|
return tool;
|
|
}
|
|
|
|
function setTool(toolID, feed = camFastFeed, plunge = camFastFeedZ) {
|
|
if (toolID !== lastTool) {
|
|
tool = new Tool(settings, toolID);
|
|
toolType = tool.getType();
|
|
toolDiam = tool.fluteDiameter();
|
|
toolDiamMove = (tool.hasTaper() ? tolerance ?? toolDiam : toolDiam) * 2;
|
|
lastTool = toolID;
|
|
}
|
|
feedRate = Math.min(camFastFeed, feed || feedRate || plunge);
|
|
plungeRate = Math.min(camFastFeed, feedRate || plunge, plunge || plungeRate || feedRate);
|
|
if (debug) console.log({ setTool: toolID, feed, plunge, plungeRate });
|
|
}
|
|
|
|
function setLasering(bool, power = 0) {
|
|
lasering = bool ? currentOp : undefined;
|
|
laserPower = power;
|
|
}
|
|
|
|
function setDrill(down, lift, dwell) {
|
|
drillDown = down;
|
|
drillLift = lift;
|
|
drillDwell = dwell;
|
|
}
|
|
|
|
function emitDrills(polys) {
|
|
polys = polys.slice();
|
|
for (; ;) {
|
|
let closestDist = Infinity,
|
|
closestI,
|
|
closest = null,
|
|
dist;
|
|
|
|
for (let i = 0; i < polys.length; i++) {
|
|
if (!polys[i]) continue;
|
|
if ((dist = polys[i].first().distTo2D(printPoint)) < closestDist) {
|
|
closestDist = dist;
|
|
closest = polys[i];
|
|
closestI = i;
|
|
}
|
|
}
|
|
|
|
if (!closest) return;
|
|
polys[closestI] = null;
|
|
emitDrill(closest, drillDown, drillLift, drillDwell);
|
|
}
|
|
}
|
|
|
|
function emitDrill(poly, down, lift, dwell) {
|
|
let remain = poly.first().z - poly.last().z,
|
|
points = [],
|
|
point = poly.first();
|
|
if (down <= 0) {
|
|
down = remain;
|
|
}
|
|
for (; ;) {
|
|
if (remain > down * 2) {
|
|
points.push(point.clone());
|
|
point.z -= down;
|
|
remain -= down;
|
|
} else if (remain < down) {
|
|
points.push(point.clone());
|
|
point.z -= remain;
|
|
points.push(point.clone());
|
|
break;
|
|
} else {
|
|
points.push(point.clone());
|
|
point.z -= remain / 2;
|
|
points.push(point.clone());
|
|
point.z -= remain / 2;
|
|
points.push(point.clone());
|
|
break;
|
|
}
|
|
}
|
|
setNextIsMove();
|
|
points.forEach(function (point, index) {
|
|
newLayer();
|
|
camOut(point);
|
|
if (index > 0 && index < points.length - 1) {
|
|
newLayer();
|
|
if (dwell) camDwell(dwell);
|
|
if (lift) camOut(point.clone().setZ(point.z + lift), 0);
|
|
}
|
|
newLayer();
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @param {Point} point
|
|
* @param {number} emit (0=move, 1=/laser on/cut mode)
|
|
* @param {number} [speed] feed/plunge rate in mm/min
|
|
* @param {number} [tool] tool number
|
|
*/
|
|
function layerPush(point, emit, speed, tool, options) {
|
|
const { type, center } = options ?? {};
|
|
if (debug_push && options?.type !== 'lerp') {
|
|
let rounded = [point.x,point.y,point.z,point.a??0].map(v => v.toFixed(3));
|
|
if (rounded.filter(v => isNaN(v)).length) console.trace('NaN');
|
|
console.log(
|
|
currentOp.type,
|
|
emit | 0,
|
|
speed | 0,
|
|
...rounded
|
|
);
|
|
}
|
|
layerOut.mode = currentOp;
|
|
if (lasering) {
|
|
let power = emit ? laserPower : 0;
|
|
if (emit && lasering.adapt) {
|
|
let { minz, maxz, minp, maxp, adaptrp } = lasering;
|
|
maxz = maxz || widgetTrackTop;
|
|
let deltaz = maxz - minz;
|
|
let { z } = point;
|
|
if (adaptrp) {
|
|
while (z > maxz) z -= deltaz;
|
|
while (z < minz) z += deltaz;
|
|
} else if (z < minz || z > maxz) {
|
|
// skip outside of band
|
|
return point;
|
|
}
|
|
z -= minz;
|
|
if (minp < maxp) {
|
|
power = minp + (z / deltaz) * (maxp - minp);
|
|
} else {
|
|
power = minp - (z / deltaz) * (minp - maxp);
|
|
}
|
|
}
|
|
if (lasering.flat) {
|
|
point.z = (stock && stock.z ? stock.z : widgetTrackTop) + lasering.flatz;
|
|
}
|
|
print.addOutput(layerOut, point, power, speed, tool, { type: 'laser' });
|
|
} else {
|
|
print.addOutput(layerOut, point, emit, speed, tool, { type, center });
|
|
}
|
|
printPoint = (point ?? printPoint).clone();
|
|
return point;
|
|
}
|
|
|
|
function camDwell(time) {
|
|
layerPush(
|
|
null,
|
|
0,
|
|
time,
|
|
tool
|
|
);
|
|
}
|
|
|
|
function setNextIsMove() {
|
|
nextIsMove = true;
|
|
}
|
|
|
|
function setChangeOp() {
|
|
nextIsNewOp = true;
|
|
}
|
|
|
|
/**
|
|
* Move a point by the widget's movement offset.
|
|
* @param {Point} p - point to move
|
|
* @return {Point} new point with offset applied
|
|
*/
|
|
function toWorkCoords(p) {
|
|
return newPoint(
|
|
p.x + wmx,
|
|
p.y + wmy,
|
|
p.z + wmz
|
|
)
|
|
.setA(p.a ?? printPoint?.a)
|
|
.annotate({ slice: p.slice });
|
|
}
|
|
|
|
function toWidgetCoords(p) {
|
|
return newPoint(
|
|
p.x - wmx,
|
|
p.y - wmy,
|
|
p.z - wmz
|
|
)
|
|
.setA(p.a)
|
|
.annotate({ slice: p.slice });
|
|
}
|
|
|
|
/**
|
|
* when moving between contour endpoints, check if we can
|
|
* instead route around the bounding area of the contour
|
|
* whih we call the coastline.
|
|
*/
|
|
function coastlineMove(point) {
|
|
let from = toWidgetCoords(printPoint);
|
|
let to = toWidgetCoords(point);
|
|
if (!coastline || from.distTo2D(to) < 0.01) {
|
|
return false;
|
|
}
|
|
let start = { dist: 1, poly: 0, pt: from };
|
|
let end = { dist: 1, poly: 1, pt: to };
|
|
for (let poly of coastline) {
|
|
let { points } = poly;
|
|
for (let i=0; i<points.length; i++) {
|
|
let pt = points[i];
|
|
let dist = from.distTo2D(pt);
|
|
if (dist < start.dist) {
|
|
start.dist = dist;
|
|
start.poly = poly;
|
|
start.pos = i;
|
|
start.mp = pt;
|
|
}
|
|
dist = to.distTo2D(pt);
|
|
if (dist < end.dist) {
|
|
end.dist = dist;
|
|
end.poly = poly;
|
|
end.pos = i;
|
|
end.mp = pt;
|
|
}
|
|
}
|
|
}
|
|
if (start.poly !== end.poly || start.pos === end.pos) {
|
|
return false;
|
|
}
|
|
let { poly } = start;
|
|
let { points } = poly;
|
|
let pl = poly.length;
|
|
let sp = start.pos, ep = end.pos;
|
|
let adist = Math.abs(ep - sp);
|
|
let bdist = ep > sp ?
|
|
sp + (pl - ep):
|
|
ep + (pl - sp);
|
|
let dir = 1;
|
|
let dist;
|
|
if (adist < bdist) {
|
|
dist = adist;
|
|
if (ep < sp) {
|
|
dir = -1;
|
|
}
|
|
} else {
|
|
dist = bdist;
|
|
if (ep < sp) {
|
|
ep += pl;
|
|
} else {
|
|
sp += pl;
|
|
dir = -1;
|
|
}
|
|
}
|
|
for (let i=sp, d=0; d < dist; i += dir, d++) {
|
|
layerPush(toWorkCoords(points[i % pl]), 1, 0, tool);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* emit a cut or move operation from the current location to a new location
|
|
* @param {Point} point destination for move in widget coordinate space
|
|
* @param {-1|0|1|2|3} emit ignore, G0, G1, G2, G3
|
|
* @param {number} opts.shortCut used to convert short moves to cuts
|
|
* @param {number} opts.factor speed scale factor
|
|
* @return {Point} translated emitted point
|
|
*/
|
|
function camOut(point, emit = 1, opts) {
|
|
let lop = lastOp;
|
|
lastOp = currentOp;
|
|
|
|
// translate widget point into workspace coordinates
|
|
let point_in = point;
|
|
point = toWorkCoords(point);
|
|
|
|
let {
|
|
center,
|
|
factor = 1,
|
|
feed = feedRate,
|
|
shortCut = toolDiamMove,
|
|
moveOnly = false,
|
|
} = opts ?? {};
|
|
let pointA = point.a;
|
|
let rate = feed * factor;
|
|
|
|
// on operation changes:
|
|
// 1. move to safe z of current point preserving angle
|
|
// 2. move to safe z of new point preserving old angle
|
|
// 3. move to safe z of new point with new angle
|
|
if (nextIsNewOp || lop !== currentOp) {
|
|
layerPush(printPoint.clone().setZ(zSafe).setA(printPoint.a), 0, feedRate, tool);
|
|
layerPush(point.clone().setZ(zSafe).setA(printPoint.a), 0, feedRate, tool);
|
|
layerPush(point.clone().setZ(zSafe), 0, feedRate, tool);
|
|
newLayer();
|
|
nextIsNewOp = false;
|
|
}
|
|
|
|
// consume forced next move flag and convert to move
|
|
// this is usually set right before a `polyEmit`
|
|
if (nextIsMove) {
|
|
emit = 0;
|
|
nextIsMove = false;
|
|
}
|
|
|
|
// carry rotation forward when not overridden
|
|
if (pointA !== undefined && printPoint.a !== undefined) {
|
|
let DA = point.a - printPoint.a;
|
|
let MaxZ = Math.max(printPoint.z, point.z);
|
|
// find rotary arc length
|
|
let arcLen = (Math.abs(DA) / 360) * (2 * Math.PI * MaxZ);
|
|
let steps = Math.ceil(arcLen);
|
|
// emit interpolated points between printPoint and point
|
|
if (steps > 2) {
|
|
// if (point.a === 0 || printPoint.a === 0) console.log({ from: printPoint.clone(), to: point.clone() });
|
|
let zStep = (point.z - printPoint.z) / steps;
|
|
let aStep = DA / steps;
|
|
let lp = printPoint.clone();
|
|
newLayer();
|
|
// create interpolated point set for rendering and animation
|
|
while (--steps > 0) {
|
|
lp.z += zStep;
|
|
lp.a += aStep;
|
|
// if (false)
|
|
layerPush(
|
|
lp.clone(),
|
|
emit,
|
|
rate,
|
|
tool,
|
|
{ type: "lerp" },
|
|
);
|
|
}
|
|
newLayer();
|
|
}
|
|
}
|
|
|
|
// measure deltas from last point in XY and Z
|
|
let deltaXY = printPoint.distTo2D(point),
|
|
deltaZ = point.z - printPoint.z,
|
|
absDeltaZ = Math.abs(deltaZ),
|
|
isMove = (emit === 0 || emit === false),
|
|
hasBounds = (travelBounds || lastTravelBounds),
|
|
upAndOver = false;
|
|
|
|
// contouring logic
|
|
if (isMove && contouring) {
|
|
if (coastline && deltaXY < 5 && coastlineMove(point)) {
|
|
// console.log('coastline move');
|
|
} else if (deltaXY > toolDiamMove) {
|
|
upAndOver = true;
|
|
} else if (absDeltaZ < 0.01) {
|
|
if (debug) console.log('contour move as cut');
|
|
emit = 1;
|
|
} else if (absDeltaZ < 0.001) {
|
|
if (debug) console.log('contour up for travel');
|
|
layerPush(printPoint.clone().move({ z: 0.1 }), 0, 0, tool);
|
|
layerPush(point.clone().move({ z: 0.1 }), 0, 0, tool);
|
|
}
|
|
} else
|
|
// when rapid pluge could cut thru stock:
|
|
// * rapid to just above stock
|
|
// * continue plunge as cut
|
|
if (isMove && deltaZ < 0 && printPoint.z > stockZ && point.z < stockZ) {
|
|
if (debug) console.log('detected plunge cut as rapid move', printPoint.z, point.z);
|
|
layerPush(point.clone().setZ(zSafe), 0, 0, tool);
|
|
// change to cutting move for remainder of plunge
|
|
newLayer();
|
|
} else
|
|
// convert short planar moves to cuts when not lasering
|
|
if (isMove && !hasBounds && deltaXY <= shortCut && deltaZ <= 0 && !lasering) {
|
|
// but only if the z plunge is not too far
|
|
if (absDeltaZ < 0.01 || (tolerance > 0 && absDeltaZ <= tolerance)) {
|
|
if (debug) console.log('shortcut', { deltaXY, deltaZ, shortCut });
|
|
emit = 1;
|
|
} else
|
|
// otherwise move over before descending
|
|
if (deltaZ <= -tolerance) {
|
|
if (debug) console.log('over before descend', deltaZ, -tolerance);
|
|
layerPush(point.clone().setZ(printPoint.z), 0, 0, tool);
|
|
newLayer();
|
|
}
|
|
} else
|
|
// when moving in lathe mode ...
|
|
if (isMove && isLathe) {
|
|
if (point.z > printPoint.z) {
|
|
layerPush(printPoint.clone().setZ(point.z), 0, 0, tool);
|
|
newLayer();
|
|
} else if (point.z < printPoint.z) {
|
|
layerPush(point.clone().setZ(printPoint.z), 0, 0, tool);
|
|
newLayer();
|
|
}
|
|
} else
|
|
// check move against a known boundary (pocketing)
|
|
if (isMove && hasBounds) {
|
|
// travel boundary "hangover" from last area op when traveling between
|
|
let check = [];
|
|
if (travelBounds) check.push(...travelBounds);
|
|
if (lastTravelBounds) check.push(...lastTravelBounds);
|
|
let from = toWidgetCoords(printPoint);
|
|
let to = toWidgetCoords(point);
|
|
for (let poly of check) {
|
|
let ints = poly.intersections(from, to);
|
|
if (ints.length) {
|
|
if (debug) console.log({ ints, poly, deltaXY, deltaZ });
|
|
upAndOver = "bounds";
|
|
break;
|
|
}
|
|
}
|
|
lastTravelBounds = undefined;
|
|
} else
|
|
// for longer moves
|
|
if (isMove) {
|
|
const bigXY = (deltaXY > shortCut && !lasering);
|
|
const bigZ = (absDeltaZ > toolDiam / 2 && deltaXY > tolerance);
|
|
const midZ = (tolerance && absDeltaZ >= tolerance);
|
|
const inStock = printPoint.z < stockZ || point.z < stockZ;
|
|
if (bigXY || bigZ || midZ) {
|
|
if (debug) console.log({ fromz: printPoint.z, toz: point.z });
|
|
// for big moves intersecting stock...
|
|
if (camForceZMax || inStock) {
|
|
upAndOver = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (upAndOver) {
|
|
if (debug) console.log('upAndOver', { upAndOver, camForceZMax });
|
|
layerPush(printPoint.clone().setZ(zSafe), 0, 0, tool);
|
|
layerPush(point.clone().setZ(zSafe), 0, 0, tool);
|
|
newLayer();
|
|
// when plunge goes below stock, convert to cut
|
|
if (point.z < stockZ) {
|
|
if (debug) console.log('point.z < stockZ');
|
|
layerPush(point.clone().setZ(stockZ + 0.1), 0, 0, tool);
|
|
newLayer();
|
|
emit = 1;
|
|
rate = plungeRate;
|
|
}
|
|
}
|
|
|
|
if (moveOnly) {
|
|
return;
|
|
}
|
|
|
|
// plunge safety catch
|
|
if (deltaZ < 0 && !contouring) {
|
|
if (debug) console.log('plunge safety', deltaZ, rate, plungeRate);
|
|
emit = 1;
|
|
rate = plungeRate;
|
|
}
|
|
|
|
layerOut.mode = currentOp;
|
|
layerOut.spindle = spindle;
|
|
layerPush(
|
|
point,
|
|
emit,
|
|
rate,
|
|
tool,
|
|
{ center }
|
|
);
|
|
|
|
return point;
|
|
}
|
|
|
|
function setTravelBoundary(polys) {
|
|
lastTravelBounds = travelBounds;
|
|
travelBounds = polys ? POLY.flatten(polys) : undefined;
|
|
}
|
|
|
|
/**
|
|
* output an array of slices that form a pocket
|
|
* used by rough and pocket ops
|
|
*
|
|
* @param {Slice[]} slices top-down Z stack of slices
|
|
* @param {boolean} cutdir true=CW false=CCW
|
|
* @param {boolean} depthFirst prioritize cut depth in pockets by nesting
|
|
*/
|
|
function pocket({ slices, cutdir, depthFirst, outline, progress }) {
|
|
let total = 0;
|
|
let depthData = [];
|
|
|
|
for (let slice of slices) {
|
|
let polys = [], t = [], c = [];
|
|
|
|
// collect polys in to tops (parents) and children
|
|
// so we can have the windings be opposite
|
|
POLY.flatten(slice.camLines).forEach((poly) => {
|
|
// poly is child if has parent
|
|
let child = poly.parent;
|
|
// for depth, collapse parent to 1 or 0 (has, missing)
|
|
if (depthFirst) { poly = poly.clone(); poly.parent = child ? 1 : 0 }
|
|
// place poly into top or child bucket
|
|
if (child) c.push(poly); else t.push(poly);
|
|
polys.push(poly);
|
|
});
|
|
|
|
// set cut direction on outer polys
|
|
POLY.setWinding(t, cutdir);
|
|
// set cut direction on inner polys
|
|
POLY.setWinding(c, !cutdir);
|
|
|
|
if (depthFirst) {
|
|
// re-nest layer polys and add to depth stack
|
|
polys = POLY.nest(polys,true,true);
|
|
polys.tool_shadow = POLY.flatten(slice.tool_shadow.clone(true));
|
|
depthData.push(polys);
|
|
} else {
|
|
// if not depth first, output the polys in slice order
|
|
setTravelBoundary(slice.tool_shadow.clone(true));
|
|
poly2polyEmit(polys, printPoint, polyEmit, { swapdir: false });
|
|
newLayer();
|
|
}
|
|
progress(++total, slices.length);
|
|
}
|
|
|
|
// crucially returns true for -0 as well as other negative #s
|
|
function isNeg(v) {
|
|
return v < 0 || (v === 0 && 1 / v === -Infinity);
|
|
}
|
|
|
|
if (depthFirst) {
|
|
for (let i=0; i<depthData.length; i++) {
|
|
descend(depthData.slice(i), undefined, outline);
|
|
}
|
|
}
|
|
}
|
|
|
|
function descend(stack, inside, outline) {
|
|
if (stack.length === 0) return;
|
|
let tops = stack[0];
|
|
let flat = (outline ? POLY.flatten(tops) : tops).filter(poly => !poly.marked);
|
|
if (flat.length === 0) return;
|
|
if (inside) {
|
|
flat = flat.filter(p => p.isInside(inside));
|
|
}
|
|
|
|
for (;;) {
|
|
let wpp = getWidgetPrintPoint();
|
|
let poly = flat.filter(poly => !poly.marked)
|
|
.map(p => p.findClosestPointTo(wpp))
|
|
.sort((a,b) => a.distance - b.distance)
|
|
.map(rec => rec.poly)[0];
|
|
|
|
if (poly) {
|
|
let output = [];
|
|
setTravelBoundary(tops.tool_shadow);
|
|
emit_flat([ poly ], output);
|
|
let engage = true;
|
|
for (let poly of output) {
|
|
polyEmit(poly, CLOSEST_TO_PP, engage);
|
|
engage = false;
|
|
}
|
|
if (outline) {
|
|
output.forEach(poly => {
|
|
descend(stack.slice(1), poly, outline);
|
|
});
|
|
} else {
|
|
descend(stack.slice(1), poly, outline);
|
|
}
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
function emit_flat(flat, output) {
|
|
flat = flat.filter(p => !p.marked);
|
|
if (!flat.length) return;
|
|
flat.sort((a,b) => a.area() - b.area());
|
|
let next = flat[0];
|
|
next.marked = true;
|
|
if (!camInnerFirst) output.push(next);
|
|
if (next.inner) emit_flat(next.inner, output);
|
|
if (camInnerFirst) output.push(next);
|
|
emit_flat(flat, output);
|
|
}
|
|
|
|
function emitTraces(camLines) {
|
|
poly2polyEmit(camLines, printPoint, polyEmit, {
|
|
swapdir: false,
|
|
weight: camInnerFirst
|
|
});
|
|
newLayer();
|
|
}
|
|
|
|
function getWidgetPrintPoint() {
|
|
return printPoint.clone().move({ x: -wmx, y: -wmy });
|
|
}
|
|
|
|
/**
|
|
* Output a single polygon as gcode. The polygon is walked in either the
|
|
* clockwise or counter-clockwise direction depending on the winding of the
|
|
* polygon. The first point of the polygon is assumed to be the starting
|
|
* point, and the last point is assumed to be the ending point. If the
|
|
* polygon is closed, the starting and ending points are the same. The
|
|
* function will automatically output a rapid move to the first point of
|
|
* the polygon if that point is not the current position.
|
|
*
|
|
* @param {Polygon} poly - the polygon to output
|
|
* @param {number} index - optional: starting point index
|
|
* @param {boolean} engage - optional: true to use camFullEngage speed ramp
|
|
* @returns {Point} - the last point emitted (in widget coordinates)
|
|
*/
|
|
function polyEmit(poly, index, engage = false) {
|
|
let arcing = camArcEnabled && !contouring;
|
|
let points = poly.points;
|
|
|
|
if (poly.isClosed()) {
|
|
// only look for the cloests starting point for closed loops
|
|
if (index === CLOSEST_TO_PP) {
|
|
let found = poly.findClosestPointTo(getWidgetPrintPoint());
|
|
index = found.index;
|
|
}
|
|
if (index) {
|
|
points = [...points.slice(index), ...points.slice(0,index)];
|
|
}
|
|
}
|
|
|
|
if (!contouring && poly.isClosed()) {
|
|
points.push(points[0].clone());
|
|
}
|
|
|
|
// run arc detection when enabled
|
|
if (arcing) {
|
|
poly = newPolygon(points).setOpenValue(poly.open).detectArcs({
|
|
tolerance: camArcTolerance,
|
|
arcRes: camArcResolution,
|
|
minPoints: 5
|
|
});
|
|
points = poly.points;
|
|
}
|
|
|
|
setNextIsMove();
|
|
|
|
// we skip ease-down logic in contouring mode or for open polys (traces .. maybe later)
|
|
if (!contouring && camEaseDown && poly.isClosed()) {
|
|
let point0 = points[0];
|
|
|
|
// perform "up and over" and get a new printPoint without "emit"
|
|
camOut(point0, 0, { moveOnly: true });
|
|
setContouring(true);
|
|
|
|
// poly points are in untranslated widget space
|
|
// so we need to translate printPoint into widget coordinates
|
|
let startPoint = printPoint.clone().move({ x: -wmx, y: -wmy, z: -wmz });
|
|
|
|
// calculate ease down for poly path output
|
|
if (startPoint.z > point0.z) {
|
|
let easeMax = feedRate * camFullEngage;
|
|
let easeLerp = plungeRate + ((feedRate - plungeRate) * easeThrottle);
|
|
let easeFeed = Math.min(easeLerp, easeMax);
|
|
let zat = startPoint.z;
|
|
let len = points.length;
|
|
let lp, lz = Infinity;
|
|
// hard cap on number of repeats to catch bad geometry
|
|
for (let i=0; i<len*50 ; i++) {
|
|
let ii = i % len;
|
|
let pt = points[ii];
|
|
if (zat <= pt.z) {
|
|
// rotate points to start at end of ease
|
|
points = [...points.slice(ii), ...points.slice(0,ii)];
|
|
break;
|
|
}
|
|
if (i > 0) {
|
|
let dd = lp.distTo2D(pt);
|
|
zat = Math.max(pt.z, zat - (dd * easeDzPerMm));
|
|
if (zat > lz) {
|
|
// rotate points to start at end of ease
|
|
// also should never get here unless bad geometry
|
|
points = [...points.slice(ii), ...points.slice(0,ii)];
|
|
break;
|
|
}
|
|
lz = zat;
|
|
}
|
|
lp = pt.clone().setZ(Math.max(pt.z, zat));
|
|
camOut(lp, 1, { feed: easeFeed });
|
|
}
|
|
}
|
|
|
|
// resume normal emit rules
|
|
setContouring(false);
|
|
}
|
|
|
|
let lastOut;
|
|
let opts = engage ? { feed: feedRate * camFullEngage } : {};
|
|
|
|
// arc output must handle shortened arcs from ease-down
|
|
// future support for 3d helical arcs will fix this
|
|
if (arcing) {
|
|
let skip = 0;
|
|
let type;
|
|
let center;
|
|
let lastP = points.peek();
|
|
for (let point of points) {
|
|
lastOut = point.clone();
|
|
if (type) {
|
|
// terminate arc early (caused by ease eating points)
|
|
skip = point === lastP ? 0 : skip - 1;
|
|
camOut(lastOut, skip ? -1 : type, { center, xfactor: xfactors[0], ...opts });
|
|
if (!skip) center = type = undefined;
|
|
continue;
|
|
} else if (point.arc) {
|
|
let { arc } = point;
|
|
skip = arc.skip;
|
|
type = arc.clockwise ? 2 : 3;
|
|
// arc center is relative to first point
|
|
center = arc.center.clone().move({ x: -point.x, y: -point.y });
|
|
xfactors.push(xfactors.shift());
|
|
}
|
|
camOut(lastOut, 1, opts);
|
|
}
|
|
} else {
|
|
for (let point of points) {
|
|
camOut(lastOut = point.clone(), 1, opts);
|
|
}
|
|
}
|
|
|
|
if (camDepthFirst) {
|
|
newLayer();
|
|
}
|
|
|
|
return lastOut;
|
|
}
|
|
|
|
// debug arc creation with visual speed cues
|
|
let xfactors = [0.2,0.5];
|
|
|
|
function depthOutlinePath(start, depth, levels, radius, emitter, dir, ease) {
|
|
let bottm = depth < levels.length - 1 ? levels[levels.length - 1] : null;
|
|
let above = levels[depth - 1];
|
|
let level = levels[depth];
|
|
if (!level) {
|
|
return start;
|
|
}
|
|
if (above) {
|
|
level = level.filter(lp => {
|
|
const conf = above.filter(ap => !ap.level_emit && lp.isNear(ap, radius, true));
|
|
return conf.length === 0;
|
|
});
|
|
}
|
|
// const thru = []; // match thru polys
|
|
level = level.filter(lp => {
|
|
if (lp.level_emit) {
|
|
return false;
|
|
}
|
|
// if (bottm && !clr) {
|
|
// const tm = bottm.filter(bp => lp.isEquivalent(bp));
|
|
// thru.appendAll(tm);
|
|
// return tm.length === 0;
|
|
// }
|
|
return true;
|
|
});
|
|
// limit level search to polys matching winding (inside vs outside)
|
|
level = level.filter(p => p.isClockwise() === dir);
|
|
// omit polys that match bottom level polys unless level above is cleared
|
|
start = poly2polyEmit(level, start, (poly, index) => {
|
|
poly.level_emit = true;
|
|
let fromPoint = printPoint.clone();
|
|
if (ease) {
|
|
fromPoint.z += ease;
|
|
}
|
|
fromPoint = polyEmit(poly, index);
|
|
if (ease) {
|
|
fromPoint.z += ease;
|
|
}
|
|
fromPoint = depthOutlinePath(fromPoint, depth + 1, levels, radius, emitter, dir, ease);
|
|
fromPoint = depthOutlinePath(fromPoint, depth + 1, levels, radius, emitter, !dir, ease);
|
|
return fromPoint;
|
|
}, {
|
|
weight: camInnerFirst,
|
|
swapdir: false
|
|
});
|
|
return start;
|
|
}
|
|
|
|
// coming from a previous widget, use previous last point as starting point
|
|
// make top start offset configurable
|
|
if (firstPoint) {
|
|
// we're coming from another widget. offset compensated below
|
|
printPoint = firstPoint;
|
|
// console.log('coming from another widget', { printPoint });
|
|
} else if (center) {
|
|
// we're the first widget output. offset is center
|
|
printPoint = origin.clone().move({ x: center.x, y: center.y });
|
|
// console.log('first widget output', { printPoint });
|
|
} else {
|
|
console.log({ missing_center_using_origin: origin });
|
|
printPoint = origin.clone();
|
|
}
|
|
|
|
let ops = {
|
|
addGCode,
|
|
camOut,
|
|
clearTravelBounds() { setTravelBoundary() },
|
|
depthOutlinePath,
|
|
emitDrills,
|
|
emitTraces,
|
|
getLastPoint() { return toWidgetCoords(printPoint) },
|
|
getTool,
|
|
newLayer,
|
|
pocket,
|
|
poly2polyEmit,
|
|
polyEmit,
|
|
printPoint,
|
|
setChangeOp,
|
|
setContouring,
|
|
setDrill,
|
|
setLasering,
|
|
setNextIsMove,
|
|
setSpindle,
|
|
setTolerance,
|
|
setTool,
|
|
setTravelBoundary,
|
|
tip2tipEmit,
|
|
widget,
|
|
zSafe,
|
|
};
|
|
|
|
let opSum = 0;
|
|
let opTot = 0;
|
|
|
|
// pre-flight check of ops
|
|
for (let op of widget.camops) {
|
|
// Skip loop operations (they're expanded during slice)
|
|
if (op.op.type === 'loop') {
|
|
continue;
|
|
}
|
|
opTot += op.weight();
|
|
// ensure tool related parameters are available
|
|
// for the first index call when no tool is specified
|
|
if (!tool && op.op.tool) {
|
|
setTool(op.op.tool);
|
|
}
|
|
}
|
|
|
|
for (let op of widget.camops) {
|
|
// Skip loop operations (they're expanded during slice)
|
|
if (op.op.type === 'loop') {
|
|
continue;
|
|
}
|
|
contouring = false;
|
|
lasering = false;
|
|
let cop = currentOp = op.op;
|
|
isIndex = cop.type === 'index';
|
|
isLathe = cop.type === 'lathe';
|
|
let weight = op.weight();
|
|
newLayer(cop);
|
|
setTolerance(0);
|
|
setNextIsMove();
|
|
if (cop.tool) setTool(cop.tool, cop.rate || feedRate, cop.plunge || plungeRate);
|
|
if (cop.spindle) setSpindle(cop.spindle);
|
|
// set printPoint in widget coordinate space
|
|
ops.printPoint = printPoint.clone().move({ x: -wmx, y: -wmy, z: -wmz });
|
|
await op.prepare(ops, (progress, message) => {
|
|
update((opSum + (progress * weight)) / opTot, message || op.type(), message);
|
|
});
|
|
opSum += weight;
|
|
if (tool && printPoint && cop.type !== 'shadow') {
|
|
newLayer();
|
|
if (!isIndex) {
|
|
layerPush(printPoint.clone().setZ(stockZClear), 0, 0, tool);
|
|
newLayer();
|
|
}
|
|
}
|
|
}
|
|
|
|
// last layer/move is to zSafe
|
|
// re-inject that point into the last layer generated
|
|
if (printPoint && newOutput.length) {
|
|
let lastLayer = newOutput.filter(layer => Array.isArray(layer)).peek();
|
|
if (Array.isArray(lastLayer)) {
|
|
if (printPoint.z < stockZClear) printPoint.setZ(stockZClear);
|
|
print.addOutput(lastLayer, printPoint, 0, 0, tool);
|
|
}
|
|
}
|
|
|
|
// console.log("prepare output", newOutput);
|
|
// replace output single flattened layer with all points
|
|
print.output = newOutput;
|
|
|
|
return printPoint;
|
|
}
|