reorganize and rename driver files for consistency and clarity

This commit is contained in:
Stewart Allen 2026-01-08 16:52:41 -05:00
commit 67f405fd7b
74 changed files with 283 additions and 283 deletions

View file

@ -0,0 +1,309 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
// WORKER BACK END ANIMATION CODE for 2D
import { Tool } from '../core/tool.js';
const asPoints = false;
const asLines = false;
let stock, center, grid, gridX, gridY, rez;
let path, pathIndex, tool, tools, last, toolID = 1;
let settings;
export function init(worker) {
const { dispatch } = worker;
dispatch.animate_setup = function(data, send) {
settings = data.settings;
const { process } = settings;
const { print } = worker.current;
const density = parseInt(settings.controller.animesh) * 1000;
pathIndex = 0;
path = print.output.flat();
tools = settings.tools;
stock = settings.stock;
rez = 1/Math.sqrt(density/(stock.x * stock.y));
// destructure arcs into path points
path = path.map(o =>
o.arcPoints ? [ ...o.arcPoints.map(point => ({ ...o, point })), o ] : [ o ]
).flat();
const step = rez;
const stepsX = Math.floor(stock.x / step);
const stepsY = Math.floor(stock.y / step);
const { pos, ind, sab } = createGrid(stepsX, stepsY, stock, step, true);
const offset = {
x: process.camOriginCenter ? 0 : stock.x / 2,
y: process.camOriginCenter ? 0 : stock.y / 2,
z: process.camOriginTop ? -stock.z : 0
}
grid = pos;
gridX = stepsX;
gridY = stepsY;
tool = null;
last = null;
animating = false;
animateClear = false;
center = Object.assign({}, stock.center);
center.z -= stock.z / 2;
send.data({ mesh_add: { id: 0, ind, offset, sab } }, [ ]); // sab not transferrable
send.data({ mesh_move: { id: 0, pos: center } });
send.done();
};
dispatch.animate = function(data, send) {
renderPause = data.pause || renderPause;
renderSpeed = data.speed || 0;
if (animating) {
return send.done();
}
renderSteps = data.steps || 1;
renderDone = false;
animating = renderSpeed > 0;
renderPath(send);
};
dispatch.animate_cleanup = function(data, send) {
if (animating) {
animateClear = true;
}
};
}
function createGrid(stepsX, stepsY, size, step, stock) {
const gridPoints = stepsX * stepsY;
const sab = new SharedArrayBuffer(gridPoints * 3 * 4)
const pos = new Float32Array(sab);
const ind = [];
const ox = size.x / 2;
const oy = size.y / 2;
let ex = stepsX - 1;
let ey = stepsY - 1;
// initialize grid points
for (let x=0, ai=0; x<stepsX; x++) {
for (let y=0; y<stepsY; y++) {
let px = pos[ai++] = x * step - ox + step / 2;
let py = pos[ai++] = y * step - oy + step / 2;
pos[ai++] = stock && (x * y === 0 || x === ex || y === ey) ? 0 : size.z;
if (asPoints) {
continue;
}
if (asLines) {
if (y > 0) ind.appendAll([
(stepsY * x) + (y - 1),
(stepsY * x) + (y )
]);
if (x > 0) ind.appendAll([
(stepsY * (x - 1)) + y,
(stepsY * (x )) + y
]);
} else {
if (x > 0 && y > 0) {
let v0 = stepsY * (x - 1) + y - 1;
let v1 = stepsY * (x - 0) + y - 1;
let v2 = stepsY * (x - 0) + y;
let v3 = stepsY * (x - 1) + y;
ind.appendAll([
v0, v1, v2, v0, v2, v3
]);
}
}
}
}
return { pos, ind, sab };
}
let animateClear = false;
let animating = false;
let renderDist = 0;
let renderDone = false;
let renderPause = 10;
let renderSteps = 0;
let renderSpeed = 0;
let skipMove = null;
let toolUpdate;
let depth = 0;
// send latest tool position and progress bar
function renderUpdate(send) {
if (toolUpdate) {
send.data(toolUpdate);
}
send.data({ progress: pathIndex / path.length, id: 0, mesh_update: 1 });
}
function renderPath(send) {
if (renderDone) {
return;
}
if (renderSteps-- === 0) {
animating = false;
renderPath(send);
return;
}
if (animating === false || animateClear || renderSpeed === 0) {
renderUpdate(send);
renderDone = true;
animating = false;
animateClear = false;
send.done();
return;
}
let next = path[pathIndex];
while (next && next.type === 'laser') {
last = next;
next = path[++pathIndex];
}
if (!next) {
animating = false;
renderPath(send);
return;
}
pathIndex++;
const firstTool = !tool && next.tool;
const toolChange = !firstTool && (tool.getID() !== next.tool.getID());
if (firstTool || toolChange) {
// on real tool change, go to safe Z first
if (tool && last.point) {
let pos = last.point = {
x: last.point.x,
y: last.point.y,
z: stock.z
};
send.data({ mesh_move: { toolID, pos }});
}
updateTool(next.tool, send);
}
const id = toolID;
const rezstep = rez;
if (last) {
const lp = last.point, np = next.point;
last = next;
// dwell ops have no point
if (!np || !lp) {
return renderPath(send);
}
const dx = np.x - lp.x, dy = np.y - lp.y, dz = np.z - lp.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
renderDist += dist;
// skip moves that are less than grid resolution
if (renderDist < rezstep) {
renderPath(send);
return;
}
const md = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz));
const st = Math.ceil(md / rezstep);
const mx = dx / st, my = dy / st, mz = dz / st;
const moves = [];
for (let i=0, x=lp.x, y=lp.y, z=lp.z; i<st; i++) {
moves.push({x,y,z});
x += mx;
y += my;
z += mz;
}
moves.push(next.point);
renderMoves(id, moves, send);
} else {
last = next;
if (tool) {
tool.pos = next.point;
toolUpdate = { mesh_move: { id, pos: next.point }};
}
renderPath(send);
}
}
function renderMoves(id, moves, send, seed = 0) {
for (let index = seed; index<moves.length; index++) {
const pos = moves[index];
if (!pos) {
throw `no pos @ ${index} of ${moves.length}`;
}
tool.pos = pos;
deformMesh(pos, send);
toolUpdate = { mesh_move: { id, pos }};
// pause renderer at specified offsets
if ((renderSpeed && renderDist >= renderSpeed) || (depth > 600)) {
renderDist = depth = 0;
renderUpdate(send);
setTimeout(() => {
renderMoves(id, moves, send, index);
}, renderPause);
return;
}
}
depth++;
renderPath(send);
}
// update stock mesh to reflect tool tip geometry at given XYZ position
function deformMesh(pos, send) {
const prof = tool.profile;
const { size, pix } = tool.profileDim;
const mid = Math.floor(pix / 2);
const rx = Math.floor((pos.x + stock.x / 2 - size / 2 - center.x) / rez);
const ry = Math.floor((pos.y + stock.y / 2 - size / 2 - center.y) / rez);
let upos = 0;
// deform mesh to lowest point on tool profile
for (let i=0, il=prof.length; i < il; ) {
const dx = mid + prof[i++];
const dy = mid + prof[i++];
const dz = prof[i++];
const gx = rx + dx;
const gy = ry + dy;
if (gx < 0|| gy < 0 || gx > gridX-1 || gy > gridY-1) {
continue;
}
const gi = gx * gridY + gy;
const iz = gi * 3 + 2;
const cz = grid[iz];
const tz = tool.pos.z - dz;
if (tz < cz) {
upos++;
grid[iz] = tz;
}
}
}
function updateTool(toolobj, send) {
if (tool) {
send.data({ mesh_del: toolID });
}
tool = new Tool(settings, toolobj.getID());
tool.generateProfile(rez);
const flen = tool.fluteLength() || 15;
const slen = tool.shaftLength() || 15;
// const frad = tool.fluteDiameter() / 2;
const prof = tool.profile;
const { size, pix } = tool.profileDim;
const { pos, ind, sab } = createGrid(pix, pix, {x:size, y:size, z:flen+slen}, rez);
const mid = Math.floor(pix/2);
// deform mesh to fit tool profile
for (let i=0, il=prof.length; i < il; ) {
const dx = mid + prof[i++];
const dy = mid + prof[i++];
const dz = prof[i++];
pos[(dx * pix + dy) * 3 + 2] = -dz;
}
send.data({ mesh_add: { id:++toolID, ind, sab }});
}

View file

@ -0,0 +1,410 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
// WORKER BACK END ANIMATION CODE for 3D (indexed)
import { CSG } from '../../../../geo/csg.js';
import { Tool } from '../core/tool.js';
let nextMeshID = 1,
stock,
path,
pathIndex,
tool,
tools,
last,
settings,
stockZ,
stockIndexMsg = false,
stockSlices,
stockIndex,
startTime,
toolID = -1,
toolMesh,
toolRadius,
toolUpdateMsg,
animateClear = false,
animating = false,
renderDone = false,
renderSteps = 0,
renderSpeed = 0,
indexCount = 0,
updates = 0,
maxR;
export function init(worker) {
const { dispatch, minions } = worker;
dispatch.animate_setup2 = function (data, send) {
settings = data.settings;
const { controller, process } = settings;
const { print } = worker.current;
const isIndexed = process.camStockIndexed;
pathIndex = 0;
path = print.output.flat();
tools = settings.tools;
stock = settings.stock;
tool = null;
last = null;
animating = false;
animateClear = false;
stockIndex = 0;
indexCount = 0;
startTime = 0;
updates = 0;
stockZ = isIndexed ? 0 : stock.z;
maxR = Math.hypot(stock.y, stock.z);
stockSlices = [];
const { x, y, z } = stock;
const sliceCount = parseInt(settings.controller.animesh || 2000) / 100;
const sliceWidth = stock.x / sliceCount;
if (controller.manifold)
for (let i = 0; i < sliceCount; i++) {
let xmin = -(x / 2) + (i * sliceWidth) + sliceWidth / 2;
let slice = new Stock(sliceWidth, y, z).translate(xmin, 0, 0);
stockSlices.push(slice);
slice.updateMesh([]);
slice.send(send);
// send.data({ mesh_move: { id: slice.id, pos: { x:0, y:0, z: stock.z/2} } });
}
send.done();
};
dispatch.animate2 = function (data, send) {
renderSpeed = data.speed || 0;
if (animating) {
return send.done();
}
renderSteps = data.steps || 1;
renderDone = false;
animating = renderSpeed > 0;
startTime = startTime || Date.now();
renderPath(send);
};
dispatch.animate_cleanup2 = function (data, send) {
if (animating) {
animateClear = true;
}
};
}
function renderPath(send) {
if (renderDone) {
return;
}
if (renderSteps-- === 0) {
animating = false;
renderPath(send);
return;
}
if (animating === false || animateClear || renderSpeed === 0) {
renderUpdate(send);
renderDone = true;
animating = false;
animateClear = false;
send.done();
return;
}
let next = path[pathIndex];
while (next && next.type === 'laser') {
last = next;
next = path[++pathIndex];
}
if (!next) {
console.log('animation completed in ', ((Date.now() - startTime) / 1000).round(2));
animating = false;
renderPath(send);
return;
}
pathIndex++;
if (next.tool && (!tool || tool.getID() !== next.tool.getID())) {
// on real tool change, go to safe Z first
if (tool && last.point) {
let pos = last.point = {
x: last.point.x,
y: last.point.y,
z: stock.z
};
toolMove(pos);
send.data(toolUpdateMsg);
}
toolUpdate(next.tool.getID(), send);
}
const id = toolID;
const rezstep = tool ? Math.min(tool.maxDiameter(), 1) / 4 : 1;
// console.log({ rezstep });
if (last) {
// console.log(last.point, next.point);
const lp = last.point, np = next.point;
last = next;
// render move line on client
send.data({ line: [ lp, np ]});
// dwell ops have no point
if (!np || !lp) {
return renderPath(send);
}
let dx = np.x - lp.x,
dy = np.y - lp.y,
dz = np.z - lp.z,
da = (np.a ?? 0) - (lp.a ?? 0), // delta rotation degrees
AD = maxR * 2, // arc diameter @ max z
dr = (Math.abs(da) / 360) * AD, // delta rotation arc
dist = Math.max(Math.hypot(dx, dy, dz), dr); // motion distance
// console.log({ dz, da, AD, dr, dist });
// sub-divide long moves for animation and better boolean ops
const md = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz), dr);
const st = Math.ceil(md / rezstep); // steps
const mx = dx / st, my = dy / st, mz = dz / st, ma = da / st;
const sd = Math.sqrt(mx * mx + my * my + mz * mz + ma * ma);
const sp = next.speed;
const moves = [];
for (let i = 0, x = lp.x, y = lp.y, z = lp.z, a = lp.a; i < st; i++) {
moves.push({ x, y, z, a, md: sd, dx, dy, dz, da, sp });
x += mx;
y += my;
z += mz;
a += ma;
}
// console.log({ dist: md, steps: st, speed: next.speed, moves });
// push last point
moves.push({ ...next.point, md: sd, sp });
// console.log(...moves);
renderMoves(id, moves, send);
} else {
last = next;
if (tool) {
toolMove(next.point);
}
renderPath(send);
}
}
function renderMoves(id, moves, send, seed = 0) {
for (let index = seed; index < moves.length; index++) {
const pos = moves[index];
if (!pos) {
throw `no pos @ ${index} of ${moves.length}`;
}
const { dx, dy, dz, da, sp, md } = pos;
toolMove(pos);
// console.log('renderMoves', {id, moves, seed});
let subs = 0;
if (dx || dy || dz < 0 || da)
for (let slice of stockSlices) {
if (slice.bounds.intersectsBox(toolMesh.bounds)) {
slice.subtractTool(toolMesh);
subs++;
}
}
// pause renderer at specified offsets
if (renderSpeed) {
let time = sp ? (md / sp) * 60000 : 10;
// console.log({ time, ...pos });
renderUpdate(send);
setTimeout(() => {
renderMoves(id, moves, send, index + 1);
}, time / renderSpeed);
return;
}
}
renderPath(send);
}
// send latest tool position and progress bar
function renderUpdate(send) {
const updated = []
for (let slice of stockSlices) {
slice.updateMesh(updated);
}
for (let slice of updated) {
slice.send(send);
}
if (toolUpdateMsg) {
send.data(toolUpdateMsg);
}
if (stockIndexMsg) {
send.data({ stock_index: stockIndex });
for (let slice of stockSlices) {
send.data({ mesh_index: { id: slice.id, index: -stockIndex } });
}
stockIndexMsg = false;
}
send.data({ progress: pathIndex / path.length });
updates++;
}
// move tool mesh animation space, update client
function toolMove(pos) {
toolUpdateMsg = { mesh_move: { id: toolID, pos: { x: pos.x, y: pos.y, z: pos.z } } };
if (toolMesh.mesh) {
toolMesh.mesh.delete();
}
toolMesh.mesh = toolMesh.root.translate(pos.x, pos.y, pos.z);
if (pos.a !== undefined) {
let tmp = toolMesh.mesh.rotate([pos.a, 0, 0]);
toolMesh.mesh.delete();
toolMesh.mesh = tmp;
if (pos.a !== stockIndex) {
stockIndexMsg = true;
stockIndex = pos.a;
}
}
toolMesh.bounds = CSG.toBox3(toolMesh.mesh);
}
// delete old tool mesh, generate tool mesh, send to client
function toolUpdate(toolid, send) {
if (tool) {
send.data({ mesh_del: toolID });
}
tool = new Tool(settings, toolid);
const Instance = CSG.Instance();
const slen = tool.shaftLength() || 15;
const srad = tool.shaftDiameter() / 2;
const flen = tool.fluteLength() || 15;
const frad = toolRadius = tool.fluteDiameter() / 2;
const tlen = slen + flen; // total tool length
let { cylinder, sphere } = Instance.Manifold;
let mesh;
if (tool.isBallMill()) {
mesh = cylinder(tlen - frad * 2, frad, frad, 20, true)
.add(sphere(frad, 20).translate(0, 0, -(tlen - frad * 2) / 2))
.add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen / 2));
} else if (tool.isTaperBall()) {
const { b, r } = tool.getBallTaperParams();
// taper length excludes most of ball
const taperLen = flen - b;
// shaft at top
mesh = cylinder(slen, srad, srad, 20, true).translate(0, 0, slen / 2)
// taper cone in middle
.add(cylinder(taperLen, b, frad, 20, true).translate(0, 0, -taperLen / 2))
// exposed ball at bottom
.add(sphere(r, 20).translate(0, 0, -taperLen));
} else if (tool.isTaperMill()) {
const trad = Math.max(tool.tipDiameter() / 2, 0.001);
mesh = cylinder(slen, srad, srad, 20, true).translate(0, 0, slen / 2)
.add(cylinder(flen, trad, frad, 20, true).translate(0, 0, -flen / 2));
} else {
mesh = cylinder(tlen, frad, frad, 20, true)
.add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen / 2));
}
mesh = mesh.translate(0, 0, (tlen - stockZ) / 2);
const raw = mesh.getMesh();
const vertex = raw.vertProperties;
const index = raw.triVerts;
toolMesh = { root: mesh, index, vertex, bounds: CSG.toBox3(mesh) };
send.data({ mesh_add: { id: --toolID, ind: index, pos: vertex } });
}
class Stock {
constructor(x, y, z) {
this.id = nextMeshID++;
this.vbuf = undefined;
this.ibuf = undefined;
this.ilen = 0;
this.sends = 0;
this.newbuf = true;
this.subtracts = 0;
this.mesh = CSG.Instance().Manifold.cube([x, y, z], true);
}
send(send) {
const newbuf = this.newbuf;
const action = this.sends++ === 0 ? 'mesh_add' : 'mesh_update';
send.data({
[action]: {
id: this.id,
ind: newbuf ? this.ibuf : undefined,
pos: newbuf ? this.vbuf : undefined,
ilen: this.ilen,
plen: this.plen
}
});
this.newbuf = false;
this.sends++;
// console.log({ send: this.id, newbuf, action });
}
translate(x, y, z) {
// console.log({ translate: this.id, x, y, z });
const oldmesh = this.mesh;
this.mesh = this.mesh.translate(x, y, z);
this.bounds = CSG.toBox3(this.mesh);
oldmesh.delete();
return this;
}
updateMesh(updates) {
if (this.sends > 0 && this.subtracts === 0) {
return;
}
const subs = this.subtracts;
this.subtracts = 0;
let start = Date.now();
let mesh = this.mesh.getMesh();
this.sharedVertexBuffer(mesh.numVert * 3);
this.sharedIndexBuffer(mesh.numTri * 3);
this.vbuf.set(mesh.vertProperties);
this.ibuf.set(mesh.triVerts);
let sub = Date.now();
updates.push(this);
if (false) console.log({
update: this.id,
time: sub - start,
subs,
mssub: ((sub - start) / subs).round(3)
});
}
subtractTool(toolMesh) {
const oldmesh = this.mesh;
this.mesh = this.mesh.subtract(toolMesh.mesh);
oldmesh.delete();
this.subtracts++;
}
sharedVertexBuffer(size) {
const old = this.vbuf;
this.plen = size;
if (old && old.length >= size) {
// console.log({svb: this.id, reuse: size, old: old.length});
return old;
}
// let buf = new Float32Array(new SharedArrayBuffer(size * 4));
let buf = new Float32Array(new SharedArrayBuffer(size * 4 + 1024 * 1024));
// console.log({new_svb: this.id, size, buf});
this.newbuf = true;
return this.vbuf = buf;
}
sharedIndexBuffer(size) {
const old = this.ibuf;
this.ilen = size;
if (old && old.length >= size) {
// console.log({sib: this.id, reuse: size, old: old.length});
return old;
}
// let buf = new Uint32Array(new SharedArrayBuffer(size * 4));
let buf = new Uint32Array(new SharedArrayBuffer(size * 4 + 1024 * 1024));
// console.log({new_sib: this.id, size, buf});
this.newbuf = true;
return this.ibuf = buf;
}
}

