move src2 -> src, web2 -> web, remove old module system, fix deps

This commit is contained in:
Stewart Allen 2025-07-06 23:44:33 -04:00
commit ce4f5d6e78
488 changed files with 26179 additions and 26609 deletions

View file

@ -0,0 +1,648 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: kiri-mode.cam.driver
// dep: kiri-mode.cam.animate2
gapp.register("kiri-mode.cam.animate", [], (root, exports) => {
const { kiri } = root;
const { driver } = kiri;
const { CAM } = driver;
const asLines = false;
const asPoints = false;
// ---( CLIENT FUNCTIONS )---
// tint points below z=0 with red
function add_red_neg_z(material) {
material.onBeforeCompile = (shader) => {
shader.vertexShader = shader.vertexShader.replace(
`#include <worldpos_vertex>`,
`
#include <worldpos_vertex>
vWorldPosition = vec3(transformed);
`
);
shader.vertexShader = `
varying vec3 vWorldPosition;
` + shader.vertexShader;
shader.fragmentShader = `
varying vec3 vWorldPosition;
` + shader.fragmentShader;
shader.fragmentShader = shader.fragmentShader.replace(
`#include <dithering_fragment>`,
`
#include <dithering_fragment>
if (vWorldPosition.z < 0.0) {
gl_FragColor.rgb += vec3(0.5, 0.0, 0.0); // Add red tint
}
`
);
};
return material;
}
kiri.load(() => {
if (!kiri.client) {
return;
}
let meshes = {},
button = {},
label = {},
unitScale = 1,
speedValues = [ 1, 2, 4, 8, 32 ],
speedPauses = [ 30, 20, 10, 5, 0 ],
speedNames = [ "1x", "2x", "4x", "8x", "!!" ],
speedMax = speedValues.length - 1,
speedIndex = 0,
speed,
color = 0,
material,
origin,
posOffset = { x:0, y:0, z:0 };
const { moto } = root;
const { space } = moto;
const { api } = kiri;
function animate_clear(api) {
let { anim } = api.ui;
moto.space.platform.showGridBelow(true);
kiri.client.animate_cleanup();
Object.keys(meshes).forEach(id => deleteMesh(id));
toggleStock(undefined,true,false);
api.uc.setVisible(anim.laba, false);
api.uc.setVisible(anim.vala, false);
}
function animate(api, delay) {
let alert = api.alerts.show("building animation");
let settings = api.conf.get();
kiri.client.animate_setup(settings, data => {
checkMeshCommands(data);
if (!(data && data.mesh_add)) {
return;
}
let { anim } = api.ui;
Object.assign(button, {
replay: anim.replay,
play: anim.play,
step: anim.step,
pause: anim.pause,
speed: anim.speed,
trans: anim.trans,
model: anim.model,
shade: anim.shade
});
Object.assign(label, {
progress: anim.progress,
speed: anim.labspd,
x: anim.valx,
y: anim.valy,
z: anim.valz,
});
origin = settings.origin;
speedIndex = api.local.getInt('cam.anim.speed') || 0;
updateSpeed();
setTimeout(step, delay || 0);
button.replay.onclick = replay;
button.play.onclick = play;
button.step.onclick = step;
button.pause.onclick = pause;
button.speed.onclick = fast;
button.trans.onclick = toggleTrans;
button.model.onclick = toggleModel;
button.shade.onclick = toggleStock;
button.play.style.display = '';
button.pause.style.display = 'none';
api.event.emit('animate', 'CAM');
api.alerts.hide(alert);
moto.space.platform.showGridBelow(false);
toggleTrans(0,api.local.getBoolean('cam.anim.trans', true));
toggleModel(0,api.local.getBoolean('cam.anim.model', false));
toggleStock(0,api.local.getBoolean('cam.anim.stock', false));
});
}
gapp.overlay(kiri.client, {
animate(data, ondone) {
kiri.client.send("animate", data, ondone);
},
animate_setup(settings, ondone) {
color = settings.controller.dark ? 0x48607B : 0x607FA4;
unitScale = settings.controller.units === 'in' ? 1/25.4 : 1;
let flatShading = true,
transparent = true,
opacity = 0.9,
side = THREE.DoubleSide;
material = new THREE.MeshPhongMaterial({
flatShading,
transparent,
opacity,
color,
side
});
add_red_neg_z(material);
kiri.client.send("animate_setup", {settings}, ondone);
},
animate_cleanup(data, ondone) {
kiri.client.send("animate_cleanup", data, ondone);
}
});
gapp.overlay(CAM, {
animate,
animate_clear
});
function meshAdd(id, ind, pos, sab) {
const geo = new THREE.BufferGeometry();
if (sab) {
// use array buffer shared with worker
pos = new Float32Array(sab);
}
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
if (ind.length) {
geo.setIndex(new THREE.BufferAttribute(new Uint32Array(ind), 1));
}
let mesh;
if (asPoints) {
const mat = new THREE.PointsMaterial({
transparent: true,
opacity: 0.75,
color: 0x888888,
size: 0.3
});
mesh = new THREE.Points(geo, mat);
} else if (asLines) {
const mat = new THREE.LineBasicMaterial({
transparent: true,
opacity: 0.75,
color
});
mesh = new THREE.LineSegments(geo, mat);
} else {
geo.computeVertexNormals();
mesh = new THREE.Mesh(geo, material);
mesh.renderOrder = -10;
}
space.world.add(mesh);
meshes[id] = mesh;
}
function meshUpdates(id) {
const mesh = meshes[id];
if (!mesh) {
return; // animate cancelled
}
mesh.geometry.attributes.position.needsUpdate = true;
space.update();
}
function deleteMesh(id) {
space.world.remove(meshes[id]);
delete meshes[id];
}
function toggleModel(ev,bool) {
api.local.toggle('cam.anim.model', bool);
api.widgets.all().forEach(w => w.toggleVisibility(bool));
}
function toggleStock(ev,bool,set) {
set !== false && api.local.toggle('cam.anim.stock', bool);
return api.event.emit('cam.stock.toggle', bool ?? undefined);
}
function toggleTrans(ev,bool) {
bool = api.local.toggle('cam.anim.trans', bool);
material.transparent = bool;
material.needsUpdate = true;
}
function step() {
updateSpeed();
kiri.client.animate({speed, steps: 1}, handleGridUpdate);
}
function play(opts) {
const { steps } = opts;
updateSpeed();
if (steps !== 1) {
button.play.style.display = 'none';
button.pause.style.display = '';
}
kiri.client.animate({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
}, handleGridUpdate);
}
function fast(opts) {
const { steps } = opts;
updateSpeed(1);
button.play.style.display = 'none';
button.pause.style.display = '';
kiri.client.animate({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
}, handleGridUpdate);
}
function pause() {
button.play.style.display = '';
button.pause.style.display = 'none';
kiri.client.animate({speed: 0}, handleGridUpdate);
}
function handleGridUpdate(data) {
checkMeshCommands(data);
if (data && data.progress) {
label.progress.value = (data.progress * 100).toFixed(1);
}
}
function updateSpeed(inc = 0) {
if (inc === Infinity) {
speedIndex = speedMax;
} else if (inc > 0) {
speedIndex = (speedIndex + inc) % speedValues.length;
}
api.local.set('cam.anim.speed', speedIndex);
speed = speedValues[speedIndex];
label.speed.value = speedNames[speedIndex];
}
function replay() {
animate_clear(api);
setTimeout(() => {
animate(api, 50);
}, 250);
}
function checkMeshCommands(data) {
if (!data) {
return;
}
if (data.mesh_add) {
const { id, ind, pos, offset, sab } = data.mesh_add;
meshAdd(id, ind, pos, sab);
space.refresh();
if (offset) {
posOffset = offset;
}
}
if (data.mesh_del) {
deleteMesh(data.mesh_del);
}
if (data.mesh_move) {
const { id, pos } = data.mesh_move;
const mesh = meshes[id];
if (mesh) {
mesh.position.x = pos.x;
mesh.position.y = pos.y;
mesh.position.z = pos.z;
space.update();
}
label.x.value = (pos.x - origin.x).toFixed(2);
label.y.value = (pos.y + origin.y).toFixed(2);
label.z.value = (pos.z - origin.z).toFixed(2);
}
if (data.mesh_update) {
meshUpdates(data.id);
}
}
});
// ---( WORKER FUNCTIONS )---
kiri.load(() => {
if (!kiri.worker) {
return;
}
let stock, center, grid, gridX, gridY, rez;
let path, pathIndex, tool, tools, last, toolID = 1;
kiri.worker.animate_setup = function(data, send) {
const { settings } = data;
const { process } = settings;
const print = worker.print;
const density = parseInt(settings.controller.animesh) * 1000;
pathIndex = 0;
path = print.output.flat();
tools = settings.tools;
stock = settings.stock;
rez = 1/Math.sqrt(density/(stock.x * stock.y));
//destructure arcs into path points
path = path.map(o=>
o.arcPoints
? [
...o.arcPoints.map(point=>
({
...o,
point
})
),
o
]
: o
)
.flat();
const step = rez;
const stepsX = Math.floor(stock.x / step);
const stepsY = Math.floor(stock.y / step);
const { pos, ind, sab } = createGrid(stepsX, stepsY, stock, step, true);
const offset = {
x: process.camOriginCenter ? 0 : stock.x / 2,
y: process.camOriginCenter ? 0 : stock.y / 2,
z: process.camOriginTop ? -stock.z : 0
}
grid = pos;
gridX = stepsX;
gridY = stepsY;
tool = null;
last = null;
animating = false;
animateClear = false;
center = Object.assign({}, stock.center);
center.z -= stock.z / 2;
send.data({ mesh_add: { id: 0, ind, offset, sab } }, [ ]); // sab not transferrable
send.data({ mesh_move: { id: 0, pos: center } });
send.done();
};
kiri.worker.animate = function(data, send) {
renderPause = data.pause || renderPause;
renderSpeed = data.speed || 0;
if (animating) {
return send.done();
}
renderSteps = data.steps || 1;
renderDone = false;
animating = renderSpeed > 0;
renderPath(send);
};
kiri.worker.animate_cleanup = function(data, send) {
if (animating) {
animateClear = true;
}
};
function createGrid(stepsX, stepsY, size, step, stock) {
const gridPoints = stepsX * stepsY;
const sab = new SharedArrayBuffer(gridPoints * 3 * 4)
const pos = new Float32Array(sab);
const ind = [];
const ox = size.x / 2;
const oy = size.y / 2;
let ex = stepsX - 1;
let ey = stepsY - 1;
// initialize grid points
for (let x=0, ai=0; x<stepsX; x++) {
for (let y=0; y<stepsY; y++) {
let px = pos[ai++] = x * step - ox + step / 2;
let py = pos[ai++] = y * step - oy + step / 2;
pos[ai++] = stock && (x * y === 0 || x === ex || y === ey) ? 0 : size.z;
if (asPoints) {
continue;
}
if (asLines) {
if (y > 0) ind.appendAll([
(stepsY * x) + (y - 1),
(stepsY * x) + (y )
]);
if (x > 0) ind.appendAll([
(stepsY * (x - 1)) + y,
(stepsY * (x )) + y
]);
} else {
if (x > 0 && y > 0) {
let v0 = stepsY * (x - 1) + y - 1;
let v1 = stepsY * (x - 0) + y - 1;
let v2 = stepsY * (x - 0) + y;
let v3 = stepsY * (x - 1) + y;
ind.appendAll([
v0, v1, v2, v0, v2, v3
]);
}
}
}
}
return { pos, ind, sab };
}
let animateClear = false;
let animating = false;
let renderDist = 0;
let renderDone = false;
let renderPause = 10;
let renderSteps = 0;
let renderSpeed = 0;
let skipMove = null;
let toolUpdate;
let depth = 0;
// send latest tool position and progress bar
function renderUpdate(send) {
if (toolUpdate) {
send.data(toolUpdate);
}
send.data({ progress: pathIndex / path.length, id: 0, mesh_update: 1 });
}
function renderPath(send) {
if (renderDone) {
return;
}
if (renderSteps-- === 0) {
animating = false;
renderPath(send);
return;
}
if (animating === false || animateClear || renderSpeed === 0) {
renderUpdate(send);
renderDone = true;
animating = false;
animateClear = false;
send.done();
return;
}
let next = path[pathIndex];
while (next && next.type === 'laser') {
last = next;
next = path[++pathIndex];
}
if (!next) {
animating = false;
renderPath(send);
return;
}
pathIndex++;
const firstTool = !tool && next.tool;
const toolChange = !firstTool && (tool.getID() !== next.tool.getID());
if (firstTool || toolChange) {
// on real tool change, go to safe Z first
if (tool && last.point) {
let pos = last.point = {
x: last.point.x,
y: last.point.y,
z: stock.z
};
send.data({ mesh_move: { toolID, pos }});
}
updateTool(next.tool, send);
}
const id = toolID;
const rezstep = rez;
if (last) {
const lp = last.point, np = next.point;
last = next;
// dwell ops have no point
if (!np || !lp) {
return renderPath(send);
}
const dx = np.x - lp.x, dy = np.y - lp.y, dz = np.z - lp.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
renderDist += dist;
// skip moves that are less than grid resolution
if (renderDist < rezstep) {
renderPath(send);
return;
}
const md = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz));
const st = Math.ceil(md / rezstep);
const mx = dx / st, my = dy / st, mz = dz / st;
const moves = [];
for (let i=0, x=lp.x, y=lp.y, z=lp.z; i<st; i++) {
moves.push({x,y,z});
x += mx;
y += my;
z += mz;
}
moves.push(next.point);
renderMoves(id, moves, send);
} else {
last = next;
if (tool) {
tool.pos = next.point;
toolUpdate = { mesh_move: { id, pos: next.point }};
}
renderPath(send);
}
}
function renderMoves(id, moves, send, seed = 0) {
for (let index = seed; index<moves.length; index++) {
const pos = moves[index];
if (!pos) {
throw `no pos @ ${index} of ${moves.length}`;
}
tool.pos = pos;
deformMesh(pos, send);
toolUpdate = { mesh_move: { id, pos }};
// pause renderer at specified offsets
if ((renderSpeed && renderDist >= renderSpeed) || (depth > 600)) {
renderDist = depth = 0;
renderUpdate(send);
setTimeout(() => {
renderMoves(id, moves, send, index);
}, renderPause);
return;
}
}
depth++;
renderPath(send);
}
// update stock mesh to reflect tool tip geometry at given XYZ position
function deformMesh(pos, send) {
const prof = tool.profile;
const { size, pix } = tool.profileDim;
const mid = Math.floor(pix / 2);
const rx = Math.floor((pos.x + stock.x / 2 - size / 2 - center.x) / rez);
const ry = Math.floor((pos.y + stock.y / 2 - size / 2 - center.y) / rez);
let upos = 0;
// deform mesh to lowest point on tool profile
for (let i=0, il=prof.length; i < il; ) {
const dx = mid + prof[i++];
const dy = mid + prof[i++];
const dz = prof[i++];
const gx = rx + dx;
const gy = ry + dy;
if (gx < 0|| gy < 0 || gx > gridX-1 || gy > gridY-1) {
continue;
}
const gi = gx * gridY + gy;
const iz = gi * 3 + 2;
const cz = grid[iz];
const tz = tool.pos.z - dz;
if (tz < cz) {
upos++;
grid[iz] = tz;
}
}
}
function updateTool(toolobj, send) {
if (tool) {
send.data({ mesh_del: toolID });
}
tool = new CAM.Tool({ tools }, toolobj.getID());
tool.generateProfile(rez);
const flen = tool.fluteLength() || 15;
const slen = tool.shaftLength() || 15;
// const frad = tool.fluteDiameter() / 2;
const prof = tool.profile;
const { size, pix } = tool.profileDim;
const { pos, ind, sab } = createGrid(pix, pix, {x:size, y:size, z:flen+slen}, rez);
const mid = Math.floor(pix/2);
// deform mesh to fit tool profile
for (let i=0, il=prof.length; i < il; ) {
const dx = mid + prof[i++];
const dy = mid + prof[i++];
const dz = prof[i++];
pos[(dx * pix + dy) * 3 + 2] = -dz;
}
send.data({ mesh_add: { id:++toolID, ind, sab }});
}
});
});

