move src2 -> src, web2 -> web, remove old module system, fix deps
This commit is contained in:
parent
1d821d4e24
commit
ce4f5d6e78
488 changed files with 26179 additions and 26609 deletions
648
src.old/kiri-mode/cam/animate.js
Normal file
648
src.old/kiri-mode/cam/animate.js
Normal 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 }});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
670
src.old/kiri-mode/cam/animate2.js
Normal file
670
src.old/kiri-mode/cam/animate2.js
Normal 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 }});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
2306
src.old/kiri-mode/cam/client.js
Normal file
2306
src.old/kiri-mode/cam/client.js
Normal file
File diff suppressed because it is too large
Load diff
188
src.old/kiri-mode/cam/driver.js
Normal file
188
src.old/kiri-mode/cam/driver.js
Normal 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
|
||||
} } )));
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
532
src.old/kiri-mode/cam/export.js
Normal file
532
src.old/kiri-mode/cam/export.js
Normal 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
1968
src.old/kiri-mode/cam/ops.js
Normal file
File diff suppressed because it is too large
Load diff
1120
src.old/kiri-mode/cam/prepare.js
Normal file
1120
src.old/kiri-mode/cam/prepare.js
Normal file
File diff suppressed because it is too large
Load diff
751
src.old/kiri-mode/cam/slice.js
Normal file
751
src.old/kiri-mode/cam/slice.js
Normal 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;
|
||||
}
|
||||
|
||||
});
|
||||
518
src.old/kiri-mode/cam/slicer.js
Normal file
518
src.old/kiri-mode/cam/slicer.js
Normal 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 });
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
326
src.old/kiri-mode/cam/slicer2.js
Normal file
326
src.old/kiri-mode/cam/slicer2.js
Normal 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;
|
||||
|
||||
});
|
||||
214
src.old/kiri-mode/cam/tool.js
Normal file
214
src.old/kiri-mode/cam/tool.js
Normal 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();
|
||||
};
|
||||
|
||||
});
|
||||
353
src.old/kiri-mode/cam/tools.js
Normal file
353
src.old/kiri-mode/cam/tools.js
Normal 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();
|
||||
}
|
||||
|
||||
});
|
||||
899
src.old/kiri-mode/cam/topo.js
Normal file
899
src.old/kiri-mode/cam/topo.js
Normal 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;
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
536
src.old/kiri-mode/cam/topo4.js
Normal file
536
src.old/kiri-mode/cam/topo4.js
Normal 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 });
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
14
src.old/kiri-mode/drag/driver.js
Normal file
14
src.old/kiri-mode/drag/driver.js
Normal 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 });
|
||||
|
||||
});
|
||||
|
||||
681
src.old/kiri-mode/fdm/client.js
Normal file
681
src.old/kiri-mode/fdm/client.js
Normal 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;
|
||||
|
||||
});
|
||||
85
src.old/kiri-mode/fdm/driver.js
Normal file
85
src.old/kiri-mode/fdm/driver.js
Normal 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,
|
||||
} } )));
|
||||
};
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
999
src.old/kiri-mode/fdm/export.js
Normal file
999
src.old/kiri-mode/fdm/export.js
Normal 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);
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
327
src.old/kiri-mode/fdm/fill.js
Normal file
327
src.old/kiri-mode/fdm/fill.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
323
src.old/kiri-mode/fdm/post.js
Normal file
323
src.old/kiri-mode/fdm/post.js
Normal 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 : [];
|
||||
}
|
||||
|
||||
});
|
||||
1516
src.old/kiri-mode/fdm/prepare.js
Normal file
1516
src.old/kiri-mode/fdm/prepare.js
Normal file
File diff suppressed because it is too large
Load diff
1946
src.old/kiri-mode/fdm/slice.js
Normal file
1946
src.old/kiri-mode/fdm/slice.js
Normal file
File diff suppressed because it is too large
Load diff
866
src.old/kiri-mode/laser/driver.js
Normal file
866
src.old/kiri-mode/laser/driver.js
Normal 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');
|
||||
};
|
||||
|
||||
});
|
||||
101
src.old/kiri-mode/sla/client.js
Normal file
101
src.old/kiri-mode/sla/client.js
Normal 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();
|
||||
}
|
||||
|
||||
});
|
||||
48
src.old/kiri-mode/sla/driver.js
Normal file
48
src.old/kiri-mode/sla/driver.js
Normal 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);
|
||||
}
|
||||
|
||||
});
|
||||
174
src.old/kiri-mode/sla/export.js
Normal file
174
src.old/kiri-mode/sla/export.js
Normal 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});
|
||||
}
|
||||
|
||||
});
|
||||
706
src.old/kiri-mode/sla/slice.js
Normal file
706
src.old/kiri-mode/sla/slice.js
Normal 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};
|
||||
}
|
||||
|
||||
});
|
||||
455
src.old/kiri-mode/sla/x_cxdlp.js
Normal file
455
src.old/kiri-mode/sla/x_cxdlp.js
Normal 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;
|
||||
}
|
||||
|
||||
});
|
||||
543
src.old/kiri-mode/sla/x_photon.js
Normal file
543
src.old/kiri-mode/sla/x_photon.js
Normal 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
|
||||
};
|
||||
|
||||
});
|
||||
132
src.old/kiri-mode/wedm/driver.js
Normal file
132
src.old/kiri-mode/wedm/driver.js
Normal 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);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
14
src.old/kiri-mode/wjet/driver.js
Normal file
14
src.old/kiri-mode/wjet/driver.js
Normal 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 });
|
||||
|
||||
});
|
||||
|
||||
Loading…
Reference in a new issue