View file

@ -0,0 +1,528 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { base } from '../../../../geo/base.js';
import { version } from '../../../../moto/license.js';
const { util } = base;
/**
* @returns {Array} gcode lines
*/
export function cam_export(print, online) {
const widget = print.widgets[0];
if (!widget) return;
const { settings } = print;
const { device } = settings;
let i,
time = 0,
lines = 0,
bytes = 0,
opnum = 0,
factor = 1,
output = [],
spindle = 0,
newSpindle = 0,
toolChanges = 0,
spindleMax = device.spindleMax,
origin = settings.origin || { x: 0, y: 0, z: 0 },
space = device.gcodeSpace ? ' ' : '',
isRML = device.gcodeFExt?.toLowerCase() === 'rml',
stripComments = device.gcodeStrip || false,
cmdToolChange = device.gcodeChange || ["M6 T{tool}"],
cmdSpindle = device.gcodeSpindle || ["M3 S{speed}"],
cmdDwell = device.gcodeDwell || ["G4 P{time}"],
axis = { X: 'X', Y: 'Y', Z: 'Z', A: 'A', F: 'F', R: 'R', I: 'I', J: 'J' },
dev = settings.device,
spro = settings.process,
maxZd = spro.camFastFeedZ,
maxXYd = spro.camFastFeed,
decimals = base.config.gcode_decimals || 3,
pos = { x: null, y: null, z: null, a: undefined, f: null, t: null, emit: null },
line,
cidx,
mode,
point,
points = 0,
lastGn = '',
rewrite_comments = false,
compact_output = false,
lasering = false,
laserOp,
stock = settings.stock || {},
zmax = stock.z,
runbox = {
max: { x: -Infinity, y: -Infinity, z: -Infinity },
min: { x: Infinity, y: Infinity, z: Infinity }
},
offset = {
x: -origin.x,
y: origin.y,
z: spro.camOriginTop ? origin.z - zmax : origin.z
},
scale = {
x: 1,
y: 1,
z: 1
},
consts = {
box: runbox,
tool: 0,
tool_name: "unknown",
top: (offset ? dev.bedDepth : dev.bedDepth / 2),
left: (offset ? 0 : -dev.bedWidth / 2),
right: (offset ? dev.bedWidth : dev.bedWidth / 2),
bottom: (offset ? 0 : -dev.bedDepth / 2),
time_sec: 0,
time_ms: 0,
time: 0
};
function section(section) {
append();
online({ section });
}
function append(line) {
if (line) {
lines++;
bytes += line.length;
if (rewrite_comments) {
const spos = line.indexOf(';');
if (spos >= 0) {
const left = line.substring(0, spos);
const right = '(' + line.substring(spos)
.replace(/; /g, '')
.replace(/;/g, '') + ')';
line = [left, right].join('');
}
}
output.append(line);
}
if (!line || output.length > 1000) {
online(output.join("\r\n"));
output = [];
}
}
/**
* Take an array of lines and emit them after:
* - splitting a string into an array
* - stripping comments if !isRML and stripComments
* - applying constant replacements
* - applying Inch/Millimeter conversions if G20 or G21 are found
*
* @param {string|Array} array - an array of lines or a single string
* @param {Object} consts - a dictionary of constants to replace
*/
function filterEmit(array, consts) {
if (!array) {
return;
}
if (typeof (array) === 'string') {
array = array.split('\n');
}
for (i = 0; i < array.length; i++) {
line = print.constReplace(array[i], consts);
if (!isRML && stripComments && (cidx = line.indexOf(";")) >= 0) {
line = line.substring(0, cidx).trim();
if (line.length === 0) continue;
}
if (line.indexOf('G20') === 0) {
factor = 1 / 25.4;
consts.top = (offset ? dev.bedDepth : dev.bedDepth / 2) * factor;
consts.left = (offset ? 0 : -dev.bedWidth / 2) * factor;
consts.right = (offset ? dev.bedWidth : dev.bedWidth / 2) * factor;
consts.bottom = (offset ? 0 : -dev.bedDepth / 2) * factor;
} else if (line.indexOf('G21') === 0) {
factor = 1;
}
append(line);
}
}
function add0(val, opt) {
let s = val.toString(),
d = s.indexOf(".");
if (d < 0 && decimals > 0) {
return opt ? s : s + '.0';
} else {
return val.toFixed(decimals);
}
}
/**
* Move to a point, applies post-processing including for laser/RML, and appends to output
* @param {Point} out.point - the point to move to
* @param {Tool} [out.tool] - the tool to move with
* @param {number} [out.speed] - the speed to move at (in mm/min)
* @param {number} [out.emit] - the emission state (0=off, 1=on)
* @param {boolean} [opt.newOp] - whether to force a new operation
* @param {number} [opt.dx] - the delta X change (if not given, uses out.point.x - pos.x)
* @param {number} [opt.dy] - the delta Y change (if not given, uses out.point.y - pos.y)
* @param {number} [opt.dz] - the delta Z change (if not given, uses out.point.z - pos.z)
* @param {number} [opt.time] - the time to move (in ms) (if not given, calculate from speed)
*/
function moveTo(out, opt = {}) {
let laser = out.type === 'laser';
let newpos = out.point;
// no point == dwell
// out.speed = time to dwell in ms
if (!newpos) {
time += out.speed / 60;
consts.time_sec = out.speed / 1000;
consts.time_ms = out.speed;
consts.time = consts.time_sec;
filterEmit(cmdDwell, consts);
return;
}
newpos.x = util.round(newpos.x, decimals);
newpos.y = util.round(newpos.y, decimals);
newpos.z = util.round(newpos.z, decimals);
// on tool change
let changeTool = !(out.tool && pos.t) || out.tool.getID() !== pos.t.getID();
if (changeTool) {
pos.t = out.tool;
consts.tool = pos.t.getNumber();
consts.tool_name = pos.t.getName();
if (!laserOp && (spro.camToolInit || toolChanges > 0)) {
filterEmit(cmdToolChange, { ...consts, spindle: newSpindle });
}
toolChanges++;
}
// on spindle change (deferred from layer transition so it's post-toolchange)
if (spindleMax && newSpindle && (changeTool || newSpindle !== spindle || opt.newOp)) {
spindle = newSpindle;
if (!laserOp) {
if (spindle > 0) {
let speed = Math.abs(spindle);
filterEmit(cmdSpindle, { speed, spindle: speed, rpm: speed });
} else {
append("M4");
}
}
}
// enforce XY origin at start of print
if (points === 0 || changeTool) {
pos.x = pos.y = pos.z = 0;
}
// split first move to X,Y then Z for that new location
// safety to prevent tool crashing
if (points === 0 || changeTool) {
points++;
if (spro.camFirstZMax) {
moveTo({
// speed: Infinity,
tool: out.tool,
point: { x: pos.x, y: pos.y, z: newpos.z, a: newpos.a },
emit: 0,
}, {
dx: 0, dy: 0, dz: 1, time: 0
});
moveTo({
// speed: Infinity,
tool: out.tool,
point: { x: newpos.x, y: newpos.y, z: newpos.z, a: pos.a },
emit: out.emit,
}, {
dx: 1, dy: 1, dz: 0, time: 0
});
} else {
moveTo({
// speed: Infinity,
tool: out.tool,
point: { x: newpos.x, y: newpos.y, z: pos.z, a: pos.a },
emit: 0
}, {
dx: 1, dy: 1, dz: 0, time: 0
});
moveTo({
// speed: Infinity,
tool: out.tool,
point: { x: newpos.x, y: newpos.y, z: newpos.z, a: newpos.a },
emit: out.emit
}, {
dx: 0, dy: 0, dz: 1, time: 0
});
}
points--;
return;
}
// skip arc points for display only
if (out.emit === -1) {
return;
}
let dx = opt.dx || newpos.x - pos.x,
dy = opt.dy || newpos.y - pos.y,
dz = opt.dz || newpos.z - pos.z,
da = newpos.a != pos.a,
gv = (dz >= 0 && out.emit >= 0 && out.emit <= 3) ? out.emit : 1,
gn = `G${gv}`,
speed = out.speed,
arc = out.emit == 2 || out.emit == 3,
center = out.center,
nl = (compact_output && lastGn === gn) ? [] : [gn],
maxf = (dz < 0 ? maxZd : maxXYd) || speed,
feed = Math.min(speed || maxf, maxf),
dist = Math.sqrt(dx * dx + dy * dy + dz * dz),
newFeed = feed && feed !== pos.f;
// catch NaN in debugging
// if (nl.length === 0 || nl[0] === undefined)
// console.log(out, nl.length, ...nl);
// drop dup points (all deltas are 0)
if (!arc && !(dx || dy || dz || da)) {
return;
}
lastGn = gn;
if (dx || newpos.x !== pos.x || arc) {
pos.x = newpos.x;
runbox.min.x = Math.min(runbox.min.x, pos.x);
runbox.max.x = Math.max(runbox.max.x, pos.x);
nl.append(space).append(axis.X).append(add0(consts.pos_x = pos.x * factor));
}
if (dy || newpos.y !== pos.y || arc) {
pos.y = newpos.y;
runbox.min.y = Math.min(runbox.min.y, pos.y);
runbox.max.y = Math.max(runbox.max.y, pos.y);
nl.append(space).append(axis.Y).append(add0(consts.pos_y = pos.y * factor));
}
if (dz || newpos.z !== pos.z || arc) {
pos.z = newpos.z;
runbox.min.z = Math.min(runbox.min.z, pos.z);
runbox.max.z = Math.max(runbox.max.z, pos.z);
nl.append(space).append(axis.Z).append(add0(consts.pos_z = pos.z * factor));
}
if (da) {
pos.a = newpos.a;
nl.append(space).append(axis.A).append(add0(consts.pos_a = pos.a * -factor));
}
if (newFeed) {
pos.f = feed;
nl.append(space).append(axis.F).append(add0(consts.feed = feed * factor, true));
}
if (arc) {
nl.append(space).append(axis.I).append(add0(center.x * factor, true))
.append(space).append(axis.J).append(add0(center.y * factor, true));
}
// temp hack to support RML1 dialect from a file extensions trigger
if (isRML) {
if (speed) {
if (newFeed) {
append(`VS${feed};`);
}
nl = [axis.Z, add0(pos.x * factor), ",", add0(pos.y * factor), ",", add0(pos.z * factor), ";"];
} else {
nl = ["PU", add0(pos.x * factor), ",", add0(pos.y * factor), ";"];
}
}
if (!lasering && laser) {
// enable laser
filterEmit(laserOp.on, consts);
}
// if (laser && pos.emit !== out.emit) {
if (laser) {
nl.append(space).append(`S${add0(out.emit)}`);
}
if (laser) {
pos.emit = out.emit;
}
if (lasering && !laser) {
// disable laser
filterEmit(laserOp.off, consts);
}
// update lasering state
lasering = laser;
// update time calculation
time += opt.time >= 0 ? opt.time : (dist / (pos.f || 1000)) * 60;
consts.time = Math.round(time);
// if (comment && !stripComments) {
// nl.append(" ; ").append(comment);
// nl.append(" ; ").append(points);
// }
append(nl.join(''));
points++;
}
// look for SCALE header directive
let gcodePre = [];
for (let line of device.gcodePre) {
if (line.indexOf(';; SCALE ') === 0) {
try {
let map = JSON.parse(line.substring(9));
if (map.X) scale.x = map.X;
if (map.Y) scale.y = map.Y;
if (map.Z) scale.z = map.Z;
if (map.A) scale.a = map.A;
if (map.DEC !== undefined) decimals = parseInt(map.DEC);
console.log('export scaling applied', map);
} catch (e) {
console.log('malformed scale directive', line);
}
} else if (line.indexOf(';; REWRITE-COMMENTS-PARENS') === 0) {
rewrite_comments = true;
} else if (line.indexOf(';; DECIMALS') === 0) {
decimals = parseInt(line.split('=')[1].trim() || 3) || 3;
} else if (line.indexOf(';; COMPACT-OUTPUT') === 0) {
compact_output = true;
stripComments = true;
space = '';
} else if (line.indexOf(";; AXISMAP ") === 0) {
let axmap = JSON.parse(line.substring(11).trim());
for (let key in axmap) {
axis[key] = axmap[key];
}
} else {
gcodePre.push(line);
}
}
section('header');
if (!stripComments) {
append(`; Generated by Kiri:Moto ${version}`);
append(`; ${new Date().toString()}`);
filterEmit(["; Bed left:{left} right:{right} top:{top} bottom:{bottom}"], consts);
append(`; Stock X:${stock.x?.round(2)} Y:${stock.y?.round(2)} Z:${stock.z?.round(2)}`);
append(`; Target: ${settings.filter[settings.mode]}`);
append("; --- process ---");
for (let pk in spro) {
if (pk !== "ops" && pk !== "op2") {
append("; " + pk + " = " + spro[pk]);
}
}
}
// collect tool info to add to header
let toolz = {}, ctool;
const isOffset = offset && (offset.x || offset.y || offset.z);
// remap points as necessary for origins, offsets, inversions
print.output.forEach(layer => {
// console.log(layer);
if (!Array.isArray(layer)) {
return;
}
layer.forEach(out => {
if (out.gcode || out.type === 'lerp') {
return;
}
if (out.tool && out.tool !== ctool) {
ctool = out.tool;
toolz[out.tool] = ctool;
}
point = out.point;
if (!point || point.mod) {
return;
}
// ensure not point is modified twice
point.mod = 1;
if (isOffset) {
point.x += offset.x ?? 0;
point.y += offset.y ?? 0;
point.z -= offset.z ?? 0;
}
if (scale) {
point.x *= scale.x;
point.y *= scale.y;
point.z *= scale.z;
}
if (point.a && scale.a) {
point.a *= scale.a;
}
if (spro.outputInvertX) point.x = -point.x;
if (spro.outputInvertY) point.y = -point.y;
if (spro.camOriginTop) point.z = point.z - zmax;
});
});
if (!stripComments) {
// emit tools used in comments
append("; --- tools ---");
Object.keys(toolz).sort().forEach(tn => {
let { number, flute_diam, flute_len, metric } = toolz[tn].tool;
append(`; tool#=${number} flute=${flute_diam} len=${flute_len} unit=${metric ? 'metric' : 'imperial'}`);
});
}
// emit gcode preamble
filterEmit(gcodePre, consts);
// emit all points in layer/point order
for (let layerout of print.output) {
const newmode = layerout.mode;
if (newmode) {
if (newmode.type === 'laser on') {
laserOp = newmode;
} else if (newmode.type === 'laser off') {
laserOp = undefined;
// force tool change in case laser tool matches next tool
pos.t = undefined;
}
}
const firstOut = layerout[0];
const newOp = newmode && !newmode.silent && mode !== newmode;
if (newOp) {
if (mode && !stripComments) {
append("; ending " + mode.type + " op after " + Math.round(time) + " seconds");
}
mode = newmode;
if (mode) {
section(`op-${opnum++}-${mode.type}`);
}
if (!stripComments && mode) {
append("; starting " + mode.type + " op");
}
}
newSpindle = layerout.spindle;
// iterate over layer output records
layerout.forEach((out, ind) => {
if (out.type === 'lerp') {
// suppress display only lerp points
return;
}
if (out.gcode && Array.isArray(out.gcode)) {
filterEmit(out.gcode, consts);
} else {
moveTo(out, { newOp: ind === 0 ? newOp : false });
}
});
if (lasering && laserOp) {
// disable laser
filterEmit(laserOp.off, consts);
lasering = false;
}
}
if (mode && !stripComments) {
append("; ending " + mode.type + " op after " + Math.round(time) + " seconds");
}
// emit gcode post
section('footer');
filterEmit(device.gcodePost, consts);
// flush buffered gcode
append();
print.time = time;
print.lines = lines;
print.bytes = bytes + lines - 1;
print.bounds = runbox;
};

View file

@ -0,0 +1,122 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { codec } from '../../../core/codec.js';
import { cam_slice_pre, cam_slice, holes, traces } from './slice.js';
import { cam_prepare } from './prepare.js';
import { cam_export } from './export.js';
import { tool as mesh_tool } from '../../../../mesh/tool.js';
import { cylinder_poly_find } from './slice.js';
import { init as init_2d } from './anim-2d.js';
import { init as init_3d } from './anim-3d.js';
function surface_prep(widget, index) {
if (!widget.tool) {
let tool = widget.tool = new mesh_tool();
let translate = index ? true : false;
tool.index(widget.getGeoVertices({ unroll: true, translate }));
}
};
function surface_find(widget, faces, radians, filterZ) {
surface_prep(widget);
return widget.tool.findConnectedSurface(faces, radians, filterZ);
};
function init(worker) {
const { dispatch } = worker;
init_2d(worker);
init_3d(worker);
dispatch.cam_surfaces = function(data, send) {
const { settings, index } = data;
const widgets = Object.values(worker.cache);
for (let widget of widgets) {
if (index) {
widget.setIndexed(true);
widget.setAxisIndex(-index);
} else {
widget.setIndexed(false);
widget.setAxisIndex(0);
}
CAM.surface_prep(widget, index);
}
send.done({});
};
dispatch.cam_surface_find = function(data, send) {
const { id, face, radians } = data;
const widget = worker.cache[id];
const faces = surface_find(widget, [face], radians);
send.done(faces);
}
dispatch.cam_traces = async function(data, send) {
const { settings, single } = data;
const widgets = Object.values(worker.cache);
const fresh = [];
for (let widget of widgets) {
if (await traces(settings, widget, single)) {
fresh.push(widget);
}
}
// const fresh = widgets.filter(widget => traces(settings, widget, single));
send.done(codec.encode(fresh.map(widget => { return {
id: widget.id,
traces: widget.traces,
} } )));
};
dispatch.cam_traces_clear = function(data, send) {
for (let widget of Object.values(worker.cache)) {
delete widget.traces;
delete widget.topo;
}
send.done({});
};
dispatch.cam_holes = async function(data, send) {
const { settings, indiv, rec } = data;
const widgets = Object.values(worker.cache);
const fresh = [];
for (let [i, widget] of widgets.entries()) {
let ret = await holes(settings, widget, indiv, rec, (prog, msg) => {
send.data({ progress: (i/widgets.length)+(prog/widgets.length), msg });
});
if (ret) {
fresh.push(widget);
}
}
// const fresh = widgets.filter(widget => CAM.traces(settings, widget, single));
send.done(codec.encode(fresh.map(widget => { return {
id: widget.id,
holes: widget.drills,
shadowed: widget.shadowedDrills
} } )));
}
dispatch.cam_cylinder_find = async function(data,send){
const { id, face, settings } = data;
const widget = worker.cache[id];
try{
send.done(cylinder_poly_find(widget, face));
}catch(error){
send.done({error});
}
}
}
export const CAM = {
init,
surface_prep,
surface_find,
slicePre: cam_slice_pre,
slice: cam_slice,
slicePost: undefined,
prepare: cam_prepare,
export: cam_export
};