View file

@ -0,0 +1,670 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: geo.csg
// dep: kiri-mode.cam.driver
gapp.register("kiri-mode.cam.animate2", [], (root) => {
const { kiri } = root;
const { driver } = kiri;
const { CAM } = driver;
// ---( CLIENT FUNCTIONS )---
kiri.load(() => {
if (!kiri.client) {
return;
}
let meshes = {},
button = {},
label = {},
material,
speedPauses = [ 0, 0, 0, 0, 0 ],
speedValues = [ 1, 2, 4, 8, 32 ],
speedNames = [ "1x", "2x", "4x", "8x", "!!" ],
speedMax = speedValues.length - 1,
speedIndex = 0,
speed,
origin,
color = 0;
const { moto } = root;
const { space } = moto;
const { api } = kiri;
function animate_clear2(api) {
let { anim } = api.ui;
kiri.client.animate_cleanup2();
Object.keys(meshes).forEach(id => deleteMesh(id));
api.widgets.setAxisIndex(0);
api.uc.setVisible(anim.laba, true);
api.uc.setVisible(anim.vala, true);
anim.vala.value = "0.0";
}
function animate2(api, delay) {
let alert = api.alerts.show("building animation");
let settings = api.conf.get();
kiri.client.animate_setup2(settings, data => {
handleUpdate(data);
if (data) {
return;
}
let { anim } = api.ui;
Object.assign(button, {
replay: anim.replay,
play: anim.play,
step: anim.step,
pause: anim.pause,
speed: anim.speed,
trans: anim.trans,
model: anim.model,
shade: anim.shade
});
Object.assign(label, {
progress: anim.progress,
speed: anim.labspd,
x: anim.valx,
y: anim.valy,
z: anim.valz,
a: anim.vala
});
updateSpeed(0);
setTimeout(step, delay || 0);
toggleTrans(undefined, false);
origin = settings.origin;
button.replay.onclick = replay;
button.play.onclick = play;
button.step.onclick = step;
button.pause.onclick = pause;
button.speed.onclick = fast;
button.trans.onclick = toggleTrans;
button.model.onclick = toggleModel;
button.shade.onclick = toggleStock;
button.play.style.display = '';
button.pause.style.display = 'none';
api.event.emit('animate', 'CAM');
api.alerts.hide(alert);
});
}
gapp.overlay(kiri.client, {
animate2(data, ondone) {
kiri.client.send("animate2", data, ondone);
},
animate_setup2(settings, ondone) {
color = settings.controller.dark ? 0x888888 : 0;
material = new THREE.MeshMatcapMaterial({
flatShading: true,
transparent: false,
opacity: 0.9,
color: 0x888888,
side: THREE.DoubleSide
});
kiri.client.send("animate_setup2", {settings}, ondone);
},
animate_cleanup2(data, ondone) {
kiri.client.send("animate_cleanup2", data, ondone);
}
});
gapp.overlay(CAM, {
animate2,
animate_clear2
});
function meshAdd(id, ind, pos, ilen, plen) {
const geo = new THREE.BufferGeometry();
const pa = plen ? pos.subarray(0, plen * 3) : pos;
const ia = ilen ? ind.subarray(0, ilen) : ind;
geo.setAttribute('position', new THREE.BufferAttribute(pa, 3));
geo.setIndex(new THREE.BufferAttribute(ia, 1));
const mesh = new THREE.Mesh(geo, material);
mesh.pos = pos;
mesh.ind = ind;
space.world.add(mesh);
meshes[id] = mesh;
}
function meshUpdate(id, ind, pos, ilen, plen) {
const mesh = meshes[id];
if (!mesh) {
return; // animate cancelled
}
const geo = mesh.geometry;
mesh.pos = pos || mesh.pos;
mesh.ind = ind || mesh.ind;
geo.setAttribute('position', new THREE.BufferAttribute(mesh.pos.subarray(0, plen * 3), 3));
geo.setIndex(new THREE.BufferAttribute(mesh.ind.subarray(0, ilen), 1));
geo.attributes.position.needsUpdate = true;
geo.index.needsUpdate = true;
space.update();
}
function deleteMesh(id) {
space.world.remove(meshes[id]);
delete meshes[id];
}
function toggleModel(ev,bool) {
api.local.toggle('cam.anim.model', bool);
api.widgets.all().forEach(w => w.toggleVisibility(bool));
}
function toggleStock(ev,bool,set) {
set !== false && api.local.toggle('cam.anim.stock', bool);
return api.event.emit('cam.stock.toggle', bool ?? undefined);
}
function toggleTrans(ev,bool) {
bool = api.local.toggle('cam.anim.trans', bool);
material.transparent = bool;
material.needsUpdate = true;
}
function step() {
updateSpeed();
kiri.client.animate2({speed, steps: 1}, handleUpdate);
}
function play(opts) {
const { steps } = opts;
updateSpeed();
if (steps !== 1) {
button.play.style.display = 'none';
button.pause.style.display = '';
}
kiri.client.animate2({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
}, handleUpdate);
}
function fast(opts) {
const { steps } = opts;
updateSpeed(1);
button.play.style.display = 'none';
button.pause.style.display = '';
kiri.client.animate2({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
}, handleUpdate);
}
function pause() {
button.play.style.display = '';
button.pause.style.display = 'none';
kiri.client.animate2({speed: 0}, handleUpdate);
}
function handleUpdate(data) {
if (!data) {
return;
}
if (data.mesh_add) {
const { id, ind, pos, ilen, plen } = data.mesh_add;
meshAdd(id, ind, pos, ilen, plen);
space.refresh();
}
if (data.mesh_del) {
deleteMesh(data.mesh_del);
}
if (data.mesh_move) {
const { id, pos } = data.mesh_move;
const mesh = meshes[id];
if (mesh) {
mesh.position.x = pos.x;
mesh.position.y = pos.y;
mesh.position.z = pos.z;
space.refresh();
}
label.x.value = (pos.x - origin.x).toFixed(2);
label.y.value = (pos.y + origin.y).toFixed(2);
label.z.value = (pos.z - origin.z).toFixed(2);
}
if (data.stock_index !== undefined) {
api.widgets.setAxisIndex(data.stock_index);
label.a.value = -data.stock_index.toFixed(1);
}
if (data.mesh_index) {
const { id, index } = data.mesh_index;
const mesh = meshes[id];
if (mesh) {
mesh.rotation.x = (Math.PI / 180) * index;
space.refresh();
}
}
if (data.mesh_update) {
const { id, ind, pos, ilen, plen } = data.mesh_update;
meshUpdate(id, ind, pos, ilen, plen);
}
if (data && data.progress) {
label.progress.value = (data.progress * 100).toFixed(1);
}
}
function updateSpeed(inc = 0) {
if (inc === Infinity) {
speedIndex = speedMax;
} else if (inc > 0) {
speedIndex = (speedIndex + inc) % speedValues.length;
}
api.local.set('cam.anim.speed', speedIndex);
speed = speedValues[speedIndex];
label.speed.value = speedNames[speedIndex];
}
function replay() {
animate_clear2(api);
setTimeout(() => {
animate2(api, 50);
}, 250);
}
});
// ---( WORKER FUNCTIONS )---
const { CSG } = root.base;
let nextMeshID = 1;
class Stock {
constructor(x, y, z) {
this.id = nextMeshID++;
this.vbuf = undefined;
this.ibuf = undefined;
this.ilen = 0;
this.sends = 0;
this.newbuf = true;
this.subtracts = 0;
this.mesh = CSG.Instance().Manifold.cube([x, y, z], true);
}
send(send) {
const newbuf = this.newbuf;
const action = this.sends++ === 0 ? 'mesh_add' : 'mesh_update';
send.data({ [action]: {
id: this.id,
ind: newbuf ? this.ibuf : undefined,
pos: newbuf ? this.vbuf : undefined,
ilen: this.ilen,
plen: this.plen
} });
this.newbuf = false;
this.sends++;
// console.log({ send: this.id, newbuf, action });
}
translate(x, y, z) {
// console.log({ translate: this.id, x, y, z });
const oldmesh = this.mesh;
this.mesh = this.mesh.translate(x, y, z);
this.bounds = CSG.toBox3(this.mesh);
oldmesh.delete();
return this;
}
updateMesh(updates) {
if (this.sends > 0 && this.subtracts === 0) {
return;
}
const subs = this.subtracts;
this.subtracts = 0;
let start = Date.now();
let mesh = this.mesh.getMesh();
this.sharedVertexBuffer(mesh.numVert * 3);
this.sharedIndexBuffer(mesh.numTri * 3);
this.vbuf.set(mesh.vertProperties);
this.ibuf.set(mesh.triVerts);
let sub = Date.now();
updates.push(this);
if (false) console.log({
update: this.id,
time: sub - start,
subs,
mssub: ((sub - start) / subs).round(3)
});
}
subtractTool(toolMesh) {
const oldmesh = this.mesh;
this.mesh = this.mesh.subtract(toolMesh.mesh);
oldmesh.delete();
this.subtracts++;
}
sharedVertexBuffer(size) {
const old = this.vbuf;
this.plen = size;
if (old && old.length >= size) {
// console.log({svb: this.id, reuse: size, old: old.length});
return old;
}
// let buf = new Float32Array(new SharedArrayBuffer(size * 4));
let buf = new Float32Array(new SharedArrayBuffer(size * 4 + 1024 * 1024));
// console.log({new_svb: this.id, size, buf});
this.newbuf = true;
return this.vbuf = buf;
}
sharedIndexBuffer(size) {
const old = this.ibuf;
this.ilen = size;
if (old && old.length >= size) {
// console.log({sib: this.id, reuse: size, old: old.length});
return old;
}
// let buf = new Uint32Array(new SharedArrayBuffer(size * 4));
let buf = new Uint32Array(new SharedArrayBuffer(size * 4 + 1024 * 1024));
// console.log({new_sib: this.id, size, buf});
this.newbuf = true;
return this.ibuf = buf;
}
}
kiri.load(() => {
if (!kiri.worker) {
return;
}
let stock, center, rez;
let path, pathIndex, tool, tools, last;
let stockZ;
let stockIndexMsg = false;
let stockSlices;
let stockIndex;
let startTime;
let toolID = -1;
let toolMesh;
let toolRadius;
let toolUpdateMsg;
let animateClear = false;
let animating = false;
let moveDist = 0;
let renderDist = 0;
let renderDone = false;
let renderPause = 10;
let renderSteps = 0;
let renderSpeed = 0;
let indexCount = 0;
let updates = 0;
kiri.worker.animate_setup2 = function(data, send) {
const { settings } = data;
const { process } = settings;
const print = worker.print;
const density = parseInt(settings.controller.animesh) * 1000;
const isIndexed = process.camStockIndexed;
pathIndex = 0;
path = print.output.flat();
tools = settings.tools;
stock = settings.stock;
rez = 1/Math.sqrt(density/(stock.x * stock.y));
tool = null;
last = null;
animating = false;
animateClear = false;
stockIndex = 0;
indexCount = 0;
startTime = 0;
updates = 0;
stockZ = isIndexed ? 0 : stock.z;
stockSlices = [];
const { x, y, z } = stock;
const sliceCount = parseInt(settings.controller.animesh || 2000) / 100;
const sliceWidth = stock.x / sliceCount;
for (let i=0; i<sliceCount; i++) {
let xmin = -(x/2) + (i * sliceWidth) + sliceWidth / 2;
let slice = new Stock(sliceWidth, y, z).translate(xmin, 0, 0);
stockSlices.push(slice);
slice.updateMesh([]);
slice.send(send);
// send({ mesh_move: { id: slice.id, pos: { x:0, y:0, z: stock.z/2} } });
}
send.done();
};
kiri.worker.animate2 = function(data, send) {
renderPause = data.pause || renderPause;
renderSpeed = data.speed || 0;
if (animating) {
return send.done();
}
renderSteps = data.steps || 1;
renderDone = false;
animating = renderSpeed > 0;
startTime = startTime || Date.now();
renderPath(send);
};
kiri.worker.animate_cleanup2 = function(data, send) {
if (animating) {
animateClear = true;
}
};
function renderPath(send) {
if (renderDone) {
return;
}
if (renderSteps-- === 0) {
animating = false;
renderPath(send);
return;
}
if (animating === false || animateClear || renderSpeed === 0) {
renderUpdate(send);
renderDone = true;
animating = false;
animateClear = false;
send.done();
return;
}
let next = path[pathIndex];
while (next && next.type === 'laser') {
last = next;
next = path[++pathIndex];
}
if (!next) {
console.log('animation completed in ', ((Date.now() - startTime)/1000).round(2));
animating = false;
renderPath(send);
return;
}
pathIndex++;
// console.log(next.point.z);
if (next.tool && (!tool || tool.getID() !== next.tool.getID())) {
// on real tool change, go to safe Z first
if (tool && last.point) {
let pos = last.point = {
x: last.point.x,
y: last.point.y,
z: stock.z
};
toolMove(pos);
send.data(toolUpdateMsg);
}
toolUpdate(next.tool.getID(), send);
}
const id = toolID;
const rezstep = rez * 2;
if (last) {
const lp = last.point, np = next.point;
last = next;
// dwell ops have no point
if (!np || !lp) {
return renderPath(send);
}
let dx = np.x - lp.x,
dy = np.y - lp.y,
dz = np.z - lp.z,
da = Math.abs((np.a || 0) - (lp.a || 0)),
dr = (da / 360) * (2 * Math.PI * Math.max(np.z, lp.z)),
dist = Math.sqrt(dx*dx + dy*dy + dz*dz + dr*dr);
moveDist += dist;
// skip moves that are less than grid resolution
if (moveDist < rezstep) {
// console.log('skip', moveDist, rezstep, next);
renderPath(send);
return;
}
const md = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz), dr);
const st = Math.ceil(md / rezstep);
const mx = dx / st, my = dy / st, mz = dz / st;
// const sd = Math.sqrt(mx*mx + my*my + mz*mz + dr*dr);
const sd = Math.sqrt(mx*mx + my*my + Math.min(1,mz*mz) + dr*dr);
const moves = [];
for (let i=0, x=lp.x, y=lp.y, z=lp.z; i<st; i++) {
moves.push({ x, y, z, a:lp.a, md:sd, dx, dy, dz });
x += mx;
y += my;
z += mz;
}
moveDist = 0;
moves.push({...next.point, md:sd});
renderMoves(id, moves, send);
} else {
last = next;
if (tool) {
toolMove(next.point);
}
renderPath(send);
}
}
function renderMoves(id, moves, send, seed = 0) {
for (let index = seed; index<moves.length; index++) {
const pos = moves[index];
if (!pos) {
throw `no pos @ ${index} of ${moves.length}`;
}
const { dx, dy, dz } = pos;
toolMove(pos);
// console.log('renderMoves', {id, moves, seed});
let subs = 0;
if (dx || dy || dz < 0)
for (let slice of stockSlices) {
if (slice.bounds.intersectsBox(toolMesh.bounds)) {
slice.subtractTool(toolMesh);
subs++;
}
}
// console.log({ index, subs });
renderDist += pos.md;
// pause renderer at specified offsets
if (renderSpeed && renderDist >= renderSpeed) {
renderDist = 0;
renderUpdate(send);
setTimeout(() => {
renderMoves(id, moves, send, index + 1);
}, renderPause);
return;
}
}
renderPath(send);
}
// send latest tool position and progress bar
function renderUpdate(send) {
const updated = []
for (let slice of stockSlices) {
slice.updateMesh(updated);
}
for (let slice of updated) {
slice.send(send);
}
if (toolUpdateMsg) {
send.data(toolUpdateMsg);
}
if (stockIndexMsg) {
send.data({ stock_index: stockIndex });
for (let slice of stockSlices) {
send.data({ mesh_index: { id: slice.id, index: -stockIndex } });
}
stockIndexMsg = false;
}
send.data({ progress: pathIndex / path.length });
updates++;
}
// move tool mesh animation space, update client
function toolMove(pos) {
toolUpdateMsg = { mesh_move: { id: toolID, pos: { x: pos.x, y: pos.y, z: pos.z } } };
if (toolMesh.mesh) {
toolMesh.mesh.delete();
}
toolMesh.mesh = toolMesh.root.translate(pos.x, pos.y, pos.z);
if (pos.a !== undefined) {
let tmp = toolMesh.mesh.rotate([ pos.a, 0, 0 ]);
toolMesh.mesh.delete();
toolMesh.mesh = tmp;
if (pos.a !== stockIndex) {
stockIndexMsg = true;
stockIndex = pos.a;
}
}
toolMesh.bounds = CSG.toBox3(toolMesh.mesh);
}
// delete old tool mesh, generate tool mesh, send to client
function toolUpdate(toolid, send) {
if (tool) {
send.data({ mesh_del: toolID });
}
tool = new CAM.Tool({ tools }, toolid);
const Instance = CSG.Instance();
const slen = tool.shaftLength() || 15;
const srad = tool.shaftDiameter() / 2;
const flen = tool.fluteLength() || 15;
const frad = toolRadius = tool.fluteDiameter() / 2;
const tlen = slen + flen; // total tool length
let { cylinder, sphere } = Instance.Manifold;
let mesh;
if (tool.isBallMill()) {
mesh = cylinder(tlen - frad * 2, frad, frad, 20, true)
.add(sphere(frad, 20).translate(0, 0, -(tlen - frad * 2)/2))
.add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen/2));
} else if (tool.isTaperMill()) {
const trad = Math.max(tool.tipDiameter() / 2, 0.001);
mesh = cylinder(slen, srad, srad, 20, true).translate(0, 0, slen/2)
.add(cylinder(flen, trad, frad, 20, true).translate(0, 0, -flen/2));
} else {
mesh = cylinder(tlen, frad, frad, 20, true)
.add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen/2));
}
mesh = mesh.translate(0, 0, (tlen - stockZ) / 2);
const raw = mesh.getMesh();
const vertex = raw.vertProperties;
const index = raw.triVerts;
toolMesh = { root: mesh, index, vertex, bounds: CSG.toBox3(mesh) };
send.data({ mesh_add: { id:--toolID, ind: index, pos: vertex }});
}
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,188 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: main.kiri
// use: kiri.codec
// use: mesh.tool
gapp.register("kiri-mode.cam.driver", [], (root, exports) => {
const { kiri } = root;
const { driver } = kiri;
const CAM = driver.CAM = {};
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.load(api => {
if (kiri.client) {
CAM.surface_prep = function(index, ondone) {
kiri.client.sync();
const settings = api.conf.get();
kiri.client.send("cam_surfaces", { settings, index }, output => {
ondone(output);
});
};
CAM.surface_show = function(widget) {
widget.selectFaces(Object.values(widget._surfaces).flat());
};
CAM.surface_toggle = function(widget, face, radians, ondone) {
let surfaces = widget._surfaces = widget._surfaces || {};
for (let [root, faces] of Object.entries(surfaces)) {
if (faces.contains(face)) {
// delete this face group
delete surfaces[root];
CAM.surface_show(widget);
ondone(Object.keys(surfaces).map(v => parseInt(v)));
return;
}
}
kiri.client.send("cam_surface_find", { id: widget.id, face, radians }, faces => {
if (faces.length) {
surfaces[face] = faces;
CAM.surface_show(widget);
}
ondone(Object.keys(surfaces).map(v => parseInt(v)));
});
};
CAM.surface_clear = function(widget) {
widget.selectFaces([]);
widget._surfaces = {};
};
CAM.traces = function(ondone, single) {
kiri.client.sync();
const settings = api.conf.get();
const widgets = api.widgets.map();
kiri.client.send("cam_traces", { settings, single }, output => {
const ids = [];
kiri.codec.decode(output).forEach(rec => {
ids.push(rec.id);
widgets[rec.id].traces = rec.traces;
});
ondone(ids);
});
};
CAM.traces_clear = function(ondone) {
kiri.client.send("cam_traces_clear", {}, () => {
// console.log({clear_traces: true});
});
};
CAM.holes = function(indiv,rec,onProgress,onDone) {
kiri.client.sync();
const settings = api.conf.get();
return new Promise((res,rej)=>{
kiri.client.send("cam_holes", { settings, rec, indiv }, output => {
let out = kiri.codec.decode(output)
if(out.progress != undefined){
//if a progress message,
onProgress(out.progress,out.msg)
}else{
api.hide.alert(alert);
onDone(out);
res(out);
}
});
})
};
}
if (kiri.worker) {
CAM.surface_prep = function(widget, index) {
if (!widget.tool) {
let tool = widget.tool = new mesh.tool();
let translate = index ? true : false;
tool.index(widget.getGeoVertices({ unroll: true, translate }));
}
};
CAM.surface_find = function(widget, faces, radians) {
CAM.surface_prep(widget);
return widget.tool.findConnectedSurface(faces, radians || 0, 0.0);
};
kiri.worker.cam_surfaces = function(data, send) {
const { settings, index } = data;
const widgets = Object.values(kiri.worker.cache);
for (let widget of widgets) {
if (index) {
widget.setIndexed(true);
widget.setAxisIndex(-index);
} else {
widget.setIndexed(false);
widget.setAxisIndex(0);
}
CAM.surface_prep(widget, index);
}
send.done({});
};
kiri.worker.cam_surface_find = function(data, send) {
const { id, face, radians } = data;
const widget = kiri.worker.cache[id];
const faces = CAM.surface_find(widget, [face], radians);
send.done(faces);
}
kiri.worker.cam_traces = async function(data, send) {
const { settings, single } = data;
const widgets = Object.values(kiri.worker.cache);
const fresh = [];
for (let widget of widgets) {
if (await CAM.traces(settings, widget, single)) {
fresh.push(widget);
}
}
// 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,
} } )));
};
kiri.worker.cam_traces_clear = function(data, send) {
for (let widget of Object.values(kiri.worker.cache)) {
delete widget.traces;
delete widget.topo;
}
send.done({});
};
kiri.worker.cam_holes = async function(data, send) {
const { settings, indiv, rec } = data;
const widgets = Object.values(kiri.worker.cache);
const fresh = [];
for (let [i,widget] of widgets.entries() ) {
if (await CAM.holes(settings, widget, indiv, rec,
( prog, msg )=>{ send.data({progress: (i/widgets.length)+(prog/widgets.length),msg})}
)){
fresh.push(widget);
}
}
// const fresh = widgets.filter(widget => CAM.traces(settings, widget, single));
send.done(kiri.codec.encode(fresh.map(widget => { return {
id: widget.id,
holes: widget.drills,
shadowed:widget.shadowedDrills
} } )));
}
}
});
});

