Merge branch 'rel-4.3' into helical-op
This commit is contained in:
commit
dfc2cd7f17
325 changed files with 18349 additions and 25230 deletions
307
src/kiri/mode/cam/anim-2d-be.js
Normal file
307
src/kiri/mode/cam/anim-2d-be.js
Normal 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 }});
|
||||
}
|
||||
309
src/kiri/mode/cam/anim-2d-fe.js
Normal file
309
src/kiri/mode/cam/anim-2d-fe.js
Normal 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;
|
||||
}
|
||||
396
src/kiri/mode/cam/anim-3d-be.js
Normal file
396
src/kiri/mode/cam/anim-3d-be.js
Normal 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;
|
||||
}
|
||||
}
|
||||
253
src/kiri/mode/cam/anim-3d-fe.js
Normal file
253
src/kiri/mode/cam/anim-3d-fe.js
Normal 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);
|
||||
}
|
||||
84
src/kiri/mode/cam/cl-flip.js
Normal file
84
src/kiri/mode/cam/cl-flip.js
Normal 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();
|
||||
}
|
||||
}
|
||||
61
src/kiri/mode/cam/cl-helical.js
Normal file
61
src/kiri/mode/cam/cl-helical.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { api } from "../../core/api.js";
|
||||
import { CAM } from "./driver-fe.js";
|
||||
|
||||
export let helicalOn = false;
|
||||
export function selectHelical() {
|
||||
if (helicalOn) {
|
||||
return func.surfaceDone();
|
||||
}
|
||||
func.clearPops();
|
||||
alert = api.show.alert("analyzing surfaces...", 1000);
|
||||
let cylinders = poppedRec.cylinders;
|
||||
|
||||
CAM.surface_prep(currentIndex * RAD2DEG, () => {
|
||||
api.hide.alert(alert);
|
||||
alert = api.show.alert("[esc] cancels surface selection");
|
||||
for (let [wid, arr] of Object.entries(cylinders)) {
|
||||
let widget = api.widgets.forid(wid);
|
||||
if (widget && arr.length)
|
||||
for (let faceid of arr) {
|
||||
CAM.cylinderToggle(widget, faceid, (faceids) => {
|
||||
cylinders[widget.id] = faceids;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
helicalOn = hoveredOp;
|
||||
helicalOn.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.cylinderToggle(widget, faceid, ({ faces, error }) => {
|
||||
if (error) {
|
||||
api.show.alert(error, 3000);
|
||||
return;
|
||||
}
|
||||
cylinders[widget.id] = faces;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function helicalDone(){
|
||||
if (!(helicalOn && poppedRec && poppedRec.cylinders)) {
|
||||
return;
|
||||
}
|
||||
let { cylinders } = poppedRec;
|
||||
for (let wid of Object.keys(cylinders)) {
|
||||
let widget = api.widgets.forid(wid);
|
||||
if (widget) {
|
||||
CAM.cylinderClear(widget);
|
||||
} else {
|
||||
delete cylinders[wid];
|
||||
}
|
||||
}
|
||||
api.hide.alert(alert);
|
||||
api.feature.on_mouse_up = undefined;
|
||||
helicalOn.classList.remove("editing");
|
||||
helicalOn = false;
|
||||
console.log("helicalOn set to false");
|
||||
};
|
||||
208
src/kiri/mode/cam/cl-hole.js
Normal file
208
src/kiri/mode/cam/cl-hole.js
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
/** 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
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
631
src/kiri/mode/cam/cl-ops.js
Normal file
631
src/kiri/mode/cam/cl-ops.js
Normal file
|
|
@ -0,0 +1,631 @@
|
|||
/** 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 { selectHelical } from './cl-helical.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('helical', {
|
||||
tool: 'camHelicalTool',
|
||||
spindle: 'camHelicalSpindle',
|
||||
rate: 'camHelicalDownSpeed',
|
||||
feed: 'camHelicalSpeed',
|
||||
down: 'camHelicalDown',
|
||||
thru: 'camHelicalThru',
|
||||
offset: 'camHelicalOffset',
|
||||
forceStartAng:'camHelicalForceStartAngle',
|
||||
startAng:'camHelicalStartAngle',
|
||||
offOver: 'camHelicalOffsetOverride',
|
||||
fromTop: 'camHelicalFromStockTop',
|
||||
}).inputs = {
|
||||
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
|
||||
offset: UC.newSelect(LANG.cc_offs_s, {title: LANG.cc_offs_l,}, "helicaloff"),
|
||||
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.toFloat, units:true}),
|
||||
feed: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toFloat, units:true}),
|
||||
down: UC.newInput(LANG.ch_sdwn_s, {title:LANG.ch_sdwn_l, convert:UC.toFloat, units:true}),
|
||||
startAng: UC.newInput(LANG.ch_stra_s, {title:LANG.ch_stra_l, convert:UC.toDegsFloat, bound:UC.bound(-360,360),show:() => poppedRec.forceStartAng}),
|
||||
offOver: UC.newInput(LANG.cc_offd_s, {title:LANG.cc_offd_l, convert:UC.toFloat, units:true, bound:UC.bound(0,Infinity)}),
|
||||
sep: UC.newBlank({class:"pop-sep"}),
|
||||
forceStartAng: UC.newBoolean(LANG.ch_fsta_s, undefined, {title:LANG.ch_fsta_l, }),
|
||||
fromTop: UC.newBoolean(LANG.cd_ftop_s,undefined, {title:LANG.cd_ftop_l}),
|
||||
sep: UC.newBlank({class:"pop-sep"}),
|
||||
thru: UC.newInput(LANG.cd_thru_s, {title:LANG.cd_thru_l, convert:UC.toFloat, units:true}),
|
||||
actions: UC.newRow([
|
||||
UC.newButton(LANG.select, selectHelical, {title:LANG.cd_seli_l}),
|
||||
], {class:"ext-buttons f-col"})
|
||||
};
|
||||
|
||||
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 = {};
|
||||
}
|
||||
143
src/kiri/mode/cam/cl-stock.js
Normal file
143
src/kiri/mode/cam/cl-stock.js
Normal 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();
|
||||
}
|
||||
65
src/kiri/mode/cam/cl-surface.js
Normal file
65
src/kiri/mode/cam/cl-surface.js
Normal 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
224
src/kiri/mode/cam/cl-tab.js
Normal 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;
|
||||
}
|
||||
181
src/kiri/mode/cam/cl-trace.js
Normal file
181
src/kiri/mode/cam/cl-trace.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
901
src/kiri/mode/cam/client.js
Normal file
901
src/kiri/mode/cam/client.js
Normal file
|
|
@ -0,0 +1,901 @@
|
|||
/** 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';
|
||||
import { helicalOn, helicalDone } from './cl-helical.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();
|
||||
helicalDone();
|
||||
}
|
||||
|
||||
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 opAddHelical = () => {
|
||||
traceDone();
|
||||
surfaceDone();
|
||||
let rec = env.popOp.helical.new();
|
||||
rec.cylinders = { /* widget.id: [ faces... ] */ };
|
||||
func.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 "helical": return opAddHelical();
|
||||
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();
|
||||
}
|
||||
119
src/kiri/mode/cam/driver-be.js
Normal file
119
src/kiri/mode/cam/driver-be.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/** 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
|
||||
} } )));
|
||||
}
|
||||
|
||||
dispatch.cam_cylinder_find = async function(data,send){
|
||||
const { id, face, settings } = data;
|
||||
const widget = kiri.worker.cache[id];
|
||||
try{
|
||||
send.done(CAM.cylinder_poly_find(widget, face));
|
||||
}catch(error){
|
||||
send.done({error});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const CAM = {
|
||||
init,
|
||||
surface_prep,
|
||||
surface_find,
|
||||
slice: cam_slice,
|
||||
prepare: cam_prepare,
|
||||
export: cam_export
|
||||
};
|
||||
131
src/kiri/mode/cam/driver-fe.js
Normal file
131
src/kiri/mode/cam/driver-fe.js
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/** 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 cylinder_show(widget) {
|
||||
widget.selectFaces(Object.values(widget._cylinders).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);
|
||||
}
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
CAM.cylinderShow = function(onProgress,onDone){
|
||||
|
||||
kiri.client.sync();
|
||||
const settings = api.conf.get();
|
||||
|
||||
|
||||
}
|
||||
|
||||
function cylinderToggle(widget, face, onDone){
|
||||
let cyls = widget._cylinders = widget._cylinders || {};
|
||||
for (let [root, faces] of Object.entries(cyls)) {
|
||||
if (faces.contains(face)) { // if a face group contains the selected face
|
||||
// delete this face group
|
||||
delete cyls[root];
|
||||
CAM.cylinder_show(widget);
|
||||
onDone(Object.keys(cyls).map(v => parseInt(v)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
//send
|
||||
kiri.client.send("cam_cylinder_find", { id: widget.id, face }, ({faces,error}) => {
|
||||
if (faces?.length) {
|
||||
cyls[face] = faces;
|
||||
CAM.cylinder_show(widget);
|
||||
}
|
||||
|
||||
onDone({faces:Object.keys(cyls).map(v => parseInt(v)),error});
|
||||
});
|
||||
}
|
||||
|
||||
function cylinderClear(widget){
|
||||
widget.selectFaces([]);
|
||||
widget._cylinders = {};
|
||||
}
|
||||
|
||||
export const CAM = {
|
||||
surface_prep,
|
||||
surface_show,
|
||||
cylinder_show,
|
||||
cylinderToggle,
|
||||
cylinderClear,
|
||||
surface_toggle,
|
||||
surface_clear,
|
||||
traces,
|
||||
traces_clear,
|
||||
holes
|
||||
};
|
||||
519
src/kiri/mode/cam/export.js
Normal file
519
src/kiri/mode/cam/export.js
Normal 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;
|
||||
};
|
||||
127
src/kiri/mode/cam/op-contour.js
Normal file
127
src/kiri/mode/cam/op-contour.js
Normal 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 };
|
||||
68
src/kiri/mode/cam/op-drill.js
Normal file
68
src/kiri/mode/cam/op-drill.js
Normal 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 };
|
||||
13
src/kiri/mode/cam/op-gcode.js
Normal file
13
src/kiri/mode/cam/op-gcode.js
Normal 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);
|
||||
}
|
||||
}
|
||||
166
src/kiri/mode/cam/op-helical.js
Normal file
166
src/kiri/mode/cam/op-helical.js
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
|
||||
import { arcToPath } from "../../../geo/paths";
|
||||
import { newPolygon } from "../../../geo/polygon";
|
||||
import { CAM } from "./driver-fe"
|
||||
|
||||
|
||||
export class OpHelical extends CamOp {
|
||||
constructor(state, op) {
|
||||
super(state, op);
|
||||
}
|
||||
|
||||
async slice(progress) {
|
||||
let { op, state } = this;
|
||||
let { settings, addSlices, widget, updateToolDiams, slicer } = state;
|
||||
let { zBottom, zThru, thruHoles, color } = state;
|
||||
let {
|
||||
cylinders,
|
||||
forceStartAng,
|
||||
startAng,
|
||||
fromTop,
|
||||
offOver,
|
||||
offset,
|
||||
down,
|
||||
thru,
|
||||
} = op;
|
||||
|
||||
let tool = new CAM.Tool(settings, op.tool),
|
||||
toolDiam = tool.fluteDiameter(),
|
||||
sliceOut = (this.sliceOut = []),
|
||||
startAngle = forceStartAng ? startAng : 0;
|
||||
|
||||
updateToolDiams(toolDiam);
|
||||
|
||||
console.log(cylinders, cylinders[widget.id]);
|
||||
let faces = cylinders[widget.id];
|
||||
|
||||
if (faces.faces) faces = faces.faces;
|
||||
console.log(faces);
|
||||
|
||||
let polys = [];
|
||||
|
||||
//iterate over selected faces
|
||||
for (let face of faces) {
|
||||
//get poly info for the cylinder
|
||||
let { zmin, zmax, diam, center, interior, faces } =
|
||||
CAM.cylinder_poly_find(widget, face);
|
||||
|
||||
console.log({ zmin, zmax, diam, center, interior });
|
||||
|
||||
let radius = diam >> 1;
|
||||
|
||||
let radAdd,
|
||||
zBottom = zmin - thru,
|
||||
clockwise = true,
|
||||
numSegs = faces.length / 2,
|
||||
poly = newPolygon().setOpen();
|
||||
|
||||
if (offOver) {
|
||||
//apply offset override
|
||||
if (offset == "auto") {
|
||||
radAdd = (interior ? -offOver : offOver) * 2;
|
||||
} else if (offset == "inside") {
|
||||
radAdd = -offOver * 2;
|
||||
} else if (offset == "outside") {
|
||||
radAdd = offOver * 2;
|
||||
}
|
||||
} else {
|
||||
//otherwise, calculate based on offset type
|
||||
if (offset == "auto") {
|
||||
radAdd = (interior ? -toolDiam : toolDiam) * 2;
|
||||
} else if (offset == "inside") {
|
||||
radAdd = -toolDiam * 2;
|
||||
} else if (offset == "outside") {
|
||||
radAdd = toolDiam * 2;
|
||||
}
|
||||
}
|
||||
console.log({ radius, radAdd });
|
||||
radius += radAdd;
|
||||
|
||||
let startPoint = center
|
||||
.clone()
|
||||
.setZ(zmax)
|
||||
.add(
|
||||
newPoint(
|
||||
Math.cos(startAngle) * radius,
|
||||
Math.sin(startAngle) * radius,
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
let currentZ = zmax;
|
||||
for (;;) {
|
||||
let bottom = currentZ - down;
|
||||
if (bottom > zBottom) {
|
||||
//if not at the bottom, do a whole helix
|
||||
|
||||
let start = startPoint.setZ(currentZ),
|
||||
end = startPoint.clone().setZ(bottom);
|
||||
console.log(currentZ, bottom, [start.z, end.z]);
|
||||
|
||||
let path = arcToPath(start, end, numSegs, {
|
||||
clockwise,
|
||||
radius,
|
||||
center,
|
||||
});
|
||||
console.log({ path, start, end, radius, center });
|
||||
poly.addPoints(path);
|
||||
} else {
|
||||
//if at the bottom, do a partial helix
|
||||
|
||||
let toBottom = currentZ - zBottom,
|
||||
ofFull = toBottom / down,
|
||||
ofCircle = ofFull * (2 * Math.PI),
|
||||
finalAngle = startAngle + ofCircle,
|
||||
finalEnd = center
|
||||
.clone()
|
||||
.setZ(zBottom)
|
||||
.add(
|
||||
newPoint(
|
||||
Math.cos(finalAngle) * radius,
|
||||
Math.sin(finalAngle) * radius,
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
//circle down to final point
|
||||
poly.addPoints(
|
||||
arcToPath(startPoint.setZ(currentZ), finalEnd, numSegs * ofFull, {
|
||||
clockwise: !clockwise,
|
||||
radius,
|
||||
center,
|
||||
})
|
||||
);
|
||||
|
||||
//do a full circle at the bottom
|
||||
poly.addPoints(
|
||||
arcToPath(finalEnd, finalEnd, numSegs, {
|
||||
clockwise,
|
||||
radius,
|
||||
center,
|
||||
})
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
currentZ = bottom;
|
||||
}
|
||||
|
||||
polys.push(poly);
|
||||
}
|
||||
|
||||
let slice = newSlice(0);
|
||||
|
||||
slice.camTrace = { tool: op.tool, rate: op.feed, plunge: op.rate };
|
||||
slice.camLines = polys;
|
||||
|
||||
slice
|
||||
.output()
|
||||
.setLayer("Helical", { face: color, line: color })
|
||||
.addPolys(slice.camLines);
|
||||
addSlices(slice);
|
||||
sliceOut.push(slice);
|
||||
}
|
||||
|
||||
async prepare(ops, progress) {}
|
||||
}
|
||||
36
src/kiri/mode/cam/op-index.js
Normal file
36
src/kiri/mode/cam/op-index.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/kiri/mode/cam/op-laser-off.js
Normal file
17
src/kiri/mode/cam/op-laser-off.js
Normal 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);
|
||||
}
|
||||
}
|
||||
17
src/kiri/mode/cam/op-laser-on.js
Normal file
17
src/kiri/mode/cam/op-laser-on.js
Normal 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);
|
||||
}
|
||||
}
|
||||
118
src/kiri/mode/cam/op-lathe.js
Normal file
118
src/kiri/mode/cam/op-lathe.js
Normal 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 };
|
||||
68
src/kiri/mode/cam/op-level.js
Normal file
68
src/kiri/mode/cam/op-level.js
Normal 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 };
|
||||
298
src/kiri/mode/cam/op-outline.js
Normal file
298
src/kiri/mode/cam/op-outline.js
Normal 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 };
|
||||
285
src/kiri/mode/cam/op-pocket.js
Normal file
285
src/kiri/mode/cam/op-pocket.js
Normal 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 };
|
||||
162
src/kiri/mode/cam/op-register.js
Normal file
162
src/kiri/mode/cam/op-register.js
Normal 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 };
|
||||
318
src/kiri/mode/cam/op-rough.js
Normal file
318
src/kiri/mode/cam/op-rough.js
Normal 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 };
|
||||
112
src/kiri/mode/cam/op-shadow.js
Normal file
112
src/kiri/mode/cam/op-shadow.js
Normal 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 };
|
||||
290
src/kiri/mode/cam/op-trace.js
Normal file
290
src/kiri/mode/cam/op-trace.js
Normal 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 };
|
||||
32
src/kiri/mode/cam/op-xray.js
Normal file
32
src/kiri/mode/cam/op-xray.js
Normal 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
20
src/kiri/mode/cam/op.js
Normal 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() { }
|
||||
}
|
||||
39
src/kiri/mode/cam/ops.js
Normal file
39
src/kiri/mode/cam/ops.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/** 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';
|
||||
import { OpHelical } from './op-helical.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,
|
||||
"helical": OpHelical,
|
||||
"laser on": OpLaserOn,
|
||||
"laser off": OpLaserOff,
|
||||
"gcode": OpGCode,
|
||||
"index": OpIndex,
|
||||
"flip": CamOp
|
||||
};
|
||||
1093
src/kiri/mode/cam/prepare.js
Normal file
1093
src/kiri/mode/cam/prepare.js
Normal file
File diff suppressed because it is too large
Load diff
935
src/kiri/mode/cam/slice.js
Normal file
935
src/kiri/mode/cam/slice.js
Normal file
|
|
@ -0,0 +1,935 @@
|
|||
/** 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;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns an array of arrays of perpindicular triangles in the mesh.
|
||||
* Each sub-array is a list of triangle data that are part of the same
|
||||
* cylinder.
|
||||
*
|
||||
* @param {object} settings - settings object
|
||||
* @param {object} widget - widget object
|
||||
* @param {object} opts - options object
|
||||
* @return {array} - array of arrays of triangle data
|
||||
*/
|
||||
CAM.cylinders = async (settings, widget, opts) => {
|
||||
|
||||
let {} = opts ?? {}
|
||||
|
||||
let {array:verts} = widget.mesh.geometry.attributes.position
|
||||
let perpTriangles = []
|
||||
|
||||
//iterate over all triangles
|
||||
for(let i = 0; i < verts.length; i+=9){
|
||||
let a = [verts[i], verts[i+1], verts[i+2]],
|
||||
b = [verts[i+3], verts[i+4], verts[i+5]],
|
||||
c = [verts[i+6], verts[i+7], verts[i+8]];
|
||||
|
||||
//calculate normal
|
||||
let normal = new THREE.Vector3(a[0]-b[0], a[1]-b[1], a[2]-b[2])
|
||||
.cross(new THREE.Vector3(b[0]-c[0], b[1]-c[1], b[2]-c[2]))
|
||||
.normalize()
|
||||
|
||||
// if perpindicular normal, and at least 2 Zs are the same
|
||||
if(normal.z.round(5) == 0 && ! (a[2] != b[2] && b[2] != c[2])){
|
||||
let minZ = Math.min(a[2], b[2], c[2]),
|
||||
maxZ = Math.max(a[2], b[2], c[2]);
|
||||
if(minZ == maxZ){ // all Zs are the same indicated malformed geometry
|
||||
continue;
|
||||
}
|
||||
perpTriangles.push({...[a,b,c], normal,minZ,maxZ,i:index})
|
||||
}
|
||||
|
||||
}
|
||||
//map where zmax, zmin -> triangleData
|
||||
let cylinderTriangles = new Map()
|
||||
for(let t of perpTriangles){
|
||||
let hash = `${t.minZ.round(5)},${t.maxZ.round(5)}`
|
||||
if(!cylinderTriangles.has(hash)){
|
||||
cylinderTriangles.set(hash,[])
|
||||
}
|
||||
cylinderTriangles.get(hash).push(t)
|
||||
}
|
||||
|
||||
return Array.from(cylinderTriangles.values())
|
||||
}
|
||||
|
||||
CAM.surface_find = function(widget, faces, radians) {
|
||||
CAM.surface_prep(widget);
|
||||
return widget.tool.findConnectedSurface(faces, radians || 0, 0.0);
|
||||
};
|
||||
|
||||
CAM.cylinder_find = (widget, face)=>{
|
||||
CAM.surface_prep(widget);
|
||||
return widget.tool.findCylinderSurface(face);
|
||||
}
|
||||
|
||||
CAM.cylinder_poly_find = (widget, face)=>{
|
||||
CAM.surface_prep(widget);
|
||||
let faces = CAM.cylinder_find(widget, face);
|
||||
|
||||
let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4));
|
||||
|
||||
let slicer = new kiri.cam_slicer(widget,{})
|
||||
|
||||
let opts = {
|
||||
dedup:false,
|
||||
edges: true,
|
||||
over:true
|
||||
}
|
||||
|
||||
let firstOffset = faces[0]*9
|
||||
|
||||
let [x1,y1,z1,x2,y2,z2,x3,y3,z3]= Array.from(vert.subarray(firstOffset,firstOffset+9))
|
||||
let zs = [z1,z2,z3]
|
||||
let zmin = Math.min(...zs);
|
||||
let zmax = Math.max(...zs);
|
||||
|
||||
|
||||
// console.log({firstOffset,zmin, zs,faces,vert})
|
||||
|
||||
let cylVerts = new Float32Array(faces.length*9)
|
||||
|
||||
let i = 0
|
||||
//for each cylinder face index
|
||||
for(let index of faces){
|
||||
let off = index*9
|
||||
//copy the 9 triangle vertices floats in
|
||||
for(let j = 0;j < 9;j++){
|
||||
cylVerts[i] = vert[off+j]
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
let poly = slicer.sliceZ(zmin, cylVerts,opts).polys[0]
|
||||
let circular = poly.circularity() > 0.98;
|
||||
|
||||
//throw error if not circular
|
||||
if(!circular) throw "faces must be circular"
|
||||
|
||||
//calculate circle properties
|
||||
let area = poly.area(),
|
||||
diam = Math.sqrt(area / Math.PI)* 2,
|
||||
center = poly.calcCircleCenter();
|
||||
|
||||
|
||||
//find direction using normal to center point
|
||||
|
||||
let point = newPoint(x1, y1, z1),
|
||||
delta = point.setZ(center.z).sub(center),
|
||||
normal = new THREE.Vector3(x1-x2, y1-y2, z1-z2)
|
||||
.cross(new THREE.Vector3(x2-x3, y2-y3, z2-z3)),
|
||||
dotProd = normal.dot(delta),
|
||||
interior = dotProd < 0
|
||||
|
||||
|
||||
|
||||
return {faces, zmin, zmax, poly, circular, area, diam, center,interior}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a list of holes in the model based on the given diameter.
|
||||
*
|
||||
* @param {Object} settings - settings object
|
||||
* @param {Object} widget - widget object
|
||||
* @param {boolean} individual - if true, drill holes individually
|
||||
* @param {Object} rec - DrillOp record
|
||||
* @param {Function} onProgress - callback function to report progress
|
||||
* @returns {Array} list of hole centers as objects with `x`, `y`, `z`, `depth`, and `selected` properties.
|
||||
*/
|
||||
export async function holes(settings, widget, individual, rec, onProgress) {
|
||||
|
||||
let { tool, 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
479
src/kiri/mode/cam/slicer.js
Normal 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;
|
||||
319
src/kiri/mode/cam/slicer_topo.js
Normal file
319
src/kiri/mode/cam/slicer_topo.js
Normal 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
208
src/kiri/mode/cam/tool.js
Normal 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
344
src/kiri/mode/cam/tools.js
Normal 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
815
src/kiri/mode/cam/topo3.js
Normal 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
467
src/kiri/mode/cam/topo4.js
Normal 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);
|
||||
};
|
||||
Loading…
Reference in a new issue