View file

@ -0,0 +1,594 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
// todo: surface offset pattern
// todo: trace dogbones, merge overlap
import { CamOp } from '../core/op.js';
import { Tool } from '../core/tool.js';
import { newSlice } from '../../../core/slice.js';
import { newPoint } from '../../../../geo/point.js';
import { newPolygon } from '../../../../geo/polygon.js';
import { polygons as POLY } from '../../../../geo/polygons.js';
import { util as base_util } from '../../../../geo/base.js';
import { tip2tipJoin } from '../../../../geo/paths.js';
import { CAM } from './init-work.js';
const DEG2RAD = Math.PI / 180;
const clib = self.ClipperLib;
const ctyp = clib.ClipType;
const ptyp = clib.PolyType;
const cfil = clib.PolyFillType;
const ts_eps = 0.01;
class OpArea extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let { op, state } = this;
let { direction, down, expand, flats, flatOff, follow } = op;
let { mode, outline, over, rename, smooth, tool } = op;
let { addSlices, axisIndex, color, cutTabs, settings } = state;
let { shadowAt, setToolDiam, tabs, widget, workarea } = state;
let areaTool = new Tool(settings, tool);
let smoothVal = (smooth ?? 0) / 10;
let toolDiam = areaTool.fluteDiameter();
let toolOver = this.toolOver = areaTool.getStepSize(over);
let zTop = workarea.top_z;
let zBottom = workarea.bottom_z;
let shadowBase = state.shadow.base;
let thruHoles = state.shadow.holes;
let roundSharps = settings.process.camRoundCorners;
// get these once and re-use for each area
let vertices = mode === 'surface' ?
widget.getGeoVertices({ unroll: true, translate: true }) :
undefined;
// also updates tab offsets
setToolDiam(toolDiam);
// selected area polygons: surfaces and edges
let { devel, edgeangle } = settings.controller;
let polys = [];
let stack = [];
let surfaces = this.surfaces = [];
let areas = this.areas = [ stack ];
let aminz = Infinity;
function newArea() {
if (stack.length) {
stack = [];
areas.push(stack);
}
}
function newLayer(z) {
stack.push(newSlice(z));
return stack.peek();
}
// gather area selections
if (!op.shadow)
for (let arr of (op.areas[widget.id] ?? [])) {
let poly = newPolygon().fromArray(arr);
aminz = Math.min(aminz, poly.minZ());
polys.push(poly);
// user-selected traces are not rotated
// those based on shadow (outline, rough) are
if (!op.rotated && axisIndex) {
// traces come from unrotatead widget edges
poly.applyRotations(axisIndex);
}
}
// connect open poly edge segments into closed loops (when possible)
// surface and edge selections produce open polygons by default
polys = POLY.nest(POLY.reconnect(polys, false));
// gather surface selections
if (!op.shadow) {
let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4));
let faces = CAM.surface_find(widget, (op.surfaces[widget.id] ?? []), (follow ?? edgeangle ?? 5) * DEG2RAD);
let fpoly = [];
for (let face of faces) {
let i = face * 9;
fpoly.push(newPolygon()
.add(vert[i++], vert[i++], aminz = Math.min(aminz, vert[i++]))
.add(vert[i++], vert[i++], aminz = Math.min(aminz, vert[i++]))
.add(vert[i++], vert[i++], aminz = Math.min(aminz, vert[i++]))
);
}
// remove invalid edges (eg. when vertical walls are the only selection)
fpoly = fpoly.filter(p => p.area() > 0.001);
// add in unioned surface areas
polys.push(...POLY.setZ(POLY.union(fpoly, 0.00001, true), aminz));
}
// use part shadow instead of areas or surfaces
if (op.shadow) {
polys.push(...shadowBase.clone(true));
}
// smoothing for jaggies usually caused by vertical walls
if (smoothVal) {
polys = polys.map(poly => POLY.offset(POLY.offset([ poly ], smoothVal), -smoothVal)).flat();
POLY.setZ(polys, aminz);
}
// expand selections (flattens z variable polys)
if (Math.abs(expand) > 0) {
let nupolys = polys.filter(p => p.open); // set aside open
for (let p of polys.filter(p => !p.open)) {
let expanded = POLY.expand([ p ], expand);
if (expanded) {
POLY.setZ(expanded, p.minZ());
nupolys.push(...expanded.flat());
}
}
// polys = nupolys;
// re-merge after expansion in case it produces overlap
polys = POLY.union(nupolys, 0.00001, true);
}
// process each area separately
let proc = 0;
let pinc = 1 / polys.length;
for (let area of polys) {
let bounds = area.getBounds3D();
let ts_off = toolDiam / 2 - ts_eps + (op.leave_xy ?? 0);
let offopt = {
arc: 250,
join: roundSharps ? ClipperLib.JoinType.jtRound : undefined,
clean: true,
simple: false,
cleanDist: 10,
minArea: 0.01,
};
if (outline) {
// remove inner voids when processing outline only
area.inner = undefined;
}
newLayer().output()
.setLayer("area", { line: 0xff8800 }, false)
.addPolys([ area ]);
newArea();
if (mode === 'clear') {
let zMov = flatOff ?? 0;
let zs = flats ?
flats.filter(z => z <= zTop && z >= zBottom).map(v => v + zMov) :
down ? base_util.lerp(zTop, zBottom, down) : [ bounds.min.z ];
let zroc = 0;
let zinc = 1 / zs.length;
let lzo;
if (!zs.length) break;
outer: for (;;)
for (let z of zs) {
let slice = newLayer(z);
let layers = slice.output();
let shadow = await shadowAt(z);
let tool_shadow = [
...POLY.offset(shadow, [ ts_off ], { count: 1, z, ...offopt }),
...POLY.offset(shadow, [ -ts_off ], { count: 1, z, ...offopt }),
];
// for roughing/outline backward compatability
if (op.omitthru) {
shadow = omitMatching(shadow, thruHoles);
}
// progressive offset of polygons inside area clipped to the shadow
let outs = [];
let clip = [];
let firstOff = -(toolDiam / 2 + (op.leave_xy ?? 0));
POLY.subtract([ area ], shadow, clip, undefined, undefined, 0);
POLY.offset(clip, [ firstOff, -toolOver ], {
count: op.walls ? 1 : (op.steps ?? 999), outs, flat: true, z: z - zMov, ...offopt
});
// if we see no offsets, re-check the mesh bottom Z then exit
if (outs.length === 0) {
if (bounds && lzo > bounds.min.z) {
// try a bottom layer matching bottom of selection
zs = [ bounds.min.z ];
bounds = undefined;
continue outer;
}
// terminate z descent when no further output possible
break outer;
}
// support legacy outline features
if (op.omitouter) {
outs = omitOuter(outs);
} else if (op.omitinner) {
outs = omitInner(outs);
}
// cut tabs when present
if (tabs.length) outs = cutTabs(tabs, outs);
// for roughing backward compatability
if (op.leave_z) {
for (let out of outs)
for (let p of out.points)
p.z += op.leave_z;
}
// add tabs to travel boundaries
if (tabs.length) {
let tab_shadows = tabs.filter(t => t.top >= z).map(t => t.poly);
if (tab_shadows) tool_shadow.push(...tab_shadows);
}
POLY.setWinding(outs, direction === 'climb');
// store travel boundary that triggers up and over moves
slice.tool_shadow = [ area, ...shadow, ...tool_shadow ];
slice.camLines = outs;
zroc += zinc;
lzo = z;
progress(proc + (pinc * zroc), 'clear');
if (devel) layers
.setLayer("base", { line: 0xff0000 }, false)
.addPolys(shadowBase)
.setLayer("shadow", { line: 0x00ff00 }, false)
.addPolys(shadow)
.setLayer("tool shadow", { line: 0x44ff88 }, false)
.addPolys(tool_shadow);
layers
.setLayer(rename ?? "clear", { line: color }, false)
.addPolys(outs);
// of the last output still cuts, we need an escape
if (z === zs.peek()) {
break outer;
}
}
proc += pinc;
progress(proc, 'clear');
} else
if (mode === 'trace') {
let { tr_over, tr_type } = op;
let zs = down ? base_util.lerp(zTop, op.thru ? zBottom : Math.max(zBottom, area.minZ()), down) : [ bounds.min.z ];
let zroc = 0;
let zinc = 1 / zs.length;
for (let z of zs) {
let slice = newLayer(z);
let layers = slice.output();
let shadow = await shadowAt(z);
let outs = [];
if (tr_type === 'none') {
// todo: move this out of the zs loop and only setZ when needed
area = area.clone(true);
outs = [ zs.length > 1 || op.thru ? area.setZ(z) : clampZ(area, zTop, zBottom) ];
} else {
// drape is legacy outline
let offit = op.drape ? shadow : [ area ];
if (op.omitthru && op.drape) {
offit = omitMatching(offit, thruHoles);
}
// todo: move this out of the zs loop
let stepping = tr_type === 'inside' ?
( tr_over ? -tr_over : [ -toolDiam / 2, -toolOver ] ) :
( tr_over ? tr_over : [ toolDiam / 2, toolOver ] );
POLY.offset(offit, stepping, {
count: op.steps ?? 1, outs, flat: true, z, minArea: 0, open: true
});
}
if (outs.length === 0 && !op.drape) {
// terminate z descent when no further output possible
break;
}
// add dogbones when specified
if (op.dogbones) outs.forEach(out => out.addDogbones(toolDiam / 5, op.revbones));
// support legacy outline features
if (op.omitouter) {
outs = omitOuter(outs);
} else if (op.omitinner) {
outs = omitInner(outs);
}
// cut tabs when present
if (tabs.length) outs = cutTabs(tabs, outs);
slice.camLines = outs;
POLY.setWinding(outs, direction === 'climb');
// store travel boundary that triggers up and over moves
let tool_shadow = slice.tool_shadow = shadow.clone(true);
if (area.isOpen()) {
tool_shadow.push(...outs[0].clone().setZ(z).offset_open(toolDiam / 2, 'round'));
} else {
tool_shadow.push(
area,
...POLY.offset(shadow, [ ts_off ], { count: 1, z, ...offopt }),
...POLY.offset(shadow, [ -ts_off ], { count: 1, z, ...offopt }),
);
}
// add tabs to travel boundaries
if (tabs) {
let tab_shadows = tabs.filter(t => t.top >= z).map(t => t.poly);
if (tab_shadows) slice.tool_shadow.push(...tab_shadows);
}
zroc += zinc;
progress(proc + (pinc * zroc), 'trace');
if (devel) layers
.setLayer("base", { line: 0xff0000 }, false)
.addPolys(shadowBase)
.setLayer("shadow", { line: 0x00ff00 }, false)
.addPolys(shadow)
.setLayer("tool shadow", { line: 0x44ff88 }, false)
.addPolys(tool_shadow);
layers
.setLayer(rename ?? "trace", { line: color }, false)
.addPolys(outs);
}
proc += pinc;
progress(proc, 'trace');
// legacy outline early termination b/c use of
// shadow will cause duplicate output with area
if (tr_type === 'outside' && op.drape) {
break;
}
} else
if (mode === 'surface') {
let { sr_type, sr_angle, sr_alter, tolerance } = op;
let resolution = tolerance || 0.05;
let raster = await self.get_raster_gpu({ mode: "tracing", resolution });
let surface = [];
let paths = [];
// prepare paths
if (sr_type === 'linear') {
// scan the area bounding box with rays at defined angle
let scan = scanBoxAtAngle(bounds, sr_angle * DEG2RAD, toolOver);
let lines = scan.map(line => {
let { a, b } = line;
return [ newPoint(a.x, a.y, 0).toClipper(), newPoint(b.x, b.y, 0).toClipper() ]
});
// use clipper to clip lines to the area polygon
let clip = new clib.Clipper();
let ctre = new clib.PolyTree();
clip.AddPaths(lines, ptyp.ptSubject, false);
clip.AddPaths(POLY.toClipper([ area ]), ptyp.ptClip, true);
if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) {
for (let node of ctre.m_AllPolys) {
paths.push(POLY.fromClipperNode(node, 0));
}
}
// in surfacing mode, direction is simply reversal
if (direction === 'climb') {
paths.forEach(path => path.reverse());
}
// optional alternating paths
if (paths.length && sr_alter) {
paths = tip2tipJoin(paths, paths[0].first(), toolOver * 10);
}
} else
if (sr_type === 'offset') {
// progressive inset from perimeter
POLY.offset([ area ], [ -toolDiam / 2, -toolOver ], {
count: 999, outs: paths, flat: true, z: 0, minArea: 0
});
paths.forEach(poly => poly.isClosed() && poly.push(poly.first()));
POLY.setWinding(paths.filter(p => p.isClosed()), direction === 'climb');
}
// convert resulting poly lines to raster float32 array groups
paths = paths.map(poly => poly.points.map(p => [ p.x, p.y ]).flat().toFloat32());
// prepare tool mesh points
let toolBounds = new THREE.Box3()
.expandByPoint({ x: -toolDiam/2, y: -toolDiam/2, z: 0 })
.expandByPoint({ x: toolDiam/2, y: toolDiam/2, z: 0 });
let toolPos = areaTool.generateProfile(resolution).profile.slice();
for (let i=0; i<toolPos.length; i+= 3) {
toolBounds.expandByPoint({ x: toolPos[i], y: toolPos[i+1], z: toolPos[i+2] });
}
let toolData = { positions: toolPos, bounds: toolBounds };
// prepare terrain and raster paths over terrain
let wbounds = bounds.clone().expandByVector({ x: toolDiam/2, y: toolDiam/2, z: 0 });
wbounds.min.z = zBottom;
wbounds.max.z = zTop;
await raster.loadTool({
sparseData: toolData
});
await raster.loadTerrain({
triangles: vertices,
boundsOverride: wbounds
});
if (paths.length === 0) {
// skip raster if no output generated
continue;
}
let output = await raster.generateToolpaths({
paths,
step: toolOver / 2,
zFloor: zBottom - 1,
onProgress: pct => progress(proc + (pinc * (pct/100)))
});
raster.terminate();
// convert terrain raster output back to open polylines
// todo: add leave_z support
for (let path of output.paths) {
path = newPolygon().fromArray([1, ...path]);
if (op.refine) path.refine(op.refine);
surface.push(path);
let slice = newLayer();
slice.camLines = [ path ];
slice.output()
.setLayer(rename ?? "linear", { line: color }, false)
.addPolys([ path ]);
}
// output this surface
surfaces.push(surface);
proc += pinc;
progress(proc);
}
}
// filter out empty slices
this.areas = areas = areas.map(area => {
return area.filter(slice => slice.camLines && slice.camLines.length);
}).filter(a => a.length);
// only render slices containing ares to mill
addSlices(areas.flat().filter(s => s.camLines && s.camLines.length));
}
prepare(ops, progress) {
let { op, state, areas, surfaces, toolOver } = this;
let { newLayer, pocket, polyEmit, printPoint, tip2tipEmit } = ops;
let { setContouring, setNextIsMove } = ops;
let { process } = state.settings;
// process surface paths
if (surfaces.length) {
setContouring(true, toolOver * 2);
for (let surface of surfaces) {
for (let poly of surface) {
setNextIsMove();
printPoint = polyEmit(poly);
newLayer();
}
}
setContouring(false);
// skip areas when processing surfaces
return;
}
// process areas as pockets
while (areas?.length) {
let min = {
dist: Infinity,
area: undefined
};
for (let area of areas.filter(p => !p.used)) {
// skip devel / debug only areas
let topPolys = area[0].camLines;
if (!topPolys) continue;
// select poly with largest area
let poly = topPolys.slice().sort((a,b) => b.area() - a.area())[0];
if (!poly) continue;
// compute move distance to top poly for efficient routing
let find = poly.findClosestPointTo(printPoint);
if (find.distance < min.dist) {
min.area = area;
min.dist = find.distance;
}
}
// if we have a next-closest top poly, pocket that
if (min.area) {
min.area.used = true;
pocket({
cutdir: op.ov_conv,
depthFirst: process.camDepthFirst && !op.drape,
easeDown: op.down && process.easeDown ? op.down : 0,
progress: (n,m) => progress(n/m, "area"),
slices: min.area.filter(slice => slice.camLines)
});
} else {
break;
}
}
}
}
function omitOuter(polys) {
let inner = [];
for (let poly of polys) {
if (poly.inner) inner.push(...poly.inner);
}
return inner;
}
function omitInner(polys) {
for (let poly of polys) {
poly.inner = undefined;
}
return polys;
}
function omitMatching(target, matches) {
target = target.clone(true);
for (let poly of target.filter(p => p.inner)) {
poly.inner = poly.inner.filter(inner => {
for (let ho of matches) {
if (inner.isEquivalent(ho)) {
return false;
}
}
return true;
});
}
return target;
}
function clampZ(poly, max, min) {
for (let p of poly.points) {
if (p.z < min) p.z = min;
else if (p.z > max) p.z = max;
}
if (poly.inner) {
for (let p of poly.inner) {
clampZ(p, max, min);
}
}
return poly;
}
// box2: THREE.Box2
// angle: radians (direction of each scan ray)
// step: spacing between parallel rays (world units)
function scanBoxAtAngle(box2, angle, step) {
const cx = (box2.min.x + box2.max.x) * 0.5;
const cy = (box2.min.y + box2.max.y) * 0.5;
const w = box2.max.x - box2.min.x;
const h = box2.max.y - box2.min.y;
// ray direction
const dx = Math.cos(angle);
const dy = Math.sin(angle);
// normal between rays (perpendicular to ray dir)
const nx = -dy;
const ny = dx;
// extent of the box along the normal
const extentN = Math.abs(nx) * w + Math.abs(ny) * h;
// length of each ray across the box (along ray dir)
const extentD = Math.abs(dx) * w + Math.abs(dy) * h;
// how many rays to cover the box; +1 so edges are covered
const count = Math.max(1, Math.ceil(extentN / step) + 1);
const halfSpan = step * (count - 1) * 0.5;
const halfD = extentD * 0.5;
const rays = [];
for (let i = 0; i < count; i++) {
// offset along normal
const o = -halfSpan + i * step;
const ox = cx + nx * o;
const oy = cy + ny * o;
// segment endpoints for this ray inside (or slightly outside) the box
const ax = ox - dx * halfD;
const ay = oy - dy * halfD;
const bx = ox + dx * halfD;
const by = oy + dy * halfD;
rays.push({
a: new THREE.Vector2(ax, ay),
b: new THREE.Vector2(bx, by),
});
}
return rays;
}
export { OpArea };

