reorganize and rename driver files for consistency and clarity
This commit is contained in:
parent
7f50bafe9e
commit
67f405fd7b
74 changed files with 283 additions and 283 deletions
309
src/kiri/mode/cam/work/anim-2d.js
Normal file
309
src/kiri/mode/cam/work/anim-2d.js
Normal 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 }});
|
||||
}
|
||||
410
src/kiri/mode/cam/work/anim-3d.js
Normal file
410
src/kiri/mode/cam/work/anim-3d.js
Normal 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;
|
||||
}
|
||||
}
|
||||
528
src/kiri/mode/cam/work/export.js
Normal file
528
src/kiri/mode/cam/work/export.js
Normal 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;
|
||||
};
|
||||
122
src/kiri/mode/cam/work/init-work.js
Normal file
122
src/kiri/mode/cam/work/init-work.js
Normal 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
|
||||
};
|
||||
594
src/kiri/mode/cam/work/op-area.js
Normal file
594
src/kiri/mode/cam/work/op-area.js
Normal 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 };
|
||||
170
src/kiri/mode/cam/work/op-contour.js
Normal file
170
src/kiri/mode/cam/work/op-contour.js
Normal 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 };
|
||||
74
src/kiri/mode/cam/work/op-drill.js
Normal file
74
src/kiri/mode/cam/work/op-drill.js
Normal 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 };
|
||||
13
src/kiri/mode/cam/work/op-gcode.js
Normal file
13
src/kiri/mode/cam/work/op-gcode.js
Normal 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);
|
||||
}
|
||||
}
|
||||
274
src/kiri/mode/cam/work/op-helical.js
Normal file
274
src/kiri/mode/cam/work/op-helical.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/kiri/mode/cam/work/op-index.js
Normal file
47
src/kiri/mode/cam/work/op-index.js
Normal 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);
|
||||
}
|
||||
}
|
||||
17
src/kiri/mode/cam/work/op-laser-off.js
Normal file
17
src/kiri/mode/cam/work/op-laser-off.js
Normal 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);
|
||||
}
|
||||
}
|
||||
17
src/kiri/mode/cam/work/op-laser-on.js
Normal file
17
src/kiri/mode/cam/work/op-laser-on.js
Normal 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);
|
||||
}
|
||||
}
|
||||
86
src/kiri/mode/cam/work/op-lathe.js
Normal file
86
src/kiri/mode/cam/work/op-lathe.js
Normal 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 };
|
||||
91
src/kiri/mode/cam/work/op-level.js
Normal file
91
src/kiri/mode/cam/work/op-level.js
Normal 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 };
|
||||
17
src/kiri/mode/cam/work/op-loop.js
Normal file
17
src/kiri/mode/cam/work/op-loop.js
Normal 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
|
||||
}
|
||||
}
|
||||
92
src/kiri/mode/cam/work/op-outline.js
Normal file
92
src/kiri/mode/cam/work/op-outline.js
Normal 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 };
|
||||
47
src/kiri/mode/cam/work/op-pocket.js
Normal file
47
src/kiri/mode/cam/work/op-pocket.js
Normal 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 };
|
||||
166
src/kiri/mode/cam/work/op-register.js
Normal file
166
src/kiri/mode/cam/work/op-register.js
Normal 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 };
|
||||
131
src/kiri/mode/cam/work/op-rough.js
Normal file
131
src/kiri/mode/cam/work/op-rough.js
Normal 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 };
|
||||
103
src/kiri/mode/cam/work/op-shadow.js
Normal file
103
src/kiri/mode/cam/work/op-shadow.js
Normal 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 };
|
||||
53
src/kiri/mode/cam/work/op-trace.js
Normal file
53
src/kiri/mode/cam/work/op-trace.js
Normal 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 };
|
||||
32
src/kiri/mode/cam/work/op-xray.js
Normal file
32
src/kiri/mode/cam/work/op-xray.js
Normal 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 };
|
||||
1051
src/kiri/mode/cam/work/prepare.js
Normal file
1051
src/kiri/mode/cam/work/prepare.js
Normal file
File diff suppressed because it is too large
Load diff
870
src/kiri/mode/cam/work/slice.js
Normal file
870
src/kiri/mode/cam/work/slice.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
417
src/kiri/mode/cam/work/slicer-cam.js
Normal file
417
src/kiri/mode/cam/work/slicer-cam.js
Normal 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)));
|
||||
}
|
||||
}
|
||||
143
src/kiri/mode/cam/work/slicer-topo.js
Normal file
143
src/kiri/mode/cam/work/slicer-topo.js
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
1035
src/kiri/mode/cam/work/topo3.js
Normal file
1035
src/kiri/mode/cam/work/topo3.js
Normal file
File diff suppressed because it is too large
Load diff
644
src/kiri/mode/cam/work/topo4.js
Normal file
644
src/kiri/mode/cam/work/topo4.js
Normal 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);
|
||||
};
|
||||
Loading…
Reference in a new issue