View file

@ -0,0 +1,532 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: geo.base
// dep: geo.polygons
// dep: kiri-mode.cam.driver
gapp.register("kiri-mode.cam.export", (root, exports) => {
const { base, kiri } = root;
const { polygons, util } = base;
const { driver } = kiri;
const { CAM } = driver;
const POLY = polygons;
const PRO = CAM.process;
/**
* @returns {Array} gcode lines
*/
CAM.export = function(print, online) {
const widget = print.widgets[0];
if (!widget) return;
console.log(print);
const { settings } = print;
const { device, tools } = settings;
let i,
time = 0,
lines = 0,
bytes = 0,
opnum = 0,
factor = 1,
output = [],
spindle = 0,
newSpindle = 0,
toolChanges = 0,
spindleMax = device.spindleMax,
origin = settings.origin || { x: 0, y: 0, z: 0 },
space = device.gcodeSpace ? ' ' : '',
isRML = device.gcodeFExt?.toLowerCase() === 'rml',
stripComments = device.gcodeStrip || false,
cmdToolChange = device.gcodeChange || [ "M6 T{tool}" ],
cmdSpindle = device.gcodeSpindle || [ "M3 S{speed}" ],
cmdDwell = device.gcodeDwell || [ "G4 P{time}" ],
axis = { X: 'X', Y: 'Y', Z: 'Z', A: 'A', F: 'F', R: 'R', I: 'I', J: 'J' },
dev = settings.device,
spro = settings.process,
maxZd = spro.camFastFeedZ,
maxXYd = spro.camFastFeed,
decimals = base.config.gcode_decimals || 3,
pos = { x:null, y:null, z:null, a:undefined, f:null, t:null, emit:null },
line,
cidx,
mode,
point,
points = 0,
lastGn = '',
rewrite_comments = false,
compact_output = false,
lasering = false,
laserOp,
stock = settings.stock || { },
ztOff = stock.z - widget.track.top,
bounds = widget.getBoundingBox(),
zmax = stock.z,
runbox = {
max: { x:-Infinity, y:-Infinity, z:-Infinity},
min: { x:Infinity, y:Infinity, z:Infinity}
},
offset = {
x: -origin.x,
y: origin.y,
z: spro.camOriginTop ? origin.z - zmax : origin.z
},
scale = {
x: 1,
y: 1,
z: 1
},
consts = {
box: runbox,
tool: 0,
tool_name: "unknown",
top: (offset ? dev.bedDepth : dev.bedDepth/2),
left: (offset ? 0 : -dev.bedWidth/2),
right: (offset ? dev.bedWidth : dev.bedWidth/2),
bottom: (offset ? 0 : -dev.bedDepth/2),
time_sec: 0,
time_ms: 0,
time: 0
};
function section(section) {
append();
online({ section });
}
function append(line) {
if (line) {
lines++;
bytes += line.length;
if (rewrite_comments) {
const spos = line.indexOf(';');
if (spos >= 0) {
const left = line.substring(0, spos);
const right = '(' + line.substring(spos)
.replace(/; /g,'')
.replace(/;/g,'') + ')';
line = [ left, right ].join('');
}
}
output.append(line);
}
if (!line || output.length > 1000) {
online(output.join("\r\n"));
output = [];
}
}
/**
* Take an array of lines and emit them after:
* - splitting a string into an array
* - stripping comments if !isRML and stripComments
* - applying constant replacements
* - applying Inch/Millimeter conversions if G20 or G21 are found
*
* @param {string|Array} array - an array of lines or a single string
* @param {Object} consts - a dictionary of constants to replace
*/
function filterEmit(array, consts) {
if (!array) {
return;
}
if (typeof(array) === 'string') {
array = array.split('\n');
}
for (i=0; i<array.length; i++) {
line = print.constReplace(array[i], consts);
if (!isRML && stripComments && (cidx = line.indexOf(";")) >= 0) {
line = line.substring(0, cidx).trim();
if (line.length === 0) continue;
}
if (line.indexOf('G20') === 0) {
factor = 1/25.4;
consts.top = (offset ? dev.bedDepth : dev.bedDepth/2) * factor;
consts.left = (offset ? 0 : -dev.bedWidth/2) * factor;
consts.right = (offset ? dev.bedWidth : dev.bedWidth/2) * factor;
consts.bottom = (offset ? 0 : -dev.bedDepth/2) * factor;
} else if (line.indexOf('G21') === 0) {
factor = 1;
}
append(line);
}
}
function add0(val, opt) {
let s = val.toString(),
d = s.indexOf(".");
if (d < 0 && decimals > 0) {
return opt ? s : s + '.0';
} else {
return val.toFixed(decimals);
}
}
/**
* Move to a point, applies post-processing including for laser/RML, and appends to output
* @param {Point} out.point - the point to move to
* @param {Tool} [out.tool] - the tool to move with
* @param {number} [out.speed] - the speed to move at (in mm/min)
* @param {number} [out.emit] - the emission state (0=off, 1=on)
* @param {boolean} [opt.newOp] - whether to force a new operation
* @param {number} [opt.dx] - the delta X change (if not given, uses out.point.x - pos.x)
* @param {number} [opt.dy] - the delta Y change (if not given, uses out.point.y - pos.y)
* @param {number} [opt.dz] - the delta Z change (if not given, uses out.point.z - pos.z)
* @param {number} [opt.time] - the time to move (in ms) (if not given, calculate from speed)
*/
function moveTo(out, opt = {}) {
let laser = out.type === 'laser';
let newpos = out.point;
// no point == dwell
// out.speed = time to dwell in ms
if (!newpos) {
time += out.speed / 60;
consts.time_sec = out.speed / 1000;
consts.time_ms = out.speed;
consts.time = consts.time_sec;
filterEmit(cmdDwell, consts);
return;
}
newpos.x = util.round(newpos.x, decimals);
newpos.y = util.round(newpos.y, decimals);
newpos.z = util.round(newpos.z, decimals);
// on tool change
let changeTool = !(out.tool && pos.t) || out.tool.getID() !== pos.t.getID();
if (changeTool) {
pos.t = out.tool;
consts.tool = pos.t.getNumber();
consts.tool_name = pos.t.getName();
if (!laserOp && (spro.camToolInit || toolChanges > 0)) {
filterEmit(cmdToolChange, { ...consts, spindle: newSpindle } );
}
toolChanges++;
}
// on spindle change (deferred from layer transition so it's post-toolchange)
if (spindleMax && newSpindle && (changeTool || newSpindle !== spindle || opt.newOp)) {
spindle = newSpindle;
if (!laserOp) {
if (spindle > 0) {
let speed = Math.abs(spindle);
filterEmit(cmdSpindle, { speed, spindle: speed, rpm: speed });
} else {
append("M4");
}
}
}
// enforce XY origin at start of print
if (points === 0 || changeTool) {
pos.x = pos.y = pos.z = 0;
}
// split first move to X,Y then Z for that new location
// safety to prevent tool crashing
if (points === 0 || changeTool) {
points++;
if (spro.camFirstZMax) {
moveTo({
// speed: Infinity,
tool: out.tool,
point: { x: pos.x, y: pos.y, z: newpos.z, a: newpos.a },
emit: 0,
}, {
dx: 0, dy: 0, dz: 1, time: 0
});
moveTo({
// speed: Infinity,
tool: out.tool,
point: { x: newpos.x, y: newpos.y, z: newpos.z, a: pos.a },
emit: 1,
}, {
dx: 1, dy: 1, dz: 0, time: 0
});
} else {
moveTo({
// speed: Infinity,
tool: out.tool,
point: { x: newpos.x, y: newpos.y, z: pos.z, a: pos.a },
emit: 0
}, {
dx: 1, dy: 1, dz: 0, time: 0
});
moveTo({
// speed: Infinity,
tool: out.tool,
point: { x: newpos.x, y: newpos.y, z: newpos.z, a: newpos.a },
emit: 1
}, {
dx: 0, dy: 0, dz: 1, time: 0
});
}
points--;
return;
}
let gn;
if ( out.emit >=0 && out.emit <= 3) gn = `G${out.emit}`;
let speed = out.speed,
arc = out.emit == 2 || out.emit == 3,
center = out.center,
nl = (compact_output && lastGn === gn) ? [] : [gn],
dx = opt.dx || newpos.x - pos.x,
dy = opt.dy || newpos.y - pos.y,
dz = opt.dz || newpos.z - pos.z,
da = newpos.a != pos.a,
maxf = dz ? maxZd : maxXYd,
feed = Math.min(speed || maxf, maxf),
dist = Math.sqrt(dx * dx + dy * dy + dz * dz),
newFeed = feed && feed !== pos.f;
// drop dup points (all deltas are 0)
if (!arc && !(dx || dy || dz || da)) {
return;
}
lastGn = gn;
if (dx || newpos.x !== pos.x) {
pos.x = newpos.x;
runbox.min.x = Math.min(runbox.min.x, pos.x);
runbox.max.x = Math.max(runbox.max.x, pos.x);
nl.append(space).append(axis.X).append(add0(consts.pos_x = pos.x * factor));
}
if (dy || newpos.y !== pos.y) {
pos.y = newpos.y;
runbox.min.y = Math.min(runbox.min.y, pos.y);
runbox.max.y = Math.max(runbox.max.y, pos.y);
nl.append(space).append(axis.Y).append(add0(consts.pos_y = pos.y * factor));
}
if (dz || newpos.z !== pos.z) {
pos.z = newpos.z;
runbox.min.z = Math.min(runbox.min.z, pos.z);
runbox.max.z = Math.max(runbox.max.z, pos.z);
nl.append(space).append(axis.Z).append(add0(consts.pos_z = pos.z * factor));
}
if (da) {
pos.a = newpos.a;
nl.append(space).append(axis.A).append(add0(consts.pos_a = pos.a * -factor));
}
if (newFeed) {
pos.f = feed;
nl.append(space).append(axis.F).append(add0(consts.feed = feed * factor, true));
}
if(arc){
nl.append(space).append(axis.I).append(add0(center.x * factor, true))
.append(space).append(axis.J).append(add0(center.y * factor, true));
}
// temp hack to support RML1 dialect from a file extensions trigger
if (isRML) {
if (speed) {
if (newFeed) {
append(`VS${feed};`);
}
nl = [ axis.Z, add0(pos.x * factor), ",", add0(pos.y * factor), ",", add0(pos.z * factor), ";" ];
} else {
nl = [ "PU", add0(pos.x * factor), ",", add0(pos.y * factor), ";" ];
}
}
if (!lasering && laser) {
// enable laser
filterEmit(laserOp.on, consts);
}
// if (laser && pos.emit !== out.emit) {
if (laser) {
nl.append(space).append(`S${add0(out.emit)}`);
}
if (laser) {
pos.emit = out.emit;
}
if (lasering && !laser) {
// disable laser
filterEmit(laserOp.off, consts);
}
// update lasering state
lasering = laser;
// update time calculation
time += opt.time >= 0 ? opt.time : (dist / (pos.f || 1000)) * 60;
consts.time = Math.round(time);
// if (comment && !stripComments) {
// nl.append(" ; ").append(comment);
// nl.append(" ; ").append(points);
// }
append(nl.join(''));
points++;
}
// look for SCALE header directive
let gcodePre = [];
for (let line of device.gcodePre) {
if (line.indexOf(';; SCALE ') === 0) {
try {
let map = JSON.parse(line.substring(9));
if (map.X) scale.x = map.X;
if (map.Y) scale.y = map.Y;
if (map.Z) scale.z = map.Z;
if (map.A) scale.a = map.A;
if (map.DEC !== undefined) decimals = parseInt(map.DEC);
console.log('export scaling applied', map);
} catch (e) {
console.log('malformed scale directive', line);
}
} else if (line.indexOf(';; REWRITE-COMMENTS-PARENS') === 0) {
rewrite_comments = true;
} else if (line.indexOf(';; DECIMALS') === 0) {
decimals = parseInt(line.split('=')[1].trim() || 3) || 3;
} else if (line.indexOf(';; COMPACT-OUTPUT') === 0) {
compact_output = true;
stripComments = true;
space = '';
} else if (line.indexOf(";; AXISMAP ") === 0) {
let axmap = JSON.parse(line.substring(11).trim());
for (let key in axmap) {
axis[key] = axmap[key];
}
} else {
gcodePre.push(line);
}
}
section('header');
if (!stripComments) {
append(`; Generated by Kiri:Moto ${kiri.version}`);
append(`; ${new Date().toString()}`);
filterEmit(["; Bed left:{left} right:{right} top:{top} bottom:{bottom}"], consts);
append(`; Stock X:${stock.x?.round(2)} Y:${stock.y?.round(2)} Z:${stock.z?.round(2)}`);
append(`; Target: ${settings.filter[settings.mode]}`);
append("; --- process ---");
for (let pk in spro) {
if (pk !== "ops" && pk !== "op2") {
append("; " + pk + " = " + spro[pk]);
}
}
}
// collect tool info to add to header
let toolz = {}, ctool;
const isOffset = offset && (offset.x || offset.y);
// remap points as necessary for origins, offsets, inversions
print.output.forEach(layer => {
if (!Array.isArray(layer)) {
return;
}
layer.forEach(out => {
if (out.gcode || out.type === 'lerp') {
return;
}
if (out.tool && out.tool !== ctool) {
ctool = out.tool;
toolz[out.tool] = ctool;
}
point = out.point;
if (!point || point.mod) return;
// ensure not point is modified twice
point.mod = 1;
if (isOffset) {
point.x += offset.x;
point.y += offset.y;
}
if (scale) {
point.x *= scale.x;
point.y *= scale.y;
point.z *= scale.z;
}
if (point.a && scale.a) {
point.a *= scale.a;
}
if (spro.outputInvertX) point.x = -point.x;
if (spro.outputInvertY) point.y = -point.y;
if (spro.camOriginTop) point.z = point.z - zmax;
});
});
if (!stripComments) {
// emit tools used in comments
append("; --- tools ---");
Object.keys(toolz).sort().forEach(tn => {
let { number, flute_diam, flute_len, metric } = toolz[tn].tool;
append(`; tool#=${number} flute=${flute_diam} len=${flute_len} unit=${metric ? 'metric' : 'imperial'}`);
});
}
// emit gcode preamble
filterEmit(gcodePre, consts);
// emit all points in layer/point order
for (let layerout of print.output) {
const newmode = layerout.mode;
if (newmode) {
if (newmode.type === 'laser on') {
laserOp = newmode;
} else if (newmode.type === 'laser off') {
laserOp = undefined;
// force tool change in case laser tool matches next tool
pos.t = undefined;
}
}
const firstOut = layerout[0];
const newOp = newmode && !newmode.silent && mode !== newmode;
if (newOp) {
if (mode && !stripComments) {
append("; ending " + mode.type + " op after " + Math.round(time) + " seconds");
}
mode = newmode;
if (mode) {
section(`op-${opnum++}-${mode.type}`);
}
if (!stripComments && mode) {
append("; starting " + mode.type + " op");
}
}
newSpindle = layerout.spindle;
// iterate over layer output records
layerout.forEach((out, ind) => {
if (out.type === 'lerp') {
// suppress display only lerp points
return;
}
if (out.gcode && Array.isArray(out.gcode)) {
filterEmit(out.gcode, consts);
} else {
moveTo(out, { newOp: ind === 0 ? newOp : false });
}
});
if (lasering && laserOp) {
// disable laser
filterEmit(laserOp.off, consts);
lasering = false;
}
}
if (mode && !stripComments) {
append("; ending " + mode.type + " op after " + Math.round(time) + " seconds");
}
// emit gcode post
section('footer');
filterEmit(device.gcodePost, consts);
// flush buffered gcode
append();
print.time = time;
print.lines = lines;
print.bytes = bytes + lines - 1;
print.bounds = runbox;
};
});