View file

@ -0,0 +1,170 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
import { Tool } from '../core/tool.js';
import { generate as Topo } from './topo3.js';
import { newPoint } from '../../../../geo/point.js';
import { newPolygon } from '../../../../geo/polygon.js';
import { newSlice } from '../../../core/slice.js';
import { tip2tipEmit } from '../../../../geo/paths.js';
function createFilter(op, origin, axis) {
// console.log({ origin, axis });
let ok = () => true;
let filter = slices => slices;
let filterString = op.filter?.map(l => l.trim()).join('\n');
if (filterString) {
try {
const obj = eval(`( ${filterString} )`);
let box = obj?.box;
let slice_fn = obj?.slices;
let index = 0;
const accept = [];
filter = function (slices) {
for (let slice of slices) {
if (slice_fn && slice_fn(slice, index++)) {
accept.push(slice);
} else if (box) {
// slice.z = x when axis = y
// slice.z = y when axis = x
let { x, y, z } = box;
x = x ?? [ -Infinity, Infinity ];
y = y ?? [ -Infinity, Infinity ];
z = z ?? [ -Infinity, Infinity ];
let ok = false;
if (axis === 'x') {
let sy = slice.z + origin.y;
if (sy >= y[0] && sy <= y[1]) {
ok = true;
for (let p of slice.camLines) {
p.points = p.points.filter(p =>
p.x - origin.x >= x[0] && p.x - origin.x <= x[1] &&
p.z - origin.z >= z[0] && p.z - origin.z <= z[1]
);
}
}
} else {
let sx = slice.z - origin.x;
if (sx >= x[0] && sx <= x[1]) {
ok = true;
for (let p of slice.camLines) {
p.points = p.points.filter(p =>
p.y + origin.y >= y[0] && p.y + origin.y <= y[1] &&
p.z - origin.z >= z[0] && p.z - origin.z <= z[1]
);
}
}
}
if (ok) {
slice.camLines = slice.camLines.map(p => {
if (p.points.length > 1) {
return newPolygon(p.points).setOpen(true);
} else {
return undefined;
}
}).filter(p => p);
accept.push(slice);
}
}
}
return accept;
};
} catch (e) {
console.log('filter parse error', e, op.filter);
}
}
return filter;
}
class OpContour extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let { op, state } = this;
let { color, addSlices, settings, updateToolDiams } = state;
let conTool = new Tool(settings, op.tool);
let filter = createFilter(op, settings.origin, op.axis.toLowerCase());
let toolDiam = this.toolDiam = conTool.fluteDiameter();
this.toolStep = conTool.getStepSize(op.step);
updateToolDiams(toolDiam);
// we need topo for safe travel moves when roughing and outlining
// not generated when drilling-only. then all z moves use bounds max.
// also generates x and y contouring when selected
let topo = this.topo = await Topo({
// onupdate: (update, msg) => {
onupdate: (index, total, msg) => {
progress(index / total, msg);
},
ondone: (slices) => {
slices = filter(slices);
this.sliceOut = slices;
addSlices(slices);
for (let slice of slices) {
slice.output()
.setLayer(`contour ${op.axis}`, { face: color, line: color })
.addPolys(slice.camLines);
}
},
contour: op,
state
});
if (this.debug && topo.coastline) {
console.log('coastline', topo.coastline);
const dbs = newSlice(-1);
const dbo = dbs.output();
dbo.setLayer("coastline", { line: 0x0000dd }).addPolys(topo.coastline);
addSlices([ dbs ])
}
// computed if set to 0
this.tolerance = topo.tolerance;
}
prepare(ops, progress) {
let { op, sliceOut, state, toolStep, topo } = this;
let { settings } = state;
let { process } = settings;
let { polyEmit, setContouring, setTolerance, setTool } = ops;
let { widget, newLayer, zmax } = ops;
let bounds = widget.getBoundingBox();
let depthData = [];
setTool(op.tool, op.rate, process.camFastFeedZ);
setContouring(true, toolStep * 1.5, topo.coastline);
setTolerance(this.tolerance);
let printPoint = newPoint(bounds.min.x, bounds.min.y, zmax);
for (let slice of sliceOut) {
// ignore debug slices
if (!slice.camLines) {
continue;
}
let polys = [], poly;
slice.camLines.forEach((poly) => {
poly = poly.clone(true).annotate({ slice: slice.index + 1 });
polys.push({ first: poly.first(), last: poly.last(), poly: poly });
});
depthData.appendAll(polys);
}
tip2tipEmit(depthData, printPoint, (el, point) => {
let poly = el.poly;
if (poly.last() === point) {
poly.reverse();
}
polyEmit(poly);
newLayer();
});
}
}
export { OpContour };

View file

@ -0,0 +1,74 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
import { Tool } from '../core/tool.js';
import { newPolygon } from '../../../../geo/polygon.js';
import { newSlice } from '../../../core/slice.js';
import { newPoint } from '../../../../geo/point.js';
class OpDrill extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let { op, state } = this;
let { settings, addSlices, widget, updateToolDiams } = state;
let { color } = state;
let { drills } = op
let drillTool = new Tool(settings, op.tool),
drillToolDiam = drillTool.fluteDiameter(),
sliceOut = this.sliceOut = [];
const allDrills = drills[widget.id] ?? []
if (allDrills.length === 0) return;
updateToolDiams(drillToolDiam);
// drill points to use center (average of all points) of the polygon
allDrills.forEach((drill) => {
if (!drill.selected) {
return
}
let slice = newSlice(0);
if (op.mark) {
// replace depth with single down peck
drill.depth = op.down
}
drill.zBottom = drill.z - drill.depth;
// for thru holes, follow z thru when set
if ((op.thru > 0)) {
drill.zBottom -= op.thru;
}
const poly = newPolygon()
poly.points.push(newPoint(drill.x, drill.y, drill.z))
poly.points.push(newPoint(drill.x, drill.y, drill.zBottom))
slice.camTrace = { tool: op.tool, rate: op.feed, plunge: op.rate };
slice.camLines = [poly];
slice.output()
.setLayer(state.layername, { face: color, line: color })
.addPolys(slice.camLines);
addSlices(slice);
sliceOut.push(slice);
});
}
prepare(ops, progress) {
let { op, sliceOut } = this;
let { setTool, setSpindle, setDrill, emitDrills } = ops;
if (sliceOut.length === 0) return;
setTool(op.tool, undefined, op.rate);
setDrill(op.down, op.lift, op.dwell);
emitDrills(sliceOut.map(slice => slice.camLines).flat());
}
}
export { OpDrill };

View file

@ -0,0 +1,13 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
export class OpGCode extends CamOp {
constructor(state, op) {
super(state, op);
}
prepare(ops, progress) {
ops.addGCode(this.op.gcode);
}
}

View file

@ -0,0 +1,274 @@
import { arcToPath } from "../../../../geo/paths.js";
import { newPolygon } from "../../../../geo/polygon.js";
import { CamOp } from "../core/op.js";
import { Tool } from "../core/tool.js";
import { cylinder_poly_find } from "./slice.js";
import { newPoint } from "../../../../geo/point.js";
import { newSlice } from "../../../core/slice.js";
export class OpHelical extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let { op, state } = this;
let { addSlices, color, settings, updateToolDiams, widget } = state;
let { stock } = settings
let {
cylinders,
forceStartAng,
startAng,
fromTop,
offOver,
offset,
down,
thru,
finish,
clockwise,
reverse,
entry,
entryOffset,
} = op;
let tool = new Tool(settings, op.tool),
toolDiam = tool.fluteDiameter(),
sliceOut = (this.sliceOut = []),
startAngle = forceStartAng ? startAng : 0;
updateToolDiams(toolDiam);
// console.log({cylinders});
let faces = cylinders[widget.id] ?? [];
if (faces.faces) faces = faces.faces;
let polys = [];
//iterate over selected faces
for (let [i, face] of faces.entries()) {
//get poly info for the cylinder
let res
try {
res = cylinder_poly_find(
widget,
face
);
} catch (error) {
// don't quit if a cylinder errors out, just skip
console.error(error)
continue
}
let { zmin, zmax, diam, center, interior, faces } = res;
// console.log({ zmin, zmax, diam, center, interior });
let radius = diam / 2;
zmax = fromTop ? stock.z : zmax;
let radAdd,
zBottom = zmin - thru,
numSegs = faces.length / 2,
poly = newPolygon().setOpen();
if (offOver) {
//apply offset override
if (offset == "auto") {
radAdd = interior ? -offOver : offOver;
} else if (offset == "inside") {
radAdd = -offOver;
} else if (offset == "outside") {
radAdd = offOver;
}
} else {
//otherwise, calculate based on offset type
if (offset == "auto") {
radAdd = (interior ? -toolDiam : toolDiam) / 2;
} else if (offset == "inside") {
radAdd = -toolDiam / 2;
} else if (offset == "outside") {
radAdd = toolDiam / 2;
}
}
radius += radAdd;
if (radius <= 0) {
//if negative radius, skip this cylinder
console.log('negative radius', radius);
continue
}
let startPoint = center
.clone()
.setZ(zmax)
.add(
newPoint(
Math.cos(startAngle) * radius,
Math.sin(startAngle) * radius,
0
)
);
let currentZ = zmax;
for (; ;) {
let bottom = currentZ - down;
if (bottom > zBottom) {
//if not at the bottom, do a whole helix
let start = startPoint.setZ(currentZ),
end = startPoint.clone().setZ(bottom);
// console.log(currentZ, bottom, [start.z, end.z]);
let path = arcToPath(start, end, numSegs, {
clockwise,
radius,
center,
});
// console.log({ path, start, end, radius, center });
poly.addPoints(path);
} else {
//if at the bottom, do a partial helix
let toBottom = currentZ - zBottom,
ofFull = toBottom / down,
ofCircle = ofFull * (2 * Math.PI),
cwMultiplier = clockwise ? -1 : 1,
finalAngle = startAngle + (ofCircle * cwMultiplier),
finalEnd = center
.clone()
.setZ(zBottom)
.add(
newPoint(
Math.cos(finalAngle) * radius,
Math.sin(finalAngle) * radius,
0
)
);
// console.log({down,toBottom,ofFull,ofCircle,startAngle,finalAngle,finalEnd});
//if not already very close to final point
if (ofCircle >= 0.001) {
//circle down to final point
poly.addPoints(
arcToPath(startPoint.setZ(currentZ), finalEnd, numSegs, {
clockwise,
radius,
center,
})
);
}
//if starting at the bottom, reverse poly
if (reverse) {
poly.reverse();
}
if (entry) {
//get first z of poly to use for entry
let firstZ = poly.first().z;
//create new pointArray
let entry = [newPoint(0, 0, 0)];
//add center point
let entryCenter = center.clone().setZ(firstZ)
//if doing a curved enter, add more
if (entryOffset > 0) {
if (entryOffset > radius) {
console.error("entryOffset must be less than radius of helix");
} else {
//calculate entry center
let lineOut = radius - entryOffset;
let centerOffset = (entryOffset ** 2 + 2 * entryOffset * lineOut) / (2 * (entryOffset + lineOut));
//add entry points
entry.push(
newPoint(0, -lineOut, 0),
...arcToPath(
newPoint(0, -lineOut, 0),
newPoint(radius, 0, 0),
128,
{
clockwise: false,
radius: lineOut,
center: newPoint(centerOffset, 0, 0)
}
),
newPoint(radius, 0, 0),
)
}
}
//for each point
//rotate by start angle
// add to hole center
let invert = ((clockwise && !reverse) || (!clockwise && reverse)) ? -1 : 1
let entryAngle = reverse ? finalAngle : startAngle
entry = entry.map(p => {
// TODO: there's totally a way to do this with a matrix
// logical XOR
p.y *= invert
p.rotate(entryAngle)
return p.add(entryCenter)
})
//append to start of poly
poly = newPolygon().setOpen().addPoints(entry).addPoints(poly.points);
}
//if doing a finish pass, do a full circle at the bottom
if (finish && !reverse) {
poly.addPoints(
arcToPath(finalEnd, finalEnd, numSegs, {
clockwise,
radius,
center,
}),
);
poly.append(
finalEnd.clone().setZ(zBottom),
)
}
break;
}
currentZ = bottom;
}
progress(i / faces.length, "Helical intrerpolation");
polys.push(poly);
}
let slice = newSlice(0);
slice.camTrace = { tool: op.tool, rate: op.feed, plunge: op.rate };
slice.camLines = polys;
slice
.output()
.setLayer("Helical", { face: color, line: color })
.addPolys(slice.camLines);
addSlices(slice);
sliceOut.push(slice);
}
async prepare(ops, progress) {
let { clearTravelBounds, polyEmit, printPoint, setNextIsMove, setTool } = ops;
let { tool, spindle, rate, feed } = this.op;
setTool(tool, feed, rate);
clearTravelBounds();
let [ polys ] = this.sliceOut.map((s) => s.camLines);
polys = polys.slice();
for (;;) {
let closestDist = Infinity,
closestI,
closest = null,
dist,
poly;
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) break;
poly = polys[closestI]
polys[closestI] = null;
printPoint = polyEmit(poly);
}
}
}

View file

@ -0,0 +1,47 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
export class OpIndex extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let { op, state } = this;
if (!state.isIndexed) {
throw 'index op requires indexed stock';
}
let { lastAxisIndex, widget, updateSlicer, computeShadows, setAxisIndex } = state;
let { degrees, absolute } = op;
if (absolute && lastAxisIndex === degrees) {
return console.log('skip redundant absolute index');
} else if (!absolute && degrees === 0) {
return console.log('skip redundant relative index');
}
this.degrees = await setAxisIndex(degrees, absolute);
state.lastAxisIndex = this.degrees;
// force recompute of topo
widget.topo = undefined;
// update slicer for new widget geometry
updateSlicer();
// recompute shadow from new widget geometry
await computeShadows(progress);
}
prepare(ops, progress) {
let { camOut, printPoint, zSafe } = ops;
// max point of stock corner radius when rotating (safe z when indexing)
// move above rotating stock
camOut(printPoint = printPoint.clone().setZ(zSafe), 0);
// issue rotation command
camOut(printPoint = printPoint.clone().setZ(zSafe).setA(this.degrees), 0);
}
}

View file

@ -0,0 +1,17 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
export class OpLaserOff extends CamOp {
constructor(state, op) {
super(state, op);
}
prepare(ops, progress) {
const { printPoint, zmax, camOut } = ops;
this.op.silent = true;
ops.addGCode(this.op.disable);
ops.setLasering(false);
camOut(printPoint.clone().setZ(zmax), 0);
}
}

View file

@ -0,0 +1,17 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
export class OpLaserOn extends CamOp {
constructor(state, op) {
super(state, op);
}
prepare(ops, progress) {
const { printPoint, setPrintPoint, setTool, zmax, camOut } = ops;
this.op.silent = true;
setTool(0);
ops.addGCode(this.op.enable);
ops.setLasering(true, this.op.power);
}
}

View file

@ -0,0 +1,86 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
import { generate as topo4_generate } from './topo4.js';
import { newPoint } from '../../../../geo/point.js';
function createFilter(op) {
let ok = () => true;
let filter = slices => slices;
let filterString = op.filter?.map(l => l.trim()).join('\n');
if (filterString) {
try {
const obj = eval(`( ${filterString} )`);
const accept = [];
let index = 0;
let sl_fn = obj?.slices ?? ok;
filter = function (slices) {
for (let slice of slices) {
if (sl_fn(slice, index++)) {
accept.push(slice);
}
}
return accept;
};
} catch (e) {
console.log('filter parse error', e, op.filter);
}
}
return filter;
}
class OpLathe extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let { op, state } = this;
let { addSlices, color } = state;
let filter = createFilter(op);
this.topo = await topo4_generate({
op,
state,
onupdate: (pct, msg) => {
progress(pct, msg);
},
ondone: (slices) => {
slices = filter(slices);
this.slices = slices;
addSlices(slices, false);
}
});
}
prepare(ops, progress) {
let { slices, state, topo } = this;
let { camOut, newLayer, setContouring, setNextIsMove, zSafe } = ops;
let { gcodeResetA } = state.settings.device;
// start top center, X = 0, Y = 0 closest to 4th axis chuck
camOut(newPoint(0, 0, zSafe).setA(0), 0);
setContouring(true);
setNextIsMove();
for (let slice of slices) {
// ignore debug slices
if (!slice.camLines) {
continue;
}
for (let path of slice.camLines) {
for (let point of path.points) {
camOut(point);
}
}
newLayer();
}
// move to safe height and reset A axis
newLayer();
ops.addGCode([`G0 Z${zSafe.round(2)}`, gcodeResetA.join('\n') ?? "G92.4 A0 R0"]);
}
}
export { OpLathe };

View file

@ -0,0 +1,91 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
import { Tool } from '../core/tool.js';
import { newPolygon } from '../../../../geo/polygon.js';
import { polygons as POLY } from '../../../../geo/polygons.js';
import { newSlice } from '../../../core/slice.js';
import { util } from '../../../../geo/base.js';
class OpLevel extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let { op, state } = this;
let { addSlices, color, settings, shadow } = state;
let { share, updateToolDiams, zMax, ztOff } = state;
let { down, tool, step, stepz, inset } = op;
let { stock } = settings;
let { center } = stock;
let toolDiam = new Tool(settings, tool).fluteDiameter();
let stepOver = this.stepOver = toolDiam * step;
let wpos = state.widget.track.pos;
let zTop = zMax + ztOff;
let zBot = zTop - down;
let zList = stepz && down ? util.lerp(zTop, zBot, stepz) : [ zBot ];
if (share.ran) {
console.log('skip');
this.skip = true;
return;
} else if (op.stock) {
share.ran = true;
}
updateToolDiams(toolDiam);
let points = [];
let clear = op.stock ?
[ newPolygon().centerRectangle({
x: -wpos.x + center.x,
y: -wpos.y + center.y,
z: wpos.z + center.z
}, stock.x + toolDiam/2, stock.y) ] :
POLY.outer(POLY.offset(shadow.base, toolDiam * (inset || 0)));
POLY.fillArea(clear, 1090, stepOver, points);
let layers = this.layers = [];
for (let z of zList) {
let lines = [];
layers.push(lines);
for (let i=0; i<points.length; i += 2) {
let slice = newSlice(z);
lines.push( newPolygon().setOpen().addPoints([ points[i], points[i+1] ]).setZ(z) );
slice.output()
.setLayer("level", {face: color, line: color})
.addPolys(lines);
addSlices(slice);
}
}
}
prepare(ops, progress) {
let { layers, skip, stepOver } = this;
let { printPoint } = ops;
let { newLayer, tip2tipEmit, camOut } = ops;
if (skip) {
return;
}
for (let lines of layers) {
lines = lines.map(p => { return { first: p.first(), last: p.last(), poly: p } });
printPoint = tip2tipEmit(lines, printPoint, (el, point, count) => {
let poly = el.poly;
if (poly.last() === point) {
poly.reverse();
}
poly.forEachPoint((point, pidx) => {
camOut(point.clone(), true, stepOver);
}, false);
});
newLayer();
}
}
}
export { OpLevel };

