package re-organization
This commit is contained in:
parent
7176ab0822
commit
678316ac95
98 changed files with 63 additions and 62 deletions
533
src/kiri-mode/cam/animate.js
Normal file
533
src/kiri-mode/cam/animate.js
Normal file
|
|
@ -0,0 +1,533 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
self.kiri.loader.push(function() {
|
||||
|
||||
let KIRI = self.kiri,
|
||||
CAM = KIRI.driver.CAM,
|
||||
API, WORLD, SPACE,
|
||||
meshes = {},
|
||||
unitScale = 1,
|
||||
progress, toolPosX, toolPosY, toolPosZ,
|
||||
speedValues = [ 25, 12, 6, 3 ],
|
||||
speedNames = [ "1x", "2x", "4x", "8x" ],
|
||||
speedMax = speedValues.length - 1,
|
||||
speedIndex = 0,
|
||||
speedLabel,
|
||||
speed,
|
||||
pauseButton,
|
||||
playButton,
|
||||
posOffset = { x:0, y:0, z:0 };
|
||||
|
||||
// ---( CLIENT FUNCTIONS )---
|
||||
|
||||
if (KIRI.client)
|
||||
CAM.animate_clear = function(api) {
|
||||
KIRI.client.animate_cleanup();
|
||||
SPACE = (API = api).const.SPACE;
|
||||
WORLD = SPACE.platform.world;
|
||||
$('layer-animate').innerHTML = '';
|
||||
$('layer-toolpos').innerHTML = '';
|
||||
Object.keys(meshes).forEach(id => deleteMesh(id));
|
||||
}
|
||||
|
||||
if (KIRI.client)
|
||||
CAM.animate = function(api, delay) {
|
||||
SPACE = (API = api).const.SPACE;
|
||||
WORLD = SPACE.platform.world;
|
||||
KIRI.client.animate_setup(API.conf.get(), data => {
|
||||
checkMeshCommands(data);
|
||||
if (!(data && data.mesh_add)) {
|
||||
return;
|
||||
}
|
||||
const UC = API.uc;
|
||||
const layer = $('layer-animate');
|
||||
layer.innerHTML = '';
|
||||
UC.setGroup(layer);
|
||||
UC.newRow([
|
||||
UC.newButton(null,replay,{icon:'<i class="fas fa-fast-backward"></i>',title:"restart"}),
|
||||
playButton = UC.newButton(null,play,{icon:'<i class="fas fa-play"></i>',title:"play"}),
|
||||
pauseButton = UC.newButton(null,pause,{icon:'<i class="fas fa-pause"></i>',title:"pause"}),
|
||||
UC.newButton(null,step,{icon:'<i class="fas fa-step-forward"></i>',title:"single step"}),
|
||||
UC.newButton(null,fast,{icon:'<i class="fas fa-forward"></i>',title:"toggle speed"}),
|
||||
UC.newButton(null,skip,{icon:'<i class="fas fa-fast-forward"></i>',title:"skip forward without animation"}),
|
||||
speedLabel = UC.newLabel("speed")
|
||||
]);
|
||||
updateSpeed();
|
||||
setTimeout(() => {
|
||||
play({steps: 1});
|
||||
}, delay || 0);
|
||||
const toolpos = $('layer-toolpos');
|
||||
toolpos.innerHTML = '';
|
||||
UC.setGroup(toolpos);
|
||||
progress = UC.newInput('%', {disabled: true, size: 5});
|
||||
toolPosX = UC.newInput('x', {disabled: true, size: 7});
|
||||
toolPosY = UC.newInput('y', {disabled: true, size: 7});
|
||||
toolPosZ = UC.newInput('z', {disabled: true, size: 7});
|
||||
playButton.style.display = '';
|
||||
pauseButton.style.display = 'none';
|
||||
API.event.emit('animate', 'CAM');
|
||||
});
|
||||
};
|
||||
|
||||
function updateSpeed(inc = 0) {
|
||||
if (inc === Infinity) {
|
||||
speedIndex = speedMax;
|
||||
} else if (inc > 0) {
|
||||
speedIndex = (speedIndex + inc) % speedValues.length;
|
||||
}
|
||||
speed = speedValues[speedIndex];
|
||||
speedLabel.innerText = speedNames[speedIndex];
|
||||
}
|
||||
|
||||
function replay() {
|
||||
CAM.animate_clear(API);
|
||||
setTimeout(() => {
|
||||
CAM.animate(API, 50);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function checkMeshCommands(data) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.mesh_add) {
|
||||
const { id, ind, pos, offset } = data.mesh_add;
|
||||
meshAdd(id, ind, pos);
|
||||
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();
|
||||
if (id !== 0) {
|
||||
toolPosX.value = ((pos.x + posOffset.x) * unitScale).toFixed(2);
|
||||
toolPosY.value = ((pos.y + posOffset.y) * unitScale).toFixed(2);
|
||||
toolPosZ.value = ((pos.z + posOffset.z) * unitScale).toFixed(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.mesh_update) {
|
||||
meshUpdates(data.id, data.mesh_update);
|
||||
}
|
||||
}
|
||||
|
||||
function meshAdd(id, ind, pos) {
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
||||
geo.setIndex(new THREE.BufferAttribute(new Uint32Array(ind), 1));
|
||||
const mat = new THREE.LineBasicMaterial({
|
||||
transparent: true,
|
||||
opacity: 0.75,
|
||||
color: 0
|
||||
});
|
||||
const mesh = new THREE.LineSegments(geo, mat);
|
||||
WORLD.add(mesh);
|
||||
meshes[id] = mesh;
|
||||
}
|
||||
|
||||
function meshUpdates(id, updates) {
|
||||
const mesh = meshes[id];
|
||||
if (!mesh) {
|
||||
return; // animate cancelled
|
||||
}
|
||||
const mpos = mesh.geometry.attributes.position;
|
||||
for (let i=0, il=updates.length; i<il; ) {
|
||||
const pos = updates[i++];
|
||||
const val = updates[i++];
|
||||
mpos.array[pos] = val;
|
||||
}
|
||||
mpos.needsUpdate = true;
|
||||
SPACE.update();
|
||||
}
|
||||
|
||||
function deleteMesh(id) {
|
||||
WORLD.remove(meshes[id]);
|
||||
delete meshes[id];
|
||||
}
|
||||
|
||||
function step(opts) {
|
||||
const { steps } = opts;
|
||||
updateSpeed();
|
||||
KIRI.client.animate({speed, steps: 1}, handleGridUpdate);
|
||||
}
|
||||
|
||||
function play(opts) {
|
||||
const { steps } = opts;
|
||||
updateSpeed();
|
||||
if (steps !== 1) {
|
||||
playButton.style.display = 'none';
|
||||
pauseButton.style.display = '';
|
||||
}
|
||||
KIRI.client.animate({speed, steps: steps || Infinity}, handleGridUpdate);
|
||||
}
|
||||
|
||||
function fast(opts) {
|
||||
const { steps } = opts;
|
||||
updateSpeed(1);
|
||||
playButton.style.display = 'none';
|
||||
pauseButton.style.display = '';
|
||||
KIRI.client.animate({speed, steps: steps || Infinity}, handleGridUpdate);
|
||||
}
|
||||
|
||||
function pause() {
|
||||
playButton.style.display = '';
|
||||
pauseButton.style.display = 'none';
|
||||
KIRI.client.animate({speed: 0}, handleGridUpdate);
|
||||
}
|
||||
|
||||
function skip() {
|
||||
API.show.alert('fast fowarding without animation');
|
||||
playButton.style.display = 'none';
|
||||
pauseButton.style.display = '';
|
||||
updateSpeed(Infinity);
|
||||
KIRI.client.animate({speed, steps: Infinity, toend: true}, handleGridUpdate);
|
||||
}
|
||||
|
||||
function handleGridUpdate(data) {
|
||||
checkMeshCommands(data);
|
||||
if (data && data.progress) {
|
||||
progress.value = (data.progress * 100).toFixed(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (KIRI.client)
|
||||
KIRI.client.animate_setup = function(settings, ondone) {
|
||||
unitScale = settings.controller.units === 'in' ? 1/25.4 : 1;
|
||||
KIRI.client.send("animate_setup", {settings}, ondone);
|
||||
};
|
||||
|
||||
if (KIRI.client)
|
||||
KIRI.client.animate = function(data, ondone) {
|
||||
KIRI.client.send("animate", data, ondone);
|
||||
};
|
||||
|
||||
if (KIRI.client)
|
||||
KIRI.client.animate_cleanup = function(data, ondone) {
|
||||
KIRI.client.send("animate_cleanup", data, ondone);
|
||||
};
|
||||
|
||||
// ---( WORKER FUNCTIONS )---
|
||||
|
||||
let stock, center, grid, gridX, gridY, rez;
|
||||
let path, pathIndex, tool, tools, last, toolID = 1;
|
||||
|
||||
if (KIRI.worker)
|
||||
KIRI.worker.animate_setup = function(data, send) {
|
||||
const { settings } = data;
|
||||
const { process } = settings;
|
||||
const print = 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));
|
||||
|
||||
const step = rez;
|
||||
const stepsX = Math.floor(stock.x / step);
|
||||
const stepsY = Math.floor(stock.y / step);
|
||||
const { pos, ind } = createGrid(stepsX, stepsY, stock, step);
|
||||
const offset = {
|
||||
x: process.outputOriginCenter ? 0 : stock.x / 2,
|
||||
y: process.outputOriginCenter ? 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, pos, ind, offset } });
|
||||
send.data({ mesh_move: { id: 0, pos: center } });
|
||||
send.done();
|
||||
};
|
||||
|
||||
function createGrid(stepsX, stepsY, size, step) {
|
||||
const gridPoints = stepsX * stepsY;
|
||||
const pos = new Float32Array(gridPoints * 3);
|
||||
const ind = [];
|
||||
const ox = size.x / 2;
|
||||
const oy = size.y / 2;
|
||||
|
||||
const b = { mx: 0, my: 0, Mx: 0, My: 0};
|
||||
|
||||
// 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++] = size.z;
|
||||
if (y > 0) ind.appendAll([
|
||||
(stepsY * x) + (y - 1),
|
||||
(stepsY * x) + (y )
|
||||
]);
|
||||
if (x > 0) ind.appendAll([
|
||||
(stepsY * (x - 1)) + y,
|
||||
(stepsY * (x )) + y
|
||||
]);
|
||||
b.mx = Math.min(b.mx, px);
|
||||
b.my = Math.min(b.my, py);
|
||||
b.Mx = Math.max(b.Mx, px);
|
||||
b.My = Math.max(b.My, py);
|
||||
}
|
||||
}
|
||||
|
||||
return { pos, ind };
|
||||
}
|
||||
|
||||
if (KIRI.worker)
|
||||
KIRI.worker.animate = function(data, send) {
|
||||
if (data.speed >= 0) {
|
||||
renderSpeed = data.speed;
|
||||
}
|
||||
if (data.steps > 0) {
|
||||
stepsRemain = data.steps;
|
||||
}
|
||||
if (data.toend) {
|
||||
skipMode = !skipMode;
|
||||
} else {
|
||||
skipMode = false;
|
||||
}
|
||||
checkStash(send);
|
||||
if (animating) {
|
||||
return send.done();
|
||||
}
|
||||
renderPath(send);
|
||||
};
|
||||
|
||||
if (KIRI.worker)
|
||||
KIRI.worker.animate_cleanup = function(data, send) {
|
||||
if (animating) {
|
||||
animateClear = true;
|
||||
}
|
||||
};
|
||||
|
||||
let animateClear = false;
|
||||
let animating = false;
|
||||
let renderSpeed = 25;
|
||||
let stepsRemain = 0;
|
||||
let skipMode = false;
|
||||
let skipMove = null;
|
||||
let skipStash = [];
|
||||
|
||||
function checkStash(send) {
|
||||
if (!skipMode && skipStash.length) {
|
||||
// merge & compress stash stack
|
||||
skipStash.forEach(mesh_update => {
|
||||
send.data({ id: 0, mesh_update }, [ mesh_update.buffer ]);
|
||||
});
|
||||
skipStash = [];
|
||||
send.data({ mesh_move: skipMove });
|
||||
}
|
||||
}
|
||||
|
||||
function renderPath(send) {
|
||||
if (animateClear) {
|
||||
animateClear = false;
|
||||
animating = false;
|
||||
send.done();
|
||||
return;
|
||||
}
|
||||
if (stepsRemain <= 0 || renderSpeed === 0) {
|
||||
skipMode = false;
|
||||
checkStash(send);
|
||||
animating = false;
|
||||
send.done();
|
||||
return;
|
||||
}
|
||||
const next = path[pathIndex++];
|
||||
if (!next) {
|
||||
skipMode = false;
|
||||
checkStash(send);
|
||||
animating = false;
|
||||
stepsRemain = 0;
|
||||
send.done();
|
||||
return;
|
||||
}
|
||||
animating = true;
|
||||
stepsRemain--;
|
||||
if (next.tool >= 0 && (!tool || tool.getNumber() !== next.tool)) {
|
||||
// on real tool change, go to safe Z first
|
||||
if (tool) {
|
||||
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;
|
||||
if (last) {
|
||||
const lp = last.point, np = next.point;
|
||||
// dwell ops have no point
|
||||
if (!np) {
|
||||
setTimeout(() => { renderPath(send) }, 0);
|
||||
return;
|
||||
}
|
||||
const dx = np.x - lp.x, dy = np.y - lp.y, dz = np.z - lp.z;
|
||||
// skip moves that are closer than resolution
|
||||
if (Math.sqrt(dx*dx +dy*dy + dz*dz) < rez) {
|
||||
setTimeout(() => { renderPath(send) }, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const md = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz));
|
||||
const st = Math.ceil(md / rez);
|
||||
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);
|
||||
send.data({ progress: pathIndex / path.length });
|
||||
} else {
|
||||
tool.pos = next.point;
|
||||
send.data({ mesh_move: { id, pos: next.point }});
|
||||
setTimeout(() => { renderPath(send) }, 0);
|
||||
}
|
||||
last = next;
|
||||
}
|
||||
|
||||
function renderMoves(id, moves, send) {
|
||||
let index = 0;
|
||||
function update() {
|
||||
if (animateClear) {
|
||||
return renderPath(send);
|
||||
}
|
||||
if (renderSpeed > 0 && index < moves.length) {
|
||||
const pos = moves[index++];
|
||||
if (!pos) throw `no pos @ ${index} of ${moves.length}`;
|
||||
tool.pos = pos;
|
||||
updateMesh(pos, send);
|
||||
if (skipMode) {
|
||||
skipMove = { id, pos };
|
||||
} else {
|
||||
send.data({ mesh_move: { id, pos } });
|
||||
}
|
||||
}
|
||||
let pauseTime = skipMode ? 0 : renderSpeed;
|
||||
if (index < moves.length) {
|
||||
setTimeout(update, pauseTime);
|
||||
} else {
|
||||
setTimeout(() => { renderPath(send) }, pauseTime);
|
||||
}
|
||||
}
|
||||
update(0);
|
||||
}
|
||||
|
||||
function updateMesh(pos, send) {
|
||||
const prof = tool.profile;
|
||||
const { size, pix } = tool.profileDim;
|
||||
const mid = Math.floor(pix / 2);
|
||||
const update = new Float32Array(Math.round(prof.length * 0.8));
|
||||
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) {
|
||||
update[upos++] = iz;
|
||||
update[upos++] = tz;
|
||||
grid[iz] = tz;
|
||||
}
|
||||
}
|
||||
if (upos > 0) {
|
||||
const mesh_update = update.slice(0,upos);
|
||||
if (skipMode) {
|
||||
skipStash.push(mesh_update);
|
||||
} else {
|
||||
send.data({ id: 0, mesh_update }, [ mesh_update.buffer ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateTool(toolnum, send) {
|
||||
if (tool) {
|
||||
send.data({ mesh_del: toolID });
|
||||
}
|
||||
tool = new CAM.Tool({ tools }, undefined, toolnum);
|
||||
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 } = 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, pos, ind }});
|
||||
}
|
||||
|
||||
// load renderer code in worker context only
|
||||
if (KIRI.worker && false)
|
||||
fetch('/wasm/kiri-ani.wasm')
|
||||
.then(response => response.arrayBuffer())
|
||||
.then(bytes => WebAssembly.instantiate(bytes, {
|
||||
env: {
|
||||
reportf: (a,b) => { console.log('[f]',a,b) },
|
||||
reporti: (a,b) => { console.log('[i]',a,b) }
|
||||
}
|
||||
}))
|
||||
.then(results => {
|
||||
let {module, instance} = results;
|
||||
let {exports} = instance;
|
||||
let heap = new Uint8Array(exports.memory.buffer);
|
||||
let wasm = self.wasm = {
|
||||
heap,
|
||||
memory: exports.memory,
|
||||
updateMesh: exports.updateMesh
|
||||
};
|
||||
// heap[0] = 5;
|
||||
// heap[100] = 6;
|
||||
// heap[200] = 8;
|
||||
// let rv = self.wasm.updateMesh(0, 0, 100, 200);
|
||||
});
|
||||
|
||||
});
|
||||
1276
src/kiri-mode/cam/client.js
Normal file
1276
src/kiri-mode/cam/client.js
Normal file
File diff suppressed because it is too large
Load diff
55
src/kiri-mode/cam/driver.js
Normal file
55
src/kiri-mode/cam/driver.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
const KIRI = self.kiri,
|
||||
CAM = KIRI.driver.CAM = {
|
||||
// init, // src/mode/cam/client.js
|
||||
// slice, // src/mode/cam/slice.js
|
||||
// prepare, // src/mode/cam/prepare.js
|
||||
// export // src/mode/cam/export.js
|
||||
},
|
||||
CPRO = CAM.process = {
|
||||
LEVEL: 1,
|
||||
ROUGH: 2,
|
||||
OUTLINE: 3,
|
||||
CONTOUR_X: 4,
|
||||
CONTOUR_Y: 5,
|
||||
TRACE: 6,
|
||||
DRILL: 7
|
||||
};
|
||||
|
||||
// defer loading until KIRI.client and KIRI.worker exist
|
||||
KIRI.loader.push(function(API) {
|
||||
|
||||
if (KIRI.client)
|
||||
CAM.traces = function(ondone, single) {
|
||||
KIRI.client.sync();
|
||||
let settings = API.conf.get();
|
||||
let widgets = API.widgets.map();
|
||||
KIRI.client.send("cam_traces", { settings, single }, output => {
|
||||
let ids = [];
|
||||
KIRI.codec.decode(output).forEach(rec => {
|
||||
ids.push(rec.id);
|
||||
widgets[rec.id].traces = rec.traces;
|
||||
});
|
||||
ondone(ids);
|
||||
});
|
||||
};
|
||||
|
||||
if (KIRI.worker)
|
||||
KIRI.worker.cam_traces = function(data, send) {
|
||||
const { settings, single } = data;
|
||||
const widgets = Object.values(wcache);
|
||||
const fresh = widgets.filter(widget => CAM.traces(settings, widget, single));
|
||||
send.done(KIRI.codec.encode(fresh.map(widget => { return {
|
||||
id: widget.id,
|
||||
traces: widget.traces,
|
||||
} } )));
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
})();
|
||||
327
src/kiri-mode/cam/export.js
Normal file
327
src/kiri-mode/cam/export.js
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
let KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
POLY = BASE.polygons,
|
||||
UTIL = BASE.util,
|
||||
CAM = KIRI.driver.CAM,
|
||||
PRO = CAM.process;
|
||||
|
||||
/**
|
||||
* @returns {Array} gcode lines
|
||||
*/
|
||||
CAM.export = function(print, online) {
|
||||
let widget = print.widgets[0];
|
||||
|
||||
if (!widget) return;
|
||||
|
||||
let i,
|
||||
time = 0,
|
||||
lines = 0,
|
||||
bytes = 0,
|
||||
factor = 1,
|
||||
output = [],
|
||||
spindle = 0,
|
||||
settings = print.settings,
|
||||
device = settings.device,
|
||||
gcodes = settings.device || {},
|
||||
origin = settings.origin || { x: 0, y: 0, z: 0 },
|
||||
tools = settings.tools,
|
||||
space = gcodes.gcodeSpace ? ' ' : '',
|
||||
isRML = device.gcodeFExt.toLowerCase() === 'rml',
|
||||
stripComments = gcodes.gcodeStrip || false,
|
||||
cmdToolChange = gcodes.gcodeChange || [ "M6 T{tool}" ],
|
||||
cmdSpindle = gcodes.gcodeSpindle || [ "M3 S{speed}" ],
|
||||
cmdDwell = gcodes.gcodeDwell || [ "G4 P{time}" ],
|
||||
dev = settings.device,
|
||||
spro = settings.process,
|
||||
maxZd = spro.camFastFeedZ,
|
||||
maxXYd = spro.camFastFeed,
|
||||
decimals = BASE.config.gcode_decimals || 4,
|
||||
pos = { x:null, y:null, z:null, f:null, t:null },
|
||||
line,
|
||||
cidx,
|
||||
mode,
|
||||
point,
|
||||
points = 0,
|
||||
stock = settings.stock,
|
||||
hasStock = spro.camStockOn && stock.x && stock.y && stock.z,
|
||||
ztOff = hasStock ? stock.z - widget.track.top : 0,
|
||||
bounds = widget.getBoundingBox(),
|
||||
zmax = hasStock ? stock.z : bounds.max.z,
|
||||
runbox = {
|
||||
max: { x:-Infinity, y:-Infinity, z:-Infinity},
|
||||
min: { x:Infinity, y:Infinity, z:Infinity}
|
||||
},
|
||||
offset = {
|
||||
x: -origin.x,
|
||||
y: origin.y
|
||||
},
|
||||
consts = {
|
||||
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
|
||||
},
|
||||
append;
|
||||
|
||||
append = function(line) {
|
||||
if (line) {
|
||||
lines++;
|
||||
bytes += line.length;
|
||||
output.append(line);
|
||||
}
|
||||
if (!line || output.length > 1000) {
|
||||
online(output.join("\r\n"));
|
||||
output = [];
|
||||
}
|
||||
};
|
||||
|
||||
function filterEmit(array, consts) {
|
||||
if (!array) return;
|
||||
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) {
|
||||
return opt ? s : s + '.0';
|
||||
} else {
|
||||
return val.toFixed(decimals);
|
||||
}
|
||||
}
|
||||
|
||||
function toolByNumber(number) {
|
||||
for (let i=0; i<tools.length; i++) {
|
||||
if (tools[i].number === number) return tools[i];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function toolNameByNumber(number) {
|
||||
for (let i=0; i<tools.length; i++) {
|
||||
if (tools[i].number === number) return tools[i].name;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function moveTo(out) {
|
||||
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
|
||||
if (out.tool != pos.t) {
|
||||
pos.t = out.tool;
|
||||
consts.tool = pos.t;
|
||||
consts.tool_name = toolNameByNumber(out.tool);
|
||||
filterEmit(cmdToolChange, consts);
|
||||
}
|
||||
|
||||
// first point out sets the current position (but not Z)
|
||||
// hacky AF way to split initial x,y,z into z then x,y
|
||||
if (points === 0) {
|
||||
pos.x = pos.y = pos.z = 0;
|
||||
points++;
|
||||
moveTo({
|
||||
tool: out.tool,
|
||||
point: { x: 0, y: 0, z: newpos.z }
|
||||
});
|
||||
moveTo({
|
||||
tool: out.tool,
|
||||
point: { x: newpos.x, y: newpos.y, z: newpos.z }
|
||||
});
|
||||
points--;
|
||||
return;
|
||||
}
|
||||
|
||||
let speed = out.speed,
|
||||
nl = [speed ? 'G1' : 'G0'],
|
||||
dx = newpos.x - pos.x,
|
||||
dy = newpos.y - pos.y,
|
||||
dz = newpos.z - pos.z,
|
||||
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 (!(dx || dy || dz)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (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("X").append(add0(pos.x * factor));
|
||||
}
|
||||
if (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("Y").append(add0(pos.y * factor));
|
||||
}
|
||||
if (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("Z").append(add0(pos.z * factor));
|
||||
}
|
||||
if (newFeed) {
|
||||
pos.f = feed;
|
||||
nl.append(space).append("F").append(add0(feed * factor, true));
|
||||
}
|
||||
|
||||
// temp hack to support RML1 dialect from a file extensions trigger
|
||||
if (isRML) {
|
||||
if (speed) {
|
||||
if (newFeed) {
|
||||
append(`VS${feed};`);
|
||||
}
|
||||
nl = [ "Z", add0(pos.x * factor), ",", add0(pos.y * factor), ",", add0(pos.z * factor), ";" ];
|
||||
} else {
|
||||
nl = [ "PU", add0(pos.x * factor), ",", add0(pos.y * factor), ";" ];
|
||||
}
|
||||
}
|
||||
|
||||
// update time calculation
|
||||
time += (dist / (pos.f || 1000)) * 60;
|
||||
|
||||
// if (comment && !stripComments) {
|
||||
// nl.append(" ; ").append(comment);
|
||||
// nl.append(" ; ").append(points);
|
||||
// }
|
||||
append(nl.join(''));
|
||||
points++;
|
||||
}
|
||||
|
||||
if (!stripComments) {
|
||||
append(`; Generated by Kiri:Moto ${KIRI.version}`);
|
||||
append(`; ${new Date().toString()}`);
|
||||
filterEmit(["; Bed left:{left} right:{right} top:{top} bottom:{bottom}"], consts);
|
||||
if (hasStock) {
|
||||
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;
|
||||
|
||||
// remap points as necessary for origins, offsets, inversions
|
||||
print.output.forEach(function(layer) {
|
||||
layer.forEach(function(out) {
|
||||
if (out.tool && out.tool !== ctool) {
|
||||
ctool = toolByNumber(out.tool);
|
||||
toolz[out.tool] = ctool;
|
||||
}
|
||||
point = out.point;
|
||||
if (!point || point.mod) return;
|
||||
// ensure not point is modified twice
|
||||
point.mod = 1;
|
||||
if (offset) {
|
||||
point.x += offset.x;
|
||||
point.y += offset.y;
|
||||
}
|
||||
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 tool = toolz[tn];
|
||||
append(`; tool=${tn} flute=${tool.flute_diam} len=${tool.flute_len} metric=${tool.metric}`);
|
||||
});
|
||||
}
|
||||
|
||||
// emit gcode preamble
|
||||
filterEmit(gcodes.gcodePre, consts);
|
||||
|
||||
// emit all points in layer/point order
|
||||
print.output.forEach(function (layerout) {
|
||||
if (mode !== layerout.mode) {
|
||||
if (mode && !stripComments) append("; ending " + mode + " op after " + Math.round(time/60) + " seconds");
|
||||
mode = layerout.mode;
|
||||
if (!stripComments) append("; starting " + mode + " op");
|
||||
}
|
||||
if (layerout.spindle && layerout.spindle !== spindle) {
|
||||
spindle = layerout.spindle;
|
||||
if (spindle > 0) {
|
||||
let speed = Math.abs(spindle);
|
||||
filterEmit(cmdSpindle, {
|
||||
speed, spindle: speed, rpm: speed
|
||||
});
|
||||
} else {
|
||||
append("M4");
|
||||
}
|
||||
// append((spindle > 0 ? "M3" : "M4") + " S" + Math.abs(spindle));
|
||||
}
|
||||
layerout.forEach(function(out) {
|
||||
moveTo(out);
|
||||
});
|
||||
});
|
||||
if (mode && !stripComments) append("; ending " + mode + " op after " + Math.round(time/60) + " seconds");
|
||||
|
||||
// emit gcode post
|
||||
filterEmit(gcodes.gcodePost, consts);
|
||||
|
||||
// flush buffered gcode
|
||||
append();
|
||||
|
||||
print.time = time;
|
||||
print.lines = lines;
|
||||
print.bytes = bytes + lines - 1;
|
||||
print.bounds = runbox;
|
||||
};
|
||||
|
||||
})();
|
||||
1332
src/kiri-mode/cam/ops.js
Normal file
1332
src/kiri-mode/cam/ops.js
Normal file
File diff suppressed because it is too large
Load diff
545
src/kiri-mode/cam/prepare.js
Normal file
545
src/kiri-mode/cam/prepare.js
Normal file
|
|
@ -0,0 +1,545 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
let KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
POLY = BASE.polygons,
|
||||
UTIL = BASE.util,
|
||||
CAM = KIRI.driver.CAM,
|
||||
PRO = CAM.process,
|
||||
newPoint = BASE.newPoint;
|
||||
|
||||
/**
|
||||
* DRIVER PRINT CONTRACT
|
||||
*
|
||||
* @param {Object} print state object
|
||||
* @param {Function} update incremental callback
|
||||
* @param {Number} [index] into widget array
|
||||
* @param {Object} [firstPoint] starting point
|
||||
*/
|
||||
CAM.prepare = function(widgets, settings, update) {
|
||||
const count = widgets.length;
|
||||
const weight = 1/count;
|
||||
const print = self.worker.print = KIRI.newPrint(settings, widgets);
|
||||
print.output = [];
|
||||
|
||||
let point;
|
||||
widgets.forEach((widget, index) => {
|
||||
point = prepEach(widget, settings, print, point, (progress, msg) => {
|
||||
update((index * weight + progress * weight) * 0.75, msg || "prepare");
|
||||
});
|
||||
});
|
||||
|
||||
print.render = KIRI.driver.FDM.prepareRender(print.output, (progress, layer) => {
|
||||
update(0.75 + progress * 0.25, "render", layer);
|
||||
}, {
|
||||
thin: true,
|
||||
print: 0,
|
||||
move: 0x557799,
|
||||
speed: false,
|
||||
moves: true,
|
||||
other: "moving",
|
||||
action: "milling"
|
||||
});
|
||||
|
||||
return print.render;
|
||||
};
|
||||
|
||||
function prepEach(widget, settings, print, firstPoint, update) {
|
||||
|
||||
if (widget.camops.length === 0) return;
|
||||
|
||||
let device = settings.device,
|
||||
process = settings.process,
|
||||
stock = settings.stock || {},
|
||||
outer = settings.bounds || widget.getPositionBox(),
|
||||
outerz = outer.max.z,
|
||||
slices = widget.slices,
|
||||
hasStock = stock.x && stock.y && stock.z && process.camStockOn,
|
||||
startCenter = process.outputOriginCenter,
|
||||
alignTop = settings.controller.alignTop,
|
||||
zclear = (process.camZClearance || 1),
|
||||
zmax_outer = hasStock ? stock.z + zclear : outerz + zclear,
|
||||
wztop = widget.track.top,
|
||||
ztOff = hasStock ? stock.z - wztop : 0,
|
||||
bounds = widget.getBoundingBox(),
|
||||
boundsz = bounds.max.z + ztOff,
|
||||
zadd = hasStock ? stock.z - boundsz : alignTop ? outerz - boundsz : 0,
|
||||
zmax = outerz + zclear,
|
||||
wmpos = widget.track.pos,
|
||||
wmx = wmpos.x,
|
||||
wmy = wmpos.y,
|
||||
originx = startCenter ? 0 : hasStock ? -stock.x / 2 : bounds.min.x,
|
||||
originy = startCenter ? 0 : hasStock ? -stock.y / 2 : bounds.min.y,
|
||||
origin = newPoint(originx + wmx, originy + wmy, zmax),
|
||||
output = print.output,
|
||||
easeDown = process.camEaseDown,
|
||||
depthFirst = process.camDepthFirst,
|
||||
tolerance = 0,
|
||||
drillDown = 0,
|
||||
drillLift = 0,
|
||||
drillDwell = 0,
|
||||
newOutput = print.output || [],
|
||||
layerOut = [],
|
||||
printPoint,
|
||||
isNewMode,
|
||||
tool,
|
||||
toolType,
|
||||
toolDiam,
|
||||
toolDiamMove,
|
||||
plungeRate = process.camFastFeedZ,
|
||||
feedRate,
|
||||
lastTool,
|
||||
lastMode,
|
||||
lastPoint,
|
||||
nextIsMove = true,
|
||||
synthPlunge = false,
|
||||
spindle = 0,
|
||||
spindleMax = device.spindleMax,
|
||||
addOutput = print.addOutput,
|
||||
tip2tipEmit = print.tip2tipEmit,
|
||||
poly2polyEmit = print.poly2polyEmit,
|
||||
maxToolDiam = widget.maxToolDiam,
|
||||
terrain = widget.terrain ? widget.terrain.map(data => {
|
||||
return {
|
||||
z: data.z,
|
||||
tops: data.tops,
|
||||
};
|
||||
}) : zmax;
|
||||
|
||||
function newLayer() {
|
||||
if (layerOut.length < 2) {
|
||||
return;
|
||||
}
|
||||
newOutput.push(layerOut);
|
||||
layerOut = [];
|
||||
layerOut.mode = lastMode;
|
||||
layerOut.spindle = spindle;
|
||||
}
|
||||
|
||||
// non-zero means contouring
|
||||
function setTolerance(dist) {
|
||||
tolerance = dist;
|
||||
}
|
||||
|
||||
function setPrintPoint(point) {
|
||||
printPoint = point;
|
||||
}
|
||||
|
||||
function setSpindle(speed) {
|
||||
spindle = Math.min(speed, spindleMax);
|
||||
}
|
||||
|
||||
function setTool(toolID, feed, plunge) {
|
||||
if (toolID !== lastTool) {
|
||||
tool = new CAM.Tool(settings, toolID);
|
||||
toolType = tool.getType();
|
||||
toolDiam = tool.fluteDiameter();
|
||||
toolDiamMove = toolType === 'endmill' ? toolDiam : tolerance * 2;
|
||||
lastTool = toolID;
|
||||
}
|
||||
feedRate = feed || feedRate;
|
||||
plungeRate = plunge || plungeRate || feedRate;
|
||||
}
|
||||
|
||||
function setDrill(down, lift, dwell) {
|
||||
drillDown = down;
|
||||
drillLift = lift;
|
||||
drillDwell = dwell;
|
||||
}
|
||||
|
||||
function emitDrills(polys) {
|
||||
polys = polys.slice();
|
||||
for (;;) {
|
||||
let closestDist = Infinity,
|
||||
closestI,
|
||||
closest = null,
|
||||
dist;
|
||||
|
||||
for (let i=0; i<polys.length; i++) {
|
||||
if (!polys[i]) continue;
|
||||
if ((dist = polys[i].first().distTo2D(printPoint)) < closestDist) {
|
||||
closestDist = dist;
|
||||
closest = polys[i];
|
||||
closestI = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (!closest) return;
|
||||
polys[closestI] = null;
|
||||
printPoint = closest.first();
|
||||
emitDrill(closest, drillDown, drillLift, drillDwell);
|
||||
}
|
||||
}
|
||||
|
||||
function emitDrill(poly, down, lift, dwell) {
|
||||
let remain = poly.first().z - poly.last().z,
|
||||
points = [],
|
||||
point = poly.first();
|
||||
if (down <= 0) {
|
||||
down = remain;
|
||||
}
|
||||
for (;;) {
|
||||
if (remain > down * 2) {
|
||||
points.push(point.clone());
|
||||
point.z -= down;
|
||||
remain -= down;
|
||||
} else if (remain < down) {
|
||||
points.push(point.clone());
|
||||
point.z -= remain;
|
||||
points.push(point.clone());
|
||||
break;
|
||||
} else {
|
||||
points.push(point.clone());
|
||||
point.z -= remain / 2;
|
||||
points.push(point.clone());
|
||||
point.z -= remain / 2;
|
||||
points.push(point.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
camOut(point.clone().setZ(zmax));
|
||||
points.forEach(function(point, index) {
|
||||
camOut(point, 1);
|
||||
if (index > 0 && index < points.length - 1) {
|
||||
if (dwell) camDwell(dwell);
|
||||
if (lift) camOut(point.clone().setZ(point.z + lift), 0);
|
||||
}
|
||||
})
|
||||
camOut(point.clone().setZ(zmax));
|
||||
newLayer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Point} point
|
||||
* @param {number} emit (0=move, !0=filament emit/laser on/cut mode)
|
||||
* @param {number} [speed] speed
|
||||
* @param {number} [tool] tool
|
||||
*/
|
||||
function layerPush(point, emit, speed, tool) {
|
||||
layerOut.mode = lastMode;
|
||||
addOutput(layerOut, point, emit, speed, tool);
|
||||
return point;
|
||||
}
|
||||
|
||||
function camDwell(time) {
|
||||
layerPush(
|
||||
null,
|
||||
0,
|
||||
time,
|
||||
tool.getNumber()
|
||||
);
|
||||
}
|
||||
|
||||
function camOut(point, cut, moveLen = toolDiamMove) {
|
||||
point = point.clone();
|
||||
point.x += wmx;
|
||||
point.y += wmy;
|
||||
point.z += zadd;
|
||||
|
||||
if (nextIsMove) {
|
||||
cut = 0;
|
||||
nextIsMove = false;
|
||||
}
|
||||
|
||||
let rate = feedRate;
|
||||
|
||||
// before first point, move cutting head to point above it
|
||||
// then set that new point as the lastPoint
|
||||
if (!lastPoint) {
|
||||
let above = point.clone().setZ(zmax + zadd + ztOff);
|
||||
lastPoint = layerPush(above, 0, 0, tool.getNumber());
|
||||
}
|
||||
|
||||
// measure deltas to last point in XY and Z
|
||||
let deltaXY = lastPoint.distTo2D(point),
|
||||
deltaZ = point.z - lastPoint.z,
|
||||
absDeltaZ = Math.abs(deltaZ),
|
||||
isMove = !cut;
|
||||
|
||||
// drop points too close together
|
||||
if (deltaXY < 0.001 && point.z === lastPoint.z) {
|
||||
// console.trace(["drop dup",lastPoint,point]);
|
||||
return;
|
||||
}
|
||||
|
||||
// convert short planar moves to cuts in some cases
|
||||
if (isMove && deltaXY <= moveLen) {
|
||||
let iscontour = tolerance > 0;
|
||||
let isflat = absDeltaZ < 0.001;
|
||||
// restrict this to contouring
|
||||
if (isflat || (iscontour && absDeltaZ <= tolerance)) {
|
||||
cut = 1;
|
||||
isMove = false;
|
||||
} else if (deltaZ <= -tolerance) {
|
||||
// move over before descending
|
||||
layerPush(point.clone().setZ(lastPoint.z), 0, 0, tool.getNumber());
|
||||
// new pos for plunge calc
|
||||
deltaXY = 0;
|
||||
}
|
||||
} else if (isMove) {
|
||||
// for longer moves, check the terrain to see if we need to go up and over
|
||||
const bigXY = (deltaXY > moveLen);
|
||||
const bigZ = (deltaZ > toolDiam/2 && deltaXY > tolerance);
|
||||
const midZ = (absDeltaZ >= tolerance);
|
||||
if ((bigXY || bigZ) && (isMove || midZ)) {
|
||||
let maxz = getZClearPath(
|
||||
terrain,
|
||||
lastPoint.x - wmx,
|
||||
lastPoint.y - wmy,
|
||||
point.x - wmx,
|
||||
point.y - wmy,
|
||||
Math.max(point.z, lastPoint.z),
|
||||
zadd,
|
||||
maxToolDiam/2,
|
||||
zclear
|
||||
) + ztOff,
|
||||
mustGoUp = Math.max(maxz - point.z, maxz - lastPoint.z) >= tolerance,
|
||||
clearz = maxz;
|
||||
// up if any point between higher than start/outline, go up first
|
||||
if (mustGoUp) {
|
||||
layerPush(lastPoint.clone().setZ(clearz), 0, 0, tool.getNumber());
|
||||
}
|
||||
// move to point above target point
|
||||
if (mustGoUp || point.z < maxz) {
|
||||
layerPush(point.clone().setZ(clearz), 0, 0, tool.getNumber());
|
||||
// new pos for plunge calc
|
||||
deltaXY = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set new plunge rate
|
||||
if (deltaZ < -tolerance) {
|
||||
let threshold = Math.min(deltaXY / 2, absDeltaZ),
|
||||
modifier = threshold / absDeltaZ;
|
||||
if (synthPlunge && threshold && modifier && deltaXY > tolerance) {
|
||||
// use modifier to speed up long XY move plunge rates
|
||||
rate = Math.round(plungeRate + ((feedRate - plungeRate) * modifier));
|
||||
} else {
|
||||
rate = plungeRate;
|
||||
}
|
||||
}
|
||||
|
||||
// todo synthesize move speed from feed / plunge accordingly
|
||||
layerOut.mode = lastMode;
|
||||
layerOut.spindle = spindle;
|
||||
lastPoint = layerPush(
|
||||
point,
|
||||
cut ? 1 : 0,
|
||||
rate,
|
||||
tool.getNumber()
|
||||
);
|
||||
}
|
||||
|
||||
// coming from a previous widget, use previous last point
|
||||
lastPoint = firstPoint;
|
||||
|
||||
// make top start offset configurable
|
||||
printPoint = firstPoint || origin;
|
||||
|
||||
// accumulated data for depth-first optimiztions
|
||||
let depthData = {
|
||||
rough: [],
|
||||
outline: [],
|
||||
roughDiam: 0,
|
||||
outlineDiam: 0,
|
||||
contourx: [],
|
||||
contoury: [],
|
||||
trace: [],
|
||||
drill: [],
|
||||
layer: 0,
|
||||
};
|
||||
|
||||
let ops = {
|
||||
setTool,
|
||||
setDrill,
|
||||
setSpindle,
|
||||
setTolerance,
|
||||
setPrintPoint,
|
||||
printPoint,
|
||||
newLayer,
|
||||
camOut,
|
||||
polyEmit,
|
||||
poly2polyEmit,
|
||||
tip2tipEmit,
|
||||
depthRoughPath,
|
||||
depthOutlinePath,
|
||||
emitDrills,
|
||||
emitTrace,
|
||||
bounds,
|
||||
zmax,
|
||||
lastPoint: () => { return lastPoint }
|
||||
};
|
||||
|
||||
let opSum = 0;
|
||||
let opTot = widget.camops.map(op => op.weight()).reduce((a,v) => a + v);
|
||||
|
||||
for (let op of widget.camops) {
|
||||
setTolerance(0);
|
||||
nextIsMove = true;
|
||||
lastMode = op.op.type;
|
||||
let weight = op.weight();
|
||||
op.prepare(ops, (progress, message) => {
|
||||
update((opSum + (progress * weight)) / opTot, message || op.type(), message);
|
||||
});
|
||||
opSum += weight;
|
||||
}
|
||||
|
||||
function emitTrace(slice) {
|
||||
let { tool, rate, plunge } = slice.camTrace;
|
||||
setTool(tool, rate, plunge);
|
||||
let traceTool = new CAM.Tool(settings, tool);
|
||||
let traceToolDiam = traceTool.fluteDiameter();
|
||||
printPoint = poly2polyEmit(slice.camLines, printPoint, polyEmit);
|
||||
newLayer();
|
||||
}
|
||||
|
||||
function polyEmit(poly, index, count, fromPoint) {
|
||||
let last = null;
|
||||
if (easeDown && poly.isClosed()) {
|
||||
last = poly.forEachPointEaseDown(function(point, offset) {
|
||||
camOut(point.clone(), offset > 0);
|
||||
}, fromPoint);
|
||||
} else {
|
||||
poly.forEachPoint(function(point, pidx, points, offset) {
|
||||
last = point;
|
||||
camOut(point.clone(), offset !== 0);
|
||||
}, poly.isClosed(), index);
|
||||
}
|
||||
newLayer();
|
||||
return last;
|
||||
}
|
||||
|
||||
function depthRoughPath(start, depth, levels, tops, emitter, fit, ease) {
|
||||
let level = levels[depth];
|
||||
if (!level) {
|
||||
return start;
|
||||
}
|
||||
let ltops = tops[depth];
|
||||
let fitted = fit ? ltops.filter(poly => poly.isInside(fit, 0.01)) : ltops;
|
||||
let ftops = fitted.filter(top => !top.level_emit);
|
||||
if (ftops.length > 1) {
|
||||
ftops = POLY.route(ftops, start);
|
||||
}
|
||||
ftops.forEach(top => {
|
||||
top.level_emit = true;
|
||||
let inside = level.filter(poly => poly.isInside(top));
|
||||
if (ease) {
|
||||
start.z += ease;
|
||||
}
|
||||
start = poly2polyEmit(inside, start, emitter, { mark: "emark", perm: true });
|
||||
if (ease) {
|
||||
start.z += ease;
|
||||
}
|
||||
start = depthRoughPath(start, depth + 1, levels, tops, emitter, top, ease);
|
||||
});
|
||||
return start;
|
||||
}
|
||||
|
||||
function depthOutlinePath(start, depth, levels, radius, emitter, dir, ease) {
|
||||
let bottm = depth < levels.length - 1 ? levels[levels.length - 1] : null;
|
||||
let above = levels[depth-1];
|
||||
let level = levels[depth];
|
||||
if (!level) {
|
||||
return start;
|
||||
}
|
||||
if (above) {
|
||||
level = level.filter(lp => {
|
||||
const conf = above.filter(ap => !ap.level_emit && lp.isNear(ap, radius, true));
|
||||
return conf.length === 0;
|
||||
});
|
||||
}
|
||||
// const thru = []; // match thru polys
|
||||
level = level.filter(lp => {
|
||||
if (lp.level_emit) {
|
||||
return false;
|
||||
}
|
||||
// if (bottm && !clr) {
|
||||
// const tm = bottm.filter(bp => lp.isEquivalent(bp));
|
||||
// thru.appendAll(tm);
|
||||
// return tm.length === 0;
|
||||
// }
|
||||
return true;
|
||||
});
|
||||
// limit level search to polys matching winding (inside vs outside)
|
||||
level = level.filter(p => p.isClockwise() === dir);
|
||||
// omit polys that match bottom level polys unless level above is cleared
|
||||
start = poly2polyEmit(level, start, (poly, index, count, fromPoint) => {
|
||||
poly.level_emit = true;
|
||||
if (ease) {
|
||||
fromPoint.z += ease;
|
||||
}
|
||||
fromPoint = polyEmit(poly, index, count, fromPoint);
|
||||
if (ease) {
|
||||
fromPoint.z += ease;
|
||||
}
|
||||
fromPoint = depthOutlinePath(fromPoint, depth + 1, levels, radius, emitter, dir, ease);
|
||||
fromPoint = depthOutlinePath(fromPoint, depth + 1, levels, radius, emitter, !dir, ease);
|
||||
return fromPoint;
|
||||
}, {weight: false});
|
||||
return start;
|
||||
}
|
||||
|
||||
// last layer/move is to zmax
|
||||
// injected into the last layer generated
|
||||
if (lastPoint)
|
||||
addOutput(newOutput[newOutput.length-1], printPoint = lastPoint.clone().setZ(zmax_outer), 0, 0, tool.getNumber());
|
||||
|
||||
// replace output single flattened layer with all points
|
||||
print.output = newOutput;
|
||||
return printPoint;
|
||||
};
|
||||
|
||||
/**
|
||||
* return tool Z clearance height for a line segment movement path
|
||||
*/
|
||||
function getZClearPath(terrain, x1, y1, x2, y2, z, zadd, off, over) {
|
||||
// when terrain skipped, top + pass used
|
||||
if (terrain > 0) {
|
||||
return terrain;
|
||||
}
|
||||
let maxz = z;
|
||||
let check = [];
|
||||
for (let i=0; i<terrain.length; i++) {
|
||||
let data = terrain[i];
|
||||
check.push(data);
|
||||
if (data.z + zadd < z) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
check.reverse();
|
||||
for (let i=0; i<check.length; i++) {
|
||||
let data = check[i];
|
||||
let p1 = newPoint(x1, y1);
|
||||
let p2 = newPoint(x2, y2);
|
||||
let int = data.tops.map(p => p.intersections(p1, p2, true)).flat();
|
||||
if (int.length) {
|
||||
maxz = Math.max(maxz, data.z + zadd + over);
|
||||
continue;
|
||||
}
|
||||
|
||||
let s1 = p1.slopeTo(p2).toUnit().normal();
|
||||
let s2 = p2.slopeTo(p1).toUnit().normal();
|
||||
let pa = p1.projectOnSlope(s1, off);
|
||||
let pb = p2.projectOnSlope(s1, off);
|
||||
int = data.tops.map(p => p.intersections(pa, pb, true)).flat();
|
||||
if (int.length) {
|
||||
maxz = Math.max(maxz, data.z + zadd + over);
|
||||
continue;
|
||||
}
|
||||
pa = p1.projectOnSlope(s2, off);
|
||||
pb = p2.projectOnSlope(s2, off);
|
||||
int = data.tops.map(p => p.intersections(pa, pb, true)).flat();
|
||||
if (int.length) {
|
||||
maxz = Math.max(maxz, data.z + zadd + over);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return maxz;
|
||||
}
|
||||
|
||||
})();
|
||||
351
src/kiri-mode/cam/slice.js
Normal file
351
src/kiri-mode/cam/slice.js
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
let KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
CAM = KIRI.driver.CAM,
|
||||
OPS = CAM.OPS,
|
||||
PRO = CAM.process,
|
||||
POLY = BASE.polygons,
|
||||
newSlice = KIRI.newSlice,
|
||||
newPoint = BASE.newPoint,
|
||||
newPolygon = BASE.newPolygon;
|
||||
|
||||
/**
|
||||
* DRIVER SLICE CONTRACT
|
||||
*
|
||||
* @param {Object} settings
|
||||
* @param {Widget} widget
|
||||
* @param {Function} output
|
||||
*/
|
||||
CAM.slice = function(settings, widget, onupdate, ondone) {
|
||||
let mesh = widget.mesh,
|
||||
proc = settings.process,
|
||||
stock = settings.stock || {},
|
||||
hasStock = stock.x && stock.y && stock.z && proc.camStockOn,
|
||||
camOps = widget.camops = [],
|
||||
sliceAll = widget.slices = [],
|
||||
bounds = widget.getBoundingBox(),
|
||||
zBottom = proc.camZBottom,
|
||||
zMin = Math.max(bounds.min.z, zBottom),
|
||||
zMax = bounds.max.z,
|
||||
zThru = zBottom === 0 ? (proc.camZThru || 0) : 0,
|
||||
wztop = widget.track.top,
|
||||
ztOff = hasStock ? stock.z - wztop : 0,
|
||||
zTop = zMax + ztOff,
|
||||
minToolDiam = Infinity,
|
||||
maxToolDiam = -Infinity,
|
||||
thruHoles,
|
||||
tabs = widget.anno.tab,
|
||||
unsafe = proc.camExpertFast,
|
||||
units = settings.controller.units === 'in' ? 25.4 : 1;
|
||||
|
||||
if (tabs) {
|
||||
// make tab polygons
|
||||
tabs.forEach(tab => {
|
||||
let zero = newPoint(0,0,0);
|
||||
let point = newPoint(tab.pos.x, tab.pos.y, tab.pos.z);
|
||||
let poly = newPolygon().centerRectangle(zero, tab.dim.x, tab.dim.y);
|
||||
let tslice = newSlice(0);
|
||||
let m4 = new THREE.Matrix4().makeRotationFromQuaternion(
|
||||
new THREE.Quaternion(tab.rot._x, tab.rot._y, tab.rot._z, tab.rot._w)
|
||||
);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
if (unsafe) {
|
||||
console.log("disabling overhang safeties");
|
||||
}
|
||||
|
||||
if (!proc.ops || proc.ops.length === 0) {
|
||||
throw 'no processes specified';
|
||||
}
|
||||
|
||||
if (stock.x && stock.y && stock.z) {
|
||||
if (stock.x + 0.00001 < bounds.max.x - bounds.min.x) {
|
||||
throw 'stock X too small for part. disable stock or use offset stock';
|
||||
}
|
||||
|
||||
if (stock.y + 0.00001 < bounds.max.y - bounds.min.y) {
|
||||
throw 'stock Y too small for part. disable stock or use offset stock';
|
||||
}
|
||||
|
||||
if (stock.z + 0.00001 < bounds.max.z - bounds.min.z) {
|
||||
throw 'stock Z too small for part. disable stock or use offset stock';
|
||||
}
|
||||
}
|
||||
|
||||
if (zMin >= bounds.max.z) {
|
||||
throw `invalid z bottom ${(zMin/units).round(3)} >= bounds z max ${(zMax/units).round(3)}`;
|
||||
}
|
||||
|
||||
let mark = Date.now();
|
||||
let slicer = new KIRI.slicer2(widget.getPoints(), {
|
||||
zlist: true,
|
||||
zline: true
|
||||
});
|
||||
|
||||
function updateToolDiams(toolDiam) {
|
||||
minToolDiam = Math.min(minToolDiam, toolDiam);
|
||||
maxToolDiam = Math.max(maxToolDiam, toolDiam);
|
||||
}
|
||||
|
||||
let state = {
|
||||
settings,
|
||||
widget,
|
||||
bounds,
|
||||
tabs,
|
||||
cutTabs,
|
||||
cutPolys,
|
||||
healPolys,
|
||||
slicer,
|
||||
sliceAll,
|
||||
updateToolDiams,
|
||||
zBottom,
|
||||
zThru,
|
||||
ztOff,
|
||||
zMax,
|
||||
zTop,
|
||||
unsafe
|
||||
};
|
||||
|
||||
let opList = [
|
||||
// silently preface op list with OpShadow
|
||||
new CAM.OPS.shadow(state, { type: "shadow" })
|
||||
];
|
||||
|
||||
if (false) {
|
||||
opList.push(new CAM.OPS.xray(state, { type: "xray" }));
|
||||
}
|
||||
|
||||
let opSum = 0;
|
||||
let opTot = opList.map(op => op.weight()).reduce((a,v) => a + v);
|
||||
|
||||
for (let op of proc.ops.filter(op => !op.disabled)) {
|
||||
let opfn = OPS[op.type];
|
||||
if (opfn) {
|
||||
let opin = new opfn(state, op);
|
||||
opList.push(opin);
|
||||
opTot += opin.weight();
|
||||
}
|
||||
}
|
||||
|
||||
// give ops access to entire sequence
|
||||
state.ops = opList;
|
||||
|
||||
for (let op of opList) {
|
||||
let weight = op.weight();
|
||||
op.slice((progress, message) => {
|
||||
onupdate((opSum + (progress * weight)) / opTot, message || op.type());
|
||||
});
|
||||
camOps.push(op);
|
||||
opSum += weight;
|
||||
}
|
||||
|
||||
// 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.shadowTop.tops, ...tabpoly, ...state.shadowTop.slice.shadow], 0, true);
|
||||
let shadowOff = maxToolDiam < 0 ? allpoly :
|
||||
POLY.offset(allpoly, [minToolDiam/2,maxToolDiam/2], { count: 2, flat: true });
|
||||
state.terrain.forEach(level => level.tops.appendAll(shadowOff));
|
||||
|
||||
widget.terrain = state.skipTerrain ? null : state.terrain;
|
||||
widget.minToolDiam = minToolDiam;
|
||||
widget.maxToolDiam = maxToolDiam;
|
||||
|
||||
ondone();
|
||||
};
|
||||
|
||||
CAM.addDogbones = function(poly, dist, reverse) {
|
||||
if (Array.isArray(poly)) {
|
||||
return poly.forEach(p => CAM.addDogbones(p, dist));
|
||||
}
|
||||
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 newpts = [ ];
|
||||
for (let i=0; i<oldpts.length + 1; 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 (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;
|
||||
poly.length = newpts.length;
|
||||
if (poly.inner) {
|
||||
CAM.addDogbones(poly.inner, dist, true);
|
||||
}
|
||||
};
|
||||
|
||||
CAM.traces = function(settings, widget, single) {
|
||||
if (widget.traces && widget.trace_single === single) {
|
||||
// do no work if cached
|
||||
return false;
|
||||
}
|
||||
let slicer = new KIRI.slicer2(widget.getPoints(), {
|
||||
zlist: true,
|
||||
zline: true
|
||||
});
|
||||
let indices = [...new Set(Object.keys(slicer.zFlat)
|
||||
.map(kv => parseFloat(kv).round(5))
|
||||
.appendAll(Object.entries(slicer.zLine).map(ze => {
|
||||
let [ zk, zv ] = ze;
|
||||
return zv > 1 ? parseFloat(zk).round(5) : null;
|
||||
})
|
||||
.filter(v => v !== null)))]
|
||||
.sort((a,b) => b - a);
|
||||
|
||||
let traces = [];
|
||||
// find and trim polys (including open) to shadow
|
||||
let oneach = (data, index, total) => {
|
||||
if (single) {
|
||||
for (let line of data.lines) {
|
||||
if (line.p1.distTo2D(line.p2) > 1) {
|
||||
traces.push(newPolygon().append(line.p1).append(line.p2).setOpen());
|
||||
}
|
||||
}
|
||||
} else
|
||||
BASE.polygons.flatten(data.tops,null,true).forEach(poly => {
|
||||
poly.inner = null;
|
||||
poly.parent = null;
|
||||
let z = poly.getZ();
|
||||
for (let i=0, il=traces.length; i<il; i++) {
|
||||
let trace = traces[i];
|
||||
// only compare polys farther apart in Z
|
||||
if (Math.abs(z - trace.getZ()) > 0.01) {
|
||||
continue;
|
||||
}
|
||||
// do not add duplicates
|
||||
if (traces[i].isEquivalent(poly)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
traces.push(poly);
|
||||
});
|
||||
};
|
||||
let opts = { each: oneach, over: false, flatoff: 0, edges: true, openok: true };
|
||||
slicer.slice(indices, opts);
|
||||
opts.over = true;
|
||||
slicer.slice(indices, opts);
|
||||
widget.traces = traces;
|
||||
widget.trace_single = single;
|
||||
return true;
|
||||
};
|
||||
|
||||
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 healPolys(noff) {
|
||||
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;
|
||||
}
|
||||
|
||||
})();
|
||||
135
src/kiri-mode/cam/tool.js
Normal file
135
src/kiri-mode/cam/tool.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
let KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
UTIL = BASE.util,
|
||||
HPI = Math.PI/2,
|
||||
CAM = KIRI.driver.CAM;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
shaftLength() {
|
||||
return this.unitScale() * this.tool.shaft_len;
|
||||
}
|
||||
|
||||
shaftDiameter() {
|
||||
return this.unitScale() * this.tool.shaft_diam;
|
||||
}
|
||||
|
||||
generateProfile(resolution) {
|
||||
// generate tool profile
|
||||
let type = this.getType(),
|
||||
ball = type === "ballmill",
|
||||
taper = type === "tapermill",
|
||||
tip_diameter = this.tipDiameter(),
|
||||
shaft_offset = this.fluteLength(),
|
||||
flute_diameter = this.fluteDiameter(),
|
||||
shaft_diameter = this.shaftDiameter(),
|
||||
shaft_radius = shaft_diameter / 2,
|
||||
shaft_pix_float = shaft_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;
|
||||
|
||||
// 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) {
|
||||
// console.log({x,y,dx,dy,dist:dist_from_center,ln:dbl.length})
|
||||
// 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) {
|
||||
z_offset = ((dist_from_center - tip_radius_pix_float) / tip_max_radius_offset) * -shaft_offset;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.profile = toolOffset;
|
||||
this.profileDim = {
|
||||
size: shaft_diameter,
|
||||
pix: profile_pix_iter + 2
|
||||
};
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
CAM.Tool = Tool;
|
||||
CAM.getToolDiameter = function(settings, id) {
|
||||
return new CAM.Tool(settings, id).fluteDiameter();
|
||||
};
|
||||
|
||||
})();
|
||||
404
src/kiri-mode/cam/topo.js
Normal file
404
src/kiri-mode/cam/topo.js
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
let KIRI = self.kiri,
|
||||
BASE = self.base,
|
||||
CAM = KIRI.driver.CAM,
|
||||
PRO = CAM.process,
|
||||
POLY = BASE.polygons,
|
||||
RAD2DEG = 180 / Math.PI,
|
||||
newSlice = KIRI.newSlice,
|
||||
newLine = BASE.newLine,
|
||||
newSlope = BASE.newSlope,
|
||||
newPoint = BASE.newPoint,
|
||||
newPolygon = BASE.newPolygon,
|
||||
noop = function() {};
|
||||
|
||||
class Topo {
|
||||
constructor(opt = {}) {
|
||||
let { state, contour, onupdate, ondone } = opt;
|
||||
let { widget, settings, tshadow, center, tabs } = opt.state;
|
||||
let density = parseInt(settings.controller.animesh) * 2500,
|
||||
axis = contour.axis.toLowerCase(),
|
||||
contourX = axis === "x",
|
||||
contourY = axis === "y",
|
||||
bounds = widget.getBoundingBox().clone(),
|
||||
tolerance = contour.tolerace,
|
||||
proc = settings.process,
|
||||
shadow = tshadow,
|
||||
minX = bounds.min.x,
|
||||
maxX = bounds.max.x,
|
||||
minY = bounds.min.y,
|
||||
maxY = bounds.max.y,
|
||||
zBottom = proc.camZBottom,
|
||||
zMin = Math.max(bounds.min.z, zBottom) + 0.0001,
|
||||
boundsX = maxX - minX,
|
||||
boundsY = maxY - minY,
|
||||
inside = contour.inside,
|
||||
resolution = tolerance ? tolerance : 1/Math.sqrt(density/(boundsX * boundsY)),
|
||||
tool = new CAM.Tool(settings, contour.tool),
|
||||
toolOffset = tool.generateProfile(resolution).profile,
|
||||
toolDiameter = tool.fluteDiameter(),
|
||||
toolStep = toolDiameter * contour.step,
|
||||
traceJoin = toolDiameter / 2,
|
||||
maxangle = contour.angle,
|
||||
curvesOnly = contour.curves,
|
||||
R2A = 180 / Math.PI,
|
||||
stepsx = Math.ceil(boundsX / resolution),
|
||||
stepsy = Math.ceil(boundsY / resolution),
|
||||
topo = widget.topo = widget.topo || {
|
||||
data: new Float32Array(stepsx * stepsy),
|
||||
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)) : 0,
|
||||
tabsOn = tabs,
|
||||
tabHeight = Math.max(proc.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;
|
||||
|
||||
if (tolerance == 0) {
|
||||
console.log(`contour auto tolerance`,resolution.round(4));
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
let newtrace,
|
||||
sliceout,
|
||||
latent,
|
||||
lastP,
|
||||
slice, lx, ly,
|
||||
startTime = time(),
|
||||
stepsTaken = 0,
|
||||
stepsTotal = 0;
|
||||
|
||||
// return the touching z given topo x,y and a tool profile
|
||||
function toolAtZ(x,y) {
|
||||
const profile = toolOffset,
|
||||
sx = stepsx,
|
||||
sy = stepsy,
|
||||
xl = sx - 1,
|
||||
yl = sy - 1;
|
||||
|
||||
let tx, ty, tz, gv, i = 0, mz = -1;
|
||||
|
||||
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 = topo.data[tx * sy + ty] || zMin;
|
||||
}
|
||||
// update the rest
|
||||
mz = Math.max(tz + gv, mz);
|
||||
}
|
||||
|
||||
return Math.max(mz, 0);
|
||||
}
|
||||
|
||||
function push_point(x,y,z) {
|
||||
const newP = newPoint(x,y,z);
|
||||
// todo: merge co-linear, not just co-planar
|
||||
if (lastP && Math.abs(lastP.z - z) < 0.005) {
|
||||
if (curvesOnly) {
|
||||
end_poly();
|
||||
} else {
|
||||
latent = newP;
|
||||
}
|
||||
} else {
|
||||
if (latent) {
|
||||
newtrace.push(latent);
|
||||
latent = null;
|
||||
}
|
||||
if (curvesOnly && lastP) {
|
||||
// maxangle
|
||||
const dz = Math.abs(lastP.z - z);
|
||||
const dv = contourX ? Math.abs(lastP.x - x) : Math.abs(lastP.y - y);
|
||||
const angle = Math.atan2(dz, dv) * RAD2DEG;
|
||||
// if (lastP.z < 0.1) console.log('pp', {dz, dxy, angle});
|
||||
if (angle > maxangle) {
|
||||
lastP = newP;
|
||||
return;
|
||||
}
|
||||
}
|
||||
newtrace.push(newP);
|
||||
}
|
||||
lastP = newP;
|
||||
}
|
||||
|
||||
function end_poly() {
|
||||
if (latent) {
|
||||
newtrace.push(latent);
|
||||
}
|
||||
if (newtrace.length > 0) {
|
||||
// add additional constraint on min perimeter()
|
||||
if (newtrace.length > 1) {
|
||||
sliceout.push(newtrace);
|
||||
}
|
||||
newtrace = newPolygon().setOpen();
|
||||
}
|
||||
latent = undefined;
|
||||
lastP = undefined;
|
||||
}
|
||||
|
||||
function raster(slices) {
|
||||
if (!topo.raster) {
|
||||
// console.log({skipping_raster: widget.id});
|
||||
return;
|
||||
}
|
||||
topo.raster = false;
|
||||
|
||||
let gridx = 0,
|
||||
gridy,
|
||||
gridi, // index
|
||||
gridv, // value
|
||||
i, il, j, jl, x, y, tv, ltv;
|
||||
|
||||
// filter lines pairs to only surface "up-facing", "uncovered" lines
|
||||
slices.map(slice => {
|
||||
newslices.push(slice);
|
||||
const points = [];
|
||||
// emit an array of valid line-pairs
|
||||
const lines = slice.lines;
|
||||
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 (p1.y === p2.y) 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);
|
||||
}
|
||||
slice.points = points;
|
||||
if (debug_topo_lines) slice.output()
|
||||
.setLayer("topo lines", {face: 0xff00ff, line: 0x880088})
|
||||
.addLines(points);
|
||||
if (debug_topo_shells) slice.output()
|
||||
.setLayer("topo shells", {face: 0xff00ff, line: 0x008888})
|
||||
.addPolys(slice.topPolys());
|
||||
});
|
||||
|
||||
// raster grid: for each Y slice, find z grid value (x/z swapped)
|
||||
for (j=0, jl=slices.length; j<jl; j++) {
|
||||
const slice = slices[j];
|
||||
const points = slice.points;
|
||||
gridy = 0;
|
||||
// rasterize one x slice
|
||||
for (y = minY; y < maxY && gridy < stepsy; y += resolution) {
|
||||
gridi = gridx * stepsy + gridy;
|
||||
gridv = data[gridi] || 0;
|
||||
// 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 (debug_topo) slice.output()
|
||||
.setLayer("topo", {face: 0, line: 0})
|
||||
.addLine(
|
||||
newPoint(p1.x, y, 0),
|
||||
newPoint(p1.x, y, gridv)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
gridy++;
|
||||
}
|
||||
gridx++;
|
||||
onupdate(++stepsTaken, stepsTotal, "raster surface");
|
||||
}
|
||||
}
|
||||
|
||||
function processSlices(slices) {
|
||||
let gridx = 0,
|
||||
gridy,
|
||||
gridi, // index
|
||||
gridv, // value
|
||||
i, il, j, jl, x, y, tv, ltv;
|
||||
|
||||
raster(slices);
|
||||
|
||||
const checkr = newPoint(0,0);
|
||||
const inClip = function(polys, checkZ) {
|
||||
checkr.x = x;
|
||||
checkr.y = y;
|
||||
for (let i=0; i<polys.length; i++) {
|
||||
let poly = polys[i];
|
||||
let zok = checkZ ? checkZ <= poly.z : true;
|
||||
tabZ = poly.z;
|
||||
if (zok && checkr.isInPolygon(poly)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
let tabZ;
|
||||
|
||||
if (contourY) {
|
||||
startTime = time();
|
||||
// emit slice per X
|
||||
for (x = minX - partOff; x <= maxX + partOff; x += toolStep) {
|
||||
gridx = Math.round(((x - minX) / boundsX) * stepsx);
|
||||
ly = gridy = -gridDelta;
|
||||
slice = newSlice(gridx);
|
||||
newtrace = newPolygon().setOpen();
|
||||
sliceout = [];
|
||||
for (y = minY - partOff; y < maxY + partOff; y += resolution) {
|
||||
// find tool z at grid point
|
||||
tv = toolAtZ(gridx, gridy);
|
||||
// when tabs are on and this point is inside the
|
||||
// tab polygon, ensure z is at least tabHeight
|
||||
if (tabsOn && tv < tabHeight && inClip(clipTab, tv)) {
|
||||
tv = tabZ;//tabHeight;
|
||||
}
|
||||
// if the value is on the floor and inside the clip
|
||||
// poly (usually shadow), end the segment
|
||||
if (clipTo && !inClip(clipTo)) {
|
||||
end_poly();
|
||||
gridy++;
|
||||
ly = -gridDelta;
|
||||
continue;
|
||||
}
|
||||
push_point(x,y,tv);
|
||||
ly = y;
|
||||
ltv = tv;
|
||||
gridy++;
|
||||
}
|
||||
end_poly();
|
||||
if (sliceout.length > 0) {
|
||||
newslices.push(slice);
|
||||
slice.camLines = sliceout;
|
||||
slice.output()
|
||||
.setLayer("contour y", {face: 0, line: 0})
|
||||
.addPolys(sliceout);
|
||||
}
|
||||
onupdate(++stepsTaken, stepsTotal, "contour y");
|
||||
}
|
||||
}
|
||||
|
||||
if (contourX) {
|
||||
startTime = time();
|
||||
// emit slice per Y
|
||||
for (y = minY - partOff; y <= maxY + partOff; y += toolStep) {
|
||||
gridy = Math.round(((y - minY) / boundsY) * stepsy);
|
||||
lx = gridx = -gridDelta;
|
||||
slice = newSlice(gridy);
|
||||
newtrace = newPolygon().setOpen();
|
||||
sliceout = [];
|
||||
for (x = minX - partOff; x <= maxX + partOff; x += resolution) {
|
||||
tv = toolAtZ(gridx, gridy);
|
||||
if (tabsOn && tv < tabHeight && inClip(clipTab, tv)) {
|
||||
tv = tabZ;
|
||||
}
|
||||
if (clipTo && !inClip(clipTo)) {
|
||||
end_poly();
|
||||
gridx++;
|
||||
lx = -gridDelta;
|
||||
continue;
|
||||
}
|
||||
push_point(x,y,tv);
|
||||
lx = x;
|
||||
ltv = tv;
|
||||
gridx++;
|
||||
}
|
||||
end_poly();
|
||||
if (sliceout.length > 0) {
|
||||
newslices.push(slice);
|
||||
slice.camLines = sliceout;
|
||||
slice.output()
|
||||
.setLayer("contour x", {face: 0, line: 0})
|
||||
.addPolys(sliceout);
|
||||
}
|
||||
onupdate(++stepsTaken, stepsTotal, "contour x");
|
||||
}
|
||||
}
|
||||
|
||||
ondone(newslices);
|
||||
}
|
||||
|
||||
let slicer = new KIRI.slicer2(widget.getPoints(), {
|
||||
swapX: true, emptyok: true, notopok: true
|
||||
});
|
||||
let sindex = slicer.interval(resolution);
|
||||
if (!topo.slices) stepsTotal += sindex.length * 2;
|
||||
if (contourX) stepsTotal += Math.round((maxY-minY) / toolStep);
|
||||
if (contourY) stepsTotal += Math.round((maxX-minX) / toolStep);
|
||||
|
||||
let slices = topo.slices = topo.slices || slicer.slice(sindex, { each: (data, index, total) => {
|
||||
onupdate(++stepsTaken, stepsTotal, "topo slice");
|
||||
}, genso: true });
|
||||
|
||||
processSlices(slices.filter(s => s.lines).map(data => data.slice));
|
||||
}
|
||||
}
|
||||
|
||||
CAM.Topo = Topo;
|
||||
|
||||
})();
|
||||
Loading…
Reference in a new issue