move src/kiri/* to src/kiri/core and src/kiri-*/ to src/kiri/* and updated all imports

This commit is contained in:
Stewart Allen 2025-07-09 13:00:52 -04:00
commit 43104fa5ac
214 changed files with 339 additions and 341 deletions

View file

@ -0,0 +1,307 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
// WORKER BACK END ANIMATION CODE for 2D
import { Tool } from './tool.js';
const asPoints = false;
const asLines = false;
let stock, center, grid, gridX, gridY, rez;
let path, pathIndex, tool, tools, last, toolID = 1;
export function init(worker) {
const { dispatch } = worker;
dispatch.animate_setup = function(data, send) {
const { settings } = data;
const { process } = settings;
const print = worker.current.print;
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({ tools }, toolobj.getID());
tool.generateProfile(rez);
const flen = tool.fluteLength() || 15;
const slen = tool.shaftLength() || 15;
// const frad = tool.fluteDiameter() / 2;
const prof = tool.profile;
const { size, pix } = tool.profileDim;
const { pos, ind, sab } = createGrid(pix, pix, {x:size, y:size, z:flen+slen}, rez);
const mid = Math.floor(pix/2);
// deform mesh to fit tool profile
for (let i=0, il=prof.length; i < il; ) {
const dx = mid + prof[i++];
const dy = mid + prof[i++];
const dz = prof[i++];
pos[(dx * pix + dy) * 3 + 2] = -dz;
}
send.data({ mesh_add: { id:++toolID, ind, sab }});
}

View file

@ -0,0 +1,309 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
// UI FRONT END ANIMATION CODE for 2D
import { api } from '../../core/api.js';
import { space } from '../../../moto/space.js';
const { client } = api;
const asLines = false;
const asPoints = false;
let meshes = {},
button = {},
label = {},
unitScale = 1,
speedValues = [ 1, 2, 4, 8, 32 ],
speedPauses = [ 30, 20, 10, 5, 0 ],
speedNames = [ "1x", "2x", "4x", "8x", "!!" ],
speedMax = speedValues.length - 1,
speedIndex = 0,
speed,
color = 0,
material,
origin,
posOffset = { x:0, y:0, z:0 };
export function animate_clear(api) {
let { anim } = api.ui;
space.platform.showGridBelow(true);
client.animate_cleanup();
Object.keys(meshes).forEach(id => deleteMesh(id));
toggleStock(undefined,true,false);
api.uc.setVisible(anim.laba, false);
api.uc.setVisible(anim.vala, false);
}
export function animate(api, delay) {
let alert = api.alerts.show("building animation");
let settings = api.conf.get();
client.animate_setup(settings, data => {
checkMeshCommands(data);
if (!(data && data.mesh_add)) {
return;
}
let { anim } = api.ui;
Object.assign(button, {
replay: anim.replay,
play: anim.play,
step: anim.step,
pause: anim.pause,
speed: anim.speed,
trans: anim.trans,
model: anim.model,
shade: anim.shade
});
Object.assign(label, {
progress: anim.progress,
speed: anim.labspd,
x: anim.valx,
y: anim.valy,
z: anim.valz,
});
origin = settings.origin;
speedIndex = api.local.getInt('cam.anim.speed') || 0;
updateSpeed();
setTimeout(step, delay || 0);
button.replay.onclick = replay;
button.play.onclick = play;
button.step.onclick = step;
button.pause.onclick = pause;
button.speed.onclick = fast;
button.trans.onclick = toggleTrans;
button.model.onclick = toggleModel;
button.shade.onclick = toggleStock;
button.play.style.display = '';
button.pause.style.display = 'none';
api.event.emit('animate', 'CAM');
api.alerts.hide(alert);
space.platform.showGridBelow(false);
toggleTrans(0,api.local.getBoolean('cam.anim.trans', true));
toggleModel(0,api.local.getBoolean('cam.anim.model', false));
toggleStock(0,api.local.getBoolean('cam.anim.stock', false));
});
}
Object.assign(client, {
animate(data, ondone) {
client.send("animate", data, ondone);
},
animate_setup(settings, ondone) {
color = settings.controller.dark ? 0x48607B : 0x607FA4;
unitScale = settings.controller.units === 'in' ? 1/25.4 : 1;
let flatShading = true,
transparent = true,
opacity = 0.9,
side = THREE.DoubleSide;
material = new THREE.MeshPhongMaterial({
flatShading,
transparent,
opacity,
color,
side
});
add_red_neg_z(material);
client.send("animate_setup", {settings}, ondone);
},
animate_cleanup(data, ondone) {
client.send("animate_cleanup", data, ondone);
}
});
function meshAdd(id, ind, pos, sab) {
const geo = new THREE.BufferGeometry();
if (sab) {
// use array buffer shared with worker
pos = new Float32Array(sab);
}
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
if (ind.length) {
geo.setIndex(new THREE.BufferAttribute(new Uint32Array(ind), 1));
}
let mesh;
if (asPoints) {
const mat = new THREE.PointsMaterial({
transparent: true,
opacity: 0.75,
color: 0x888888,
size: 0.3
});
mesh = new THREE.Points(geo, mat);
} else if (asLines) {
const mat = new THREE.LineBasicMaterial({
transparent: true,
opacity: 0.75,
color
});
mesh = new THREE.LineSegments(geo, mat);
} else {
geo.computeVertexNormals();
mesh = new THREE.Mesh(geo, material);
mesh.renderOrder = -10;
}
space.world.add(mesh);
meshes[id] = mesh;
}
function meshUpdates(id) {
const mesh = meshes[id];
if (!mesh) {
return; // animate cancelled
}
mesh.geometry.attributes.position.needsUpdate = true;
space.update();
}
function deleteMesh(id) {
space.world.remove(meshes[id]);
delete meshes[id];
}
function toggleModel(ev,bool) {
api.local.toggle('cam.anim.model', bool);
api.widgets.all().forEach(w => w.toggleVisibility(bool));
}
function toggleStock(ev,bool,set) {
set !== false && api.local.toggle('cam.anim.stock', bool);
return api.event.emit('cam.stock.toggle', bool ?? undefined);
}
function toggleTrans(ev,bool) {
bool = api.local.toggle('cam.anim.trans', bool);
material.transparent = bool;
material.needsUpdate = true;
}
function step() {
updateSpeed();
client.animate({speed, steps: 1}, handleGridUpdate);
}
function play(opts) {
const { steps } = opts;
updateSpeed();
if (steps !== 1) {
button.play.style.display = 'none';
button.pause.style.display = '';
}
client.animate({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
}, handleGridUpdate);
}
function fast(opts) {
const { steps } = opts;
updateSpeed(1);
button.play.style.display = 'none';
button.pause.style.display = '';
client.animate({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
}, handleGridUpdate);
}
function pause() {
button.play.style.display = '';
button.pause.style.display = 'none';
client.animate({speed: 0}, handleGridUpdate);
}
function handleGridUpdate(data) {
checkMeshCommands(data);
if (data && data.progress) {
label.progress.value = (data.progress * 100).toFixed(1);
}
}
function updateSpeed(inc = 0) {
if (inc === Infinity) {
speedIndex = speedMax;
} else if (inc > 0) {
speedIndex = (speedIndex + inc) % speedValues.length;
}
api.local.set('cam.anim.speed', speedIndex);
speed = speedValues[speedIndex];
label.speed.value = speedNames[speedIndex];
}
function replay() {
animate_clear(api);
setTimeout(() => {
animate(api, 50);
}, 250);
}
function checkMeshCommands(data) {
if (!data) {
return;
}
if (data.mesh_add) {
const { id, ind, pos, offset, sab } = data.mesh_add;
meshAdd(id, ind, pos, sab);
space.refresh();
if (offset) {
posOffset = offset;
}
}
if (data.mesh_del) {
deleteMesh(data.mesh_del);
}
if (data.mesh_move) {
const { id, pos } = data.mesh_move;
const mesh = meshes[id];
if (mesh) {
mesh.position.x = pos.x;
mesh.position.y = pos.y;
mesh.position.z = pos.z;
space.update();
}
label.x.value = (pos.x - origin.x).toFixed(2);
label.y.value = (pos.y + origin.y).toFixed(2);
label.z.value = (pos.z - origin.z).toFixed(2);
}
if (data.mesh_update) {
meshUpdates(data.id);
}
}
// SHADER: tint points below z=0 with red
function add_red_neg_z(material) {
material.onBeforeCompile = (shader) => {
shader.vertexShader = shader.vertexShader.replace(
`#include <worldpos_vertex>`,
`
#include <worldpos_vertex>
vWorldPosition = vec3(transformed);
`
);
shader.vertexShader = `
varying vec3 vWorldPosition;
` + shader.vertexShader;
shader.fragmentShader = `
varying vec3 vWorldPosition;
` + shader.fragmentShader;
shader.fragmentShader = shader.fragmentShader.replace(
`#include <dithering_fragment>`,
`
#include <dithering_fragment>
if (vWorldPosition.z < 0.0) {
gl_FragColor.rgb += vec3(0.5, 0.0, 0.0); // Add red tint
}
`
);
};
return material;
}

View file

@ -0,0 +1,396 @@
/** 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 './tool.js';
let nextMeshID = 1;
let stock, center, rez;
let path, pathIndex, tool, tools, last;
let stockZ;
let stockIndexMsg = false;
let stockSlices;
let stockIndex;
let startTime;
let toolID = -1;
let toolMesh;
let toolRadius;
let toolUpdateMsg;
let animateClear = false;
let animating = false;
let moveDist = 0;
let renderDist = 0;
let renderDone = false;
let renderPause = 10;
let renderSteps = 0;
let renderSpeed = 0;
let indexCount = 0;
let updates = 0;
export function init(worker) {
const { dispatch } = worker;
dispatch.animate_setup2 = function (data, send) {
const { settings } = data;
const { process } = settings;
const print = worker.current.print;
const density = parseInt(settings.controller.animesh) * 1000;
const isIndexed = process.camStockIndexed;
pathIndex = 0;
path = print.output.flat();
tools = settings.tools;
stock = settings.stock;
rez = 1 / Math.sqrt(density / (stock.x * stock.y));
tool = null;
last = null;
animating = false;
animateClear = false;
stockIndex = 0;
indexCount = 0;
startTime = 0;
updates = 0;
stockZ = isIndexed ? 0 : stock.z;
stockSlices = [];
const { x, y, z } = stock;
const sliceCount = parseInt(settings.controller.animesh || 2000) / 100;
const sliceWidth = stock.x / sliceCount;
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({ mesh_move: { id: slice.id, pos: { x:0, y:0, z: stock.z/2} } });
}
send.done();
};
dispatch.animate2 = 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;
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++;
// console.log(next.point.z);
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 = rez * 2;
if (last) {
const lp = last.point, np = next.point;
last = next;
// 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 = Math.abs((np.a || 0) - (lp.a || 0)),
dr = (da / 360) * (2 * Math.PI * Math.max(np.z, lp.z)),
dist = Math.sqrt(dx * dx + dy * dy + dz * dz + dr * dr);
moveDist += dist;
// skip moves that are less than grid resolution
if (moveDist < rezstep) {
// console.log('skip', moveDist, rezstep, next);
renderPath(send);
return;
}
const md = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz), dr);
const st = Math.ceil(md / rezstep);
const mx = dx / st, my = dy / st, mz = dz / st;
// const sd = Math.sqrt(mx*mx + my*my + mz*mz + dr*dr);
const sd = Math.sqrt(mx * mx + my * my + Math.min(1, mz * mz) + dr * dr);
const moves = [];
for (let i = 0, x = lp.x, y = lp.y, z = lp.z; i < st; i++) {
moves.push({ x, y, z, a: lp.a, md: sd, dx, dy, dz });
x += mx;
y += my;
z += mz;
}
moveDist = 0;
moves.push({ ...next.point, md: sd });
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 } = pos;
toolMove(pos);
// console.log('renderMoves', {id, moves, seed});
let subs = 0;
if (dx || dy || dz < 0)
for (let slice of stockSlices) {
if (slice.bounds.intersectsBox(toolMesh.bounds)) {
slice.subtractTool(toolMesh);
subs++;
}
}
// console.log({ index, subs });
renderDist += pos.md;
// pause renderer at specified offsets
if (renderSpeed && renderDist >= renderSpeed) {
renderDist = 0;
renderUpdate(send);
setTimeout(() => {
renderMoves(id, moves, send, index + 1);
}, renderPause);
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({ tools }, 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.isTaperMill()) {
const trad = Math.max(tool.tipDiameter() / 2, 0.001);
mesh = cylinder(slen, srad, srad, 20, true).translate(0, 0, slen / 2)
.add(cylinder(flen, trad, frad, 20, true).translate(0, 0, -flen / 2));
} else {
mesh = cylinder(tlen, frad, frad, 20, true)
.add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen / 2));
}
mesh = mesh.translate(0, 0, (tlen - stockZ) / 2);
const raw = mesh.getMesh();
const vertex = raw.vertProperties;
const index = raw.triVerts;
toolMesh = { root: mesh, index, vertex, bounds: CSG.toBox3(mesh) };
send.data({ mesh_add: { id: --toolID, ind: index, pos: vertex } });
}
class Stock {
constructor(x, y, z) {
this.id = nextMeshID++;
this.vbuf = undefined;
this.ibuf = undefined;
this.ilen = 0;
this.sends = 0;
this.newbuf = true;
this.subtracts = 0;
this.mesh = CSG.Instance().Manifold.cube([x, y, z], true);
}
send(send) {
const newbuf = this.newbuf;
const action = this.sends++ === 0 ? 'mesh_add' : 'mesh_update';
send.data({
[action]: {
id: this.id,
ind: newbuf ? this.ibuf : undefined,
pos: newbuf ? this.vbuf : undefined,
ilen: this.ilen,
plen: this.plen
}
});
this.newbuf = false;
this.sends++;
// console.log({ send: this.id, newbuf, action });
}
translate(x, y, z) {
// console.log({ translate: this.id, x, y, z });
const oldmesh = this.mesh;
this.mesh = this.mesh.translate(x, y, z);
this.bounds = CSG.toBox3(this.mesh);
oldmesh.delete();
return this;
}
updateMesh(updates) {
if (this.sends > 0 && this.subtracts === 0) {
return;
}
const subs = this.subtracts;
this.subtracts = 0;
let start = Date.now();
let mesh = this.mesh.getMesh();
this.sharedVertexBuffer(mesh.numVert * 3);
this.sharedIndexBuffer(mesh.numTri * 3);
this.vbuf.set(mesh.vertProperties);
this.ibuf.set(mesh.triVerts);
let sub = Date.now();
updates.push(this);
if (false) console.log({
update: this.id,
time: sub - start,
subs,
mssub: ((sub - start) / subs).round(3)
});
}
subtractTool(toolMesh) {
const oldmesh = this.mesh;
this.mesh = this.mesh.subtract(toolMesh.mesh);
oldmesh.delete();
this.subtracts++;
}
sharedVertexBuffer(size) {
const old = this.vbuf;
this.plen = size;
if (old && old.length >= size) {
// console.log({svb: this.id, reuse: size, old: old.length});
return old;
}
// let buf = new Float32Array(new SharedArrayBuffer(size * 4));
let buf = new Float32Array(new SharedArrayBuffer(size * 4 + 1024 * 1024));
// console.log({new_svb: this.id, size, buf});
this.newbuf = true;
return this.vbuf = buf;
}
sharedIndexBuffer(size) {
const old = this.ibuf;
this.ilen = size;
if (old && old.length >= size) {
// console.log({sib: this.id, reuse: size, old: old.length});
return old;
}
// let buf = new Uint32Array(new SharedArrayBuffer(size * 4));
let buf = new Uint32Array(new SharedArrayBuffer(size * 4 + 1024 * 1024));
// console.log({new_sib: this.id, size, buf});
this.newbuf = true;
return this.ibuf = buf;
}
}

View file

@ -0,0 +1,253 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
// UI FRONT END ANIMATION CODE for 3D (indexed)
import { api } from '../../core/api.js';
import { space } from '../../../moto/space.js';
const { client } = api;
let meshes = {},
button = {},
label = {},
material,
speedPauses = [0, 0, 0, 0, 0],
speedValues = [1, 2, 4, 8, 32],
speedNames = ["1x", "2x", "4x", "8x", "!!"],
speedMax = speedValues.length - 1,
speedIndex = 0,
speed,
origin,
color = 0;
export function animate_clear2(api) {
let { anim } = api.ui;
client.animate_cleanup2();
Object.keys(meshes).forEach(id => deleteMesh(id));
api.widgets.setAxisIndex(0);
api.uc.setVisible(anim.laba, true);
api.uc.setVisible(anim.vala, true);
anim.vala.value = "0.0";
}
export function animate2(api, delay) {
let alert = api.alerts.show("building animation");
let settings = api.conf.get();
client.animate_setup2(settings, data => {
handleUpdate(data);
if (data) {
return;
}
let { anim } = api.ui;
Object.assign(button, {
replay: anim.replay,
play: anim.play,
step: anim.step,
pause: anim.pause,
speed: anim.speed,
trans: anim.trans,
model: anim.model,
shade: anim.shade
});
Object.assign(label, {
progress: anim.progress,
speed: anim.labspd,
x: anim.valx,
y: anim.valy,
z: anim.valz,
a: anim.vala
});
updateSpeed(0);
setTimeout(step, delay || 0);
toggleTrans(undefined, false);
origin = settings.origin;
button.replay.onclick = replay;
button.play.onclick = play;
button.step.onclick = step;
button.pause.onclick = pause;
button.speed.onclick = fast;
button.trans.onclick = toggleTrans;
button.model.onclick = toggleModel;
button.shade.onclick = toggleStock;
button.play.style.display = '';
button.pause.style.display = 'none';
api.event.emit('animate', 'CAM');
api.alerts.hide(alert);
});
}
Object.assign(client, {
animate2(data, ondone) {
client.send("animate2", data, ondone);
},
animate_setup2(settings, ondone) {
color = settings.controller.dark ? 0x888888 : 0;
material = new THREE.MeshMatcapMaterial({
flatShading: true,
transparent: false,
opacity: 0.9,
color: 0x888888,
side: THREE.DoubleSide
});
client.send("animate_setup2", { settings }, ondone);
},
animate_cleanup2(data, ondone) {
client.send("animate_cleanup2", data, ondone);
}
});
function meshAdd(id, ind, pos, ilen, plen) {
const geo = new THREE.BufferGeometry();
const pa = plen ? pos.subarray(0, plen * 3) : pos;
const ia = ilen ? ind.subarray(0, ilen) : ind;
geo.setAttribute('position', new THREE.BufferAttribute(pa, 3));
geo.setIndex(new THREE.BufferAttribute(ia, 1));
const mesh = new THREE.Mesh(geo, material);
mesh.pos = pos;
mesh.ind = ind;
space.world.add(mesh);
meshes[id] = mesh;
}
function meshUpdate(id, ind, pos, ilen, plen) {
const mesh = meshes[id];
if (!mesh) {
return; // animate cancelled
}
const geo = mesh.geometry;
mesh.pos = pos || mesh.pos;
mesh.ind = ind || mesh.ind;
geo.setAttribute('position', new THREE.BufferAttribute(mesh.pos.subarray(0, plen * 3), 3));
geo.setIndex(new THREE.BufferAttribute(mesh.ind.subarray(0, ilen), 1));
geo.attributes.position.needsUpdate = true;
geo.index.needsUpdate = true;
space.update();
}
function deleteMesh(id) {
space.world.remove(meshes[id]);
delete meshes[id];
}
function toggleModel(ev, bool) {
api.local.toggle('cam.anim.model', bool);
api.widgets.all().forEach(w => w.toggleVisibility(bool));
}
function toggleStock(ev, bool, set) {
set !== false && api.local.toggle('cam.anim.stock', bool);
return api.event.emit('cam.stock.toggle', bool ?? undefined);
}
function toggleTrans(ev, bool) {
bool = api.local.toggle('cam.anim.trans', bool);
material.transparent = bool;
material.needsUpdate = true;
}
function step() {
updateSpeed();
client.animate2({ speed, steps: 1 }, handleUpdate);
}
function play(opts) {
const { steps } = opts;
updateSpeed();
if (steps !== 1) {
button.play.style.display = 'none';
button.pause.style.display = '';
}
client.animate2({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
}, handleUpdate);
}
function fast(opts) {
const { steps } = opts;
updateSpeed(1);
button.play.style.display = 'none';
button.pause.style.display = '';
client.animate2({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
}, handleUpdate);
}
function pause() {
button.play.style.display = '';
button.pause.style.display = 'none';
client.animate2({ speed: 0 }, handleUpdate);
}
function handleUpdate(data) {
if (!data) {
return;
}
if (data.mesh_add) {
const { id, ind, pos, ilen, plen } = data.mesh_add;
meshAdd(id, ind, pos, ilen, plen);
space.refresh();
}
if (data.mesh_del) {
deleteMesh(data.mesh_del);
}
if (data.mesh_move) {
const { id, pos } = data.mesh_move;
const mesh = meshes[id];
if (mesh) {
mesh.position.x = pos.x;
mesh.position.y = pos.y;
mesh.position.z = pos.z;
space.refresh();
}
label.x.value = (pos.x - origin.x).toFixed(2);
label.y.value = (pos.y + origin.y).toFixed(2);
label.z.value = (pos.z - origin.z).toFixed(2);
}
if (data.stock_index !== undefined) {
api.widgets.setAxisIndex(data.stock_index);
label.a.value = -data.stock_index.toFixed(1);
}
if (data.mesh_index) {
const { id, index } = data.mesh_index;
const mesh = meshes[id];
if (mesh) {
mesh.rotation.x = (Math.PI / 180) * index;
space.refresh();
}
}
if (data.mesh_update) {
const { id, ind, pos, ilen, plen } = data.mesh_update;
meshUpdate(id, ind, pos, ilen, plen);
}
if (data && data.progress) {
label.progress.value = (data.progress * 100).toFixed(1);
}
}
function updateSpeed(inc = 0) {
if (inc === Infinity) {
speedIndex = speedMax;
} else if (inc > 0) {
speedIndex = (speedIndex + inc) % speedValues.length;
}
api.local.set('cam.anim.speed', speedIndex);
speed = speedValues[speedIndex];
label.speed.value = speedNames[speedIndex];
}
function replay() {
animate_clear2(api);
setTimeout(() => {
animate2(api, 50);
}, 250);
}

View file

@ -0,0 +1,84 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { api } from '../../core/api.js';
import { env, opRender } from './client.js';
import { CAM } from './driver-fe.js';
import { clearTabs, restoreTabs } from './cl-tab.js';
import { updateStock } from './cl-stock.js';
export function opFlip() {
api.view.set_arrange();
let widgets = api.widgets.all();
let { process } = env.current;
let { ops, op2 } = process;
// add flip singleton to b-side
let add2 = op2.length === 0;
let axis = env.poppedRec.axis;
env.flipping = true;
process.camZAnchor = {
top: "bottom",
bottom: "top",
middle: "middle"
}[process.camZAnchor];
// flip tabs
for (let widget of widgets) {
let anno = api.widgets.annotate(widget.id).tab || [];
let wbm = widget.bounds.max.z;
for (let tab of anno) {
let box = widget.tabs[tab.id].box;
let bpo = box.position;
let xr = 0, yr = 0;
let flz = wbm - bpo.z;
if (axis === 'X') {
tab.pos.y = -tab.pos.y;
bpo.y = -bpo.y;
xr = Math.PI / 2;
}
if (axis === 'Y') {
tab.pos.x = -tab.pos.x;
bpo.x = -bpo.x;
yr = Math.PI / 2;
}
tab.pos.z = bpo.z = flz;
let [rx, ry, rz, rw] = tab.rot;
let qat = new THREE.Quaternion(rx, ry, rz, rw);
let eul = new THREE.Euler().setFromQuaternion(qat);
eul._z = -eul._z;
tab.rot = new THREE.Quaternion().setFromEuler(eul);
}
clearTabs(widget, true);
restoreTabs([widget]);
}
// flip widget
if (axis === 'X') {
api.selection.rotate(Math.PI, 0, 0);
}
if (axis === 'Y') {
api.selection.rotate(0, Math.PI, 0);
}
// clear traces cache
CAM.traces_clear();
api.client.clear();
env.flipping = false;
process.ops = op2;
process.op2 = ops;
// flip camZBottom
if (env.poppedRec.invert && process.camZBottom && env.camZBottom) {
const maxZ = env.camZBottom._max.z
process.camZBottom = maxZ - process.camZBottom;
api.util.rec2ui(process);
updateStock();
}
// keep flip operations in sync
for (let op of op2) {
if (op.type === 'flip') {
op.axis = env.poppedRec.axis;
op.invert = env.poppedRec.invert;
}
}
if (add2) {
opAdd(env.poppedRec);
} else {
opRender();
}
}

View file

@ -0,0 +1,218 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { api } from '../../core/api.js';
import { env, clearPops } from './client.js';
import { CAM } from './driver-fe.js';
import { lastTrace, setLastTrace } from './cl-trace.js';
export let holeSelOn = false;
export let lastSelHoles;
/**
* Client side function to select holes in widgets for CAM operations.
* If holes have already been analyzed, they are displayed immediately.
* Otherwise, the CAM server is queried and the results are cached.
* @param {boolean} individual - select all holes, false to only select
* holes of tool diameter.
*/
export async function selectHoles(individual) {
// console.log("client individual selected",individual)
if (holeSelOn) {
return selectHolesDone();
}
clearPops();
let alert = api.show.alert("analyzing parts...");
holeSelOn = env.hoveredOp;
holeSelOn.classList.add("editing");
api.feature.hover = true;
api.feature.hoverAdds = true;
env.hover = selectHolesHover;
env.hoverUp = selectHolesHoverUp;
const widgets = api.widgets.all()
/**
* creates a mesh for a hole and adds it to a widget
* @param {Object3D} widget - widget to add the hole mesh to
* @param {Object} drill - {depth,selected} object of the hole
* @returns {Mesh} the created mesh
*/
function createHoleMesh(widget, drill) {
let { depth, selected, diam } = drill
let color = selected ? 0xFF0000 : 0x39e366
let geo = new THREE.CylinderGeometry(diam / 2, diam / 2, depth, 20);
const material = new THREE.MeshPhongMaterial({ color });
let mesh = new THREE.Mesh(geo, material);
mesh.position.copy(drill)
mesh.position.z -= depth / 2
mesh.rotation.x = Math.PI / 2
drill.widgetID = widget.id
drill.meshId = mesh.id; // add pointers to both objects
mesh.hole = drill;
mesh.parent = widget.mesh;
widget.mesh.add(mesh);
widget.adds.push(mesh); // for click detection
return mesh
}
let meshesCached = widgets.every(widget => env.poppedRec.drills[widget.id] != undefined)
if (individual && meshesCached) {
// if any widget already has cached holes
// console.log("already has cached holes",env.poppedRec.drills)
api.hide.alert(alert);
api.widgets.for(widget => {
if (widget.adds) {
let drills = env.poppedRec.drills[widget.id];
let ids = drills.map(hole => hole.meshId);
if (false && widget.adds.includes(mesh)) {
// mesh in the old code was a dead artifact
// so this code branch never actually ran
widget.adds.forEach(add => {
if (ids.includes(add.id)) {
add.visible = true;
}
})
} else {
drills.forEach(drill => {
createHoleMesh(widget, drill);
});
}
}
})
} else {
// if no widget has cached holes
let alert2 = api.show.alert("");
let found = await CAM.holes(
individual,
env.poppedRec,
(progress, msg) => {
alert2[0] = msg;
api.show.progress(progress, msg);
api.alerts.update();
},
async centers => {
api.show.progress(0);
if (!Array.isArray(centers)) {
console.log("worker returned a malformed drills response");
return;
}
if (centers.length === 0) {
console.log("no drill holes found");
return;
}
let shadow = centers.some(c => c.shadowed);
api.hide.alert(alert2);
if (shadow) {
alert2 = api.show.alert("Some holes are shadowed by part and are not shown.");
}
centers = centers ?? [];
// list of all hole centers and if they are selected
api.widgets.for(widget => {
const { holes } = centers.find(center => center.id == widget.id);
// console.log(holes)
if (!holes.length) unselectHoles(holes);
holes.forEach(hole => {
createHoleMesh(widget, hole);
});
//add hole data to record
env.poppedRec.drills = env.poppedRec.drills ?? {};
env.poppedRec.drills[widget.id] = holes;
//give widget access to an array of drill records that refrence it
//so that it can be cleared when widget is rotated or mirrored etc.
if (!widget.drills) { widget.drills = [] };
widget.drills.push(holes);
})
}
);
if (!found || found.length === 0) {
api.hide.alert(alert);
api.hide.alert(alert2);
api.show.alert("no drill holes found");
return;
}
}
//hide the alert once hole meshes are calculated on the worker, and then added to the scene
// api.hide.alert(alert);
let escAlert = api.show.alert("[esc] cancels drill editing", 1000);
setTimeout(() => {
api.hide.alert(escAlert);
}, 5000);
api.widgets.opacity(0.8);
}
export function selectHolesHover(data) {
//not used right now. may be useful in the future
if (lastTrace) {
let { color, colorSave } = lastTrace.material[0] || lastTrace.material;
color.r = colorSave.r;
color.g = colorSave.g;
color.b = colorSave.b;
lastTrace.position.z -= 0.01;
}
if (data.type === 'platform') {
setLastTrace();
return;
}
}
export function selectHolesHoverUp(int, ev) {
if (!int) return; //if not a hole mesh return
let { object } = int;
selectHoleToggle(object);
}
/**
* Toggle the selection of a hole mesh and update its color
* @param {Object3D} mesh - the hole mesh to toggle
*/
export function selectHoleToggle(mesh) {
let { hole } = mesh
if (!hole) return
hole.selected = !hole.selected;
mesh.material.color.setHex(hole.selected ? 0xFF0000 : 0x39e366);
}
/**
* Clears the recorded holes in the widget (widget.drills array)
* and also clears the widget.adds array.
* @param {Object} widget - the widget with the drills array to clear
*/
export function clearHolesRec(widget) {
if (widget.drills) {
widget.drills.forEach(rec => {
})
}
if (widget.adds) {
widget.adds.length = 0 //clear adds array
}
}
/**
* Cleanup function for selectHoles.
* Removes all adds from the scene, hides the alert, and resets the opacity of the widgets.
* Also resets the hover features and the editing class on the holeSelOn html element.
*/
export function selectHolesDone() {
if (!holeSelOn) {
return;
}
env.func.unpop();
holeSelOn.classList.remove("editing");
holeSelOn = false;
api.widgets.opacity(1);
api.hide.alert(alert);
api.feature.hover = false;
api.feature.hoverAdds = false;
api.widgets.for(widget => {
for (let add of widget.adds) {
add.visible = false
widget.mesh.remove(add);
}
});
}
export function unselectHoles(widget) {
if (!widget.holes) return
widget.holes.forEach(hole => {
hole.selected = false
})
}

598
src/kiri/mode/cam/cl-ops.js Normal file
View file

@ -0,0 +1,598 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { $, h } from '../../../moto/webui.js';
import { api } from '../../core/api.js';
import { env } from './client.js';
import { Tool } from './tool.js';
import { opFlip } from './cl-flip.js';
import { selectHoles } from './cl-hole.js';
import { surfaceAdd } from './cl-surface.js';
import { traceAdd } from './cl-trace.js';
const { MODES } = api.const;
const { uc: UC } = api;
const { alerts, conf } = api;
const { current: LANG } = api.language;
let seed = Date.now();
function hasIndexing() {
return env.isIndexed;
}
function hasSpindle() {
return env.current.device.spindleMax > 0;
}
function zTop() {
return api.conf.get().process.camZTop > 0;
}
function zBottom() {
return api.conf.get().process.camZBottom > 0;
}
// create custom gcode editor function
function gcodeEditor(label, field) {
return function () {
opGCode(label, field);
}
}
function opGCode(label, field = 'gcode') {
api.dialog.show('any');
const { c_gcode } = h.bind(
$('mod-any'), h.div({ id: "camop_dialog" }, [
h.label(label || 'custom gcode operation'),
h.textarea({ id: "c_gcode", rows: 15, cols: 50 }),
h.button({
_: 'done', onclick: () => {
api.dialog.hide();
api.conf.save();
}
})
])
);
let av = env.poppedRec[field] || [];
c_gcode.value = typeof (av) === 'string' ? av : av.join('\n');
c_gcode.onkeyup = (el) => {
env.poppedRec[field] = c_gcode.value.trim().split('\n');
};
c_gcode.focus();
}
/**
* Create a new popup operation (popOp) for a given type.
* @param {string} type - the name of the popOp to create
* @param {object} map - an object mapping popOp keys to either a string
* (representing a key in current.process) or a value (to be used as the default).
* @returns {object} the newly created popOp.
*/
export function createPopOp(type, map) {
let op = env.popOp[type] = {
div: UC.newElement('div', { id: `${type}-op`, class: "cam-pop-op" }),
use: (rec) => {
op.rec = rec;
for (let [key, val] of Object.entries(op.inputs)) {
let type = val.type;
let from = map[key];
let rval = rec[key];
// fill undef entries older defines
if (type && (rval === null || rval === undefined)) {
if (typeof (from) === 'string') {
rec[key] = env.current.process[from];
} else if (from !== undefined) {
rec[key] = from;
} else {
console.log('error', { key, val, type, from });
}
}
}
api.util.rec2ui(rec, op.inputs);
op.hideshow();
},
using: (rec) => {
return op.rec === rec;
},
bind: (ev) => {
api.util.ui2rec(op.rec, op.inputs);
const settings = conf.get();
const { tool } = new Tool(settings, op.rec.tool); //get tool by id
const opType = op.rec.type
const drillOrRegister = opType == "drill" || opType == "register"
if (!drillOrRegister && tool.type == "drill") {
alerts.show(`Warning: Drills should not be used for ${opType} operations.`)
}
else if (drillOrRegister && tool.type != "drill") {
alerts.show(`Warning: Only drills should be used for drilling operations.`)
}
for (let [key, val] of Object.entries(op.rec)) {
let saveTo = map[key];
if (saveTo && typeof (key) === 'string' && !key.startsWith("~")) {
env.current.process[saveTo] = val;
}
}
api.conf.save();
op.hideshow();
},
new: () => {
let rec = { type };
for (let [key, src] of Object.entries(map)) {
rec[key] = typeof (src) === 'string'
? env.current.process[src.replace('~', '')]
: src;
}
return rec;
},
hideshow: () => {
for (let inp of Object.values(op.inputs)) {
let parent = inp.parentElement;
if (parent && parent.setVisible && parent.__opt.show) {
parent.setVisible(parent.__opt.show(op, api.conf.get()));
}
}
},
addNote: () => {
if (!op.note && type !== 'flip') {
const divid = `div-${++seed}`;
const noteid = `note-${++seed}`;
const div = document.createElement('div');
div.setAttribute('id', divid);
div.classList.add('pop-tics')
op.div.appendChild(div);
div.innerHTML = h.build(
h.div([h.label({ id: noteid })])
);
op.note = { divid, noteid };
}
if (op.note) {
const { divid, noteid } = op.note;
const div = $(divid);
if (div) div.onclick = () => {
api.uc.prompt('Edit Note for Operation', env.poppedRec.note || '').then(note => {
if (note !== undefined && note !== null) {
env.poppedRec.note = op.x = note;
api.conf.save();
}
opRender();
});
};
const note = $(noteid);
if (note) note.innerText = env.poppedRec.note || '';
}
},
group: [],
};
/**
* @function createRecordGetter
* @description Creates a getter function for the current record.
* The getter will bind the current record to the op before returning it.
* This is useful for passing a record to a pre-slice function
* @returns {function} A getter function for the current record.
*/
op.createRecordGetter = () => {
return () => {
op.bind();
return op.rec;
}
}
UC.restore({
addTo: op.div,
bindTo: op.bind,
lastDiv: op.div,
lastGroup: op.group
});
return op;
}
export function createPopOps() {
createPopOp('level', {
tool: 'camLevelTool',
spindle: 'camLevelSpindle',
step: 'camLevelOver',
rate: 'camLevelSpeed',
down: 'camLevelDown',
over: 'camLevelOver',
stock: 'camLevelStock'
}).inputs = {
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
sep: UC.newBlank({ class: "pop-sep" }),
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: UC.toInt, show: hasSpindle }),
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: UC.toFloat, bound: UC.bound(0.01, 1.0) }),
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: UC.toInt, units: true }),
down: UC.newInput(LANG.cc_loff_s, { title: LANG.cc_loff_l, convert: UC.toFloat, units: true }),
over: UC.newInput(LANG.cc_lxyo_s, { title: LANG.cc_lxyo_l, convert: UC.toFloat, units: true }),
sep: UC.newBlank({ class: "pop-sep" }),
stock: UC.newBoolean(LANG.cc_lsto_s, undefined, { title: LANG.cc_lsto_l }),
};
createPopOp('rough', {
tool: 'camRoughTool',
spindle: 'camRoughSpindle',
down: 'camRoughDown',
step: 'camRoughOver',
rate: 'camRoughSpeed',
plunge: 'camRoughPlunge',
leave: 'camRoughStock',
leavez: 'camRoughStockZ',
all: 'camRoughAll',
voids: 'camRoughVoid',
flats: 'camRoughFlat',
inside: 'camRoughIn',
ov_topz: 0,
ov_botz: 0,
ov_conv: '~camConventional',
}).inputs = {
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
sep: UC.newBlank({ class: "pop-sep" }),
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: UC.toInt, show: hasSpindle }),
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: UC.toInt, units: true }),
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: UC.toInt, units: true }),
sep: UC.newBlank({ class: "pop-sep" }),
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: UC.toFloat, units: true }),
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: UC.toFloat, bound: UC.bound(0.01, 1.0) }),
leave: UC.newInput(LANG.cr_lsto_s, { title: LANG.cr_lsto_l, convert: UC.toFloat, units: true }),
leavez: UC.newInput(LANG.cr_lstz_s, { title: LANG.cr_lstz_l, convert: UC.toFloat, bound: UC.bound(0, 10), units: true }),
sep: UC.newBlank({ class: "pop-sep" }),
all: UC.newBoolean(LANG.cr_clst_s, undefined, { title: LANG.cr_clst_l, show: hasIndexing }),
voids: UC.newBoolean(LANG.cr_clrp_s, undefined, { title: LANG.cr_clrp_l }),
flats: UC.newBoolean(LANG.cr_clrf_s, undefined, { title: LANG.cr_clrf_l }),
inside: UC.newBoolean(LANG.cr_olin_s, undefined, { title: LANG.cr_olin_l }),
sep: UC.newBlank({ class: "pop-sep" }),
exp: UC.newExpand("overrides"),
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: UC.toFloat, units: true }),
ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: UC.toFloat, units: true }),
ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, { title: LANG.ou_conv_l }),
exp_end: UC.endExpand(),
};
createPopOp('outline', {
tool: 'camOutlineTool',
spindle: 'camOutlineSpindle',
step: 'camOutlineOver',
steps: 'camOutlineOverCount',
down: 'camOutlineDown',
rate: 'camOutlineSpeed',
plunge: 'camOutlinePlunge',
dogbones: 'camOutlineDogbone',
omitvoid: 'camOutlineOmitVoid',
omitthru: 'camOutlineOmitThru',
outside: 'camOutlineOut',
inside: 'camOutlineIn',
wide: 'camOutlineWide',
top: 'camOutlineTop',
ov_topz: 0,
ov_botz: 0,
ov_conv: '~camConventional',
}).inputs = {
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
sep: UC.newBlank({ class: "pop-sep" }),
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: UC.toInt, show: hasSpindle }),
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: UC.toInt, units: true }),
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: UC.toInt, units: true }),
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: UC.toFloat, units: true }),
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: UC.toFloat, bound: UC.bound(0.01, 1.0), show: () => env.popOp.outline.rec.wide }),
steps: UC.newInput(LANG.cc_sovc_s, { title: LANG.cc_sovc_l, convert: UC.toInt, bound: UC.bound(1, 5), show: () => env.popOp.outline.rec.wide }),
sep: UC.newBlank({ class: "pop-sep" }),
top: UC.newBoolean(LANG.co_clrt_s, undefined, { title: LANG.co_clrt_l }),
inside: UC.newBoolean(LANG.co_olin_s, undefined, { title: LANG.co_olin_l, show: (op) => { return !op.inputs.outside.checked } }),
outside: UC.newBoolean(LANG.co_olot_s, undefined, { title: LANG.co_olot_l, show: (op) => { return !op.inputs.inside.checked } }),
sep: UC.newBlank({ class: "pop-sep" }),
omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l, xshow: (op) => { return op.inputs.outside.checked } }),
omitvoid: UC.newBoolean(LANG.co_omvd_s, undefined, { title: LANG.co_omvd_l, xshow: (op) => { return op.inputs.outside.checked } }),
wide: UC.newBoolean(LANG.co_wide_s, undefined, { title: LANG.co_wide_l, show: (op) => { return !op.inputs.inside.checked } }),
dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: (op) => { return !op.inputs.wide.checked } }),
sep: UC.newBlank({ class: "pop-sep" }),
exp: UC.newExpand("overrides"),
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: UC.toFloat, units: true }),
ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: UC.toFloat, units: true }),
ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, { title: LANG.ou_conv_l }),
exp_end: UC.endExpand(),
};
const contourFilter = gcodeEditor('Layer Filter', 'filter');
createPopOp('contour', {
tool: 'camContourTool',
spindle: 'camContourSpindle',
step: 'camContourOver',
rate: 'camContourSpeed',
angle: 'camContourAngle',
leave: 'camContourLeave',
tolerance: 'camTolerance',
flatness: 'camFlatness',
reduction: 'camContourReduce',
bridging: 'camContourBridge',
bottom: 'camContourBottom',
curves: 'camContourCurves',
inside: 'camContourIn',
filter: 'camContourFilter',
axis: 'X'
}).inputs = {
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
axis: UC.newSelect(LANG.cd_axis, {}, "xyaxis"),
sep: UC.newBlank({ class: "pop-sep" }),
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: UC.toInt, show: hasSpindle }),
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: UC.toInt, units: true }),
sep: UC.newBlank({ class: "pop-sep" }),
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: UC.toFloat, bound: UC.bound(0.01, 10.0) }),
leave: UC.newInput(LANG.cf_leav_s, { title: LANG.cf_leav_l, convert: UC.toFloat, bound: UC.bound(0, 100) }),
sep: UC.newBlank({ class: "pop-sep" }),
angle: UC.newInput(LANG.cf_angl_s, { title: LANG.cf_angl_l, convert: UC.toFloat, bound: UC.bound(45, 90), show: (op) => op.inputs.curves.checked }),
flatness: UC.newInput(LANG.ou_flat_s, { title: LANG.ou_flat_l, convert: UC.toFloat, bound: UC.bound(0, 1.0), units: false, round: 4 }),
tolerance: UC.newInput(LANG.ou_toll_s, { title: LANG.ou_toll_l, convert: UC.toFloat, bound: UC.bound(0, 10.0), units: true, round: 4 }),
reduction: UC.newInput(LANG.ou_redu_s, { title: LANG.ou_redu_l, convert: UC.toInt, bound: UC.bound(0, 10), units: false }),
// bridging: UC.newInput(LANG.ou_brdg_s, {title:LANG.ou_brdg_l, convert:UC.toFloat, bound:UC.bound(0,1000.0), units:true, round:4, show:(op) => op.inputs.curves.checked}),
sep: UC.newBlank({ class: "pop-sep" }),
curves: UC.newBoolean(LANG.cf_curv_s, undefined, { title: LANG.cf_curv_l }),
inside: UC.newBoolean(LANG.cf_olin_s, undefined, { title: LANG.cf_olin_l }),
bottom: UC.newBoolean(LANG.cf_botm_s, undefined, { title: LANG.cf_botm_l, show: (op, conf) => conf ? conf.process.camZBottom : 0 }),
filter: UC.newRow([UC.newButton(LANG.filter, contourFilter)], { class: "ext-buttons f-row" })
};
createPopOp('lathe', {
tool: 'camLatheTool',
spindle: 'camLatheSpindle',
step: 'camLatheOver',
angle: 'camLatheAngle',
rate: 'camLatheSpeed',
tolerance: 'camTolerance',
filter: 'camContourFilter',
leave: 'camContourLeave',
linear: 'camLatheLinear'
}).inputs = {
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
// axis: UC.newSelect(LANG.cd_axis, {}, "xyaxis"),
sep: UC.newBlank({ class: "pop-sep" }),
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: UC.toInt, show: hasSpindle }),
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: UC.toInt, units: true }),
sep: UC.newBlank({ class: "pop-sep" }),
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: UC.toFloat, bound: UC.bound(0.01, 100.0) }),
angle: UC.newInput(LANG.cc_sang_s, { title: LANG.cc_sang_l, convert: UC.toFloat, bound: UC.bound(0.01, 180.0) }),
sep: UC.newBlank({ class: "pop-sep" }),
tolerance: UC.newInput(LANG.ou_toll_s, { title: LANG.ou_toll_l, convert: UC.toFloat, bound: UC.bound(0, 10.0), units: true, round: 4 }),
leave: UC.newInput(LANG.cf_leav_s, { title: LANG.cf_leav_l, convert: UC.toFloat, bound: UC.bound(0, 100) }),
sep: UC.newBlank({ class: "pop-sep" }),
linear: UC.newBoolean(LANG.ci_line_s, undefined, { title: LANG.ci_line_l }),
// filter: UC.newRow([ UC.newButton(LANG.filter, contourFilter) ], {class:"ext-buttons f-row"})
};
function canDogBones() {
if (!env.poppedRec) return false;
return env.poppedRec.mode === 'follow';// && env.poppedRec.offset && env.poppedRec.offset !== 'none';
}
function canDogBonesRev() {
return canDogBones() && env.poppedRec.dogbone;
}
function zDogSep() {
return canDogBones() || zBottom();
}
createPopOp('trace', {
mode: 'camTraceType',
offset: 'camTraceOffset',
spindle: 'camTraceSpindle',
tool: 'camTraceTool',
step: 'camTraceOver',
down: 'camTraceDown',
thru: 'camTraceThru',
rate: 'camTraceSpeed',
plunge: 'camTracePlunge',
offover: 'camTraceOffOver',
dogbone: 'camTraceDogbone',
revbone: 'camTraceDogbone',
merge: 'camTraceMerge',
ov_topz: 0,
ov_botz: 0,
ov_conv: '~camConventional',
}).inputs = {
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
mode: UC.newSelect(LANG.cu_type_s, { title: LANG.cu_type_l }, "trace"),
offset: UC.newSelect(LANG.cc_offs_s, { title: LANG.cc_offs_l, show: () => (env.poppedRec.mode === 'follow') }, "traceoff"),
sep: UC.newBlank({ class: "pop-sep" }),
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: UC.toInt, show: hasSpindle }),
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: UC.toInt, units: true }),
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: UC.toInt, units: true }),
sep: UC.newBlank({ class: "pop-sep" }),
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: UC.toFloat, bound: UC.bound(0.01, 1.0), show: (op) => env.popOp.trace.rec.mode === "clear" }),
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: UC.toFloat, units: true }),
thru: UC.newInput(LANG.cc_thru_s, { title: LANG.cc_thru_l, convert: UC.toFloat, units: true }),
offover: UC.newInput(LANG.cc_offd_s, { title: LANG.cc_offd_l, convert: UC.toFloat, units: true, show: () => env.poppedRec.offset !== "none" || env.poppedRec.mode === "clear" }),
sep: UC.newBlank({ class: "pop-sep", modes: MODES.CAM, xshow: zDogSep }),
merge: UC.newBoolean(LANG.co_merg_s, undefined, { title: LANG.co_merg_l, show: () => !env.popOp.trace.rec.down }),
dogbone: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: canDogBones }),
revbone: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: canDogBonesRev }),
exp: UC.newExpand("overrides"),
sep: UC.newBlank({ class: "pop-sep" }),
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: UC.toFloat, units: true }),
ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: UC.toFloat, units: true }),
ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, { title: LANG.ou_conv_l }),
exp_end: UC.endExpand(),
sep: UC.newBlank({ class: "pop-sep" }),
menu: UC.newRow([UC.newButton("select", traceAdd)], { class: "ext-buttons f-row" }),
};
createPopOp('pocket', {
spindle: 'camPocketSpindle',
tool: 'camPocketTool',
step: 'camPocketOver',
down: 'camPocketDown',
rate: 'camPocketSpeed',
plunge: 'camPocketPlunge',
expand: 'camPocketExpand',
smooth: 'camPocketSmooth',
refine: 'camPocketRefine',
follow: 'camPocketFollow',
contour: 'camPocketContour',
engrave: 'camPocketEngrave',
outline: 'camPocketOutline',
ov_topz: 0,
ov_botz: 0,
ov_conv: '~camConventional',
tolerance: 'camTolerance',
}).inputs = {
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
sep: UC.newBlank({ class: "pop-sep" }),
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: UC.toInt, show: hasSpindle }),
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: UC.toInt, units: true }),
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: UC.toInt, units: true }),
sep: UC.newBlank({ class: "pop-sep" }),
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: UC.toFloat, bound: UC.bound(0.01, 1.0) }),
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: UC.toFloat, units: true, show: () => !env.poppedRec.contour }),
sep: UC.newBlank({ class: "pop-sep" }),
expand: UC.newInput(LANG.cp_xpnd_s, { title: LANG.cp_xpnd_l, convert: UC.toFloat, units: true, xshow: () => !env.poppedRec.contour }),
refine: UC.newInput(LANG.cp_refi_s, { title: LANG.cp_refi_l, convert: UC.toInt, show: () => env.poppedRec.contour }),
smooth: UC.newInput(LANG.cp_smoo_s, { title: LANG.cp_smoo_l, convert: UC.toInt }),
tolerance: UC.newInput(LANG.ou_toll_s, { title: LANG.ou_toll_l, convert: UC.toFloat, bound: UC.bound(0, 10.0), units: true, round: 4, show: () => env.poppedRec.contour }),
follow: UC.newInput(LANG.cp_foll_s, { title: LANG.cp_foll_l, convert: UC.toFloat }),
sep: UC.newBlank({ class: "pop-sep" }),
contour: UC.newBoolean(LANG.cp_cont_s, undefined, { title: LANG.cp_cont_s }),
engrave: UC.newBoolean(LANG.cp_engr_s, undefined, { title: LANG.cp_engr_l, show: () => env.poppedRec.contour }),
outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }),
exp: UC.newExpand("overrides"),
sep: UC.newBlank({ class: "pop-sep" }),
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: UC.toFloat, units: true }),
ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: UC.toFloat, units: true }),
ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, { title: LANG.ou_conv_l }),
exp_end: UC.endExpand(),
sep: UC.newBlank({ class: "pop-sep" }),
menu: UC.newRow([UC.newButton("select", surfaceAdd)], { class: "ext-buttons f-row" }),
};
createPopOp('drill', {
tool: 'camDrillTool',
spindle: 'camDrillSpindle',
down: 'camDrillDown',
rate: 'camDrillDownSpeed',
dwell: 'camDrillDwell',
lift: 'camDrillLift',
mark: 'camDrillMark',
precision: 'camDrillPrecision',
thru: 'camDrillThru',
fromTop: 'camDrillFromStockTop',
}).inputs = {
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
sep: UC.newBlank({ class: "pop-sep" }),
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: UC.toInt, show: hasSpindle }),
rate: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: UC.toInt, units: true }),
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: UC.toFloat, units: true }),
dwell: UC.newInput(LANG.cd_dwll_s, { title: LANG.cd_dwll_l, convert: UC.toFloat }),
lift: UC.newInput(LANG.cd_lift_s, { title: LANG.cd_lift_l, convert: UC.toFloat, units: true, show: () => !env.poppedRec.mark }),
mark: UC.newBoolean(LANG.cd_mark_s, undefined, { title: LANG.cd_mark_l, show: () => !env.poppedRec.fromTop }),
fromTop: UC.newBoolean(LANG.cd_ftop_s, undefined, { title: LANG.cd_ftop_l, show: () => !env.poppedRec.mark }),
sep: UC.newBlank({ class: "pop-sep" }),
thru: UC.newInput(LANG.cd_dtru_s, { title: LANG.cd_dtru_l, convert: UC.toFloat, units: true, show: () => !env.poppedRec.mark }),
precision: UC.newInput(LANG.cd_prcn_s, { title: LANG.cd_prcn_l, convert: UC.toFloat, units: true, show: () => !env.poppedRec.mark }),
sep: UC.newBlank({ class: "pop-sep", }),
actions: UC.newRow([
UC.newButton(LANG.select, () => selectHoles(true), { title: LANG.cd_seli_l }),
UC.newButton(LANG.cd_sela_s, () => selectHoles(false), { title: LANG.cd_sela_l })
], { class: "ext-buttons f-col" })
};
createPopOp('register', {
tool: 'camDrillTool',
spindle: 'camDrillSpindle',
down: 'camDrillDown',
rate: 'camDrillDownSpeed',
dwell: 'camDrillDwell',
lift: 'camDrillLift',
feed: 'camRegisterSpeed',
thru: 'camRegisterThru'
}).inputs = {
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
axis: UC.newSelect(LANG.cd_axis, {}, "regaxis"),
points: UC.newSelect(LANG.cd_points, { show: () => env.poppedRec.axis !== '-' }, "regpoints"),
sep: UC.newBlank({ class: "pop-sep" }),
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: UC.toInt, show: hasSpindle }),
rate: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: UC.toInt, units: true }),
feed: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: UC.toInt, units: true, show: () => env.poppedRec.axis === '-' }),
sep: UC.newBlank({ class: "pop-sep" }),
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: UC.toFloat, units: true }),
dwell: UC.newInput(LANG.cd_dwll_s, { title: LANG.cd_dwll_l, convert: UC.toFloat, show: () => env.poppedRec.axis !== '-' }),
lift: UC.newInput(LANG.cd_lift_s, { title: LANG.cd_lift_l, convert: UC.toFloat, units: true, show: () => env.poppedRec.axis !== '-' }),
sep: UC.newBlank({ class: "pop-sep" }),
thru: UC.newInput(LANG.cd_thru_s, { title: LANG.cd_thru_l, convert: UC.toFloat, units: true }),
};
createPopOp('flip', {
axis: 'camFlipAxis',
invert: 'camFlipInvert'
}).inputs = {
axis: UC.newSelect(LANG.cd_axis, {}, "regaxis"),
sep: UC.newBlank({ class: "pop-sep", modes: MODES.CAM, show: zBottom }),
invert: UC.newBoolean(LANG.cf_nvrt_s, undefined, { title: LANG.cf_nvrt_l, show: zBottom }),
sep: UC.newBlank({ class: "pop-sep" }),
action: UC.newRow([UC.newButton(LANG.cf_menu, opFlip)], { class: "ext-buttons f-row" })
};
createPopOp('gcode', {
gcode: 'camCustomGcode',
}).inputs = {
action: UC.newRow([UC.newButton(LANG.edit, gcodeEditor())], { class: "ext-buttons f-row" })
};
function angleTowardZUp() {
api.event.emit('tool.mesh.face-up');
}
createPopOp('index', {
degrees: 'camIndexAxis',
absolute: 'camIndexAbs'
}).inputs = {
degrees: UC.newInput(LANG.ci_degr_s, { title: LANG.ci_degr_l, convert: UC.toFloat, bound: UC.bound(-360, 360.0) }),
absolute: UC.newBoolean(LANG.ci_abso_s, undefined, { title: LANG.ci_abso_l }),
select: UC.newRow([UC.newButton(LANG.ci_face_s, angleTowardZUp)], { class: "ext-buttons f-row" })
};
const editEnable = gcodeEditor('Laser Enable Script', 'enable');
const editOn = gcodeEditor('Laser On Script', 'on');
const editOff = gcodeEditor('Laser Off Script', 'off');
createPopOp('laser on', {
enable: 'camLaserEnable',
on: 'camLaserOn',
off: 'camLaserOff',
power: 'camLaserPower',
adapt: 'camLaserAdaptive',
adaptrp: 'camLaserAdaptMod',
flat: 'camLaserFlatten',
flatz: 'camLaserFlatZ',
minp: 'camLaserPowerMin',
maxp: 'camLaserPowerMax',
minz: 'camLaserZMin',
maxz: 'camLaserZMax',
}).inputs = {
enable: UC.newRow([UC.newButton(LANG.enable, editEnable)], { class: "ext-buttons f-row" }),
on: UC.newRow([UC.newButton(LANG.on, editOn)], { class: "ext-buttons f-row" }),
off: UC.newRow([UC.newButton(LANG.off, editOff)], { class: "ext-buttons f-row" }),
sep: UC.newBlank({ class: "pop-sep" }),
power: UC.newInput(LANG.cl_powr_s, { title: LANG.cl_powr_l, convert: UC.toFloat, bound: UC.bound(0, 1.0), show: () => !env.poppedRec.adapt }),
maxp: UC.newInput(LANG.cl_maxp_s, { title: LANG.cl_maxp_l, convert: UC.toFloat, bound: UC.bound(0, 1.0), show: () => env.poppedRec.adapt }),
minp: UC.newInput(LANG.cl_minp_s, { title: LANG.cl_minp_l, convert: UC.toFloat, bound: UC.bound(0, 1.0), show: () => env.poppedRec.adapt }),
maxz: UC.newInput(LANG.cl_maxz_s, { title: LANG.cl_maxz_l, convert: UC.toFloat, show: () => env.poppedRec.adapt }),
minz: UC.newInput(LANG.cl_minz_s, { title: LANG.cl_minz_l, convert: UC.toFloat, show: () => env.poppedRec.adapt }),
flatz: UC.newInput(LANG.cl_flaz_s, { title: LANG.cl_flaz_l, convert: UC.toFloat, show: () => env.poppedRec.flat }),
sep: UC.newBlank({ class: "pop-sep" }),
adapt: UC.newBoolean(LANG.cl_adap_s, undefined, { title: LANG.cl_adap_l }),
adaptrp: UC.newBoolean(LANG.cl_adrp_s, undefined, { title: LANG.cl_adrp_l, show: () => env.poppedRec.adapt }),
flat: UC.newBoolean(LANG.cl_flat_s, undefined, { title: LANG.cl_flat_l }),
};
const editDisable = gcodeEditor('Laser Disable Script', 'disable');
createPopOp('laser off', {
disable: 'camLaserDisable',
}).inputs = {
disable: UC.newRow([UC.newButton(LANG.disable, editDisable)], { class: "ext-buttons f-row" }),
};
createPopOp('|', {}).inputs = {};
}

View file

@ -0,0 +1,143 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { api } from '../../core/api.js';
import { env, isDark } from './client.js';
import { space as SPACE } from '../../../moto/space.js';
const { ui: UI } = api;
let meshZTop, meshZBottom;
export function updateStock() {
if (env.isAnimate) {
if (env.isIndexed) {
SPACE.world.remove(env.camStock);
env.camStock = undefined;
}
return;
}
if (!env.isCamMode) {
SPACE.world.remove(meshZTop);
SPACE.world.remove(meshZBottom);
SPACE.world.remove(env.camStock);
env.camStock = null;
meshZTop = null;
meshZBottom = null;
return;
}
api.platform.update_bounds();
const settings = api.conf.get();
const widgets = api.widgets.all();
const { stock, process } = settings;
const { x, y, z, center } = stock;
UI.func.animate.classList.add('disabled');
if (env.camStock) {
SPACE.world.remove(env.camStock);
env.camStock = null;
}
if (x && y && z) {
UI.func.animate.classList.remove('disabled');
{
let geo = new THREE.BoxGeometry(1, 1, 1);
let mat = new THREE.MeshBasicMaterial({
color: 0x777777,
opacity: 0.05,
transparent: true,
side: THREE.DoubleSide
});
env.camStock = new THREE.Mesh(geo, mat);
env.camStock.renderOrder = 2;
let lo = 0.5;
let lidat = [
lo, lo, lo, lo, lo, -lo,
lo, lo, lo, lo, -lo, lo,
lo, lo, lo, -lo, lo, lo,
-lo, -lo, -lo, -lo, -lo, lo,
-lo, -lo, -lo, -lo, lo, -lo,
-lo, -lo, -lo, lo, -lo, -lo,
lo, lo, -lo, -lo, lo, -lo,
lo, lo, -lo, lo, -lo, -lo,
lo, -lo, -lo, lo, -lo, lo,
lo, -lo, lo, -lo, -lo, lo,
-lo, -lo, lo, -lo, lo, lo,
-lo, lo, lo, -lo, lo, -lo
];
let ligeo = new THREE.BufferGeometry();
ligeo.setAttribute('position', new THREE.BufferAttribute(lidat.toFloat32(), 3));
let limat = new THREE.LineBasicMaterial({ color: 0xaaaaaa });
let lines = new THREE.LineSegments(ligeo, limat);
env.camStock.lines = lines;
env.camStock.add(lines);
SPACE.world.add(env.camStock);
}
// fight z fighting in threejs
const { scale, position, lines } = env.camStock;
scale.x = x + 0.005;
scale.y = y + 0.005;
scale.z = z + 0.005;
position.x = center.x;
position.y = center.y;
position.z = center.z;
lines.material.color = new THREE.Color(isDark() ? 0x555555 : 0xaaaaaa);
}
SPACE.world.remove(meshZTop);
if (process.camZTop && widgets.length) {
let max = { x, y, z };
for (let w of widgets) {
max.x = Math.max(max.x, w.track.box.w);
max.y = Math.max(max.y, w.track.box.h);
max.z = Math.max(max.z, w.track.box.d);
}
let geo = new THREE.PlaneGeometry(max.x, max.y);
let mat = new THREE.MeshBasicMaterial({
color: 0x777777,
opacity: 0.55,
transparent: true,
side: THREE.DoubleSide
});
meshZTop = new THREE.Mesh(geo, mat);
meshZTop._max = max;
meshZTop.renderOrder = 1;
meshZTop.position.x = center.x;
meshZTop.position.y = center.y;
meshZTop.position.z = process.camZTop;
SPACE.world.add(meshZTop);
} else {
meshZTop = undefined;
}
SPACE.world.remove(meshZBottom);
if (process.camZBottom && widgets.length) {
let max = { x, y, z };
for (let w of widgets) {
max.x = Math.max(max.x, w.track.box.w);
max.y = Math.max(max.y, w.track.box.h);
max.z = Math.max(max.z, w.track.box.d);
}
let geo = new THREE.PlaneGeometry(max.x, max.y);
let mat = new THREE.MeshBasicMaterial({
color: 0x777777,
opacity: 0.55,
transparent: true,
side: THREE.DoubleSide
});
meshZBottom = new THREE.Mesh(geo, mat);
meshZBottom._max = max;
meshZBottom.renderOrder = 1;
meshZBottom.position.x = center.x;
meshZBottom.position.y = center.y;
meshZBottom.position.z = process.camZBottom;
SPACE.world.add(meshZBottom);
} else {
meshZBottom = undefined;
}
SPACE.update();
}

View file

@ -0,0 +1,65 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { api } from '../../core/api.js';
import { env, clearPops } from './client.js';
import { CAM } from './driver-fe.js';
const DEG2RAD = Math.PI / 180;
const RAD2DEG = 180 / Math.PI;
export let surfaceOn = false;
let alert, lastWidget;
export function surfaceAdd(ev) {
if (surfaceOn) {
return surfaceDone();
}
clearPops();
alert = api.show.alert("analyzing surfaces...", 1000);
let surfaces = env.poppedRec.surfaces;
let radians = env.poppedRec.follow * DEG2RAD;
CAM.surface_prep(env.currentIndex * RAD2DEG, () => {
api.hide.alert(alert);
alert = api.show.alert("[esc] cancels surface selection");
for (let [wid, arr] of Object.entries(surfaces)) {
let widget = api.widgets.forid(wid);
if (widget && arr.length)
for (let faceid of arr) {
CAM.surface_toggle(widget, faceid, radians, faceids => {
// surfaces[widget.id] = faceids;
});
}
}
});
surfaceOn = env.hoveredOp;
surfaceOn.classList.add("editing");
api.feature.on_mouse_up = (obj, ev) => {
let { face } = obj;
let min = Math.min(face.a, face.b, face.c);
let faceid = min / 3;
let widget = lastWidget = obj.object.widget;
CAM.surface_toggle(widget, faceid, radians, faceids => {
surfaces[widget.id] = faceids;
});
};
}
export function surfaceDone() {
if (!(surfaceOn && env.poppedRec && env.poppedRec.surfaces)) {
return;
}
let surfaces = env.poppedRec.surfaces;
for (let wid of Object.keys(surfaces)) {
let widget = api.widgets.forid(wid);
if (widget) {
CAM.surface_clear(widget);
} else {
delete surfaces[wid];
}
}
api.hide.alert(alert);
api.feature.on_mouse_up = undefined;
surfaceOn.classList.remove("editing");
surfaceOn = false;
}

224
src/kiri/mode/cam/cl-tab.js Normal file
View file

@ -0,0 +1,224 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { api } from '../../core/api.js';
import { env, isDark } from './client.js';
import { addbox, delbox } from '../../core/boxes.js';
import { traceDone } from './cl-trace.js';
let showTab, lastTab, tab, iw, ic;
export function mirrorTabs(widget) {
let tabs = api.widgets.annotate(widget.id).tab || [];
tabs.forEach(rec => {
let { id, pos, rot } = rec;
let tab = widget.tabs[id];
let e = new THREE.Euler().setFromQuaternion(rot);
e._z = Math.PI - e._z;
let { _x, _y, _z, _w } = rec.rot;
let or = new THREE.Quaternion(_x, _y, _z, _w);
let nr = new THREE.Quaternion().setFromEuler(e);
let ra = or.angleTo(nr);
// console.log({or, nr, ra});
rec.rot = nr;
// let m4 = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(0,0,e._z));
// tab.box.geometry.applyMatrix4(m4);
tab.box.position.x = pos.x = -pos.x;
});
SPACE.update();
}
export function rotateTabs(widget, x, y, z) {
let tabs = api.widgets.annotate(widget.id).tab || [];
tabs.forEach(rec => {
let { id, pos, rot } = rec;
if (!Array.isArray(rot)) {
rot = rot.toArray();
}
let coff = widget.track.center;
let tab = widget.tabs[id];
let m4 = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(x || 0, y || 0, z || 0));
// update position vector
let vc = new THREE.Vector3(pos.x, pos.y, pos.z).applyMatrix4(m4);
// update rotation quaternion
let [rx, ry, rz, rw] = rot;
rec.rot = new THREE.Quaternion().multiplyQuaternions(
new THREE.Quaternion(rx, ry, rz, rw),
new THREE.Quaternion().setFromRotationMatrix(m4)
).toArray();
tab.box.geometry.applyMatrix4(m4);
tab.box.position.x = pos.x = vc.x - coff.dx;
tab.box.position.y = pos.y = vc.y - coff.dy;
tab.box.position.z = pos.z = vc.z;
});
SPACE.update();
}
export function createTabBox(iw, ic, n) {
const { track } = iw;
const { stock, bounds, process } = api.conf.get();
const { camTabsWidth, camTabsHeight, camTabsDepth, camTabsMidline } = process;
const { camZBottom, camStockIndexed } = process;
const sz = stock.z || bounds.max.z;
const zto = sz - iw.track.top;
const zp = (env.camZBottom || camStockIndexed ? env.camZBottom : sz - track.box.d - zto) + (camTabsMidline ? 0 : camTabsHeight / 2);
ic.x += n.x * camTabsDepth / 2; // offset from part
ic.z -= n.y * camTabsDepth / 2; // offset swap z,y
ic.y = zp; // offset swap in world space y,z
const rot = new THREE.Quaternion().setFromAxisAngle(env.zaxis, Math.atan2(n.y, n.x));
const pos = { x: ic.x, y: ic.y, z: ic.z };
const dim = { x: camTabsDepth, y: camTabsWidth, z: camTabsHeight };
const tab = addbox(pos, boxColor(), 'tabb', dim, { rotate: rot, opacity: boxOpacity() });
return { pos, dim, rot, tab, width: camTabsWidth, height: camTabsHeight, stock };
}
export function addWidgetTab(widget, rec) {
const { pos, dim, rot, id } = rec;
const tabs = widget.tabs = (widget.tabs || {});
// prevent duplicate restore from repeated settings load calls
if (!tabs[id]) {
pos.box = addbox(
pos, boxColor(), id,
dim, { group: widget.mesh, rotate: rot, opacity: boxOpacity() }
);
pos.box.tab = Object.assign({ widget, id }, pos);
widget.adds.push(pos.box);
tabs[id] = pos;
}
}
export function restoreTabs(widgets) {
widgets.forEach(widget => {
const tabs = api.widgets.annotate(widget.id).tab || [];
tabs.forEach(rec => {
let [x, y, z, w] = rec.rot;
rec = Object.clone(rec);
rec.rot = new THREE.Quaternion(x, y, z, w);
addWidgetTab(widget, rec);
});
});
}
export function clearTabs(widget, skiprec) {
Object.values(widget.tabs || {}).forEach(rec => {
widget.adds.remove(rec.box);
widget.mesh.remove(rec.box);
});
widget.tabs = {};
if (!skiprec) {
delete api.widgets.annotate(widget.id).tab;
}
}
export function updateTabs() {
// update tab color and opacity
api.widgets.all().forEach(widget => {
Object.values(widget.tabs || {}).forEach(rec => {
for (let rec of widget.adds || []) {
rec.material.color = new THREE.Color(boxColor());
rec.material.opacity = boxOpacity();
}
});
});
}
export function tabHover(data) {
delbox('tabb');
const { int, type, point } = data;
const object = int ? int.object : null;
const tab = int ? object.tab : null;
if (lastTab) {
lastTab.box.material.color.r = 0;
lastTab = null;
}
if (tab) {
tab.box.material.color.r = 0.5;
lastTab = tab;
return;
}
if (type !== 'widget') {
iw = null;
return;
}
let n = int.face.normal;
iw = int.object.widget;
ic = int.point;
// only near vertical faces
// if (Math.abs(n.z) > 0.3) {
// return;
// }
showTab = createTabBox(iw, ic, n);
}
export function tabHoverUp(int) {
delbox('tabb');
if (lastTab) {
const { widget, box, id } = lastTab;
widget.adds.remove(box);
widget.mesh.remove(box);
delete widget.tabs[id];
let ta = api.widgets.annotate(widget.id).tab;
let ix = 0;
ta.forEach((rec, i) => {
if (rec.id === id) {
ix = i;
}
});
ta.splice(ix, 1);
api.conf.save();
widget.saveState();
return;
}
if (!iw) return;
let ip = iw.track.pos;
let wa = api.widgets.annotate(iw.id);
let wt = (wa.tab = wa.tab || []);
let pos = {
x: showTab.pos.x - ip.x,
y: -showTab.pos.z - ip.y,
z: showTab.stock.z ?
showTab.pos.y + ip.z + (env.isIndexed ? 0 : iw.track.tzoff) :
showTab.dim.z / 2,
}
let id = Date.now();
let { dim, rot } = showTab;
let rec = { pos, dim, rot, id };
wt.push(Object.clone(rec));
addWidgetTab(iw, rec);
api.conf.save();
iw.saveState();
}
export function tabAdd() {
traceDone();
alert = api.show.alert("[esc] cancels tab editing");
api.feature.hover = true;
env.hover = tabHover;
env.hoverUp = tabHoverUp;
}
export function tabDone() {
delbox('tabb');
api.hide.alert(alert);
api.feature.hover = false;
if (lastTab) {
lastTab.box.material.color.r = 0;
lastTab = null;
}
}
export function tabClear() {
tabDone();
api.widgets.all().forEach(widget => {
clearTabs(widget);
widget.saveState();
});
api.conf.save();
}
function boxColor() {
return isDark() ? 0x00ddff : 0x0000dd;
}
function boxOpacity() {
return isDark() ? 0.75 : 0.6;
}

View file

@ -0,0 +1,181 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { api } from '../../core/api.js';
import { env, clearPops } from './client.js';
import { CAM } from './driver-fe.js';
import { Layers } from '../../core/layers.js';
import { Stack } from '../../core/stack.js';
export let traceOn = false;
export let lastTrace;
export function setLastTrace(trace) {
lastTrace = trace;
}
export function traceAdd(ev) {
if (traceOn) {
return traceDone();
}
clearPops();
alert = api.show.alert("analyzing parts...", 1000);
traceOn = env.hoveredOp;
traceOn.classList.add("editing");
api.feature.hover = true;
api.feature.hoverAdds = true;
env.hover = traceHover;
env.hoverUp = traceHoverUp;
CAM.traces((ids) => {
api.hide.alert(alert);
alert = api.show.alert("[esc] cancels trace editing");
api.widgets.for(widget => {
if (ids.indexOf(widget.id) >= 0) {
unselectTraces(widget, true);
widget.trace_stack = null;
}
if (widget.trace_stack) {
widget.adds.appendAll(widget.trace_stack.meshes);
widget.trace_stack.show();
return;
}
let areas = (env.poppedRec.areas[widget.id] || []);
let stack = new Stack(widget.mesh);
widget.trace_stack = stack;
widget.traces?.forEach(poly => {
let match = areas.filter(arr => poly.matches(arr));
let layers = new Layers();
layers.setLayer("trace", { line: 0xaaaa55, fat: 4, order: -10 }, false).addPoly(poly);
stack.addLayers(layers);
stack.new_meshes.forEach(mesh => {
mesh.trace = { widget, poly };
// ensure trace poly singleton from matches
if (match.length > 0) {
poly._trace = match[0];
} else {
poly._trace = poly.toArray();
}
});
widget.adds.appendAll(stack.new_meshes);
// console.log(widget)
});
});
// ensure appropriate traces are toggled matching current record
api.widgets.for(widget => {
widget.setVisualState({ opacity: 0.25 });
let areas = (env.poppedRec.areas[widget.id] || []);
let stack = widget.trace_stack;
stack.meshes.forEach(mesh => {
let { poly } = mesh.trace;
let match = areas.filter(arr => poly.matches(arr));
if (match.length > 0) {
if (!mesh.selected) {
traceToggle(mesh, true);
}
} else if (mesh.selected) {
traceToggle(mesh, true);
}
});
});
}, env.poppedRec.select === 'lines');
}
export function traceDone() {
if (!traceOn) {
return;
}
env.func.unpop();
traceOn.classList.remove("editing");
traceOn = false;
api.widgets.opacity(1);
api.hide.alert(alert);
api.feature.hover = false;
api.feature.hoverAdds = false;
api.widgets.for(widget => {
widget.restoreVisualState();
if (widget.trace_stack) {
widget.trace_stack.hide();
widget.adds.removeAll(widget.trace_stack.meshes);
}
});
}
export function traceHover(data) {
if (lastTrace) {
let { color, colorSave } = lastTrace.material[0] || lastTrace.material;
color.r = colorSave.r;
color.g = colorSave.g;
color.b = colorSave.b;
}
lastTrace = null;
if (data.type === 'platform') {
return;
}
if (!data.int.object.trace) {
return;
}
lastTrace = data.int.object;
if (lastTrace.selected) {
let event = data.event;
let target = event.target;
let { clientX, clientY } = event;
let { offsetWidth, offsetHeight } = target;
}
let material = lastTrace.material[0] || lastTrace.material;
let color = material.color;
material.colorSave = color.clone();
color.setHex(env.isDark() ? 0x0066ff : 0x0000ff);
}
export function traceHoverUp(int, ev) {
if (!int) return;
let { object } = int;
traceToggle(object);
if (ev.metaKey || ev.ctrlKey) {
let { selected } = object;
let { widget, poly } = object.trace;
let avgZ = poly.avgZ();
for (let add of widget.adds) {
if (add.trace && add.selected !== selected && add.trace.poly.onZ(avgZ)) {
traceToggle(add);
}
}
}
}
export function traceToggle(obj, skip) {
let material = obj.material[0] || obj.material;
if (!material) return;
let { color, colorSave } = material;
let { widget, poly } = obj.trace;
let areas = env.poppedRec.areas;
if (!areas) {
return;
}
let wlist = areas[widget.id] = areas[widget.id] || [];
obj.selected = !obj.selected;
if (!colorSave) {
colorSave = material.colorSave = color.clone();
}
if (obj.selected) {
color.setHex(env.isDark() ? 0xdd0011 : 0xff0033);
colorSave.r = color.r;
colorSave.g = color.g;
colorSave.b = color.b;
if (!skip) wlist.push(poly._trace);
} else {
color.setHex(0xaaaa55);
colorSave.setHex(0xaaaa55);
if (!skip) wlist.remove(poly._trace);
}
api.conf.save();
}
export function unselectTraces(widget, skip) {
if (widget.trace_stack) {
widget.trace_stack.meshes.forEach(mesh => {
if (mesh.selected) {
traceToggle(mesh, skip);
}
});
}
}

890
src/kiri/mode/cam/client.js Normal file
View file

@ -0,0 +1,890 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { $, h } from '../../../moto/webui.js';
import { api } from '../../core/api.js';
import { load } from '../../../load/file.js';
import { animate as anim_2d, animate_clear as anim_2d_clear } from './anim-2d-fe.js';
import { animate2 as anim_3d, animate_clear2 as anim_3d_clear } from './anim-3d-fe.js';
import { space as SPACE } from '../../../moto/space.js';
import { Layers } from '../../core/layers.js';
import { Stack } from '../../core/stack.js';
import { updateStock } from './cl-stock.js';
import { createPopOps } from './cl-ops.js';
import { tabAdd, tabDone, tabClear, restoreTabs, updateTabs } from './cl-tab.js';
import { traceOn, traceDone, unselectTraces } from './cl-trace.js';
import { holeSelOn, selectHolesDone, clearHolesRec } from './cl-hole.js';
import { surfaceOn, surfaceDone } from './cl-surface.js';
const DEG2RAD = Math.PI / 180;
const RAD2DEG = 180 / Math.PI;
const hasSharedArrays = self.SharedArrayBuffer ? true : false;
const { VIEWS, STACKS } = api.const;
const { noop, space } = api;
const { ui: UI, uc: UC } = api;
const { widgets: WIDGETS } = api;
class Client {
animVer = 0;
camStock;
current;
currentIndex;
flipping;
func = {};
hoveredOp;
hover = noop;
hoverUp = noop;
isAnimate;
isArrange;
isPreview;
isCamMode;
isIndexed;
isParsed;
lastMode;
popOp = {};
poppedRec;
zaxis = { x: 0, y: 0, z: 1 };
}
export const env = new Client();
export function isDark() {
return api.space.is_dark()
}
export function clearPops() {
if (env.func.unpop) env.func.unpop();
tabDone();
traceDone();
surfaceDone();
selectHolesDone();
}
function animFn() {
return [{
animate: anim_2d,
animate_clear: anim_2d_clear
}, {
animate: anim_3d,
animate_clear: anim_3d_clear
}][env.animVer];
}
function updateIndex() {
let oplist = env.current.process.ops;
if (!(env.isCamMode && oplist) || env.lastMode === VIEWS.ANIMATE) {
return;
}
let index = 0;
for (let op of oplist) {
if (op.type === '|') {
break;
}
if (op.type === 'index' && !op.disabled) {
if (op.absolute) {
index = op.degrees
} else {
index += op.degrees;
}
}
}
WIDGETS.setAxisIndex(env.isPreview || !env.isIndexed ? 0 : -index);
env.currentIndex = env.isIndexed && !env.isPreview ? index * DEG2RAD : 0;
}
function updateAxisMode(refresh) {
const { camStockIndexGrid, camStockIndexed } = env.current.process;
let newIndexed = camStockIndexed;
let changed = refresh || env.isIndexed !== newIndexed;
env.isIndexed = newIndexed;
if (!env.isIndexed || !env.isCamMode) {
WIDGETS.setAxisIndex(0);
}
if (!env.isCamMode) {
return;
}
if (env.isIndexed) {
env.current.process.camZAnchor = "middle";
}
env.animVer = env.isIndexed ? 1 : 0;
SPACE.platform.setVisible(!env.isIndexed);
SPACE.platform.showGrid2(!env.isIndexed || camStockIndexGrid);
const showIndexed = env.isIndexed ? '' : 'none';
const showNonIndexed = env.isIndexed ? 'none' : '';
$('cam-index').style.display = showIndexed;
$('cam-lathe').style.display = showIndexed;
$('cam-flip').style.display = showNonIndexed;
$('cam-reg').style.display = showNonIndexed;
if (!changed) {
return;
}
WIDGETS.setIndexed(env.isIndexed ? true : false);
api.platform.update_bounds();
// add or remove clock op depending on indexing
const cp = env.current.process;
if (!cp.ops) {
return;
}
const clockOp = cp.ops.filter(op => op.type === '|')[0];
if (!clockOp) {
opAdd(env.popOp['|'].new());
} else {
opRender();
}
updateStock();
}
const opAddLaserOn = () => {
opAdd(env.popOp['laser on'].new());
};
const opAddLaserOff = () => {
opAdd(env.popOp['laser off'].new());
};
const opAddGCode = () => {
opAdd(env.popOp.gcode.new());
};
const opAddIndex = () => {
opAdd(env.popOp.index.new());
};
const opAddLevel = () => {
opAdd(env.popOp.level.new());
};
const opAddRough = () => {
opAdd(env.popOp.rough.new());
};
const opAddOutline = () => {
opAdd(env.popOp.outline.new());
};
const opAddPocket = () => {
traceDone();
surfaceDone();
let rec = env.popOp.pocket.new();
rec.surfaces = { /* widget.id: [ faces... ] */ };
opAdd(rec);
};
const opAddContour = (axis) => {
let rec = env.popOp.contour.new();
rec.axis = axis.toUpperCase();
opAdd(rec);
};
const opAddLathe = (axis) => {
let rec = env.popOp.lathe.new();
rec.axis = axis.toUpperCase();
opAdd(rec);
};
const opAddTrace = () => {
let rec = env.popOp.trace.new();
rec.areas = { /* widget.id: [ polygons... ] */ };
opAdd(rec);
};
const opAddDrill = () => {
let rec = env.popOp.drill.new();
rec.drills = {};
opAdd(rec);
};
const opAddRegister = (axis, points) => {
let rec = env.popOp.register.new();
rec.axis = axis.toUpperCase();
rec.points = points;
opAdd(rec);
};
const opAddFlip = () => {
opAdd(env.popOp.flip.new());
};
const traceClear = () => {
traceDone();
api.widgets.all().forEach(widget => {
unselectTraces(widget);
});
api.conf.save();
};
const opAdd = (rec) => {
if (!env.isCamMode) return;
clearPops();
let oplist = env.current.process.ops;
if (oplist.indexOf(rec) < 0) {
if (oplist.length && oplist[oplist.length - 1].type === '|') {
oplist.splice(oplist.length - 1, 0, rec);
} else {
oplist.push(rec);
}
let fpos = oplist.findWith(rec => rec.type === 'flip');
if (fpos >= 0 && oplist.length > 1) {
let oprec = oplist.splice(fpos, 1);
oplist.push(oprec[0]);
}
api.conf.save();
opRender();
}
};
const opDel = (rec) => {
if (!env.isCamMode) return;
clearPops();
let oplist = env.current.process.ops;
let pos = oplist.indexOf(rec);
if (pos >= 0) {
oplist.splice(pos, 1);
api.conf.save();
opRender();
}
};
export function opRender() {
let oplist = env.current?.process?.ops;
if (!(env.isCamMode && oplist)) {
return;
}
oplist = oplist.filter(rec => !Array.isArray(rec));
let mark = Date.now();
let html = [];
let bind = {};
let scale = api.view.unit_scale();
let notime = false;
oplist.forEach((rec, i) => {
let title = '';
let clock = rec.type === '|';
let label = clock ? `` : rec.type;
let clazz = notime ? ["draggable", "notime"] : ["draggable"];
let notable = rec.note ? rec.note.split(' ').filter(v => v.charAt(0) === '#') : undefined;
if (clock) { clazz.push('clock'); title = ` title="end of ops chain\ndrag/drop like an op\nops after this are disabled"` }
if (notable?.length) label += ` (${notable[0].slice(1)})`;
html.appendAll([
`<div id="${mark + i}" class="${clazz.join(' ')}"${title}>`,
`<label class="label">${label}</label>`,
clock ? '' :
`<label id="${mark + i}-x" class="del"><i class="fa fa-trash"></i></label>`,
`</div>`
]);
bind[mark + i] = rec;
notime = notime || clock;
});
let listel = $('oplist');
listel.innerHTML = html.join('');
let bounds = [];
let unpop = null;
let index = 0;
let indexing = true;
// drag and drop re-ordering
for (let [id, rec] of Object.entries(bind)) {
let type = rec.type;
let clock = type === '|';
if (!clock) {
$(`${id}-x`).onmousedown = (ev) => {
ev.stopPropagation();
ev.preventDefault();
surfaceDone();
traceDone();
tabDone();
opDel(rec);
};
} else {
indexing = false;
}
let el = $(id);
if (!env.isIndexed && type === 'lathe') {
rec.disabled = true;
}
if (!hasSharedArrays && (type === 'contour' || type === 'lathe')) {
rec.disabled = true;
}
if (rec.disabled) {
el.classList.add("disabled");
}
bounds.push(el);
let timer = null;
let inside = true;
let popped = false;
let poprec = env.popOp[rec.type];
if (type === 'index' && indexing && !rec.disabled) {
index = rec.absolute ? rec.degrees : index + rec.degrees;
}
el.rec = rec;
el.unpop = () => {
let pos = [...el.childNodes].indexOf(poprec.div);
if (pos >= 0) {
el.removeChild(poprec.div);
}
popped = false;
};
function onEnter(ev) {
if ((surfaceOn || traceOn) && env.poppedRec != rec) {
return;
}
if (popped && env.poppedRec != rec) {
surfaceDone();
traceDone();
}
if (unpop) unpop();
env.func.unpop = unpop = el.unpop;
inside = true;
// pointer to current rec for trace editing
env.poppedRec = rec;
popped = true;
poprec.use(rec);
env.hoveredOp = el;
if (!clock) {
// offset Y position of pop div by % of Y screen location of button
el.appendChild(poprec.div);
poprec.addNote();
const { innerHeight } = window;
const brect = ev.target.getBoundingClientRect();
const prect = poprec.div.getBoundingClientRect();
const pcty = (brect.top / innerHeight) * 0.9;
const offpx = -pcty * prect.height;
poprec.div.style.transform = `translateY(${offpx}px)`;
}
// option click event appears latent
// and overides the sticky settings
setTimeout(() => {
UC.setSticky(false);
}, 0);
}
function onLeave(ev) {
inside = false;
clearTimeout(timer);
timer = setTimeout(() => {
if (!inside && poprec.using(rec) && !UC.isSticky()) {
el.unpop();
}
}, 250);
}
function onDown(ev) {
if (!ev.target.rec) {
// only trigger on operation buttons bound to recs
return;
}
let mobile = ev.touches;
surfaceDone();
traceDone();
let target = ev.target, clist = target.classList;
if (!clist.contains("draggable")) {
return;
}
// toggle enable / disable
if (!clock && (ev.ctrlKey || ev.metaKey)) {
ev.preventDefault();
ev.stopPropagation();
rec.disabled = !rec.disabled;
for (let op of ev.shiftKey ? oplist : [rec]) {
if (op !== rec) {
op.disabled = op.type !== '|' ? !rec.disabled : false;
}
}
for (let el of bounds) {
if (el.rec.disabled) {
el.classList.add("disabled");
} else {
el.classList.remove("disabled");
}
}
if (env.isIndexed) {
updateIndex();
}
return true;
}
// duplicate op
if (!clock && ev.shiftKey) {
ev.preventDefault();
ev.stopPropagation();
oplist = env.current.process.ops;
oplist.push(Object.clone(rec));
api.conf.save();
opRender();
return true;
}
clist.add("drag");
ev.stopPropagation();
ev.preventDefault();
let tracker = UI.tracker;
tracker.style.display = 'block';
let cancel = tracker.onmouseup = (ev) => {
oplist = env.current.process.ops;
clist.remove("drag");
tracker.style.display = 'none';
if (ev) {
ev.stopPropagation();
ev.preventDefault();
}
oplist.length = 0;
for (let child of listel.childNodes) {
oplist.push(child.rec);
}
api.conf.save();
opRender();
if (mobile) {
el.ontouchmove = onDown;
el.ontouchend = undefined;
}
};
function onMove(ev) {
ev.stopPropagation();
ev.preventDefault();
if (ev.buttons === 0) {
return cancel();
}
for (let el of bounds) {
if (el === target) continue;
let rect = el.getBoundingClientRect();
let top = rect.top;
let bottom = rect.bottom;// + rect.height;
let tar = mobile ? ev.touches[0] : ev;
if (tar.pageY >= top && tar.pageY <= bottom) {
let mid = (top + bottom) / 2;
try { listel.removeChild(target); } catch (e) { }
el.insertAdjacentElement(tar.pageY < mid ? "beforebegin" : "afterend", target);
}
}
}
tracker.onmousemove = onMove;
if (mobile) {
el.ontouchmove = onMove;
el.ontouchend = cancel;
}
}
if (SPACE.info.mob) {
let touched = false;
el.ontouchstart = (ev) => {
touched = true;
if (env.poppedRec === rec && popped) {
onLeave(ev);
} else {
onEnter(ev);
}
};
el.ontouchmove = onDown;
el.onmouseenter = (ev) => {
if (touched) {
// touches block mouse events on touchscreen PCs
// which are often sent along with touch events
// but when not touched, allow the mouse to work
return;
}
el.onmousedown = onDown;
el.onmouseleave = onLeave;
onEnter(ev);
};
} else {
el.onmousedown = onDown;
el.onmouseenter = onEnter;
el.onmouseleave = onLeave;
}
}
if (env.lastMode !== VIEWS.ANIMATE) {
// update widget rotations from timeline marker
WIDGETS.setAxisIndex(env.isPreview || !env.isIndexed ? 0 : -index);
}
env.currentIndex = env.isIndexed && !env.isPreview ? index * DEG2RAD : 0;
updateStock();
}
export function init() {
api.event.on('tool.mesh.face-normal', normal => {
// console.log({ env.poppedRec });
env.poppedRec.degrees = (Math.atan2(normal.y, normal.z) * RAD2DEG).round(2);
env.poppedRec.absolute = true;
opRender();
updateStock();
});
api.event.on("cam.trace.clear", traceClear);
api.event.on("cam.parse.gerber", opts => {
const { data, mesh } = opts;
const { open, closed, circs, rects } = load.GBR.parse(data);
const stack = new Stack(mesh || SPACE.world.newGroup());
const layers = new Layers();
for (let poly of open) {
layers.setLayer("open", { line: 0xff8800 }, false).addPoly(poly);
let diam = poly.tool?.shape?.diameter;
if (diam) {
const exp = poly.offset_open(diam, 'round');
layers.setLayer("open-exp", { line: 0xff5555 }, false).addPolys(exp);
}
}
for (let poly of closed) {
layers.setLayer("close", { line: 0xff0000 }, false).addPoly(poly);
}
for (let poly of circs) {
layers.setLayer("circs", { line: 0x008800 }, false).addPoly(poly);
}
for (let poly of rects) {
layers.setLayer("rects", { line: 0x0000ff }, false).addPoly(poly);
}
stack.addLayers(layers);
});
api.event.on("widget.add", widget => {
if (env.isCamMode && !Array.isArray(widget)) {
updateAxisMode(true);
widget.setIndexed(env.isIndexed ? true : false);
api.platform.update_bounds();
}
});
// wire up animate button in ui
api.event.on("function.animate", (mode) => {
if (env.isAnimate || !env.isCamMode) {
return;
}
api.function.prepare(() => {
if (env.isCamMode) {
animate();
}
});
});
api.event.on("function.export", (mode) => {
if (env.isAnimate) {
env.isAnimate = false;
animFn().animate_clear(api);
}
});
api.event.on("mode.set", (mode) => {
env.isIndexed = undefined;
env.isCamMode = mode === 'CAM';
SPACE.platform.setColor(env.isCamMode ? 0xeeeeee : 0xcccccc);
api.uc.setVisible(UI.func.animate, env.isCamMode);
// hide/show cam mode elements
for (let el of [...document.getElementsByClassName('mode-cam')]) {
api.uc.setClass(el, 'hide', !env.isCamMode);
}
if (!env.isCamMode) {
clearPops();
tabClear();
}
// do not persist traces across page reloads
traceClear();
opRender();
updateStock();
});
api.event.on("view.set", (mode) => {
env.lastMode = mode;
env.isArrange = (mode === VIEWS.ARRANGE);
env.isPreview = (mode === VIEWS.PREVIEW);
env.isAnimate = (mode === VIEWS.ANIMATE);
animFn().animate_clear(api);
clearPops();
if (env.isCamMode && env.isPreview) {
WIDGETS.setAxisIndex(0);
}
updateStock();
opRender();
api.uc.setVisible($('layer-animate'), env.isAnimate && env.isCamMode);
});
api.event.on("settings.saved", (settings) => {
validateTools(settings.tools);
env.current = settings;
let proc = settings.process;
let hasTabs = false;
let hasTraces = false;
if (env.isCamMode && proc.ops) {
proc.ops = proc.ops.filter(v => v);
}
// for any tabs or traces to set markers
for (let widget of api.widgets.all()) {
let wannot = widget.anno;
if (wannot.tab && wannot.tab.length) hasTabs = true;
if (wannot.trace && wannot.trace.length) hasTraces = true;
}
api.platform.update_bounds();
updateIndex();
updateStock();
updateAxisMode();
if (!env.poppedRec) {
opRender();
}
});
api.event.on("settings.load", (settings) => {
opRender();
if (!env.isCamMode) return;
validateTools(settings.tools);
restoreTabs(api.widgets.all());
updateAxisMode();
});
api.event.on("cam.stock.toggle", (bool) => {
env.camStock && (env.camStock.visible = bool ?? !env.camStock.visible);
});
api.event.on("boolean.click", api.platform.update_bounds);
api.event.on([
"init-done",
"view.set",
// update stock when modes change?
"slice.end",
"preview.end",
// update stock when preferences change
// "boolean.click",
"boolean.update",
// update stock when objects move
"platform.layout",
"selection.drag",
"selection.move",
// force bounds update, too
"selection.scale",
"selection.rotate"
], updateStock);
// invalidate trace and drill ops on scale or rotate
api.event.on([
"selection.scale",
"selection.rotate"
], () => {
if (!env.isCamMode) return;
for (let op of env.current.process.ops) {
if (op.type === 'trace' && !env.flipping) {
op.areas = {};
}
else if (op.type === 'drill' && !env.flipping) {
op.drills = {};
}
}
});
// invalidate tabs when scaleds
api.event.on([
"selection.scale",
], () => {
tabClear();
});
api.event.on([
// update tab color/opacity on dark/light change
"boolean.update"
], updateTabs);
api.event.on("preview.end", () => {
env.isParsed = false;
if (env.isCamMode) {
let bounds = STACKS.getStack("bounds");
if (bounds) bounds.button("animate", animate);
}
});
api.event.on("code.loaded", (info) => {
if (env.isCamMode) {
env.isParsed = true;
let parse = STACKS.getStack("parse", SPACE.world);
if (parse) parse.button("animate", animate);
}
});
$('op-add').onmouseenter = () => {
if (env.func.unpop) env.func.unpop();
};
$('op-add-list').onclick = (ev) => {
let settings = api.conf.get();
let { process, device } = settings;
switch (ev.target.innerText.toLowerCase()) {
case "index": return opAddIndex();
case "laser on": return opAddLaserOn();
case "laser off": return opAddLaserOff();
case "gcode": return opAddGCode();
case "level": return opAddLevel();
case "rough": return opAddRough();
case "outline": return opAddOutline();
case "contour":
let caxis = "X";
for (let op of env.current.process.ops) {
if (op.type === "contour" && op.axis === "X") {
caxis = "Y";
}
}
return opAddContour(caxis);
case "lathe":
let laxis = "X";
for (let op of env.current.process.ops) {
if (op.type === "lathe" && op.axis === "X") {
laxis = "Y";
}
}
return opAddLathe(laxis);
case "register": return opAddRegister('X', 2);
case "drill": return opAddDrill();
case "trace": return opAddTrace();
case "pocket": return opAddPocket();
case "flip":
// only one flip op permitted
for (let op of env.current.process.ops) {
if (op.type === 'flip') {
return;
}
}
return opAddFlip();
}
};
// TAB/TRACE BUTTON HANDLERS
api.event.on("button.click", target => {
let process = api.conf.get().process;
switch (target) {
case api.ui.tabAdd:
return tabAdd();
case api.ui.tabDun:
return tabDone();
case api.ui.tabClr:
api.uc.confirm("clear tabs?").then(ok => {
if (ok) tabClear();
});
break;
}
});
api.event.on("cam.op.add", opAdd);
api.event.on("cam.op.del", opDel);
api.event.on("cam.op.render", opRender);
api.event.on("cam.tabs.add", tabAdd);
api.event.on("cam.tabs.done", tabDone);
api.event.on("cam.tabs.clear", tabClear);
// COMMON TAB/TRACE EVENT HANDLERS
api.event.on("slice.begin", () => {
if (env.isCamMode) {
clearPops();
}
});
api.event.on("key.esc", () => {
if (env.isCamMode) {
clearPops();
}
});
api.event.on("selection.scale", () => {
if (env.isCamMode) {
clearPops();
}
});
api.event.on("widget.duplicate", (widget, oldwidget) => {
if (!env.isCamMode) {
return;
}
if (traceOn) {
traceDone();
}
unselectTraces(widget);
if (env.flipping) {
return;
}
let ann = api.widgets.annotate(widget.id);
if (ann.tab) {
ann.tab.forEach((tab, i) => {
tab.id = Date.now() + i;
});
restoreTabs([widget]);
}
});
api.event.on("widget.mirror", widget => {
if (!env.isCamMode) {
return;
}
if (traceOn) {
traceDone();
}
if (holeSelOn) {
selectHolesDone();
}
clearHolesRec(widget)
unselectTraces(widget);
if (env.flipping) {
return;
}
mirrorTabs(widget);
});
api.event.on("widget.rotate", rot => {
if (!env.isCamMode) {
return;
}
let { widget, x, y, z } = rot;
if (traceOn) {
traceDone();
}
unselectTraces(widget);
if (holeSelOn) {
selectHolesDone();
}
if (env.flipping) {
return;
}
clearHolesRec(widget)
if (x || y) {
clearTabs(widget);
} else {
rotateTabs(widget, x, y, z);
}
});
api.event.on("mouse.hover.up", rec => {
if (!env.isCamMode) {
return;
}
let { object, event } = rec;
env.hoverUp(object, event);
});
api.event.on("mouse.hover", data => {
if (!env.isCamMode) {
return;
}
env.hover(data);
});
createPopOps();
}
function validateTools(tools) {
if (tools) {
let max = 0;
for (let t of tools) {
if (Number.isInteger(t.number)) {
max = Math.max(max, t.number);
}
}
for (let t of tools) {
if (!Number.isInteger(t.number)) {
t.number = ++max;
console.log('added tool #', t);
}
}
}
}
function animate() {
env.isAnimate = true;
api.widgets.opacity(env.isParsed ? 0 : 0.75);
api.hide.slider();
STACKS.clear();
animFn().animate(api);
api.view.set_animate();
}

View file

@ -0,0 +1,109 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { codec } from '../../core/codec.js';
import { 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 { init as init_2d } from './anim-2d-be.js';
import { init as init_3d } from './anim-3d-be.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) {
surface_prep(widget);
return widget.tool.findConnectedSurface(faces, radians || 0, 0.0);
};
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
} } )));
}
}
export const CAM = {
init,
surface_prep,
surface_find,
slice: cam_slice,
prepare: cam_prepare,
export: cam_export
};

View file

@ -0,0 +1,89 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { api } from '../../core/api.js';
import { codec } from '../../core/codec.js';
function surface_prep(index, ondone) {
api.client.sync();
const settings = api.conf.get();
api.client.send("cam_surfaces", { settings, index }, output => {
ondone(output);
});
};
function surface_show(widget) {
widget.selectFaces(Object.values(widget._surfaces).flat());
};
function surface_toggle(widget, face, radians, ondone) {
let surfaces = widget._surfaces = widget._surfaces || {};
for (let [root, faces] of Object.entries(surfaces)) {
if (faces.contains(face)) {
// delete this face group
delete surfaces[root];
surface_show(widget);
ondone(Object.keys(surfaces).map(v => parseInt(v)));
return;
}
}
api.client.send("cam_surface_find", { id: widget.id, face, radians }, faces => {
if (faces.length) {
surfaces[face] = faces;
surface_show(widget);
}
ondone(Object.keys(surfaces).map(v => parseInt(v)));
});
};
function surface_clear(widget) {
widget.selectFaces([]);
widget._surfaces = {};
};
function traces(ondone, single) {
api.client.sync();
const settings = api.conf.get();
const widgets = api.widgets.map();
api.client.send("cam_traces", { settings, single }, output => {
const ids = [];
codec.decode(output).forEach(rec => {
ids.push(rec.id);
widgets[rec.id].traces = rec.traces;
});
ondone(ids);
});
};
function traces_clear(ondone) {
api.client.send("cam_traces_clear", {}, () => {
// console.log({clear_traces: true});
});
};
function holes(indiv, rec, onProgress, onDone) {
api.client.sync();
const settings = api.conf.get();
return new Promise((res,rej)=>{
api.client.send("cam_holes", { settings, rec, indiv }, output => {
let out = codec.decode(output);
if (out.progress != undefined) {
//if a progress message,
onProgress(out.progress,out.msg)
} else {
api.hide.alert(alert);
onDone(out);
res(out);
}
});
})
};
export const CAM = {
surface_prep,
surface_show,
surface_toggle,
surface_clear,
traces,
traces_clear,
holes
};

519
src/kiri/mode/cam/export.js Normal file
View file

@ -0,0 +1,519 @@
/** 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;
console.log({ cam_export: print });
const { settings } = print;
const { device, tools } = 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 || {},
ztOff = stock.z - widget.track.top,
bounds = widget.getBoundingBox(),
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: 1,
}, {
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: 1
}, {
dx: 0, dy: 0, dz: 1, time: 0
});
}
points--;
return;
}
let gn;
if (out.emit >= 0 && out.emit <= 3) gn = `G${out.emit}`;
let speed = out.speed,
arc = out.emit == 2 || out.emit == 3,
center = out.center,
nl = (compact_output && lastGn === gn) ? [] : [gn],
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,
maxf = dz ? maxZd : maxXYd,
feed = Math.min(speed || maxf, maxf),
dist = Math.sqrt(dx * dx + dy * dy + dz * dz),
newFeed = feed && feed !== pos.f;
// drop dup points (all deltas are 0)
if (!arc && !(dx || dy || dz || da)) {
return;
}
lastGn = gn;
if (dx || newpos.x !== pos.x) {
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) {
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) {
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);
// remap points as necessary for origins, offsets, inversions
print.output.forEach(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;
point.y += offset.y;
}
if (scale) {
point.x *= scale.x;
point.y *= scale.y;
point.z *= scale.z;
}
if (point.a && scale.a) {
point.a *= scale.a;
}
if (spro.outputInvertX) point.x = -point.x;
if (spro.outputInvertY) point.y = -point.y;
if (spro.camOriginTop) point.z = point.z - zmax;
});
});
if (!stripComments) {
// emit tools used in comments
append("; --- tools ---");
Object.keys(toolz).sort().forEach(tn => {
let { number, flute_diam, flute_len, metric } = toolz[tn].tool;
append(`; tool#=${number} flute=${flute_diam} len=${flute_len} unit=${metric ? 'metric' : 'imperial'}`);
});
}
// emit gcode preamble
filterEmit(gcodePre, consts);
// emit all points in layer/point order
for (let layerout of print.output) {
const newmode = layerout.mode;
if (newmode) {
if (newmode.type === 'laser on') {
laserOp = newmode;
} else if (newmode.type === 'laser off') {
laserOp = undefined;
// force tool change in case laser tool matches next tool
pos.t = undefined;
}
}
const firstOut = layerout[0];
const newOp = newmode && !newmode.silent && mode !== newmode;
if (newOp) {
if (mode && !stripComments) {
append("; ending " + mode.type + " op after " + Math.round(time) + " seconds");
}
mode = newmode;
if (mode) {
section(`op-${opnum++}-${mode.type}`);
}
if (!stripComments && mode) {
append("; starting " + mode.type + " op");
}
}
newSpindle = layerout.spindle;
// iterate over layer output records
layerout.forEach((out, ind) => {
if (out.type === 'lerp') {
// suppress display only lerp points
return;
}
if (out.gcode && Array.isArray(out.gcode)) {
filterEmit(out.gcode, consts);
} else {
moveTo(out, { newOp: ind === 0 ? newOp : false });
}
});
if (lasering && laserOp) {
// disable laser
filterEmit(laserOp.off, consts);
lasering = false;
}
}
if (mode && !stripComments) {
append("; ending " + mode.type + " op after " + Math.round(time) + " seconds");
}
// emit gcode post
section('footer');
filterEmit(device.gcodePost, consts);
// flush buffered gcode
append();
print.time = time;
print.lines = lines;
print.bytes = bytes + lines - 1;
print.bounds = runbox;
};

View file

@ -0,0 +1,127 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js';
import { Tool } from './tool.js';
import { generate as Topo } from './topo3.js';
import { newPoint } from '../../../geo/point.js';
import { tip2tipEmit } from '../../../geo/paths.js';
function createFilter(op) {
let filter = slices => slices;
let filterString = op.filter?.map(l => l.trim()).join('\n');
if (filterString) {
try {
const obj = eval(`( ${filterString} )`);
let idx = 0;
if (obj && obj.slices) {
const nadd = [];
filter = function(slices) {
for (let slice of slices) {
if (obj.slices(slice, idx++)) {
nadd.push(slice);
}
}
return nadd;
};
}
} 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 { addSlices } = state;
let filter = createFilter(op);
// 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 = await Topo({
// onupdate: (update, msg) => {
onupdate: (index, total, msg) => {
progress(index / total, msg);
},
ondone: (slices) => {
slices = filter(slices);
this.sliceOut = slices;
addSlices(slices);
},
contour: op,
state: state
});
// computed if set to 0
this.tolerance = topo.tolerance;
}
prepare(ops, progress) {
let { op, state, sliceOut } = this;
let { settings, widget } = state;
let { process } = settings;
let { setTolerance, setTool, setSpindle, setPrintPoint } = ops;
let { camOut, polyEmit, newLayer, printPoint, lastPoint } = ops;
let { bounds, zmax } = ops;
let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter();
let stepover = toolDiam * op.step * 2;
let depthFirst = process.camDepthFirst;
let depthData = [];
setTool(op.tool, op.rate, process.camFastFeedZ);
setSpindle(op.spindle);
setTolerance(this.tolerance);
printPoint = newPoint(bounds.min.x,bounds.min.y,zmax);
for (let slice of sliceOut) {
// ignore debug slices
if (!slice.camLines) {
continue;
}
let polys = [], poly, emit;
slice.camLines.forEach(function (poly) {
if (depthFirst) poly = poly.clone(true);
polys.push({first:poly.first(), last:poly.last(), poly:poly});
});
if (depthFirst) {
depthData.appendAll(polys);
} else {
printPoint = tip2tipEmit(polys, printPoint, function(el, point, count) {
poly = el.poly;
if (poly.last() === point) {
poly.reverse();
}
poly.forEachPoint(function(point, pidx) {
camOut(point.clone(), pidx > 0, stepover);
}, false);
});
newLayer();
}
}
if (depthFirst) {
printPoint = tip2tipEmit(depthData, printPoint, function(el, point, count) {
let poly = el.poly;
if (poly.last() === point) {
poly.reverse();
}
poly.forEachPoint(function(point, pidx) {
camOut(point.clone(), pidx > 0, stepover);
}, false);
newLayer();
return lastPoint();
});
}
setPrintPoint(printPoint);
}
}
export { OpContour };

View file

@ -0,0 +1,68 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js';
import { Tool } from './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 { zBottom, zThru, thruHoles, color } = state;
let { drills, drillThrough} = op
let drillTool = new Tool(settings, op.tool),
drillToolDiam = drillTool.fluteDiameter(),
sliceOut = this.sliceOut = [];
updateToolDiams(drillToolDiam);
const allDrills = drills[widget.id] ?? []
// 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))
// poly.points.pop();
slice.camTrace = { tool: op.tool, rate: op.feed, plunge: op.rate };
slice.camLines = [poly];
slice.output()
.setLayer("drill", {face: color, line: color})
.addPolys(slice.camLines);
addSlices(slice);
sliceOut.push(slice);
});
}
prepare(ops, progress) {
let { op, state } = this;
let { settings, widget, addSlices, updateToolDiams } = state;
let { setTool, setSpindle, setDrill, emitDrills } = ops;
setTool(op.tool, undefined, op.rate);
setDrill(op.down, op.lift, op.dwell,op.thru);
setSpindle(op.spindle);
emitDrills(this.sliceOut.map(slice => slice.camLines).flat());
}
}
export { OpDrill };

View file

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

View file

@ -0,0 +1,36 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js';
export class OpIndex extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice() {
let { op, state } = this;
if (!state.isIndexed) {
throw 'index op requires indexed stock';
}
let { widget, updateSlicer, computeShadows, setAxisIndex } = state;
this.degrees = setAxisIndex(op.degrees, op.absolute);
// force recompute of topo
widget.topo = undefined;
updateSlicer();
await computeShadows();
}
prepare(ops, progress) {
const { lastPoint, zmax, zclear, camOut, stock } = ops;
let last = lastPoint();
if (last) {
// max point of stock corner radius when rotating (safe z when indexing)
const rzmax = (Math.max(stock.y, stock.z) * Math.sqrt(2)) / 2 + zclear;
const zmove = Math.max(rzmax, zmax);
// move above rotating stock
camOut(last = last.clone().setZ(zmove), 0);
// issue rotation command
camOut(last = last.clone().setY(0).setA(this.degrees), 0);
}
}
}

View file

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

View file

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

View file

@ -0,0 +1,118 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js';
import { Tool } from './tool.js';
import { newPoint } from '../../../geo/point.js';
function createFilter(op) {
let filter = slices => slices;
if (op.filter) {
try {
const obj = eval(`( ${op.filter.join('\n')} )`);
let idx = 0;
if (obj && obj.slices) {
const nadd = [];
filter = function(slices) {
for (let slice of slices) {
if (obj.slices(slice, idx++)) {
nadd.push(slice);
}
}
return nadd;
};
}
} 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 } = state;
let filter = createFilter(op);
this.topo = await Topo4({
op,
state,
onupdate: (pct, msg) => {
progress(pct, msg);
},
ondone: (slices) => {
slices = filter(slices);
this.slices = slices;
addSlices(slices, false);
}
});
}
prepare(ops, progress) {
let { op, state, slices, topo } = this;
let { settings } = state;
let { setTool, setSpindle, setPrintPoint } = ops;
let { camOut, newLayer, printPoint } = ops;
let { zmax } = ops;
let toolDiam = new Tool(settings, op.tool).fluteDiameter();
let stepover = toolDiam * op.step * 2;
let rez = topo.resolution;
setTool(op.tool, op.rate, op.plunge);
setSpindle(op.spindle);
// start top center, X = 0, Y = 0 closest to 4th axis chuck
printPoint = newPoint(0, 0, zmax);
for (let slice of slices) {
// ignore debug slices
if (!slice.camLines) {
continue;
}
let last;
for (let path of slice.camLines) {
let latent;
path.forEachPoint((point, pidx) => {
if (last) {
const dz = Math.abs(last.z - point.z);
if (dz < rez) {
// latent point should still be included in
// preview b/c arcs would look like straight lines
latent = point.clone();
return;
}
if (latent) {
camOut(latent, true, stepover);
latent = undefined;
}
}
camOut(last = point.clone(), pidx > 0, stepover);
}, false);
if (latent) {
camOut(latent, true, stepover);
}
}
newLayer();
}
// move to safe height and reset A axis
let last = ops.lastPoint();
let amax = (Math.round(last.a / 360) * 360).round(2);
// camOut(last = last.clone().setZ(zmax), 0);
// camOut(last = last.clone().setA(amax), 0);
newLayer();
ops.addGCode([`G0 Z${zmax.round(2)}`,`G0 A${amax}`,"G92 A0"]);
setPrintPoint(last);
}
}
export { OpLathe };

View file

@ -0,0 +1,68 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js';
import { Tool } from './tool.js';
import { newPolygon } from '../../../geo/polygon.js';
import { polygons as POLY } from '../../../geo/polygons.js';
import { newSlice } from '../../core/slice.js';
class OpLevel extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let { op, state } = this;
let { settings, widget, addSlices } = state;
let { updateToolDiams, tabs, cutTabs } = state;
let { bounds, zMax, ztOff, color, tshadow } = state;
let { stock } = settings;
let toolDiam = new Tool(settings, op.tool).fluteDiameter();
let stepOver = this.stepOver = toolDiam * op.step;
let z = zMax + ztOff - op.down;
updateToolDiams(toolDiam);
let points = [];
let clear = op.stock ?
[ newPolygon().centerRectangle({x:0,y:0,z:0}, stock.x, stock.y) ] :
POLY.outer(POLY.offset(tshadow, toolDiam * (op.over || 0)));
POLY.fillArea(clear, 90, stepOver, points);
let lines = this.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(this.lines);
addSlices(slice);
}
}
prepare(ops, progress) {
let { op, state, lines, stepOver } = this;
let { setTool, setSpindle, printPoint, setPrintPoint } = ops;
let { polyEmit, newLayer, tip2tipEmit, camOut } = ops;
setTool(op.tool, op.rate);
setSpindle(op.spindle);
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);
});
setPrintPoint(printPoint);
newLayer();
}
}
export { OpLevel };

View file

@ -0,0 +1,298 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js';
import { Tool } from './tool.js';
import { newPolygon } from '../../../geo/polygon.js';
import { newSlice } from '../../core/slice.js';
import { polygons as POLY } from '../../../geo/polygons.js';
import { util as base_util } from '../../../geo/base.js';
import { poly2polyEmit } from '../../../geo/paths.js';
import { newPoint } from '../../../geo/point.js';
class OpOutline extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let { op, state } = this;
let { settings, widget, slicer, addSlices, tshadow, thruHoles, unsafe, color } = state;
let { updateToolDiams, tabs, cutTabs, cutPolys, workarea, zMax } = state;
let { process, stock } = settings;
if (op.down <= 0) {
throw `invalid step down "${op.down}"`;
}
let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter();
updateToolDiams(toolDiam);
let shadow = [];
let slices = [];
let intopt = {
off: 0.01,
fit: true,
down: true,
min: Math.max(0, workarea.bottom_z),
max: workarea.top_z
};
let indices = slicer.interval(op.down, intopt);
let trueShadow = process.camTrueShadow === true;
// shift out first (top-most) slice
indices.shift();
// add flats to shadow
const flats = Object.keys(slicer.zFlat)
.map(v => (parseFloat(v) - 0.01).round(5))
.filter(v => v > 0 && indices.indexOf(v) < 0);
indices = indices.appendAll(flats).sort((a,b) => b-a);
let cnt = 0;
let tot = 0;
if (op.outside && !op.inside) {
// console.log({outline_bypass: indices, down: op.down});
indices.forEach((ind,i) => {
if (flats.indexOf(ind) >= 0) {
// exclude flats
return;
}
let slice = newSlice(ind);
slice.shadow = shadow.clone(true);
slices.push(slice);
});
} else
await slicer.slice(indices, { each: data => {
shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true);
if (flats.indexOf(data.z) >= 0) {
// exclude flats injected to complete shadow
return;
}
data.shadow = trueShadow ? shadowAt(widget, data.z) : shadow.clone(true);
data.slice.shadow = data.shadow;
// data.slice.tops[0].inner = data.shadow;
// data.slice.tops[0].inner = POLY.setZ(tshadow.clone(true), data.z);
slices.push(data.slice);
// data.slice.xray();
// onupdate(0.2 + (index/total) * 0.1, "outlines");
progress(0.5 + 0.5 * (++cnt / tot));
}, progress: (index, total) => {
tot = total;
progress((index / total) * 0.5);
} });
shadow = POLY.union(shadow.appendAll(state.shadow.base), 0.01, true);
// start slices at top of stock when `clear top` enabled
if (op.top) {
let first = slices[0];
let zlist = slices.map(s => s.z);
for (let z of indices.filter(v => v >= zMax)) {
if (zlist.contains(z)) {
continue;
}
let add = first.clone(true);
add.tops.forEach(top => top.poly.setZ(add.z));
add.shadow = first.shadow.clone(true);
add.z = z;
slices.splice(0,0,add);
}
}
// extend cut thru (only when z bottom is 0)
if (workarea.bottom_z < 0) {
let last = slices[slices.length-1];
for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) {
if (!last) continue;
let add = last.clone(true);
add.tops.forEach(top => top.poly.setZ(add.z));
add.shadow = last.shadow.clone(true);
add.z -= zneg;
slices.push(add);
}
}
slices.forEach(slice => {
let tops = slice.shadow;
// outside only (use tshadow for entire cut)
if (op.outside) {
tops = tshadow;
}
if (op.omitthru) {
// eliminate thru holes from shadow
for (let hole of thruHoles) {
for (let top of tops) {
if (!top.inner) continue;
top.inner = top.inner.filter(innr => {
return !innr.isEquivalent(hole, false, 0.1);
});
}
}
}
if (op.omitvoid) {
for (let top of tops) {
delete top.inner;
}
}
let offset = POLY.expand(tops, toolDiam / 2, slice.z);
if (!(offset && offset.length)) {
return;
}
// when pocket only, drop first outer poly
// if it matches the shell and promote inner polys
if (op.inside) {
let shell = POLY.expand(tops.clone(), toolDiam / 2);
offset = POLY.filter(offset, [], function(poly) {
if (poly.area() < 1) {
return null;
}
for (let sp=0; sp<shell.length; sp++) {
// eliminate shell only polys
if (poly.isEquivalent(shell[sp])) {
if (poly.inner) return poly.inner;
return null;
}
}
return poly;
});
} else {
if (op.wide) {
let stepover = toolDiam * op.step;
let wideCuts = [] //accumulator for wide cuts
for (let c = (op.steps || 1); c > 0; c--){
offset.slice().forEach(op => {
// clone removes inners but the real solution is
// to limit expanded shells to through holes
let wideCut = POLY.expand([op.clone(true)], stepover*c, slice.z, [], 1);
wideCut.forEach(cut =>{ //set order of cuts when wide
cut.order = c
if(cut.inner) cut.inner.forEach(inn =>{ inn.order = c })
});
wideCuts.push(...wideCut)
});
}
offset.appendAll(wideCuts);
}
}
if (op.dogbones && !op.wide) {
addDogbones(offset, toolDiam / 5);
}
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
offset = cutTabs(tabs, offset, slice.z);
}
if (process.camStockClipTo && stock.x && stock.y && stock.center) {
let rect = newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y);
offset = cutPolys([rect], offset, slice.z, true);
}
// offset.xout(`slice ${slice.z}`);
slice.camLines = offset;
});
// when top expand fails above, it creates an empty slice
slices = slices.filter(s => s.camLines);
// project empty up and render
for (let slice of slices) {
if (false) slice.output()
.setLayer("slice", {line: 0xaaaa00}, false)
.addPolys(slice.topPolys())
slice.output()
.setLayer("outline", {face: color, line: color})
.addPolys(slice.camLines);
}
addSlices(slices);
this.sliceOut = slices;
}
prepare(ops, progress) {
let { op, state, sliceOut } = this;
let { setTool, setSpindle, setPrintPoint } = ops;
let { polyEmit, depthOutlinePath } = ops;
let { camOut, newLayer, printPoint } = ops;
let { settings, widget } = state;
let { process, controller } = settings;
let easeDown = process.camEaseDown;
let toolDiam = this.toolDiam;
let cutdir = op.ov_conv;//process.camConventional;
let depthFirst = process.camDepthFirst;
let depthData = [];
setTool(op.tool, op.rate, op.plunge);
setSpindle(op.spindle);
// printpoint becomes NaN in engine mode. not sure why but this fixes it
if(Object.values(printPoint).some(v=>Number.isNaN(v))){
printPoint = newPoint(0,0,0);
}
for (let slice of sliceOut) {
let polys = [], t = [], c = [];
let lines =POLY.flatten(slice.camLines)
// console.log(lines);
lines.forEach((poly)=> {
poly.order = poly.order ?? 0;
let child = poly.parent;
if (depthFirst) { poly = poly.clone(); poly.parent = child ? 1 : 0 }
if (child) c.push(poly); else t.push(poly);
poly.layer = depthData.layer;
polys.push(poly);
});
// set cut direction on outer polys
POLY.setWinding(t, cutdir);
// set cut direction on inner polys
POLY.setWinding(c, !cutdir);
if (depthFirst) {
depthData.push(polys);
} else {
let orderSplit = {}
polys.forEach(poly => {
if(poly.order in orderSplit) orderSplit[poly.order].push(poly);
else orderSplit[poly.order] = [poly];
})
Object.entries(orderSplit) //split the polys by order
.sort((a,b) => -(a[0] - b[0] )) //sort by order (highest first)
.forEach(([order, orderPolys]) => { // emit based on closest for each order
let polyLast;
// console.log({order, orderPolys});
printPoint = poly2polyEmit(orderPolys, printPoint, function(poly, index, count) {
polyLast = polyEmit(poly, index, count, polyLast);
}, { swapdir: false });
})
newLayer();
}
}
if (depthFirst) {
let flatLevels = depthData.map(level => {
return POLY.flatten(level.clone(true), [], true).filter(p => !(p.depth = 0));
}).filter(l => l.length > 0);
if (flatLevels.length && flatLevels[0].length) {
// start with the smallest polygon on the top
printPoint = flatLevels[0]
.sort((a,b) => { return a.area() - b.area() })[0]
.average();
// experimental start of ease down
let ease = op.down && easeDown ? 0.001 : 0;
printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, false, ease);
printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, true, ease);
}
}
setPrintPoint(printPoint);
}
}
export { OpOutline };

View file

@ -0,0 +1,285 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js';
import { Tool } from './tool.js';
import { generate as Topo } from './topo3.js';
import { newPolygon } from '../../../geo/polygon.js';
import { newSlice } from '../../core/slice.js';
import { polygons as POLY } from '../../../geo/polygons.js';
import { util as base_util } from '../../../geo/base.js';
import { calc_normal, calc_vertex } from '../../../geo/paths.js';
import { CAM } from './driver-be.js';
const DEG2RAG = Math.PI / 180;
class OpPocket extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
const pocket = this;
const debug = false;
let { op, state } = this;
let { tool, rate, down, plunge, expand, contour, smooth, tolerance } = op;
let { ov_botz, ov_conv } = op;
let { settings, widget, addSlices, zBottom, zThru, tabs, color } = state;
let { updateToolDiams, cutTabs, healPolys, shadowAt, workarea } = state;
let { process } = settings;
zBottom = ov_botz ? workarea.bottom_stock + ov_botz : zBottom;
// generate tracing offsets from chosen features
let sliceOut;
let pockets = this.pockets = [];
let camTool = new Tool(settings, tool);
let toolDiam = camTool.fluteDiameter();
let toolOver = toolDiam * op.step;
let cutdir = ov_conv;
let engrave = contour && op.engrave;
let zTop = workarea.top_z;
if (contour) {
down = 0;
this.topo = await Topo({
// onupdate: (update, msg) => {
onupdate: (index, total, msg) => {
progress((index / total) * 0.9, msg);
},
ondone: (slices) => {
// console.log({ contour: slices });
},
contour: {
tool,
tolerance,
inside: true,
axis: "-"
},
state: state
});
}
updateToolDiams(toolDiam);
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
}
function newPocket() {
pockets.push(sliceOut = []);
}
function newSliceOut(z) {
let slice = newSlice(z);
sliceOut.push(slice);
return slice;
}
function clearZ(polys, z, down) {
if (down) {
// adjust step down to a value <= down that
// ends on the lowest z specified
let diff = zTop - z;
down = diff / Math.ceil(diff / down);
}
let zs = down ? base_util.lerp(zTop, z, down) : [ z ];
if (engrave) {
toolDiam = toolOver;
}
if (contour) {
expand = engrave ? 0 : expand;
} else if (expand) {
polys = POLY.offset(polys, expand);
}
let zpro = 0, zinc = 1 / (polys.length * zs.length);
for (let poly of polys) {
newPocket();
for (let z of zs) {
let clip = [], shadow;
if (contour) {
if (smooth) {
clip = POLY.offset(POLY.offset([ poly ], smooth), -smooth);
} else {
clip = [ poly ];
}
} else {
shadow = shadowAt(z);
if (smooth) {
shadow = POLY.setZ(POLY.offset(POLY.offset(shadow, smooth), -smooth), z);
}
POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0);
if (op.outline) {
POLY.clearInner(clip);
}
}
if (clip.length === 0) {
continue;
}
let slice = newSliceOut(z);
let count = engrave ? 1 : 999;
slice.camTrace = { tool, rate, plunge };
if (toolDiam) {
const offs = contour ?
[ expand || (-0.02), -toolOver ] :
[ -toolDiam / 2, -toolOver ];
POLY.offset(clip, offs, {
count, outs: slice.camLines = [], flat:true, z, minArea: 0
});
} else {
// when engraving with a 0 width tip
slice.camLines = clip;
}
if (tabs) {
slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z);
} else {
slice.camLines = POLY.flatten(slice.camLines, null, true);
}
POLY.setWinding(slice.camLines, cutdir, false);
if (contour) {
slice.camLines = pocket.conform(slice.camLines, op.refine, engrave, pct => {
progress(0.9 + (zpro + zinc * pct) * 0.1, "conform");
});
}
slice.output()
.setLayer("pocket", {line: color}, false)
.addPolys(slice.camLines)
if (debug && shadow) slice.output()
.setLayer("pocket shadow", {line: 0xff8811}, false)
.addPolys(shadow)
if (!contour) {
progress(zpro, "pocket");
}
zpro += zinc;
addSlices(slice);
}
}
}
let surfaces = op.surfaces[widget.id] || [];
let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4));
// let vert = widget.getVertices().array.map(v => v.round(4));
let outline = [];
let faces = CAM.surface_find(widget, surfaces, (op.follow || 5) * DEG2RAG);
let zmin = Infinity;
let j=0, k=faces.length;
for (let face of faces) {
let i = face * 9;
outline.push(newPolygon()
.add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++]))
.add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++]))
.add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++]))
);
}
zmin = Math.max(zBottom, zmin);
outline = POLY.union(outline, 0.0001, true);
outline = POLY.setWinding(outline, cutdir, false);
outline = healPolys(outline);
if (smooth) {
outline = POLY.offset(POLY.offset(outline, smooth), -smooth);
}
if (outline.length) {
// option to skip interior features (holes, pillars)
if (op.outline) {
POLY.clearInner(outline);
}
if (debug) newSliceOut(zmin).output()
.setLayer("pocket area", {line: 0x1188ff}, false)
.addPolys(outline)
clearZ(outline, zmin + 0.0001, down);
progress(1, "pocket");
}
}
// mold cam output lines to the surface of the topo offset by tool geometry
conform(camLines, refine, engrave, progress) {
const topo = this.topo;
// re-segment polygon to a higher resolution
const hirez = camLines.map(p => p.segment(topo.tolerance * 2));
// walk points and offset from surface taking into account tool geometry
let steps = hirez.length;
let iter = 0;
for (let poly of hirez) {
for (let point of poly.points) {
point.z = engrave ? topo.zAtXY(point.x, point.y) : topo.toolAtXY(point.x, point.y);
}
progress((iter++ / steps) * 0.8);
}
steps = steps * refine;
iter = 0;
// walk points noting z deltas and smoothing z sawtooth patterns
for (let j=0; j<refine; j++) {
for (let poly of hirez) {
const points = poly.points, length = points.length;
let sn = []; // segment normals
for (let i=0; i<length; i++) {
let p1 = points[i];
let p2 = points[(i + 1) % length];
sn.push(calc_normal(p1, p2));
}
let vn = []; // vertex normals
for (let i=0; i<length; i++) {
let n1 = sn[(i + length - 1) % length];
let n2 = sn[i];
let vi = calc_vertex(n1, n2, 1);
vn.push(vi);
let vl = Math.abs(1 - vi.vl).round(2);
// vl should be close to zero on smooth / continuous curves
// factoring out hard turns, we smooth the z using the weighted
// z values of the points before and after the current point
if (vl === 0) {
let p0 = points[(i + length - 1) % length];
let p1 = points[i];
let p2 = points[(i + 1) % length];
p1.z = (p0.z + p2.z + p1.z) / 3;
}
}
progress((iter++ / steps) * 0.2 + 0.8);
}
}
// return hirez.map(p => p.midpoints(topo.tolerance * 8));
return hirez;
}
prepare(ops, progress) {
let { op, state, pockets } = this;
let { setTool, setSpindle, setTolerance, sliceOutput, getPrintPoint } = ops;
let { process } = state.settings;
setTool(op.tool, op.rate);
setSpindle(op.spindle);
if (this.topo) {
setTolerance(this.topo.tolerance);
}
// eliminate empty pockets
pockets = pockets.filter(p => p.length);
// pockets is an [ array of an [ array of slices ] ]
// each top level array is a pocket containing a [ z layer array of slices ]
// follow each pocket to the next closest one from previous exit
for (;;) {
let printPoint = getPrintPoint();
let min = {
dist: Infinity,
pocket: undefined
};
for (let pocket of pockets.filter(p => !p.used)) {
let poly = pocket[0].camLines.slice().sort((a,b) => b.area() - a.area())[0];
if (!poly) continue;
let find = poly.findClosestPointTo(printPoint);
if (find.distance < min.dist) {
min.pocket = pocket;
min.dist = find.distance;
}
}
if (min.pocket) {
min.pocket.used = true;
sliceOutput(min.pocket, {
cutdir: op.ov_conv,
depthFirst: process.camDepthFirst && !state.isIndexed,
easeDown: op.down && process.easeDown ? op.down : 0,
progress: (n,m) => progress(n/m, "pocket")
});
} else {
break;
}
}
}
}
export { OpPocket };

View file

@ -0,0 +1,162 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js';
import { Tool } from './tool.js';
import { newPolygon } from '../../../geo/polygon.js';
import { newSlice } from '../../core/slice.js';
import { newPoint } from '../../../geo/point.js';
import { util as base_util } from '../../../geo/base.js';
class OpRegister extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let { op, state } = this;
let { settings, widget, bounds, addSlices, zMax, zThru, color } = state;
let { updateToolDiams } = state;
let tool = new Tool(settings, op.tool);
let sliceOut = this.sliceOut = [];
updateToolDiams(tool.fluteDiameter());
let { stock } = settings,
tz = widget.track.pos.z,
lx = bounds.min.x,
hx = bounds.max.x,
ly = bounds.min.y,
hy = bounds.max.y,
o3 = tool.fluteDiameter() * 2,
mx = (lx + hx) / 2,
my = (ly + hy) / 2,
mz = op.thru || zThru || 0,
dx = (stock.x - (hx - lx)) / 4,
dy = (stock.y - (hy - ly)) / 4,
dz = stock.z,
points = [],
wo = stock.z - bounds.max.z,
z1 = bounds.max.z + wo + tz,
z2 = tz - mz;
if (!(stock.x && stock.y && stock.z)) {
return;
}
switch (op.axis) {
case "X":
case "x":
if (op.points == 3) {
points.push(newPoint(lx - dx, my, 0));
points.push(newPoint(hx + dx, my - o3, 0));
points.push(newPoint(hx + dx, my + o3, 0));
} else {
points.push(newPoint(lx - dx, my, 0));
points.push(newPoint(hx + dx, my, 0));
}
break;
case "Y":
case "y":
if (op.points == 3) {
points.push(newPoint(mx, ly - dy, 0));
points.push(newPoint(mx - o3, hy + dy, 0));
points.push(newPoint(mx + o3, hy + dy, 0));
} else {
points.push(newPoint(mx, ly - dy, 0));
points.push(newPoint(mx, hy + dy, 0));
}
break;
case "-":
let o2 = o3 / 2,
x0 = lx - dx,
x1 = hx + dx,
y0 = ly - dy - o2,
y1 = hy + dy + o2,
x4 = (x1 - x0 - o2) / 4,
y4 = (y1 - y0 - o2 * 3) / 4,
poly, cp, cz;
function start(z) {
cz = z;
cp = {x:x0 + o2 * 0.5, y:y0 + o2 * 1.5};
poly = newPolygon().add(cp.x, cp.y, z);
}
function move(dx, dy) {
cp.x += dx;
cp.y += dy;
poly.add(cp.x, cp.y, cz);
}
function rept(count, tv, fn) {
while (count-- > 0) {
fn(tv, count === 0);
tv = -tv;
}
}
for (let z of base_util.lerp(z1, z2, op.down)) {
let slice = newSlice(z);
addSlices(slice);
sliceOut.push(slice);
start(z);
rept(4, o2, oy => {
move(0, -oy);
move(x4, 0);
});
rept(4, o2, ox => {
move(ox, 0);
move(0, y4);
});
rept(4, o2, oy => {
move(0, oy);
move(-x4, 0);
});
rept(4, o2, ox => {
move(-ox, 0);
move(0, -y4);
});
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 (points.length) {
let slice = newSlice(0,null), polys = [];
points.forEach(point => {
polys.push(newPolygon()
.append(point.clone().setZ(z1))
.append(point.clone().setZ(z2)));
});
slice.camLines = polys;
slice.output()
.setLayer("register", {face: color, line: color})
.addPolys(polys);
addSlices(slice);
sliceOut.push(slice);
}
}
prepare(ops, progress) {
let { op, state } = this;
let { settings, widget, addSlices, updateToolDiams } = state;
let { setTool, setSpindle, setDrill, emitDrills } = ops;
if (op.axis === '-') {
setTool(op.tool, op.feed, op.rate);
setSpindle(op.spindle);
for (let slice of this.sliceOut) {
ops.emitTrace(slice);
}
} else {
setTool(op.tool, undefined, op.rate);
setDrill(op.down, op.lift, op.dwell);
setSpindle(op.spindle);
emitDrills(this.sliceOut.map(slice => slice.camLines).flat());
}
}
}
export { OpRegister };

View file

@ -0,0 +1,318 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js';
import { Tool } from './tool.js';
import { newPolygon } from '../../../geo/polygon.js';
import { newSlice } from '../../core/slice.js';
import { polygons as POLY } from '../../../geo/polygons.js';
import { util as base_util } from '../../../geo/base.js';
import { poly2polyEmit } from '../../../geo/paths.js';
class OpRough extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
let { op, state } = this;
let { settings, slicer, addSlices, unsafe, color } = state;
let { updateToolDiams, thruHoles, tabs, cutTabs, cutPolys } = state;
let { ztOff, zMax, shadowAt, isIndexed} = state;
let { workarea } = state;
let { process, stock } = settings;
if (op.down <= 0) {
throw `invalid step down "${op.down}"`;
}
let roughIn = op.inside;
let roughDown = op.down;
let roughLeave = op.leave || 0;
let roughLeaveZ = op.leavez || 0;
let roughStock = op.all && isIndexed;
let toolDiam = new Tool(settings, op.tool).fluteDiameter();
let trueShadow = process.camTrueShadow === true;
updateToolDiams(toolDiam);
// clear the stock above the area to be roughed out
if (workarea.top_z > workarea.top_part) {
let shadow = state.shadow.base.clone();
let step = toolDiam * op.step;
let inset = roughStock ?
POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], step) :
POLY.offset(shadow, roughIn ? step : step + roughLeave + toolDiam / 2);
let facing = POLY.offset(inset, -step, { count: 999, flat: true });
let zdiv = ztOff / roughDown;
let zstep = (zdiv % 1 > 0) ? ztOff / (Math.floor(zdiv) + 1) : roughDown;
if (ztOff === 0) {
// compensate for lack of z top offset in this scenario
ztOff = zstep;
}
let zsteps = Math.round(ztOff / zstep);
let camFaces = this.camFaces = [];
let zstart = zMax + ztOff - zstep;
for (let z = zstart; zsteps > 0; zsteps--) {
let slice = newSlice();
slice.z = z;
slice.camLines = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ);
slice.output()
.setLayer("face", {face: color, line: color})
.addPolys(slice.camLines);
addSlices(slice);
camFaces.push(slice);
z -= zstep;
}
}
// create roughing slices
let flats = [];
let shadow = [];
let slices = [];
let indices = slicer.interval(roughDown, {
down: true, min: 0, fit: true, off: 0.01
});
// shift out first (top-most) slice
indices.shift();
// find flats and add to indices for slicing
if (op.flats) {
let flatArea = (Math.PI * (toolDiam/2) * (toolDiam/2)) / 2;
let flats = Object.entries(slicer.zFlat)
.filter(row => row[1] > flatArea)
.map(row => row[0])
.map(v => parseFloat(v).round(5))
.filter(v => v >= workarea.bottom_z);
flats.forEach(v => {
if (!indices.contains(v)) {
indices.push(v);
}
});
indices = indices.sort((a,b) => { return b - a });
// if layer is not on a flat and next one is,
// then move this layer up to mid-point to previous layer
// this is not perfect. the best method is to interpolate
// between flats so that each step is < step down. on todo list
for (let i=1; i<indices.length-1; i++) {
const prev = indices[i-1];
const curr = indices[i];
const next = indices[i+1];
if (!flats.contains(curr) && flats.contains(next)) {
// console.log('move',curr,'up toward',prev,'b/c next',next,'is flat');
indices[i] = next + ((prev - next) / 2);
}
}
} else {
// add flats to shadow
flats = Object.keys(slicer.zFlat)
.map(v => (parseFloat(v) - 0.01).round(5))
.filter(v => v > 0 && indices.indexOf(v) < 0);
indices = indices.appendAll(flats).sort((a,b) => b-a);
}
indices = indices.filter(v => v >= workarea.bottom_z);
// console.log('indices', ...indices, {zBottom});
let cnt = 0;
let tot = 0;
await slicer.slice(indices, { each: data => {
shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true);
if (flats.indexOf(data.z) >= 0) {
// exclude flats injected to complete shadow
return;
}
if (data.z > workarea.top_z) {
return;
}
data.shadow = trueShadow ? shadowAt(data.z) : shadow.clone(true);
data.slice.shadow = data.shadow;
slices.push(data.slice);
progress(0.25 + 0.25 * (++cnt / tot));
}, progress: (index, total) => {
tot = total;
progress((index / total) * 0.25);
} });
if (trueShadow) {
shadow = state.shadow.base.clone(true);
} else {
shadow = POLY.union(shadow.appendAll(state.shadow.base), 0.01, true);
}
// inset or eliminate thru holes from shadow
shadow = POLY.flatten(shadow.clone(true), [], true);
thruHoles.forEach(hole => {
shadow = shadow.map(p => {
if (p.isEquivalent(hole)) {
let po = POLY.offset([p], -(toolDiam / 2 + roughLeave + 0.05));
return po ? po[0] : undefined;
} else {
return p;
}
}).filter(p => p);
});
shadow = POLY.nest(shadow);
if (op.voids) {
// eliminate voids from shadow when "clear voids" enables
for (let s of shadow) s.inner = undefined;
}
// shell = shadow expanded by half tool diameter + leave stock
const sadd = roughIn ? toolDiam / 2 : toolDiam / 2;
const shell = roughStock ?
POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], sadd) :
POLY.offset(shadow, sadd + roughLeave);
slices.forEach((slice, index) => {
let offset = [shell.clone(true),slice.shadow.clone(true)].flat();
let flat = POLY.flatten(offset, [], true);
let nest = POLY.setZ(POLY.nest(flat), slice.z);
// inset offset array by 1/2 diameter then by tool overlap %
offset = POLY.offset(nest, [-(toolDiam / 2 + roughLeave), -toolDiam * op.step], {
minArea: Math.min(0.01, toolDiam * op.step / 4),
z: slice.z,
count: 999,
flat: true,
call: (polys, count, depth) => {
// used in depth-first path creation
polys.forEach(p => {
p.depth = depth;
if (p.inner) {
p.inner.forEach(p => p.depth = depth);
}
});
}
}) || [];
// add outside pass if not inside only
if (!roughIn && !roughStock) {
const outside = POLY.offset(shadow.clone(), toolDiam / 2 + roughLeave, {z: slice.z});
if (outside) {
outside.forEach(p => p.depth = -p.depth);
offset.appendAll(outside);
}
}
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
offset = cutTabs(tabs, offset, slice.z);
}
if (!offset) return;
if (process.camStockClipTo && stock.x && stock.y && stock.center) {
let rect = newPolygon().centerRectangle(stock.center, stock.x, stock.y);
offset = cutPolys([rect], offset, slice.z, true);
}
// elimate double inset on inners
offset.forEach(op => {
if (op.inner) {
let pv1 = op.perimeter();
let newinner = [];
op.inner.forEach(oi => {
let pv2 = oi.perimeter();
let pct = pv1 > pv2 ? pv2/pv1 : pv1/pv2;
if (pct < 0.98) {
newinner.push(oi);
}
});
op.inner = newinner;
}
});
slice.camLines = offset;
if (roughLeaveZ) {
// offset roughing in Z as well to minimize
// tool marks on curved surfaces
// const roughLeaveZ = 1 * Math.min(roughDown, roughLeave / 2);
slice.camLines.forEach(p => {
p.setZ(p.getZ() + roughLeaveZ);
});
}
if (false) slice.output()
.setLayer("slice", {line: 0xaaaa00}, true)
.addPolys(slice.topPolys())
// .setLayer("top shadow", {line: 0x0000aa})
// .addPolys(tshadow)
// .setLayer("rough shadow", {line: 0x00aa00})
// .addPolys(shadow)
.setLayer("rough shell", {line: 0xaa0000})
.addPolys(shell);
progress(0.5 + 0.5 * (index / slices.length));
});
let last = slices[slices.length-1];
if (workarea.bottom_z < 0)
for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) {
if (!last) continue;
let add = last.clone(true);
add.z -= zneg;
add.camLines = last.camLines.clone(true);
add.camLines.forEach(p => p.setZ(add.z + roughLeaveZ));
// add.tops.forEach(top => top.poly.setZ(add.z));
// add.shadow = last.shadow.clone(true);
slices.push(add);
}
slices.forEach(slice => {
slice.output()
.setLayer("roughing", {face: color, line: color})
.addPolys(slice.camLines);
});
this.sliceOut = slices.filter(slice => slice.camLines);
addSlices(this.sliceOut);
}
prepare(ops, progress) {
let { op, state, sliceOut, camFaces } = this;
let { setTool, setSpindle, setPrintPoint, sliceOutput, polyEmit } = ops;
let { camOut, newLayer, printPoint } = ops;
let { settings } = state;
let { process } = settings;
let easeDown = process.camEaseDown;
let cutdir = op.ov_conv;
let depthFirst = process.camDepthFirst && !state.isIndexed;
let depthData = [];
setTool(op.tool, op.rate, op.plunge);
setSpindle(op.spindle);
// output the clearing of stock above roughing
for (let slice of (camFaces || [])) {
const level = [];
for (let poly of slice.camLines) {
level.push(poly);
if (poly.inner) {
poly.inner.forEach(function(inner) {
level.push(inner);
});
}
}
// set winding specified in output
POLY.setWinding(level, cutdir, false);
poly2polyEmit(level, printPoint, (poly, index, count) => {
printPoint = polyEmit(poly, index, count, printPoint);
});
newLayer();
}
// output the roughing passes
setPrintPoint(printPoint);
sliceOutput(sliceOut, {
cutdir,
depthFirst,
easeDown: op.down && easeDown ? 0.001 : 0,
progress: (n,m) => progress(n/m, "routing")
});
}
}
export { OpRough };

View file

@ -0,0 +1,112 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js';
import { polygons as POLY } from '../../../geo/polygons.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 { ops, slicer, widget, unsafe, addSlices, shadowAt } = state;
let realOps = ops.map(rec => rec.op).filter(op => op);
let trueShadow = state.settings.process.camTrueShadow === true;
let minStepDown = realOps
.map(op => (op.down || 3) / (trueShadow ? 1 : 3))
.reduce((a,v) => Math.min(a, v, 1));
let tslices = [];
let tshadow = [];
let tzindex = slicer.interval(minStepDown, {
fit: true, off: 0.01, down: true, flats: true
});
let skipTerrain = unsafe;
if (skipTerrain) {
console.log("skipping terrain generation");
tzindex = [ tzindex.pop() ];
}
let lsz; // only shadow up to bottom of last shadow for progressive union
let cnt = 0;
let tot = 0;
// terrain is the "shadow stack" where index 0 = top of part
// thus array.length -1 = bottom of part
let terrain = await slicer.slice(tzindex, { each: data => {
let shadow = trueShadow ? shadowAt(data.z, lsz) : [];
tshadow = POLY.union(tshadow.slice().appendAll(data.tops).appendAll(shadow), 0.01, true);
tslices.push(data.slice);
// capture current shadow for this slice
data.slice.shadow = tshadow;
if (false) {
const slice = data.slice;
addSlices(slice);
slice.output()
.setLayer("shadow", {line: 0x888800, thin: true })
.addPolys(POLY.setZ(tshadow.clone(true), data.z), { thin: true });
slice.output()
.setLayer("slice", {line: 0x886622, thin: true })
.addPolys(POLY.setZ(data.tops.clone(true), data.z), { thin: true });
// let p1 = [], p2 = [], cp = p1;
// for (let line of data.lines) {
// cp.push(line.p1);
// cp.push(line.p2);
// cp = (cp === p1 ? p2 : p1);
// }
// slice.output()
// .setLayer("lines1", {line: 0x884444, thin: true })
// .addLines(p1, { thin: true });
// slice.output()
// .setLayer("lines2", {line: 0x444488, thin: true })
// .addLines(p2, { thin: true });
}
lsz = data.z;
progress(0.5 + 0.5 * (++cnt / tot));
}, progress: (index, total) => {
tot = total;
progress((index / total) * 0.5);
} });
if (terrain.length === 0) {
throw `invalid widget shadow`;
}
// TODO: deprecate use of separate shadow vars in state
state.center = tshadow[0].bounds.center();
state.tshadow = tshadow; // true shadow (base of part)
state.terrain = terrain; // stack of shadow slices stored in tops
state.tslices = tslices; // raw slicer 'data' layer outputs
state.skipTerrain = skipTerrain;
// TODO: refactor ops to use a unified shadow object
state.shadow = {
base: tshadow, // computed shadow union at base of part
stack: terrain, // stack of shadow slices
slices: tslices, // raw slicer 'data' objects
skip: skipTerrain
};
// identify through holes which are inner/child polygons
// on the bottom-most layer of the shadow stack (tshadow, index == 0)
state.thruHoles = tshadow.map(p => p.inner || []).flat();
}
}
export { OpShadow };

View file

@ -0,0 +1,290 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js';
import { Tool } from './tool.js';
import { newPolygon } from '../../../geo/polygon.js';
import { newSlice } from '../../core/slice.js';
import { polygons as POLY } from '../../../geo/polygons.js';
import { util as base_util } from '../../../geo/base.js';
import { poly2polyEmit } from '../../../geo/paths.js';
import { newPoint } from '../../../geo/point.js';
class OpTrace extends CamOp {
constructor(state, op) {
super(state, op);
}
async slice(progress) {
const debug = false;
let { op, state } = this;
let { tool, rate, down, plunge, offset, offover, thru } = op;
let { ov_conv } = op;
let { settings, widget, addSlices, zThru, tabs, workarea } = state;
let { updateToolDiams, cutTabs, cutPolys, healPolys, color, shadowAt } = state;
let { process, stock } = settings;
let { camStockClipTo } = process;
if (state.isIndexed) {
throw 'trace op not supported with indexed stock';
}
// generate tracing offsets from chosen features
let zTop = workarea.top_z;
let zBottom = workarea.bottom_z;
let sliceOut = this.sliceOut = [];
let areas = op.areas[widget.id] || [];
let camTool = new Tool(settings, tool);
let toolDiam = camTool.fluteDiameter();
let toolOver = toolDiam * op.step;
let traceOffset = camTool.traceOffset()
let cutdir = ov_conv;
let polys = [];
let reContour = false;
let canRecontour = offset !== 'none' && down === 0;
let stockRect = stock.center && stock.x && stock.y ?
newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y) : undefined;
updateToolDiams(toolDiam);
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
}
for (let arr of areas) {
let poly = newPolygon().fromArray(arr);
POLY.setWinding([ poly ], cutdir, false);
polys.push(poly);
let zs = poly.points.map(p => p.z);
let min = Math.min(...zs);
let max = Math.max(...zs);
if (max - min > 0.0001 && canRecontour) {
reContour = true;
}
}
if (false) newSliceOut(0).output()
.setLayer("polys", {line: 0xaaaa00}, false)
.addPolys(polys);
function newSliceOut(z) {
let slice = newSlice(z);
addSlices(slice);
sliceOut.push(slice);
return slice;
}
function minZ(z) {
return zBottom ? Math.max(zBottom, z - thru) : z - thru;
}
function followZ(poly) {
if (op.dogbone) {
addDogbones(poly, toolDiam / 5, !op.revbone);
}
let z = poly.getZ();
let slice = newSliceOut(z);
slice.camTrace = { tool, rate, plunge };
if (tabs) {
slice.camLines = cutTabs(tabs, [poly], z);
} else {
slice.camLines = [ poly ];
}
if (camStockClipTo && stockRect) {
slice.camLines = cutPolys([stockRect], slice.camLines, z, true);
}
if (reContour) {
state.contourPolys(widget, slice.camLines);
}
slice.output()
.setLayer("trace follow", {line: color}, false)
.addPolys(slice.camLines)
}
function clearZnew(polys, z, down) {
if (down) {
// adjust step down to a value <= down that
// ends on the lowest z specified
let diff = zTop - z;
down = diff / Math.ceil(diff / down);
}
let zs = down ? base_util.lerp(zTop, z, down) : [ z ];
let zpro = 0, zinc = 1 / (polys.length * zs.length);
for (let poly of polys) {
// newPocket();
for (let z of zs) {
let clip = [], shadow;
shadow = shadowAt(z);
POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0);
if (op.outline) {
POLY.clearInner(clip);
}
if (clip.length === 0) {
continue;
}
let slice = newSliceOut(z);
let count = 999;
slice.camTrace = { tool, rate, plunge };
if (toolDiam) {
const offs = [ -toolDiam / 2, -toolOver ];
POLY.offset(clip, offs, {
count, outs: slice.camLines = [], flat:true, z, minArea: 0
});
} else {
// when engraving with a 0 width tip
slice.camLines = clip;
}
if (tabs) {
slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z);
} else {
slice.camLines = POLY.flatten(slice.camLines, null, true);
}
POLY.setWinding(slice.camLines, cutdir, false);
slice.output()
.setLayer("trace", {line: color}, false)
.addPolys(slice.camLines)
if (debug && shadow) slice.output()
.setLayer("trace shadow", {line: 0xff8811}, false)
.addPolys(shadow)
progress(zpro, "trace");
zpro += zinc;
addSlices(slice);
}
}
}
function similar(v1, v2, epsilon = 0.01) {
return Math.abs(v1-v2) <= epsilon;
}
function centerPoly(p1, p2) {
// follow poly with most points
if (p2.length > p1.length) {
let t = p1;
p1 = p2;
p2 = t;
}
let np = newPolygon().setOpen(true);
for (let p of p1.points) {
let q = p2.findClosestPointTo(p);
np.push(p.midPointTo3D(q.point));
}
return np;
}
function centerPolys(polys) {
// select open polys and sort by length
let ptst = polys.filter(p => p.isOpen()).sort((a,b) => b.perimeter() - a.perimeter());
if (ptst.length < 2) {
return polys;
}
let pt = newPoint(0,0,0);
// ensure polys are ordered with start point closest to 0,0
ptst.forEach(p => {
if (p.last().distTo2D(pt) < p.first().distTo2D(pt)) {
p.reverse();
}
});
let pout = polys.filter(p => p.isClosed());
outer: for (let i=0,l=ptst.length; i<l-1; i++) {
let p0 = ptst[i];
if (!p0) continue;
for (let j=i+1; j<l; j++) {
let p1 = ptst[j];
if (!p1) continue;
if (
similar(p0.perimeter(), p1.perimeter(), 0.1) &&
similar(p0.first().distTo2D(p1.first()), toolDiam) &&
similar(p0.last().distTo2D(p1.last()), toolDiam)
) {
pout.push(centerPoly(p0, p1));
ptst[i] = undefined;
ptst[j] = undefined;
continue outer;
}
}
}
pout.appendAll(ptst.filter(p => p));
return pout;
}
// connect selected segments if open and touching
polys = healPolys(polys);
// find center line for open polys spaced by tool diameter
polys = centerPolys(polys);
switch (op.mode) {
case "follow":
let routed = [];
poly2polyEmit(polys, newPoint(0,0,0), (poly, index, count, spoint) => {
routed.push(poly);
});
let output = [];
for (let poly of POLY.nest(routed)) {
let offdist = offset !== 'none' ? offover : 0;
if (!offdist)
switch (offset) {
case "outside": offdist = traceOffset; break;
case "inside": offdist = -traceOffset; break;
} else if (offset === "inside") {
offdist = -offdist;
}
if (offdist) {
let pnew = POLY.offset([poly], offdist, { minArea: 0, open: true });
if (pnew) {
poly = POLY.setZ(pnew, poly.getZ());
} else {
continue;
}
} else {
poly = [ poly ];
}
for (let pi of POLY.flatten(poly, [], true))
if (down) {
let zto = minZ(pi.getZ());
if (zThru && similar(zto,0)) {
zto -= zThru;
}
for (let z of base_util.lerp(zTop, zto, down)) {
output.push(pi.clone().setZ(z));
}
} else {
if (thru) {
pi.setZ(pi.getZ() - thru);
}
output.push(pi);
}
if (!down && op.merge) {
let nest = POLY.nest(output);
let union = POLY.union(nest, 0, true);
output = POLY.flatten(union, [], true);
}
}
for (let poly of output) {
followZ(poly);
}
break;
case "clear":
const zbo = widget.track.top - widget.track.box.d;
let zmap = {};
for (let poly of polys) {
let z = minZ(poly.minZ());
if (offover) {
let pnew = POLY.offset([poly], -offover, { minArea: 0, open: true });
if (pnew) {
poly = POLY.setZ(pnew, poly.getZ());
} else {
continue;
}
} else {
poly = [ poly ];
}
(zmap[z] = zmap[z] || []).appendAll(poly);
}
for (let [zv, polys] of Object.entries(zmap)) {
clearZnew(polys, parseFloat(zv), down);
}
}
}
prepare(ops, progress) {
let { op, state } = this;
let { settings } = state;
let { setTool, setSpindle } = ops;
setTool(op.tool, op.rate);
setSpindle(op.spindle);
for (let slice of this.sliceOut) {
ops.emitTrace(slice);
}
}
}
export { OpTrace };

View file

@ -0,0 +1,32 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js';
import { newSlice } from '../../core/slice.js';
import { Slicer } from './slicer.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 };

20
src/kiri/mode/cam/op.js Normal file
View file

@ -0,0 +1,20 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
export class CamOp {
constructor(state, op) {
this.state = state;
this.op = op
}
type() {
return this.op.type;
}
weight() {
return 1;
}
async slice() { }
prepare() { }
}

37
src/kiri/mode/cam/ops.js Normal file
View file

@ -0,0 +1,37 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { OpXRay } from './op-xray.js';
import { OpShadow } from './op-shadow.js';
import { OpLevel } from './op-level.js';
import { OpRough } from './op-rough.js';
import { OpOutline } from './op-outline.js';
import { OpContour } from './op-contour.js';
import { OpPocket } from './op-pocket.js';
import { OpLathe } from './op-lathe.js';
import { OpTrace } from './op-trace.js';
import { OpDrill } from './op-drill.js';
import { OpRegister } from './op-register.js';
import { OpLaserOn } from './op-laser-on.js';
import { OpLaserOff } from './op-laser-off.js';
import { OpGCode } from './op-gcode.js';
import { OpIndex } from './op-index.js';
import { CamOp } from './op.js';
export const ops = {
"xray": OpXRay,
"shadow": OpShadow,
"level": OpLevel,
"rough": OpRough,
"outline": OpOutline,
"contour": OpContour,
"pocket": OpPocket,
"lathe": OpLathe,
"trace": OpTrace,
"drill": OpDrill,
"register": OpRegister,
"laser on": OpLaserOn,
"laser off": OpLaserOff,
"gcode": OpGCode,
"index": OpIndex,
"flip": CamOp
};

1093
src/kiri/mode/cam/prepare.js Normal file

File diff suppressed because it is too large Load diff

808
src/kiri/mode/cam/slice.js Normal file
View file

@ -0,0 +1,808 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { base, util } from '../../../geo/base.js';
import { newLine } from '../../../geo/line.js';
import { newPoint } from '../../../geo/point.js';
import { newPolygon } from '../../../geo/polygon.js';
import { polygons as POLY } from '../../../geo/polygons.js';
import { setSliceTracker } from '../../core/slice.js';
import { ops as OPS } from './ops.js';
import { Tool } from './tool.js';
import { Slicer as cam_slicer } from './slicer.js';
/**
* DRIVER SLICE CONTRACT
*
* @param {Object} settings
* @param {Widget} widget
* @param {Function} output
*/
export async function cam_slice(settings, widget, onupdate, ondone) {
let proc = settings.process,
sliceAll = widget.slices = [],
camOps = widget.camops = [],
isIndexed = proc.camStockIndexed;
let stock, bounds, track,
camZTop, camZBottom, camZThru, wztop, ztOff, zbOff,
zBottom, zMin, zMax, zThru, zTop,
minToolDiam, maxToolDiam, dark, color, tabs, unsafe, units,
axisRotation, axisIndex,
part_size,
bottom_gap, bottom_part, bottom_stock, bottom_thru, bottom_z, bottom_cut,
top_stock, top_part, top_gap, top_z,
workarea;
// allow recomputing later if widget or settings changes
const var_compute = () => {
let { camStockX, camStockY, camStockZ, camStockOffset } = proc;
bounds = widget.getBoundingBox();
stock = camStockOffset ? {
x: bounds.dim.x + camStockX,
y: bounds.dim.y + camStockY,
z: bounds.dim.z + camStockZ,
} : {
x: camStockX,
y: camStockY,
z: camStockZ
}
track = widget.track;
({ camZTop, camZBottom, camZThru } = proc);
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;
zThru = camZThru;
zTop = zMax + ztOff;
minToolDiam = Infinity;
maxToolDiam = -Infinity;
dark = !!settings.controller.dark;
color = dark ? 0xbbbbbb : 0;
tabs = widget.anno.tab;
unsafe = proc.camExpertFast;
units = settings.controller.units === 'in' ? 25.4 : 1;
axisRotation = axisIndex = undefined;
part_size = bounds.dim;
bottom_gap = zbOff;
bottom_part = 0;
bottom_stock = -bottom_gap;
bottom_thru = zThru;
bottom_z = isIndexed ? zBottom : Math.max(
(camZBottom ? bottom_stock + camZBottom : bottom_part) - bottom_thru,
(camZBottom ? bottom_stock + camZBottom : bottom_stock - bottom_thru)
);
bottom_cut = Math.max(bottom_z, -zThru);
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, bottom_cut
}, 3);
return structuredClone(workarea);
};
// initial setup
var_compute();
if (tabs) {
// make tab polygons
tabs.forEach(tab => {
let zero = newPoint(0, 0, 0),
point = newPoint(tab.pos.x, tab.pos.y, tab.pos.z),
poly = newPolygon().centerRectangle(zero, tab.dim.x, tab.dim.y),
[rx, ry, rz, rw] = tab.rot,
m4 = new THREE.Matrix4().makeRotationFromQuaternion(
new THREE.Quaternion(rx, ry, rz, rw)
);
poly.points = poly.points
.map(p => new THREE.Vector3(p.x, p.y, p.z).applyMatrix4(m4))
.map(v => newPoint(v.x, v.y, v.z));
poly.move(point);
tab.poly = poly;
// tslice.output().setLayer("tabs", 0xff0000).addPoly(poly);
// sliceAll.push(tslice);
});
}
function error(msg) {
ondone(msg);
}
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) {
if (stock.x + 0.00001 < bounds.max.x - bounds.min.x) {
return error('stock X too small for part. resize stock or use offset stock');
}
if (stock.y + 0.00001 < bounds.max.y - bounds.min.y) {
return error('stock Y too small for part. resize stock or use offset stock');
}
if (stock.z + 0.00001 < bounds.max.z - bounds.min.z) {
return error('stock Z too small for part. resize stock or use offset stock');
}
}
if (zMin >= bounds.max.z) {
return error(`invalid z bottom ${(zMin / units).round(3)} >= bounds z max ${(zMax / units).round(3)}`);
}
let mark = Date.now();
let opList = [];
let opSum = 0;
let opTot = 0;
let shadows = {};
let slicer;
let state = {
settings,
widget,
bounds,
tabs,
cutTabs,
cutPolys,
contourPolys,
healPolys,
shadowAt,
slicer,
addSlices,
isIndexed,
setAxisIndex,
updateToolDiams,
updateSlicer,
computeShadows,
zBottom,
zThru,
ztOff,
zMax,
zTop,
unsafe,
color,
dark,
ops: opList
};
let tracker = setSliceTracker({ rotation: 0 });
function updateSlicer() {
slicer = state.slicer = new cam_slicer(widget);
}
async function computeShadows() {
shadows = {};
await new OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => {
// console.log('reshadow', progress.round(3));
});
}
function updateToolDiams(toolDiam) {
minToolDiam = Math.min(minToolDiam, toolDiam);
maxToolDiam = Math.max(maxToolDiam, toolDiam);
}
function shadowAt(z) {
let cached = shadows[z];
if (cached) {
return cached;
}
// find closest shadow above and use to speed up delta shadow gen
let minZabove;
let zover = Object.keys(shadows).map(v => parseFloat(v)).filter(v => v > z);
for (let zkey of zover) {
if (minZabove && zkey < minZabove) {
minZabove = zkey;
} else {
minZabove = zkey;
}
}
let shadow = computeShadowAt(widget, z, minZabove);
if (minZabove) {
// const merge = shadow.length;
// const plus = shadows[minZabove].length;
// const mark = Date.now();
shadow = POLY.union([...shadow, ...shadows[minZabove]], 0.001, true);
// console.log({merge, plus, equals: shadow.length, time: Date.now() - mark});
}
return shadows[z] = POLY.setZ(shadow, z);
}
function setAxisIndex(degrees = 0, absolute = true) {
axisIndex = absolute ? degrees : axisIndex + degrees;
axisRotation = (Math.PI / 180) * axisIndex;
widget.setAxisIndex(isIndexed ? -axisIndex : 0);
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) {
// 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(new OPS.xray(state, { type: "xray" }));
}
let activeOps = proc.ops.filter(op => !op.disabled);
// silently preface op list with OpShadow
if (isIndexed) {
if (activeOps.length === 0 || activeOps[0].type !== 'index')
opList.push(new OPS.index(state, { type: "index", index: 0 }));
} else {
opList.push(new OPS.shadow(state, { type: "shadow", silent: true }));
}
// determing # of steps and step weighting for progress bar
for (let op of activeOps) {
if (op.type === '|') {
break;
}
let opfn = OPS[op.type];
if (opfn) {
let opin = new opfn(state, op);
opList.push(opin);
opTot += opin.weight();
}
}
// call slice() function on all ops in order
setAxisIndex();
updateSlicer();
for (let op of opList) {
let weight = op.weight();
// apply operation override vars
let workover = var_compute();
let valz = op.op;
if (valz.ov_topz) {
workover.top_z = isIndexed ? valz.ov_topz : bottom_stock + valz.ov_topz;
}
if (valz.ov_botz) {
workover.bottom_z = isIndexed ? valz.ov_botz : bottom_stock + valz.ov_botz;
workover.bottom_cut = Math.max(workover.bottom_z, -zThru);
}
// state.workarea = workover;
Object.assign(state, {
zBottom,
zThru,
ztOff,
zMax,
zTop,
workarea: workover
});
// console.log({
// op,
// workover,
// bounds: structuredClone(bounds),
// stock: structuredClone(stock)
// });
await op.slice((progress, message) => {
onupdate((opSum + (progress * weight)) / opTot, message || op.type());
});
// 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);
// used in printSetup()
// used in CAM.prepare.getZClearPath()
// add tabs to terrain tops so moves avoid them
if (tabs) {
state.terrain.forEach(slab => {
tabs.forEach(tab => {
if (tab.pos.z + tab.dim.z / 2 >= slab.z) {
let all = [...slab.tops, tab.poly];
slab.tops = POLY.union(all, 0, true);
// slab.slice.output()
// .setLayer("debug-tabs", {line: 0x880088, thin: true })
// .addPolys(POLY.setZ(slab.tops.clone(true), slab.z), { thin: true });
}
});
});
}
// add shadow perimeter to terrain to catch outside moves off part
let tabpoly = tabs ? tabs.map(tab => tab.poly) : [];
let allpoly = POLY.union([...state.shadow.base, ...tabpoly], 0, true);
let shadowOff = maxToolDiam < 0 ? allpoly :
POLY.offset(allpoly, [minToolDiam / 2, maxToolDiam / 2], { count: 2, flat: true, minArea: 0 });
state.terrain.forEach(level => level.tops.appendAll(shadowOff));
widget.terrain = state.skipTerrain ? null : state.terrain;
widget.minToolDiam = minToolDiam;
widget.maxToolDiam = maxToolDiam;
ondone();
};
export function addDogbones(poly, dist, reverse) {
if (Array.isArray(poly)) {
return poly.forEach(p => addDogbones(p, dist));
}
let open = poly.open;
let isCW = poly.isClockwise();
if (reverse || poly.parent) isCW = !isCW;
let oldpts = poly.points.slice();
let lastpt = oldpts[oldpts.length - 1];
let lastsl = lastpt.slopeTo(oldpts[0]).toUnit();
let length = oldpts.length + (open ? 0 : 1);
let newpts = [];
for (let i = 0; i < length; i++) {
let nextpt = oldpts[i % oldpts.length];
let nextsl = lastpt.slopeTo(nextpt).toUnit();
let adiff = lastsl.angleDiff(nextsl, true);
let bdiff = ((adiff < 0 ? (180 - adiff) : (180 + adiff)) / 2) + 180;
if (!open || (i > 1 && i < length)) {
if (isCW && adiff > 45) {
let newa = base.newSlopeFromAngle(lastsl.angle + bdiff);
newpts.push(lastpt.projectOnSlope(newa, dist));
newpts.push(lastpt.clone());
} else if (!isCW && adiff < -45) {
let newa = base.newSlopeFromAngle(lastsl.angle - bdiff);
newpts.push(lastpt.projectOnSlope(newa, dist));
newpts.push(lastpt.clone());
}
}
lastsl = nextsl;
lastpt = nextpt;
if (i < oldpts.length) {
newpts.push(nextpt);
}
}
poly.points = newpts;
if (poly.inner) {
addDogbones(poly.inner, dist, true);
}
};
export async function traces(settings, widget, single) {
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;
};
/**
* 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, mark, 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,
{ camZTop, camZBottom, camZThru } = 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;
// console.log("slices",slices)
for (let [i, slice] of slices.entries()) {
for (let top of slice.tops) {
// console.log("slicing",slice.z,top)
slice.shadow = computeShadowAt(widget, slice.z, 0)
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;
// console.log("center",center)
if (poly.circularity() < 0.985) {
// 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, z, inter) {
tabs = tabs.filter(tab => z < tab.pos.z + tab.dim.z / 2).map(tab => tab.off).flat();
return cutPolys(tabs, offset, z, false);
}
function cutPolys(polys, offset, z, inter) {
let noff = [];
offset.forEach(op => noff.appendAll(op.cut(POLY.union(polys, 0, true), inter)));
return healPolys(noff);
}
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;
}
}
}
function healPolys(noff) {
for (let p of noff) {
if (p.appearsClosed()) {
p.points.pop();
p.setClosed();
}
}
if (noff.length > 1) {
let heal = 0;
// heal/rejoin open segments that share endpoints
outer: for (; ; heal++) {
let ntmp = noff, tlen = ntmp.length;
for (let i = 0; i < tlen; i++) {
let s1 = ntmp[i];
if (!s1) continue;
for (let j = i + 1; j < tlen; j++) {
let s2 = ntmp[j];
if (!s2) continue;
// require polys at same Z to heal
if (Math.abs(s1.getZ() - s2.getZ()) > 0.01) {
continue;
}
if (!(s1.open && s2.open)) continue;
if (s1.last().isMergable2D(s2.first())) {
s1.addPoints(s2.points.slice(1));
ntmp[j] = null;
continue outer;
}
if (s2.last().isMergable2D(s1.first())) {
s2.addPoints(s1.points.slice(1));
ntmp[i] = null;
continue outer;
}
if (s1.first().isMergable2D(s2.first())) {
s1.reverse();
s1.addPoints(s2.points.slice(1));
ntmp[j] = null;
continue outer;
}
if (s1.last().isMergable2D(s2.last())) {
s2.reverse();
s1.addPoints(s2.points.slice(1));
ntmp[j] = null;
continue outer;
}
}
}
break;
}
if (heal > 0) {
// cull nulls
noff = noff.filter(o => o);
}
// close poly if head meets tail
for (let poly of noff) {
if (poly.open && poly.first().isMergable2D(poly.last())) {
poly.points.pop();
poly.open = false;
}
}
}
return noff;
}
// union triangles > z (opt cap < ztop) into polygon(s)
export function computeShadowAt(widget, z, ztop) {
const geo = widget.cache.geo;
const length = geo.length;
// cache faces with normals up
if (!widget.cache.shadow) {
const faces = [];
for (let i = 0, ip = 0; i < length; i += 3) {
const a = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
const b = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
const c = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
const n = THREE.computeFaceNormal(a, b, c);
if (n.z > 0.001) {
faces.push(a, b, c);
// faces.push(newPoint(...a), newPoint(...b), newPoint(...c));
}
}
widget.cache.shadow = faces;
}
const found = [];
const faces = widget.cache.shadow;
const { checkOverUnderOn, intersectPoints } = cam_slicer;
for (let i = 0; i < faces.length;) {
const a = faces[i++];
const b = faces[i++];
const c = faces[i++];
let where = undefined;
if (ztop && a.z > ztop && b.z > ztop && c.z > ztop) {
// skip faces over top threshold
continue;
}
if (a.z < z && b.z < z && c.z < z) {
// skip faces under threshold
continue;
} else if (a.z > z && b.z > z && c.z > z) {
found.push([a, b, c]);
} else {
// check faces straddling threshold
const where = { under: [], over: [], on: [] };
checkOverUnderOn(newPoint(a.x, a.y, a.z), z, where);
checkOverUnderOn(newPoint(b.x, b.y, b.z), z, where);
checkOverUnderOn(newPoint(c.x, c.y, c.z), z, where);
if (where.on.length === 0 && (where.over.length === 2 || where.under.length === 2)) {
// compute two point intersections and construct line
let line = intersectPoints(where.over, where.under, z);
if (line.length === 2) {
if (where.over.length === 2) {
found.push([where.over[1], line[0], line[1]]);
found.push([where.over[0], where.over[1], line[0]]);
} else {
found.push([where.over[0], line[0], line[1]]);
}
} else {
console.log({ msg: "invalid ips", line: line, where: where });
}
}
}
}
let polys = found.map(a => {
return newPolygon()
.add(a[0].x, a[0].y, a[0].z)
.add(a[1].x, a[1].y, a[1].z)
.add(a[2].x, a[2].y, a[2].z);
});
polys = POLY.union(polys, 0.001, true);
return polys;
}

479
src/kiri/mode/cam/slicer.js Normal file
View file

@ -0,0 +1,479 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { base, util } from '../../../geo/base.js';
import { decode, decodePointArray } from '../../core/codec.js';
import { newLine, newOrderedLine } 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 { slicer } from '../../../geo/slicer.js';
const { sliceConnect, sliceDedup } = slicer;
const { config } = base;
const timing = false;
const zDecimal = 3;
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 (p1.z === p2.z && p2.z === p3.z) {
// detect zFlat faces to avoid slicing directly on them
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).forEach((rec, i) => {
rec.tops = rec.polys;
rec.slice = newSlice(rec.z).addTops(rec.tops);
if (opt.each) {
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 = sliceDedup(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)));
}
}
/**
* given a point, append to the correct
* 'where' objec tarray (on, over or under)
*
* @param {Point} p
* @param {number} z offset
* @param {Obejct} where
*/
function checkOverUnderOn(p, z, where) {
let delta = p.z - z;
if (Math.abs(delta) < config.precision_slice_z) { // on
where.on.push(p);
} else if (delta < 0) { // under
where.under.push(p);
} else { // over
where.over.push(p);
}
}
/**
* Given a point over and under a z offset, calculate
* and return the intersection point on that z plane
*
* @param {Point} over
* @param {Point} under
* @param {number} z offset
* @returns {Point} intersection point
*/
function intersectPoints(over, under, z) {
let ip = [];
for (let i = 0; i < over.length; i++) {
for (let j = 0; j < under.length; j++) {
ip.push(over[i].intersectZ(under[j], z));
}
}
return ip;
}
/**
* Ensure points are unique with a cache/key algorithm
*/
function getCachedPoint(phash, p) {
let cached = phash[p.key];
if (!cached) {
phash[p.key] = p;
return p;
}
return cached;
}
/**
* Given two points and hints about their edges,
* return a new Line object with points sorted
* lexicographically by key. This allows for future
* line de-duplication and joins.
*
* @param {Object} phash
* @param {Point} p1
* @param {Point} p2
* @param {boolean} [coplanar]
* @param {boolean} [edge]
* @returns {Line}
*/
function makeZLine(phash, p1, p2, coplanar, edge) {
p1 = getCachedPoint(phash, p1.clone());
p2 = getCachedPoint(phash, p2.clone());
let line = newOrderedLine(p1, p2);
line.coplanar = coplanar || false;
line.edge = edge || false;
return line;
}
Slicer.checkOverUnderOn = checkOverUnderOn;
Slicer.intersectPoints = intersectPoints;

View file

@ -0,0 +1,319 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { base } from '../../../geo/base.js';
import { newPoint } from '../../../geo/point.js';
import { newOrderedLine } from '../../../geo/line.js';
const { config } = base;
/** Slicer used in Topo3 and Topo4 to find contour lines */
export class Slicer {
static intersectPoints = intersectPoints;
static checkOverUnderOn = checkOverUnderOn;
static removeDuplicateLines = removeDuplicateLines;
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);
}
}
/**
* given a point, append to the correct
* 'where' objec tarray (on, over or under)
*
* @param {Point} p
* @param {number} z offset
* @param {Obejct} where
*/
function checkOverUnderOn(p, z, where) {
let delta = p.z - z;
if (Math.abs(delta) < config.precision_slice_z) { // on
where.on.push(p);
} else if (delta < 0) { // under
where.under.push(p);
} else { // over
where.over.push(p);
}
}
/**
* Given a point over and under a z offset, calculate
* and return the intersection point on that z plane
*
* @param {Point} over
* @param {Point} under
* @param {number} z offset
* @returns {Point} intersection point
*/
function intersectPoints(over, under, z) {
let ip = [];
for (let i = 0; i < over.length; i++) {
for (let j = 0; j < under.length; j++) {
ip.push(over[i].intersectZ(under[j], z));
}
}
return ip;
}
/**
* Ensure points are unique with a cache/key algorithm
*/
function getCachedPoint(phash, p) {
let cached = phash[p.key];
if (!cached) {
phash[p.key] = p;
return p;
}
return cached;
}
/**
* Given two points and hints about their edges,
* return a new Line object with points sorted
* lexicographically by key. This allows for future
* line de-duplication and joins.
*
* @param {Object} phash
* @param {Point} p1
* @param {Point} p2
* @param {boolean} [coplanar]
* @param {boolean} [edge]
* @returns {Line}
*/
function makeZLine(phash, p1, p2, coplanar, edge) {
p1 = getCachedPoint(phash, p1);
p2 = getCachedPoint(phash, p2);
let line = newOrderedLine(p1,p2);
line.coplanar = coplanar || false;
line.edge = edge || false;
return line;
}
/**
* eliminate duplicate lines and interior-only lines (coplanar)
*
* lines are sorted using lexicographic point keys such that
* they are comparable even if their points are reversed. hinting
* for deletion, co-planar and suspect shared edge is detectable at
* this time.
*
* @param {Line[]} lines
* @returns {Line[]}
*/
function removeDuplicateLines(lines, debug) {
let output = [],
tmplines = [],
points = [],
pmap = {};
function cachePoint(p) {
let cp = pmap[p.key];
if (cp) return cp;
points.push(p);
pmap[p.key] = p;
return p;
}
function addLinesToPoint(point, line) {
cachePoint(point);
if (!point.group) point.group = [ line ];
else point.group.push(line);
}
// mark duplicates for deletion preserving edges
lines.sort(function (l1, l2) {
if (l1.key === l2.key) {
l1.del = !l1.edge;
l2.del = !l2.edge;
if (debug && (l1.del || l2.del)) {
console.log('dup', l1, l2);
}
return 0;
}
return l1.key < l2.key ? -1 : 1;
});
// associate points with their lines, cull deleted
lines.forEach(function(line) {
if (!line.del) {
tmplines.push(line);
addLinesToPoint(line.p1, line);
addLinesToPoint(line.p2, line);
}
});
// merge collinear lines
points.forEach(function(point) {
if (point.group.length != 2) return;
let l1 = point.group[0],
l2 = point.group[1];
if (l1.isCollinear(l2)) {
l1.del = true;
l2.del = true;
// find new endpoints that are not shared point
let p1 = l1.p1 != point ? l1.p1 : l1.p2,
p2 = l2.p1 != point ? l2.p1 : l2.p2,
newline = newOrderedLine(p1,p2);
// remove deleted lines from associated points
p1.group.remove(l1);
p1.group.remove(l2);
p2.group.remove(l1);
p2.group.remove(l2);
// associate new line with points
p1.group.push(newline);
p2.group.push(newline);
// add new line to lines array
newline.edge = l1.edge || l2.edge;
tmplines.push(newline);
}
});
// mark duplicates for deletion
// but preserve one if it's an edge
tmplines.sort(function (l1, l2) {
if (l1.key === l2.key) {
l1.del = true;
l2.del = !l2.edge;
return 0;
}
return l1.key < l2.key ? -1 : 1;
});
// create new line array culling deleted
tmplines.forEach(function(line) {
if (!line.del) {
output.push(line);
line.p1.group = null;
line.p2.group = null;
}
});
return output;
}

208
src/kiri/mode/cam/tool.js Normal file
View file

@ -0,0 +1,208 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
const HPI = Math.PI/2;
const RAD2DEG = 180/Math.PI;
class Tool {
constructor(settings, id, number) {
if (number >= 0) {
this.tool = settings.tools.filter(tool => tool.number == number)[0];
} else {
this.tool = settings.tools.filter(tool => tool.id == id)[0];
}
if (!this.tool) {
this.tool = Object.assign({}, settings.tools[0]);
this.tool.number = number >= 0 ? number : this.tool.number;
this.tool.id = id >= 0 ? id : this.tool.id;
}
}
getID() {
return this.tool.id;
}
getName() {
return this.tool.name;
}
getType() {
return this.tool.type;
}
getNumber() {
return this.tool.number;
}
isMetric() {
return this.tool.metric;
}
unitScale() {
return this.isMetric() ? 1 : 25.4;
}
fluteLength() {
return this.unitScale() * this.tool.flute_len;
}
fluteDiameter() {
return this.unitScale() * this.tool.flute_diam;
}
tipDiameter() {
return this.unitScale() * this.tool.taper_tip;
}
maxDiameter() {
return Math.max(this.fluteDiameter(), this.tipDiameter(), this.shaftDiameter());
}
traceOffset() {
return (this.hasTaper() ? this.tipDiameter() : this.fluteDiameter()) / 2;
}
contourOffset(step) {
const diam = Math.min(this.hasTaper() ? this.tipDiameter() : this.fluteDiameter());
return diam ? diam * step : step;
}
setTaperLengthFromAngle(angle) {
const rad = (this.flute_diam - this.taper_tip) / 2;
return this.flute_len = calcTaperLength(rad, angle);
}
getTaperAngle() {
return calcTaperAngle((this.flute_diam - this.taper_tip) / 2, this.flute_len);
}
shaftLength() {
return this.unitScale() * this.tool.shaft_len;
}
/**
* returns the length of the drill tip as a function of the flute diameter,
* given a 118 deg point angle. the result is in workspace units.
*
* @return {number} length of the drill tip.
*/
drillTipLength() {
const drillAngleRad = 140 * Math.PI / 180;
return 0.5 * this.fluteDiameter() * Math.sin(drillAngleRad);
}
shaftDiameter() {
return this.unitScale() * this.tool.shaft_diam;
}
isBallMill() {
return this.tool.type === "ballmill";
}
isTaperMill() {
return this.tool.type === "tapermill";
}
isDrill(){
return this.tool.type === "drill";
}
hasTaper() {
return this.isTaperMill();
}
/**
* generate tool profile with resolution number of pixels. the result is a
* Float32Array stored in `this.profile` and `this.profileDim` containing
* dimensions of the profile in pixels and shared array buffer size.
*
* @param {number} resolution - number of pixels for the tool profile
*
* @return {Tool} this
*/
generateProfile(resolution) {
// generate tool profile
let ball = this.isBallMill(),
taper = this.hasTaper(),
drill = this.isDrill(),
tip_diameter = this.tipDiameter(),
drill_tip_length = this.drillTipLength(),
shaft_offset = this.fluteLength(),
flute_diameter = this.fluteDiameter(),
shaft_diameter = Math.max(flute_diameter, this.shaftDiameter()),
max_diameter = Math.max(flute_diameter, shaft_diameter),
shaft_pix_float = max_diameter / resolution,
shaft_pix_int = Math.round(shaft_pix_float),
shaft_radius_pix_float = shaft_pix_float / 2,
flute_radius = flute_diameter / 2,
flute_pix_float = flute_diameter / resolution,
flute_radius_pix_float = flute_pix_float / 2,
tip_pix_float = tip_diameter / resolution,
tip_radius_pix_float = tip_pix_float / 2,
tip_max_radius_offset = flute_radius_pix_float - tip_radius_pix_float,
profile_pix_iter = shaft_pix_int + (1 - shaft_pix_int % 2),
toolCenter = (shaft_pix_int - (shaft_pix_int % 2)) / 2,
toolOffset = [],
larger_shaft = shaft_diameter - flute_diameter > 0.001,
rpixsq = flute_radius_pix_float * flute_radius_pix_float,
maxo = -Infinity;
// for each point in tool profile, check inside radius
for (let x = 0; x < profile_pix_iter; x++) {
for (let y = 0; y < profile_pix_iter; y++) {
let dx = x - toolCenter,
dy = y - toolCenter,
dist_from_center = Math.sqrt(dx * dx + dy * dy);
if (dist_from_center <= flute_radius_pix_float) { // if xy point inside flute radius
maxo = Math.max(maxo, dx, dy);
// flute offset points
let z_offset = 0;
if (ball) {
let rd = dist_from_center * dist_from_center;
z_offset = Math.sqrt(rpixsq - rd) * resolution - flute_radius;
// z_offset = (1 - Math.cos((dist_from_center / flute_radius_pix_float) * HPI)) * -flute_radius;
} else if (taper && dist_from_center >= tip_radius_pix_float) {// if tapered and not in the flat tip radius
z_offset = ((dist_from_center - tip_radius_pix_float) / tip_max_radius_offset) * -shaft_offset;
}else if(drill){
z_offset = -dist_from_center /45;
}
toolOffset.push(dx, dy, z_offset);
} else if (shaft_offset && larger_shaft && dist_from_center <= shaft_radius_pix_float) {
// shaft offset points
toolOffset.push(dx, dy, -shaft_offset);
}
}
}
// convert to shared array for use with minions
const profile = new Float32Array(new SharedArrayBuffer(toolOffset.length * 4));
profile.set(toolOffset);
this.profile = profile;
this.profileDim = {
size: shaft_diameter,
pix: profile_pix_iter + 2,
maxo
};
return this;
}
}
function calcTaperAngle(rad, len) {
return (Math.atan(rad / len) * RAD2DEG);
};
function calcTaperLength(rad, angle) {
return (rad / Math.tan(angle));
};
function getToolDiameter(settings, id) {
return new Tool(settings, id).fluteDiameter();
};
export {
Tool,
calcTaperAngle,
calcTaperLength,
getToolDiameter
};

344
src/kiri/mode/cam/tools.js Normal file
View file

@ -0,0 +1,344 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { $ } from '../../../moto/webui.js';
import { api } from '../../core/api.js';
import { consts } from '../../core/consts.js';
import { settings as setconf } from '../../core/settings.js';
const DEG2RAD = Math.PI / 180;
let { MODES } = consts,
DOC = document,
selectedTool = null,
editTools = null,
maxTool = 0,
toolNames = ['endmill','ballmill','tapermill','drill'];
function settings() {
return api.conf.get();
}
function renderTools() {
api.ui.toolSelect.innerHTML = '';
maxTool = 0;
editTools.forEach(function(tool, index) {
maxTool = Math.max(maxTool, tool.number);
tool.order = index;
let opt = DOC.createElement('option');
opt.appendChild(DOC.createTextNode(tool.name));
opt.onclick = function() { selectTool(tool) };
api.ui.toolSelect.appendChild(opt);
});
}
function selectTool(tool) {
const { ui } = api;
selectedTool = tool;
ui.toolName.value = tool.name;
ui.toolNum.value = tool.number;
ui.toolFluteDiam.value = tool.flute_diam;
ui.toolFluteLen.value = tool.flute_len;
ui.toolShaftDiam.value = tool.shaft_diam;
ui.toolShaftLen.value = tool.shaft_len;
ui.toolTaperTip.value = tool.taper_tip || 0;
ui.toolMetric.checked = tool.metric;
ui.toolType.selectedIndex = toolNames.indexOf(tool.type);
if (tool === 'tapermill') {
ui.toolTaperAngle.value = kiri.driver.CAM.calcTaperAngle(
(tool.flute_diam - tool.taper_tip) / 2, tool.flute_len
).round(1);
} else if(tool === 'drill'){
ui.toolTaperAngle.value = 118;
} else {
ui.toolTaperAngle.value = 0;
}
renderTool(tool);
}
function otag(o) {
if (Array.isArray(o)) {
let out = []
o.forEach(oe => out.push(otag(oe)));
return out.join('');
}
let tags = [];
Object.keys(o).forEach(key => {
let val = o[key];
let att = [];
Object.keys(val).forEach(tk => {
let tv = val[tk];
att.push(`${tk.replace(/_/g,'-')}="${tv}"`);
});
tags.push(`<${key} ${att.join(' ')}></${key}>`);
});
return tags.join('');
}
function renderTool(tool) {
const { ui } = api;
let type = selectedTool.type;
let taper = type=== 'tapermill'
let drill = type === 'drill'
const drillAngleRad = 140 * Math.PI / 180
ui.toolTaperAngle.disabled = taper ? undefined : 'true';
ui.toolTaperTip.disabled = taper ? undefined : 'true';
$('tool-view').innerHTML = '<svg id="tool-svg" width="100%" height="100%"></svg>';
setTimeout(() => {
let svg = $('tool-svg'),
pad = 10,
dim = { w: svg.clientWidth, h: svg.clientHeight },
max = { w: dim.w - pad * 2, h: dim.h - pad * 2},
off = { x: pad, y: pad },
isBall = type === "ballmill",
shaft_fill = "#cccccc",
flute_fill = "#dddddd",
stroke = "#777777",
stroke_width = 3,
stroke_thin = stroke_width / 2,
shaft = tool.shaft_len || 1,
flute = tool.flute_len || 1,
drillTip = drill ? 0.5 * tool.flute_diam * Math.sin(drillAngleRad) : 0,
total_len = shaft + flute+drillTip,
units = dim.h / total_len,
shaft_len = (shaft / total_len) * max.h,
flute_len = (flute / total_len) * max.h,
drill_tip_len = (drillTip / total_len) * max.h,
shaft_diam = tool.shaft_diam * units,
flute_diam = tool.flute_diam * units,
// total_wid = Math.max(flute_diam, shaft_diam),
shaft_off = (max.w - shaft_diam) / 2,
flute_off = (max.w - flute_diam) / 2,
taper_off = (max.w - (tool.taper_tip || 0) * units) / 2,
parts = [
// shaft rectangle
{ rect: {
x: off.x + shaft_off,
y: off.y,
width: max.w - shaft_off * 2,
height: shaft_len,
fill: shaft_fill,
stroke_width,
stroke
} }
];
if (taper) {
let yoff = off.y + shaft_len;
// let mid = dim.w / 2;
parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[
`M ${off.x + flute_off} ${yoff}`,
`L ${off.x + taper_off} ${yoff + flute_len}`,
`L ${dim.w - off.x - taper_off} ${yoff + flute_len}`,
`L ${dim.w - off.x - flute_off} ${yoff}`,
`z`
].join('\n')}});
} else if(drill){
const x1 = off.x + flute_off,
y1 = off.y + shaft_len,
x2 = dim.w - off.x - flute_off,
y2 = y1 + flute_len,
xMid = dim.w / 2
parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[
`M ${x1} ${y1}`, //move to top left
`L ${x1} ${y2}`, //line to bottom left
`L ${xMid} ${y2+drill_tip_len}`, //line to bottom mid point
`L ${x2} ${y2}`, //line to bottom right
`L ${x2} ${y1}`, //line to top right
`z`
].join('\n')}});
//add drill flute lines
parts.push({ line: {
x1, y1, x2, y2: (y1 + y2) / 2,
stroke, stroke_width: stroke_thin
} });
parts.push({ line: {
x1, y1: (y1 + y2) / 2, x2, y2,
stroke, stroke_width: stroke_thin
} });
} else {
let fl = isBall ? flute_len - flute_diam/2 : flute_len;
let x1 = off.x + flute_off;
let y1 = off.y + shaft_len;
let x2 = x1 + max.w - flute_off * 2;
let y2 = y1 + fl;
// flute rectangle
parts.push({ rect: {
x: off.x + flute_off,
y: off.y + shaft_len,
width: max.w - flute_off * 2,
height: fl,
fill: flute_fill,
stroke_width,
stroke,
} });
// hatch "fill" flute
parts.push({ line: { x1, y1, x2, y2, stroke, stroke_width: stroke_thin } });
parts.push({ line: {
x1: (x1 + x2) / 2, y1, x2, y2: (y1 + y2) / 2,
stroke, stroke_width: stroke_thin
} });
parts.push({ line: {
x1, y1: (y1 + y2) / 2, x2: (x1 + x2) / 2, y2,
stroke, stroke_width: stroke_thin
} });
}
if (isBall) {
let rad = (max.w - flute_off * 2) / 2;
let xend = dim.w - off.x - flute_off;
let yoff = off.y + shaft_len + flute_len + stroke_width/2 - flute_diam/2;
parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[
`M ${off.x + flute_off} ${yoff}`,
`A ${rad} ${rad} 0 0 0 ${xend} ${yoff}`,
// `L ${off.x + flute_off} ${yoff}`
].join('\n')}})
}
svg.innerHTML = otag(parts);
}, 10);
}
export function updateTool(ev) {
const { ui } = api;
selectedTool.name = ui.toolName.value;
selectedTool.number = parseInt(ui.toolNum.value);
selectedTool.flute_diam = parseFloat(ui.toolFluteDiam.value);
selectedTool.flute_len = parseFloat(ui.toolFluteLen.value);
selectedTool.shaft_diam = parseFloat(ui.toolShaftDiam.value);
selectedTool.shaft_len = parseFloat(ui.toolShaftLen.value);
selectedTool.taper_tip = parseFloat(ui.toolTaperTip.value);
selectedTool.metric = ui.toolMetric.checked;
selectedTool.type = toolNames[ui.toolType.selectedIndex];
if (selectedTool.type === 'tapermill') {
const CAM = kiri.driver.CAM;
const rad = (selectedTool.flute_diam - selectedTool.taper_tip) / 2;
if (ev && ev.target === ui.toolTaperAngle) {
const angle = parseFloat(ev.target.value);
const len = CAM.calcTaperLength(rad, angle * DEG2RAD);
selectedTool.flute_len = len;
ui.toolTaperAngle.value = angle.round(1);
ui.toolFluteLen.value = selectedTool.flute_len.round(4);
} else {
ui.toolTaperAngle.value = CAM.calcTaperAngle(rad, selectedTool.flute_len).round(1);
}
} else {
ui.toolTaperAngle.value = 0;
}
renderTools();
ui.toolSelect.selectedIndex = selectedTool.order;
setToolChanged(true);
renderTool(selectedTool);
}
function setToolChanged(changed) {
editTools.changed = changed;
api.ui.toolsSave.disabled = !changed;
}
export function showTools() {
if (api.mode.get_id() !== MODES.CAM) return;
setconf.sync.get().then(_showTools);
}
function _showTools() {
const { ui } = api;
editTools = settings().tools.slice().sort((a,b) => {
return a.name > b.name ? 1 : -1;
});
setToolChanged(false);
ui.toolsClose.onclick = function() {
if (editTools.changed && !confirm("abandon changes?")) return;
api.dialog.hide();
};
ui.toolAdd.onclick = function() {
let metric = settings().controller.units === 'mm';
editTools.push(Object.assign({
id: Date.now(),
number: maxTool + 1,
name: "new tool",
type: "endmill",
taper_tip: 0,
metric
}, metric ? {
shaft_diam: 2,
shaft_len: 15,
flute_diam: 2,
flute_len: 20,
} : {
shaft_diam: 0.25,
shaft_len: 1.5,
flute_diam: 0.25,
flute_len: 2,
}));
setToolChanged(true);
renderTools();
ui.toolSelect.selectedIndex = editTools.length-1;
selectTool(editTools[editTools.length-1]);
};
ui.toolCopy.onclick = function() {
let clone = Object.assign({}, selectedTool);
let { name } = clone;
let split = name.split(' ');
let endv = parseInt(split.pop());
if (endv) {
name = split.join(' ') + ' ' + (endv + 1);
} else {
name = `${name} 2`;
}
clone.id = Date.now();
clone.number = maxTool + 1;
clone.name = name;
editTools.push(clone);
setToolChanged(true);
renderTools();
ui.toolSelect.selectedIndex = editTools.length-1;
selectTool(editTools[editTools.length-1]);
};
ui.toolDelete.onclick = function() {
editTools.remove(selectedTool);
setToolChanged(true);
renderTools();
};
ui.toolsSave.onclick = function() {
if (selectedTool) updateTool();
settings().tools = editTools.sort((a,b) => {
return a.name < b.name ? -1 : 1;
});
setToolChanged(false);
api.conf.save();
api.conf.update_fields();
api.event.settings();
setconf.sync.put();
};
ui.toolsImport.onclick = (ev) => api.event.import(ev);
ui.toolsExport.onclick = () => {
api.uc.prompt("Export Tools Filename", "tools").then(name => {
if (!name) {
return;
}
const record = {
version: kiri.version,
tools: api.conf.get().tools,
time: Date.now()
};
api.util.download(api.util.b64enc(record), `${name}.km`);
});
};
renderTools();
if (editTools.length > 0) {
selectTool(editTools[0]);
ui.toolSelect.selectedIndex = 0;
} else {
ui.toolAdd.onclick();
}
api.dialog.show('tools');
ui.toolSelect.focus();
}
export const tools = {
updateTool,
showTools
};

815
src/kiri/mode/cam/topo3.js Normal file
View file

@ -0,0 +1,815 @@
/** 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 { newSlice } from '../../core/slice.js';
import { Slicer as topo_slicer } from './slicer_topo.js';
import { polygons as POLY } from '../../../geo/polygons.js';
import { Tool } from './tool.js';
const RAD2DEG = 180 / Math.PI;
const DEG2RAD = Math.PI / 180;
class Topo {
constructor() { }
async generate(opt = {}) {
let { state, contour, onupdate, ondone } = opt;
let { widget, settings, tshadow, center, tabs, color } = opt.state;
let { controller, process } = settings,
animesh = parseInt(controller.animesh || 100) * 2500,
axis = contour.axis.toLowerCase(),
contourX = axis === "x",
contourY = axis === "y",
bounds = widget.getBoundingBox().clone(),
tolerance = contour.tolerance,
flatness = contour.flatness || (tolerance / 100),
shadow = tshadow,
minX = bounds.min.x,
maxX = bounds.max.x,
minY = bounds.min.y,
maxY = bounds.max.y,
zBottom = contour.bottom ? state.zBottom : 0,
zMin = Math.max(bounds.min.z, zBottom) + 0.0001,
boundsX = maxX - minX,
boundsY = maxY - minY,
inside = contour.inside,
density = 1 + (contour.reduction || 0),
resolution = tolerance ? tolerance : 1/Math.sqrt(animesh/(boundsX * boundsY)),
tool = new Tool(settings, contour.tool),
toolOffset = tool.generateProfile(resolution).profile,
toolDiameter = tool.fluteDiameter(),
toolStep = toolDiameter * contour.step, //tool.contourOffset(contour.step),
leave = contour.leave || 0,
maxangle = contour.angle,
curvesOnly = contour.curves,
bridge = contour.bridging || 0,
// R2A = 180 / Math.PI,
stepsX = Math.ceil(boundsX / resolution),
stepsY = Math.ceil(boundsY / resolution),
widtopo = widget.topo,
topoCache = widtopo
&& widtopo.resolution === resolution
&& widtopo.diameter === toolDiameter
? widtopo : undefined,
topo = widget.topo = topoCache || {
data: new Float32Array(new SharedArrayBuffer(stepsX * stepsY * 4)),
stepsX: stepsX,
stepsY: stepsY,
bounds: bounds,
diameter: toolDiameter,
resolution: resolution,
profile: toolOffset,
widget: widget,
raster: true,
slices: null
},
data = topo.data,
newslices = [],
tabsMax = tabs ? Math.max(...tabs.map(tab => tab.dim.z/2 + tab.pos.z)) : 0,
tabsOn = tabs,
tabHeight = Math.max(process.camTabsHeight + zBottom, tabsMax),
clipTab = tabsOn ? [] : null,
clipTo = inside ? shadow : POLY.expand(shadow, toolDiameter/2 + resolution * 3),
partOff = inside ? 0 : toolDiameter / 2 + resolution,
gridDelta = Math.floor(partOff / resolution),
debug = false,
debug_clips = debug && true,
debug_topo = debug && true,
debug_topo_lines = debug && true,
debug_topo_shells = debug && true,
time = Date.now;
if (tolerance === 0 && !topoCache) {
console.log(widget.id, 'topo auto tolerance', resolution.round(4));
}
// console.log({ resolution, flatness });
// used by Pocket -> Contour
this.tolerance = resolution;
if (tabs) {
clipTab.appendAll(tabs.map(tab => {
let ctab = POLY.expand([tab.poly], toolDiameter/2);
ctab.forEach(ct => ct.z = tab.dim.z/2 + tab.pos.z);
return ctab;
}).flat());
}
// debug clipTab and clipTo
if (debug_clips) {
const debug = newSlice(-1);
const output = debug.output();
// if (clipTab) output.setLayer("clip.tab", { line: 0xff0000 }).addPolys(clipTab);
if (clipTo) output.setLayer("clip.to", { line: 0x00dd00 }).addPolys(clipTo);
newslices.push(debug);
}
const probe = this.probe = new Probe({
profile: toolOffset,
data,
stepsX,
stepsY,
boundsX,
boundsY,
minX,
minY,
zMin
});
const { toolAtZ, toolAtXY, zAtXY } = probe;
this.toolAtZ = toolAtZ;
this.toolAtXY = toolAtXY;
this.zAtXY = zAtXY;
const trace = this.trace = new Trace(probe, {
curvesOnly,
maxangle,
flatness,
bridge,
contourX
});
if (topo.raster) {
const box = topo.box = new THREE.Box2();
const params = {
resolution,
curvesOnly,
flatness,
stepsY,
minY,
maxY,
zMin,
data,
box
};
onupdate(0, 1, "raster");
await this.raster(widget, params, (i, l, p) => {
onupdate(i / 2, l, p);
});
topo.raster = false;
}
await this.contour({
box: topo.box,
minX,
maxX,
minY,
maxY,
stepsX,
stepsY,
boundsX,
boundsY,
partOff,
gridDelta,
resolution,
toolStep,
contourX,
contourY,
density,
clipTo,
clipTab,
clipTabZ: clipTab ? clipTab.map(t => t.z) : undefined,
tabHeight,
newslices,
leave,
color
}, (i, l, p) => {
onupdate(l / 2 + i / 2, l, p);
});
ondone(newslices);
return this;
}
async raster(widget, params, onupdate) {
const { resolution } = params;
const { box } = params;
const { dispatch, minions } = self.kiri_worker;
const vertices = widget.getGeoVertices({ unroll: true, translate: true }).toShared();
const 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];
vertices[i] = z;
vertices[i + 2] = x;
range.min = Math.min(range.min, x);
range.max = Math.max(range.max, x);
}
const shards = Math.ceil(Math.min(25, vertices.length / 27000));
const step = (range.max - range.min) / shards;
let slices = [];
let index = 0;
let slice = { min: range.min, max: range.min + step, index };
for (let z = range.min; z < range.max; z += resolution) {
if (z > slice.max) {
slices.push(slice);
slice = { min: z, max: z + step, index };
}
index++;
}
slices.push(slice);
// console.log({ shards, range, step, slices });
let complete = 0;
// define sharded ranges
if (minions.running > 1) {
dispatch.putCache({ key: widget.id, data: vertices }, { done: data => {
// console.log({ put_cache_done: data });
}});
let promises = slices.map(slice => {
return new Promise(resolve => {
minions.queue({
cmd: "topo_raster",
id: widget.id,
params,
slice
}, data => {
resolve(data);
onupdate(++complete, slices.length, "raster");
});
});
});
// merge boxes for all rasters for contouring clipping
(await Promise.all(promises))
.map(rec => rec.box)
.map(box => new THREE.Box2(
new THREE.Vector2(box.min.x, box.min.y),
new THREE.Vector2(box.max.x, box.max.y)
))
.map(box2 => {
box.union(box2);
return box2;
});
dispatch.clearCache({}, { done: data => {
// console.log({ clear_cache_done: data });
}});
} else {
// iterate over shards, merge output
// const output = [];
for (let slice of slices) {
new topo_slicer(slice.index)
.setFromArray(vertices, slice)
.slice(resolution)
.map(rec => {
const slice = newSlice(rec.z);
slice.index = rec.index;
slice.lines = rec.lines;
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 }
}
raster_slice({
...params,
box,
lines: rec.lines,
gridx: rec.index
});
return slice;
});
onupdate(++complete, slices.length, "raster");
}
}
}
async contour(params, onupdate) {
const trace = this.trace;
const concurrent = self.kiri_worker.minions.running;
const { minX, maxX, minY, maxY, boundsX, boundsY, stepsX, stepsY } = params;
const { gridDelta, resolution, density, partOff, toolStep, contourX, contourY } = params;
const { clipTo, clipTab, clipTabZ, tabHeight, newslices, color, leave } = params;
let stepsTaken = 0,
stepsTotal = 0;
if (contourX) {
stepsTotal += ((maxY - minY + partOff * 2) / toolStep) | 0;
}
if (contourY) {
stepsTotal += ((maxX - minX + partOff * 2) / toolStep) | 0;
}
if (stepsTotal === 0) {
return;
}
const box = params.box.clone().expandByVector(new THREE.Vector3(
partOff, partOff, 0
));
trace.init({
box,
leave,
clipTo,
clipTab,
clipTabZ,
tabHeight,
resolution,
concurrent,
density
});
let resolver;
let pcount = 0;
let slicesY = [];
let slicesX = [];
let promise = new Promise(resolve => {
resolver = () => {
// sort output slices (required for async)
slicesY.sort((a,b) => a.z - b.z);
slicesX.sort((a,b) => a.z - b.z);
newslices.appendAll(slicesY);
newslices.appendAll(slicesX);
resolve();
}
});
let inc = () => { pcount++ };
let dec = () => { if (--pcount === 0 && concurrent) resolver() };
if (contourY) {
onupdate(0, stepsTotal, "contour y");
// emit slice per X
for (let x = minX - partOff; x <= maxX + partOff; x += toolStep) {
if (x < box.min.x || x > box.max.x) continue;
const gridx = Math.round(((x - minX) / boundsX) * stepsX);
const gridy = -gridDelta;
inc();
trace.crossY({
from: minY - partOff,
to: maxY + partOff,
x,
gridx,
gridy
}, segments => {
if (segments.length > 0) {
let slice = newSlice(gridx);
slice.camLines = segments;
slice.output()
.setLayer("contour y", {face: color, line: color})
.addPolys(segments);
slicesY.push(slice);
}
onupdate(++stepsTaken, stepsTotal, "contour y");
dec();
});
}
}
if (contourX) {
// emit slice per Y
onupdate(0, stepsTotal, "contour x");
for (let y = minY - partOff; y <= maxY + partOff; y += toolStep) {
if (y < box.min.y || y > box.max.y) continue;
const gridy = Math.round(((y - minY) / boundsY) * stepsY);
const gridx = -gridDelta;
inc();
trace.crossX({
from: minX - partOff,
to: maxX + partOff,
y,
gridx,
gridy
}, segments => {
if (segments.length > 0) {
let slice = newSlice(gridy);
slice.camLines = segments;
slice.output()
.setLayer("contour x", {face: color, line: color})
.addPolys(segments);
slicesX.push(slice);
}
onupdate(++stepsTaken, stepsTotal, "contour x");
dec();
});
}
}
if (!concurrent) resolver();
await promise;
// const lines = newslices.map(s => (s.camLines || []).map(p => p.length));
// const points = lines.flat().reduce((a,b) => a+b);
// console.log({ lines, points });
trace.cleanup();
}
}
export class Probe {
constructor(params) {
const { data, profile } = params;
const { stepsX, stepsY, boundsX, zMin, minX, minY } = params;
this.params = params;
// return the touching z given topo x,y and a tool profile
const toolAtZ = this.toolAtZ = function(x,y) {
let sx = stepsX,
sy = stepsY,
xl = sx - 1,
yl = sy - 1;
let gv, i = 0, mz = -Infinity;
while (i < profile.length) {
// tool profile point x, y, and z offsets
const tx = profile[i++] + x;
const ty = profile[i++] + y;
const tz = profile[i++];
if (tx < 0 || tx > xl || ty < 0 || ty > yl) {
// if outside max topo steps, use zMin
gv = zMin;
} else {
// lookup grid value @ tx, ty
gv = data[tx * sy + ty] || zMin;
}
// update the rest
mz = Math.max(tz + gv, mz);
}
return Math.max(mz, zMin);
}
// export z probe function
const rx = stepsX / boundsX;
const ry = stepsX / boundsX;
const toolAtXY = this.toolAtXY = function(px, py) {
px = Math.round(rx * (px - minX));
py = Math.round(ry * (py - minY));
return toolAtZ(px, py);
};
const zAtXY = this.zAtXY = function(px, py) {
let ix = Math.round(rx * (px - minX));
let iy = Math.round(ry * (py - minY));
return data[ix * stepsY + iy] || zMin;
};
}
}
export class Trace {
constructor(probe, params) {
const { curvesOnly, maxangle, flatness, bridge, contourX, leave } = params;
this.params = params;
this.probe = probe;
let trace,
slice,
latent,
lastPP,
lastSlope;
const newslice = this.newslice = () => {
this.slice = slice = [];
}
const newtrace = this.newtrace = function() {
trace = newPolygon().setOpen();
}
const end_poly = this.end_poly = function(point) {
if (latent) {
trace.push(latent);
}
if (trace.length > 0) {
// add additional constraint on min perimeter()
if (trace.length > 1) {
slice.push(trace);
}
newtrace();
}
lastPP = undefined;
latent = undefined;
lastSlope = undefined;
if (point) {
trace.push(point);
lastPP = point;
}
}
const log = function(map) {
for (let key in map) {
const val = map[key];
if (typeof val === 'number') {
map[key] = val.round(4);
}
}
console.log(...arguments);
}
const push_point = this.push_point = function(x, y, z) {
const newP = newPoint(x, y, z);
const lastP = lastPP;//trace.last();
if (lastP) {
const dl = (x - lastP.x) || (y - lastP.y);
const dz = z - lastP.z;
const slope = Math.atan2(dz, dl);
if (curvesOnly && Math.abs(dz) < flatness) {
end_poly(newP);
} else if (lastSlope !== undefined && Math.abs(lastSlope - slope) < 0.001) {
latent = newP;
} else {
if (latent) {
trace.push(latent);
latent = undefined;
}
if (curvesOnly) {
const dv = contourX ? Math.abs(lastP.x - x) : Math.abs(lastP.y - y);
// const dz = lastPP.z - z;
const angle = Math.atan2( Math.abs(dz), dv) * RAD2DEG;
if (angle > maxangle) {
end_poly();
}
}
trace.push(newP);
}
lastSlope = slope;
} else {
trace.push(newP);
}
lastPP = newP;
}
const object = this.object = this;
this.inClip = function (clips, checkZ, point) {
for (let i=0; i<clips.length; i++) {
let poly = clips[i];
let zok = checkZ ? checkZ <= poly.z : true;
object.tabZ = poly.z;
if (zok && point.isInPolygon(poly)) {
return true;
}
}
return false;
}
}
init(params) {
this.cross = params;
const { minions } = self.kiri_worker ?? {};
const { clipTab, clipTabZ } = params;
// because codec does not encode arbitrary fields
// in this case, z is appended to clip tabs in topo constructor
// we pass it as a side-channel and re-consitute here
if (clipTab)
for (let i=0, l=clipTab.length; i<l; i++) {
clipTab[i].z = clipTabZ[i];
}
if (minions && this.cross.concurrent) {
minions.broadcast("trace_init", codec.encode({
probe: this.probe.params,
trace: this.params,
cross: params,
}));
}
}
cleanup() {
const { minions } = self.kiri_worker;
if (minions && this.cross.concurrent) {
minions.broadcast("trace_cleanup");
}
}
crossY(params, then) {
const { minions } = self.kiri_worker;
if (minions && this.cross.concurrent) {
minions.queue({
cmd: "trace_y",
params
}, data => {
then(codec.decode(data.slice));
});
} else {
this.crossY_sync(params, then);
}
}
crossX(params, then) {
const { minions } = self.kiri_worker;
if (minions && this.cross.concurrent) {
minions.queue({
cmd: "trace_x",
params
}, data => {
then(codec.decode(data.slice));
});
} else {
this.crossX_sync(params, then);
}
}
crossY_sync(params, then) {
const { push_point, end_poly, newtrace, newslice, inClip } = this.object;
const { clipTab, tabHeight, clipTo, box, resolution, density, leave } = this.cross;
const { toolAtZ } = this.probe;
let { from, to, x, gridx, gridy } = params;
const step = resolution * density;
const checkr = newPoint(0,0);
newslice();
newtrace();
for (let y = from; y < to; y += step) {
if (y < box.min.y || y > box.max.y) {
gridy += density;
continue;
}
// find tool z at grid point
let tv = toolAtZ(gridx, gridy);
checkr.x = x;
checkr.y = y;
// when tabs are on and this point is inside the
// tab polygon, ensure z is at least tabHeight
if (clipTab && tv < tabHeight && inClip(clipTab, tv, checkr)) {
tv = this.tabZ;
}
// if the value is on the floor and inside the clip
// poly (usually shadow), end the segment
if (clipTo && !inClip(clipTo, undefined, checkr)) {
end_poly();
gridy += density;
continue;
}
push_point(x, y, tv + leave);
gridy += density;
}
end_poly();
then(this.slice);
}
crossX_sync(params, then) {
const { push_point, end_poly, newtrace, newslice, inClip } = this.object;
const { clipTab, tabHeight, clipTo, box, resolution, density, leave } = this.cross;
const { toolAtZ } = this.probe;
let { from, to, y, gridx, gridy } = params;
const step = resolution * density;
const checkr = newPoint(0,0);
newslice();
newtrace();
for (let x = from; x < to; x += step) {
if (x < box.min.x || x > box.max.x) {
gridx += density;
continue;
}
// find tool z at grid point
let tv = toolAtZ(gridx, gridy);
checkr.x = x;
checkr.y = y;
// when tabs are on and this point is inside the
// tab polygon, ensure z is at least tabHeight
if (clipTab && tv < tabHeight && inClip(clipTab, tv, checkr)) {
tv = this.tabZ;
}
// if the value is on the floor and inside the clip
// poly (usually shadow), end the segment
if (clipTo && !inClip(clipTo, undefined, checkr)) {
end_poly();
gridx += density;
continue;
}
push_point(x, y, tv + leave);
gridx += density;
}
end_poly();
then(this.slice);
}
}
export function raster_slice(inputs) {
const { lines, data, box, resolution, curvesOnly } = inputs;
const { flatness, zMin, minY, maxY, stepsY, gridx } = inputs;
const { slice } = inputs;
let gridy,
gridi, // index
gridv, // value
i, il, j, x, y, tv;
// filter lines pairs to only surface "up-facing", "uncovered" lines
let points = [];
// emit an array of valid line-pairs
const len = lines.length;
outer: for (let i=0; i<len; i++) {
let l1 = lines[i], p1 = l1.p1, p2 = l1.p2;
// eliminate vertical
if (Math.abs(p1.y - p2.y) < flatness) continue;
// eliminate if both points below cutoff
if (p1.z < zMin && p2.z < zMin) continue;
// sort p1,p2 by y for comparison
if (p1.y > p2.y) { const tp = p1; p1 = p2; p2 = tp };
// eliminate if points "under" other lines
for (let j=0; j<len; j++) {
// skip self and adjacent
if (j >= i-1 && j <= i+1) continue;
let l2 = lines[j], p3 = l2.p1, p4 = l2.p2;
// sort p3,p4 by y for comparison
if (p3.y > p4.y) { const tp = p3; p3 = p4; p4 = tp };
// it's under the other line
if (Math.max(p1.z,p2.z) < Math.min(p3.z,p4.z)) {
// it's inside the other line, too, so skip
if (p1.y >= p3.y && p2.y <= p4.y) continue outer;
}
}
points.push(p1, p2);
box.expandByPoint(p1);
box.expandByPoint(p2);
}
gridy = 0;
// rasterize one x slice
for (y = minY; y < maxY && gridy < stepsY; y += resolution) {
gridi = gridx * stepsY + gridy;
gridv = data[gridi] || zMin;
// strategy using raw lines (faster slice, but more lines)
for (i=0, il=points.length; i<il; i += 2) {
const p1 = points[i], p2 = points[i+1];
// one endpoint above grid
const crossz = (p1.z > gridv || p2.z > gridv);
// segment crosses grid y
const spansy = (p1.y <= y && p2.y >= y);
if (crossz && spansy) {
// compute intersection of z ray up
// and segment at this grid point
const dy = p1.y - p2.y,
dz = p1.z - p2.z,
pct = (p1.y - y) / dy,
nz = p1.z - (dz * pct);
// save if point is greater than existing grid point
if (nz > gridv) {
gridv = data[gridi] = Math.max(nz, zMin);
if (slice) slice.output()
.setLayer("heights", {face: 0, line: 0})
.addLine(
newPoint(p1.x, y, 0),
newPoint(p1.x, y, gridv)
);
}
}
}
gridy++;
}
// remove flat lines when curvesOnly
if (curvesOnly) {
let nup = [];
for (let i=0, p=points, l=p.length; i<l; i += 2) {
const p1 = p[i];
const p2 = p[i+1];
if (Math.abs(p1.z - p2.z) >= flatness) {
nup.push(p1, p2);
}
}
points = nup;
}
return points;
};
export async function generate(opt) {
return new Topo().generate(opt);
}

467
src/kiri/mode/cam/topo4.js Normal file
View file

@ -0,0 +1,467 @@
/** 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 { Tool } from './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);
}
}
class Topo4 {
constructor() { }
async generate(opt = {}) {
let { state, op, onupdate, ondone } = opt;
let { widget, settings, tabs } = opt.state;
let { controller, process } = settings;
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"
},
zMin = min.z + 0.0001,
tolerance = op.tolerance,
resolution = tolerance ? tolerance : 1 / Math.sqrt(density / (span.x * span.y)),
step = this.step = (tool.traceOffset() * 2) * op.step,
angle = this.angle = op.angle || 1;
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;
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.zoff = widget.track.top || 0;
this.leave = op.leave || 0;
this.linear = op.linear || false;
this.lineColor = state.settings.controller.dark ? 0xffff00 : 0x555500;
onupdate(0, "lathe");
const range = this.range = { min: Infinity, max: -Infinity };
const slices = this.sliced = await this.slice(scale(onupdate, 0.25, 0));
for (let slice of slices) {
range.min = Math.min(range.min, slice.z);
range.max = Math.max(range.max, slice.z);
slice.output()
.setLayer("lathe", { line: 0x888888 })
.addPolys(slice.topPolys());
}
const lathe = await this.lathe(scale(onupdate, 0.75, 0.25));
onupdate(1, "lathe");
ondone(lathe);
// ondone([...slices, ...lathe]);
return this;
}
async slice(onupdate) {
const { vertices, resolution, tabverts, zoff } = this;
const range = this.range = { min: Infinity, max: -Infinity };
const box = this.box = new THREE.Box2();
// 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 radius to slice min/max range ot fully carve part
range.min -= this.diam / 2;
range.max += this.diam / 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();
const shards = Math.ceil(Math.min(25, vertices.length / 27000));
const step = (range.max - range.min) / shards;
let index = 0;
let slices = this.slices = [];
let slice = { min: range.min, max: range.min + step, index };
for (let z = range.min; z < range.max; z += resolution) {
if (z > slice.max) {
slices.push(slice);
slice = { min: z, max: z + step, index };
}
index++;
}
slices.push(slice);
// console.log({ shards, range, step, slices });
if (kiri.minions.running > 1) {
return await this.sliceMinions(onupdate);
} else {
return await this.sliceWorker(onupdate);
}
}
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) {
if (kiri.minions.running > 1) {
return await this.latheMinions(onupdate);
} else {
return await this.latheWorker(onupdate);
}
}
lathePath(slices, tool) {
const { resolution, step, zBottom, maxo } = this;
const tlen = tool.length;
const slen = slices.length;
const sinc = Math.max(1, Math.ceil(step / resolution));
const heights = [];
// 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 si = 0; si < slen; si += sinc) {
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; // Math.min(0, lz)
} else if (zBottom && mz < zBottom) {
mz = zBottom;
}
heights.push(rx, 0, lz = mz);
}
return heights;
}
async latheWorker(onupdate) {
const { sliced, tool, zoff, leave } = 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 = recs[0].heights.length / 3;
while (count-- > 0) {
let slice = newSlice(count);
slice.camLines = [ newPolygon().setOpen() ];
paths.push(slice);
}
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 });
// let linear = true;
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) {
kiri.worker.putCache({ key, data }, { done: data => { } });
}
clearCache() {
kiri.worker.clearCache({}, { done: data => { } });
}
queue(cmd, params) {
return new Promise(resolve => {
kiri.minions.queue({ cmd, ...params }, resolve);
});
}
}
function rotatePoints(lines, rot) {
new THREE.BufferAttribute(lines, 3).applyMatrix4(rot);
}
export function Topo4(opt) {
return new Topo4().generate(opt);
};

View file

@ -0,0 +1,5 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
export function init() {
// TODO
}

View file

@ -0,0 +1,13 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { LASER, TYPE } from '../laser/driver.js';
function init(worker) {
// console.log({ DRAGKNIFE_INIT: worker });
}
export const DRAG = Object.assign({}, LASER, {
type: TYPE.DRAG,
name: 'DragKnife',
init
});

647
src/kiri/mode/fdm/client.js Normal file
View file

@ -0,0 +1,647 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { $ } from '../../../moto/webui.js';
import { api } from '../../core/api.js';
import { conf } from '../../core/conf.js';
import { util } from '../../../geo/base.js';
import { client } from '../../core/client.js';
import { addbox, delbox, getlastbox } from '../../core/boxes.js';
import { Layers } from '../../core/layers.js';
import { Stack } from '../../core/stack.js';
import { Widget, newWidget } from '../../core/widget.js';
import { space } from '../../../moto/space.js';
const { Matrix4, Vector3, BufferAttribute, BufferGeometryUtils, Raycaster, Euler } = THREE;
const { VIEWS } = api.const;
const LANG = api.language.current;
let lastBox, lastMode, lastView, lastPillar, fromPillar,
addingSupports = false,
isFdmMode = false,
nextID = Date.now(),
p1, p2, iw,
alert = [],
func = {};
export function init() {
const { ui } = api;
const proc_keys = Object.keys(conf.defaults.fdm.p);
const rangeVars = {
// slice
// "sliceHeight": LANG.sl_lahi_s,
"sliceShells": LANG.sl_shel_s,
"sliceFillType": LANG.fi_type,
"sliceFillWidth": LANG.fi_wdth_s,
"sliceFillSparse": LANG.fi_pcnt_s,
"sliceFillRate": LANG.ou_feed_s,
"sliceFillGrow": LANG.fi_grow_s,
"sliceSolidRate": LANG.ou_fini_s,
"sliceSolidMinArea": LANG.ad_msol_s,
"sliceTopLayers": LANG.sl_ltop_s,
"sliceBottomLayers": LANG.sl_lbot_s,
"firstLayerRate": LANG.fl_rate_s,
"firstLayerPrintMult": LANG.fl_mult_s,
"firstLayerFanSpeed": LANG.ou_fans_s,
// prepare
"sliceShellOrder": LANG.sl_ordr_s,
"sliceFillOverlap": LANG.fi_over_s,
"outputFeedrate": LANG.ou_feed_s,
"outputFinishrate": LANG.ou_fini_s,
"outputShellMult": LANG.ou_shml_s,
"outputFillMult": LANG.ou_flml_s,
"outputSparseMult": LANG.ou_spml_s,
"outputRetractWipe": LANG.ad_wpln_s,
"outputAlternating": LANG.ad_altr_s,
"outputShortPoly": LANG.ad_spol_s,
"outputMinSpeed": LANG.ad_mins_s,
"outputCoastDist": LANG.ad_scst_s,
"outputLoops": LANG.ag_loop_s,
"sliceSupportDensity": LANG.sp_dens_s,
"sliceSupportOffset": LANG.sp_offs_s,
"sliceLayerStart": LANG.sl_strt_s,
"firstLayerBrim": LANG.fl_brim_s,
"firstLayerBrimIn": LANG.fl_brin_s,
"firstLayerBrimTrig": LANG.fl_brmn_s,
"firstLayerBrimGap": LANG.fl_brgp_s,
// export
"zHopDistance": LANG.ad_zhop_s,
"arcTolerance": LANG.ad_zhop_s,
"antiBacklash": LANG.ad_abkl_s,
"outputTemp": LANG.ou_nozl_s,
"outputBedTemp": LANG.ou_bedd_s,
"outputFanSpeed": LANG.ou_fans_s,
"outputRetractDist": LANG.ad_rdst_s,
"outputRetractSpeed": LANG.ad_rrat_s,
"outputRetractDwell": LANG.ad_rdwl_s,
"outputLayerRetract": LANG.ad_lret_s
};
for (let key of Object.keys(rangeVars)) {
if (ui[key]) {
ui[key].range = true;
}
}
function filterSynth() {
api.widgets.filter((widget) => {
if (widget.track.synth) {
space.world.remove(widget.mesh);
Widget.Groups.remove(widget);
}
return !widget.track.synth
});
}
function clearRanges() {
api.conf.get().process.ranges = [];
ui.fdmRanges.innerHTML = '';
}
// re-render ranges menu
function updateRanges(ranges = []) {
let html = [];
let bind = [];
let now = Date.now();
let sorted = ranges.sort((a,b) => b.lo - a.lo);
for (let range of sorted) {
let id = (now++).toString(36);
let rows = Object.entries(range.fields).map(a => `<div><label class="pad">${rangeVars[a[0]]}</label><span></span><label class="val">${a[1]}</label></div>`).join('');
let hover = `<div id="hov_${id}" class="range-detail">${rows}</div>`;
let info = `<button id="sel_${id}" class="j-center grow">${range.lo} - ${range.hi}</button><button id="del_${id}"><i class="fa fa-trash"></i></button>`;
html.appendAll([
`<div id="rng_${id}" class="range-info">${hover}${info}</div>`
]);
bind.push({id, range});
}
ui.fdmRanges.innerHTML = html.join('');
for (let rec of bind) {
$(`sel_${rec.id}`).onclick = () => {
api.show.layer(rec.range.hi, rec.range.lo);
};
$(`del_${rec.id}`).onclick = () => {
let io = ranges.indexOf(rec.range);
ranges.splice(io,1);
updateRanges(ranges);
};
}
}
api.event.on("function.animate", (mode) => {
if (!(isFdmMode && lastView === VIEWS.PREVIEW)) {
return;
}
let slider = $('top-slider');
slider.style.display = 'flex';
slider.oninput = slider.onchange = (ev) => {
api.const.STACKS.setFraction(parseInt(ev.target.value)/1000);
api.space.update();
};
$('anim-slider').value = 500;
api.const.STACKS.setFraction(0.5);
});
api.event.on("mode.set", mode => {
isFdmMode = mode === 'FDM';
lastMode = mode;
// hide/show fdm mode elements
for (let el of [...document.getElementsByClassName('mode-fdm')]) {
api.uc.setClass(el, 'hide', !isFdmMode);
}
updateSupportVisiblity();
});
api.event.on("view.set", view => {
$('top-slider').style.display = 'none';
lastView = view;
updateSupportVisiblity();
filterSynth();
func.sdone();
// let ranges = api.conf.get().process.ranges;
if (isFdmMode) {
if (lastView === VIEWS.SLICE) {
for (let key of proc_keys) {
if (ui[key] && !ui[key].range) {
ui[key].disabled = true;
}
}
} else {
for (let key of proc_keys) {
if (ui[key]) ui[key].disabled = false;
}
}
}
});
api.event.on("range.updates", updateRanges);
api.event.on("settings.load", (settings) => {
if (settings.mode !== 'FDM') return;
restoreSupports(api.widgets.all());
updateRanges(settings.process.ranges);
});
api.event.on("settings.saved", (settings) => {
updateRanges(settings.process.ranges);
});
api.event.on("button.click", target => {
switch (target) {
case api.ui.ssaGen: return func.sgen();
case api.ui.ssmAdd: return func.sadd();
case api.ui.ssmDun: return func.sdone();
case api.ui.ssmClr: return func.sclear();
}
});
api.event.on("fdm.supports.detect", func.sgen = () => {
let alerts = [];
let { process, device } = api.conf.get();
if (!device.bedBelt) {
if (process.sliceSupportAngle < 10) {
alerts.push(api.show.alert("angles below 10 degrees may fail"));
}
}
alerts.push(api.show.alert("analyzing part(s)...", 1000));
support_generate(array => {
func.sclear();
api.hide.alert(undefined,alerts);
for (let rec of array) {
let { widget, supports } = rec;
let wa = api.widgets.annotate(widget.id);
let ws = wa.support || [];
for (let support of supports) {
let { from, to, mid } = support;
let dw = api.conf.get().process.sliceSupportSize / 2;
let dh = from.z - to.z;
let rec = {
x: mid.x,
y: mid.y,
z: mid.z,
dw,
dh,
id: Math.random() * 0xffffffffff
};
addWidgetSupport(widget, rec);
ws.push(Object.clone(rec));
}
wa.support = ws;
}
api.event.emit("fdm.supports.detected");
});
});
api.event.on("fdm.supports.add", func.sadd = () => {
alert = api.show.alert("[esc] key cancels support editing");
api.feature.hover = addingSupports = true;
});
api.event.on("fdm.supports.done", func.sdone = () => {
delbox('intZ');
delbox('intW');
delbox('supp');
api.hide.alert(alert);
api.feature.hover = addingSupports = false;
fromPillar = undefined;
});
api.event.on("fdm.supports.clear", func.sclear = () => {
func.sdone();
if (clearAllWidgetSupports()) {
api.conf.save();
}
});
api.event.on("slice.begin", () => {
if (!isFdmMode) {
return;
}
func.sdone();
updateSupportVisiblity();
// synth support widget for each widget group
// iow, create support meshes based on support widget annotation
for (let group of Widget.Groups.list()) {
let merge = group.filter(w => w.sups).map(w => Object.values(w.sups)).flat();
if (!merge.length) {
continue;
}
let boxen = merge.map(m => {
let geo = m.box.geometry.clone();
if (geo.index) geo = geo.toNonIndexed();
return geo.translate(m.x, m.y, m.z);
});
for (let box of boxen) {
// eliminate / normalize uv to allow other widget merge
box.setAttribute('uv',new BufferAttribute(new Float32Array(0), 3));
}
let bbg = BufferGeometryUtils.mergeGeometries(boxen);
let sw = newWidget(null, group);
let fwp = group[0].track.pos;
sw.loadGeometry(bbg);
sw._move(fwp.x, fwp.y, fwp.z);
api.widgets.add(sw);
sw.track.synth = true;
sw.track.support = true;
space.world.add(sw.mesh);
}
});
api.event.on("slice.end", () => {
if (!isFdmMode) {
return;
}
});
api.event.on("key.esc", () => {
if (isFdmMode) {
func.sdone()
}
});
api.event.on("selection.scale", () => {
if (isFdmMode) {
clearRanges();
func.sclear();
}
});
api.event.on("widget.delete", widget => {
if (isFdmMode) {
clearRanges();
}
});
api.event.on("widget.duplicate", (widget, oldwidget) => {
if (!isFdmMode) {
return;
}
let ann = api.widgets.annotate(widget.id);
if (ann.support) {
for (let supp of Object.values(ann.support)) {
supp.id = Math.abs(Math.random() * 0xffffffffffff);
}
restoreSupports([widget]);
}
});
api.event.on("widget.mirror", widget => {
if (!isFdmMode) {
return;
}
let ann = api.widgets.annotate(widget.id);
let sups = ann.support || [];
sups.forEach(sup => {
let wsup = widget.sups[sup.id];
wsup.box.position.x = wsup.x = sup.x = -sup.x;
});
});
api.event.on("widget.rotate", rot => {
if (!isFdmMode) {
return;
}
let {widget, x, y, z} = rot;
if (x || y) {
clearWidgetSupports(widget);
} else {
let ann = api.widgets.annotate(widget.id);
let sups = ann.support || [];
sups.forEach(sup => {
let wsup = widget.sups[sup.id];
let vc = new Vector3(sup.x, sup.y, sup.z);
let m4 = new Matrix4();
m4 = m4.makeRotationFromEuler(new Euler(x || 0, y || 0, z || 0));
vc.applyMatrix4(m4);
wsup.box.position.x = wsup.x = sup.x = vc.x;
wsup.box.position.y = wsup.y = sup.y = vc.y;
wsup.box.position.z = wsup.z = sup.z = vc.z;
});
}
});
api.event.on("mouse.hover.up", func.hoverup = (on = {}) => {
let { event } = on;
if (!isFdmMode) {
return;
}
if (!addingSupports) {
return;
}
delbox('supp');
if (lastPillar) {
removeWidgetSupport(lastPillar.widget, lastPillar);
return;
}
lastBox = getlastbox();
if (!(iw && lastBox)) return;
let { point, dim } = lastBox;
p1.y = Math.max(0, p1.y);
p2.y = Math.max(0, p2.y);
let rz = dim.rz;
let dh = dim.z;
let dw = api.conf.get().process.sliceSupportSize / 2;
let ip = iw.track.pos;
let wa = api.widgets.annotate(iw.id);
let ws = (wa.support = wa.support || []);
let id = ++nextID;
let rec = {x: point.x - ip.x, y: -point.z - ip.y, z: point.y, rz, dw, dh, id};
if (fromPillar && event && event.shiftKey) {
let targets = api.widgets.meshes().append(space.internals().platform);
let from = fromPillar,
d = {
x: rec.x - from.x,
y: rec.y - from.y,
z: rec.z - from.z
};
let dist = Math.sqrt(d.x*d.x + d.y*d.y + d.z*d.z);
let lerp = util.lerp(0,dist,dw).map(v => v/dist);
let angle = rec.rz = Math.atan2(d.y,d.x);
let fromrec = ws.filter(r => r.id === from.id)[0];
if (fromrec) {
from.rz = fromrec.rz = angle;
removeWidgetSupport(iw, fromPillar);
ws.push(Object.clone(fromrec));
addWidgetSupport(iw, fromrec);
}
lerp.pop();
let seed = Math.random();
for (let pct of lerp) {
let point = new Vector3(from.x + d.x * pct, from.z + d.z * pct, from.y + d.y * pct);
addSupportAtPoint(targets, point, ip, angle, seed++);
}
}
ws.push(Object.clone(rec));
fromPillar = addWidgetSupport(iw, rec);
api.conf.save();
});
function addSupportAtPoint(targets, point, ip, angle, id) {
let up = new Vector3(0,1,0)
let dn = new Vector3(0,-1,0)
let rp = new Vector3(point.x + ip.x, point.y, -point.z - ip.y);
let iup = new Raycaster(rp, up)
.intersectObjects(targets, false)
.filter(t => !t.object.pillar);
if (iup.length && iup.length % 2 === 0) {
let idn = new Raycaster(rp, dn)
.intersectObjects(targets, false)
.filter(t => !t.object.pillar);
if (idn.length && idn.length % 2 === 0) {
iw = iup[0].object.widget || iw;
let wa = api.widgets.annotate(iw.id);
let ws = (wa.support = wa.support || []);
let phi = iup[0].point;
let plo = idn[0].point;
let ply = Math.max(0, plo.y);
let mp = (phi.y + ply) / 2;
let dy = Math.abs(phi.y - ply);
let dw = api.conf.get().process.sliceSupportSize / 2;
let rec = { rz:angle, x:point.x, y:point.z, z:mp, dw, dh:dy, id };
ws.push(Object.clone(rec));
fromPillar = addWidgetSupport(iw, rec);
}
}
}
api.event.on("mouse.hover", data => {
if (!isFdmMode) {
return;
}
if (!addingSupports) {
return;
}
delbox('supp');
const { int, type, point, event } = data;
const pillar = int ? int.object.pillar : undefined;
if (lastPillar) {
lastPillar.box.material.color.r = 0;
lastPillar = null;
}
if (pillar) {
pillar.box.material.color.r = 0.5;
lastPillar = pillar;
if (event && (event.metaKey || event.ctrlKey)) {
return func.hoverup();
}
return;
}
if (int && type === 'widget') {
iw = int.object.widget || iw;
} else {
iw = null;
}
p1 = point;
let dir = new Vector3(0,1,0)
let ray = new Raycaster(point, dir);
let rz = int && int.face ? Math.atan2(int.face.normal.y, int.face.normal.x) : 0;
// when on object, project down on downward faces
if (int && int.face && int.face.normal.z < -0.1) {
dir.y = -1;
}
let targets = api.widgets.meshes().append(space.internals().platform);
let i2 = ray.intersectObjects(targets, false)
.filter(t => !t.object.pillar) // eliminate other pillars
.filter(i => i.distance > 0.1); // false matches close to origin of ray
if (i2.length && i2.length % 2 === 0) {
p2 = i2[0].point;
iw = i2[0].object.widget || iw;
let p1y = Math.max(0, p1.y);
let p2y = Math.max(0, p2.y);
let hy = (p1y + p2y) / 2;
let dy = Math.abs(p1y - p2y);
let dw = api.conf.get().process.sliceSupportSize / 2;
addbox({x:p1.x, y:hy, z:p1.z}, 0x0000dd, 'supp', {
x:dw, y:dw, z:dy, rz
});
} else {
lastBox = null;
}
if (event && event.altKey) {
return func.hoverup();
}
});
api.event.on("export.debug", msg => {
if (msg.arcQ && msg.arcQ.length > 2) {
if (xpdebug) {
xpdebug.destroy();
xpdebug = undefined;
}
let poly = base.newPolygon().setOpen();
for (let rec of msg.arcQ) {
rec.z += 1;
poly.addObj(rec);
}
let layers = new Layers();
let layer = layers.setLayer("arcq", { line: 0xff00ff, face: 0xff00ff, opacity: 1 });
layer.addPoly(poly, {thin:true});
layer = layers.setLayer("arcc", { line: 0xff0000, face: 0xff0000, opacity: 1 });
for (let center of msg.arcQ.center) {
center.z += 0.5;
layer.addPoly(base.newPolygon().centerCircle(
center, center.r, 20
));
}
xpdebug = new Stack(space.world, false);
xpdebug.addLayers(layers);
xpdebug.setVisible(0,Infinity);
xpdebug.show();
space.refresh();
console.log(msg, poly);
}
});
}
let xpdebug;
let supsave = {};
let suptimer;
function support_generate(ondone) {
client.clear();
client.sync();
const settings = api.conf.get();
const widgets = api.widgets.map();
client.send("fdm_support_generate", { settings }, (gen) => {
if (gen && gen.error) {
api.show.alert('support generation canceled');
return ondone([]);
}
for (let g of gen) {
g.widget = widgets[g.id];
}
ondone(gen);
});
};
function scheduleSupportSave(widget) {
clearTimeout(suptimer);
supsave[widget.id] = widget;
suptimer = setTimeout(() => {
for (let w of Object.values(supsave)) {
w.saveState();
}
}, 50);
}
export function restoreSupports(widgets) {
widgets.forEach(widget => {
const supports = api.widgets.annotate(widget.id).support || [];
supports.forEach(pos => {
addWidgetSupport(widget, pos);
});
});
}
function addWidgetSupport(widget, pos) {
const { x, y, z, rz, dw, dh, id } = pos;
const sups = widget.sups = (widget.sups || {});
// prevent duplicate restore from repeated settings load calls
if (!sups[id]) {
pos.box = addbox(
{ x, y, z }, 0x0000dd, id,
{ x:dw, y:dw, z:dh, rz }, { group: widget.mesh }
);
pos.box.pillar = Object.assign({widget}, pos);
sups[id] = pos;
widget.adds.push(pos.box);
scheduleSupportSave(widget);
}
return sups[id];
}
function removeWidgetSupport(widget, rec) {
const { box, id } = rec;
widget.adds.remove(box);
widget.mesh.remove(box);
delete widget.sups[id];
let sa = api.widgets.annotate(widget.id).support;
let ix = 0;
sa.forEach((rec,i) => {
if (rec.id === id) {
ix = i;
}
});
sa.splice(ix,1);
api.conf.save();
scheduleSupportSave(widget);
fromPillar = undefined;
}
function updateSupportVisiblity() {
const visible = (lastMode === 'FDM' && lastView === VIEWS.ARRANGE);
for (let w of api.widgets.all()) {
setSupportVisiblity(w, visible);
}
}
function setSupportVisiblity(widget, bool) {
Object.values(widget.sups || {}).forEach(support => {
support.box.visible = bool;
});
}
function clearAllWidgetSupports() {
let cleared = 0;
api.widgets.all().forEach(widget => {
cleared += clearWidgetSupports(widget);
});
return cleared;
}
function clearWidgetSupports(widget) {
let cleared = 0;
Object.values(widget.sups || {}).forEach(support => {
widget.adds.remove(support.box);
widget.mesh.remove(support.box);
cleared++;
});
widget.sups = {};
delete api.widgets.annotate(widget.id).support;
scheduleSupportSave(widget);
return cleared;
}

View file

@ -0,0 +1,44 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { util } from '../../../geo/base.js';
import { fdm_slice } from './slice.js';
import { fdm_prepare } from './prepare.js';
import { fdm_export } from './export.js';
// noz = nozzle diameter
// fil = filament diameter
// slice = slice height
function extrudePerMM(noz, fil, slice) {
return ((Math.PI * util.sqr(noz / 2)) / (Math.PI * util.sqr(fil / 2))) * (slice / noz);
};
// dist = distance between extrusion points
// perMM = amount extruded per MM (from extrudePerMM)
// factor = scaling factor (usually 1.0)
function extrudeMM(dist, perMM, factor) {
return dist * perMM * factor;
}
// defer loading until client and worker exist
function init(worker) {
worker.dispatch.fdm_support_generate = function(data, send) {
const { settings } = data;
const widgets = Object.values(worker.cache);
const fresh = widgets.filter(widget => FDM.supports(settings, widget));
send.done(kiri.codec.encode(fresh.map(widget => { return {
id: widget.id,
supports: widget.supports,
} } )));
};
}
export const FDM = {
init,
extrudePerMM,
extrudeMM,
slice: fdm_slice,
prepare: fdm_prepare,
export: fdm_export
};

View file

@ -0,0 +1,22 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
// shared by client and worker contexts
export function getRangeParameters(process, index) {
if (index === undefined || index === null || index < 0) {
return process;
}
let ranges = process.ranges;
if (!(ranges && ranges.length)) {
return process;
}
let params = Object.clone(process);
for (let range of ranges) {
if (index >= range.lo && index <= range.hi) {
for (let [key, value] of Object.entries(range.fields)) {
params[key] = value;
params._range = true;
}
}
}
return params;
}

991
src/kiri/mode/fdm/export.js Normal file
View file

@ -0,0 +1,991 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { base, util } from '../../../geo/base.js';
import { getRangeParameters } from './driver.js';
import { FDM } from './driver-be.js';
import { version } from '../../../moto/license.js';
const { config } = base;
const debug = false;
export function fdm_export(print, online, ondone, ondebug) {
const { widgets, settings, belty, tools, firstTool } = print;
const { bounds, controller, device, process, filter, mode } = settings;
const { extruders, fwRetract } = device;
const { bedWidth, bedDepth, bedRound, bedBelt, maxHeight } = device;
const { gcodeFan, gcodeLayer, gcodeTrack, gcodePause, gcodeFeature, gcodeChange } = device;
let model_labels = [];
for (let widget of widgets) {
model_labels.push(widget.track.grid_id);
}
let layers = print.output,
extras = device.extras || {},
isBambu = extras.bbl,
{ extrudeAbs } = device,
{ exportThumb } = controller,
{ extrudeMM, extrudePerMM } = FDM,
extused = Object.keys(print.extruders).map(v => parseInt(v)),
timeFactor = (device.gcodeTime || 1) * 1.5,
decimals = config.gcode_decimals || 4,
zMoveMax = device.deviceZMax || 0,
isBelt = bedBelt,
bedType = isBelt ? "belt" : "fixed",
time = 0,
layer = 0,
layerno = 0,
pause = [],
output = [],
outputLength = 0, // absolute extruder position
lastProgress = 0,
progress = 0,
distance = 0,
emitted = 0,
retracted = 0,
axis = { X:' X', Y:' Y', Z:' Z', E:' E'},
pos = { x:0, y:0, z:0, f:0 },
lout = { x:0, y:0, z:0 },
last = null,
zpos = 0,
bmax = 0,
blastz = 0,
belt_add_y = (process.firstLayerYOffset || 0) - (belty || 0),
oloops = process.outputLoops || 0,
zhop = process.zHopDistance || 0, // range
lineWidth = process.sliceLineWidth || 0,
seekMMM = process.outputSeekrate * 60,
retDist = process.outputRetractDist || 0, // range
retSpeed = process.outputRetractSpeed * 60 || 1, // range
retDwell = process.outputRetractDwell || 0, // range
timeDwell = retDwell / 1000,
arcDist = isBelt ? 0 : (process.arcTolerance || 0),
arcMin = 1,
arcRes = 20,
arcDev = 0.5,
arcMax = 40,
originCenter = device.originCenter || bedRound,
offset = originCenter ? {
x: 0,
y: 0,
z: 0
} : {
x: bedWidth/2,
y: isBelt ? 0 : bedDepth/2,
z: 0
},
tool = firstTool || 0,
extruder = extruders[tool],
offset_x = extruder.extOffsetX,
offset_y = extruder.extOffsetY,
extrudeSet = false,
nozzleTemp = process.firstLayerNozzleTemp || process.outputTemp,
bedTemp = process.firstLayerBedTemp || process.outputBedTemp,
fanSpeedSave = undefined,
fanSpeedBase = 0,
fanSpeed = 0,
lastType = undefined,
lastNozzleTemp = nozzleTemp,
lastBedTemp = bedTemp,
lastFanSpeed = fanSpeed,
subst = {
minx: bounds.min.x + offset.x,
maxx: bounds.max.x + offset.x,
miny: bounds.min.y + offset.y,
maxy: bounds.max.y + offset.y,
travel_speed: seekMMM,
retract_speed: retSpeed,
retract_distance: retDist,
temp: nozzleTemp,
temp_bed: bedTemp,
bed_temp: bedTemp,
fan_speed_base: fanSpeedBase,
fan_speed: fanSpeed,
speed: fanSpeed, // legacy
top: offset.y ? bedDepth : bedDepth/2,
left: offset.x ? 0 : -bedWidth/2,
right: offset.x ? bedWidth : bedWidth/2,
bottom: offset.y ? 0 : -bedDepth/2,
z_max: maxHeight,
layers: layers.length,
progress: 0,
nozzle: tool,
tool: tool,
model_labels: model_labels.sort().join(',')
},
pidx, path, out, speedMMM, emitMM, emitPerMM, lastp, laste, dist,
lines = 0,
bytes = 0,
bcos = Math.cos(Math.PI / 4),
icos = 1 / bcos,
inloops = 0,
arcQ = [],
minz = { x: Infinity, y: Infinity, z: Infinity },
// lenghts of each filament (by nozzle) consumed
segments = [];
let tools_used = Object.keys(tools);
for (let tool of tools_used) {
subst[`tool_used_${tool}`] = true;
}
subst.tool_count = tools_used.length;
// encodes an array offset as a single "on" bit in an
// 8 byte array, then converts the array to base64 which
// is what Bambu's M624 uses to flag an object as currently
// being printed. the array is presented at the top of the
// gcode as a comment: "; model label id: {model_labels}"
function encodeBitOffset(index) {
let bytes = new Uint8Array(8);
let byteIndex = Math.floor(index / 8);
let bitPosition = index % 8;
bytes[byteIndex] |= (1 << bitPosition);
return btoa(String.fromCharCode(...bytes));
}
function setTempFanSpeed(tempSpeed) {
if (tempSpeed > 0) {
fanSpeedSave = fanSpeedSave >= 0 ? fanSpeedSave : fanSpeed;
fanSpeed = tempSpeed;
subst.fan_speed = fanSpeed;
subst.fan_speed_base = tempSpeed;
} else {
fanSpeed = fanSpeedSave >= 0 ? fanSpeedSave : fanSpeed
fanSpeedSave = undefined;
subst.fan_speed = fanSpeed;
subst.fan_speed_base = fanSpeedBase;
}
}
// smallish band-aid. refactor above to remove redundancy
function updateParams(layer, params) {
// params = getRangeParameters(process, layer);
zhop = params.zHopDistance || 0; // range
retDist = params.outputRetractDist || 0; // range
retSpeed = params.outputRetractSpeed * 60 || 1; // range
retDwell = params.outputRetractDwell || 0; // range
timeDwell = retDwell / 1000;
nozzleTemp = layer === 0 ?
params.firstLayerNozzleTemp || params.outputTemp :
params.outputTemp || params.firstLayerNozzleTemp;
bedTemp = layer === 0 ?
params.firstLayerBedTemp || params.outputBedTemp :
params.outputBedTemp || params.firstLayerBedTemp;
fanSpeedBase = params.firstLayerFanSpeed || 0;
fanSpeed = layer >= (params.outputFanLayer ?? 1) ?
params.outputFanSpeed || 0 : fanSpeedBase;
Object.assign(subst, {
temp_bed: bedTemp,
bed_temp: bedTemp,
fan_speed: fanSpeed,
fan_speed_base: fanSpeedBase,
speed: fanSpeed, // legacy
retract_speed: retSpeed,
retract_distance: retDist,
temp: params.outputTemp, // range
temp_bed: params.outputBedTemp, // range
bed_temp: params.outputBedTemp, // range
});
}
let rloops = [];
if (oloops > 0) {
// if oloops set, loop entire part
rloops.push({
start: layers[0].slice.index,
end: Infinity,
iter: oloops - 1
});
} else if (oloops < 0) {
// if oloops negative, loop entire part to infinity
rloops.push({
start: layers[0].slice.index,
end: Infinity,
iter: 0
});
}
if (process.ranges) {
// collect loops from ranges and synth range array
for (let range of process.ranges) {
if (range.fields.outputLoops) {
rloops.push({
start: range.lo,
end: range.hi,
iter: range.fields.outputLoops - 1
});
}
}
}
let loops = isBelt && rloops.length ? rloops : undefined;
function append(line) {
if (line) {
lines++;
bytes += line.length;
output.append(line);
}
// batch gcode output to free memory in worker
// consider alternate transfer schemes like indexeddb
// since this transfers the burden to the main thread
if (!line || output.length > 1000) {
online(output.join("\n"));
output = [];
}
};
function appendSubPad(line, pad) {
appendSub(line, true);
}
let subon = true;
let ifhit = false;
// eval() based var substitution (from print)
// with logic flow IF / ELIF / ELSE / END
// IF / IF is valid but will not nest
function appendSub(line, pad) {
if (line.indexOf(';; DEFINE ') === 0) {
// ignore var declarations
} else if (line.indexOf(';; IF ') === 0) {
line = line.substring(6).trim();
let evil = print.constReplace(line, subst, 0, 666);
subon = evil;
ifhit = subon;
} else if (line.indexOf(';; ELIF ') === 0) {
line = line.substring(6).trim();
let evil = print.constReplace(line, subst, 0, 666);
subon = !subon && evil;
ifhit = ifhit || subon;
} else if (line.indexOf(';; ELSE') === 0) {
subon = !ifhit && !subon;
} else if (line.indexOf(';; END') === 0) {
subon = true;
ifhit = false;
} else if (subon) {
append(print.constReplace(line, subst, 0, pad));
}
}
function appendAll(arr) {
if (!arr) return;
if (!Array.isArray(arr)) arr = [ arr ];
arr.forEach(function(line) { append(line) });
}
function appendAllSub(arr, pad) {
if (!arr || arr.length === 0) return;
if (!Array.isArray(arr)) arr = [ arr ];
arr.forEach(function(line) { appendSub(line, pad) });
}
function appendTok() {
let arg = [...arguments].map(v => {
return typeof(v) === 'object' ? JSON.stringify(v) : v;
});
append(arg.join(''));
}
function preamble() {
append(`; Generated by Kiri:Moto ${version}`);
append(`; ${new Date().toString()}`);
appendSub("; Bed left:{left} right:{right} top:{top} bottom:{bottom}");
append(`; Bed type: ${bedType}`);
append(`; Target: ${filter[mode]}`);
// inject thumbnail preview
if (exportThumb && worker.snap) {
let { width, height, url } = worker.snap;
let data = url.substring(url.indexOf(',') + 1);
append(`; thumbnail begin ${width} ${height} ${data.length}`);
for (let i=0; i<data.length; i += 78) {
append(`; ${data.substring(i, i + 78)}`);
}
append('; thumbnail end');
}
append("; --- process ---");
for (let pk in process) {
appendTok("; ", pk, " = ", process[pk]);
}
append("; --- startup ---");
}
// looking for ";; PREAMBLE <MODE>" comment
let pre = 0;
let gcpre = [];
for (let line of device.gcodePre) {
line = line.trim();
if (line.indexOf(";; PREAMBLE") === 0) {
if (line === ';; PREAMBLE OFF') pre = 1;
else if (line === ';; PREAMBLE END') pre = 2;
else gcpre.push(pre = 123);
} else if (line.indexOf(";; AXISMAP ") === 0) {
let axmap = JSON.parse(line.substring(11).trim());
for (let key in axmap) {
axis[key] = ` ${axmap[key]}`;
}
} else {
gcpre.push(line);
}
}
if (pre === 0) preamble();
let t0 = false;
let t1 = false;
for (let line of gcpre) {
if (line === pre) {
preamble();
continue;
}
if (line.indexOf('T0') === 0) t0 = true; else
if (line.indexOf('T1') === 0) t1 = true; else
if (line.indexOf('M82') === 0) {
extrudeAbs = true;
extrudeSet = true;
} else
if (line.indexOf('M83') === 0) {
extrudeAbs = false;
extrudeSet = true;
} else
if (line.indexOf('G90') === 0 && !extrudeSet) extrudeAbs = true; else
if (line.indexOf('G91') === 0 && !extrudeSet) extrudeAbs = false; else
if (line.indexOf('G92') === 0) {
line.split(";")[0].split(' ').forEach(function (tok) {
let val = parseFloat(tok.substring(1) || 0) || 0;
switch (tok[0]) {
case 'X': pos.x = val; break;
case 'Y': pos.y = val; break;
case 'Z': pos.z = val; break;
case 'E': outputLength = val; break;
}
});
}
if (line.indexOf('E') > 0) {
line.split(";")[0].split(' ').forEach(function (tok) {
// use max E position from gcode-preamble
if (tok[0] == 'E') {
if (extrudeAbs) {
outputLength = Math.max(outputLength, parseFloat(tok.substring(1)) || 0);
emitted = outputLength;
} else {
emitted += parseFloat(tok.substring(1) || 0);
}
}
});
}
appendSubPad(line);
}
if (pre === 2) preamble();
function dwell(ms) {
time += ms/1000;
// break up dwell times over 4s because of
// limitations in some firmwares
while (ms > 0) {
let wait = Math.min(4000, ms);
append(`G4 P${wait}`);
ms -= wait;
if (ms) {
append('G1');
}
}
}
function retract(zhop) {
if (retracted) {
// console.log({double_retract: zhop});
return;
}
retracted = retDist;
if (fwRetract) {
append('G10');
} else {
moveTo({e:-retracted}, retSpeed, `e-retract ${retDist}`);
}
if (zhop) moveTo({z:zpos + zhop}, seekMMM, "z-hop start");
time += (retDist / retSpeed) * 60 * 2; // retraction time
}
function unretract() {
if (!retracted) {
return;
}
drainQ();
// console.log({engage:zhop});
// when enabled, resume previous Z
if (zhop && pos.z != zpos) moveTo({z:zpos}, seekMMM, "z-hop end");
// re-engage retracted filament
if (fwRetract) {
append('G11');
} else {
moveTo({e:retracted}, retSpeed, `e-engage ${retracted}`);
}
retracted = 0;
// optional dwell after re-engaging filament to allow pressure to build
if (retDwell) dwell(retDwell);
time += (retDist / retSpeed) * 60 * 2; // retraction time
}
let taxis = new THREE.Vector3( 1, 0, 0 );
let tcent = new THREE.Vector2( 0, 0 );
let angle = -Math.PI / 4;
let savePos = pos;
function pushPos(newpos, rate, comment) {
savePos = Object.clone(pos);
moveTo({
x: newpos.x + offset.x,
y: newpos.y + offset.y,
z: newpos.z + offset.z
}, rate, comment);
}
function popPos(rate, comment) {
moveTo(savePos, rate, comment);
}
function moveTo(newpos, rate, comment) {
let o = [!rate && !newpos.e ? 'G0' : 'G1'];
let emit = { x: false, y: false, z: false };
if (typeof newpos.x === 'number' && newpos.x !== pos.x) {
pos.x = newpos.x;
emit.x = true;
}
if (typeof newpos.y === 'number' && newpos.y !== pos.y) {
pos.y = newpos.y;
emit.y = true;
if (isBelt) emit.z = true;
}
if (typeof newpos.z === 'number' && newpos.z !== pos.z) {
pos.z = newpos.z;
emit.z = true;
if (isBelt) emit.y = true;
}
let epos = isBelt ? { x: pos.x, y: pos.y, z: pos.z } : pos;
if (isBelt) {
let zheight = path ? path.height || 0 : 0;
epos.x = originCenter ? -pos.x : device.bedWidth - pos.x;
epos.z = blastz = pos.z * icos;
epos.y = -pos.y + epos.z * bcos + belt_add_y;
lout = epos;
}
if (emit.x) o.append(axis.X).append(epos.x.toFixed(decimals));
if (emit.y) o.append(axis.Y).append(epos.y.toFixed(decimals));
if (emit.z) o.append(axis.Z).append(epos.z.toFixed(decimals));
if (debug) {
if (emit.x) minz.x = Math.min(minz.x, epos.x);
if (emit.y) minz.y = Math.min(minz.y, epos.y);
if (emit.z) minz.z = Math.min(minz.z, epos.z);
}
if (typeof newpos.e === 'number') {
outputLength += newpos.e;
if (extrudeAbs) {
// for cumulative (absolute) extruder positions
o.append(axis.E).append(outputLength.toFixed(decimals));
} else {
o.append(axis.E).append(newpos.e.toFixed(decimals));
}
}
if (zMoveMax && emit.z) {
rate = Math.min(zMoveMax, rate) * 60;
}
if (rate && rate != pos.f) {
o.append(" F").append(Math.round(rate));
pos.f = rate
}
if (comment) {
o.append(` ; ${comment}`);
}
if (o.length === 1) {
// console.trace({no_move: o, out, newpos, pos, lastp, emit});
return;
}
let line = o.join('');
if (last == line) {
// console.log({dup:line});
return;
}
last = line;
subst.pos_x = epos.x.round(decimals);
subst.pos_y = epos.y.round(decimals);
subst.pos_z = epos.z.round(decimals);
append(line);
}
// calc total distance traveled by head as proxy for progress
let allout = [];
let totaldistance = 0;
layers.forEach(function(outs) {
allout.appendAll(outs);
});
allout.forEachPair(function (o1, o2) {
totaldistance += o1.point.distTo2D(o2.point);
}, 1);
// retract before first move
retract();
while (layer < layers.length) {
path = layers[layer];
layerno = path.slice.index;
// range overrides
if (path.layer >= 0) {
updateParams(path.layer, path.params);
}
emitPerMM = extrudePerMM(
lineWidth || extruder.extNozzle,
extruder.extFilament,
path.layer === 0 ?
(process.firstSliceHeight || process.sliceHeight) : path.height);
zpos = path.z || zpos;
bmax = Math.max(bmax, pos.z * icos);
subst.z = subst.Z = zpos.round(3);
subst.e = subst.E = outputLength;
subst.layer = layer;
subst.height = path.height;
if (isBelt) {
pos.z = zpos;
}
if (gcodePause && pause.indexOf(layer) >= 0) {
appendAllSub(gcodePause)
}
let endloop = 0;
if (loops) {
for (let loop of loops) {
if (layerno === loop.start && !loop.started) {
loop.started = true;
append(`M808 L${loop.iter}`);
if (extrudeAbs) {
append(`G92 Z${lout.z.round(decimals)} E${outputLength.round(decimals)}`);
} else {
append(`G92 Z${lout.z.round(decimals)}`);
}
inloops++;
}
if (layerno === loop.end && !loop.ended) {
loop.ended = true;
endloop++;
inloops--;
}
}
}
if (gcodeLayer && gcodeLayer.length) {
appendAllSub(gcodeLayer);
} else {
append(`;; --- layer ${layer} (${subst.height.toFixed(3)} @ ${subst.z.round(3)}) ---`);
}
// layer temp and fan overrides at layer changes
if (fanSpeed !== lastFanSpeed) {
appendAllSub(gcodeFan);
lastFanSpeed = fanSpeed;
}
if (bedTemp !== lastBedTemp) {
append(`M140 S${bedTemp} T0`);
lastBedTemp = bedTemp;
}
if (nozzleTemp !== lastNozzleTemp) {
if (t0) append(`M104 S${nozzleTemp} T0`);
if (t1) append(`M104 S${nozzleTemp} T1`);
if (!(t0 || t1)) append(`M104 S${nozzleTemp} T${tool}`);
lastNozzleTemp = nozzleTemp;
}
// move Z to layer height
if (layer > 0 || !isBelt) {
moveTo({z:zpos}, seekMMM);
}
let cwidget;
// iterate through layer outputs
for (pidx=0; pidx<path.length; pidx++) {
out = path[pidx];
speedMMM = (out.speed || process.outputFeedrate) * 60; // range
// hint to controller that we're working on a specific object
// so that gcode between start/stop comments can be cancelled
if (out.widget !== cwidget) {
if (cwidget) {
append(`; end object id: ${cwidget.track.grid_id}`);
isBambu && append('M625');
}
if (out.widget) {
let off = model_labels.indexOf(out.widget.track.grid_id);
let b64 = encodeBitOffset(off);
append(`; start object id: ${out.widget.track.grid_id}`);
isBambu && append(`M624 ${b64}`);
}
cwidget = out.widget;
}
// emit comment on output type chage
if (last && out.type !== last.type) {
lastType = subst.feature = out.type;
if (gcodeFeature && gcodeFeature.length) {
appendAllSub(gcodeFeature);
} else {
append(`; feature ${out.type}`);
}
}
// look for extruder change, run scripts, recalc emit factor
if (out.tool !== undefined && out.tool != tool) {
segments.push({emitted, tool});
subst.last_tool = tool;
tool = out.tool;
subst.nozzle = subst.tool = tool;
extruder = extruders[tool];
offset_x = extruder.extOffsetX;
offset_y = extruder.extOffsetY;
emitPerMM = extrudePerMM(
lineWidth || extruder.extNozzle,
extruder.extFilament,
path.layer === 0 ?
(process.firstSliceHeight || process.sliceHeight) : path.height);
appendAllSub(gcodeChange ?? [ "T{tool}" ]);
}
// if no point in output, it's a dwell command
if (!out.point) {
dwell(out.speed);
continue;
}
let x = out.point.x + offset_x,
y = out.point.y + offset_y,
z = out.point.z;
// adjust for inversions and origin offsets
if (process.outputInvertX) x = -x;
if (process.outputInvertY) y = -y;
if (offset) {
x += offset.x;
y += offset.y;
}
dist = lastp ? lastp.distTo2D(out.point) : 0;
// re-engage post-retraction before new extrusion
if (out.emit && retracted) {
unretract();
}
// in belt mode, fan can change per segment (for base)
if (isBelt) {
setTempFanSpeed(out.fan);
if (fanSpeed !== lastFanSpeed) {
appendAllSub(gcodeFan);
lastFanSpeed = fanSpeed;
}
}
if (lastp && out.emit) {
if (arcDist) {
let rec = {e:out.emit, x, y, z, dist, emitPerMM, speedMMM};
arcQ.push(rec);
let deem = false; // do arcQ[0] and rec have differing emit values?
let depm = false; // do arcQ[0] and rec have differing emit speeds?
let desp = false; // do arcQ[0] and rec have differing move speeds?
if (arcQ.length > 1) {
let el = arcQ.length;
deem = arcQ[0].e !== rec.e;
depm = arcQ[0].emitPerMM !== rec.emitPerMM;
desp = arcQ[0].speedMMM !== rec.speedMMM;
}
// ondebug({arcQ});
if (arcQ.length > 2) {
let el = arcQ.length;
let e1 = arcQ[0]; // first in arcQ
let e2 = arcQ[Math.floor(el/2)]; // mid in arcQ
let e3 = arcQ[el-1]; // last in arcQ
let e4 = arcQ[el-2]; // second last in arcQ
let e5 = arcQ[el-3]; // third last in arcQ
let cc = util.center2d(e1, e2, e3, 1); // find center
let lr = util.center2d(e3, e4, e5, 1); // find local radius
let dc = 0;
let radFault = false;
if (lr) {
let angle = 2 * Math.asin(dist/(2*lr.r));
radFault = Math.abs(angle) > Math.PI * 2 / arcRes; // enforce arcRes(olution)
// if (arcQ.center) {
// arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 );
// let avg = arcQ.rSum / arcQ.center.length;
// radFault = radFault || Math.abs(avg - lr.r) / avg > arcDev; // eliminate sharps and flats when local rad is out of arcDev(iation)
// }
} else {
radFault = true;
}
if (cc) {
if ([cc.x,cc.y,cc.z,cc.r].hasNaN()) {
console.log({cc, e1, e2, e3});
}
if (arcQ.length === 3) {
arcQ.center = [ cc ];
arcQ.xSum = cc.x;
arcQ.ySum = cc.y;
arcQ.rSum = cc.r;
} else {
// check center point delta
arcQ.xSum = arcQ.center.reduce( function (t, v) { return t + v.x }, 0 );
arcQ.ySum = arcQ.center.reduce( function (t, v) { return t + v.y }, 0 );
arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 );
let dx = cc.x - arcQ.xSum / arcQ.center.length;
let dy = cc.y - arcQ.ySum / arcQ.center.length;
dc = Math.sqrt(dx * dx + dy * dy);
}
// if new point is off the arc
// if (deem || depm || desp || dc > arcDist || cc.r < arcMin || cc.r > arcMax || dist > cc.r) {
if (deem || depm || desp || dc * arcQ.center.length / arcQ.rSum > arcDist || dist > cc.r || cc.r > arcMax || radFault || !arcValid()) {
// let debug = [deem, depm, desp, dc * arcQ.center.length / arcQ.rSum > arcDist, dist > cc.r, cc.r > arcMax, radFault];
if (arcQ.length === 4) {
// not enough points for an arc, drop first point and recalc center
emitQrec(arcQ.shift());
let tc = util.center2d(arcQ[0], arcQ[1], arcQ[2], 1);
// the new center is invalid as well. drop the first point
if (!tc) {
emitQrec(arcQ.shift());
} else {
arcQ.center = [ tc ];
let angle = 2 * Math.asin(arcQ[1].dist/(2*tc.r));
if (Math.abs(angle) > Math.PI * 2 / arcRes) { // enforce arcRes on initial angle
emitQrec(arcQ.shift());
}
}
} else {
// enough to consider an arc, emit and start new arc
let defer = arcQ.pop();
drainQ();
// re-add point that was off the last arc
arcQ.push(defer);
}
} else {
// new point is on the arc
arcQ.center.push(cc);
}
} else {
// drainQ on invalid center
drainQ();
}
}
} else {
// emitMM = emitPerMM * out.emit * dist;
emitMM = extrudeMM(dist, emitPerMM, out.emit);
moveTo({x:x, y:y, e:emitMM}, speedMMM);
emitted += emitMM;
}
} else {
drainQ();
moveTo({x:x, y:y}, seekMMM);
// TODO disabling out of plane z moves until a better mechanism
// can be built that doesn't rely on computed zpos from layer heights...
// when making z moves (like polishing) allow slowdown vs fast seek
// let moveSpeed = (lastp && lastp.z !== z) ? speedMMM : seekMMM;
// moveTo({x:x, y:y, z:z}, moveSpeed);
}
// retract filament if point retract flag set
if (out.retract) {
drainQ();
retract(zhop);
}
// update time and distance (should calc in moveTo() instead)
if (dist && speedMMM) {
time += (dist / speedMMM) * 60 * timeFactor;
}
// if (isNaN(time)) {
// console.log({ layer, path, dist, speedMMM, timeFactor });
// throw "NaN";
// }
distance += dist;
subst.progress = progress = Math.round((distance / totaldistance) * 100);
// emit tracked progress
if (gcodeTrack && progress != lastProgress) {
appendAllSub(gcodeTrack);
lastProgress = progress;
}
lastp = out.point;
laste = out.emit;
}
layer++;
if (cwidget) {
append(`; end object id: ${cwidget.track.grid_id}`);
isBambu && append('M625');
cwidget = undefined;
}
// end open loop when detected
while (endloop-- > 0) {
append(`M808`);
}
drainQ();
}
function emitQrec(rec) {
let {e, x, y, dist, emitPerMM, speedMMM} = rec;
// emitMM = emitPerMM * e * dist;
emitMM = extrudeMM(dist, emitPerMM, e);
moveTo({x:x, y:y, e:emitMM}, speedMMM);
emitted += emitMM;
}
function drainQ() {
if (!arcDist) {
return;
}
if (arcQ.length > 4) {
// ondebug({arcQ});
let vec1 = new THREE.Vector2(arcQ[1].x - arcQ[0].x, arcQ[1].y - arcQ[0].y);
let vec2 = new THREE.Vector2(arcQ.center[0].x - arcQ[0].x, arcQ.center[0].y - arcQ[0].y);
let gc = vec1.cross(vec2) < 0 ? 'G2' : 'G3';
let from = arcQ[0];
let to = arcQ.peek();
arcQ.xSum = arcQ.center.reduce( function (t, v) { return t + v.x }, 0 );
arcQ.ySum = arcQ.center.reduce( function (t, v) { return t + v.y }, 0 );
arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 );
let cl = arcQ.center.length;
let cc;
let angle = util.thetaDiff(
Math.atan2((from.y - arcQ.ySum / cl), (from.x - arcQ.xSum / cl)),
Math.atan2((to.y - arcQ.ySum / cl), (to.x - arcQ.xSum / cl)),
gc === "G2"
);
if (Math.abs(angle) <= 3 * Math.PI / 4) {
cc = util.center2pr(from, to, arcQ.rSum / cl, gc === "G3");
}
if (!cc) {
cc = {x:arcQ.xSum/cl, y:arcQ.ySum/cl, z:arcQ[0].z, r:arcQ.rSum/cl};
}
// first arc point
emitQrec(from);
// console.log(arcQ.slice(), arcQ.center);
// console.log({first: from, last: arcQ.peek(), center: cc});
// rest of arc to final point
let dist = arcQ.slice(1).map(v => v.dist).reduce((a,v) => a+v);
let emit = from.e;//arcQ.slice(1).map(v => v.e).reduce((a,v) => a+v);
emit = (from.emitPerMM * emit * dist);
outputLength += emit;
emitted += emit;
if (extrudeAbs) {
emit = outputLength;
}
// XYR form
// let pre = `${gc} X${to.x.toFixed(decimals)} Y${to.y.toFixed(decimals)} R${cc.r.toFixed(decimals)} E${emit.toFixed(decimals)}`;
// XYIJ form
let pre = `${gc} X${to.x.toFixed(decimals)} Y${to.y.toFixed(decimals)} I${(cc.x - pos.x).toFixed(decimals)} J${(cc.y - pos.y).toFixed(decimals)} E${emit.toFixed(decimals)}`;
let add = pos.f !== from.speedMMM ? ` E${from.speedMMM}` : '';
append(`${pre}${add} ; merged=${cl-1} len=${dist.toFixed(decimals)} cp=${cc.x.round(2)},${cc.y.round(2)}`);
pos.x = to.x;
pos.y = to.y;
pos.z = to.z;
} else {
for (let rec of arcQ) {
emitQrec(rec);
}
}
arcQ.length = 0;
arcQ.center = undefined;
}
// comprehensive arc validator
function arcValid() {
if (arcQ.length < 3) {
return false;
}
let globalCenters = []; // see how a point first the curve within the context of the arc's end points.
for (let i = 0; i < arcQ.length - 2; i++) {
let cc = util.center2d(arcQ[0], arcQ[i+1], arcQ[arcQ.length - 1], 1);
if (!cc) {
return false;
}
globalCenters.push(cc);
}
let ac = { // average center
x:globalCenters.reduce( (t,v) => t + v.x , 0) / (globalCenters.length),
y:globalCenters.reduce( (t,v) => t + v.y , 0) / (globalCenters.length),
z:globalCenters.reduce( (t,v) => t + v.z , 0) / (globalCenters.length),
r:globalCenters.reduce( (t,v) => t + v.r , 0) / (globalCenters.length)
};
// make sure centers are within specified tolerance
for (let cc of globalCenters) {
let dc = Math.sqrt(Math.pow(cc.x - ac.x, 2) + Math.pow(cc.y - ac.y, 2));
if (dc / ac.r > arcDist) {
return false;
}
}
// make sure radii are within specified tolerance
for (let point of arcQ) {
let rad = Math.sqrt(Math.pow(point.x - ac.x, 2) + Math.pow(point.y - ac.y, 2));
if (Math.abs(rad - ac.r) > ac.r * arcDist) {
return false;
}
}
// enforce arcRes(olution)
for (let i = 1; i < arcQ.length; i++) {
let angle = 2 * Math.asin(arcQ[i].dist/(2*ac.r));
if (Math.abs(angle) > Math.PI * 2 / arcRes) {
return false;
}
}
// check points in the context of neighbors
for (let i = 0; i < arcQ.length - 2; i++) {
let cc = util.center2d(arcQ[i], arcQ[i+1], arcQ[i+2], 1);
if (!cc || Math.abs((cc.r - ac.r) / cc.r) > arcDev) {
return false;
}
}
return true;
}
if (inloops) {
append(`M808`);
}
segments.push({emitted, tool});
subst.material = util.round(emitted,2);
subst.time = util.round(time,2);
subst['print_time'] = subst.time;
append("; --- shutdown ---");
appendAllSub(device.gcodePost);
append(`; --- filament used: ${subst.material} mm ---`);
append(`; --- print time: ${time.toFixed(0)}s ---`);
// force emit of buffer
append();
// console.log({segments, emitted, outputLength});
print.distance = emitted;
print.lines = lines;
print.bytes = bytes + lines - 1;
print.time = time;
print.labels = model_labels;
if (debug) {
console.log('segments', segments);
console.log('minz', minz);
}
};

320
src/kiri/mode/fdm/fill.js Normal file
View file

@ -0,0 +1,320 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { newPolygon } from '../../../geo/polygon.js';
import { slice as gyroid_slice } from '../../../geo/gyroid.js';
export const fill_fixed = {
hex: fillHexFull,
grid: fillGrid,
triangle: fillTriangle
};
export const fill = {
hex: fillHexFull,
grid: fillGrid,
gyroid: fillGyroid,
triangle: fillTriangle,
linear: fillLinear,
cubic: fillCubic
};
const DEG2RAD = Math.PI / 180;
function fillHexFull(target) {
fillHex(target, true);
}
/**
* emitter creates a hex infill pattern and sends to target
*
* @param {Object} target
* @param {boolean} full continuous walls
*/
function fillHex(target, full) {
// compute segment lengths (vert/horiz and 45)
let spacing = target.offset();
// let vhlen = (1 / target.density()) * (target.lineWidth() + spacing);
let vhlen = (1 / target.density()) * target.lineWidth() * 0.5;
let anxlen = (Math.cos(30 * DEG2RAD) * vhlen).round(7);
let anylen = (Math.sin(30 * DEG2RAD) * vhlen).round(7);
let bounds = target.bounds();
let even = true;
let evenZ = target.zIndex() % 2 === 0;
let maxy = bounds.max.y + (vhlen + anylen * 2);
let x, y;
if (full || evenZ) {
x = bounds.min.x;
for (;;) {
if (even && x > bounds.max.x) break;
if (!even && x > bounds.max.x + anxlen + spacing) break;
y = bounds.min.y;
target.newline();
while (y <= maxy) {
target.emit(x,y);
y += vhlen;
target.emit(x,y);
if (even) x += anxlen; else x -= anxlen;
y += anylen;
target.emit(x,y);
y += vhlen;
target.emit(x,y);
if (even) x -= anxlen; else x += anxlen;
y += anylen;
}
x += spacing;
if (even) x += (anxlen * 2);
even = !even;
target.newline();
}
} else {
y = bounds.min.y + vhlen;
for (;;) {
if (even && y > bounds.max.y) break;
if (!even && y > bounds.max.y + anylen) break;
x = bounds.min.x;
target.newline();
while (x < bounds.max.x) {
target.emit(x,y);
if (even) y += anylen; else y -= anylen;
x += anxlen;
target.emit(x,y);
x += spacing;
target.emit(x,y);
if (even) y -= anylen; else y += anylen;
x += anxlen;
target.emit(x,y);
x += spacing;
}
y += vhlen;
if (even) y += (anylen * 2);
even = !even;
target.newline();
}
}
}
function fillGyroid(target) {
let bounds = target.bounds();
let height = target.zHeight();
let span_x = bounds.max.x - bounds.min.x;
let span_y = bounds.max.y - bounds.min.y;
let density = target.density();
let tile = 1 + (1 - density) * 15;
let tile_x = span_x / tile;
let tile_y = span_y / tile;
let tile_z = 1 / tile;
let gen = gyroid_slice(target.zValue() * tile_z, (1 - density) * 500);
let polys = [];
if (gen.dir == 'lr') {
for (let ty=0; ty<=tile_y; ty++) {
for (let tx=0; tx<=tile_x; tx++) {
for (let poly of gen.polys) {
target.newline();
let points = poly.map(el => {
return {
x: el.x * tile + tx * tile + bounds.min.x,
y: el.y * tile + ty * tile + bounds.min.y,
z: 0
}
});
polys.push(newPolygon().setOpen(true).addObj(points));
}
}
polys = connectOpenPolys(polys);
}
} else {
for (let tx=0; tx<=tile_x; tx++) {
for (let ty=0; ty<=tile_y; ty++) {
for (let poly of gen.polys) {
target.newline();
let points = poly.map(el => {
return {
x: el.x * tile + tx * tile + bounds.min.x,
y: el.y * tile + ty * tile + bounds.min.y,
z: 0
}
});
polys.push(newPolygon().setOpen(true).addObj(points));
}
}
polys = connectOpenPolys(polys);
}
}
for (let poly of polys.filter(p => p.perimeter() > 2)) {
target.newline();
for (let point of poly.points) {
target.emit(point.x, point.y);
}
}
}
function connectOpenPolys(noff, dist = 0.1) {
if (noff.length <= 1) {
return noff;
}
let heal = 0;
// heal/rejoin open segments that have close endpoints
outer: for(;; heal++) {
let ntmp = noff, tlen = ntmp.length;
for (let i=0; i<tlen; i++) {
let s1 = ntmp[i];
if (!s1 || !s1.open) continue;
for (let j=i+1; j<tlen; j++) {
let s2 = ntmp[j];
if (!s2 || !s2.open) continue;
if (s1.last().distTo2D(s2.first()) <= dist) {
s1.addPoints(s2.points);
ntmp[j] = null;
continue outer;
}
if (s1.first().distTo2D(s2.last()) <= dist) {
s2.addPoints(s1.points);
ntmp[i] = null;
continue outer;
}
if (s1.first().distTo2D(s2.first()) <= dist) {
s1.reverse();
s1.addPoints(s2.points);
ntmp[j] = null;
continue outer;
}
if (s1.last().distTo2D(s2.last()) <= dist) {
s2.reverse();
s1.addPoints(s2.points);
ntmp[j] = null;
continue outer;
}
}
}
break;
}
if (heal > 0) {
// cull nulls
noff = noff.filter(o => o);
}
return noff;
}
function fillGrid(target) {
let bounds = target.bounds();
let height = target.zHeight();
let span_x = bounds.max.x - bounds.min.x;
let span_y = bounds.max.y - bounds.min.y;
let offset = target.offset() / 2;
let tile = (1 / target.density()) * target.lineWidth();
let tile_x = tile + offset;
let tile_xc = span_x / tile_x;
let tile_yc = span_y / tile;
for (let tx=0; tx<=tile_xc; tx++) {
target.newline();
for (let ty=0; ty<=tile_yc; ty++) {
let bx = tx * tile_x + bounds.min.x;
let by = ty * tile + bounds.min.y;
if ((tx + ty) % 2) {
target.emit(bx, by);
target.emit(bx + tile_x - offset, by + tile);
} else {
target.emit(bx + tile_x - offset, by);
target.emit(bx, by + tile);
}
}
}
}
function fillCubic(target) {
let bounds = target.bounds();
let span = Math.max(
bounds.max.x - bounds.min.x,
bounds.max.y - bounds.min.y
);
let steps = Math.floor((span / target.lineWidth()) * target.density());
let step = span / steps;
let ztype = Math.floor(target.zIndex() / target.repeat()) % 3;
if (ztype === 1) {
for (let tx=bounds.min.x; tx<=bounds.max.x; tx += step) {
target.newline();
target.emit(tx, bounds.min.y);
target.emit(tx, bounds.max.y);
}
} else if (ztype === 0) {
for (let ty=bounds.min.y; ty<=bounds.max.y; ty += step) {
target.newline();
target.emit(bounds.min.x, ty);
target.emit(bounds.max.x, ty);
}
} else {
step *- Math.sqrt(2);
for (let tx=bounds.min.x; tx<=bounds.max.x; tx += step) {
target.newline();
target.emit(tx, bounds.min.y);
target.emit(tx + 1000, bounds.max.y + 1000);
}
for (let ty=bounds.min.y; ty<=bounds.max.y; ty += step) {
target.newline();
target.emit(bounds.min.x, ty);
target.emit(bounds.min.x + 1000, ty + 1000);
}
}
}
function fillLinear(target) {
let bounds = target.bounds();
let span = Math.max(
bounds.max.x - bounds.min.x,
bounds.max.y - bounds.min.y
);
let steps = Math.floor((span / target.lineWidth()) * target.density());
let step = span / steps;
let ztype = Math.floor(target.zIndex() / target.repeat()) % 2;
if (ztype === 1) {
for (let tx=bounds.min.x; tx<=bounds.max.x; tx += step) {
target.newline();
target.emit(tx, bounds.min.y);
target.emit(tx, bounds.max.y);
}
} else if (ztype === 0) {
for (let ty=bounds.min.y; ty<=bounds.max.y; ty += step) {
target.newline();
target.emit(bounds.min.x, ty);
target.emit(bounds.max.x, ty);
}
}
}
function fillTriangle(target) {
let bounds = target.bounds();
let span_x = bounds.max.x - bounds.min.x;
let span_y = bounds.max.y - bounds.min.y;
let offset = target.offset();
let line_w = target.lineWidth() / 2;
let tile = (1 / target.density()) * (target.lineWidth() * 1.25);
let tile_x = tile + offset*2 + line_w;
let tile_xc = span_x / tile_x;
let tile_yc = span_y / tile;
for (let tx=0; tx<=tile_xc; tx++) {
target.newline();
for (let ty=0; ty<=tile_yc; ty++) {
let bx = tx * tile_x + bounds.min.x;
let by = ty * tile + bounds.min.y;
if ((tx + ty) % 2) {
target.emit(bx, by);
target.emit(bx + tile_x - offset - line_w, by + tile);
} else {
target.emit(bx + tile_x - offset - line_w, by);
target.emit(bx, by + tile);
}
}
}
for (let tx=0; tx<=tile_xc; tx++) {
let bx = tx * tile_x + bounds.min.x;
let xp = bx + tile_x - line_w/2 - offset/2;
target.newline();
target.emit(xp, bounds.min.y);
target.emit(xp, bounds.max.y);
}
}

313
src/kiri/mode/fdm/post.js Normal file
View file

@ -0,0 +1,313 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { base } from '../../../geo/base.js';
import { polygons as POLY, fillArea } from '../../../geo/polygons.js';
import { getRangeParameters } from './driver.js';
import { slicer } from '../../../geo/slicer.js';
slicer.slicePost.FDM = slicePost;
/**
* may run in minion or worker context. do not create objects
* that will not quickly encode in threaded mode. add to existing
* data object. return is ignored.
*/
export function slicePost(data, options) {
const { z, lines, groups } = data;
const { useAssembly, post_args, zIndexes } = options;
const { process, isSynth, vaseMode } = post_args;
const { shellOffset, fillOffset, clipOffset } = post_args;
data.tops = POLY.nest(groups);
if (isSynth) {
const process = post_args.process;
if (process.sliceSupportGrow > 0) {
// expand synth supports here so they can be clipped later
data.tops = POLY.expand(data.tops, process.sliceSupportGrow, data.z, []);
}
// do not shell synth widgets because
// they will be clipped against peers later
// which requires shelling post-clip
// but we still need to generate tops
delete data.groups;
return;
}
const index = zIndexes.indexOf(z);
const range = getRangeParameters(process, index);
// calculate fractional shells
let shellFrac = (range.sliceShells - (range.sliceShells | 0));
let sliceShells = range.sliceShells | 0;
if (shellFrac) {
let v1 = shellFrac > 0.5 ? 1 - shellFrac : shellFrac;
let v2 = 1 - v1;
let parts = Math.round(v2/v1) + 1;
let rem = index % parts;
let trg = shellFrac > 0.5 ? 1 : parts - 1;
sliceShells += rem >= trg ? 1 : 0;
}
const isFirst = index === 0;
const height = process.sliceHeight;
const spaceMult = isFirst ? process.firstLayerLineMult || 1 : 1;
const count = isSynth ? 1 : sliceShells;
const offset = shellOffset * spaceMult;
const fillOff = fillOffset * spaceMult;
const thinType = isSynth ? undefined : process.sliceDetectThin;
const nutops = [];
// co-locate shell processing with top generation in slicer
for (let top of data.tops) {
nutops.push(doTopShells(z, top, count, offset/2, offset, fillOff, {
thinType,
vaseMode,
useAssembly
}));
}
data.clip = clipOffset ? POLY.offset(nutops.map(t => t.simple), clipOffset) : undefined;
data.tops = nutops;
delete data.groups;
};
function offset_default(params) {
let { z, top, count, top_poly, offset1, offsetN, wasm, last, gaps } = params;
// standard wall offsetting strategy
POLY.offset(
top_poly,
[-offset1, -offsetN],
{
z,
wasm,
count,
outs: top.shells,
flat: true,
call: (polys, onCount) => {
last = polys;
// mark each poly with depth (offset #) starting at 0
for (let p of polys) {
p.depth = count - onCount;
if (p.fill_off) p.fill_off.forEach(function(pi) {
// use negative offset for inners
pi.depth = -(count - onCount);
});
// mark inner depth to match parent
if (p.inner) {
for (let pi of p.inner) {
pi.depth = p.depth;
}
}
}
}
}
);
return { last, gaps };
}
function thin_type_1(params) {
let { z, top, count, top_poly, offsetN, last, gaps } = params;
top.thin_fill = [];
top.fill_sparse = [];
let layers = POLY.inset(top_poly, offsetN, count, z, true);
last = layers.last().mid;
top.shells = layers.map(r => r.mid).flat();
top.gaps = layers.map(r => r.gap).flat();
let off = offsetN;
let min = off * 0.75;
let max = off * 4;
for (let poly of layers.map(r => r.gap).flat()) {
let centers = poly.centers(off/2, z, min, max, {lines:false});
top.fill_sparse.appendAll(centers);
}
return { last, gaps };
}
function thin_type_2(params) {
let { z, top, count, top_poly, offset1, offsetN, last, gaps, wasm } = params;
top.gaps = gaps;
top.thin_fill = [];
let oso = {z, count, gaps: [], outs: [], minArea: 0.05, wasm};
POLY.offset(top_poly, [-offset1, -offsetN], oso);
oso.outs.forEach((polys, i) => {
polys.forEach(p => {
p.depth = i;
if (p.fill_off) {
p.fill_off.forEach(pi => pi.depth = i);
}
if (p.inner) {
for (let pi of p.inner) {
pi.depth = p.depth;
}
}
top.shells.push(p);
});
last = polys;
});
// slice.solids.trimmed = slice.solids.trimmed || [];
oso.gaps.forEach((polys, i) => {
let off = (i == 0 ? offset1 : offsetN);
polys = POLY.offset(polys, -off * 0.8, {z, minArea: 0, wasm});
top.thin_fill.appendAll(cullIntersections(
fillArea(polys, 45, off/2, [], 0.01, off*2),
fillArea(polys, 135, off/2, [], 0.01, off*2),
));
gaps.push(...polys);
});
return { last, gaps };
}
function thin_type_3(params) {
let { z, top, count, offsetN, last, gaps } = params;
top.thin_fill = [];
top.fill_sparse = [];
let { noodle, remain } = top.poly.noodle(offsetN);
top.shells = noodle;
top.gaps = remain;
// top.fill_off = noodle;
// if (remain.length) {
// top.gaps = [];
// for (let poly of remain) {
// let { noodle } = poly.noodle(offsetN);
// console.log({ next_noodle: noodle });
// top.gaps.push(...noodle);
// }
// }
return { last, gaps };
}
/**
* may run in minion or worker context. performs shell offsetting
* including thin wall detection when enabled
*/
export function doTopShells(z, top, count, offset1, offsetN, fillOffset, opt = {}) {
// pretend we're a top object in minions
if (!top.poly) {
top = { poly: top };
}
// add simple (low rez poly) where less accuracy is OK
top.simple = top.poly.simple();
let wasm = opt.useAssembly;
let top_poly = [ top.poly ];
if (opt.vaseMode) {
// remove top poly inners in vase mode
top.poly = top.poly.clone(false);
}
top.shells = []; // strategies output shells here
top.fill_off = []; // offset from innermost shell for infill
top.fill_lines = []; // fill lines inside fill_off
let last = [],
gaps = [];
if (count) {
// permit offset of 0 for laser and drag knife
if (offset1 === 0 && count === 1) {
last = top_poly.clone(true);
top.shells = last;
} else {
// heal top open polygons if the ends are close (benchy tilt test)
top_poly.forEach(p => { if (p.open) {
let dist = p.first().distTo2D(p.last());
if (dist < 1) p.open = false;
} });
let ret = { last, gaps };
switch (opt.thinType) {
case "type 1":
ret = thin_type_1({ z, top, count, top_poly, offsetN, fillOffset });
break;
case "type 2":
ret = thin_type_2({ z, top, count, top_poly, offset1, offsetN, fillOffset, gaps, wasm });
break;
case "type 3":
ret = thin_type_3({ z, top, count, top_poly, offsetN });
fillOffset = 0;
break;
default:
ret = offset_default({ z, top, count, top_poly, offset1, offsetN, wasm });
break;
}
last = ret.last || last;
gaps = ret.gaps || gaps;
}
} else {
// no shells, just infill, is permitted
last = [top.poly];
}
// generate fill offset poly set from last offset to top.fill_off
if (fillOffset && last.length > 0) {
// if gaps present, remove that area from fill inset
if (gaps.length) {
let nulast = [];
POLY.subtract(last, gaps, nulast, null, z);
last = nulast;
}
last.forEach(function(inner) {
POLY.offset([inner], -fillOffset, {outs: top.fill_off, flat: true, z});
});
}
// for diffing
top.last = last;
// top.last_simple = last.map(p => p.clean(true, undefined, config.clipper / 10));
return top;
}
/**
* given an array of arrays of points (lines), eliminate intersections
* between groups, then return a unified array of shortest non-intersects.
*/
function cullIntersections() {
function toLines(pts) {
let lns = [];
for (let i=0, il=pts.length; i<il; i += 2) {
lns.push({a: pts[i], b: pts[i+1], l: pts[i].distTo2D(pts[i+1])});
}
return lns;
}
let aOa = [...arguments].filter(t => t);
if (aOa.length < 1) return;
let aa = toLines(aOa.shift());
while (aOa.length) {
let bb = toLines(aOa.shift());
loop: for (let i=0, il=aa.length; i<il; i++) {
let al = aa[i];
if (al.del) {
continue;
}
for (let j=0, jl=bb.length; j<jl; j++) {
let bl = bb[j];
if (bl.del) {
continue;
}
if (base.util.intersect(al.a, al.b, bl.a, bl.b, base.key.SEGINT)) {
if (al.l < bl.l) {
bl.del = true;
} else {
al.del = true;
}
continue;
}
}
}
aa = aa.filter(l => !l.del).concat(bb.filter(l => !l.del));
}
let good = [];
for (let i=0, il=aa.length; i<il; i++) {
let al = aa[i];
good.push(al.a);
good.push(al.b);
}
return good.length > 2 ? good : [];
}

1504
src/kiri/mode/fdm/prepare.js Normal file

File diff suppressed because it is too large Load diff

1929
src/kiri/mode/fdm/slice.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
export function init() {
// TODO
}

View file

@ -0,0 +1,860 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { base } from '../../../geo/base.js';
import { Packer } from '../../core/pack.js';
import { newBounds } from '../../../geo/bounds.js';
import { newPoint } from '../../../geo/point.js';
import { newPrint } from '../../core/print.js';
import { newSlopeFromAngle } from '../../../geo/slope.js';
import { polygons as POLY } from '../../../geo/polygons.js';
import { render } from '../../core/render.js';
import { slicer } from '../../../geo/slicer.js';
import { newSlice } from '../../core/slice.js';
export const TYPE = {
LASER: 0,
DRAG: 1,
WJET: 2,
WEDM: 3
};
function init(worker) {
// console.log({ LASER_INIT: worker });
}
function polyLabel(poly, label) {
console.log('label', label);
}
function sliceEmitObjects(print, slice, groups, opt = { }) {
let process = print.settings.process,
{ ctOutMark, ctOutStack, ctOutGroup, ctOutShaper, outputLaserLabel } = process,
grouped = ctOutMark || ctOutGroup,
label = false && outputLaserLabel,
simple = opt.simple || false,
emit = { in: [], out: [], mark: [] },
lastEmit = opt.lastEmit,
zcolor = print.settings.process.ctOutZColor;
function polyOut(poly, group, type, indexed) {
if (!poly) {
return;
}
if (Array.isArray(poly)) {
poly.forEach((pi, index) => {
polyOut(pi, group, type, indexed ? index : undefined);
});
} else {
let pathOpt = zcolor ? {extrude: slice.z, rate: slice.z} : {extrude: 1, rate: 1};
if (type === "mark") {
pathOpt.rate = 0.001;
pathOpt.extrude = 2;
} else if (label && type === "out") {
let lbl = indexed !== undefined ? `${slice.index}-${indexed}` : `${slice.index}`;
polyLabel(poly, lbl);
}
if (simple) {
pathOpt.simple = true;
}
if (poly.open) {
pathOpt.open = true;
}
emit[type].push(poly);
print.PPP(poly, group, pathOpt);
// when stacking (top down widget slices), if the last layer is fully contained
// by the current layer, then it is "marked" onto the current layer in a different color
if (ctOutMark && type === "out" && lastEmit) {
for (let out of lastEmit.out) {
if (out.isInside(poly)) {
polyOut(out.clone().setZ(poly.getZ()), group, "mark");
}
}
}
}
}
if (grouped) {
let group = [];
let outer = slice.offset;
let inner = outer.map(poly => poly.inner || []).flat();
// preserve slice thickness in group
group.thick = slice.thick;
// cut inside before outside
polyOut(inner, group, "in", outer.length > 1);
polyOut(outer, group, "out", outer.length > 1);
groups.push(group);
} else {
for (let top of slice.offset) {
let group = [];
group.thick = slice.thick;
polyOut([ top ], group, "in");
polyOut(top.inner || [], group, "out");
groups.push(group);
}
}
return emit;
};
// convert arc into line segments
function arc(center, rad, s1, s2, out) {
let a1 = s1.angle;
let step = 5;
let diff = s1.angleDiff(s2, true);
let ticks = Math.abs(Math.floor(diff / step));
let dir = Math.sign(diff);
let off = (diff % step) / 2;
if (off == 0) {
ticks++;
} else {
out.push( center.projectOnSlope(s1, rad) );
}
while (ticks-- > 0) {
out.push( center.projectOnSlope(newSlopeFromAngle(a1 + off), rad) );
a1 += step * dir;
}
out.push( center.projectOnSlope(s2, rad) );
}
// start to the "left" of the first point
function addKnifeRadii(poly, tipoff) {
poly.setClockwise();
let oldpts = poly.points.slice();
// find leftpoint and make that the first point
let start = oldpts[0];
let startI = 0;
for (let i=1; i<oldpts.length; i++) {
let pt = oldpts[i];
if (pt.x < start.x || pt.y > start.y) {
start = pt;
startI = i;
}
}
if (startI > 0) {
oldpts = oldpts.slice(startI,oldpts.length).appendAll(oldpts.slice(0,startI));
}
let lastpt = oldpts[0].clone().move({x:-tipoff,y:0,z:0});
let lastsl = lastpt.slopeTo(oldpts[0]).toUnit();
let newpts = [ lastpt, lastpt = oldpts[0].clone() ];
let tmp;
for (let i=1; i<oldpts.length + 1; i++) {
let nextpt = oldpts[i % oldpts.length];
let nextsl = lastpt.slopeTo(nextpt).toUnit();
if (lastsl.angleDiff(nextsl) >= 10) {
if (lastpt.distTo2D(nextpt) >= tipoff) {
arc(lastpt, tipoff, lastsl, nextsl, newpts);
} else {
// todo handle short segments
// newpts.push(lastpt.projectOnSlope(lastsl, tipoff) );
// newpts.push( lastpt.projectOnSlope(nextsl, tipoff) );
}
}
newpts.push(nextpt);
lastsl = nextsl;
lastpt = nextpt;
}
newpts.push( tmp = lastpt.projectOnSlope(lastsl, tipoff) );
newpts.push( tmp.clone().move({x:tipoff, y:0, z: 0}) );
poly.open = true;
poly.points = newpts;
}
/**
* DRIVER SLICE CONTRACT
*
* @param {Object} settings
* @param {Widget} Widget
* @param {Function} onupdate (called with % complete and optional message)
* @param {Function} ondone (called when complete with an array of Slice objects)
*/
function laser_slice(settings, widget, onupdate, ondone) {
let proc = settings.process;
let offset = proc.ctSliceKerf || 0;
let color = settings.controller.dark ? 0xbbbbbb : 0;
if (proc.ctSliceHeight < 0) {
return ondone("invalid slice height");
}
let { ctSliceSingle, ctSliceHeight, ctSliceHeightMin, ctOmitInner } = proc;
let bounds = widget.getBoundingBox();
let points = widget.getPoints();
let indices = [];
slicer.slice(points, {
zMin: bounds.min.z,
zMax: bounds.max.z,
zGen(zopt) {
let { zMin, zMax, zIndexes } = zopt;
if (ctSliceSingle) {
indices = [ ctSliceHeight ];
} else if (ctSliceHeight) {
for (let z = zMin + ctSliceHeight / 2; z < zMax; z += ctSliceHeight) {
indices.push(z);
}
indices;
} else {
for (let i = 1; i < zIndexes.length; i++) {
indices.push((zIndexes[i-1] + zIndexes[i]) / 2);
}
// discard layers too
if (ctSliceHeightMin) {
let last;
indices = indices.filter(v => {
let ok = true;
if (last !== undefined && Math.abs(v - last) < ctSliceHeightMin) {
ok = false;
} else {
last = v;
}
return ok;
});
}
}
return indices;
},
onupdate(v) {
onupdate(v);
}
}).then(output => {
// ensure slice order is high Z to low Z for stacking
widget.slices = output.slices.map(data => {
let { z, lines, groups } = data;
let tops = POLY.nest(groups);
// for WireEDM, we commonly omit inner polys because we can't
// navigate to cut them separately unlike laser, water, drag
if (ctOmitInner) tops.forEach(top => delete top.inner);
let slice = newSlice(z).addTops(tops);
slice.index = indices.indexOf(z);
// create top offsets (even if 0)
let offsets = slice.offset = offset ?
POLY.offset(tops, offset, {z: slice.z, miter: 2 / offset}) : tops;
slice.output().setLayer("object", { line: 0x888800 }).addPolys(tops);
slice.output().setLayer("cut", { line: color }).addPolys(offsets);
return slice;
}).sort((a,b) => b.z - a.z);
ondone();
});
};
/**
* DRIVER PRINT CONTRACT
*
* @param {Object} print state object
* @param {Function} update incremental callback
*/
async function laser_prepare(widgets, settings, update) {
let worker = self.kiri_worker.current,
device = settings.device,
process = settings.process,
print = worker.print = newPrint(settings, widgets),
isWire = worker.mode === 'WEDM',
isKnife = worker.mode === 'DRAG',
isLaser = worker.mode === 'LASER',
output = print.output = [],
totalSlices = 0,
slices = 0,
maxZ = 0,
{ bedWidth, bedDepth } = device,
{ ctOutLayer, ctOutTileSpacing } = process,
{ ctOriginCenter, ctOriginBounds } = process,
{ ctOriginOffX, ctOriginOffY } = process,
{ ctOutStack, ctOutMerged, ctOutKnifeTip, ctOutShaper } = process;
ctOutStack = ctOutStack && isLaser;
// shim to adapt older code -- should be refactored
// let pppo = [];
let pppg = [];
let tile = [];
let tiles = [];
print.PPP = function(poly, group, options) {
if (!pppg.contains(group)){
pppg.push(group);
tiles.push(tile = []);
tile.num = tiles.length;
}
// pppo.push({ poly, group, options });
tile.push({ poly, group, options });
};
// filter ignored widgets
widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled);
// find max layers (for updates) and Z (for shaper)
for (let widget of widgets) {
totalSlices += widget.slices.length;
maxZ = Math.max(maxZ, widget.bounds.max.z);
}
// emit objects from each slice into output array
let layers = [];
for (let widget of widgets) {
if (ctOutStack) {
// 3d stack output, no merging or layout
for (let slice of widget.slices) {
let group = [];
let polys = [];
for (let poly of slice.offset) {
if (poly.inner) {
polys.push(...poly.inner);
}
polys.push(poly);
}
for (let poly of polys) {
print.PPP(poly, group, {
extrude: 1,
rate: 1,
});
}
}
} else if (ctOutMerged) {
// slice stack merging, no layout
// there are no inner vs outer polys
// merged polys "increase" their color / weight
let merged = [];
for (let slice of widget.slices) {
let polys = [];
slice.offset.clone(true).forEach(p => p.flattenTo(polys));
for (let poly of polys) {
let match = false;
for (let i=0; i<merged.length; i++) {
let mp = merged[i];
if (poly.isEquivalent(mp)) {
// increase weight
match = true;
mp.depth++;
}
}
if (!match) {
poly.depth = 1;
merged.push(poly);
}
}
update((slices++ / totalSlices) * 0.5, "prepare");
}
let gather = [];
for (let poly of merged) {
if (isKnife) {
addKnifeRadii(poly, ctOutKnifeTip);
}
print.PPP(poly, gather, {
extrude: poly.depth,
rate: poly.depth * 10
})
}
layers.push(gather);
} else {
// output a layer for each slice
if (isKnife) {
for (let slice of widget.slices) {
for (let poly of slice.offset) {
addKnifeRadii(poly, ctOutKnifeTip);
if (poly.inner) poly.inner.forEach(ip => {
addKnifeRadii(ip, ctOutKnifeTip);
});
}
}
}
let lastEmit;
let slices = ctOutStack ? widget.slices.reverse() : widget.slices;
for (let slice of slices) {
lastEmit = sliceEmitObjects(print, slice, layers, {simple: isKnife, lastEmit});
update((slices++ / totalSlices) * 0.5, "prepare");
}
}
}
// for tile layout packing
let dw = device.bedWidth / 2,
dh = device.bedDepth / 2,
sort = !ctOutLayer,
spacing = ctOutTileSpacing;
// compute tile bounds
for (let tile of tiles) {
let bounds = newBounds();
tile.forEach(rec => bounds.merge(rec.poly.bounds));
tile.w = bounds.width();
tile.h = bounds.height();
tile.bounds = bounds;
}
// pack tiles
let tp = new Packer(dw, dh, spacing, { invy:isWire, simple: !sort }).pack(tiles, packer => {
return packer.rescale(1.1, 1.1);
});
// reposition tiles into their packed locations (unless 3d stack)
if (!ctOutStack)
for (let tile of tiles) {
let { fit, bounds } = tile;
for (let { poly } of tile) {
poly.setZ(0);
poly.move({
x: fit.x - (tp.max.w / 2) - bounds.minx,
y: fit.y - (tp.max.h / 2) - bounds.miny,
z: 0}, true);
}
tile.bounds = newBounds();
for (let { poly } of tile) {
tile.bounds.merge(poly.bounds);
}
}
// set starting point based on origin type
let currentPos =
ctOriginCenter ? newPoint(0,0,0) :
ctOriginBounds ? newPoint(
- tp.max.w/2 - (ctOriginOffX || 0),
- tp.max.h/2 - (ctOriginOffY || 0), 0
) :
newPoint(-dw, -dh, 0);
// output tiles starting with closest to origin
let remain = tiles.slice();
while (remain.length) {
let closest = ctOutStack ?
{ dist: 0, tile: remain[0] } :
{ dist: Infinity };
for (let tile of remain) {
for (let { poly } of tile) {
for (let p of poly.points) {
let pd = currentPos.distTo2D(p);
if (pd < closest.dist) {
closest.dist = pd;
closest.tile = tile;
}
}
}
}
if (!closest.tile) throw "no closest found";
for (let { poly, group, options } of closest.tile) {
currentPos = print.polyPrintPath(poly, currentPos, group, options);
if (!output.contains(group)) {
output.push(group);
}
}
remain.remove(closest.tile);
}
// clone output points to prevent double offsetting during export
for (let layer of output) {
for (let rec of layer) {
rec.point = rec.point.clone();
}
}
return render.path(output, (progress, layer) => {
if (ctOutShaper) {
for (let layer of output) {
for (let out of layer) {
out.point.zi = maxZ - out.point.z;
}
}
}
update(0.5 + progress * 0.5, "render", layer);
}, { thin: true, z: ctOutStack ? undefined : 0, action: "cut", moves: process.isKnife });
};
function laser_export(print, online, ondone) {
ondone(print.output);
}
/**
*
*/
function exportElements(settings, output, onpre, onpoly, onpost, onpoint, onlayer) {
let { process, device } = settings,
{ bedWidth, bedDepth } = device,
{ ctOriginCenter, ctOriginBounds } = process,
{ outputInvertX, outputInvertY } = process,
{ ctOriginOffX, ctOriginOffY } = process,
{ ctOutPower, ctOutSpeed, maxZ } = process,
last,
point,
poly = [],
off = ctOriginBounds ? {x:ctOriginOffX||0, y:ctOriginOffY||0} : {x:0, y:0},
min = {x:0, y:0},
max = {x:0, y:0},
size = {w:0, h:0};
// compute bounds for entire output
output.forEach(layer => {
layer.forEach(out => {
point = out.point;
if (outputInvertX) point.x = -point.x;
if (outputInvertY) point.y = -point.y;
min.x = Math.min(min.x, point.x);
max.x = Math.max(max.x, point.x);
min.y = Math.min(min.y, point.y);
max.y = Math.max(max.y, point.y);
});
});
size.w = max.x - min.x;
size.h = max.y - min.y;
let bounds = newBounds();
if (ctOriginCenter) {
// place origin at geometric center of all points
// regardless of workspace size
for (let layer of output) {
for (let out of layer) {
point = out.point;
point.x = point.x - min.x - size.w / 2 + off.x;
point.y = point.y - min.y - size.h / 2 + off.y;
bounds.update(point);
}
}
max.x = max.x - min.x + off.x;
max.y = max.y - min.y + off.y;
min.x = 0;
min.y = 0;
} else if (ctOriginBounds) {
// place origin at min x,y of all points
for (let layer of output) {
for (let out of layer) {
point = out.point;
point.x = point.x - min.x + off.x;
point.y = point.y - min.y + off.y;
bounds.update(point);
}
}
max.x = max.x - min.x + off.x;
max.y = max.y - min.y + off.y;
min.x = 0;
min.y = 0;
} else {
// place origin at min x,y taking into account
// each layer's relative position
let w = bedWidth;
let h = bedDepth;
for (let layer of output) {
for (let out of layer) {
point = out.point;
point.x += w / 2 + off.x;
point.y += h / 2 + off.y;
bounds.update(point);
}
}
max.x = w + off.x;
max.y = h + off.y;
min.x = 0;
min.y = 0;
}
onpre(min, max, ctOutPower, ctOutSpeed);
// output each layer with moves between them
output.forEach((layer, index) => {
let thick = 0;
let color = 0;
layer.forEach((out, li) => {
thick = out.thick;
point = out.point;
if (onlayer) {
onlayer(index, point.z, out.thick, output.length);
}
if (out.emit) {
color = out.emit;
if (last && poly.length === 0) {
poly.push(onpoint(last));
}
poly.push(onpoint(point));
} else if (poly.length > 0) {
onpoly(poly, color, thick);
poly = [];
}
last = point;
});
if (poly.length > 0) {
onpoly(poly, color, thick);
poly = [];
}
});
onpost();
};
/**
*
*/
function exportGCode(settings, data) {
let lines = [], dx = 0, dy = 0, z = 0, feedrate;
let dev = settings.device;
let proc = settings.process;
let space = dev.gcodeSpace ? ' ' : '';
let max_power = dev.laserMaxPower || 255;
let power = max_power;
let cut_on = dev.gcodeLaserOn || dev.gcodeWaterOn || dev.gcodeKnifeDn || [];
let cut_off = dev.gcodeLaserOff || dev.gcodeWaterOff || dev.gcodeKnifeUp || [];
let isKnife = proc.isKnife;
let knifeDepth = proc.ctOutKnifeDepth;
let passes = isKnife ? proc.ctOutKnifePasses : 1;
exportElements(
settings,
data,
function(min, max, pct, speed) {
let width = (max.x - min.x),
height = (max.y - min.y);
dx = min.x;
dy = min.y;
feedrate = `${space}F${speed}`;
power = (max_power * (pct / 100)).toFixed(3);
(dev.gcodePre || []).forEach(line => {
lines.push(line);
});
},
function(poly, color, thick) {
if (isKnife) {
lines.push(`; start new poly id=${poly.id} len=${poly.length}`);
}
for (let i=1; i<passes + 1; i++) {
poly.forEach(function(point, index) {
if (index === 0) {
if (isKnife) {
// lift
lines.appendAll(['; drag-knife lift', `G0${space}Z5`]);
}
lines.push(`G0${space}${point}`);
if (isKnife) {
// drop
lines.appendAll(['; drag-knife down', `G0${space}Z${-i * knifeDepth}`]);
}
} else if (index === 1) {
cut_on.forEach(line => {
line = line.replace('{power}', power);
line = line.replace('{color}', color);
line = line.replace('{thick}', thick);
line = line.replace('{z}', z);
lines.push(line);
});
lines.push(`G1${space}${point}${feedrate}`);
} else {
lines.push(`G1${space}${point}`);
}
});
cut_off.forEach(line => {
lines.push(line);
});
}
if (isKnife) {
lines.appendAll([`G0${space}Z5`]);
}
},
function() {
(dev.gcodePost || []).forEach(line => {
lines.push(line);
});
},
function(point) {
z = point.z;
return `X${(point.x - dx).toFixed(3)}${space}Y${(point.y - dy).toFixed(3)}`;
},
function(layer, z, thick, layers) {
// ununsed
}
);
return lines.join('\n');
};
/**
*
*/
function exportSVG(settings, data, cut_color) {
let { process } = settings;
let { ctOutInches, ctOutStack, ctOutShaper } = process;
let swidth = "0.1mm";
let zcolor = process.ctOutZColor ? 1 : 0;
let lines = [], dx = 0, dy = 0, my, z = 0;
let colors = [
"black",
"purple",
"blue",
"red",
"orange",
"yellow",
"green",
"brown",
"gray"
];
exportElements(
settings,
data,
function(min, max) {
let width = (max.x - min.x),
height = (max.y - min.y);
dx = min.x;
dy = min.y;
my = max.y;
lines.push('<?xml version="1.0" standalone="no"?>');
lines.push('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">');
let svg = ['<svg xmlns="http://www.w3.org/2000/svg"'];
if (ctOutShaper) {
svg.push('xmlns:shaper="http://www.shapertools.com/namespaces/shaper"');
}
if (ctOutInches) {
width = (width / 25.4).round(2);
height = (height / 25.4).round(2);
svg.push(`width="${width}in" height="${height}in" viewBox="0 0 ${width} ${height}"`);
} else {
svg.push(`width="${width}mm" height="${height}mm" viewBox="0 0 ${width} ${height}"`);
}
lines.push(svg.join(' ') + ' version="1.1">');
},
function(poly, color, thick) {
let cout = zcolor || colors[((color-1) % colors.length)];
if (ctOutStack) {
let def = ["path"];
if (z !== undefined && ctOutShaper) {
if (ctOutInches) {
def.push(`shaper:cutDepth="${(z / 25.4).toFixed(2)}in"`);
} else {
def.push(`shaper:cutDepth="${(z).toFixed(2)}mm"`);
}
def.push(`shaper:cutType="online"`);
// def.push(`shaper:cutOffset="0in"`);
// def.push(`shaper:toolDia="0in"`);
} else if (z !== undefined) {
def.push(`z="${z}"`);
}
let path = poly.map((xy, i) => i > 0 ? `L${xy}` : `M${xy}`);
let std = ctOutShaper ? '' : `fill="none" stroke="${cout}" stroke-width="${swidth}" `;
lines.push(`<${def.join(' ')} d="${path.join(' ')}" ${std}/>`);
} else {
let def = ["polyline"];
if (z !== undefined) def.push(`z="${z}"`);
if (thick !== undefined) def.push(`h="${thick}"`);
lines.push(`<${def.join(' ')} points="${poly.join(' ')}" fill="none" stroke="${cout}" stroke-width="${swidth}" />`);
}
},
function() {
lines.push("</svg>");
},
function(point) {
z = ctOutStack ? (point.zi ?? point.z) : undefined;
let px = point.x - dx;
let py = my - point.y - dy;
// convert to imperial for stacked
if (ctOutInches) {
px /= 25.4;
py /= 25.4;
}
return base.util.round(px, 3) + "," + base.util.round(py, 3);
},
function(layer, z, thick, layers) {
if (zcolor) {
zcolor = Math.round(((layer + 1) / layers) * 0xffffff).toString(16);
zcolor = `#${zcolor.padStart(6,0)}`;
}
}
);
return lines.join('\n');
};
/**
*
*/
function exportDXF(settings, data) {
let lines = [];
exportElements(
settings,
data,
function(min, max) {
lines.appendAll([
" 0",
"SECTION",
" 2",
"HEADER",
" 0",
"ENDSEC",
" 0",
"SECTION",
" 2",
"TABLES",
" 0",
"ENDSEC",
" 0",
"SECTION",
" 2",
"BLOCKS",
" 0",
"ENDSEC",
" 0",
"SECTION",
" 2",
"ENTITIES",
]);
},
function(poly) {
lines.appendAll([
" 0",
"POLYLINE",
" 8",
"0",
" 66",
"1",
" 70",
"1",
]);
poly.forEach(point => {
lines.appendAll([
' 0',
'VERTEX',
' 8',
'0',
' 10',
point.x,
' 20',
point.y
]);
});
lines.appendAll([
' 0',
'SEQEND',
]);
},
function() {
lines.appendAll([
' 0',
'ENDSEC',
' 0',
'SECTION',
' 2',
'OBJECTS',
' 0',
'ENDSEC',
' 0',
'EOF',
]);
},
function(point) {
return {x:point.x,y:point.y};
}
);
return lines.join('\n');
};
export const LASER = {
TYPE,
type: TYPE.LASER,
name: 'Laser',
init,
slice: laser_slice,
prepare: laser_prepare,
export: laser_export,
exportGCode,
exportDXF,
exportSVG
};

View file

@ -0,0 +1,86 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { $ } from '../../../moto/webui.js';
import { api } from '../../core/api.js';
export const SLA = {
init,
printDownload
}
export function init() {
api.event.on("mode.set", (mode) => {
if (mode === 'SLA') {
api.ui.func.preview.classList.add('hide');
} else {
api.ui.func.preview.classList.remove('hide');
}
});
}
export function printDownload(output, api, names) {
const { file, width, height, layers, volume } = output;
const fileroot = names[0] || "print";
const filename = `${fileroot}-${new Date().getTime().toString(36)}`;
api.modal.show('xsla');
let settings = api.conf.get(),
process = settings.process,
device = settings.device,
print_sec = (process.slaBaseLayers * process.slaBaseOn) +
(layers - process.slaBaseLayers) * process.slaLayerOn;
// add peel lift/drop times to total print time
for (let i = 0; i < layers; i++) {
let dist = process.slaPeelDist,
lift = process.slaPeelLiftRate,
drop = process.slaPeelDropRate,
off = process.slaLayerOff;
if (i < process.slaBaseLayers) {
dist = process.slaBasePeelDist;
lift = process.slaBasePeelLiftRate;
off = process.slaBaseOff;
}
print_sec += (dist * lift) / 60;
print_sec += (dist * drop) / 60;
print_sec += off;
}
let print_min = Math.floor(print_sec / 60),
print_hrs = Math.floor(print_min / 60),
download = $('print-sla');
// add lift/drop time
print_sec -= (print_min * 60);
print_min -= (print_hrs * 60);
print_sec = Math.round(print_sec).toString().padStart(2, '0');
print_min = print_min.toString().padStart(2, '0');
print_hrs = print_hrs.toString().padStart(2, '0');
$('print-filename-sla').value = filename;
$('print-volume').value = (volume / 1000).round(2);
$('print-layers').value = layers;
$('print-time').value = `${print_hrs}:${print_min}:${print_sec}`;
switch (device.deviceName) {
case 'Anycubic.Photon':
download.innerText += " .photon";
download.onclick = () => { saveFile(api, file, ".photon") };
break;
case 'Anycubic.Photon.S':
download.innerText += " .photons";
download.onclick = () => { saveFile(api, file, ".photons") };
break;
case 'Creality.Halot.Sky':
default:
download.innerText += " .cxdlp";
download.onclick = () => { saveFile(api, file, ".cxdlp") };
break;
}
}
function saveFile(api, file, ext) {
api.util.download(file, $('print-filename-sla').value + ext);
api.modal.hide();
}

View file

@ -0,0 +1,49 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { newPrint } from '../../core/print.js';
import { sla_slice } from './slice.js';
import { sla_export } from './export.js';
export const SLA = {
init,
legacy: false,
slice: sla_slice,
prepare: sla_prepare,
export: sla_export,
};
if (SLA.legacy) {
console.log("SLA Driver in Legacy Mode");
}
function init(worker) {
// console.log({ INIT_SLA: worker });
}
// runs in worker. would usually be in src/mode/sla/prepare.js
// but the SLA driver skips the prepare step because there is no path routing
async function sla_prepare(widgets, settings, update) {
self.kiri_worker.current.print = newPrint(settings, widgets);
if (!SLA.wasm) {
fetch('/wasm/kiri-sla.wasm')
.then(response => response.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes, {
env: {
reportf: (a,b) => { console.log('[f]',a,b) },
reporti: (a,b) => { console.log('[i]',a,b) }
}
}))
.then(results => {
let {module, instance} = results;
let {exports} = instance;
let heap = new Uint8Array(exports.memory.buffer);
SLA.wasm = {
heap,
memory: exports.memory,
render: exports.render,
rle_encode: exports.rle_encode
};
});
}
update(1);
}

163
src/kiri/mode/sla/export.js Normal file
View file

@ -0,0 +1,163 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CXDLP } from './x_cxdlp.js';
import { photon } from './x_photon.js';
import { SLA } from './driver.js';
/**
* DRIVER CONTRACT - runs in worker
* @param {Object} print state object
* @param {Function} online streaming reply
* @param {Function} ondone last reply
*/
export function sla_export(print, online, ondone) {
let widgets = print.widgets,
settings = print.settings,
device = settings.device,
process = settings.process,
width = device.resolutionX,
height = device.resolutionY,
scaleX = width / device.bedWidth,
scaleY = height / device.bedDepth,
layerZ = process.slaSlice,
alias = process.slaAntiAlias || 1,
mark = Date.now(),
layermax = 0,
volume = 0;
// filter ignored widgets
widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled);
// find max layer count
widgets.forEach(widget => {
layermax = Math.max(widget.slices.length);
});
let isPhoton = false;
switch (device.deviceName) {
case 'Anycubic.Photon':
case 'Anycubic.Photon.S':
isPhoton = true;
break;
}
if (isPhoton) {
let legacyMode = SLA.legacy || alias > 1,
part1 = legacyMode ? 0.25 : 0.85,
part2 = (1 - part1),
images = [],
slices = [];
// generate layer bitmaps
// in wasm mode, rle layers generated here, too
let d = 8 / alias;
let masks = [];
for (let i=0; i<alias; i++) {
masks.push((1 << (8 - i * d)) - 1);
}
let render = legacyMode ? photon.renderLayer : photon.renderLayerWasm;
for (let index=0; index < layermax; index++) {
let param = { index, width, height, widgets, scaleX, scaleY, masks };
let { image, layers, end, area } = render(param);
volume += (area * layerZ);
images.push(image);
slices.push(layers);
online({
progress: (index / layermax) * part1,
message: "image_gen",
});
if (end) break;
}
let exp_func = {
'Anycubic.Photon': photon.generatePhoton,
'Anycubic.Photon.S': photon.generatePhotons,
}[device.deviceName] || photon.generatePhoton;
let file = exp_func(print, {
width: width,
height: height,
small: SLA.previewSmall.data,
large: SLA.previewLarge.data,
lines: images,
slices: slices
}, (progress, message) => {
online({progress: progress * part2 + part1, message});
});
ondone({ width, height, file, layers: images.length, volume }, [file]);
} else {
let part1 = 0.95;
let part2 = 1 - part1;
let slices = [];
for (let index=0; index < layermax; index++) {
let param = { index, width, height, widgets, scaleX, scaleY };
let { lines, area } = CXDLP.render(param);
volume += (area * layerZ);
slices.push(lines);
online({
progress: (index / layermax) * part1,
message: "image_gen"
});
// bail on an empty layer
if (lines.length === 0) {
break;
}
}
// generate thumb, preview1, preview2
let thumb = [];
let tdata = SLA.previewSmall.data;
for (let x=0; x<116; x++) {
for (let y=0; y<116; y++) {
let p = (x * 116 + y) * 4;
let r = (tdata[p + 0] >> 3) << 11;
let g = (tdata[p + 1] >> 2) << 5;
let b = (tdata[p + 2] >> 3);
let v = (r | g | b) & 0xffff;
thumb.push((v >> 8) & 0xff);
thumb.push(v & 0xff);
}
}
let preview1 = [];
let pdata = SLA.previewLarge.data;
for (let x=0; x<290; x++) {
for (let y=0; y<290; y++) {
let p = (x * 290 + y) * 4;
let r = (pdata[p + 0] >> 3) << 11;
let g = (pdata[p + 1] >> 2) << 5;
let b = (pdata[p + 2] >> 3);
let v = (r | g | b) & 0xffff;
preview1.push((v >> 8) & 0xff);
preview1.push(v & 0xff);
}
}
let preview2 = preview1;
let file = CXDLP.export({
settings,
width,
height,
slices,
thumb,
preview1,
preview2
});
ondone({
width: width,
height: height,
file: file,
layers: slices.length,
volume
}, [file]);
}
console.log('print.export', Date.now() - mark);
};
export function generateCXDLP(print, conf, progress) {
console.log({generateCXDLP: print, conf, progress});
}

696
src/kiri/mode/sla/slice.js Normal file
View file

@ -0,0 +1,696 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { util } from '../../../geo/base.js';
import { slicer } from '../../../geo/slicer.js';
import { newPolygon } from '../../../geo/polygon.js';
import { polygons as POLY } from '../../../geo/polygons.js';
import { newSlice, newTop } from '../../core/slice.js';
import { doShells, doDiff, projectFlats, projectBridges } from '../fdm/slice.js';
import { PNG } from '../../../ext/pngjs.esm.js';
import { SLA } from './driver.js';
const tracker = util.pwait;
let fill_cache;
/**
* DRIVER CONTRACT - runs in worker
* @param {Object} settings
* @param {Widget} Widget
* @param {Function} onupdate (called with % complete and optional message)
* @param {Function} ondone (called when complete with an array of Slice objects)
*/
export function sla_slice(settings, widget, onupdate, ondone) {
let { minions } = self.kiri_worker,
{ process, device, controller } = settings,
isConcurrent = controller.threaded && minions.concurrent,
work_total,
work_remain;
if (SLA.legacy && !self.OffscreenCanvas) {
return ondone("browser lacks support for OffscreenCanvas",true);
}
// calculate % complete and call onupdate()
function doupdate(work, msg) {
onupdate(0.25 + ((work_total - work_remain) / work_total) * 0.75, msg);
work_remain -= work;
}
// for each slice, perform a function and call doupdate()
function forSlices(slices, work, fn, msg) {
slices.forEach(function(slice,index) {
fn(slice,index);
doupdate(work / slices.length, msg)
});
}
let smallDims = { x: 200, y: 125 };
let largeDims = { x: 400, y: 300 };
switch (device.deviceName) {
case 'Anycubic.Photon':
case 'Anycubic.Photon.S':
// defaults above
break;
case 'Creality.Halot.One':
case 'Creality.Halot.Max':
case 'Creality.Halot.Sky':
smallDims = { x: 116, y: 116 };
largeDims = { x: 290, y: 290 };
break;
}
let sws = self.kiri_worker.current.snap.url;
let b64 = atob(sws.substring(sws.indexOf(',') + 1));
let bin = Uint8Array.from(b64, c => c.charCodeAt(0));
let img = new PNG();
img.parse(bin, (err, data) => {
SLA.preview = img;
SLA.previewSmall = samplePNG(img, smallDims.x, smallDims.y);
SLA.previewLarge = samplePNG(img, largeDims.x, largeDims.y);
});
let height = process.slaSlice || 0.05;
async function onSliceDone(slices) {
// remove empty slices
slices = widget.slices = slices.filter(slice => slice.tops.length);
if (!process.slaOpenTop && !process.xray) {
// re-add last empty slice for closed top
let cap = newSlice(bounds.max.z + height);
cap.index = slices.last().index + 1;
slices.push(cap);
}
// prepend raft layers to slices array
if (process.slaSupportEnable && process.slaSupportLayers) {
let layers = process.slaSupportLayers,
zoff = height / 2,
snew = [],
polys = [],
gap = process.slaSupportGap, // gap layers above raft
grow = height, // union per layer expand
off = 1 - (layers * grow); // starting union offset from part
let outer = slices.forEach(slice => {
// poly.clone prevents inner voids from forming
polys.appendAll(slice.tops.map(t => t.poly.clone()));
});
// p.clone prevents inner voids from forming
let union = POLY.union(polys, undefined, true).map(p => p.clone());
let expand = POLY.expand(union, off, zoff, [], 1);
let lastraft;
for (let s=0; s<layers + gap; s++) {
let slice = newSlice(zoff);
slice.height = height;
slice.index = snew.length;
if (s < layers) {
slice.synth = true;
expand.forEach(u => {
slice.tops.push(newTop(u.clone(true).setZ(zoff)));
});
expand = POLY.expand(expand, grow, zoff, [], 1);
lastraft = slice;
}
snew.push(slice);
zoff += height;
}
// compensate for midline start
zoff -= height / 2;
// replace slices with new appended array
slices = widget.slices = snew.concat(slices.map(s => {
s.tops.forEach(t => t.poly.setZ(s.z + zoff));
s.index += snew.length;
s.z += zoff;
return s;
}));
// annotate widget for support generation
widget.union = union;
widget.lastraft = lastraft;
}
// re-connect slices into linked list for island/bridge projections
for (let i=1; i<slices.length; i++) {
slices[i-1].up = slices[i];
slices[i].down = slices[i-1];
}
let solidLayers = Math.round(process.slaShell / process.slaSlice);
// setup solid fill
slices.forEach(function(slice) {
slice.solids = [];
});
// compute total work for progress bar
work_total = [
5, // shell
10, // diff
solidLayers ? 10 : 0, // shell project
solidLayers ? 10 : 0, // shell fill
!solidLayers ? 10 : 0, // solid
process.slaFillDensity && process.slaShell ? 60 : 0, // infill
process.slaSupportEnable && process.slaSupportLayers && process.slaSupportDensity ? 100 : 0
].reduce((t,v) => { return t+v });
work_remain = work_total;
forSlices(slices, 5, (slice,index) => {
if (process.slaShell) {
doShells(slice, 2, 0, process.slaShell);
} else {
doShells(slice, 1, 0);
}
}, "shells");
forSlices(slices, 10, (slice) => {
if (slice.synth) return;
doDiff(slice, 0.000001, {
sla: true,
fakedown: !process.slaOpenBase
});
}, "delta");
if (solidLayers) {
forSlices(slices, 10, (slice) => {
if (slice.synth) return;
projectFlats(slice, solidLayers);
projectBridges(slice, solidLayers);
}, "project");
async function doUnionSolid(slice) {
if (slice.synth) return;
let traces = POLY.nest(POLY.flatten(slice.topShells()));
if (slice.solids) {
let trims = slice.solids || [];
traces.appendAll(trims);
// slice.unioned = POLY.setZ(POLY.union(traces, undefined, true), slice.z);
slice.unioned = POLY.setZ(await minions.union(traces), slice.z);
} else {
slice.unioned = traces;
}
}
let promises = slices.map(slice => doUnionSolid(slice));
await tracker(promises, (i, t) => {
doupdate(10 / promises.length, "solid");
});
} else {
forSlices(slices, 10, (slice) => {
if (slice.synth) return;
slice.unioned = slice.topPolys();
}, "solid");
}
if (process.slaFillDensity && process.slaShell) {
fill_cache = [];
forSlices(slices, 60, (slice) => {
if (slice.synth) return;
fillPolys(slice, settings);
}, "infill");
}
if (process.slaSupportEnable && process.slaSupportLayers && process.slaSupportDensity) {
computeSupports(widget, process, progress => {
doupdate(100 * progress, "support");
});
}
doRender(widget);
}
let bounds = widget.getBoundingBox();
let points = widget.getPoints();
slicer.slice(points, {
indices: process.indices || process.xray,
union: controller.healMesh,
debug: process.xray,
xray: process.xray,
zMin: bounds.min.z + height / 2,
zMax: bounds.max.z,
zInc: height,
// slicer function (worker local or minion distributed)
slicer(z, points, opts) {
return (isConcurrent ? minions.sliceZ : slicer.sliceZ)(z, points, opts);
},
onupdate(v) {
return onupdate(0.0 + v * 0.25);
}
}).then(output => {
let slices = widget.slices = output.slices.map(data => {
let { z, lines, groups } = data;
let tops = POLY.nest(groups);
return newSlice(z).addTops(tops);
});
onSliceDone(slices).then(ondone);
});
};
function doRender(widget) {
widget.slices.forEach(slice => {
const render = slice.output(), lopacity = 0.6, line = 0x010101;
if (slice.unioned) {
slice.unioned.forEach(poly => {
poly = poly.clone(true);//.move(widget.track.pos);
render
.setLayer("layers", { line, face: 0x0099cc, lopacity })
.addAreas([poly], { outline: true });
});
} else if (slice.tops) {
slice.tops.forEach(top => {
let poly = top.poly;//.clone(true).move(widget.track.pos);
render
.setLayer("layers", { line, face: 0xfcba03, lopacity })
.addAreas([poly], { outline: true });
});
}
if (slice.supports) {
slice.supports.forEach(poly => {
render
.setLayer("support", { line, face: 0xfcba03, lopacity })
.addAreas([poly], { outline: true });
});
}
});
}
function computeSupports(widget, process, progress) {
let area = widget.union.reduce((t,p) => { return t + p.areaDeep() }, 0),
perim = widget.union.reduce((t,p) => { return t + p.perimeter() }, 0),
slices = widget.slices,
length = slices.length,
tot_mass = 0, // total widget "mass"
tot_bear = 0; // total "mass-bearing" area
let ops = slices.filter(slice => !slice.synth);
// compute total "mass" by slice
ops.forEach(slice => {
slice.mass = slice.unioned.reduce((t,p) => { return t + p.areaDeep() }, 0);
if (slice.up && slice.up.bridges) {
slice.bear = slice.up.bridges.reduce((t,p) => { return t + p.areaDeep() }, 0);
slice.bear_up = slice.up.bridges;
tot_bear += slice.bear;
} else {
slice.bear = 0;
}
tot_mass += slice.mass;
});
let mass_per_bear = (tot_mass / tot_bear) * (1 / process.slaSupportDensity);
// console.log({tot_mass, tot_bear, ratio: tot_mass / tot_bear, mass_per_bear});
let first;
ops.slice().map((s,i) => {
if (!first && s.bear) {
s.ord_first = first = true;
}
// 30x lowest to 1x highest
s.ord_weight = Math.pow(30,((ops.length - i) / ops.length));
return s;
}).sort((a,b) => {
if (a.ord_first) return -1;
if (b.ord_first) return 1;
return (b.bear * b.ord_weight) - (a.bear * a.ord_weight);
}).forEach((slice, index) => {
slice.ord_bear = index;
})
let rem_mass = tot_mass,
rem_bear = tot_bear;
// compute remaining mass, bearing surface, for each slice
let run, runLast, runCount = 0, runList = [];
ops.forEach(slice => {
slice.rem_mass = rem_mass;
slice.rem_bear = rem_bear;
// slice.can_bear = slice.mass * mass_per_bear;
slice.can_bear = slice.bear * mass_per_bear;
rem_mass -= slice.mass;
rem_bear -= slice.bear;
});
let ord = ops.sort((a,b) => {
return a.ord_bear - b.ord_bear;
});
rem_mass = tot_mass;
rem_bear = tot_bear;
// in order of load bearing capability, select layer
// and recompute the requirements on the slices below
for (let i=0; i<ord.length; i++) {
let slice = ord[i],
bearing = Math.min(slice.can_bear, slice.rem_mass);
// remove from slices below the amount of mass they have to bear
for (let j=slice.index - 1; j>ops[0].index; j--) {
slices[j].rem_mass -= bearing;
}
// remove total mass left to bear
rem_mass -= bearing;
slice.can_emit = true;
if (rem_mass <= 0) {
// console.log({break: i, of: ord.length});
break;
}
}
let seq = 0,
seqLast = 0,
spacing = (1 - process.slaSupportDensity) * 10,
size = Math.bound(process.slaSupportSize / 2, 0.25, 1);
// compute and project support pillars
slices.forEach((slice,index) => {
if (slice.can_emit) {
if (seq === 0 || slice.index - seqLast > 5) {
slice.bear_up.map(p => {
return p.clone(true).setZ(slice.z);
}).forEach(p => {
projectSupport(process, slice, p, size, spacing);
});
seq++;
} else if (seq > 5) {
seq = 0;
} else {
seq++;
}
seqLast = slice.index;
}
progress(1 / slices.length);
});
// union support pillars
slices.forEach(slice => {
if (slice.supports) {
slice.supports = POLY.union(slice.supports, 0, true);
}
});
}
function projectSupport(process, slice, poly, size, spacing) {
let flat = poly.circularityDeep() > 0.1,
arr = [ poly ];
// insetting polys produces arrays. consume til gone
while (arr.length) {
poly = arr.shift();
let out = [],
seg = [],
per = poly.perimeter(),
crit = 0,
polys = poly.clone(true).flattenTo([]);
polys.forEach(p => p.forEachSegment((p1, p2) => {
let rec = {dist: p1.distTo2D(p2), p1, p2};
if (rec.dist >= spacing || slice.ord_first) crit++;
seg.push(rec);
}));
seg.sort((a,b) => {
return b.dist - a.dist;
});
// emit all critical (long) segments. min 3
while (seg.length && (crit > 0 || out.length < 3)) {
let {dist, p1, p2} = seg.shift();
if (dist >= spacing) {
// spaced along line
let num = Math.ceil(dist / spacing) + 1,
step = dist / num,
pt = p1;
while (num-- >= 0) {
out.push(pt);
pt = pt.offsetPointTo(p2, step);
}
crit--;
} else {
// line midpoint
out.push(p1.offsetPointTo(p2, dist / 2));
}
}
// drop points too close to other pillars
if (!slice.ord_first)
drop: for (let i=0; i<out.length; i++) {
for (let j=i+1; j<out.length; j++) {
if (out[i].distTo2D(out[j]) <= size) {
out[i] = null;
continue drop;
}
}
if (slice.pillars) slice.pillars.forEach(rec => {
if (out[i] && out[i].distTo2D(rec.point) <= size) {
out[i] = null;
}
});
}
// mark support pillar for each point
out.filter(p => p !== null)
.forEach(p => {
let track = projectPillar(process, slice, p, size, []);
// find stunted pillars terminating on a face (not raft or merged)
if (track.length && !(track.max || track.synth || track.merged)) {
// remove stunted pillar
track.forEach(rec => {
let sp = rec.slice.supports;
let rp = sp.indexOf(rec.pillar);
if (rp >= 0) sp.splice(rp,1);
});
}
});
// inset polygon for flat area support
if (flat) poly.offset(Math.max(0.2, 1 - process.slaSupportDensity), arr);
}
}
function projectPillar(process, slice, point, size, track) {
if (!slice.supports) slice.supports = [];
if (!slice.pillars) slice.pillars = [];
let points = process.slaSupportPoints,
pillar = newPolygon()
.centerCircle(point, size/2, points, true)
.setZ(slice.z),
max = process.slaSupportSize,
inc = process.slaSlice/2,
end = false, // center intersects
over = [], // overlapping points
safe = [], // non-overlapping points
low = slice.index < process.slaSupportGap + process.slaSupportLayers;
track.min = track.min ? Math.min(track.min, size) : size;
track.max = (track.max ? true : false) || size >= max;
track.synth = track.synth || slice.synth;
slice.tops.forEach(t => {
if (track.length > 3 && point.isInPolygon(t.poly)) {
end = true;
}
pillar.points.forEach(p => {
let isin = p.isInPolygon(t.poly);// || p.nearPolygon(t.poly, 0.001);
if (isin) {
over.push(p);
} else {
safe.push(p);
}
});
});
if (end) {
// backtrack shrinking pillars to point if
// not landing on the base and size is max'd
if (!slice.synth && track.max) {
let nusize = track.min;
while (track.length) {
let prec = track.pop(),
npil = newPolygon()
.centerCircle(prec.point, nusize/2, points, true)
.setZ(prec.slice.z),
spos = prec.slice.supports.indexOf(prec.pillar);
// if we find our old pillar, replace
if (spos >= 0) {
prec.slice.supports[spos] = npil;
nusize += inc;
}
if (nusize > prec.size) {
break;
}
}
}
return track;
}
let nextpoint = point;
if (over.length) {
// move toward average of safe (non-overlapping) points
if (safe.length) {
let x = 0, y = 0;
safe.forEach(p => { x += p.x; y += p.y });
x /= safe.length;
y /= safe.length;
nextpoint = point.offsetPointTo({x, y, z:slice.z}, inc);
}
// once max'ed out, can only shrink in size
if (track.max) {
size -= inc;
}
} else if (!track.max || low) {
size += inc;
if (low) max += (process.slaSupportGap * process.slaSlice * 2);
}
if (size < track.min) {
return track;
}
// let close = [];
// for (let i=0; i<slice.pillars.length; i++) {
// let p = slice.pillars[i].point;
// let d = point.distTo2D(p);
// // terminate if we're inside another pillar
// if (d < inc) {
// track.merged = true;
// return track;
// }
// if (d <= process.slaSupportSize * 1.5) {
// close.push(p);
// }
// }
// if (close.length) {
// let newp = point.clone();
// close.forEach(p => {
// newp.x += p.x;
// newp.y += p.y;
// });
// newp.x /= (close.length + 1);
// newp.y /= (close.length + 1);
// nextpoint = point.offsetPointTo(newp, inc);
// }
slice.supports.push(pillar);
slice.pillars.push({point, pillar, size});
track.push({slice, point, pillar, size});
if (slice.down) {
projectPillar(process, slice.down, nextpoint, Math.min(size, max), track);
}
return track;
}
function fillPolys(slice, settings) {
let process = settings.process,
device = settings.device,
polys = slice.unioned,
bounds = settings.bounds,
width = bounds.max.x - bounds.min.x,
depth = bounds.max.y - bounds.min.y,
max = Math.max(width,depth),
seq = Math.round(process.slaFillLine / process.slaSlice),
linew = process.slaFillLine,
units_w = (width / linew) * process.slaFillDensity,
units_d = (depth / linew) * process.slaFillDensity,
step_x = width / units_w,
step_y = depth / units_d,
start_x = -(width / 2),
start_y = -(depth / 2),
end_x = width / 2,
end_y = depth / 2,
fill = [];
let seq_i = Math.floor(slice.index / seq),
seq_c = seq_i % 4,
cached = fill_cache[seq_c];
if (!cached && seq_c !== 1)
for (let x=start_x; x<end_x; x += step_x) {
fill.push(
newPolygon().centerRectangle({
x: x + step_x/2,
y: 0,
z: slice.z
}, linew, depth)
);
}
if (!cached && seq_c !== 3)
for (let y=start_y; y<end_y; y += step_y) {
fill.push(
newPolygon().centerRectangle({
x: 0,
y: y + step_y/2,
z: slice.z
}, width, linew)
);
}
if (!cached) {
fill = POLY.union(fill, 0, true);
fill_cache[seq_c] = fill;
} else {
fill = cached.slice().map(p => p.clone(true).setZ(slice.z));
}
fill = POLY.trimTo(fill, slice.tops.map(t => t.poly));
fill = POLY.union(slice.unioned.appendAll(fill), 0, true);
slice.unioned = fill;
}
function pixAt(png,x,y) {
let idx = (x + png.width * y) * 4;
let dat = png.data;
return [
dat[idx++],
dat[idx++],
dat[idx++],
dat[idx++]
];
}
function averageBlock(png,x1,y1,x2,y2) {
let val = [0, 0, 0, 0], count = 0, x, y, z, v2;
for (x=x1; x<x2; x++) {
for (y=y1; y<y2; y++) {
v2 = pixAt(png,x,y);
for (z=0; z<4; z++) {
val[z] += v2[z];
}
count++;
}
}
for (z=0; z<4; z++) {
val[z] = Math.abs(val[z] / count);
}
return val;
};
function samplePNG(png, width, height) {
let th = width, tw = height,
aspect = width / height, // was fixed at 4/3 for photon
ratio = png.width / png.height,
buf = new Uint8Array(th * tw * 4),
div, xoff, yoff, dx, ex, dy, ey, bidx, pixval;
if (ratio >= aspect) {
div = png.height / tw;
xoff = Math.round((png.width - (th * div)) / 2);
yoff = 0;
} else {
div = png.width / th;
xoff = 0;
yoff = Math.round((png.height - (tw * div)) / 2);
}
for (let y=0; y<tw; y++) {
dy = Math.round(y * div + yoff);
if (dy < 0 || dy > png.height) continue;
ey = Math.round((y+1) * div + yoff);
for (let x=0; x<th; x++) {
dx = Math.round(x * div + xoff);
if (dx < 0 || dx > png.width) continue;
ex = Math.round((x+1) * div + xoff);
bidx = (y * th + x) * 4;
pixval = averageBlock(png,dx,dy,ex,ey);
buf[bidx+0] = pixval[0];
buf[bidx+1] = pixval[1];
buf[bidx+2] = pixval[2];
buf[bidx+3] = pixval[3];
}
}
return {width, height, data:buf, png};
}

View file

@ -0,0 +1,431 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
const default_values = {
magic1: 'CXSW3DV2',
magic2: 'CXSW3DV2',
model: 'CL-89',
version: 1,
layer_count: 0,
res_x: 3840,
res_y: 2400,
height: 0,
thumb: undefined,
preview1: undefined,
preview2: undefined,
dim_x: '192.000000',
dim_y: '120.000000',
layer: '0.050000',
light_on: 6,
light_off: 2,
light_pwm: 255,
lift_dist: 6,
lift_speed: 60,
down_speed: 150,
base_layers: 8,
base_light_on: 60,
base_light_pwm: 255,
base_lift_dist: 5,
base_lift_speed: 60
};
const data_term = 0x0d0a;
// https://github.com/sn4k3/UVtools/blob/v2.20.4/UVtools.Core/FileFormats/CXDLPFile.cs
// CL-60 1620 x 2560
// CL-89 3840 x 2400
// CL-133 3840 x 2160
export class CXDLP {
constructor() {
Object.assign(this, default_values);
}
// takes a DataView Object
read(view) {
let read = new DataReader(view, 0);
function read_string(dbl) {
let len = read.readU32();
let str = '';
while (--len > 0) {
if (dbl) {
read.readU8();
len--;
}
str += String.fromCharCode(read.readU8());
}
if (!dbl) {
read.skip(1); // null term on byte strings
}
return str;
}
let meta = {
view,
magic1: read_string(),
version: read.readU16(),
model: read_string(),
layer_count: read.readU16(),
res_x: read.readU16(),
res_y: read.readU16(),
height: read.readU32(),
skip: read.skip(60),
thumb: read.readBytes(26912),
skip: read.skip(2), // 0xd0a term
preview1: read.readBytes(168200),
skip: read.skip(2), // 0xd0a term
preview2: read.readBytes(168200),
skip: read.skip(2), // 0xd0a term
dim_x: read_string(true),
dim_y: read_string(true),
layer: read_string(true),
light_on: read.readU16(),
light_off: read.readU16(),
base_light_on: read.readU16(),
base_layers: read.readU16(),
base_lift_dist: read.readU16(),
base_lift_speed: read.readU16(),
lift_dist: read.readU16(),
lift_speed: read.readU16(),
down_speed: read.readU16(),
base_light_pwm: read.readU16(),
light_pwm: read.readU16()
};
// read layer record lengths
let layers = this.layers = [];
for (let i=0; i<meta.layer_count; i++) {
layers.push({ length: read.readU32() });
}
if (read.readU16() !== data_term) {
throw `invalid data term @ ${read.pos - 2}`;
}
// read layer meta data, not line data
for (let i=0; i<meta.layer_count; i++) {
let size = read.readU32();
if (size !== layers[i].length) {
throw `layer length mismatch: ${size} != ${layers[i].length} @ i=${i}`;
}
let lines = read.readU32();
layers[i].lines = lines;
layers[i].pos = read.pos; // store line data position
// skip line data and check data term is valid
read.skip(lines * 6);
if (read.readU16() !== data_term) {
throw `invalid data term @ ${read.pos - 2}`;
}
}
// read trailing magic
meta.magic2 = read_string();
// compute checksum and verify
let cksum = 0;
for (let i=0; i<read.pos; i++) {
cksum = cksum ^ view.getUint8(i);
}
meta.checksum = cksum;
if (cksum !== read.readU32()) {
throw `checksum mismatch`;
}
delete meta.skip;
Object.assign(this, meta);
return this;
}
get_layer_lines(layer) {
if (this.view) {
if (layer > this.layers.length) {
throw `layer out of range 0-${this.layers.length}`;
}
let out = [];
let rec = this.layers[layer];
let { lines, pos } = rec;
for (let j=0; j<lines; j++) {
let d1 = this.view.getUint16(pos); pos += 2;
let d2 = this.view.getUint16(pos); pos += 2;
let d3 = this.view.getUint16(pos); pos += 2;
let y_start = d1 >> 3;
let y_end = ((d1 & 0b111) << 10) | (d2 >> 6);
let x_end = ((d2 & 0b111111) << 8) | (d3 >> 8);
let color = d3 & 0xff;
out.push({y_start, y_end, x_end, color});
}
return out;
} else {
throw "missing data view";
}
}
write() {
let output = new ArrayWriter();
output.write_string = function write_string(str, dbl = false) {
let len = str.length;
output.writeU32(dbl ? len * 2 : len + 1);
let pos = 0;
while (pos < len) {
if (dbl) {
output.writeU8(0);
}
output.writeU8(str.charCodeAt(pos++));
}
if (!dbl) {
output.writeU8(0);
}
}
output.write_bytes = function(array, len) {
if (!array) {
output.skip(len);
} else if (array.byteLength) {
if (len !== array.byteLength) {
throw `invalid array length: ${array.byteLength} != ${len}`;
}
// ArrayBuffer
let read = new DataReader(array);
for (let i=0; i<array.byteLength; i++) {
output.writeU8(read.readU8());
}
} else if (array.length) {
if (len !== array.length) {
throw `invalid array length: ${array.length} != ${len}`;
}
// standard array
output.writeBytes(array);
} else {
throw "invalid byte array";
}
};
let layers = this.layers;
this.layer_count = layers.length;
output.write_string(this.magic1);
output.writeU16(this.version);
output.write_string(this.model);
output.writeU16(this.layer_count);
output.writeU16(this.res_x);
output.writeU16(this.res_y);
output.writeU32(this.height);
output.skip(60);
output.write_bytes(this.thumb, 26912); // thumb
output.writeU16(data_term);
output.write_bytes(this.preview1, 168200); // preview1
output.writeU16(data_term);
output.write_bytes(this.preview2, 168200); // preview1
output.writeU16(data_term);
output.write_string(this.dim_x, true);
output.write_string(this.dim_y, true);
output.write_string(this.layer, true);
output.writeU16(this.light_on);
output.writeU16(this.light_off);
output.writeU16(this.base_light_on);
output.writeU16(this.base_layers);
output.writeU16(this.base_lift_dist);
output.writeU16(this.base_lift_speed);
output.writeU16(this.lift_dist);
output.writeU16(this.lift_speed);
output.writeU16(this.down_speed);
output.writeU16(this.base_light_pwm);
output.writeU16(this.light_pwm);
// write placeholder layer length to capture position
for (let layer of layers) {
layer.l1 = output.pos;
output.writeU32(0);
}
output.writeU16(data_term);
// write out encoded layers
for (let layer of layers) {
layer.l2 = output.pos;
// placeholder to be written post
output.writeU32(0);
output.writeU32(layer.lines.length);
let start = output.pos;
for (let line of layer.lines) {
let b1 = (line.y_start >> 5);
let b2 = ((line.y_start << 3) | (line.y_end >> 10)) & 0xff;
let b3 = (line.y_end >> 2) & 0xff;
let b4 = ((line.y_end << 6) | (line.x_end >> 8)) & 0xff;
let b5 = (line.x_end) & 0xff;
output.writeU8(b1);
output.writeU8(b2);
output.writeU8(b3);
output.writeU8(b4);
output.writeU8(b5);
output.writeU8(line.color);
}
layer.length = output.pos - start;
output.writeU16(data_term);
}
output.write_string(this.magic2);
let ckpos = output.pos;
// retrace and write layer lengths
for (let layer of layers) {
output.seek(layer.l1);
output.writeU32(layer.length);
output.seek(layer.l2);
output.writeU32(layer.length);
}
output.seek(ckpos);
// compute xor checksum
let cksum = 0;
let array = output.array;
for (let i=0; i<array.length; i++) {
cksum = cksum ^ (array[i] || 0);
}
output.writeU32(cksum);
return output.toBuffer();
}
}
CXDLP.export = function(params) {
let { settings, width, height, slices } = params;
let { thumb, preview1, preview2 } = params;
let { device, process } = settings;
let cxdlp = new CXDLP();
if (width === 1620 && height === 2560) {
cxdlp.model = "CL-60";
} else
if (width === 3840 && height === 2400) {
cxdlp.model = "CL-89";
} else
if (width === 3840 && height === 2160) {
cxdlp.model = "CL-133";
} else {
throw `invalid printer dimensions ${width} x ${height}`;
}
cxdlp.layers = slices.map(a => {return { lines: a }});
cxdlp.res_x = device.resolutionX;
cxdlp.res_y = device.resolutionY;
cxdlp.dim_x = device.bedWidth.toFixed(5);
cxdlp.dim_y = device.bedDepth.toFixed(5);
cxdlp.layer = process.slaSlice.toFixed(5);
cxdlp.thumb = thumb;
cxdlp.preview1 = preview1;
cxdlp.preview2 = preview2;
cxdlp.light_on = process.slaLayerOn;
cxdlp.light_off = process.slaLayerOff;
// cxdlp.light_pwm = 255;
cxdlp.lift_dist = process.slaPeelDist;
cxdlp.lift_speed = process.slaPeelLiftRate * 60;
cxdlp.base_layers = process.slaBaseLayers;
cxdlp.base_light_on = process.slaBaseOn;
// cxdlp.base_light_pwm = 255;
cxdlp.base_lift_dist = process.slaBasePeelDist;
cxdlp.base_lift_speed = process.slaBasePeelLiftRate * 60;
return cxdlp.write();
};
CXDLP.render = function(params) {
let { width, height, index, widgets, scaleX, scaleY } = params;
let width2 = width / 2, height2 = height / 2;
let array = [];
let count = 0;
let area = 0;
function scaleMovePoly(poly) {
let points = poly.points;
poly._bounds = undefined;
for (let i=0, il=points.length; i<il; i++) {
let p = points[i];
p.y = height - (p.y * scaleY + height2);
p.x = p.x * scaleX + width2;
}
if (poly.inner) {
for (let i=0, ia=poly.inner, il=poly.inner.length; i<il; i++) {
scaleMovePoly(ia[i]);
}
}
}
// serialize poly into wasm heap memory
function writePoly(writer, poly) {
let pos = writer.skip(2);
let inner = poly.inner;
writer.writeU16(inner ? inner.length : 0, true);
let points = poly.points;
let bounds = poly.bounds;
writer.writeU16(points.length, true);
writer.writeU16(bounds.minx, true);
writer.writeU16(bounds.maxx, true);
writer.writeU16(bounds.miny, true);
writer.writeU16(bounds.maxy, true);
for (let j=0, jl=points.length; j<jl; j++) {
let point = points[j];
writer.writeF32(point.x, true);
writer.writeF32(point.y, true);
}
if (inner && inner.length) {
for (let i=0, il=inner.length; i<il; i++) {
writePoly(writer, inner[i]);
}
}
// write total struct length at struct head
writer.view.setUint16(pos, writer.pos - pos, true);
}
widgets.forEach(widget => {
let slice = widget.slices[index];
if (slice) {
if (slice.synth) count++;
let polys = slice.unioned;
if (!polys) polys = slice.tops.map(t => t.poly);
if (slice.supports) polys.appendAll(slice.supports);
array.appendAll(polys.map(poly => {
return poly.clone(true).move(widget.track.pos);
}));
count += polys.length;
}
});
let wasm = kiri.driver.SLA.wasm;
let imagelen = width * height;
let writer = new self.DataWriter(new DataView(wasm.memory.buffer), imagelen);
writer.writeU16(width, true);
writer.writeU16(height, true);
writer.writeU16(array.length, true);
// scale and move all polys to fit in rendered platform coordinates
for (let i=0, il=array.length; i<il; i++) {
let poly = array[i];
area += poly.areaDeep();
scaleMovePoly(poly);
writePoly(writer, poly);
}
wasm.render(0, imagelen, 0);
let image = wasm.heap;//.slice(0, imagelen);
let lines = [];
for (let x=0; x<width; x++) {
let y_start = 0;
let lastv = 0;
for (let y=0; y<height; y++) {
let v = image[x * height + y];
if (v !== lastv) {
if (lastv) {
// emit any non-zero sequence
lines.push({y_start, y_end: y, x_end: x, color: lastv});
}
if (v) {
// start a new sequence
y_start = y;
}
}
lastv = v;
}
}
return { lines, area };
}

View file

@ -0,0 +1,534 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { SLA } from './driver.js';
function generatePhoton(print, conf, progress) {
let printset = print.settings,
process = printset.process,
device = printset.device,
width = conf.width,
height = conf.height,
layerCount = conf.lines.length,
layerBytes = width * height,
small = conf.small,
large = conf.large,
slices = conf.slices,
subcount = process.slaAntiAlias || 1,
masks = [],
coded;
if (SLA.legacy || subcount > 1) {
let d = 8 / subcount;
for (let i=0; i<subcount; i++) {
masks.push((1 << (8 - i * d)) - 1);
}
let ccl = 0;
let tcl = conf.lines.length * subcount;
let converted = conf.lines.map((line, index) => {
let count = line.length;
let lineDV = new DataView(line.buffer);
let bits = new Uint8Array(line.length);
let bitsDV = new DataView(bits.buffer);
let subs = [{ data: bits, view: bitsDV }];
for (let sl=1; sl<subcount; sl++) {
bits = bits.slice();
bitsDV = new DataView(bits.buffer);
subs.push({ data: bits, view: bitsDV });
}
// use R from RGB since that was painted on the canvas
for (let s=0; s<subcount; s++) {
let view = subs[s].view;
let mask = masks[s];
for (let i = 0; i < count; i++) {
let dv = lineDV.getUint8(i);
view.setUint8(i, (dv / subcount) & mask ? 1 : 0);
}
progress((ccl++/tcl) * 0.4, `layer_convert`);
}
return { subs };
});
coded = encodeLayers(converted, "photon", (pro => {
progress(pro * 0.4 + 0.4, "layer_encode");
}));
} else {
let codedlen = slices.reduce((t,l) => {
return t + l.reduce((t,a) => {
return t + a.length
}, 0);
}, 0);
coded = {
layers: slices.map(slice => { return { sublayers: slice }}),
length: codedlen
};
}
let codelen = coded.layers.length;
let buflen = 3000 + coded.length + (codelen * subcount * 28) + small.byteLength + large.byteLength;
let filebuf = new ArrayBuffer(buflen);
let filedat = new self.DataWriter(new DataView(filebuf));
let printtime = (process.slaBaseLayers * process.slaBaseOn) +
(coded.layers.length - process.slaBaseLayers) * process.slaLayerOn;
filedat.writeU32(0x1900fd12); // header
filedat.writeU32(2,true); // version
filedat.writeF32(68.04, true); // bed x
filedat.writeF32(120.96, true); // bed y
filedat.writeF32(150.0, true); // bed z
filedat.skip(12); // padding
filedat.writeF32(process.slaSlice, true); // layer height
filedat.writeF32(process.slaLayerOn, true); // default lamp on
filedat.writeF32(process.slaBaseOn, true); // base lamp on
filedat.writeF32(process.slaLayerOff, true); // lamp off
filedat.writeU32(process.slaBaseLayers, true); // base layers
filedat.writeU32(1440, true); // device x
filedat.writeU32(2560, true); // device y
let hirez = filedat.skip(4); // hirez preview address filled pater
let layerpos = filedat.skip(4); // layer data address filled later
filedat.writeU32(codelen, true);
let lorez = filedat.skip(4); // hirez preview address filled later
filedat.writeU32(printtime, true); // print time seconds
filedat.writeU32(1, true); // projection type (1=lcd, 0=cast)
let proppos = filedat.skip(4); // print properties address filled later
let proplen = filedat.skip(4); // print properties length filled later
filedat.writeU32(subcount, true); // AA level (sub layers)
filedat.writeU16(0x00ff, true); // light pwm (TODO);
filedat.writeU16(0x00ff, true); // light pwm bottom (TODO);
let propstart = filedat.pos;
filedat.view.setUint32(proppos, filedat.pos, true);
// write print properties
filedat.writeF32(process.slaBasePeelDist, true);
filedat.writeF32(process.slaBasePeelLiftRate * 60 , true);
filedat.writeF32(process.slaPeelDist, true);
filedat.writeF32(process.slaPeelLiftRate * 60 , true);
filedat.writeF32(process.slaPeelDropRate * 60, true);
filedat.writeF32(0, true); // volume of used
filedat.writeF32(0, true); // weight of used
filedat.writeF32(0, true); // cost of used
filedat.writeF32(0, true); // bottom off delay time
filedat.writeF32(0, true); // light off delay time
filedat.writeU32(process.slaBaseLayers, true);
filedat.writeF32(0, true); // p1 ?
filedat.writeF32(0, true); // p2 ?
filedat.writeF32(0, true); // p3 ?
filedat.writeF32(0, true); // p4 ?
filedat.view.setUint32(proplen, filedat.pos - propstart, true);
filedat.view.setUint32(layerpos, filedat.pos, true);
// write layer headers
let layers = coded.layers;
let layerat = [];
for (let sc=0; sc<subcount; sc++)
for (let l=0; l<layers.length; l++) {
let layer = layers[l].sublayers[sc];
filedat.writeF32(process.slaFirstOffset + process.slaSlice * l, true); // layer height
filedat.writeF32(l < process.slaBaseLayers ? process.slaBaseOn : process.slaLayerOn, true);
filedat.writeF32(l < process.slaBaseLayers ? process.slaBaseOff : process.slaLayerOff, true);
layerat.push(layer.repos = filedat.skip(4)); // rewrite later
filedat.writeU32(layer.length, true);
filedat.skip(16); // padding
}
// write layer data
let clo = 0;
let tlo = layers.length * subcount;
for (let sc=0; sc<subcount; sc++)
for (let l=0; l<layers.length; l++) {
let layer = layers[l].sublayers[sc];
filedat.view.setUint32(layer.repos, filedat.pos, true);
for (let j=0; j<layer.length; j++) {
filedat.writeU8(layer[j], false);
}
progress(((clo++/tlo) * 0.1) + 0.9, "layer_write");
}
filedat.view.setUint32(hirez, filedat.pos, true);
writePhotonImage({
width: 400,
height: 300,
data: conf.large
}, filedat);
filedat.view.setUint32(lorez, filedat.pos, true);
writePhotonImage({
width: 200,
height: 125,
data: conf.small
}, filedat);
return filebuf;
}
function generatePhotons(print, conf, progress) {
let printset = print.settings,
process = printset.process,
device = printset.device,
width = conf.width,
height = conf.height,
slices = conf.slices,
layerCount = conf.lines.length,
layerBytes = width * height,
coded;
if (SLA.legacy) {
let converted = conf.lines.map((line, index) => {
let count = line.length / 4;
let bits = new Uint8Array(line.length / 4);
let bitsDV = new DataView(bits.buffer);
let lineDV = new DataView(line.buffer);
// reduce RGB to R = 0||1
for (let i = 0; i < count; i++) {
// defeat anti-aliasing for the moment
bitsDV.setUint8(i, lineDV.getUint8(i * 4) > 0 ? 1 : 0);
}
progress(index / conf.lines.length);
return { subs: [{
exposureTime: process.slaLayerOn,
data: bits
}] };
});
coded = encodeLayers(converted, "photons");
} else {
let codedlen = slices.reduce((t,l) => {
return t + l.reduce((t,a) => {
return t + a.length
}, 0);
}, 0);
coded = {
layers: slices.map(slice => { return { sublayers: slice }}),
length: codedlen
};
}
let filebuf = new ArrayBuffer(75366 + coded.length + 28 * layerCount);
let filedat = new DataView(filebuf);
let filePos = 0;
filedat.setUint32 (0, 2, false);
filedat.setUint32 (4, 3227560, false);
filedat.setUint32 (8, 824633720, false);
filedat.setUint16 (12, 10, false);
filedat.setFloat64(14, process.slaSlice, false);
filedat.setFloat64(22, process.slaLayerOn, false);
filedat.setFloat64(30, process.slaLayerOff, false);
filedat.setFloat64(38, process.slaBaseOn, false);
filedat.setUint32 (46, process.slaBaseLayers, false);
filedat.setFloat64(50, process.slaPeelDist, false);
filedat.setFloat64(58, process.slaPeelLift, false);
filedat.setFloat64(66, process.slaPeelDrop, false);
filedat.setFloat64(74, 69420, false);
filedat.setUint32 (82, 224, false);
filedat.setUint32 (86, 42, false);
filedat.setUint32 (90, 168, false);
filedat.setUint32 (94, 10, false);
filedat.setUint32 (75362, layerCount, false);
filePos = 75366;
for (let i = 0; i < layerCount; i++) {
let layer = coded.layers[i],
sublayer = layer.sublayers[0],
numbytes = sublayer.length;
filedat.setUint32 (filePos + 0, 69420, false);
filedat.setFloat64(filePos + 4, 0);
filedat.setUint32 (filePos + 12, height, false);
filedat.setUint32 (filePos + 16, width, false);
filedat.setUint32 (filePos + 20, numbytes * 8 + 32, false);
filedat.setUint32 (filePos + 24, 2684702720, false);
filePos += 28;
for (let j = 0; j < numbytes; j++) {
filedat.setUint8(filePos + j, sublayer[j], false);
}
filePos += numbytes;
progress((i / layerCount) / 2 + 0.5);
}
return filebuf;
}
function encodeLayers(input, type, progress) {
let layers = [], length = 0, total = 0, count = 0;
input.forEach(layer => {
layer.subs.forEach(sub => total++);
});
for (let index = 0; index < input.length; index++) {
let subs = input[index].subs,
sublayers = [],
sublength = 0;
for (let subindex = 0; subindex < subs.length; subindex++) {
let data = subs[subindex].data;
let encoded = rleEncode(data, type);
sublength += encoded.length;
sublayers.push(encoded);
if (progress) progress(count++/total);
if (type == "photons") break;
}
length += sublength;
layers.push({
sublength,
sublayers
});
}
return { length, layers };
}
function rleEncode(data, type) {
let maxlen = (type === 'photons') ? 128 : 125,
color = data[0],
runlen = 1,
output = [];
for (let index = 1; index < data.length; index++) {
let newColor = data[index];
if (newColor !== color) {
output.push(rleByte(color, runlen, type));
color = newColor;
runlen = 1;
} else {
if (runlen === maxlen) {
output.push(rleByte(color, runlen, type));
runlen = 1;
} else {
runlen++;
}
}
}
if (runlen > 0) {
output.push(rleByte(color, runlen, type));
}
return output;
}
function rleByte(color, length, type) {
switch (type) {
case 'photon':
return (length & 0x7f) | ((color << 7) & 0x80);
case 'photons':
length--;
return (length & 1 ? 128 : 0) |
(length & 2 ? 64 : 0) |
(length & 4 ? 32 : 0) |
(length & 8 ? 16 : 0) |
(length & 16 ? 8 : 0) |
(length & 32 ? 4 : 0) |
(length & 64 ? 2 : 0) | color;
}
}
function rleDecode(data, type) {
let bytes = [];
if (type === 'photon') {
for (let i = 0; i < data.length; i++) {
let val = data[i],
color = val >> 7,
count = val & 0x7f;
for (let j = 0; j < count; j++) {
bytes.push(color);
}
}
} else {
for (let i = 0; i < data.length; i++) {
let val = data[i],
color = val & 1,
count =
((val & 128 ? 1 : 0) |
(val & 64 ? 2 : 0) |
(val & 32 ? 4 : 0) |
(val & 16 ? 8 : 0) |
(val & 8 ? 16 : 0) |
(val & 4 ? 32 : 0) |
(val & 2 ? 64 : 0)) + 1;
for (let j = 0; j < count; j++) {
bytes.push(color);
}
}
}
return bytes;
}
// write out a thumbnail image
function writePhotonImage(preview, writer) {
let data = new Uint8Array(preview.data), len = data.byteLength;
writer.writeU32(preview.width, true);
writer.writeU32(preview.height, true);
let hpos = writer.skip(4);
writer.writeU32(len/2, true);
writer.view.setUint32(hpos, writer.pos, true);
let pos = 0;
while (pos < len) {
let r = data[pos++],
g = data[pos++],
b = data[pos++],
a = data[pos++],
v = (((r/4)&0x1f) << 11) |
(((g/4)&0x1f) << 6) |
(((b/4)&0x1f) << 0) ;
writer.writeU16(v, true);
}
}
// for unbound workers
// if (self.WASM) {
// let {exports} = wasmInstance;
// let heap = new Uint8Array(exports.memory.buffer);
// self.wasm = {
// heap,
// memory: exports.memory,
// // heap: wasmMemory,
// // memory: memoryBytes,
// render: exports.render,
// rle_encode: exports.rle_encode
// };
// } else
// new WebAssembly rasterizer
function renderLayerWasm(params) {
let { width, height, index, widgets, scaleX, scaleY, masks } = params;
let width2 = width / 2, height2 = height / 2;
let array = [];
let count = 0;
let area = 0;
function scaleMovePoly(poly) {
let points = poly.points;
poly._bounds = undefined;
for (let i=0, il=points.length; i<il; i++) {
let p = points[i];
p.y = height - (p.y * scaleY + height2);
p.x = p.x * scaleX + width2;
}
if (poly.inner) {
for (let i=0, ia=poly.inner, il=poly.inner.length; i<il; i++) {
scaleMovePoly(ia[i]);
}
}
}
// serialize poly into wasm heap memory
function writePoly(writer, poly) {
let pos = writer.skip(2);
let inner = poly.inner;
writer.writeU16(inner ? inner.length : 0, true);
let points = poly.points;
let bounds = poly.bounds;
writer.writeU16(points.length, true);
writer.writeU16(bounds.minx, true);
writer.writeU16(bounds.maxx, true);
writer.writeU16(bounds.miny, true);
writer.writeU16(bounds.maxy, true);
for (let j=0, jl=points.length; j<jl; j++) {
let point = points[j];
writer.writeF32(point.x, true);
writer.writeF32(point.y, true);
}
if (inner && inner.length) {
for (let i=0, il=inner.length; i<il; i++) {
writePoly(writer, inner[i]);
}
}
// write total struct length at struct head
writer.view.setUint16(pos, writer.pos - pos, true);
}
widgets.forEach(widget => {
let slice = widget.slices[index];
if (slice) {
if (slice.synth) count++;
let polys = slice.unioned;
if (!polys) polys = slice.tops.map(t => t.poly);
if (slice.supports) polys.appendAll(slice.supports);
array.appendAll(polys.map(poly => {
return poly.clone(true).move(widget.track.pos);
}));
count += polys.length;
}
});
let wasm = SLA.wasm;
let imagelen = width * height;
let writer = new self.DataWriter(new DataView(wasm.memory.buffer), imagelen);
writer.writeU16(width, true);
writer.writeU16(height, true);
writer.writeU16(array.length, true);
// scale and move all polys to fit in rendered platform coordinates
for (let i=0, il=array.length; i<il; i++) {
let poly = array[i];
area += poly.areaDeep();
scaleMovePoly(poly);
writePoly(writer, poly);
}
wasm.render(0, imagelen, 0);
let image = wasm.heap.slice(0, imagelen), layers = [];
// one rle encoded bitstream for each mash (anti-alias sublayer)
for (let l=0; l<masks.length; l++) {
// while the image is still in wasm heap memory, rle encode it
let rlelen = wasm.rle_encode(0, 0, imagelen, masks[l], imagelen, 0);
layers.push(wasm.heap.slice(imagelen, imagelen + rlelen));
}
return { image, layers, end: count === 0, area };
}
// legacy JS-only rasterizer uses OffscreenCanvas
function renderLayer(params) {
let {width, height, index, widgets, scaleX, scaleY} = params;
let layer = new OffscreenCanvas(height,width);
let opt = { scaleX, scaleY, width, height, width2: width/2, height2: height/2 };
let ctx = layer.getContext('2d');
ctx.fillStyle = 'rgb(200, 0, 0)';
let count = 0;
let area = 0;
widgets.forEach(widget => {
let slice = widget.slices[index];
if (slice) {
// prevent premature exit on empty synth slice
if (slice.synth) count++;
let polys = slice.unioned;
if (!polys) polys = slice.tops.map(t => t.poly);
if (slice.supports) polys.appendAll(slice.supports);
polys.forEach(poly => {
area += poly.areaDeep();
poly.move(widget.track.pos);
ctx.beginPath();
polyout(poly.setClockwise(), ctx, opt);
if (poly.inner) {
poly.inner.forEach(inner => {
polyout(inner.setCounterClockwise(), ctx, opt);
});
}
ctx.fill();
count++;
});
} else {
// console.log({no_slice_at: index})
}
});
let data = ctx.getImageData(0,0,height,width).data;
// reduce RGBA to R
let red = new Uint8ClampedArray(data.length / 4);
for (let i=0; i<red.length; i++) {
red[i] = data[i*4];
}
return { image: red, end: count === 0, area };
}
function polyout(poly, ctx, opt) {
let { scaleX, scaleY, width, height, width2, height2 } = opt;
poly.forEachPoint((p,i) => {
if (i === 0) {
ctx.moveTo(height - (p.y * scaleY + height2), p.x * scaleX + width2);
} else {
ctx.lineTo(height - (p.y * scaleY + height2), p.x * scaleX + width2);
}
}, true);
ctx.closePath();
}
export const photon = {
generatePhoton,
generatePhotons,
renderLayer,
renderLayerWasm
};

View file

@ -0,0 +1,98 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { api } from '../../core/api.js';
const state = {
api: undefined,
alert: undefined,
radians: 0,
surfaces: [],
selecting: false
};
export function init() {
api.event.on("key.esc", () => {
if (state.selecting) {
faceDone();
}
});
// api.event.on("mode.set", (mode) => { });
// api.event.on("view.set", (mode) => { });
api.event.on("preview.view", view => {
const process = api.conf.get().process;
if (!process.ctOriginBounds) return;
const bounds = new THREE.Box3().expandByObject(view);
api.platform.set_bounds(new THREE.Box3(
new THREE.Vector3(bounds.min.x, bounds.min.z, bounds.min.y),
new THREE.Vector3(bounds.max.x, bounds.max.z, bounds.max.y),
));
});
// Surface Selection Buttons
api.event.on("button.click", target => {
let process = api.conf.get().process;
switch (target) {
case api.ui.faceAdd:
target.classList.add('editing');
return faceAdd();
case api.ui.faceDun:
return faceDone();
case api.ui.faceClr:
api.uc.confirm("clear surface selection?").then(ok => {
// todo
});
break;
}
});
}
function faceAdd() {
if (state.selecting) return faceDone();
const { api, radians, surfaces } = state;
CAM.surface_prep(radians, () => {
api.hide.alert(alert);
state.alert = api.show.alert("[esc] cancels surface selection");
for (let [wid, arr] of Object.entries(surfaces)) {
let widget = api.widgets.forid(wid);
if (widget && arr.length)
for (let faceid of arr) {
CAM.surface_toggle(widget, faceid, radians, faceids => {
// surfaces[widget.id] = faceids;
});
}
}
api.feature.on_mouse_up = (obj, ev) => {
let { face } = obj;
let min = Math.min(face.a, face.b, face.c);
let faceid = min / 3;
let widget = obj.object.widget;
CAM.surface_toggle(widget, faceid, radians, faceids => {
surfaces[widget.id] = faceids;
});
};
state.selecting = true;
});
}
function faceDone() {
if (!state.selecting) return;
const { api, alert, surfaces } = state;
for (let wid of Object.keys(surfaces)) {
let widget = api.widgets.forid(wid);
if (widget) {
CAM.surface_clear(widget);
} else {
delete surfaces[wid];
}
}
api.hide.alert(alert);
api.feature.on_mouse_up = undefined;
api.ui.faceAdd.classList.remove('editing')
state.selecting = false;
}

View file

@ -0,0 +1,21 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { LASER, TYPE } from '../laser/driver.js';
const DEG2RAD = Math.PI / 180;
function init(worker) {
// console.log({ WIREEDM_INIT: worker });
}
async function prepare(widgets, settings, update) {
widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled);
LASER.prepare(widgets, settings, update);
}
export const WEDM = Object.assign({}, LASER, {
type: TYPE.WEDM,
name: 'WireEDM',
init,
prepare
});

View file

@ -0,0 +1,5 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
export function init() {
// TODO
}

View file

@ -0,0 +1,13 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { LASER, TYPE } from '../laser/driver.js';
function init(worker) {
// console.log({ WATERJET_INIT: worker });
}
export const WJET = Object.assign({}, LASER, {
type: TYPE.WJET,
name: 'WaterJet',
init
});