View file

@ -0,0 +1,17 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
export class OpLoop extends CamOp {
weight() {
return 0; // No weight, acts as marker
}
async slice() {
// No-op: expansion happens in slice.js
}
prepare() {
// No-op: ignored during g-code generation
}
}

View file

@ -0,0 +1,92 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
import { OpArea } from './op-area.js';
class OpOutline extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let { op, state } = this;
let { direction, dogbones, down, inside, omitthru, omitvoid, outside } = op;
let { ov_botz, ov_topz, plunge, rate, rename, revbones, spindle, tool } = op;
let { shadow, widget } = state;
let shadow_base = shadow.base;
if (!(shadow_base && shadow_base.length)) {
throw 'missing shadow base';
}
let ops_list = this.ops_list = [ ];
if (outside) {
let areas = shadow.base.clone(true);
ops_list.push(new OpArea(state, {
areas: { [widget.id]: areas.map(p => p.toArray()) },
direction,
dogbones,
down,
expand: 0,
mode: 'trace',
omitinner: omitvoid,
omitthru,
ov_botz,
ov_topz,
over: op.wide ? op.step : 0,
plunge,
rate,
rename: rename ?? "outline",
revbones,
rotated: true,
smooth: 0,
spindle,
steps: op.wide ? op.steps + 1 : 1,
surfaces: {},
tool,
thru: true,
tr_type: 'outside',
}));
} else {
let areas = shadow.base.clone(true);
ops_list.push(new OpArea(state, {
areas: { [widget.id]: areas.map(p => p.toArray()) },
direction,
dogbones,
down,
drape: true,
expand: 0,
mode: 'trace',
omitinner: omitvoid,
omitouter: inside,
omitthru,
ov_botz,
ov_topz,
plunge,
rate,
rename: rename ?? "outline",
revbones,
rotated: true,
smooth: 0,
spindle,
surfaces: {},
tool,
thru: true,
tr_type: 'outside',
}));
}
for (let op of this.ops_list) {
await op.slice(progress);
}
}
async prepare(ops, progress) {
for (let op of this.ops_list) {
await op.prepare(ops, progress);
}
}
}
export { OpOutline };

View file

@ -0,0 +1,47 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
import { OpArea } from './op-area.js';
class OpPocket extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let { op, state } = this;
let { contour, direction, down, expand, follow, outline, ov_botz, ov_topz } = op;
let { plunge, rate, refine, smooth, spindle, surfaces, tolerance, tool } = op;
let pocket = {
areas: {},
direction,
down,
expand,
follow,
mode: contour ? 'surface' : 'clear',
outline,
ov_botz,
ov_topz,
over: op.step,
plunge,
rate,
refine,
rename: op.rename ?? "pocket",
smooth,
spindle,
sr_type: 'offset',
surfaces,
tolerance,
tool,
tr_type: 'none',
};
this.op_pocket = new OpArea(state, pocket);
return this.op_pocket.slice(progress);
}
prepare(ops, progress) {
return this.op_pocket.prepare(ops, progress);
}
}
export { OpPocket };

View file

@ -0,0 +1,166 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
import { Tool } from '../core/tool.js';
import { newPolygon } from '../../../../geo/polygon.js';
import { newSlice } from '../../../core/slice.js';
import { newPoint } from '../../../../geo/point.js';
import { util } from '../../../../geo/base.js';
class OpRegister extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let { op, state } = this;
let { addSlices, bounds, color, settings, widget } = state;
let { updateToolDiams } = state;
let tool = new Tool(settings, op.tool);
let sliceOut = this.sliceOut = [];
let axis = op.axis.toLowerCase();
updateToolDiams(tool.fluteDiameter());
let { stock } = settings;
let { pos } = widget.track;
let toolZ = pos.z,
boundMinX = bounds.min.x,
boundMaxX = bounds.max.x,
boundMinY = bounds.min.y,
boundMaxY = bounds.max.y,
toolOffset = tool.fluteDiameter() * 2,
centerX = (boundMinX + boundMaxX) / 2,
centerY = (boundMinY + boundMaxY) / 2,
cutDepth = op.thru || 0,
pathPoints = [],
stockToSurfaceOffset = stock.z - bounds.max.z,
startZ = bounds.max.z + stockToSurfaceOffset + toolZ,
endZ = toolZ - cutDepth,
cutOffset = op.offset;
if (!(stock.x && stock.y && stock.z)) {
return;
}
switch (axis) {
case "x":
if (op.points == 3) {
pathPoints.push(newPoint(boundMinX - cutOffset, centerY, 0));
pathPoints.push(newPoint(boundMaxX + cutOffset, centerY - toolOffset, 0));
pathPoints.push(newPoint(boundMaxX + cutOffset, centerY + toolOffset, 0));
} else {
pathPoints.push(newPoint(boundMinX - cutOffset, centerY, 0));
pathPoints.push(newPoint(boundMaxX + cutOffset, centerY, 0));
}
break;
case "y":
if (op.points == 3) {
pathPoints.push(newPoint(centerX, boundMinY - cutOffset, 0));
pathPoints.push(newPoint(centerX - toolOffset, boundMaxY + cutOffset, 0));
pathPoints.push(newPoint(centerX + toolOffset, boundMaxY + cutOffset, 0));
} else {
pathPoints.push(newPoint(centerX, boundMinY - cutOffset, 0));
pathPoints.push(newPoint(centerX, boundMaxY + cutOffset, 0));
}
break;
case "-":
case "=":
let halfOffset = toolOffset / 2,
loopMinX = boundMinX - cutOffset,
loopMaxX = boundMaxX + cutOffset,
loopMinY = boundMinY - cutOffset - halfOffset,
loopMaxY = boundMaxY + cutOffset + halfOffset,
deltaX = (loopMaxX - loopMinX - halfOffset) / 4,
deltaY = (loopMaxY - loopMinY - halfOffset * 3) / 4,
poly, currentPoint, currentZ;
function start(z) {
currentZ = z;
currentPoint = { x: loopMinX + halfOffset * 0.5, y: loopMinY + halfOffset * 1.5 };
poly = newPolygon().add(currentPoint.x, currentPoint.y, z);
}
function move(dx, dy) {
currentPoint.x += dx;
currentPoint.y += dy;
poly.add(currentPoint.x, currentPoint.y, currentZ);
}
function rept(count, step, fn) {
while (count-- > 0) {
fn(step, count);
step = -step;
}
}
for (let z of util.lerp(startZ, endZ, op.down)) {
let slice = newSlice(z);
addSlices(slice);
sliceOut.push(slice);
start(z);
rept(4, halfOffset, (oy, count) => {
move(0, -oy);
if (axis === '=' && count % 2 === 1) {
move(deltaX/2, 0);
move(0, oy/2);
move(0, -oy/2);
move(deltaX/2, 0);
} else {
move(deltaX, 0);
}
});
rept(4, halfOffset, ox => {
move(ox, 0);
move(0, deltaY);
});
rept(4, halfOffset, oy => {
move(0, oy);
move(-deltaX, 0);
});
rept(4, halfOffset, ox => {
move(-ox, 0);
move(0, -deltaY);
});
poly.points.pop();
slice.camTrace = { tool: tool.getID(), rate: op.feed, plunge: op.rate };
slice.camLines = [ poly ];
slice.output()
.setLayer("register", { line: color }, false)
.addPolys(slice.camLines)
}
break;
}
if (pathPoints.length) {
let slice = newSlice(0,null), polys = [];
pathPoints.forEach(point => {
polys.push(newPolygon()
.append(point.clone().setZ(startZ))
.append(point.clone().setZ(endZ)));
});
slice.camLines = polys;
slice.output()
.setLayer("register", {face: color, line: color})
.addPolys(polys);
addSlices(slice);
sliceOut.push(slice);
}
}
prepare(ops, progress) {
let { op } = this;
let { emitDrills, setDrill, setTool, setTravelBoundary } = ops;
setTravelBoundary();
if (op.axis === '-' || op.axis === '=') {
setTool(op.tool, op.feed, op.rate);
for (let slice of this.sliceOut) {
ops.emitTrace(slice);
}
} else {
setTool(op.tool, undefined, op.rate);
setDrill(op.down, op.lift, op.dwell);
emitDrills(this.sliceOut.map(slice => slice.camLines).flat());
}
}
}
export { OpRegister };

View file

@ -0,0 +1,131 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
import { OpArea } from './op-area.js';
import { newPolygon } from '../../../../geo/polygon.js';
import { polygons as POLY } from '../../../../geo/polygons.js';
class OpRough extends CamOp {
constructor(state, op) {
super(state, op);
}
// todo: cutThruBypass
async slice(progress) {
let { op, state } = this;
let { newSlicer, shadow, stock, tool, widget } = state;
let cutOutside = !op.inside;
let shadowBase = shadow.base;
if (op.down <= 0) {
throw `invalid step down "${op.down}"`;
}
if (op.all) {
shadowBase = [ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ];
}
let areas = POLY.flatten(POLY.expand(shadowBase, tool.fluteDiameter() / 2 - 0.001));
let ops_list = this.ops_list = [ ];
ops_list.push(new OpArea(state, {
rename: op.rename ?? "rough",
spindle: op.spindle,
direction: op.direction,
tool: op.tool,
rate: op.rate,
plunge: op.plunge,
mode: 'clear',
over: op.step,
down: op.down,
expand: 0,
smooth: 0,
outline: true,
omitthru: op.omitthru,
leave_xy: op.leave,
leave_z: op.leavez,
ov_botz: op.ov_botz,
ov_topz: op.ov_topz,
rotated: true,
areas: { [widget.id]: areas.map(p => p.toArray()) },
surfaces: {}
}));
if (op.flats) {
let slicer = newSlicer({ zflatup: true });
ops_list.push(new OpArea(state, {
rename: op.rename ?? "flats",
spindle: op.spindle,
direction: op.direction,
tool: op.tool,
rate: op.rate,
plunge: op.plunge,
mode: 'clear',
over: op.step,
down: op.down,
expand: 0,
smooth: 0,
outline: true,
omitthru: op.omitthru,
leave_xy: op.leave,
leave_z: op.leavez,
ov_botz: op.ov_botz,
ov_topz: op.ov_topz,
rotated: true,
areas: { [widget.id]: areas.map(p => p.toArray()) },
surfaces: {},
flats: Object.keys(slicer.zFlat).map(v => parseFloat(v)).sort((a,b) => b-a),
flatOff: 0.01
}));
}
// outside only if we're not clearing all of stock
if (cutOutside && !op.all) {
ops_list.push(new OpArea(state, {
rename: op.rename ?? "cutout",
spindle: op.spindle,
direction: op.direction === 'climb' ? 'conventional' : 'climb',
tool: op.tool,
rate: op.rate,
plunge: op.plunge,
mode: 'trace',
tr_type: 'none',
down: op.down,
expand: 0,
smooth: 1,
outline: !op.omitthru,
ov_botz: op.ov_botz,
ov_topz: op.ov_topz,
rotated: true,
areas: { [widget.id]: areas.map(p => p.toArray()) },
surfaces: {},
thru: true
}));
}
let len = ops_list.length;
let per = 1 / len;
let bas = 0;
for (let op of ops_list) {
await op.slice(pct => progress(bas + pct * per));
bas += per;
}
}
async prepare(ops, progress) {
let { ops_list } = this;
let { setChangeOp } = ops;
let len = ops_list.length;
let per = 1 / len;
let bas = 0;
for (let op of ops_list) {
await op.prepare(ops, pct => progress(bas + pct * per));
bas += per;
setChangeOp();
}
}
}
export { OpRough };

View file

@ -0,0 +1,103 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
import { newSlice } from '../../../core/slice.js';
import { newPolygon } from '../../../../geo/polygon.js';
/**
* Computes the Part "shadow" and attaches relevant data to the "state" object
*
* The part shadow consists of top-cown layers at which the polygon shadow changes
* shape. For curved or sloped surfaces, this is approximated and paths that clip
* to it should use the next lower layer from current Z to ensure no part collisions.
*
* The shadow at each layer is computed by top-down unioning the part outline with
* the shadow from the layer above.
*
* This operation is injected at the start of the operation chain before processing.
*/
class OpShadow extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let state = this.state;
let { addSlices, settings, shadowAt, unsafe, widget, workarea } = state;
let { devel } = settings.controller;
let bounds = widget.getBoundingBox();
let slices = [];
let shadowBase;
let skipTerrain = unsafe;
let terrain = [];
let tzindex = [];
let minZ = Math.floor(bounds.min.z);
let maxZ = Math.floor(bounds.max.z);
for (let z = maxZ; z >= minZ; z--) {
tzindex.push(z);
}
if (skipTerrain) {
let { stock } = settings;
let { center } = stock;
console.log("skipping terrain generation");
shadowBase = [ newPolygon().centerRectangle(center, stock.x, stock.y) ];
tzindex = [ ];
state.shadow = {
base: shadowBase, // computed shadow union at base of part
holes: [],
skip: skipTerrain,
slices: [], // legacy / transitional
stack: [], // stack of shadow slices
};
return;
}
// distributed pre-fill shadow layer cache
await widget.computeShadowStack(tzindex, prog => progress(prog / 2, 'shadow'));
// terrain is the "shadow stack" where index 0 = top of part
// thus array.length -1 = bottom of part
for (let i=0; i<tzindex.length; i++) {
let z = tzindex[i];
let shadow = shadowBase = await shadowAt(z);
let slice = newSlice(z);
slice.shadow = shadow;
slice.addTops(shadow);
slices.push(slice);
terrain.push({ slice, tops: shadow });
if (devel) {
slice.output()
.setLayer("shadow", {line: 0x888800, thin: true })
.addPolys(shadow, { thin: true });
}
progress(0.5 + (i / tzindex.length) * 0.5, 'shadow');
}
if (devel && slices.length) {
addSlices(slices);
}
if (terrain.length === 0) {
throw `invalid widget shadow`;
}
// TODO: refactor ops to use a unified shadow object
state.shadow = {
base: shadowBase, // computed shadow union at base of part
holes: shadowBase.map(p => p.inner || []).flat(),
skip: skipTerrain,
slices: slices, // legacy / transitional
stack: terrain, // stack of shadow slices
};
}
}
export { OpShadow };

View file

@ -0,0 +1,53 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
import { OpArea } from './op-area.js';
class OpTrace extends CamOp {
constructor(state, op) {
super(state, op);
}
// todo: cut thru, wide steps
async slice(progress) {
let { op, state } = this;
let { areas, direction, down, expand, follow, offover, offset, outline, mode, ov_botz, ov_topz } = op;
let { plunge, rate, refine, smooth, spindle, step, steps, thru, tolerance, tool } = op;
let trace = {
areas,
direction,
dogbones: op.dogbone,
down,
expand,
follow,
mode: mode === 'clear' ? 'clear' : 'trace',
outline,
ov_botz,
ov_topz,
over: op.step,
plunge,
rate,
refine,
rename: op.rename ?? "trace",
revbones: op.revbone,
smooth,
spindle,
step,
surfaces: {},
tolerance,
tool,
thru,
tr_over: offover,
tr_type: offset
};
this.op_trace = new OpArea(state, trace);
return this.op_trace.slice(progress);
}
async prepare(ops, progress) {
return this.op_trace.prepare(ops, progress);
}
}
export { OpTrace };

View file

