package re-organization

This commit is contained in:
Stewart Allen 2021-12-28 16:38:05 -05:00
commit 678316ac95
98 changed files with 63 additions and 62 deletions

View 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

File diff suppressed because it is too large Load diff

View 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
View 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

File diff suppressed because it is too large Load diff

View 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
View 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
View 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
View 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;
})();