1968
src.old/kiri-mode/cam/ops.js Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,751 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: geo.line
// dep: geo.point
// dep: geo.polygons
// dep: kiri.slice
// dep: kiri-mode.cam.driver
// use: kiri-mode.cam.slicer
// use: kiri-mode.cam.slicer2
// use: kiri-mode.cam.ops
gapp.register("kiri-mode.cam.slice", (root, exports) => {
const { base, kiri } = root;
const { util } = base;
const { driver, newSlice, setSliceTracker } = kiri;
const { polygons, newLine, newPoint, newPolygon } = base;
const { CAM } = driver;
const { OPS } = CAM;
const PRO = CAM.process;
const POLY = polygons;
/**
* DRIVER SLICE CONTRACT
*
* @param {Object} settings
* @param {Widget} widget
* @param {Function} output
*/
CAM.slice = async function(settings, widget, onupdate, ondone) {
let proc = settings.process,
sliceAll = widget.slices = [],
camOps = widget.camops = [],
isIndexed = proc.camStockIndexed;
let stock, bounds, track,
camZTop, camZBottom, camZThru, wztop, ztOff, zbOff,
zBottom, zMin, zMax, zThru, zTop,
minToolDiam, maxToolDiam, dark, color, tabs, unsafe, units,
axisRotation, axisIndex,
part_size,
bottom_gap, bottom_part, bottom_stock, bottom_thru, bottom_z, bottom_cut,
top_stock, top_part, top_gap, top_z,
workarea;
// allow recomputing later if widget or settings changes
const var_compute = () => {
let { camStockX, camStockY, camStockZ, camStockOffset } = proc;
bounds = widget.getBoundingBox();
stock = camStockOffset ? {
x: bounds.dim.x + camStockX,
y: bounds.dim.y + camStockY,
z: bounds.dim.z + camStockZ,
} : {
x: camStockX,
y: camStockY,
z: camStockZ
}
track = widget.track;
({ camZTop, camZBottom, camZThru } = proc);
wztop = track.top;
ztOff = isIndexed ? (stock.z - bounds.dim.z) / 2 : (stock.z - wztop);
zbOff = isIndexed ? (stock.z - bounds.dim.z) / 2 : (wztop - track.box.d);
zBottom = isIndexed ? camZBottom : camZBottom - zbOff;
zMin = isIndexed ? bounds.min.z : Math.max(bounds.min.z, zBottom);
zMax = bounds.max.z;
zThru = camZThru;
zTop = zMax + ztOff;
minToolDiam = Infinity;
maxToolDiam = -Infinity;
dark = !!settings.controller.dark;
color = dark ? 0xbbbbbb : 0;
tabs = widget.anno.tab;
unsafe = proc.camExpertFast;
units = settings.controller.units === 'in' ? 25.4 : 1;
axisRotation = axisIndex = undefined;
part_size = bounds.dim;
bottom_gap = zbOff;
bottom_part = 0;
bottom_stock = -bottom_gap;
bottom_thru = zThru;
bottom_z = isIndexed ? zBottom : Math.max(
(camZBottom ? bottom_stock + camZBottom : bottom_part) - bottom_thru,
(camZBottom ? bottom_stock + camZBottom : bottom_stock - bottom_thru)
);
bottom_cut = Math.max(bottom_z, -zThru);
top_stock = zTop;
top_part = zMax;
top_gap = ztOff;
top_z = camZTop ? bottom_stock + camZTop : top_stock;
workarea = util.round({
top_stock, top_part, top_gap, top_z,
bottom_stock, bottom_part, bottom_gap,
bottom_z, bottom_cut
}, 3);
return structuredClone(workarea);
};
// initial setup
var_compute();
if (tabs) {
// make tab polygons
tabs.forEach(tab => {
let zero = newPoint(0,0,0),
point = newPoint(tab.pos.x, tab.pos.y, tab.pos.z),
poly = newPolygon().centerRectangle(zero, tab.dim.x, tab.dim.y),
[ rx, ry, rz, rw ] = tab.rot,
m4 = new THREE.Matrix4().makeRotationFromQuaternion(
new THREE.Quaternion(rx, ry, rz, rw)
);
poly.points = poly.points
.map(p => new THREE.Vector3(p.x,p.y,p.z).applyMatrix4(m4))
.map(v => newPoint(v.x, v.y, v.z));
poly.move(point);
tab.poly = poly;
// tslice.output().setLayer("tabs", 0xff0000).addPoly(poly);
// sliceAll.push(tslice);
});
}
function error(msg) {
ondone(msg);
}
if (unsafe) {
console.log("disabling overhang safeties");
}
if (!proc.ops || proc.ops.length === 0) {
return error('no processes specified');
}
if (stock.x === 0 || stock.y === 0 || stock.z === 0) {
return error("one or more stock dimensions is zero<br>offset stock or set to non zero value");
}
if (stock.x && stock.y && stock.z && !isIndexed) {
if (stock.x + 0.00001 < bounds.max.x - bounds.min.x) {
return error('stock X too small for part. resize stock or use offset stock');
}
if (stock.y + 0.00001 < bounds.max.y - bounds.min.y) {
return error('stock Y too small for part. resize stock or use offset stock');
}
if (stock.z + 0.00001 < bounds.max.z - bounds.min.z) {
return error('stock Z too small for part. resize stock or use offset stock');
}
}
if (zMin >= bounds.max.z) {
return error(`invalid z bottom ${(zMin/units).round(3)} >= bounds z max ${(zMax/units).round(3)}`);
}
let mark = Date.now();
let opList = [];
let opSum = 0;
let opTot = 0;
let shadows = {};
let slicer;
let state = {
settings,
widget,
bounds,
tabs,
cutTabs,
cutPolys,
contourPolys,
healPolys,
shadowAt,
slicer,
addSlices,
isIndexed,
setAxisIndex,
updateToolDiams,
updateSlicer,
computeShadows,
zBottom,
zThru,
ztOff,
zMax,
zTop,
unsafe,
color,
dark,
ops: opList
};
let tracker = setSliceTracker({ rotation: 0 });
function updateSlicer() {
slicer = state.slicer = new kiri.cam_slicer(widget);
}
async function computeShadows() {
shadows = {};
await new CAM.OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => {
// console.log('reshadow', progress.round(3));
});
}
function updateToolDiams(toolDiam) {
minToolDiam = Math.min(minToolDiam, toolDiam);
maxToolDiam = Math.max(maxToolDiam, toolDiam);
}
function shadowAt(z) {
let cached = shadows[z];
if (cached) {
return cached;
}
// find closest shadow above and use to speed up delta shadow gen
let minZabove;
let zover = Object.keys(shadows).map(v => parseFloat(v)).filter(v => v > z);
for (let zkey of zover) {
if (minZabove && zkey < minZabove) {
minZabove = zkey;
} else {
minZabove = zkey;
}
}
let shadow = CAM.shadowAt(widget, z, minZabove);
if (minZabove) {
// const merge = shadow.length;
// const plus = shadows[minZabove].length;
// const mark = Date.now();
shadow = POLY.union([...shadow, ...shadows[minZabove]], 0.001, true);
// console.log({merge, plus, equals: shadow.length, time: Date.now() - mark});
}
return shadows[z] = POLY.setZ(shadow, z);
}
function setAxisIndex(degrees = 0, absolute = true) {
axisIndex = absolute ? degrees : axisIndex + degrees;
axisRotation = (Math.PI / 180) * axisIndex;
widget.setAxisIndex(isIndexed ? -axisIndex : 0);
return isIndexed ? -axisIndex : 0;
}
function addPolyIndexing(poly, a) {
if (!poly) {
return;
}
if (Array.isArray(poly)) {
for (let p of poly) {
addPolyIndexing(p, a);
}
return;
}
for (let point of poly.points) {
point.a = a;
}
addPolyIndexing(poly.inner, a);
}
function addSlices(slices, addIndexing = isIndexed) {
if (!Array.isArray(slices)) {
slices = [ slices ];
}
sliceAll.appendAll(slices);
if (addIndexing && axisIndex !== undefined) {
// update slice cam lines to add axis indexing
for (let slice of slices.filter(s => s.camLines)) {
addPolyIndexing(slice.camLines, -axisIndex);
}
}
}
if (false) {
opList.push(new CAM.OPS.xray(state, { type: "xray" }));
}
let activeOps = proc.ops.filter(op => !op.disabled);
// silently preface op list with OpShadow
if (isIndexed) {
if (activeOps.length === 0 || activeOps[0].type !== 'index')
opList.push(new CAM.OPS.index(state, { type: "index", index: 0 }));
} else {
opList.push(new CAM.OPS.shadow(state, { type: "shadow", silent: true }));
}
// determing # of steps and step weighting for progress bar
for (let op of activeOps) {
if (op.type === '|') {
break;
}
let opfn = OPS[op.type];
if (opfn) {
let opin = new opfn(state, op);
opList.push(opin);
opTot += opin.weight();
}
}
// call slice() function on all ops in order
setAxisIndex();
updateSlicer();
for (let op of opList) {
let weight = op.weight();
// apply operation override vars
let workover = var_compute();
let valz = op.op;
if (valz.ov_topz) {
workover.top_z = isIndexed ? valz.ov_topz : bottom_stock + valz.ov_topz;
}
if (valz.ov_botz) {
workover.bottom_z = isIndexed ? valz.ov_botz : bottom_stock + valz.ov_botz;
workover.bottom_cut = Math.max(workover.bottom_z, -zThru);
}
// state.workarea = workover;
Object.assign(state, {
zBottom,
zThru,
ztOff,
zMax,
zTop,
workarea: workover
});
// console.log({
// op,
// workover,
// bounds: structuredClone(bounds),
// stock: structuredClone(stock)
// });
await op.slice((progress, message) => {
onupdate((opSum + (progress * weight)) / opTot, message || op.type());
});
// update tracker rotation for next slice output() visualization
tracker.rotation = isIndexed ? axisRotation : 0;
camOps.push(op);
opSum += weight;
}
setSliceTracker();
// reindex
sliceAll.forEach((slice, index) => slice.index = index);
// used in printSetup()
// used in CAM.prepare.getZClearPath()
// add tabs to terrain tops so moves avoid them
if (tabs) {
state.terrain.forEach(slab => {
tabs.forEach(tab => {
if (tab.pos.z + tab.dim.z/2 >= slab.z) {
let all = [...slab.tops, tab.poly];
slab.tops = POLY.union(all, 0, true);
// slab.slice.output()
// .setLayer("debug-tabs", {line: 0x880088, thin: true })
// .addPolys(POLY.setZ(slab.tops.clone(true), slab.z), { thin: true });
}
});
});
}
// add shadow perimeter to terrain to catch outside moves off part
let tabpoly = tabs ? tabs.map(tab => tab.poly) : [];
let allpoly = POLY.union([...state.shadow.base, ...tabpoly], 0, true);
let shadowOff = maxToolDiam < 0 ? allpoly :
POLY.offset(allpoly, [minToolDiam/2,maxToolDiam/2], { count: 2, flat: true, minArea: 0 });
state.terrain.forEach(level => level.tops.appendAll(shadowOff));
widget.terrain = state.skipTerrain ? null : state.terrain;
widget.minToolDiam = minToolDiam;
widget.maxToolDiam = maxToolDiam;
ondone();
};
CAM.addDogbones = function(poly, dist, reverse) {
if (Array.isArray(poly)) {
return poly.forEach(p => CAM.addDogbones(p, dist));
}
let open = poly.open;
let isCW = poly.isClockwise();
if (reverse || poly.parent) isCW = !isCW;
let oldpts = poly.points.slice();
let lastpt = oldpts[oldpts.length - 1];
let lastsl = lastpt.slopeTo(oldpts[0]).toUnit();
let length = oldpts.length + (open ? 0 : 1);
let newpts = [ ];
for (let i=0; i<length; i++) {
let nextpt = oldpts[i % oldpts.length];
let nextsl = lastpt.slopeTo(nextpt).toUnit();
let adiff = lastsl.angleDiff(nextsl, true);
let bdiff = ((adiff < 0 ? (180 - adiff) : (180 + adiff)) / 2) + 180;
if (!open || (i > 1 && i < length)) {
if (isCW && adiff > 45) {
let newa = base.newSlopeFromAngle(lastsl.angle + bdiff);
newpts.push(lastpt.projectOnSlope(newa, dist));
newpts.push(lastpt.clone());
} else if (!isCW && adiff < -45) {
let newa = base.newSlopeFromAngle(lastsl.angle - bdiff);
newpts.push(lastpt.projectOnSlope(newa, dist));
newpts.push(lastpt.clone());
}
}
lastsl = nextsl;
lastpt = nextpt;
if (i < oldpts.length) {
newpts.push(nextpt);
}
}
poly.points = newpts;
if (poly.inner) {
CAM.addDogbones(poly.inner, dist, true);
}
};
CAM.traces = async function(settings, widget, single) {
if (widget.traces) {
return false;
}
// --- points → line segments ---
let edges = new THREE.EdgesGeometry(widget.mesh.geometry, settings.controller.edgeangle ?? 20);
let array = edges.attributes.position.array;
let pcache = {};
let points = new Array(2);
let lines = new Array(points.length / 2);
for (let i=0, j=0, k=0, l=array.length; i<l; ) {
let ps = [ array[i++], array[i++], array[i++] ];
let key = ps.map(v => (v * 10000) | 0).join(',');
let point = pcache[key];
if (!point) {
point = newPoint(ps[0], ps[1], ps[2], key);
point.lines = [];
pcache[key] = point;
}
points[j++ % 2] = point;
if (j % 2 === 0) {
let [ p0, p1 ] = points;
let line = lines[k++] = newLine(p0, p1);
p0.lines.push(line);
p1.lines.push(line);
}
}
// --- segments → chains (bidirectional walk) ---
let chains = [];
{
const segs = lines.slice(); // shallow copy
segs.forEach(s => (s.visited = false));
function step(dirPoint, prevSeg, pushFront, chain) {
let curr = dirPoint;
let prev = prevSeg;
while (curr.lines.length === 2) { // stay inside the arc
const next = curr.lines.find(l => !l.visited && l !== prev);
if (!next) break; // should not happen
next.visited = true;
if (pushFront) chain.unshift(next); // prepend or append?
else chain.push(next);
curr = next.p1 === curr ? next.p2 : next.p1;
prev = next;
}
}
for (const seed of segs) {
if (seed.visited) continue;
seed.visited = true; // always include the seed
const chain = [seed];
// grow backwards from p1 (prepend)
step(seed.p1, seed, true, chain);
// grow forwards from p2 (append)
step(seed.p2, seed, false, chain);
chains.push(chain);
}
}
// --- chains → polylines ---
const polylines = [];
for (const chain of chains) {
const poly = newPolygon().setOpen();
// choose any node whose degree !== 2; if none, it’s a closed loop
let start = null;
for (const s of chain) {
if (s.p1.lines.length !== 2) { start = s.p1; break; }
if (s.p2.lines.length !== 2) { start = s.p2; break; }
}
if (!start) start = chain[0].p1; // closed loop
let curr = start, prevSeg = null;
poly.push(curr); // first point
while (true) {
const nextSeg = curr.lines.find(
l => l !== prevSeg && chain.includes(l)
);
if (!nextSeg) break; // open end reached
curr = nextSeg.p1 === curr ? nextSeg.p2 : nextSeg.p1;
poly.push(curr); // << push *before* test
if (curr === start) break; // loop closed
prevSeg = nextSeg;
}
polylines.push(poly);
}
widget.traces = polylines;
return true;
};
/**
* Generate a list of holes in the model based on the given diameter.
*
* @param {Object} settings - settings object
* @param {Object} widget - widget object
* @param {boolean} individual - if true, drill holes individually
* @param {Object} rec - DrillOp record
* @param {Function} onProgress - callback function to report progress
* @returns {Array} list of hole centers as objects with `x`, `y`, `z`, `depth`, and `selected` properties.
*/
CAM.holes = async function(settings, widget, individual, rec,onProgress) {
let {tool,mark,precision} = rec //TODO: display some visual difference if mark is selected
let toolDiam = new CAM.Tool(settings,tool).fluteDiameter()
let diam = individual ? 1 : toolDiam; // sets default diameter when select individual used
let proc = settings.process,
stock = settings.stock || {},
isIndexed = proc.camStockIndexed,
track = widget.track,
{ camZTop, camZBottom, camZThru } = proc,
// widget top z as defined by setTopz()
wztop = track.top,
// distance between top of part and top of stock
ztOff = isIndexed ? 0 : (stock.z - wztop),
// distance between bottom of part and bottom of stock
zbOff = isIndexed ? 0 : (wztop - track.box.d),
// defined z bottom offset by distance to stock bottom
// keeps the z bottom relative to the part when z align changes
zBottom = isIndexed ? camZBottom : camZBottom - zbOff;
let slicerOpts = {flatoff: 0.001}
let slicer = new kiri.cam_slicer(widget,slicerOpts);
let zFlats = Object.keys(slicer.zFlat).map(Number).map(z=>[z,z-0.002]).flat()
precision = Math.max( 0, precision )
let intervals = (precision == 0) ? [] : slicer.interval(
precision,
{
fit: false, off: -0.01, flats: true
}
)
let zees = [...zFlats,...intervals]
let indices = [...new Set(zees
.map(kv => parseFloat(kv).round(5))
.filter(z => z !== null)
)]
let centerDiff = diam * 0.1,
area = (diam/2) * (diam/2) * Math.PI,
circles = [],
slices = [];
function onEach(slice) {
slices.push(slice);
}
let opts = { each: onEach, progress: (num,total)=> onProgress(num/total*0.5,"slice") };
await slicer.slice(indices, opts);
let shadowedDrills = false;
// console.log("slices",slices)
for (let [i,slice] of slices.entries()) {
for(let top of slice.tops){
// console.log("slicing",slice.z,top)
slice.shadow = CAM.shadowAt(widget, slice.z, 0)
let inner = top.inner;
if (!inner) { //no holes
continue;
}
for (let poly of inner) {
if ( poly.points.length < 7 ) continue;
let center = poly.calcCircleCenter();
center.area = poly.area();
center.overlapping = [center]
center.depth = 0;
// console.log("center",center)
if ( poly.circularity() < 0.985 ){
// if not circular, don't add to holes
continue;
}
if (center.isInPolygon(slice.shadow)) {
// if shadowed, don't add, and inform client
shadowedDrills = true;
continue;
}
let overlap = false;
for (let [i,circle] of circles.entries()) {
let dist = circle.distTo2D(center)
// //if on the same xy point,
if (dist <= centerDiff ) {
// console.log("overlap",center,circle);
circle.overlapping.push(center);
// if overlapping, don't add and continue
overlap = true;
continue;
}
}
if (!overlap) circles.push(center);
}
}
onProgress(0.5+(i/slices.length*0.25),"recognize circles")
}
let drills = []
for (let [i,c] of circles.entries()) {
let overlapping = c.overlapping
let last = overlapping.shift();
while (overlapping.length) {
let circ = overlapping.shift();
let aveArea = (circ.area + last.area) / 2;
let areaDelta = Math.abs(circ.area -last.area)
if (areaDelta < aveArea * 0.05){ // if area delta less than 5% of average area
//keep top circle selected
last.depth = last.z - circ.z;
}else{ // if not the same area
//push and move on
drills.push(last);
last = circ;
}
}
if (last.depth != 0) drills.push(last) //add last circle
onProgress(0.75+(i/circles.length*0.25),"assemble holes")
}
drills.forEach( h=>{
if(rec.fromTop){
// set z top if selected
h.depth += wztop - h.z
h.z = wztop
}
delete h.overlapping //for encoding
h.diam = toolDiam // for mesh generation
h.selected = (!individual && Math.abs(h.area - area) <= area * 0.05 ); //for same size selection
})
// console.log("unfiltered circles",circles)
// console.log("drills",drills)
drills = drills.filter(drill => drill.depth > 0)
widget.shadowedDrills = shadowedDrills
widget.drills = drills;
return drills;
}
function cutTabs(tabs, offset, z, inter) {
tabs = tabs.filter(tab => z < tab.pos.z + tab.dim.z/2).map(tab => tab.off).flat();
return cutPolys(tabs, offset, z, false);
}
function cutPolys(polys, offset, z, inter) {
let noff = [];
offset.forEach(op => noff.appendAll( op.cut(POLY.union(polys, 0, true), inter) ));
return healPolys(noff);
}
function contourPolys(widget, polys) {
const raycaster = new THREE.Raycaster();
raycaster.ray.direction.set(0, 0, -1); // ray pointing down Z
for (let poly of polys) {
for (let point of poly.points) {
raycaster.ray.origin.set(point.x, point.y, 10000);
const intersects = raycaster.intersectObject(widget.mesh, false);
const firstHit = intersects[0] || null;
point.z = firstHit.point.z;
}
}
}
function healPolys(noff) {
for (let p of noff) {
if (p.appearsClosed()) {
p.points.pop();
p.setClosed();
}
}
if (noff.length > 1) {
let heal = 0;
// heal/rejoin open segments that share endpoints
outer: for(;; heal++) {
let ntmp = noff, tlen = ntmp.length;
for (let i=0; i<tlen; i++) {
let s1 = ntmp[i];
if (!s1) continue;
for (let j=i+1; j<tlen; j++) {
let s2 = ntmp[j];
if (!s2) continue;
// require polys at same Z to heal
if (Math.abs(s1.getZ() - s2.getZ()) > 0.01) {
continue;
}
if (!(s1.open && s2.open)) continue;
if (s1.last().isMergable2D(s2.first())) {
s1.addPoints(s2.points.slice(1));
ntmp[j] = null;
continue outer;
}
if (s2.last().isMergable2D(s1.first())) {
s2.addPoints(s1.points.slice(1));
ntmp[i] = null;
continue outer;
}
if (s1.first().isMergable2D(s2.first())) {
s1.reverse();
s1.addPoints(s2.points.slice(1));
ntmp[j] = null;
continue outer;
}
if (s1.last().isMergable2D(s2.last())) {
s2.reverse();
s1.addPoints(s2.points.slice(1));
ntmp[j] = null;
continue outer;
}
}
}
break;
}
if (heal > 0) {
// cull nulls
noff = noff.filter(o => o);
}
// close poly if head meets tail
for (let poly of noff) {
if (poly.open && poly.first().isMergable2D(poly.last())) {
poly.points.pop();
poly.open = false;
}
}
}
return noff;
}
});