@ -0,0 +1,32 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from '../core/op.js';
import { newSlice } from '../../../core/slice.js';
import { Slicer } from './slicer-cam.js';
class OpXRay extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let { widget, addSlices } = this.state;
let slicer = new Slicer(widget);
let xrayind = Object.keys(slicer.zLine)
.map(v => parseFloat(v).round(5))
.sort((a,b) => a-b);
let xrayopt = { each: data => {
let slice = newSlice(data.z);
slice.addTops(data.tops);
// data.tops.forEach(top => slice.addTop(top));
slice.lines = data.lines;
slice.xray();
addSlices(slice);
}, over: false, flatoff: 0, edges: true, openok: true };
await slicer.slice(xrayind, xrayopt);
// xrayopt.over = true;
// slicer.slice(xrayind, xrayopt);
}
}
export { OpXRay };

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,870 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CAM } from './init-work.js';
import { newLine } from '../../../../geo/line.js';
import { newPoint } from '../../../../geo/point.js';
import { newPolygon } from '../../../../geo/polygon.js';
import { ops as OPS } from '../core/ops.js';
import { polygons as POLY } from '../../../../geo/polygons.js';
import { setSliceTracker } from '../../../core/slice.js';
import { Slicer as cam_slicer } from './slicer-cam.js';
import { Tool } from '../core/tool.js';
import { util } from '../../../../geo/base.js';
// state shared across all widget slicing
let shared;
/**
* DRIVER SLICE CONTRACT
*
* global slice operation run before individual widget slice() operations
*/
export function cam_slice_pre(settings) {
shared = { ops: { } };
}
/**
* DRIVER SLICE CONTRACT
*
* slice operations run per-widget between slicePre and sliceAll
*
* @param {Object} settings
* @param {Widget} widget
* @param {Function} output
*/
export async function cam_slice(settings, widget, onupdate, ondone) {
if (widget.track.synth) return ondone();
// get tab widgets
let tabW = widget.group.filter(w => w != widget);
// merge overlapping tabs
for (let i=0; i<tabW.length; i++) {
for (let j=i+1; j<tabW.length; j++) {
let t1 = tabW[i];
let t2 = tabW[j];
let b1 = t1.mesh.getBoundingBox();
let b2 = t2.mesh.getBoundingBox();
if (b1.intersectsBox(b2)) {
let a = t1.getVertices().array;
let b = t2.getVertices().array;
let arr = new Float32Array(a.length + b.length);
arr.set(a, 0);
arr.set(b, a.length);
t1.loadVertices(arr);
t2.del = true;
}
}
}
// filter merged tabs
tabW = tabW.filter(tw => !tw.del);
let proc = settings.process,
camOps = widget.camops = [],
sliceAll = widget.slices = [],
isIndexed = proc.camStockIndexed,
lastTime = Date.now();
let axisRotation, axisIndex,
bounds, dark, color, stock, tabs, track, tool, unsafe, units, workarea,
camZTop, camZBottom, minToolDiam, maxToolDiam,
bottom_gap, bottom_part, bottom_stock, bottom_z,
top_stock, top_part, top_gap, top_z,
zBottom, zMin, zMax, zTop,
ztOff, zbOff, wztop;
axisRotation = axisIndex = undefined;
dark = settings.controller.dark;
color = dark ? 0xbbbbbb : 0;
minToolDiam = Infinity;
maxToolDiam = -Infinity;
tabs = widget.anno.tab ?? [];
unsafe = proc.camExpertFast;
units = settings.controller.units === 'in' ? 25.4 : 1;
// allow recomputing later if widget or settings changes
const var_compute = () => {
let { camStockX, camStockY, camStockZ, camStockOffset } = proc;
({ camZTop, camZBottom } = proc);
bounds = widget.getBoundingBox();
let pos = widget.track.pos;
stock = camStockOffset ? {
x: bounds.dim.x + camStockX,
y: bounds.dim.y + camStockY,
z: bounds.dim.z + camStockZ,
center: newPoint(pos.x, pos.y, pos.z)
} : {
x: camStockX,
y: camStockY,
z: camStockZ,
center: newPoint(pos.x, pos.y, pos.z)
};
if (!camStockOffset && axisIndex && isIndexed) {
if (axisIndex === 0 || axisIndex === 180) {
// do nothing
} else if (axisIndex === 90 || axisIndex === 270) {
// swap YZ
let tmp = stock.y;
stock.y = stock.z;
stock.z = tmp;
stock.center = newPoint(pos.x, pos.z, pos.y);
} else {
// compute YZ hypotenuse
let p = new THREE.Vector2(stock.y, stock.z);
let center = new THREE.Vector2(0, 0);
p.rotateAround(center, axisRotation);
stock.y = p.y;
stock.z = p.z;
}
}
track = widget.track;
wztop = track.top;
ztOff = isIndexed ? (stock.z - bounds.dim.z) / 2 : (stock.z - wztop);
zbOff = isIndexed ? (stock.z - bounds.dim.z) / 2 : (wztop - track.box.d);
zBottom = isIndexed ? camZBottom : camZBottom - zbOff;
zMin = isIndexed ? bounds.min.z : Math.max(bounds.min.z, zBottom);
zMax = bounds.max.z;
zTop = zMax + ztOff;
bottom_gap = zbOff;
bottom_part = 0;
bottom_stock = -bottom_gap;
bottom_z = isIndexed ? zBottom : Math.max(
(camZBottom ? bottom_stock + camZBottom : bottom_part),
(camZBottom ? bottom_stock + camZBottom : bottom_stock)
);
top_stock = zTop;
top_part = zMax;
top_gap = ztOff;
top_z = camZTop ? bottom_stock + camZTop : top_stock;
workarea = util.round({
top_stock, top_part, top_gap, top_z,
bottom_stock, bottom_part, bottom_gap, bottom_z,
}, 3);
// console.log({ bounds, stock, track, workarea });
return structuredClone(workarea);
};
// initial setup
var_compute();
function error(msg) {
ondone(msg);
}
function alert(msg) {
onupdate(null,null,msg);
}
function shadowAt(z) {
return widget.shadowAt(z);
}
if (unsafe) {
console.log("disabling overhang safeties");
}
if (!proc.ops || proc.ops.length === 0) {
return error('no processes specified');
}
if (stock.x === 0 || stock.y === 0 || stock.z === 0) {
return error("one or more stock dimensions is zero<br>offset stock or set to non zero value");
}
if (stock.x && stock.y && stock.z && !isIndexed) {
let maxDelta = 1e-3;
if (stock.x + maxDelta < (bounds.max.x - bounds.min.x)) {
alert('stock X too small for part');
}
if (stock.y + maxDelta < (bounds.max.y - bounds.min.y)) {
alert('stock Y too small for part');
}
if (stock.z + maxDelta < (bounds.max.z - bounds.min.z)) {
alert('stock Z too small for part');
}
}
if (zMin >= bounds.max.z) {
alert(`invalid z bottom ${(zMin / units).round(3)} >= bounds z max ${(zMax / units).round(3)}`);
}
let opList = [];
let opSum = 0;
let opTot = 0;
let slicer;
let state = {
addSlices,
axisIndex,
axisRotation,
bounds,
color,
computeShadows,
contourPolys,
cutTabs,
dark,
isIndexed,
newSlicer,
ops: opList,
setAxisIndex,
setToolDiam,
settings,
shadowAt,
slicer,
stock,
tabs,
tool,
unsafe,
updateSlicer,
updateToolDiams,
widget,
zBottom,
zMax,
ztOff,
zTop
};
let tracker = setSliceTracker({ rotation: 0 });
async function updateTab(tab) {
tab.setAxisIndex(isIndexed ? -axisIndex : 0)
let ts = new cam_slicer(tab);
let si = Object.keys(ts.zList).map(k => parseFloat(k));
si = [ ...si.map(v => v-0.01), ...si.map(v => v+0.01) ];
let zt = Math.max(...si);
let zb = Math.min(...si);
let shadow;
await ts.slice(si, { each: data => {
if (shadow) {
shadow = POLY.union([...shadow, ...data.polys]);
} else {
shadow = data.polys;
}
}});
return {
top: zt,
poly: shadow[0],
pos: { z: (zt+zb)/2 },
dim: { z: (zt-zb) },
NEW: true
}
}
function updateProgress(percent, message) {
const mark = Date.now();
if (false) console.log({
time: ((mark - lastTime)/1000).toFixed(2),
pct: ((percent ?? 0) * 100).toFixed(2),
msg: message
});
onupdate(percent, message);
lastTime = mark;
}
function newSlicer(opts) {
return new cam_slicer(widget, opts);
}
function updateSlicer(opts) {
return slicer = state.slicer = new cam_slicer(widget, opts);
}
async function computeShadows(progress) {
console.time('compute shadow');
await new OPS.shadow(state, { type: "shadow", xsilent: true }).slice(progress);
console.timeEnd('compute shadow');
}
function setToolDiam(toolDiam) {
updateToolDiams(toolDiam);
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([ tab.poly ], toolDiam / 2).flat();
});
}
}
function updateToolDiams(toolDiam) {
minToolDiam = Math.min(minToolDiam, toolDiam);
maxToolDiam = Math.max(maxToolDiam, toolDiam);
}
async function setAxisIndex(degrees = 0, absolute = true) {
state.axisIndex = axisIndex = absolute ? degrees : (axisIndex || 0) + degrees;
state.axisRotation = axisRotation = (Math.PI / 180) * axisIndex;
widget.setAxisIndex(isIndexed ? -axisIndex : 0);
state.tabs = tabs = [];
for (let tab of tabW) {
tabs.push(await updateTab(tab));
}
return isIndexed ? -axisIndex : 0;
}
function addPolyIndexing(poly, a) {
if (!poly) {
return;
}
if (Array.isArray(poly)) {
for (let p of poly) {
addPolyIndexing(p, a);
}
return;
}
for (let point of poly.points) {
point.a = a;
}
addPolyIndexing(poly.inner, a);
}
function addSlices(slices, addIndexing = isIndexed) {
if (!Array.isArray(slices)) {
slices = [slices];
}
sliceAll.appendAll(slices);
if (addIndexing && axisIndex !== undefined) {
for (let slice of slices) {
slice.angle = axisIndex;
}
// update slice cam lines to add axis indexing
for (let slice of slices.filter(s => s.camLines)) {
addPolyIndexing(slice.camLines, -axisIndex);
}
}
}
if (false) {
opList.push({ op: new OPS.xray(state, { type: "xray" }) });
}
// find index ops
let activeOps = proc.ops.filter(op => !op.disabled);
if (isIndexed) {
// preface op list with OpIndex
if (activeOps.length === 0 || activeOps[0].type !== 'index') {
opList.push({ op: new OPS.index(state, { type: "index", degrees: 0, absolute: true }) });
opTot += opList.peek().op.weight();
}
} else {
// preface op list with OpShadow
opList.push({ op: new OPS.shadow(state, { type: "shadow", silent: true }) });
opTot += opList.peek().op.weight();
}
// LOOP EXPANSION - duplicate next N operations M times
let expandedOps = [];
for (let i=0; i<activeOps.length; i++) {
let op = activeOps[i];
if (op.type === 'loop') {
let { count, repeat } = op;
let remain = activeOps.slice(i+1).filter(op => op.type !== '|');
if (remain < count) {
return error(`Loop operation requires ${count} following operation(s), but only ${remain.length - i - 1} available`);
}
for (let r=1; r<repeat; r++)
for (let c=0; c<count; c++) {
let dup = remain[c];
if (dup.type === 'loop') {
return error("Nested loops are not supported");
}
expandedOps.push(dup);
}
} else if (op.type === '|') {
break;
} else {
expandedOps.push(op);
}
}
activeOps = expandedOps;
// determine # of steps and step weighting for progress bar
let opos = 0;
for (let op of activeOps) {
if (op.type === '|') {
break;
}
let opfn = OPS[op.type];
if (opfn) {
let opin = new opfn(state, op);
opTot += opin.weight();
opos++;
let share = shared.ops[opos];
if (!share) {
share = shared.ops[opos] = {};
}
opList.push({ op: opin, share });
}
}
// drop last op if it's type index
if (opList.peek().op.op.type === 'index') {
opList.pop();
}
// call slice() function on all ops in order
await setAxisIndex();
updateSlicer();
for (let { op, share } of opList) {
let weight = op.weight();
// apply operation override vars
let workover = var_compute();
let valz = op.op;
let { ov_topz, ov_botz } = valz;
if (ov_topz) {
workover.top_z = isIndexed ? ov_topz : bottom_stock + ov_topz;
}
if (ov_botz) {
workover.bottom_z = isIndexed ? ov_botz : bottom_stock + ov_botz;
}
if (workover.bottom_z >= workover.top_z) {
return error("Z Bottom cannot be above or equal to Z Top");
}
if (valz.tool) {
tool = new Tool(settings, valz.tool);
}
let { note, type } = op.op;
let named = note ? note.split(' ').filter(v => v.charAt(0) === '#') : [];
let layername = named.length ? named : (note ? `${type} (${note})` : type);
Object.assign(state, {
layername,
share,
stock,
tool,
zBottom,
ztOff,
zMax,
zTop,
workarea: workover
});
let operr;
await op.slice((progress, message) => {
updateProgress((opSum + (progress * weight)) / opTot, message || op.type());
}).catch(e => {
operr = e;
console.trace(e);
});
if (operr) {
return error(operr);
}
// update tracker rotation for next slice output() visualization
tracker.rotation = isIndexed ? axisRotation : 0;
camOps.push(op);
opSum += weight;
}
setSliceTracker();
// reindex
sliceAll.forEach((slice, index) => slice.index = index);
widget.minToolDiam = minToolDiam;
widget.maxToolDiam = maxToolDiam;
ondone();
};
export async function traces(settings, widget) {
if (widget.traces) {
return false;
}
// --- points → line segments ---
let edges = new THREE.EdgesGeometry(widget.mesh.geometry, settings.controller.edgeangle ?? 20);
let array = edges.attributes.position.array;
let pcache = {};
let points = new Array(2);
let lines = new Array(points.length / 2);
for (let i = 0, j = 0, k = 0, l = array.length; i < l;) {
let ps = [array[i++], array[i++], array[i++]];
let key = ps.map(v => (v * 10000) | 0).join(',');
let point = pcache[key];
if (!point) {
point = newPoint(ps[0], ps[1], ps[2], key);
point.lines = [];
pcache[key] = point;
}
points[j++ % 2] = point;
if (j % 2 === 0) {
let [p0, p1] = points;
let line = lines[k++] = newLine(p0, p1);
p0.lines.push(line);
p1.lines.push(line);
}
}
// --- segments → chains (bidirectional walk) ---
let chains = [];
{
const segs = lines.slice(); // shallow copy
segs.forEach(s => (s.visited = false));
function step(dirPoint, prevSeg, pushFront, chain) {
let curr = dirPoint;
let prev = prevSeg;
while (curr.lines.length === 2) { // stay inside the arc
const next = curr.lines.find(l => !l.visited && l !== prev);
if (!next) break; // should not happen
next.visited = true;
if (pushFront) chain.unshift(next); // prepend or append?
else chain.push(next);
curr = next.p1 === curr ? next.p2 : next.p1;
prev = next;
}
}
for (const seed of segs) {
if (seed.visited) continue;
seed.visited = true; // always include the seed
const chain = [seed];
// grow backwards from p1 (prepend)
step(seed.p1, seed, true, chain);
// grow forwards from p2 (append)
step(seed.p2, seed, false, chain);
chains.push(chain);
}
}
// --- chains → polylines ---
const polylines = [];
for (const chain of chains) {
const poly = newPolygon().setOpen();
// choose any node whose degree !== 2; if none, it’s a closed loop
let start = null;
for (const s of chain) {
if (s.p1.lines.length !== 2) { start = s.p1; break; }
if (s.p2.lines.length !== 2) { start = s.p2; break; }
}
if (!start) start = chain[0].p1; // closed loop
let curr = start, prevSeg = null;
poly.push(curr); // first point
while (true) {
const nextSeg = curr.lines.find(
l => l !== prevSeg && chain.includes(l)
);
if (!nextSeg) break; // open end reached
curr = nextSeg.p1 === curr ? nextSeg.p2 : nextSeg.p1;
poly.push(curr); // << push *before* test
if (curr === start) break; // loop closed
prevSeg = nextSeg;
}
polylines.push(poly);
}
widget.traces = polylines;
return true;
}
/**
* Returns an array of arrays of perpindicular triangles in the mesh.
* Each sub-array is a list of triangle data that are part of the same
* cylinder.
*
* @param {object} settings - settings object
* @param {object} widget - widget object
* @param {object} opts - options object
* @return {array} - array of arrays of triangle data
*/
export async function cylinders(settings, widget, opts) {
let { } = opts ?? {};
let { array: verts } = widget.mesh.geometry.attributes.position;
let perpTriangles = [];
//iterate over all triangles
for (let i = 0; i < verts.length; i += 9) {
let a = [verts[i], verts[i + 1], verts[i + 2]],
b = [verts[i + 3], verts[i + 4], verts[i + 5]],
c = [verts[i + 6], verts[i + 7], verts[i + 8]];
//calculate normal
let normal = new THREE.Vector3(a[0] - b[0], a[1] - b[1], a[2] - b[2])
.cross(new THREE.Vector3(b[0] - c[0], b[1] - c[1], b[2] - c[2]))
.normalize();
// if perpindicular normal, and at least 2 Zs are the same
if (normal.z.round(5) == 0 && !(a[2] != b[2] && b[2] != c[2])) {
let minZ = Math.min(a[2], b[2], c[2]),
maxZ = Math.max(a[2], b[2], c[2]);
if (minZ == maxZ) { // all Zs are the same indicated malformed geometry
continue;
}
perpTriangles.push({ ...[a, b, c], normal, minZ, maxZ, i: index });
}
}
//map where zmax, zmin -> triangleData
let cylinderTriangles = new Map();
for (let t of perpTriangles) {
let hash = `${t.minZ.round(5)},${t.maxZ.round(5)}`;
if (!cylinderTriangles.has(hash)) {
cylinderTriangles.set(hash, []);
}
cylinderTriangles.get(hash).push(t);
}
return Array.from(cylinderTriangles.values());
}
/**
* Find all triangles that are part of the same cylinder as the given triangle
* @param {Widget} widget - widget object
* @param {number} face - index of face in triangle vertex data
* @return {array} - array of all triangle data that belong to the same cylinder
* @requires surface_prep must be called first
*/
export function cylinder_find (widget, face){
CAM.surface_prep(widget,false);
return widget.tool.findCylinderSurface(face);
}
export function cylinder_poly_find(widget, face) {
let faces = cylinder_find(widget, face);
let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4));
let slicer = new cam_slicer(widget, {});
let firstOffset = faces[0] * 9;
let [x1, y1, z1, x2, y2, z2, x3, y3, z3] = Array.from(vert.subarray(firstOffset, firstOffset + 9));
let zs = [z1, z2, z3].map(z => z.round(5));
let zmin = Math.min(...zs);
let zmax = Math.max(...zs);
let zmid = (zmin + zmax) / 2;
let cylVerts = new Float32Array(faces.length * 9);
let opts = {
dedup: true,
edges: false,
over: true
};
let i = 0
//for each cylinder face index
for (let index of faces) {
let off = index * 9;
//copy the 9 triangle vertices floats in
for (let j = 0; j < 9; j++) {
cylVerts[i] = vert[off + j];
i++;
}
}
//slice the poly at the midpoint
let poly = slicer.sliceZ(zmid, cylVerts, opts)?.polys?.at(0);
if (!poly) throw "slicing returned no poly";
let circular = poly.circularity() > 0.98;
// console.log({ zmid, cylVerts, opts, poly, circularity: poly.circularity() });
//throw error if not circular
if (!circular) throw "faces must be circular";
//calculate circle properties
let area = poly.area(),
diam = Math.sqrt(area / Math.PI) * 2,
center = poly.calcCircleCenter();
//find direction using normal to center point
let point = newPoint(x1, y1, z1),
delta = point.setZ(center.z).sub(center),
normal = new THREE.Vector3(x1 - x2, y1 - y2, z1 - z2)
.cross(new THREE.Vector3(x2 - x3, y2 - y3, z2 - z3)),
dotProd = normal.dot(delta),
interior = dotProd < 0
return { faces, zmin, zmax, poly, circular, area, diam, center, interior }
}
/**
* Generate a list of holes in the model based on the given diameter.
*
* @param {Object} settings - settings object
* @param {Object} widget - widget object
* @param {boolean} individual - if true, drill holes individually
* @param {Object} rec - DrillOp record
* @param {Function} onProgress - callback function to report progress
* @returns {Array} list of hole centers as objects with `x`, `y`, `z`, `depth`, and `selected` properties.
*/
export async function holes(settings, widget, individual, rec, onProgress) {
let { tool, precision } = rec //TODO: display some visual difference if mark is selected
let toolDiam = new Tool(settings, tool).fluteDiameter()
let diam = individual ? 1 : toolDiam; // sets default diameter when select individual used
let proc = settings.process,
stock = settings.stock || {},
isIndexed = proc.camStockIndexed,
track = widget.track,
{ camZBottom } = proc,
// widget top z as defined by setTopz()
wztop = track.top,
// distance between top of part and top of stock
ztOff = isIndexed ? 0 : (stock.z - wztop),
// distance between bottom of part and bottom of stock
zbOff = isIndexed ? 0 : (wztop - track.box.d),
// defined z bottom offset by distance to stock bottom
// keeps the z bottom relative to the part when z align changes
zBottom = isIndexed ? camZBottom : camZBottom - zbOff;
let slicerOpts = { flatoff: 0.001 };
let slicer = new cam_slicer(widget, slicerOpts);
let zFlats = Object.keys(slicer.zFlat).map(Number).map(z => [z, z - 0.002]).flat();
precision = Math.max(0, precision)
let intervals = (precision == 0) ? [] : slicer.interval(
precision,
{
fit: false, off: -0.01, flats: true
}
)
let zees = [...zFlats, ...intervals];
let indices = [...new Set(zees
.map(kv => parseFloat(kv).round(5))
.filter(z => z !== null)
)];
let centerDiff = diam * 0.1,
area = (diam / 2) * (diam / 2) * Math.PI,
circles = [],
slices = [];
function onEach(slice) {
slices.push(slice);
}
let opts = { each: onEach, progress: (num, total) => onProgress(num / total * 0.5, "slice") };
await slicer.slice(indices, opts);
let shadowedDrills = false;
for (let [i, slice] of slices.entries()) {
for (let top of slice.tops) {
// console.log("slicing",slice.z,top)
slice.shadow = await widget.shadowAt(slice.z);
let inner = top.inner;
if (!inner) { //no holes
continue;
}
for (let poly of inner) {
if (poly.points.length < 7) {
continue;
}
let center = poly.calcCircleCenter();
center.area = poly.area();
center.overlapping = [center];
center.depth = 0;
if (poly.circularity() < 0.98) {
// if not circular, don't add to holes
continue;
}
if (center.isInPolygon(slice.shadow)) {
// if shadowed, don't add, and inform client
shadowedDrills = true;
continue;
}
let overlap = false;
for (let [i, circle] of circles.entries()) {
let dist = circle.distTo2D(center);
// if on the same xy point,
if (dist <= centerDiff) {
// console.log("overlap",center,circle);
circle.overlapping.push(center);
// if overlapping, don't add and continue
overlap = true;
continue;
}
}
if (!overlap) circles.push(center);
}
}
onProgress(0.5 + (i / slices.length * 0.25), "recognize circles");
}
let drills = [];
for (let [i, c] of circles.entries()) {
let overlapping = c.overlapping;
let last = overlapping.shift();
while (overlapping.length) {
let circ = overlapping.shift();
let aveArea = (circ.area + last.area) / 2;
let areaDelta = Math.abs(circ.area - last.area);
if (areaDelta < aveArea * 0.05) { // if area delta less than 5% of average area
//keep top circle selected
last.depth = last.z - circ.z;
} else { // if not the same area
//push and move on
drills.push(last);
last = circ;
}
}
if (last.depth != 0) drills.push(last); // add last circle
onProgress(0.75 + (i / circles.length * 0.25), "assemble holes");
}
drills.forEach(h => {
if (rec.fromTop) {
// set z top if selected
h.depth += wztop - h.z;
h.z = wztop;
}
delete h.overlapping; //for encoding
h.diam = toolDiam; // for mesh generation
h.selected = (!individual && Math.abs(h.area - area) <= area * 0.05); // for same size selection
})
// console.log("unfiltered circles",circles);
// console.log("drills",drills);
drills = drills.filter(drill => drill.depth > 0);
widget.shadowedDrills = shadowedDrills;
widget.drills = drills;
return drills;
}
function cutTabs(tabs, offset) {
let out = [];
for (let tab of tabs) {
tab.top = tab.pos.z + tab.dim.z / 2;
}
for (let poly of offset) {
let polyZ = poly.getZ();
let cut = tabs.filter(tab => tab.top >= polyZ);
let lo = poly.cut(cut.map(tab => tab.off).flat(), false);
let hi = [];
for (let tab of cut) {
hi.appendAll(POLY.setZ(poly.cut(tab.off, true), tab.top));
}
if (hi.length) {
let heal = POLY.reconnect([ ...lo, ...hi ], false);
POLY.setWinding(heal, poly.isClockwise());
out.push(...heal);
} else {
out.push(poly);
}
}
return out;
}
function contourPolys(widget, polys) {
const raycaster = new THREE.Raycaster();
raycaster.ray.direction.set(0, 0, -1); // ray pointing down Z
for (let poly of polys) {
for (let point of poly.points) {
raycaster.ray.origin.set(point.x, point.y, 10000);
const intersects = raycaster.intersectObject(widget.mesh, false);
const firstHit = intersects[0] || null;
if (firstHit) point.z = firstHit.point.z;
}
}
}