View file

@ -0,0 +1,518 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
/**
* Slicing engine used by CAM
*/
// dep: kiri.slice
// dep: moto.broker
gapp.register("kiri-mode.cam.slicer", [], (root, exports) => {
const { base, kiri, moto } = root;
const { config, util, polygons, newOrderedLine, newPoint, newLine } = base;
const { sliceConnect, sliceDedup } = base;
const { newSlice } = kiri;
const POLY = polygons;
const timing = false;
const begin = function() {
if (timing) console.time(...arguments);
};
const end = function() {
if (timing) console.timeEnd(...arguments);
};
class Slicer {
constructor(widget, options = { zlist: true, zline: true, lines: false }) {
this.options = {};
this.setOptions(options);
if (widget) {
this.setPoints(widget.getGeoVertices({ unroll: true, translate: true }));
this.computeFeatures();
}
}
get threaded() {
return this.options.threaded;
}
// zList = generate list of z vertices
// zline = generate list of z vertices with coplanar lines
// trace = find z coplanar trace lines
// flatoff = amount to offset z when slicing on detected flats
// each = call for each slice generated from an interval
setOptions(options) {
Object.assign(this.options, options || {});
return this.options;
}
setPoints(points) {
this.bounds = null;
this.points = points;
this.zFlat = {}; // accumulated flat area at z height
this.zLine = {}; // count of z coplanar lines
this.zList = {}; // count of z values for auto slicing
return this;
}
// gather z-index stats
// these are used for auto-slicing in laser
// and to flats detection in CAM mode
computeFeatures(options) {
begin('compute features');
const opt = this.setOptions(options);
const bounds = this.bounds = new THREE.Box3();
const points = this.points;
const zFlat = this.zFlat;
const zLine = this.zLine;
const zList = this.zList;
function countZ(z) {
z = z.round(5);
zList[z] = (zList[z] || 0) + 1;
}
let p1 = newPoint(0,0,0),
p2 = newPoint(0,0,0),
p3 = newPoint(0,0,0);
// for (let i = 0, il = points.length; i < il; i++) {
// points[i] = points[i].round(5);
// }
for (let i = 0, il = points.length; i < il; ) {
p1.set(points[i++], points[i++], points[i++]);
p2.set(points[i++], points[i++], points[i++]);
p3.set(points[i++], points[i++], points[i++]);
// update bounds
bounds.expandByPoint(p1);
bounds.expandByPoint(p2);
bounds.expandByPoint(p3);
// count occurrences of z values for auto slicing
if (opt.zlist) {
countZ(p1.z);
countZ(p2.z);
countZ(p3.z);
}
// use co-flat and co-line detection to adjust slice Z
if (p1.z === p2.z && p2.z === p3.z) {
// detect zFlat faces to avoid slicing directly on them
let zkey = p1.z.toFixed(5),
area = Math.abs(util.area2(p1,p2,p3)) / 2;
if (!zFlat[zkey]) {
zFlat[zkey] = area;
} else {
zFlat[zkey] += area;
}
} else if (opt.zline) {
// detect zLine (curved region tops/bottoms)
// in cam used for ball and v mill tracing
if (p1.z === p2.z) {
let zkey = p1.z.toFixed(5);
let zval = zLine[zkey];
zLine[zkey] = (zval || 0) + 1;
}
if (p2.z === p3.z) {
let zkey = p2.z.toFixed(5);
let zval = zLine[zkey];
zLine[zkey] = (zval || 0) + 1;
}
if (p3.z === p1.z) {
let zkey = p3.z.toFixed(5);
let zval = zLine[zkey];
zLine[zkey] = (zval || 0) + 1;
}
}
}
end('compute features');
// console.log({ bounds, zFlat, zLine, zList });
return this;
}
/**
* Given an array of z-slice positions, generate a list of polygons
* representing the intersection of the mesh with the given z-slice
* positions.
*
* @param {number[]} zs - array of z-slice positions
* @param {Object} options - slicing parameters
* @param {number} [options.flatoff=0.01] - z offset when slicing on a flat surface
* @param {boolean} [options.progress=false] - callback with slice progress
* @param {function} [options.each] - callback with each slice result
* @returns {Promise<Object[]>} - list of slice objects with z and tops (polygons)
*/
async slice(zs, options) {
const opt = this.setOptions(options);
// slice through points at given Z and return polygons
if (!Array.isArray(zs)) {
throw "slice zs parameter must be ab array";
}
// sort Z and offset when on a flat
const flatoff = util.numOrDefault(opt.flatoff, 0.01);
zs = zs.sort((a,b) => a-b).map(z => {
if (!flatoff) {
return z;
}
const znorm = z.toFixed(5);
return this.zFlat[znorm] ? z + flatoff : z;
});
// compute buckets from z slice list
const { points } = this;
const plen = points.length;
const zlen = zs.length;
const ppz = plen / zlen;
const count = 25;//Math.ceil(ppz / 10000);
const step = Math.ceil(zlen / count);
const buckets = [];
// console.log({ points, zs, count, step });
for (let c = 0, b = 0; c < count; c++) {
if (b >= zlen) break;
buckets.push({
zs: zs.slice(b, b + step),
index: []
});
b += step;
}
let p1 = newPoint(0,0,0),
p2 = newPoint(0,0,0),
p3 = newPoint(0,0,0),
ep = 0.001;
begin("create buckets");
for (let i = 0, il = points.length; i < il; ) {
p1.set(points[i++], points[i++], points[i++]);
p2.set(points[i++], points[i++], points[i++]);
p3.set(points[i++], points[i++], points[i++]);
let zmin = Math.min(p1.z, p2.z, p3.z);
let zmax = Math.max(p1.z, p2.z, p3.z);
for (let bucket of buckets) {
const { zs, index } = bucket;
const min = Math.min(...zs);
const max = Math.max(...zs);
if (zmin < min && zmax < min) {
if (Math.abs(zmin - min) > ep && Math.abs(zmax - min) > ep) {
continue;
}
}
if (zmin > max && zmax > max) {
if (Math.abs(zmin - max) > ep && Math.abs(zmax - max) > ep) {
continue;
}
}
index.push(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z);
}
}
end("create buckets");
// console.log({ zs, zlen, count, step, buckets, flatoff });
begin("slicing");
const { minions } = kiri;
const threaded = minions && minions.running;
const sliceFn = (threaded ? this.sliceBucketMinion : this.sliceBucket).bind(this);
const track = { count: 0, total: zs.length };
if (threaded) minions.broadcast("cam_slice_init", {});
const promises = []
for (let bucket of buckets) {
promises.push(sliceFn(bucket, { ...opt, minZ: this.bounds.min.z }, slice => {
track.count++;
if (opt.progress) {
opt.progress(track.count, track.total);
}
// console.log({ oneach: slice, ...track });
}));
}
const data = (await Promise.all(promises)).flat();
data.sort((a,b) => b.z - a.z).forEach((rec, i) => {
rec.tops = rec.polys;
rec.slice = newSlice(rec.z).addTops(rec.tops);
if (opt.each) {
opt.each(rec, i, data.length);
}
});
if (threaded) minions.broadcast("cam_slice_cleanup");
end("slicing");
return data;
}
async sliceBucketMinion(bucket, opt, oneach) {
const { decode, decodePointArray } = kiri.codec;
const slices = (await new Promise(resolve => {
kiri.minions.queue({
cmd: "cam_slice",
opt: Object.clone(opt),
bucket,
}, resolve);
})).slices;
for (let slice of slices) {
slice.polys = decode(slice.polys);
if (slice.lines) {
slice.lines = decodePointArray(slice.lines)
.group(2)
.map(arr => {
return newLine(arr[0], arr[1])
});
}
oneach(slice);
}
// console.log(bucket, slices);
return slices;
}
async sliceBucket(bucket, opt, oneach) {
const { zs, index } = bucket;
const slices = [];
for (let z of zs) {
const slice = this.sliceZ(z, index, opt);
if (slice) {
slices.push(slice);
}
oneach(slice);
}
return slices;
}
sliceZ(z, indices, opt) {
let links = opt.links !== false,
dedup = opt.dedup !== false,
edges = opt.edges || false,
over = opt.over || z <= opt.minZ,
debug = opt.debug,
phash = {},
lines = [];
// const { points } = this,
const points = indices,
p1 = newPoint(0,0,0),
p2 = newPoint(0,0,0),
p3 = newPoint(0,0,0);
for (let index = 0; index < points.length; ) {
p1.set(points[index++], points[index++], points[index++]);
p2.set(points[index++], points[index++], points[index++]);
p3.set(points[index++], points[index++], points[index++]);
let where = {under: [], over: [], on: []};
checkOverUnderOn(p1, z, where);
checkOverUnderOn(p2, z, where);
checkOverUnderOn(p3, z, where);
if (where.under.length === 3 || where.over.length === 3) {
// does not intersect (all 3 above or below)
} else if (where.on.length === 2) {
// one side of triangle is on the Z plane and 3rd is below
// drop lines with 3rd above because that leads to ambiguities
// with complex nested polygons on flat surface
if ((over && where.over.length === 1) || (!over && where.under.length === 1)) {
lines.push(makeZLine(phash, where.on[0], where.on[1], false, true));
}
} else if (where.on.length === 3) {
// triangle is coplanar with Z
// we drop these because this face is attached to 3 others
// that will satisfy the if above (line) with 2 points
} else if (where.under.length === 0 || where.over.length === 0) {
// does not intersect but one point is on the slice Z plane
} else if (!edges) {
// compute two point intersections and construct line
let line = intersectPoints(where.over, where.under, z);
if (line.length < 2 && where.on.length === 1) {
line.push(where.on[0].clone());
}
if (line.length === 2) {
lines.push(makeZLine(phash, line[0], line[1]));
} else {
console.log({msg: "invalid ips", line: line, where: where});
}
}
}
if (dedup) {
// console.log({ z, dedup: lines, points });
lines = sliceDedup(lines, debug);
}
return lines.length ? { z,
lines: opt.lines !== false ? lines : undefined,
polys: links ? POLY.nest(sliceConnect(lines, opt, debug)) : undefined
} : null;
}
interval(step, options) {
let opt = options || {},
bounds = this.bounds,
boff = opt.boff || opt.off || 0, // bottom offset
toff = opt.toff || opt.off || 0, // top offset
zmin = opt.min !== undefined ? opt.min : bounds.min.z + boff,
zmax = (opt.max || bounds.max.z) - toff,
steps = (zmax - zmin) / step,
rem = steps % 1 != 0 ? 0 : 1,
count = Math.floor(steps) + rem,
array = [];
if (opt.fit) {
count++;
step = (zmax - zmin) / count;
}
if (opt.down) {
for (let i=0; i<count; i++) {
array.push(zmax);
zmax -= step;
}
} else {
for (let i=0; i<count; i++) {
array.push(zmin);
zmin += step;
}
}
if (opt.fit) {
array.push(opt.down ? zmax : zmin);
}
if (opt.flats && opt.off) {
let add = [];
Object.keys(this.zFlat).forEach(z => {
z = parseFloat(z);
add.push(z + opt.off);
if (z > zmin) {
add.push(z - opt.off);
}
});
// add over and under all flats by 'off'
array.appendAll(add).sort((a,b) => {
return opt.down ? b-a : a-b;
});
}
// filter duplicate values
array = array.map(v => v.round(5)).filter((e,i,a) => i < 1 || a[i-1] !== a[i]);
// return array.map(v => Math.abs(parseFloat(v.toFixed(5))));
return array.map(v => parseFloat(v.toFixed(5)));
}
}
/**
* given a point, append to the correct
* 'where' objec tarray (on, over or under)
*
* @param {Point} p
* @param {number} z offset
* @param {Obejct} where
*/
function checkOverUnderOn(p, z, where) {
let delta = p.z - z;
if (Math.abs(delta) < config.precision_slice_z) { // on
where.on.push(p);
} else if (delta < 0) { // under
where.under.push(p);
} else { // over
where.over.push(p);
}
}
/**
* Given a point over and under a z offset, calculate
* and return the intersection point on that z plane
*
* @param {Point} over
* @param {Point} under
* @param {number} z offset
* @returns {Point} intersection point
*/
function intersectPoints(over, under, z) {
let ip = [];
for (let i = 0; i < over.length; i++) {
for (let j = 0; j < under.length; j++) {
ip.push(over[i].intersectZ(under[j], z));
}
}
return ip;
}
/**
* Ensure points are unique with a cache/key algorithm
*/
function getCachedPoint(phash, p) {
let cached = phash[p.key];
if (!cached) {
phash[p.key] = p;
return p;
}
return cached;
}
/**
* Given two points and hints about their edges,
* return a new Line object with points sorted
* lexicographically by key. This allows for future
* line de-duplication and joins.
*
* @param {Object} phash
* @param {Point} p1
* @param {Point} p2
* @param {boolean} [coplanar]
* @param {boolean} [edge]
* @returns {Line}
*/
function makeZLine(phash, p1, p2, coplanar, edge) {
p1 = getCachedPoint(phash, p1.clone());
p2 = getCachedPoint(phash, p2.clone());
let line = newOrderedLine(p1,p2);
line.coplanar = coplanar || false;
line.edge = edge || false;
return line;
}
Slicer.checkOverUnderOn = checkOverUnderOn;
Slicer.intersectPoints = intersectPoints;
kiri.cam_slicer = Slicer;
moto.broker.subscribe("minion.started", msg => {
const { funcs, cache, reply, log } = msg;
funcs.cam_slice_init = () => {
cache.slicer = new Slicer();
};
funcs.cam_slice_cleanup = () => {
delete cache.slicer;
};
funcs.cam_slice = (data, seq) => {
const { bucket, opt } = data;
const { encode, encodePointArray } = kiri.codec;
// log({ slice: bucket, opt });
cache.slicer.sliceBucket(bucket, opt, slice => {
// console.log({ slice });
}).then(data => {
data.forEach(rec => {
rec.polys = encode(rec.polys);
if (rec.lines) {
const points = rec.lines.map(l => [l.p1, l.p2]).flat();
rec.lines = encodePointArray(points);
}
});
reply({ seq, slices: data });
});
};
});
});

View file

@ -0,0 +1,326 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
/**
* Slicing engine used by CAM Topo
*/
gapp.register("kiri-mode.cam.slicer2", [], (root, exports) => {
const { base, kiri } = root;
const { config, newOrderedLine, newPoint } = base;
class Slicer {
static intersectPoints = intersectPoints;
static checkOverUnderOn = checkOverUnderOn;
static removeDuplicateLines = removeDuplicateLines;
constructor(index = 0) {
this.min = Infinity;
this.max = -Infinity;
this.index = index;
}
// position is a non-indexed 3js geometry position attribute array
// create Point array from optionally filtered Z range
setFromArray(position, options = {}) {
const min = options.min || -Infinity;
const max = options.max || Infinity;
const len = position.length;
const pts = this.points = [];
for (let i=0; i<len; ) {
const x0 = position[i++];
const y0 = position[i++];
const z0 = position[i++];
const x1 = position[i++];
const y1 = position[i++];
const z1 = position[i++];
const x2 = position[i++];
const y2 = position[i++];
const z2 = position[i++];
const minz = Math.min(z0, z1, z2);
const maxz = Math.max(z0, z1, z2);
if (maxz < min || minz > max) {
continue;
}
pts.push(
newPoint(x0, y0, z0),
newPoint(x1, y1, z1),
newPoint(x2, y2, z2)
);
this.min = options.min || Math.min(this.min, minz);
this.max = options.max || Math.max(this.max, maxz);
}
return this;
}
setFromPoints(points, options = {}) {
const pts = this.points = [];
const len = points.length;
const min = options.min || -Infinity;
const max = options.max || Infinity;
for (let i=0; i<len; ) {
const p0 = points[i++];
const p1 = points[i++];
const p2 = points[i++];
const minz = Math.min(p0.z, p1.z, p2.z);
const maxz = Math.max(p0.z, p1.z, p2.z);
if (maxz < min || minz > max) {
continue;
}
pts.push(p0, p1, p2);
this.min = options.min || Math.min(this.min, minz);
this.max = options.max || Math.max(this.max, maxz);
}
return this;
}
slice(interval = 1) {
const ret = [];
for (let z = this.min; z < this.max; z += interval) {
ret.push({ z, index: this.index++, lines: this.sliceZ(z) });
}
return ret;
}
sliceZ(z, options = {}) {
const points = this.points;
const zMin = this.min;
const zMax = this.max;
const phash = {};
const lines = [];
let { under, over, both } = options;
let p1, p2, p3;
// default to 'over' selection with 2 points on a line
if (!under && !both) over = true;
// iterate over matching buckets for this z offset
for (let i = 0; i < points.length; ) {
p1 = points[i++];
p2 = points[i++];
p3 = points[i++];
let where = {under: [], over: [], on: []};
checkOverUnderOn(p1, z, where);
checkOverUnderOn(p2, z, where);
checkOverUnderOn(p3, z, where);
if (where.under.length === 3 || where.over.length === 3) {
// does not intersect (all 3 above or below)
} else if (where.on.length === 2) {
// one side of triangle is on the Z plane and 3rd is below
// drop lines with 3rd above because that leads to ambiguities
// with complex nested polygons on flat surface
let add2 = both ||
(over && (where.over.length === 1 || z === zMax)) ||
(under && (where.under.length === 1 || z === zMin));
if (add2) {
lines.push(makeZLine(phash, where.on[0], where.on[1], false, true));
}
} else if (where.on.length === 3) {
// triangle is coplanar with Z
// we drop these because this face is attached to 3 others
// that will satisfy the if above (line) with 2 points
} else if (where.under.length === 0 || where.over.length === 0) {
// does not intersect but one point is on the slice Z plane
} else {
// compute two point intersections and construct line
let line = intersectPoints(where.over, where.under, z);
if (line.length < 2 && where.on.length === 1) {
line.push(where.on[0]);
}
if (line.length === 2) {
lines.push(makeZLine(phash, line[0], line[1]));
} else {
console.log({msg: "invalid ips", line: line, where: where});
}
}
}
if (lines.length == 0 && options.noEmpty) {
return;
}
return removeDuplicateLines(lines);
}
}
/**
* given a point, append to the correct
* 'where' objec tarray (on, over or under)
*
* @param {Point} p
* @param {number} z offset
* @param {Obejct} where
*/
function checkOverUnderOn(p, z, where) {
let delta = p.z - z;
if (Math.abs(delta) < config.precision_slice_z) { // on
where.on.push(p);
} else if (delta < 0) { // under
where.under.push(p);
} else { // over
where.over.push(p);
}
}
/**
* Given a point over and under a z offset, calculate
* and return the intersection point on that z plane
*
* @param {Point} over
* @param {Point} under
* @param {number} z offset
* @returns {Point} intersection point
*/
function intersectPoints(over, under, z) {
let ip = [];
for (let i = 0; i < over.length; i++) {
for (let j = 0; j < under.length; j++) {
ip.push(over[i].intersectZ(under[j], z));
}
}
return ip;
}
/**
* Ensure points are unique with a cache/key algorithm
*/
function getCachedPoint(phash, p) {
let cached = phash[p.key];
if (!cached) {
phash[p.key] = p;
return p;
}
return cached;
}
/**
* Given two points and hints about their edges,
* return a new Line object with points sorted
* lexicographically by key. This allows for future
* line de-duplication and joins.
*
* @param {Object} phash
* @param {Point} p1
* @param {Point} p2
* @param {boolean} [coplanar]
* @param {boolean} [edge]
* @returns {Line}
*/
function makeZLine(phash, p1, p2, coplanar, edge) {
p1 = getCachedPoint(phash, p1);
p2 = getCachedPoint(phash, p2);
let line = newOrderedLine(p1,p2);
line.coplanar = coplanar || false;
line.edge = edge || false;
return line;
}
/**
* eliminate duplicate lines and interior-only lines (coplanar)
*
* lines are sorted using lexicographic point keys such that
* they are comparable even if their points are reversed. hinting
* for deletion, co-planar and suspect shared edge is detectable at
* this time.
*
* @param {Line[]} lines
* @returns {Line[]}
*/
function removeDuplicateLines(lines, debug) {
let output = [],
tmplines = [],
points = [],
pmap = {};
function cachePoint(p) {
let cp = pmap[p.key];
if (cp) return cp;
points.push(p);
pmap[p.key] = p;
return p;
}
function addLinesToPoint(point, line) {
cachePoint(point);
if (!point.group) point.group = [ line ];
else point.group.push(line);
}
// mark duplicates for deletion preserving edges
lines.sort(function (l1, l2) {
if (l1.key === l2.key) {
l1.del = !l1.edge;
l2.del = !l2.edge;
if (debug && (l1.del || l2.del)) {
console.log('dup', l1, l2);
}
return 0;
}
return l1.key < l2.key ? -1 : 1;
});
// associate points with their lines, cull deleted
lines.forEach(function(line) {
if (!line.del) {
tmplines.push(line);
addLinesToPoint(line.p1, line);
addLinesToPoint(line.p2, line);
}
});
// merge collinear lines
points.forEach(function(point) {
if (point.group.length != 2) return;
let l1 = point.group[0],
l2 = point.group[1];
if (l1.isCollinear(l2)) {
l1.del = true;
l2.del = true;
// find new endpoints that are not shared point
let p1 = l1.p1 != point ? l1.p1 : l1.p2,
p2 = l2.p1 != point ? l2.p1 : l2.p2,
newline = base.newOrderedLine(p1,p2);
// remove deleted lines from associated points
p1.group.remove(l1);
p1.group.remove(l2);
p2.group.remove(l1);
p2.group.remove(l2);
// associate new line with points
p1.group.push(newline);
p2.group.push(newline);
// add new line to lines array
newline.edge = l1.edge || l2.edge;
tmplines.push(newline);
}
});
// mark duplicates for deletion
// but preserve one if it's an edge
tmplines.sort(function (l1, l2) {
if (l1.key === l2.key) {
l1.del = true;
l2.del = !l2.edge;
return 0;
}
return l1.key < l2.key ? -1 : 1;
});
// create new line array culling deleted
tmplines.forEach(function(line) {
if (!line.del) {
output.push(line);
line.p1.group = null;
line.p2.group = null;
}
});
return output;
}
kiri.topo_slicer = Slicer;
});