View file

@ -0,0 +1,417 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { util } from '../../../../geo/base.js';
import { decode, decodePointArray } from '../../../core/codec.js';
import { newLine } from '../../../../geo/line.js';
import { newPoint } from '../../../../geo/point.js';
import { newSlice } from '../../../core/slice.js';
import { polygons as POLY } from '../../../../geo/polygons.js';
import {
checkOverUnderOn,
intersectPoints,
makeZLine,
removeDuplicateLines,
sliceConnect,
} from '../../../../geo/slicer.js';
const timing = false;
const zDecimal = 3;
const epsilon = 10e-5;
const begin = function () {
if (timing) console.time(...arguments);
};
const end = function () {
if (timing) console.timeEnd(...arguments);
};
export class Slicer {
constructor(widget, options = { zlist: true, zline: true, lines: false }) {
this.minions = self.kiri_worker?.minions;
this.options = {};
this.setOptions(options);
if (widget) {
this.threaded = this.minions && this.minions.running;
this.setPoints(widget.getGeoVertices({ unroll: true, translate: true }));
this.computeFeatures();
}
}
// zList = generate list of z vertices
// zline = generate list of z vertices with coplanar lines
// trace = find z coplanar trace lines
// flatoff = amount to offset z when slicing on detected flats
// each = call for each slice generated from an interval
setOptions(options) {
Object.assign(this.options, options || {});
return this.options;
}
setPoints(points) {
this.bounds = null;
this.points = points;
this.zFlat = {}; // accumulated flat area at z height
this.zLine = {}; // count of z coplanar lines
this.zList = {}; // count of z values for auto slicing
return this;
}
// gather z-index stats
// these are used for auto-slicing in laser
// and to flats detection in CAM mode
computeFeatures(options) {
begin('compute features');
const opt = this.setOptions(options);
const bounds = this.bounds = new THREE.Box3();
const points = this.points;
const zFlat = this.zFlat;
const zLine = this.zLine;
const zList = this.zList;
function countZ(z) {
z = z.round(zDecimal);
zList[z] = (zList[z] || 0) + 1;
}
let p1 = newPoint(0, 0, 0),
p2 = newPoint(0, 0, 0),
p3 = newPoint(0, 0, 0);
// for (let i = 0, il = points.length; i < il; i++) {
// points[i] = points[i].round(zDecimal);
// }
for (let i = 0, il = points.length; i < il;) {
p1.set(points[i++], points[i++], points[i++]);
p2.set(points[i++], points[i++], points[i++]);
p3.set(points[i++], points[i++], points[i++]);
// update bounds
bounds.expandByPoint(p1);
bounds.expandByPoint(p2);
bounds.expandByPoint(p3);
// count occurrences of z values for auto slicing
if (opt.zlist) {
countZ(p1.z);
countZ(p2.z);
countZ(p3.z);
}
// use co-flat and co-line detection to adjust slice Z
if (Math.abs(p1.z - p2.z) < epsilon && Math.abs(p2.z - p3.z) < epsilon) {
// detect zFlat faces to avoid slicing directly on them
if (opt.zflatup) {
// filter to up-facing flats (more expensive)
let norm = THREE.computeFaceNormal(p1,p2,p3);
if (norm.z < -0.00001) continue;
}
let zkey = p1.z.toFixed(zDecimal),
area = Math.abs(util.area2(p1, p2, p3)) / 2;
if (!zFlat[zkey]) {
zFlat[zkey] = area;
} else {
zFlat[zkey] += area;
}
} else if (opt.zline) {
// detect zLine (curved region tops/bottoms)
// in cam used for ball and v mill tracing
if (p1.z === p2.z) {
let zkey = p1.z.toFixed(zDecimal);
let zval = zLine[zkey];
zLine[zkey] = (zval || 0) + 1;
}
if (p2.z === p3.z) {
let zkey = p2.z.toFixed(zDecimal);
let zval = zLine[zkey];
zLine[zkey] = (zval || 0) + 1;
}
if (p3.z === p1.z) {
let zkey = p3.z.toFixed(zDecimal);
let zval = zLine[zkey];
zLine[zkey] = (zval || 0) + 1;
}
}
}
end('compute features');
// console.log({ bounds, zFlat, zLine, zList });
return this;
}
/**
* Given an array of z-slice positions, generate a list of polygons
* representing the intersection of the mesh with the given z-slice
* positions.
*
* @param {number[]} zs - array of z-slice positions
* @param {Object} options - slicing parameters
* @param {number} [options.flatoff=0.01] - z offset when slicing on a flat surface
* @param {boolean} [options.progress=false] - callback with slice progress
* @param {function} [options.each] - callback with each slice result
* @returns {Promise<Object[]>} - list of slice objects with z and tops (polygons)
*/
async slice(zs, options) {
const opt = this.setOptions(options);
// slice through points at given Z and return polygons
if (!Array.isArray(zs)) {
throw "slice zs parameter must be ab array";
}
// sort Z and offset when on a flat
const flatoff = util.numOrDefault(opt.flatoff, 0.01);
zs = zs.sort((a, b) => a - b).map(z => {
if (!flatoff) {
return z;
}
const znorm = z.toFixed(zDecimal);
return this.zFlat[znorm] ? z + flatoff : z;
});
// compute buckets from z slice list
const { points } = this;
const plen = points.length;
const zlen = zs.length;
const ppz = plen / zlen;
const count = 25;//Math.ceil(ppz / 10000);
const step = Math.ceil(zlen / count);
const buckets = [];
// console.log({ points, zs, count, step });
for (let c = 0, b = 0; c < count; c++) {
if (b >= zlen) break;
buckets.push({
zs: zs.slice(b, b + step),
index: []
});
b += step;
}
let p1 = newPoint(0, 0, 0),
p2 = newPoint(0, 0, 0),
p3 = newPoint(0, 0, 0),
ep = 0.001;
begin("create buckets");
for (let i = 0, il = points.length; i < il;) {
p1.set(points[i++], points[i++], points[i++]);
p2.set(points[i++], points[i++], points[i++]);
p3.set(points[i++], points[i++], points[i++]);
let zmin = Math.min(p1.z, p2.z, p3.z);
let zmax = Math.max(p1.z, p2.z, p3.z);
for (let bucket of buckets) {
const { zs, index } = bucket;
const min = Math.min(...zs);
const max = Math.max(...zs);
if (zmin < min && zmax < min) {
if (Math.abs(zmin - min) > ep && Math.abs(zmax - min) > ep) {
continue;
}
}
if (zmin > max && zmax > max) {
if (Math.abs(zmin - max) > ep && Math.abs(zmax - max) > ep) {
continue;
}
}
index.push(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z);
}
}
end("create buckets");
// console.log({ zs, zlen, count, step, buckets, flatoff });
begin("slicing");
const { minions } = this;
const threaded = minions && minions.running;
const sliceFn = (threaded ? this.sliceBucketMinion : this.sliceBucket).bind(this);
const track = { count: 0, total: zs.length };
if (threaded) minions.broadcast("cam_slice_init", {});
const promises = []
for (let bucket of buckets) {
promises.push(sliceFn(bucket, { ...opt, minZ: this.bounds.min.z }, slice => {
track.count++;
if (opt.progress) {
opt.progress(track.count, track.total);
}
// console.log({ oneach: slice, ...track });
}));
}
const data = (await Promise.all(promises)).flat();
data.sort((a, b) => b.z - a.z);
for (let i=0; i<data.length; i++) {
let rec = data[i];
rec.tops = rec.polys;
rec.slice = newSlice(rec.z).addTops(rec.tops);
if (opt.each) {
await opt.each(rec, i, data.length);
}
}
if (threaded) minions.broadcast("cam_slice_cleanup");
end("slicing");
return data;
}
async sliceBucketMinion(bucket, opt, oneach) {
const slices = (await new Promise(resolve => {
this.minions.queue({
cmd: "cam_slice",
opt: Object.clone(opt),
bucket,
}, resolve);
})).slices;
for (let slice of slices) {
slice.polys = decode(slice.polys);
if (slice.lines) {
slice.lines = decodePointArray(slice.lines)
.group(2)
.map(arr => {
return newLine(arr[0], arr[1])
});
}
oneach(slice);
}
// console.log(bucket, slices);
return slices;
}
async sliceBucket(bucket, opt, oneach) {
const { zs, index } = bucket;
const slices = [];
for (let z of zs) {
const slice = this.sliceZ(z, index, opt);
if (slice) {
slices.push(slice);
}
oneach(slice);
}
return slices;
}
sliceZ(z, indices, opt) {
let links = opt.links !== false,
dedup = opt.dedup !== false,
edges = opt.edges || false,
over = opt.over || z <= opt.minZ,
debug = opt.debug,
phash = {},
lines = [];
// const { points } = this,
const points = indices,
p1 = newPoint(0, 0, 0),
p2 = newPoint(0, 0, 0),
p3 = newPoint(0, 0, 0);
for (let index = 0; index < points.length;) {
p1.set(points[index++], points[index++], points[index++]);
p2.set(points[index++], points[index++], points[index++]);
p3.set(points[index++], points[index++], points[index++]);
let where = { under: [], over: [], on: [] };
checkOverUnderOn(p1, z, where);
checkOverUnderOn(p2, z, where);
checkOverUnderOn(p3, z, where);
if (where.under.length === 3 || where.over.length === 3) {
// does not intersect (all 3 above or below)
} else if (where.on.length === 2) {
// one side of triangle is on the Z plane and 3rd is below
// drop lines with 3rd above because that leads to ambiguities
// with complex nested polygons on flat surface
if ((over && where.over.length === 1) || (!over && where.under.length === 1)) {
lines.push(makeZLine(phash, where.on[0], where.on[1], false, true));
}
} else if (where.on.length === 3) {
// triangle is coplanar with Z
// we drop these because this face is attached to 3 others
// that will satisfy the if above (line) with 2 points
} else if (where.under.length === 0 || where.over.length === 0) {
// does not intersect but one point is on the slice Z plane
} else if (!edges) {
// compute two point intersections and construct line
let line = intersectPoints(where.over, where.under, z);
if (line.length < 2 && where.on.length === 1) {
line.push(where.on[0].clone());
}
if (line.length === 2) {
lines.push(makeZLine(phash, line[0], line[1]));
} else {
console.log({ msg: "invalid ips", line: line, where: where });
}
}
}
if (dedup) {
// console.log({ z, dedup: lines, points });
lines = removeDuplicateLines(lines, debug);
}
return lines.length ? {
z,
lines: opt.lines !== false ? lines : undefined,
polys: links ? POLY.nest(sliceConnect(lines, opt, debug)) : undefined
} : null;
}
interval(step, options) {
let opt = options || {},
bounds = this.bounds,
boff = opt.boff || opt.off || 0, // bottom offset
toff = opt.toff || opt.off || 0, // top offset
zmin = opt.min !== undefined ? opt.min : bounds.min.z + boff,
zmax = (opt.max || bounds.max.z) - toff,
steps = (zmax - zmin) / step,
rem = steps % 1 != 0 ? 0 : 1,
count = Math.floor(steps) + rem,
array = [];
if (opt.fit) {
count++;
step = (zmax - zmin) / count;
}
if (opt.down) {
for (let i = 0; i < count; i++) {
array.push(zmax);
zmax -= step;
}
} else {
for (let i = 0; i < count; i++) {
array.push(zmin);
zmin += step;
}
}
if (opt.fit) {
array.push(opt.down ? zmax : zmin);
}
if (opt.flats && opt.off) {
let add = [];
Object.keys(this.zFlat).forEach(z => {
z = parseFloat(z);
add.push(z + opt.off);
if (z > zmin) {
add.push(z - opt.off);
}
});
// add over and under all flats by 'off'
array.appendAll(add).sort((a, b) => {
return opt.down ? b - a : a - b;
});
}
// filter duplicate values
array = array.map(v => v.round(zDecimal)).filter((e, i, a) => i < 1 || a[i - 1] !== a[i]);
// return array.map(v => Math.abs(parseFloat(v.toFixed(zDecimal))));
return array.map(v => parseFloat(v.toFixed(zDecimal)));
}
}

View file