View file

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

View file

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

View file

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

View file

@ -0,0 +1,536 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: geo.base
// dep: geo.line
// dep: geo.point
// dep: geo.polygon
// dep: geo.polygons
// dep: moto.broker
// dep: kiri.slice
// dep: kiri-mode.cam.driver
// dep: kiri-mode.cam.slicer2
gapp.register("kiri-mode.cam.topo", [], (root, exports) => {
const { base, kiri, moto } = root;
const { driver, newSlice } = kiri;
const { CAM } = driver;
const { polygons, newPoint, newPolygon, sliceConnect } = base;
const PRO = CAM.process;
const POLY = polygons;
const RAD2DEG = 180 / Math.PI;
const DEG2RAD = Math.PI / 180;
function scale(fn, factor = 1, base = 0) {
return (value, msg) => {
fn(base + value * factor, msg);
}
}
class Topo4 {
constructor() { }
async generate(opt = {}) {
let { state, op, onupdate, ondone } = opt;
let { widget, settings, tabs } = opt.state;
let { controller, process } = settings;
let axis = op.axis.toLowerCase(),
tool = new CAM.Tool(settings, op.tool),
bounds = widget.getBoundingBox().clone(),
density = parseInt(controller.animesh || 100) * 2500,
{ min, max } = bounds,
span = {
x: max.x - min.x,
y: max.y - min.y
},
contour = {
x: axis === "x",
y: axis === "y"
},
zMin = min.z + 0.0001,
tolerance = op.tolerance,
resolution = tolerance ? tolerance : 1 / Math.sqrt(density / (span.x * span.y)),
step = this.step = (tool.traceOffset() * 2) * op.step,
angle = this.angle = op.angle || 1;
if (tool.isTaperMill() && step === 0) {
step = this.step = op.step * tool.unitScale();
}
if (tolerance === 0) {
console.log(widget.id, 'topo4 auto tolerance', resolution.round(4));
}
this.zBottom = state.zBottom;
this.resolution = resolution;
this.vertices = widget.getGeoVertices({ unroll: true, translate: true });
this.tabverts = widget.getTabVertices();
this.tool = tool.generateProfile(resolution).profile;
this.maxo = tool.profileDim.maxo * resolution;
this.diam = tool.fluteDiameter();
this.zoff = widget.track.top || 0;
this.leave = op.leave || 0;
this.linear = op.linear || false;
this.lineColor = state.settings.controller.dark ? 0xffff00 : 0x555500;
onupdate(0, "lathe");
const range = this.range = { min: Infinity, max: -Infinity };
const slices = this.sliced = await this.slice(scale(onupdate, 0.25, 0));
for (let slice of slices) {
range.min = Math.min(range.min, slice.z);
range.max = Math.max(range.max, slice.z);
slice.output()
.setLayer("lathe", { line: 0x888888 })
.addPolys(slice.topPolys());
}
const lathe = await this.lathe(scale(onupdate, 0.75, 0.25));
onupdate(1, "lathe");
ondone(lathe);
// ondone([...slices, ...lathe]);
return this;
}
async slice(onupdate) {
const { vertices, resolution, tabverts, zoff } = this;
const range = this.range = { min: Infinity, max: -Infinity };
const box = this.box = new THREE.Box2();
// swap XZ in shared array
for (let i=0, l=vertices.length; i<l; i += 3) {
const x = vertices[i];
const z = vertices[i + 2] + zoff;
vertices[i] = z;
vertices[i + 2] = x;
range.min = Math.min(range.min, x);
range.max = Math.max(range.max, x);
}
// add tool radius to slice min/max range ot fully carve part
range.min -= this.diam / 2;
range.max += this.diam / 2;
// merge in tab vertices here so they don't affect slice range / dimensions
for (let i=0, l=tabverts.length; i<l; i += 3) {
const x = tabverts[i];
const z = tabverts[i + 2] + zoff;
tabverts[i] = z;
tabverts[i + 2] = x;
}
// re-create shared vertex array for workers
this.vertices = [].appendAll(vertices).appendAll(tabverts).toFloat32().toShared();
const shards = Math.ceil(Math.min(25, vertices.length / 27000));
const step = (range.max - range.min) / shards;
let index = 0;
let slices = this.slices = [];
let slice = { min: range.min, max: range.min + step, index };
for (let z = range.min; z < range.max; z += resolution) {
if (z > slice.max) {
slices.push(slice);
slice = { min: z, max: z + step, index };
}
index++;
}
slices.push(slice);
// console.log({ shards, range, step, slices });
if (kiri.minions.running > 1) {
return await this.sliceMinions(onupdate);
} else {
return await this.sliceWorker(onupdate);
}
}
async sliceWorker(onupdate) {
const { vertices, slices, resolution } = this;
const { codec } = kiri;
let output = [];
let complete = 0;
for (let slice of slices) {
const recs = new kiri.topo_slicer(slice.index)
.setFromArray(vertices, slice)
.slice(resolution)
.map(rec => {
const slice = kiri.newSlice(rec.z);
for (let line of rec.lines) {
const { p1, p2 } = line;
if (!p1.swapped) { p1.swapXZ(); p1.swapped = true }
if (!p2.swapped) { p2.swapXZ(); p2.swapped = true }
}
slice.index = rec.index;
slice.addTops(sliceConnect(rec.lines));
const points = codec.encodePointArray(rec.lines.map(l => [ l.p1, l.p2 ]).flat());
const shared = new Float32Array(new SharedArrayBuffer(points.length * 4));
shared.set(points);
slice.shared = shared;
return slice;
});
output.appendAll(recs);
onupdate(++complete /slices.length);
}
return output;
}
async sliceMinions(onupdate) {
const { codec } = kiri;
const { queue, putCache, clearCache } = this;
const { vertices, slices, resolution } = this;
putCache("vertices", vertices);
let complete = 0;
let promises = slices.map(slice => {
return queue("topo4_slice", {
resolution,
slice
}).then(data => {
onupdate(++complete / slices.length);
return data;
});
});
// merge boxes for all rasters for contouring clipping
const output = codec.decode(await Promise.all(promises))
.map(rec => rec.recs)
.flat()
.map(rec => newSlice(rec.z)
.addTops(rec.polys)
.setFields({ shared: rec.shared }))
.sort((a,b) => a.z - b.z);
clearCache();
return output;
}
async lathe(onupdate) {
if (kiri.minions.running > 1) {
return await this.latheMinions(onupdate);
} else {
return await this.latheWorker(onupdate);
}
}
lathePath(slices, tool) {
const { resolution, step, zBottom, maxo } = this;
const tlen = tool.length;
const slen = slices.length;
const sinc = Math.max(1, Math.ceil(step / resolution));
const heights = [];
// cull slice lines to only the ones in range (5x faster)
const oslices = [];
for (let slice of slices) {
const lines = slice.lines;
const plen = lines.length;
const rec = { z: slice.z, lines: [] };
for (let i = 0; i < plen; ) {
++i; // skip x which should match slice.z
let py0 = lines[i++];
const pz0 = lines[i++];
++i; // skip x which should match slice.z
let py1 = lines[i++];
const pz1 = lines[i++];
if ((py0 < -maxo && py1 < -maxo) || (py0 > maxo && py1 > maxo)) {
continue;
}
rec.lines.push(0, py0, pz0, 0, py1, pz1);
}
oslices.push(rec);
}
// iterate over all slices (real x = z)
// find max real z using z ray intersect from tool point to slice lines + offset
let lz = 0;
for (let si = 0; si < slen; si += sinc) {
const rx = oslices[si].z;
let mz = -Infinity;
// iterate over tool offsets
for (let ti = 0; ti < tlen; ) {
// tool offset in grid units from present x (si)
const xo = tool[ti++]; // x grid offset (slice)
const yo = tool[ti++] * resolution; // y grid offset (mult rez to get real y)
const zo = tool[ti++]; // real z delta offset
// get slice index corresponding with offset
const ts = si + xo;
// outside of slice array, skip
if (ts < 0 || ts >= slen - 1) {
continue;
}
const slice = oslices[ts];
const lines = slice.lines;
const plen = lines.length;
for (let i = 0; i < plen; ) {
++i; // skip x which should match slice.z
let py0 = lines[i++];
const pz0 = lines[i++];
++i; // skip x which should match slice.z
let py1 = lines[i++];
const pz1 = lines[i++];
if ((py0 <= yo && py1 >= yo) || (py1 <= yo && py0 >= yo)) {
const dz = pz1 - pz0;
const dy = Math.abs(py1 - py0);
if (dy === 0) continue;
const fr = Math.abs(yo - py0) / dy;
const lz = pz0 + dz * fr + zo;
// check z height
mz = Math.max(mz, lz);
}
}
if (mz === -Infinity && xo === 0 && yo === 0) {
// tool tip is off the model
continue;
}
}
if (mz === -Infinity) {
mz = zBottom; // Math.min(0, lz)
} else if (zBottom && mz < zBottom) {
mz = zBottom;
}
heights.push(rx, 0, lz = mz);
}
return heights;
}
async latheWorker(onupdate) {
const { sliced, tool, zoff, leave } = this;
const rota = this.angle * DEG2RAD;
const steps = (Math.PI * 2) / rota;
const axis = new THREE.Vector3(1, 0, 0);
const mrot = new THREE.Matrix4().makeRotationAxis(axis, rota);
const slices = sliced.map(s => { return { z: s.z, lines: s.shared } });
const paths = [];
const recs = [];
let angle = 0;
let count = 0;
// for each step angle, find Z spine heights, produce record
while (count++ < steps) {
const heights = this.lathePath(slices, tool, paths);
recs.push({ angle, heights, degrees: angle * RAD2DEG });
angle -= rota;
for (let lines of slices.map(s => s.lines)) {
rotatePoints(lines, mrot);
}
onupdate(count / steps);
}
count = recs[0].heights.length / 3;
while (count-- > 0) {
let slice = newSlice(count);
slice.camLines = [ newPolygon().setOpen() ];
paths.push(slice);
}
for (let rec of recs) {
const { degrees, heights } = rec;
[...heights].group(3).forEach((a,i) => {
// progress each path 360 degrees to prevent A rolling backwards
paths[i].camLines[0].push( newPoint(a[0], a[1], a[2] + leave).setA(degrees + i * -360) );
});
}
for (let slice of paths) {
const poly = slice.camLines[0];
if (!poly.length) {
console.log('empty', slice);
continue;
}
// repeat first point 360 degrees progressed
const repeat = poly.points[0];
slice.camLines[0].push(repeat.clone().setA(repeat.a - 360));
slice.output()
.setLayer("lathe", { line: this.lineColor })
.addPoly(poly.clone().applyRotations().move({ z: -zoff, x:0, y:0 }));
}
// console.log({ tool, slices, paths });
return paths;
}
async latheMinions(onupdate) {
const { sliced, tool, zoff, leave, maxo, zBottom, step, resolution, linear } = this;
const { putCache, clearCache, queue } = this;
const rota = this.angle * DEG2RAD;
const steps = (Math.PI * 2) / rota;
const slices = sliced.map(s => { return { z: s.z, lines: s.shared } });
const paths = [];
const recs = [];
putCache("lathe", {
maxo,
tool,
step,
slices,
zBottom,
resolution,
});
let done = 0;
let tangle = 0;
let count = 0;
let promises = [];
// for each step angle, find Z spine heights, produce record
while (count++ < steps) {
const angle = tangle;
let p = new Promise(resolve => {
queue("topo4_lathe", { angle }).then(data => {
// console.log({ angle, data });
recs.push({ angle, heights: data.heights, degrees: angle * RAD2DEG });
onupdate(++done / steps);
resolve();
});
});
// await p;
promises.push(p);
tangle -= rota;
}
await Promise.all(promises);
recs.sort((a, b) => { return b.angle - a.angle });
// let linear = true;
count = linear ? recs.length : recs[0].heights.length / 3;
while (count-- > 0) {
let slice = newSlice(count);
slice.camLines = [ newPolygon().setOpen() ];
paths.push(slice);
}
if (linear) {
recs.forEach((rec,i) => {
const { degrees, heights } = rec;
[...heights].group(3).forEach((a) => {
paths[i].camLines[0].push( newPoint(a[0], a[1], a[2] + leave).setA(degrees) );
});
if (i % 2 === 1) {
paths[i].camLines[0].reverse();
}
});
} else {
for (let rec of recs) {
const { degrees, heights } = rec;
[...heights].group(3).forEach((a,i) => {
// progress each path 360 degrees to prevent A rolling backwards
paths[i].camLines[0].push( newPoint(a[0], a[1], a[2] + leave).setA(degrees + i * -360) );
});
}
}
for (let slice of paths) {
const poly = slice.camLines[0];
if (!poly.length) {
console.log('empty', slice);
continue;
}
if (!linear) {
// repeat first point 360 degrees progressed
const repeat = poly.points[0];
slice.camLines[0].push(repeat.clone().setA(repeat.a - 360));
}
slice.output()
.setLayer("lathe", { line: this.lineColor })
.addPoly(poly.clone().applyRotations().move({ z: -zoff, x:0, y:0 }));
}
// console.log({ tool, slices, paths });
clearCache();
return paths;
}
putCache(key, data) {
kiri.worker.putCache({ key, data }, { done: data => { } });
}
clearCache() {
kiri.worker.clearCache({}, { done: data => { } });
}
queue(cmd, params) {
return new Promise(resolve => {
kiri.minions.queue({ cmd, ...params }, resolve);
});
}
}
function rotatePoints(lines, rot) {
new THREE.BufferAttribute(lines, 3).applyMatrix4(rot);
}
CAM.Topo4 = function(opt) {
return new Topo4().generate(opt);
};
moto.broker.subscribe("minion.started", msg => {
const { funcs, cache, reply, log } = msg;
const { codec } = kiri;
funcs.topo4_slice = (data, seq) => {
const { slice, resolution } = data;
const vertices = cache.vertices;
const recs = new kiri.topo_slicer(slice.index)
.setFromArray(vertices, slice)
.slice(resolution)
.map(rec => {
const { z, index, lines } = rec;
for (let line of lines) {
const { p1, p2 } = line;
if (!p1.swapped) { p1.swapXZ(); p1.swapped = true }
if (!p2.swapped) { p2.swapXZ(); p2.swapped = true }
}
const points = codec.encodePointArray(lines.map(l => [ l.p1, l.p2 ]).flat());
const shared = new Float32Array(new SharedArrayBuffer(points.length * 4));
shared.set(points);
return {
z, index, shared,
polys: codec.encode(sliceConnect(lines)),
};
});
// only pass back bounds of rasters to be merged
reply({ seq, recs });
};
funcs.topo4_lathe = (data, seq) => {
const { angle } = data;
const { slices, tool } = cache.lathe;
const axis = new THREE.Vector3(1, 0, 0);
const mrot = new THREE.Matrix4().makeRotationAxis(axis, -angle);
const stmp = slices.map(s => {
const lines = s.lines.slice();
rotatePoints(lines, mrot);
return { z: s.z, lines }
});
const topo4 = Object.assign(new Topo4(), cache.lathe);
const heights = topo4.lathePath(stmp, tool);
reply({ seq, heights });
}
});
});