@ -0,0 +1,143 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { newPoint } from '../../../../geo/point.js';
import {
checkOverUnderOn,
makeZLine,
removeDuplicateLines,
intersectPoints
} from '../../../../geo/slicer.js';
/** Slicer used in Topo3 and Topo4 to find contour lines */
export class Slicer {
constructor(index = 0) {
this.min = Infinity;
this.max = -Infinity;
this.index = index;
}
// position is a non-indexed 3js geometry position attribute array
// create Point array from optionally filtered Z range
setFromArray(position, options = {}) {
const min = options.min || -Infinity;
const max = options.max || Infinity;
const len = position.length;
const pts = this.points = [];
for (let i = 0; i < len;) {
const x0 = position[i++];
const y0 = position[i++];
const z0 = position[i++];
const x1 = position[i++];
const y1 = position[i++];
const z1 = position[i++];
const x2 = position[i++];
const y2 = position[i++];
const z2 = position[i++];
const minz = Math.min(z0, z1, z2);
const maxz = Math.max(z0, z1, z2);
if (maxz < min || minz > max) {
continue;
}
pts.push(
newPoint(x0, y0, z0),
newPoint(x1, y1, z1),
newPoint(x2, y2, z2)
);
this.min = options.min || Math.min(this.min, minz);
this.max = options.max || Math.max(this.max, maxz);
}
return this;
}
setFromPoints(points, options = {}) {
const pts = this.points = [];
const len = points.length;
const min = options.min || -Infinity;
const max = options.max || Infinity;
for (let i = 0; i < len;) {
const p0 = points[i++];
const p1 = points[i++];
const p2 = points[i++];
const minz = Math.min(p0.z, p1.z, p2.z);
const maxz = Math.max(p0.z, p1.z, p2.z);
if (maxz < min || minz > max) {
continue;
}
pts.push(p0, p1, p2);
this.min = options.min || Math.min(this.min, minz);
this.max = options.max || Math.max(this.max, maxz);
}
return this;
}
slice(interval = 1) {
const ret = [];
for (let z = this.min; z < this.max; z += interval) {
ret.push({ z, index: this.index++, lines: this.sliceZ(z) });
}
return ret;
}
sliceZ(z, options = {}) {
const points = this.points;
const zMin = this.min;
const zMax = this.max;
const phash = {};
const lines = [];
let { under, over, both } = options;
let p1, p2, p3;
// default to 'over' selection with 2 points on a line
if (!under && !both) over = true;
// iterate over matching buckets for this z offset
for (let i = 0; i < points.length;) {
p1 = points[i++];
p2 = points[i++];
p3 = points[i++];
let where = { under: [], over: [], on: [] };
checkOverUnderOn(p1, z, where);
checkOverUnderOn(p2, z, where);
checkOverUnderOn(p3, z, where);
if (where.under.length === 3 || where.over.length === 3) {
// does not intersect (all 3 above or below)
} else if (where.on.length === 2) {
// one side of triangle is on the Z plane and 3rd is below
// drop lines with 3rd above because that leads to ambiguities
// with complex nested polygons on flat surface
let add2 = both ||
(over && (where.over.length === 1 || z === zMax)) ||
(under && (where.under.length === 1 || z === zMin));
if (add2) {
lines.push(makeZLine(phash, where.on[0], where.on[1], false, true));
}
} else if (where.on.length === 3) {
// triangle is coplanar with Z
// we drop these because this face is attached to 3 others
// that will satisfy the if above (line) with 2 points
} else if (where.under.length === 0 || where.over.length === 0) {
// does not intersect but one point is on the slice Z plane
} else {
// compute two point intersections and construct line
let line = intersectPoints(where.over, where.under, z);
if (line.length < 2 && where.on.length === 1) {
line.push(where.on[0]);
}
if (line.length === 2) {
lines.push(makeZLine(phash, line[0], line[1]));
} else {
console.log({ msg: "invalid ips", line: line, where: where });
}
}
}
if (lines.length == 0 && options.noEmpty) {
return;
}
return removeDuplicateLines(lines);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,644 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { codec } from '../../../core/codec.js';
import { newPoint } from '../../../../geo/point.js';
import { newPolygon } from '../../../../geo/polygon.js';
import { sliceConnect } from '../../../../geo/slicer.js';
import { newSlice } from '../../../core/slice.js';
import { Tool } from '../core/tool.js';
import { Slicer as topo_slicer } from './slicer-topo.js';
const RAD2DEG = 180 / Math.PI;
const DEG2RAD = Math.PI / 180;
function scale(fn, factor = 1, base = 0) {
return (value, msg) => {
fn(base + value * factor, msg);
}
}
export class Topo {
constructor() { }
async generate(opt = {}) {
let { state, op, onupdate, ondone } = opt;
let { widget, settings, tabs, color } = state;
let { controller, process } = settings;
let { webGPU } = controller;
let axis = op.axis.toLowerCase(),
tool = new Tool(settings, op.tool),
bounds = widget.getBoundingBox().clone(),
density = parseInt(controller.animesh || 100) * 2500,
{ min, max } = bounds,
span = {
x: max.x - min.x,
y: max.y - min.y
},
contour = {
x: axis === "x",
y: axis === "y"
},
tolerance = op.tolerance,
resolution = (tolerance ? tolerance : 1 / Math.sqrt(density / (span.x * span.y))).round(5),
step = this.step = (tool.traceOffset() * 2) * op.step,
angle = this.angle = op.angle || 1,
down = this.down = op.down,
expand = this.expand = op.expand;
if (tool.isTaperMill() && step === 0) {
step = this.step = op.step * tool.unitScale();
}
if (tolerance === 0) {
console.log(widget.id, 'topo4 auto tolerance', resolution.round(4));
}
this.zBottom = state.zBottom ?? 0;
this.resolution = resolution;
this.vertices = widget.getGeoVertices({ unroll: true, translate: true });
this.tabverts = widget.getTabVertices();
this.tool = tool.generateProfile(resolution).profile;
this.maxo = tool.profileDim.maxo * resolution;
this.diam = tool.fluteDiameter();
this.unit = tool.unitScale();
this.units = controller.units === 'in' ? 25.4 : 1
this.zoff = widget.track.top || 0;
this.leave = op.leave || 0;
this.linear = op.linear || false;
this.lineColor = color;//controller.dark ? 0xffff00 : 0x555500;
this.offStart = op.offStart ?? 0;
this.offEnd = op.offEnd ?? 0;
this.bounds = bounds;
this.gpu = webGPU ?? false;
onupdate(0, "lathe");
const parts = webGPU ? [ 0.8, 0.2 ] : [ 0.25, 0.75 ];
const range = this.range = { min: Infinity, max: -Infinity };
const slices = this.sliced = await this.slice(scale(onupdate, parts[0], 0));
for (let slice of slices) {
range.min = Math.min(range.min, slice.z);
range.max = Math.max(range.max, slice.z);
}
const lathe = await this.lathe(scale(onupdate, parts[1], parts[0]));
onupdate(1, "lathe");
ondone(lathe);
// ondone([...slices, ...lathe]);
return this;
}
async slice(onupdate) {
const { vertices, resolution, tabverts, zoff, offStart, offEnd, units } = this;
const { minions } = self.kiri_worker;
const range = this.range = { min: Infinity, max: -Infinity };
// swap XZ in shared array
for (let i = 0, l = vertices.length; i < l; i += 3) {
const x = vertices[i];
const z = vertices[i + 2] + zoff;
vertices[i] = z;
vertices[i + 2] = x;
range.min = Math.min(range.min, x);
range.max = Math.max(range.max, x);
}
// add tool diameter to slice min/max range to fully carve part
range.min += offStart * units;
range.max -= offEnd * units;
range.max += resolution * 2;
// merge in tab vertices here so they don't affect slice range / dimensions
for (let i = 0, l = tabverts.length; i < l; i += 3) {
const x = tabverts[i];
const z = tabverts[i + 2] + zoff;
tabverts[i] = z;
tabverts[i + 2] = x;
}
// re-create shared vertex array for workers
this.vertices = [].appendAll(vertices).appendAll(tabverts).toFloat32().toShared();
// rp.rasterizeMesh(vertices, resolution).then(rpo => console.log({ rpo }));
if (this.gpu) {
return await this.sliceGPU(onupdate);
}
const zSpan = range.max - range.min;
const shards = Math.ceil(Math.min(25, vertices.length / 27000));
const totalSteps = Math.ceil(zSpan / resolution);
const stardSteps = Math.ceil(totalSteps / shards);
const stepWidth = stardSteps * resolution;
let slices = this.slices = [];
for (let i=0; i<shards; i++) {
let s;
slices.push(s = {
min: range.min + i * stepWidth,
max: Math.min(range.max, range.min + (i+1) * stepWidth),
index: i * stardSteps
});
}
// console.log({
// shards,
// step: stepWidth,
// range,
// resolution,
// slices: slices.slice(),
// minions
// });
if (minions?.running > 1) {
return await this.sliceMinions(onupdate);
} else {
return await this.sliceWorker(onupdate);
}
}
async sliceGPU(onupdate) {
const { angle, diam, leave, linear, offStart, offEnd, resolution, tool, units, vertices, zBottom } = this;
const { down, expand } = this;
// invert tool Z offset for gpu code
let toolBounds = new THREE.Box3()
.expandByPoint({ x: -this.diam/2, y: -diam/2, z: 0 })
.expandByPoint({ x: this.diam/2, y: diam/2, z: 0 });
let toolPos = tool.slice();
let minz = Infinity;
for (let i=0; i<toolPos.length; i += 3) {
// toolPos[i+2] = -toolPos[i+2];
toolBounds.expandByPoint({ x: 0, y: 0, z: toolPos[i+2] });
minz = Math.min(minz,toolPos[i+2]);
}
let toolData = { positions: toolPos, bounds: toolBounds };
// console.time('swap XZ vertices');
// swap XZ vertices for gpu code
for (let i=0; i<vertices.length; i+= 3) {
let tmp = vertices[i+2];
vertices[i+2] = vertices[i+0];
vertices[i+1] = -vertices[i+1];
vertices[i+0] = tmp;
}
// console.timeEnd('swap XZ vertices');
let gpu = await self.get_raster_gpu({
mode: "radial",
resolution,
rotationStep: angle,
radialRotationOffset: 90
});
let xStep = Math.max(1, Math.round(this.step / resolution));
let boundsOverride = this.bounds.clone();
boundsOverride.min.x += offStart * units;
boundsOverride.max.x -= offEnd * units;
await gpu.loadTool({
sparseData: toolData
});
await gpu.loadTerrain({
triangles: vertices,
boundsOverride,
onProgress: (pct) => onupdate(pct / 1000)
});
let output = await gpu.generateToolpaths({
xStep,
yStep: 1,
zFloor: zBottom,
onProgress: (i,j) => onupdate(0.1 + i / j)
});
gpu.terminate();
let { numStrips, strips } = output;
let degPerRow = 360 / numStrips;
let slices = this.gpu_slices = [];
let xmult = resolution * xStep;
let xoff = boundsOverride.min.x;
let rows = [];
for (let i=0; i<numStrips; i++) {
let points = Array.from(strips[i].pathData).map((v,j) => newPoint(j * xmult + xoff, 0, v + leave).setA(-i * degPerRow));
rows.push(points);
}
if (linear) {
for (let i=0; i<rows.length; i++) {
let slice = newSlice(i);
slice.index = i;
slice.camLines = [ newPolygon(rows[i]).setOpen() ];
if (!down && i % 2 === 1) slice.camLines[0].reverse();
slices.push(slice);
}
} else {
let { pointsPerLine } = strips[0];
for (let i=0; i<pointsPerLine; i++) {
let slice = newSlice(i);
let points = rows.map(row => row[i]);
points.push(points[0].clone().setA(-360));
if (!down && i % 2 === 1) points.reverse();
slice.index = i;
slice.camLines = [ newPolygon(points).setOpen() ];
slices.push(slice);
}
}
let maxZ = 0;
if (down) {
for (let r of rows)
for (let p of r) {
maxZ = Math.max(maxZ, p.z);
}
maxZ += expand;
}
if (down) {
// step "up" points on trace until they max out
// then reverse the stack to cut top down
let reverse = 0;
for (let slice of slices) {
let z = slice.z;
let poly = slice.camLines[0].clone();
let pMinZ = poly.minZ();
let steps = Math.floor((maxZ - pMinZ) / down);
let stack = [ poly.clone() ];
for (let i=0; i<steps; i++) {
for (let p of poly.points) {
p.z = Math.min(p.z + down, maxZ);
}
stack.push(poly.clone());
}
stack.reverse();
slice.stack = stack.map(poly => {
if (reverse++ % 2 === 1) poly.reverse();
let slice = newSlice(z);
slice.camLines = [ poly ];
return slice;
});
// first move of new stack is to maxZ (problematic)
// let first = stack[0].points;
// first.splice(0,0,first[0].clone().setZ(maxZ));
// last move of stack is to maxZ
let last = stack.peek();
last.push(last.last().clone().setZ(maxZ));
}
slices = this.gpu_slices = slices.map(slice => slice.stack).flat();
slices.forEach((slice,index) => slice.index = index);
}
for (let slice of slices) {
slice.output()
.setLayer("lathe", { line: this.lineColor })
.addPoly(slice.camLines[0].clone().applyRotations());
}
// console.log({ webGPU: output, slices });
return slices;
}
async sliceWorker(onupdate) {
const { vertices, slices, resolution } = this;
let output = [];
let complete = 0;
for (let slice of slices) {
const recs = new topo_slicer(slice.index)
.setFromArray(vertices, slice)
.slice(resolution)
.map(rec => {
const slice = newSlice(rec.z);
for (let line of rec.lines) {
const { p1, p2 } = line;
if (!p1.swapped) { p1.swapXZ(); p1.swapped = true }
if (!p2.swapped) { p2.swapXZ(); p2.swapped = true }
}
slice.index = rec.index;
slice.addTops(sliceConnect(rec.lines));
const points = codec.encodePointArray(rec.lines.map(l => [l.p1, l.p2]).flat());
const shared = new Float32Array(new SharedArrayBuffer(points.length * 4));
shared.set(points);
slice.shared = shared;
return slice;
});
output.appendAll(recs);
onupdate(++complete / slices.length);
}
return output;
}
async sliceMinions(onupdate) {
const { queue, putCache, clearCache } = this;
const { vertices, slices, resolution } = this;
putCache("vertices", vertices);
let complete = 0;
let promises = slices.map(slice => {
return queue("topo4_slice", {
resolution,
slice
}).then(data => {
onupdate(++complete / slices.length);
return data;
});
});
// merge boxes for all rasters for contouring clipping
const output = codec.decode(await Promise.all(promises))
.map(rec => rec.recs)
.flat()
.map(rec => newSlice(rec.z)
.addTops(rec.polys)
.setFields({ shared: rec.shared }))
.sort((a, b) => a.z - b.z);
clearCache();
return output;
}
async lathe(onupdate) {
const { minions } = self.kiri_worker;
if (this.gpu) {
return await this.latheGPU(onupdate);
} else if (minions?.running > 1) {
return await this.latheMinions(onupdate);
} else {
return await this.latheWorker(onupdate);
}
}
async latheGPU(onupdate) {
return this.gpu_slices;
}
lathePath(slices, tool) {
const { resolution, step, zBottom, maxo } = this;
const tlen = tool.length;
const slen = slices.length;
const heights = [];
// console.log({ tlen, slen, sinc, slices: slices.map(s => s.z) });
// cull slice lines to only the ones in range (~5x faster)
const oslices = [];
for (let slice of slices) {
const lines = slice.lines;
const plen = lines.length;
const rec = { z: slice.z, lines: [] };
for (let i = 0; i < plen;) {
++i; // skip x which should match slice.z
let py0 = lines[i++];
const pz0 = lines[i++];
++i; // skip x which should match slice.z
let py1 = lines[i++];
const pz1 = lines[i++];
if ((py0 < -maxo && py1 < -maxo) || (py0 > maxo && py1 > maxo)) {
continue;
}
rec.lines.push(0, py0, pz0, 0, py1, pz1);
}
oslices.push(rec);
}
// iterate over all slices (real x = z)
// find max real z using z ray intersect from tool point to slice lines + offset
let lz = 0;
for (let sz = 0; ; sz += step) {
let si = Math.ceil(sz / resolution);
if (si >= oslices.length) break;
const rx = oslices[si].z;
let mz = -Infinity;
// iterate over tool offsets
for (let ti = 0; ti < tlen;) {
// tool offset in grid units from present x (si)
const xo = tool[ti++]; // x grid offset (slice)
const yo = tool[ti++] * resolution; // y grid offset (mult rez to get real y)
const zo = tool[ti++]; // real z delta offset
// get slice index corresponding with offset
const ts = si + xo;
// outside of slice array, skip
if (ts < 0 || ts >= slen - 1) {
continue;
}
const slice = oslices[ts];
const lines = slice.lines;
const plen = lines.length;
for (let i = 0; i < plen;) {
++i; // skip x which should match slice.z
let py0 = lines[i++];
const pz0 = lines[i++];
++i; // skip x which should match slice.z
let py1 = lines[i++];
const pz1 = lines[i++];
if ((py0 <= yo && py1 >= yo) || (py1 <= yo && py0 >= yo)) {
const dz = pz1 - pz0;
const dy = Math.abs(py1 - py0);
if (dy === 0) continue;
const fr = Math.abs(yo - py0) / dy;
const lz = pz0 + dz * fr + zo;
// check z height
mz = Math.max(mz, lz);
}
}
if (mz === -Infinity && xo === 0 && yo === 0) {
// tool tip is off the model
// continue;
}
}
if (mz === -Infinity) {
mz = zBottom;
} else if (mz < zBottom) {
mz = zBottom;
}
heights.push(rx, 0, lz = mz);
}
return heights;
}
async latheWorker(onupdate) {
const { sliced, tool, zoff, leave, linear } = this;
const rota = this.angle * DEG2RAD;
const steps = (Math.PI * 2) / rota;
const axis = new THREE.Vector3(1, 0, 0);
const mrot = new THREE.Matrix4().makeRotationAxis(axis, rota);
const slices = sliced.map(s => { return { z: s.z, lines: s.shared } });
const paths = [];
const recs = [];
let angle = 0;
let count = 0;
// for each step angle, find Z spine heights, produce record
while (count++ < steps) {
const heights = this.lathePath(slices, tool, paths);
recs.push({ angle, heights, degrees: angle * RAD2DEG });
angle -= rota;
for (let lines of slices.map(s => s.lines)) {
rotatePoints(lines, mrot);
}
onupdate(count / steps);
}
count = linear ? recs.length : recs[0].heights.length / 3;
// count = recs[0].heights.length / 3;
while (count-- > 0) {
let slice = newSlice(count);
slice.camLines = [newPolygon().setOpen()];
paths.push(slice);
}
if (linear) {
recs.forEach((rec, i) => {
const { degrees, heights } = rec;
[...heights].group(3).forEach((a) => {
paths[i].camLines[0].push(newPoint(a[0], a[1], a[2] + leave).setA(degrees));
});
if (i % 2 === 1) {
paths[i].camLines[0].reverse();
}
});
} else {
for (let rec of recs) {
const { degrees, heights } = rec;
[...heights].group(3).forEach((a, i) => {
// progress each path 360 degrees to prevent A rolling backwards
paths[i].camLines[0].push(newPoint(a[0], a[1], a[2] + leave).setA(degrees + i * -360));
});
}
}
for (let slice of paths) {
const poly = slice.camLines[0];
if (!poly.length) {
console.log('empty', slice);
continue;
}
// repeat first point 360 degrees progressed
const repeat = poly.points[0];
slice.camLines[0].push(repeat.clone().setA(repeat.a - 360));
slice.output()
.setLayer("lathe", { line: this.lineColor })
.addPoly(poly.clone().applyRotations().move({ z: -zoff, x: 0, y: 0 }));
}
// console.log({ tool, slices, paths });
return paths;
}
async latheMinions(onupdate) {
const { sliced, tool, zoff, leave, maxo, zBottom, step, resolution, linear } = this;
const { putCache, clearCache, queue } = this;
const rota = this.angle * DEG2RAD;
const steps = (Math.PI * 2) / rota;
const slices = sliced.map(s => { return { z: s.z, lines: s.shared } });
const paths = [];
const recs = [];
putCache("lathe", {
maxo,
tool,
step,
slices,
zBottom,
resolution,
});
let done = 0;
let tangle = 0;
let count = 0;
let promises = [];
// for each step angle, find Z spine heights, produce record
while (count++ < steps) {
const angle = tangle;
let p = new Promise(resolve => {
queue("topo4_lathe", { angle }).then(data => {
// console.log({ angle, data });
recs.push({ angle, heights: data.heights, degrees: angle * RAD2DEG });
onupdate(++done / steps);
resolve();
});
});
// await p;
promises.push(p);
tangle -= rota;
}
await Promise.all(promises);
recs.sort((a, b) => { return b.angle - a.angle });
count = linear ? recs.length : recs[0].heights.length / 3;
while (count-- > 0) {
let slice = newSlice(count);
slice.camLines = [newPolygon().setOpen()];
paths.push(slice);
}
if (linear) {
recs.forEach((rec, i) => {
const { degrees, heights } = rec;
[...heights].group(3).forEach((a) => {
paths[i].camLines[0].push(newPoint(a[0], a[1], a[2] + leave).setA(degrees));
});
if (i % 2 === 1) {
paths[i].camLines[0].reverse();
}
});
} else {
for (let rec of recs) {
const { degrees, heights } = rec;
[...heights].group(3).forEach((a, i) => {
// progress each path 360 degrees to prevent A rolling backwards
paths[i].camLines[0].push(newPoint(a[0], a[1], a[2] + leave).setA(degrees + i * -360));
});
}
}
for (let slice of paths) {
const poly = slice.camLines[0];
if (!poly.length) {
console.log('empty', slice);
continue;
}
if (!linear) {
// repeat first point 360 degrees progressed
const repeat = poly.points[0];
slice.camLines[0].push(repeat.clone().setA(repeat.a - 360));
}
slice.output()
.setLayer("lathe", { line: this.lineColor })
.addPoly(poly.clone().applyRotations().move({ z: -zoff, x: 0, y: 0 }));
}
// console.log({ tool, slices, paths });
clearCache();
return paths;
}
putCache(key, data) {
const { dispatch } = self.kiri_worker;
dispatch.putCache({ key, data }, { done: data => { } });
}
clearCache() {
const { dispatch } = self.kiri_worker;
dispatch.clearCache({}, { done: data => { } });
}
queue(cmd, params) {
const { minions } = self.kiri_worker;
return new Promise(resolve => {
minions.queue({ cmd, ...params }, resolve);
});
}
}
export function rotatePoints(lines, rot) {
new THREE.BufferAttribute(lines, 3).applyMatrix4(rot);
}
export async function generate(opt) {
return new Topo().generate(opt);
};