View file

@ -0,0 +1,14 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: kiri-mode.laser.driver
gapp.register("kiri-mode.drag.driver", [], (root, exports) => {
const DRIVERS = root.kiri.driver;
const { LASER } = DRIVERS;
const { TYPE } = LASER;
DRIVERS.DRAG = Object.assign({}, LASER, { name: "DragKnife", type: TYPE.DRAG });
});

View file

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

View file

@ -0,0 +1,85 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
gapp.register("kiri-mode.fdm.driver", [], (root, exports) => {
const { base, kiri } = root;
const { driver } = kiri;
const { util } = base;
const FDM = driver.FDM = { getRangeParameters, extrudePerMM, extrudeMM };
// shared by client and worker contexts
function getRangeParameters(process, index) {
if (index === undefined || index === null || index < 0) {
return process;
}
let ranges = process.ranges;
if (!(ranges && ranges.length)) {
return process;
}
let params = Object.clone(process);
for (let range of ranges) {
if (index >= range.lo && index <= range.hi) {
for (let [key, value] of Object.entries(range.fields)) {
params[key] = value;
params._range = true;
}
}
}
return params;
}
// noz = nozzle diameter
// fil = filament diameter
// slice = slice height
function extrudePerMM(noz, fil, slice) {
return ((Math.PI * util.sqr(noz / 2)) / (Math.PI * util.sqr(fil / 2))) * (slice / noz);
};
// dist = distance between extrusion points
// perMM = amount extruded per MM (from extrudePerMM)
// factor = scaling factor (usually 1.0)
function extrudeMM(dist, perMM, factor) {
return dist * perMM * factor;
}
// defer loading until client and worker exist
kiri.load(api => {
const { client, worker } = kiri;
if (client) {
FDM.support_generate = function(ondone) {
client.clear();
client.sync();
const settings = api.conf.get();
const widgets = api.widgets.map();
client.send("fdm_support_generate", { settings }, (gen) => {
if (gen && gen.error) {
api.show.alert('support generation canceled');
return ondone([]);
}
for (let g of gen) {
g.widget = widgets[g.id];
}
ondone(gen);
});
};
}
if (worker) {
worker.fdm_support_generate = function(data, send) {
const { settings } = data;
const widgets = Object.values(worker.cache);
const fresh = widgets.filter(widget => FDM.supports(settings, widget));
send.done(kiri.codec.encode(fresh.map(widget => { return {
id: widget.id,
supports: widget.supports,
} } )));
};
}
});
});

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,132 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: kiri-mode.laser.driver
gapp.register("kiri-mode.wedm.driver", [], (root, exports) => {
const { kiri, moto } = root;
const DRIVERS = kiri.driver;
const { CAM, LASER } = DRIVERS;
const { TYPE } = LASER;
const WEDM = DRIVERS.WEDM = Object.assign({}, LASER, {
name: "WireEDM",
type: TYPE.WEDM,
init,
prepare
});
const DEG2RAD = Math.PI / 180;
const state = {
api: undefined,
alert: undefined,
radians: 0,
surfaces: [],
selecting: false
};
function faceAdd() {
if (state.selecting) return faceDone();
const { api, radians, surfaces } = state;
CAM.surface_prep(radians, () => {
api.hide.alert(alert);
state.alert = api.show.alert("[esc] cancels surface selection");
for (let [wid, arr] of Object.entries(surfaces)) {
let widget = api.widgets.forid(wid);
if (widget && arr.length)
for (let faceid of arr) {
CAM.surface_toggle(widget, faceid, radians, faceids => {
// surfaces[widget.id] = faceids;
});
}
}
api.feature.on_mouse_up = (obj, ev) => {
let { face } = obj;
let min = Math.min(face.a, face.b, face.c);
let faceid = min / 3;
let widget = obj.object.widget;
CAM.surface_toggle(widget, faceid, radians, faceids => {
surfaces[widget.id] = faceids;
});
};
state.selecting = true;
});
}
function faceDone() {
if (!state.selecting) return;
const { api, alert, surfaces } = state;
for (let wid of Object.keys(surfaces)) {
let widget = api.widgets.forid(wid);
if (widget) {
CAM.surface_clear(widget);
} else {
delete surfaces[wid];
}
}
api.hide.alert(alert);
api.feature.on_mouse_up = undefined;
api.ui.faceAdd.classList.remove('editing')
state.selecting = false;
}
function init(kiri, api, driver) {
LASER.init(kiri, api, driver);
state.api = api;
api.event.on("key.esc", () => {
if (state.selecting) {
faceDone();
}
});
// api.event.on("mode.set", (mode) => { });
// api.event.on("view.set", (mode) => { });
api.event.on("preview.view", view => {
const process = api.conf.get().process;
if (!process.ctOriginBounds) return;
const bounds = new THREE.Box3().expandByObject(view);
api.platform.set_bounds(new THREE.Box3(
new THREE.Vector3(bounds.min.x, bounds.min.z, bounds.min.y),
new THREE.Vector3(bounds.max.x, bounds.max.z, bounds.max.y),
));
});
// Surface Selection Buttons
api.event.on("button.click", target => {
let process = api.conf.get().process;
switch (target) {
case api.ui.faceAdd:
target.classList.add('editing');
return faceAdd();
case api.ui.faceDun:
return faceDone();
case api.ui.faceClr:
api.uc.confirm("clear surface selection?").then(ok => {
// todo
});
break;
}
});
};
async function prepare(widgets, settings, update) {
// let device = settings.device,
// process = settings.process,
// print = self.worker.print = kiri.newPrint(settings, widgets),
// output = print.output = [];
// filter ignored widgets
widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled);
LASER.prepare(widgets, settings, update);
}
});

View file

@ -0,0 +1,14 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: kiri-mode.laser.driver
gapp.register("kiri-mode.wjet.driver", [], (root, exports) => {
const DRIVERS = root.kiri.driver;
const { LASER } = DRIVERS;
const { TYPE } = LASER;
DRIVERS.WJET = Object.assign({}, LASER, { name: 'WaterJet', type: TYPE.WJET });
});