Merge branch 'master' of https://github.com/GridSpace/grid-apps into drill-optimization

This commit is contained in:
bakedpotatolord 2025-11-25 15:56:11 -07:00
commit efb41e14fa
128 changed files with 8990 additions and 4345 deletions

View file

@ -5,38 +5,37 @@
import { CSG } from '../../../geo/csg.js';
import { Tool } from './tool.js';
let nextMeshID = 1;
let stock, center, rez;
let path, pathIndex, tool, tools, last;
let stockZ;
let stockIndexMsg = false;
let stockSlices;
let stockIndex;
let startTime;
let toolID = -1;
let toolMesh;
let toolRadius;
let toolUpdateMsg;
let animateClear = false;
let animating = false;
let moveDist = 0;
let renderDist = 0;
let renderDone = false;
let renderPause = 10;
let renderSteps = 0;
let renderSpeed = 0;
let indexCount = 0;
let updates = 0;
let nextMeshID = 1,
stock,
path,
pathIndex,
tool,
tools,
last,
stockZ,
stockIndexMsg = false,
stockSlices,
stockIndex,
startTime,
toolID = -1,
toolMesh,
toolRadius,
toolUpdateMsg,
animateClear = false,
animating = false,
renderDone = false,
renderSteps = 0,
renderSpeed = 0,
indexCount = 0,
updates = 0,
maxR;
export function init(worker) {
const { dispatch } = worker;
dispatch.animate_setup2 = function (data, send) {
const { settings } = data;
const { process } = settings;
const { controller, process } = settings;
const print = worker.current.print;
const density = parseInt(settings.controller.animesh) * 1000;
const isIndexed = process.camStockIndexed;
@ -45,8 +44,6 @@ export function init(worker) {
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;
@ -56,25 +53,27 @@ export function init(worker) {
startTime = 0;
updates = 0;
stockZ = isIndexed ? 0 : stock.z;
maxR = Math.hypot(stock.y, stock.z);
stockSlices = [];
const { x, y, z } = stock;
const sliceCount = parseInt(settings.controller.animesh || 2000) / 100;
const sliceWidth = stock.x / sliceCount;
if (controller.manifold)
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.data({ mesh_move: { id: slice.id, pos: { x:0, y:0, z: stock.z/2} } });
}
send.done();
};
dispatch.animate2 = function (data, send) {
renderPause = data.pause || renderPause;
renderSpeed = data.speed || 0;
if (animating) {
return send.done();
@ -126,7 +125,6 @@ function 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
@ -143,44 +141,50 @@ function renderPath(send) {
}
const id = toolID;
const rezstep = rez * 2;
const rezstep = Math.min(tool.maxDiameter(), 1) / 4;
// console.log({ rezstep });
if (last) {
// console.log(last.point, next.point);
const lp = last.point, np = next.point;
last = next;
// render move line on client
send.data({ line: [ lp, np ]});
// 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;
}
da = (np.a ?? 0) - (lp.a ?? 0), // delta rotation degrees
AD = maxR * 2, // arc diameter @ max z
dr = (Math.abs(da) / 360) * AD, // delta rotation arc
dist = Math.max(Math.hypot(dx, dy, dz), dr); // motion distance
// console.log({ dz, da, AD, dr, dist });
// sub-divide long moves for animation and better boolean ops
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 st = Math.ceil(md / rezstep); // steps
const mx = dx / st, my = dy / st, mz = dz / st, ma = da / st;
const sd = Math.sqrt(mx * mx + my * my + mz * mz + ma * ma);
const sp = next.speed;
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 });
for (let i = 0, x = lp.x, y = lp.y, z = lp.z, a = lp.a; i < st; i++) {
moves.push({ x, y, z, a, md: sd, dx, dy, dz, da, sp });
x += mx;
y += my;
z += mz;
a += ma;
}
moveDist = 0;
moves.push({ ...next.point, md: sd });
// console.log({ dist: md, steps: st, speed: next.speed, moves });
// push last point
moves.push({ ...next.point, md: sd, sp });
// console.log(...moves);
renderMoves(id, moves, send);
} else {
last = next;
@ -197,26 +201,25 @@ function renderMoves(id, moves, send, seed = 0) {
if (!pos) {
throw `no pos @ ${index} of ${moves.length}`;
}
const { dx, dy, dz } = pos;
const { dx, dy, dz, da, sp, md } = pos;
toolMove(pos);
// console.log('renderMoves', {id, moves, seed});
let subs = 0;
if (dx || dy || dz < 0)
if (dx || dy || dz < 0 || da)
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;
if (renderSpeed) {
let time = sp ? (md / sp) * 60000 : 10;
// console.log({ time, ...pos });
renderUpdate(send);
setTimeout(() => {
renderMoves(id, moves, send, index + 1);
}, renderPause);
}, time / renderSpeed);
return;
}
}

View file

@ -10,29 +10,35 @@ const { client } = api;
let meshes = {},
button = {},
label = {},
lineTracker,
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;
color = 0,
dark = false,
manifold = false,
A2R = Math.PI / 180;
export function animate_clear2(api) {
let { anim } = api.ui;
client.animate_cleanup2();
lineTracker?.clear();
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";
client.animate_cleanup2();
}
export function animate2(api, delay) {
let alert = api.alerts.show("building animation");
let settings = api.conf.get();
dark = settings.controller.dark;
manifold = settings.controller.manifold;
client.animate_setup2(settings, data => {
handleUpdate(data);
if (data) {
@ -48,7 +54,8 @@ export function animate2(api, delay) {
speed: anim.speed,
trans: anim.trans,
model: anim.model,
shade: anim.shade
shade: anim.shade,
path: anim.path
});
Object.assign(label, {
progress: anim.progress,
@ -59,6 +66,14 @@ export function animate2(api, delay) {
a: anim.vala
});
if (manifold) {
anim.trans.classList.remove('hide');
// anim.shade.classList.remove('hide');
} else {
anim.trans.classList.add('hide');
// anim.shade.classList.add('hide');
}
updateSpeed(0);
setTimeout(step, delay || 0);
toggleTrans(undefined, false);
@ -72,6 +87,7 @@ export function animate2(api, delay) {
button.trans.onclick = toggleTrans;
button.model.onclick = toggleModel;
button.shade.onclick = toggleStock;
button.path.onclick = togglePath;
button.play.style.display = '';
button.pause.style.display = 'none';
@ -86,8 +102,9 @@ Object.assign(client, {
},
animate_setup2(settings, ondone) {
color = settings.controller.dark ? 0x888888 : 0;
material = new THREE.MeshPhongMaterial({
initPathMesh();
color = dark ? 0x888888 : 0;
material = material ?? new THREE.MeshPhongMaterial({
flatShading: true,
transparent: false,
opacity: 0.5,
@ -102,6 +119,73 @@ Object.assign(client, {
}
});
function initPathMesh() {
if (lineTracker) {
return lineTracker.clear();
}
let count = 0,
max = 10000,
rot = new THREE.Euler(0,0,0),
geo = new THREE.BufferGeometry(),
mat = new THREE.LineBasicMaterial({ color: dark ? 0xffff00 : 0x771100 }),
pos = new Float32Array(max * 6),
lines = new THREE.LineSegments(geo, mat),
vec = new THREE.Vector3(0,0,0),
ang = new THREE.Vector3(1,0,0),
show = api.local.get('cam.anim.path') ?? true;
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
let track = lineTracker = {
xro(p) {
vec.set(p.x, p.y, p.z);
vec.applyAxisAngle(ang, p.a * A2R);
},
add(p1, p2) {
let ind = count * 6;
track.xro(p1);
pos[ind+0] = vec.x;
pos[ind+1] = vec.y;
pos[ind+2] = vec.z;
track.xro(p2);
pos[ind+3] = vec.x;
pos[ind+4] = vec.y;
pos[ind+5] = vec.z;
count++;
if (count >= max) {
let opos = pos;
max += 10000;
pos = new Float32Array(max * 6),
pos.set(opos);
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
}
geo.setDrawRange(0, count * 2);
geo.attributes.position.needsUpdate = true;
track.visible(true);
},
clear() {
count = 0;
geo.setDrawRange(0,0);
track.rotate(0);
track.visible(false);
},
rotate(angle) {
rot.x = angle * A2R;
lines.setRotationFromEuler(rot);
},
visible(bool) {
if (show && bool !== lines.visible) {
lines.visible = bool;
} else if (!show) {
lines.visible = false;
}
},
show(bool) {
show = bool;
lines.visible = bool;
}
}
space.world.add(lines);
}
function meshAdd(id, ind, pos, ilen, plen) {
const geo = new THREE.BufferGeometry();
const pa = plen ? pos.subarray(0, plen * 3) : pos;
@ -132,6 +216,7 @@ function meshUpdate(id, ind, pos, ilen, plen) {
function deleteMesh(id) {
space.world.remove(meshes[id]);
meshes[id].geometry.dispose();
delete meshes[id];
}
@ -141,8 +226,15 @@ function toggleModel(ev, bool) {
}
function toggleStock(ev, bool, set) {
set !== false && api.local.toggle('cam.anim.stock', bool);
return api.event.emit('cam.stock.toggle', bool ?? undefined);
bool = api.local.toggle('cam.anim.stock', bool);
for (let [ id, mesh ] of Object.entries(meshes)) {
if (id >= 0) mesh.visible = bool;
}
}
function togglePath(ev, bool, set) {
bool = api.local.toggle('cam.anim.path', bool);
lineTracker?.show(bool);
}
function toggleTrans(ev, bool) {
@ -165,8 +257,7 @@ function play(opts) {
}
client.animate2({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
steps: steps || Infinity
}, handleUpdate);
}
@ -177,8 +268,7 @@ function fast(opts) {
button.pause.style.display = '';
client.animate2({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
steps: steps || Infinity
}, handleUpdate);
}
@ -216,6 +306,7 @@ function handleUpdate(data) {
if (data.stock_index !== undefined) {
api.widgets.setAxisIndex(data.stock_index);
label.a.value = -data.stock_index.toFixed(1);
lineTracker.rotate(-data.stock_index);
}
if (data.mesh_index) {
const { id, index } = data.mesh_index;
@ -229,9 +320,12 @@ function handleUpdate(data) {
const { id, ind, pos, ilen, plen } = data.mesh_update;
meshUpdate(id, ind, pos, ilen, plen);
}
if (data && data.progress) {
if (data.progress) {
label.progress.value = (data.progress * 100).toFixed(1);
}
if (data.line) {
lineTracker.add(...data.line);
}
}
function updateSpeed(inc = 0) {

View file

@ -9,6 +9,7 @@ import { selectHoles } from './cl-hole.js';
import { selectHelical } from './cl-helical.js';
import { surfaceAdd } from './cl-surface.js';
import { traceAdd } from './cl-trace.js';
import { opRender } from './client.js';
const { MODES } = api.const;
const { uc: UC } = api;
@ -206,7 +207,7 @@ export function createPopOps() {
sep: UC.newBlank({ class: "pop-sep" }),
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: UC.toInt, show: hasSpindle }),
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: UC.toFloat, bound: UC.bound(0.01, 1.0) }),
stepz: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: UC.toFloat, bound: UC.bound(0, 100.0) }),
stepz: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: UC.toFloat, units: true, bound: UC.bound(0, 100.0) }),
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: UC.toInt, units: true }),
down: UC.newInput(LANG.cc_loff_s, { title: LANG.cc_loff_l, convert: UC.toFloat, units: true }),
inset: UC.newInput(LANG.cc_lxyo_s, { title: LANG.cc_lxyo_l, convert: UC.toFloat, units: true, show: () => !env.popOp.level.rec.stock }),
@ -282,7 +283,7 @@ export function createPopOps() {
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: UC.toInt, units: true }),
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: UC.toFloat, units: true }),
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: UC.toFloat, bound: UC.bound(0.01, 1.0), show: () => env.popOp.outline.rec.wide }),
steps: UC.newInput(LANG.cc_sovc_s, { title: LANG.cc_sovc_l, convert: UC.toInt, bound: UC.bound(1, 5), show: () => env.popOp.outline.rec.wide }),
steps: UC.newInput(LANG.cc_sovc_s, { title: LANG.cc_sovc_l, convert: UC.toInt, bound: UC.bound(1, 500), show: () => env.popOp.outline.rec.wide }),
sep: UC.newBlank({ class: "pop-sep" }),
top: UC.newBoolean(LANG.co_clrt_s, undefined, { title: LANG.co_clrt_l }),
inside: UC.newBoolean(LANG.co_olin_s, undefined, { title: LANG.co_olin_l, show: (op) => { return !op.inputs.outside.checked } }),
@ -349,7 +350,9 @@ export function createPopOps() {
tolerance: 'camTolerance',
filter: 'camContourFilter',
leave: 'camContourLeave',
linear: 'camLatheLinear'
linear: 'camLatheLinear',
offStart: 'camLatheOffStart',
offEnd: 'camLatheOffEnd'
}).inputs = {
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
// axis: UC.newSelect(LANG.cd_axis, {}, "xyaxis"),
@ -360,6 +363,9 @@ export function createPopOps() {
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: UC.toFloat, bound: UC.bound(0.01, 100.0) }),
angle: UC.newInput(LANG.cc_sang_s, { title: LANG.cc_sang_l, convert: UC.toFloat, bound: UC.bound(0.01, 180.0) }),
sep: UC.newBlank({ class: "pop-sep" }),
offStart: UC.newInput(LANG.ci_laso_s, { title: LANG.ci_laso_l, convert: UC.toFloat}),
offEnd: UC.newInput(LANG.ci_laeo_s, { title: LANG.ci_laeo_l, convert: UC.toFloat}),
sep: UC.newBlank({ class: "pop-sep" }),
tolerance: UC.newInput(LANG.ou_toll_s, { title: LANG.ou_toll_l, convert: UC.toFloat, bound: UC.bound(0, 10.0), units: true, round: 4 }),
leave: UC.newInput(LANG.cf_leav_s, { title: LANG.cf_leav_l, convert: UC.toFloat, bound: UC.bound(0, 100) }),
sep: UC.newBlank({ class: "pop-sep" }),

View file

@ -140,8 +140,8 @@ function pointHoverUp(int, ev) {
x += stock.x / 2;
y += stock.y / 2;
}
process.camOriginOffX = x;
process.camOriginOffY = y;
process.camOriginOffX = x - stock.center.x;
process.camOriginOffY = y - stock.center.y;
process.camOriginOffZ = z;
api.settings.update_fields(process);
api.platform.update_origin();

View file

@ -144,7 +144,7 @@ export function traceHoverUp(int, ev) {
export function traceToggle(obj, skip) {
let material = obj.material[0] || obj.material;
if (!material) return;
if (!(material && obj.trace)) return;
let { color, colorSave } = material;
let { widget, poly } = obj.trace;
let areas = env.poppedRec.areas;

View file

@ -10,7 +10,7 @@ import { Layers } from '../../core/layers.js';
import { Stack } from '../../core/stack.js';
import { updateStock } from './cl-stock.js';
import { createPopOps } from './cl-ops.js';
import { tabAdd, tabDone, tabClear, restoreTabs, rotateTabs, updateTabs, clearTabs } from './cl-tab.js';
import { tabAdd, tabDone, tabClear, restoreTabs, rotateTabs, updateTabs, clearTabs, mirrorTabs } from './cl-tab.js';
import { traceOn, traceDone, unselectTraces } from './cl-trace.js';
import { holeSelOn, selectHolesDone, clearHolesRec } from './cl-hole.js';
import { surfaceOn, surfaceDone } from './cl-surface.js';
@ -29,6 +29,7 @@ const { widgets: WIDGETS } = api;
class Client {
animVer = 0;
camStock;
showStock = true;
current;
currentIndex;
flipping;
@ -320,6 +321,7 @@ export function opRender() {
el.classList.add("disabled");
}
bounds.push(el);
let parent = $('camops');
let timer = null;
let inside = true;
let popped = false;
@ -329,14 +331,14 @@ export function opRender() {
}
el.rec = rec;
el.unpop = () => {
let pos = [...el.childNodes].indexOf(poprec.div);
let pos = [...parent.childNodes].indexOf(poprec.div);
if (pos >= 0) {
el.removeChild(poprec.div);
parent.removeChild(poprec.div);
}
popped = false;
};
function onEnter(ev) {
if ((surfaceOn || traceOn) && env.poppedRec != rec) {
if ((clock || surfaceOn || traceOn) && env.poppedRec != rec) {
return;
}
if (popped && env.poppedRec != rec) {
@ -351,22 +353,24 @@ export function opRender() {
popped = true;
poprec.use(rec);
env.hoveredOp = el;
if (!clock) {
// offset Y position of pop div by % of Y screen location of button
el.appendChild(poprec.div);
poprec.addNote();
const { innerHeight } = window;
const brect = ev.target.getBoundingClientRect();
const prect = poprec.div.getBoundingClientRect();
const pcty = (brect.top / innerHeight) * 0.9;
const offpx = -pcty * prect.height;
poprec.div.style.transform = `translateY(${offpx}px)`;
}
// offset Y position of pop div by % of Y screen location of button
parent.appendChild(poprec.div);
poprec.addNote();
const { innerHeight } = window;
const brect = ev.target.getBoundingClientRect();
const prect = parent.getBoundingClientRect();
const Prect = poprec.div.getBoundingClientRect();
const tdiff = prect.top - brect.top;
const botoff = innerHeight - (brect.top + Prect.height);
const offpx = -tdiff + (botoff < 0 ? botoff : -Prect.height/3);
poprec.div.style.transform = `translateY(${offpx}px)`;
poprec.div.onmouseenter = () => { inside = true };
poprec.div.onmouseleave = onLeave;
// option click event appears latent
// and overides the sticky settings
setTimeout(() => {
UC.setSticky(false);
}, 0);
}, 10);
}
function onLeave(ev) {
inside = false;
@ -637,7 +641,8 @@ export function init() {
});
api.event.on("cam.stock.toggle", (bool) => {
env.camStock && (env.camStock.visible = bool ?? !env.camStock.visible);
env.showStock = bool ?? !env.showStock;
updateStock();
});
api.event.on("boolean.click", api.platform.update_bounds);

View file

@ -12,10 +12,8 @@ export function cam_export(print, online) {
const widget = print.widgets[0];
if (!widget) return;
console.log({ cam_export: print });
const { settings } = print;
const { device, tools } = settings;
const { device } = settings;
let i,
time = 0,
@ -257,17 +255,18 @@ export function cam_export(print, online) {
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,
let 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,
gv = (dz >= 0 && out.emit >= 0 && out.emit <= 3) ? out.emit : 1,
gn = `G${gv}`,
speed = out.speed,
arc = out.emit == 2 || out.emit == 3,
center = out.center,
nl = (compact_output && lastGn === gn) ? [] : [gn],
maxf = (dz < 0 ? maxZd : maxXYd) || speed,
feed = Math.min(speed || maxf, maxf),
dist = Math.sqrt(dx * dx + dy * dy + dz * dz),
newFeed = feed && feed !== pos.f;
@ -424,7 +423,9 @@ export function cam_export(print, online) {
toolz[out.tool] = ctool;
}
point = out.point;
if (!point || point.mod) return;
if (!point || point.mod) {
return;
}
// ensure not point is modified twice
point.mod = 1;
if (isOffset) {

View file

@ -58,6 +58,7 @@ export function menu() {
anim.trans = newButton(null,null,{icon:'<i class="fa-solid fa-border-none"></i>',title:"transparency",class:"padleft"}),
anim.model = newButton(null,null,{icon:'<i class="fa-solid fa-eye"></i>',title:"show model"}),
anim.shade = newButton(null,null,{icon:'<i class="fa-solid fa-cube"></i>',title:"stock box"}),
anim.path = newButton(null,null,{icon:'<i class="fa-solid fa-draw-polygon"></i>',title:"path"}),
])
},
@ -105,7 +106,7 @@ export function menu() {
separator: newBlank({ class:"set-sep", driven }),
camEaseAngle: newInput(LANG.ou_eang_s, {title:LANG.ou_eang_l, convert:toFloat, bound:bound(0.1,85), show:() => ui.camEaseDown.checked}),
camFullEngage: newInput(LANG.ou_feng_s, {title:LANG.ou_feng_l, convert:toFloat, bound:bound(0.1,1.0)}),
_____: newGroup(LANG.co_menu, $('cam-origin'), { modes:CAM, driven, separator }),
_____: newGroup(LANG.or_menu, $('cam-origin'), { modes:CAM, driven, separator }),
camOriginTop: newBoolean(LANG.or_topp_s, onBooleanClick, {title:LANG.or_topp_l}),
camOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, {title:LANG.or_cntr_l}),
separator: newBlank({ class:"set-sep", driven }),

View file

@ -4,26 +4,70 @@ import { CamOp } from './op.js';
import { Tool } from './tool.js';
import { generate as Topo } from './topo3.js';
import { newPoint } from '../../../geo/point.js';
import { newPolygon } from '../../../geo/polygon.js';
import { tip2tipEmit } from '../../../geo/paths.js';
function createFilter(op) {
function createFilter(op, origin, axis) {
// console.log({ origin, axis });
let ok = () => true;
let filter = slices => slices;
let filterString = op.filter?.map(l => l.trim()).join('\n');
if (filterString) {
try {
const obj = eval(`( ${filterString} )`);
let idx = 0;
if (obj && obj.slices) {
const nadd = [];
filter = function (slices) {
for (let slice of slices) {
if (obj.slices(slice, idx++)) {
nadd.push(slice);
let box = obj?.box;
let slice_fn = obj?.slices;
let index = 0;
const accept = [];
filter = function (slices) {
for (let slice of slices) {
if (slice_fn && slice_fn(slice, index++)) {
accept.push(slice);
} else if (box) {
// slice.z = x when axis = y
// slice.z = y when axis = x
let { x, y, z } = box;
x = x ?? [ -Infinity, Infinity ];
y = y ?? [ -Infinity, Infinity ];
z = z ?? [ -Infinity, Infinity ];
let ok = false;
if (axis === 'x') {
let sy = slice.z + origin.y;
if (sy >= y[0] && sy <= y[1]) {
ok = true;
for (let p of slice.camLines) {
p.points = p.points.filter(p =>
p.x - origin.x >= x[0] && p.x - origin.x <= x[1] &&
p.z - origin.z >= z[0] && p.z - origin.z <= z[1]
);
}
}
} else {
let sx = slice.z - origin.x;
if (sx >= x[0] && sx <= x[1]) {
ok = true;
for (let p of slice.camLines) {
p.points = p.points.filter(p =>
p.y + origin.y >= y[0] && p.y + origin.y <= y[1] &&
p.z - origin.z >= z[0] && p.z - origin.z <= z[1]
);
}
}
}
if (ok) {
slice.camLines = slice.camLines.map(p => {
if (p.points.length > 1) {
return newPolygon(p.points).setOpen(true);
} else {
return undefined;
}
}).filter(p => p);
accept.push(slice);
}
}
return nadd;
};
}
}
return accept;
};
} catch (e) {
console.log('filter parse error', e, op.filter);
}
@ -38,8 +82,8 @@ class OpContour extends CamOp {
async slice(progress) {
let { op, state } = this;
let { addSlices, settings, updateToolDiams } = state;
let filter = createFilter(op);
let { color, addSlices, settings, updateToolDiams } = state;
let filter = createFilter(op, settings.origin, op.axis.toLowerCase());
let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter();
updateToolDiams(toolDiam);
// we need topo for safe travel moves when roughing and outlining
@ -54,6 +98,11 @@ class OpContour extends CamOp {
slices = filter(slices);
this.sliceOut = slices;
addSlices(slices);
for (let slice of slices) {
slice.output()
.setLayer(`contour ${op.axis}`, { face: color, line: color })
.addPolys(slice.camLines);
}
},
contour: op,
state: state
@ -64,11 +113,11 @@ class OpContour extends CamOp {
prepare(ops, progress) {
let { op, state, sliceOut } = this;
let { settings, widget } = state;
let { settings } = state;
let { process } = settings;
let { setTolerance, setTool, setSpindle, setPrintPoint } = ops;
let { camOut, polyEmit, newLayer, printPoint, lastPoint } = ops;
let { camOut, newLayer, printPoint, lastPoint } = ops;
let { bounds, zmax } = ops;
let toolDiam = this.toolDiam;
@ -87,9 +136,9 @@ class OpContour extends CamOp {
if (!slice.camLines) {
continue;
}
let polys = [], poly, emit;
let polys = [], poly;
slice.camLines.forEach(function (poly) {
if (depthFirst) poly = poly.clone(true);
if (depthFirst) poly = poly.clone(true).annotate({ slice: slice.index + 1 });
polys.push({ first: poly.first(), last: poly.last(), poly: poly });
});
if (depthFirst) {
@ -101,7 +150,7 @@ class OpContour extends CamOp {
poly.reverse();
}
poly.forEachPoint(function (point, pidx) {
camOut(point.clone(), pidx > 0, stepover);
camOut(point.clone(), pidx > 0, { moveLen: stepover });
}, false);
});
newLayer();
@ -115,7 +164,7 @@ class OpContour extends CamOp {
poly.reverse();
}
poly.forEachPoint(function (point, pidx) {
camOut(point.clone(), pidx > 0, stepover);
camOut(point.clone().annotate({ slice: poly.slice }), pidx > 0, { moveLen: stepover });
}, false);
newLayer();
return lastPoint();

View file

@ -14,8 +14,8 @@ class OpDrill extends CamOp {
async slice(progress) {
let { op, state } = this;
let { settings, addSlices, widget, updateToolDiams } = state;
let { zBottom, zThru, thruHoles, color } = state;
let { drills, drillThrough } = op
let { color } = state;
let { drills } = op
let drillTool = new Tool(settings, op.tool),
drillToolDiam = drillTool.fluteDiameter(),
@ -47,7 +47,7 @@ class OpDrill extends CamOp {
slice.camLines = [poly];
slice.output()
.setLayer("drill", { face: color, line: color })
.setLayer(state.layername, { face: color, line: color })
.addPolys(slice.camLines);
addSlices(slice);
sliceOut.push(slice);
@ -55,11 +55,10 @@ class OpDrill extends CamOp {
}
prepare(ops, progress) {
let { op, state } = this;
let { settings, widget, addSlices, updateToolDiams } = state;
let { op } = this;
let { setTool, setSpindle, setDrill, emitDrills } = ops;
setTool(op.tool, undefined, op.rate);
setDrill(op.down, op.lift, op.dwell, op.thru);
setDrill(op.down, op.lift, op.dwell);
setSpindle(op.spindle);
emitDrills(this.sliceOut.map(slice => slice.camLines).flat());
}

View file

@ -25,7 +25,7 @@ export class OpIndex extends CamOp {
}
prepare(ops, progress) {
const { lastPoint, zmax, zclear, camOut, stock } = ops;
const { lastPoint, zmax, zclear, camOut, stock, newLayer } = ops;
let last = lastPoint();
if (last) {
// max point of stock corner radius when rotating (safe z when indexing)
@ -34,7 +34,7 @@ export class OpIndex extends CamOp {
// move above rotating stock
camOut(last = last.clone().setZ(zmove), 0);
// issue rotation command
camOut(last = last.clone().setY(0).setA(this.degrees), 0);
camOut(last = last.clone().setZ(zmove).setA(this.degrees), 0);
}
}
}

View file

@ -6,22 +6,23 @@ import { generate as topo4_generate } from './topo4.js';
import { newPoint } from '../../../geo/point.js';
function createFilter(op) {
let ok = () => true;
let filter = slices => slices;
if (op.filter) {
let filterString = op.filter?.map(l => l.trim()).join('\n');
if (filterString) {
try {
const obj = eval(`( ${op.filter.join('\n')} )`);
let idx = 0;
if (obj && obj.slices) {
const nadd = [];
filter = function (slices) {
for (let slice of slices) {
if (obj.slices(slice, idx++)) {
nadd.push(slice);
}
const obj = eval(`( ${filterString} )`);
const accept = [];
let index = 0;
let sl_fn = obj?.slices ?? ok;
filter = function (slices) {
for (let slice of slices) {
if (sl_fn(slice, index++)) {
accept.push(slice);
}
return nadd;
};
}
}
return accept;
};
} catch (e) {
console.log('filter parse error', e, op.filter);
}
@ -36,9 +37,8 @@ class OpLathe extends CamOp {
async slice(progress) {
let { op, state } = this;
let { addSlices } = state;
let { addSlices, color } = state;
let filter = createFilter(op);
this.topo = await topo4_generate({
op,
state,

View file

@ -28,10 +28,10 @@ class OpLevel extends CamOp {
let points = [];
let clear = op.stock ?
[ newPolygon().centerRectangle({x:0,y:0,z:0}, stock.x, stock.y) ] :
[ newPolygon().centerRectangle({x:0,y:0,z:0}, stock.x + toolDiam/2, stock.y) ] :
POLY.outer(POLY.offset(tshadow, toolDiam * (op.inset || 0)));
POLY.fillArea(clear, 90, stepOver, points);
POLY.fillArea(clear, 1090, stepOver, points);
let layers = this.layers = [];
for (let z of zList) {

View file

@ -17,9 +17,11 @@ class OpOutline extends CamOp {
async slice(progress) {
let { op, state } = this;
let { settings, widget, slicer, addSlices, tshadow, thruHoles, unsafe, color } = state;
let { settings, slicer, addSlices, tshadow, thruHoles, unsafe, color, widget } = state;
let { updateToolDiams, tabs, cutTabs, cutPolys, workarea, zMax, shadowAt } = state;
let { process, stock } = settings;
let { controller } = settings;
let center_off = widget.track.pos ?? { x: 0, y: 0, z: 0};
if (op.down <= 0) {
throw `invalid step down "${op.down}"`;
@ -39,6 +41,8 @@ class OpOutline extends CamOp {
let indices = slicer.interval(op.down, intopt);
let trueShadow = process.camTrueShadow === true;
let lastShadowZ;
let stockClip;
// shift out first (top-most) slice
indices.shift();
// add flats to shadow
@ -168,16 +172,12 @@ class OpOutline extends CamOp {
let stepover = toolDiam * op.step;
let wideCuts = [] //accumulator for wide cuts
for (let c = (op.steps || 1); c > 0; c--){
offset.slice().forEach(op => {
// clone removes inners but the real solution is
// to limit expanded shells to through holes
let wideCut = POLY.expand([op.clone(true)], stepover*c, slice.z, [], 1);
wideCut.forEach(cut =>{ //set order of cuts when wide
cut.order = c
if(cut.inner) cut.inner.forEach(inn =>{ inn.order = c })
});
wideCuts.push(...wideCut)
let wideCut = POLY.expand(offset.clone(true), stepover * c, slice.z, [], 1);
wideCut.forEach(cut =>{ //set order of cuts when wide
cut.order = c
if(cut.inner) cut.inner.forEach(inn =>{ inn.order = c })
});
wideCuts.push(...wideCut)
}
offset.appendAll(wideCuts);
}
@ -187,16 +187,19 @@ class OpOutline extends CamOp {
addDogbones(offset, toolDiam / 5);
}
if (process.camStockClipTo && stock.x && stock.y && stock.center) {
let { center } = stock;
let x = center.x - center_off.x;
let y = center.y - center_off.y;
stockClip = newPolygon().centerRectangle({ x, y }, stock.x + 0.001, stock.y + 0.001);
offset = cutPolys([stockClip], offset, slice.z, true);
}
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
offset = cutTabs(tabs, offset, slice.z);
}
if (process.camStockClipTo && stock.x && stock.y && stock.center) {
let rect = newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y);
offset = cutPolys([rect], offset, slice.z, true);
offset = cutTabs(tabs, offset);
}
// offset.xout(`slice ${slice.z}`);
@ -208,11 +211,17 @@ class OpOutline extends CamOp {
// project empty up and render
for (let slice of slices) {
if (false) slice.output()
if (controller.devel) slice.output()
.setLayer("slice", {line: 0xaaaa00}, false)
.addPolys(slice.topPolys())
if (controller.devel) slice.output()
.setLayer("shadow", {line: 0x557799}, false)
.addPolys(slice.shadow)
if (controller.devel && stockClip) slice.output()
.setLayer("stock clip", {line: 0x557799}, false)
.addPolys([ stockClip ])
slice.output()
.setLayer("outline", {face: color, line: color})
.setLayer(state.layername, {face: color, line: color})
.addPolys(slice.camLines);
}
@ -224,9 +233,9 @@ class OpOutline extends CamOp {
let { op, state, sliceOut } = this;
let { setTool, setSpindle, setPrintPoint } = ops;
let { polyEmit, depthOutlinePath } = ops;
let { camOut, newLayer, printPoint } = ops;
let { settings, widget } = state;
let { process, controller } = settings;
let { newLayer, printPoint } = ops;
let { settings } = state;
let { process } = settings;
let easeDown = process.camEaseDown;
let toolDiam = this.toolDiam;

View file

@ -19,13 +19,11 @@ class OpPocket extends CamOp {
async slice(progress) {
const pocket = this;
const debug = false;
let { op, state } = this;
let { tool, rate, down, plunge, expand, contour, smooth, tolerance } = op;
let { ov_botz, ov_conv } = op;
let { settings, widget, addSlices, zBottom, zThru, tabs, color } = state;
let { settings, widget, addSlices, zBottom, tabs, color } = state;
let { updateToolDiams, cutTabs, healPolys, shadowAt, workarea } = state;
let { process } = settings;
zBottom = ov_botz ? workarea.bottom_stock + ov_botz : zBottom;
// generate tracing offsets from chosen features
let sliceOut;
@ -36,24 +34,18 @@ class OpPocket extends CamOp {
let cutdir = ov_conv;
let engrave = contour && op.engrave;
let zTop = workarea.top_z;
let devel = settings.controller.devel;
let smoothVal = (smooth ?? 0) / 10;
if (contour) {
down = 0;
this.topo = await Topo({
// onupdate: (update, msg) => {
onupdate: (index, total, msg) => {
progress((index / total) * 0.9, msg);
},
ondone: (slices) => {
// console.log({ contour: slices });
},
contour: {
tool,
tolerance,
inside: true,
axis: "-"
},
state: state
});
this.contour = {
axis: "-",
inside: true,
nogpu: true,
step: toolOver,
tolerance,
tool,
};
}
updateToolDiams(toolDiam);
if (tabs) {
@ -69,7 +61,7 @@ class OpPocket extends CamOp {
sliceOut.push(slice);
return slice;
}
function clearZ(polys, z, down) {
async function clearZ(polys, z, down) {
if (down) {
// adjust step down to a value <= down that
// ends on the lowest z specified
@ -92,14 +84,14 @@ class OpPocket extends CamOp {
let clip = [], shadow;
if (contour) {
if (smooth) {
clip = POLY.offset(POLY.offset([ poly ], smooth), -smooth);
clip = POLY.offset(POLY.offset([ poly ], smoothVal), -smoothVal);
} else {
clip = [ poly ];
}
} else {
shadow = shadowAt(z);
if (smooth) {
shadow = POLY.setZ(POLY.offset(POLY.offset(shadow, smooth), -smooth), z);
shadow = POLY.setZ(POLY.offset(POLY.offset(shadow, smoothVal), -smoothVal), z);
}
POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0);
if (op.outline) {
@ -130,16 +122,16 @@ class OpPocket extends CamOp {
}
POLY.setWinding(slice.camLines, cutdir, false);
if (contour) {
slice.camLines = pocket.conform(slice.camLines, op.refine, engrave, pct => {
slice.camLines = await pocket.conform(slice.camLines, op.refine, engrave, pct => {
progress(0.9 + (zpro + zinc * pct) * 0.1, "conform");
});
}
slice.output()
.setLayer("pocket", {line: color}, false)
.setLayer(state.layername, {line: color}, false)
.addPolys(slice.camLines)
if (debug && shadow) slice.output()
if (devel && shadow) slice.output()
.setLayer("pocket shadow", {line: 0xff8811}, false)
.addPolys(shadow)
.addPolys(shadow);
if (!contour) {
progress(zpro, "pocket");
}
@ -168,23 +160,37 @@ class OpPocket extends CamOp {
outline = POLY.setWinding(outline, cutdir, false);
outline = healPolys(outline);
if (smooth) {
outline = POLY.offset(POLY.offset(outline, smooth), -smooth);
outline = POLY.offset(POLY.offset(outline, smoothVal), -smoothVal);
}
if (outline.length) {
// option to skip interior features (holes, pillars)
if (op.outline) {
POLY.clearInner(outline);
}
if (debug) newSliceOut(zmin).output()
await clearZ(outline, zmin + 0.0001, down);
if (devel && sliceOut?.length) sliceOut[0].output()
.setLayer("pocket area", {line: 0x1188ff}, false)
.addPolys(outline)
clearZ(outline, zmin + 0.0001, down);
progress(1, "pocket");
}
}
// mold cam output lines to the surface of the topo offset by tool geometry
conform(camLines, refine, engrave, progress) {
async conform(camLines, refine, engrave, progress) {
if (!this.topo) {
console.log('deferred topo');
this.topo = await Topo({
// onupdate: (update, msg) => {
onupdate: (index, total, msg) => {
progress((index / total) * 0.9, msg);
},
ondone: (slices) => {
// console.log({ contour: slices });
},
contour: this.contour,
state: this.state
});
}
const topo = this.topo;
// re-segment polygon to a higher resolution
const hirez = camLines.map(p => p.segment(topo.tolerance * 2));
@ -236,7 +242,7 @@ class OpPocket extends CamOp {
prepare(ops, progress) {
let { op, state, pockets } = this;
let { setTool, setSpindle, setTolerance, sliceOutput, getPrintPoint } = ops;
let { getPrintPoint , pocket, setTool, setSpindle, setTolerance } = ops;
let { process } = state.settings;
setTool(op.tool, op.rate);
@ -269,11 +275,12 @@ class OpPocket extends CamOp {
}
if (min.pocket) {
min.pocket.used = true;
sliceOutput(min.pocket, {
pocket({
cutdir: op.ov_conv,
depthFirst: process.camDepthFirst && !state.isIndexed,
easeDown: op.down && process.easeDown ? op.down : 0,
progress: (n,m) => progress(n/m, "pocket")
progress: (n,m) => progress(n/m, "pocket"),
slices: min.pocket
});
} else {
break;

View file

@ -19,6 +19,7 @@ class OpRegister extends CamOp {
let tool = new Tool(settings, op.tool);
let sliceOut = this.sliceOut = [];
let axis = op.axis.toLowerCase();
updateToolDiams(tool.fluteDiameter());
@ -42,7 +43,7 @@ class OpRegister extends CamOp {
return;
}
switch (op.axis.toLowerCase()) {
switch (axis) {
case "x":
if (op.points == 3) {
pathPoints.push(newPoint(boundMinX - cutOffset, centerY, 0));
@ -64,6 +65,7 @@ class OpRegister extends CamOp {
}
break;
case "-":
case "=":
let halfOffset = toolOffset / 2,
loopMinX = boundMinX - cutOffset,
loopMaxX = boundMaxX + cutOffset,
@ -84,7 +86,7 @@ class OpRegister extends CamOp {
}
function rept(count, step, fn) {
while (count-- > 0) {
fn(step, count === 0);
fn(step, count);
step = -step;
}
}
@ -93,9 +95,16 @@ class OpRegister extends CamOp {
addSlices(slice);
sliceOut.push(slice);
start(z);
rept(4, halfOffset, oy => {
rept(4, halfOffset, (oy, count) => {
move(0, -oy);
move(deltaX, 0);
if (axis === '=' && count % 2 === 1) {
move(deltaX/2, 0);
move(0, oy/2);
move(0, -oy/2);
move(deltaX/2, 0);
} else {
move(deltaX, 0);
}
});
rept(4, halfOffset, ox => {
move(ox, 0);
@ -136,11 +145,10 @@ class OpRegister extends CamOp {
}
prepare(ops, progress) {
let { op, state } = this;
let { settings, widget, addSlices, updateToolDiams } = state;
let { op } = this;
let { setTool, setSpindle, setDrill, emitDrills } = ops;
if (op.axis === '-') {
if (op.axis === '-' || op.axis === '=') {
setTool(op.tool, op.feed, op.rate);
setSpindle(op.spindle);
for (let slice of this.sliceOut) {

View file

@ -15,11 +15,12 @@ class OpRough extends CamOp {
async slice(progress) {
let { op, state } = this;
let { settings, slicer, addSlices, unsafe, color } = state;
let { settings, slicer, addSlices, unsafe, color, widget } = state;
let { updateToolDiams, thruHoles, tabs, cutTabs, cutPolys } = state;
let { ztOff, zMax, shadowAt, isIndexed} = state;
let { workarea } = state;
let { process, stock } = settings;
let { controller, process, stock } = settings;
let center_off = widget.track.pos ?? { x: 0, y: 0, z: 0};
if (op.down <= 0) {
throw `invalid step down "${op.down}"`;
@ -36,6 +37,12 @@ class OpRough extends CamOp {
updateToolDiams(toolDiam);
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
}
// clear the stock above the area to be roughed out
if (workarea.top_z > workarea.top_part && !cutThruBypass) {
let shadow = state.shadow.base.clone();
@ -58,14 +65,11 @@ class OpRough extends CamOp {
slice.z = z;
let polys = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ);
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
polys = cutTabs(tabs, polys, slice.z);
}
slice.camLines = polys;
slice.output()
.setLayer("roughing", {face: color, line: color})
.setLayer(state.layername, {face: color, line: color})
.addPolys(slice.camLines);
addSlices(slice);
camFaces.push(slice);
@ -206,20 +210,6 @@ class OpRough extends CamOp {
}
}
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
offset = cutTabs(tabs, offset, slice.z);
}
if (!offset) return;
if (process.camStockClipTo && stock.x && stock.y && stock.center) {
let rect = newPolygon().centerRectangle(stock.center, stock.x, stock.y);
offset = cutPolys([rect], offset, slice.z, true);
}
// elimate double inset on inners
offset.forEach(op => {
if (op.inner) {
@ -236,24 +226,38 @@ class OpRough extends CamOp {
}
});
if (process.camStockClipTo && stock.x && stock.y && stock.center) {
let { center } = stock;
let x = center.x - center_off.x;
let y = center.y - center_off.y;
let rect = newPolygon().centerRectangle({ x, y }, stock.x + 0.001, stock.y + 0.001);
offset = cutPolys([rect], offset, slice.z, true);
}
if (!offset) return;
slice.camLines = offset;
if (roughLeaveZ) {
// offset roughing in Z as well to minimize
// tool marks on curved surfaces
// offset roughing in Z as well to minimize tool marks on curved surfaces
// const roughLeaveZ = 1 * Math.min(roughDown, roughLeave / 2);
slice.camLines.forEach(p => {
p.setZ(p.getZ() + roughLeaveZ);
});
for (let poly of slice.camLines) {
for (let point of poly.points) point.z += roughLeaveZ;
}
}
if (controller.devel) {
if (tabs) slice.output()
.setLayer("tabs clip", {line: 0xaa00aa}, true)
.addPolys(tabs.map(tab => tab.off).flat());
slice.output()
.setLayer("slice", {line: 0xaaaa00}, true)
.addPolys(slice.topPolys())
// .setLayer("top shadow", {line: 0x0000aa})
// .addPolys(tshadow)
// .setLayer("rough shadow", {line: 0x00aa00})
// .addPolys(shadow)
.setLayer("shadow clip", {line: 0xaa0000})
.addPolys(shell);
}
if (false) slice.output()
.setLayer("slice", {line: 0xaaaa00}, true)
.addPolys(slice.topPolys())
// .setLayer("top shadow", {line: 0x0000aa})
// .addPolys(tshadow)
// .setLayer("rough shadow", {line: 0x00aa00})
// .addPolys(shadow)
.setLayer("rough shell", {line: 0xaa0000})
.addPolys(shell);
progress(0.5 + 0.5 * (index / slices.length));
});
@ -278,8 +282,11 @@ class OpRough extends CamOp {
}
slices.forEach(slice => {
if (slice.camLines && tabs) {
slice.camLines = cutTabs(tabs, slice.camLines);
}
slice.output()
.setLayer("roughing", {face: color, line: color})
.setLayer(state.layername, {face: color, line: color})
.addPolys(slice.camLines);
});
this.sliceOut = slices.filter(slice => slice.camLines);
@ -289,15 +296,13 @@ class OpRough extends CamOp {
prepare(ops, progress) {
let { op, state, sliceOut, camFaces } = this;
let { setTool, setSpindle, setPrintPoint, sliceOutput, polyEmit } = ops;
let { camOut, newLayer, printPoint } = ops;
let { setTool, setSpindle, setPrintPoint, pocket, polyEmit, newLayer, printPoint } = ops;
let { settings } = state;
let { process } = settings;
let easeDown = process.camEaseDown;
let cutdir = op.ov_conv;
let depthFirst = process.camDepthFirst && !state.isIndexed;
let depthData = [];
setTool(op.tool, op.rate, op.plunge);
setSpindle(op.spindle);
@ -325,11 +330,12 @@ class OpRough extends CamOp {
// output the roughing passes
setPrintPoint(printPoint);
sliceOutput(sliceOut, {
pocket({
cutdir,
depthFirst,
easeDown: op.down && easeDown ? 0.001 : 0,
progress: (n,m) => progress(n/m, "routing")
progress: (n,m) => progress(n/m, "routing"),
slices: sliceOut
});
}
}

View file

@ -92,7 +92,7 @@ class OpTrace extends CamOp {
}
POLY.setWinding(slice.camLines, offset === "outside" ? !cutdir : cutdir, false);
slice.output()
.setLayer("trace follow", {line: color}, false)
.setLayer(state.layername, {line: color}, false)
.addPolys(slice.camLines)
}
function clearZnew(polys, z, down) {
@ -154,7 +154,7 @@ class OpTrace extends CamOp {
.setLayer("trace poly", {line: 0x1188ff}, false)
.addPolys([ poly ])
slice.output()
.setLayer("trace", {line: color}, false)
.setLayer(state.layername, {line: color}, false)
.addPolys(slice.camLines)
progress(zpro, "trace");
zpro += zinc;
@ -295,7 +295,6 @@ class OpTrace extends CamOp {
prepare(ops, progress) {
let { op, state } = this;
let { settings } = state;
let { setTool, setSpindle } = ops;
setTool(op.tool, op.rate);

View file

@ -56,8 +56,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
if (widget.camops.length === 0 || widget.meta.disabled) return;
let device = settings.device,
process = settings.process,
let { device, process } = settings,
isIndexed = process.camStockIndexed,
startCenter = process.camOriginCenter,
alignTop = settings.controller.alignTop,
@ -65,7 +64,6 @@ export function prepEach(widget, settings, print, firstPoint, update) {
stockz = stock.z * (isIndexed ? 0.5 : 1),
outer = settings.bounds || widget.getPositionBox(),
outerz = outer.max.z,
slices = widget.slices,
zclear = (process.camZClearance || 1),
zmax_force = process.camForceZMax || false,
zmax_outer = stockz + zclear,
@ -75,13 +73,13 @@ export function prepEach(widget, settings, print, firstPoint, update) {
boundsz = isIndexed ? stock.z / 2 : bounds.max.z + ztOff,
zadd = !isIndexed ? stock.z - boundsz : alignTop ? outerz - boundsz : 0,
zmax = outerz + zclear + (process.camOriginOffZ || 0),
zsafe = isIndexed ? Math.hypot(bounds.dim.y, bounds.dim.z) / 2 + zclear : zmax,
wmpos = widget.track.pos,
wmx = wmpos.x,
wmy = wmpos.y,
originx = (startCenter ? 0 : -stock.x / 2) + (process.camOriginOffX || 0),
originy = (startCenter ? 0 : -stock.y / 2) + (process.camOriginOffY || 0),
origin = newPoint(originx + wmx, originy + wmy, zmax),
output = print.output,
easeDown = process.camEaseDown,
easeAngle = process.camEaseAngle,
depthFirst = process.camDepthFirst,
@ -94,13 +92,11 @@ export function prepEach(widget, settings, print, firstPoint, update) {
drillDown = 0,
drillLift = 0,
drillDwell = 0,
drillThru = 0,
lasering = false,
laserPower = 0,
newOutput = print.output || [],
layerOut = [],
printPoint,
isNewMode,
isPocket,
isContour,
isRough,
@ -112,6 +108,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
toolDiamMove,
plungeRate = process.camFastFeedZ,
feedRate,
lastOp,
lastTool,
lastPush,
lastPoint,
@ -187,11 +184,10 @@ export function prepEach(widget, settings, print, firstPoint, update) {
laserPower = power;
}
function setDrill(down, lift, dwell, thru) {
function setDrill(down, lift, dwell) {
drillDown = down;
drillLift = lift;
drillDwell = dwell;
drillThru = thru;
}
function emitDrills(polys) {
@ -264,10 +260,11 @@ export function prepEach(widget, settings, print, firstPoint, update) {
*/
function layerPush(point, emit, speed, tool, options) {
const { type, center, arcPoints } = options ?? {};
const dz = (point && lastPush && lastPush.point) ? point.z - lastPush.point.z : 0;
if (dz < 0 && speed > plungeRate) {
const dz = (point && lastPush?.point) ? point.z - lastPush.point.z : 0;
if (dz < -0.05 && speed > plungeRate) {
speed = plungeRate;
}
// if (options?.type !== 'lerp') console.log( point, currentOp.type );
layerOut.mode = currentOp;
if (lasering) {
let power = emit ? laserPower : 0;
@ -320,7 +317,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
p.x + wmx,
p.y + wmy,
p.z + zadd
).setA(p.a ?? lastPoint?.a);
).setA(p.a ?? lastPoint?.a).annotate({ slice: p.slice });
}
/**
@ -333,7 +330,26 @@ export function prepEach(widget, settings, print, firstPoint, update) {
* @param {number} opts.factor speed scale factor
*/
function camOut(point, emit = 1, opts) {
// console.trace({point, emit, opts})
let lop = lastOp;
lastOp = currentOp;
// on operation changes:
// 1. move to safe z of current point preserving angle
// 2. move to safe z of new point preserving old angle
// 3. move to safe z of new point with new angle
if (lop && lop !== currentOp && lastPoint) {
// compensate for applyWidgetMovement() applied to lastPoint
let lpo = lastPoint.clone().move({ x: -wmx, y: -wmy, z: -zadd });
camOut(lpo.clone().setZ(zsafe).setA(lastPoint.a), 0);
camOut(point.clone().setZ(zsafe).setA(lastPoint.a), 0);
camOut(point.clone().setZ(zsafe), 0);
}
if (lop?.type === 'index' && lop !== currentOp ) {
// console.log('post index first point', point);
camOut(point.clone().setZ(lastPoint.z).setA(lastPoint.a), 1);
}
let {
center = {},
clockwise = true,
@ -342,14 +358,12 @@ export function prepEach(widget, settings, print, firstPoint, update) {
factor = 1,
} = opts ?? {}
// console.log({radius})
const isArc = emit == 2 || emit == 3;
const pointA = point.a;
// apply widget movement pos
const pointA = point.a;
point = applyWidgetMovement(point);
// console.log(point.z);
if (nextIsMove) {
emit = 0;
nextIsMove = false;
@ -370,7 +384,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
for (let a of lerp) {
let lp = point.clone().setA(a);
// console.log(lp.a, lp.x, lp.y, lp.z);
lastPoint = layerPush(
layerPush(
lp,
emit,
rate,
@ -403,16 +417,20 @@ export function prepEach(widget, settings, print, firstPoint, update) {
layerPush(point.clone().setZ(stockz + 1), 0, 0, tool);
// change to cutting move for remainder of plunge
emit = 1;
isMove = false;
}
// drop points too close together
if (!isLathe && !isArc && deltaXY < 0.001 && point.z === lastPoint.z) {
if (!isLathe && !isArc && deltaXY < 0.001 && point.z === lastPoint.z && point.a === lastPoint.a) {
// console.trace(["drop dup",lastPoint,point]);
return;
}
// no jump moves in contour mode to adjacent slice points
let steady = lastPoint && currentOp.type === 'contour' && Math.abs(point.slice - lastPoint.slice) < 4;
// convert short planar moves to cuts in some cases
if (hasDelta && !isRough && !isArc && isMove && deltaXY <= moveLen && deltaZ <= 0 && !lasering) {
if (hasDelta && !isArc && isMove && deltaXY <= moveLen && deltaZ <= 0 && !lasering) {
let iscontour = tolerance > 0;
let isflat = absDeltaZ < 0.001;
// restrict this to contouring
@ -431,7 +449,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
} else if (point.z < lastPoint.z) {
layerPush(point.clone().setZ(lastPoint.z), 0, 0, tool);
}
} else if (isMove) {
} else if (isMove && !steady) {
// for longer moves, check the terrain to see if we need to go up and over
const bigXY = (deltaXY > moveLen && !lasering);
const bigZ = (deltaZ > toolDiam / 2 && deltaXY > tolerance);
@ -478,13 +496,15 @@ export function prepEach(widget, settings, print, firstPoint, update) {
}
// set new plunge rate
let tmprate;
if (false)
if (!lasering && !isLathe && deltaZ < -tolerance) {
let threshold = Math.min(deltaXY / 2, absDeltaZ),
modifier = threshold / absDeltaZ;
if (synthPlunge && threshold && modifier && deltaXY > tolerance) {
// use modifier to speed up long XY move plunge rates
// console.log('modifier', modifier);
rate = Math.max(
tmprate = Math.max(
plungeRate,
Math.round(plungeRate + ((feedRate - plungeRate) * modifier))
);
@ -492,7 +512,8 @@ export function prepEach(widget, settings, print, firstPoint, update) {
let L = Math.hypot(deltaXY, absDeltaZ);
let limXY = feedRate * L / deltaXY;
let limZ = plungeRate * L / Math.abs(deltaZ);
rate = Math.min(feedRate, limXY, limZ);
tmprate = Math.min(feedRate, limXY, limZ);
console.log({ rate_override: rate, was: rate });
// let zps = len / absDeltaZ;
// rate = Math.max(
// plungeRate,
@ -508,7 +529,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
lastPoint = layerPush(
point,
clockwise ? 2 : 3,
rate,
tmprate ?? rate,
tool,
{
center,
@ -523,7 +544,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
lastPoint = layerPush(
point,
emit,
rate,
tmprate ?? rate,
tool
);
}
@ -533,16 +554,15 @@ export function prepEach(widget, settings, print, firstPoint, update) {
* output an array of slices that form a pocket
* used by rough and pocket ops, does not support arcs
*
* @param {Slice[]} top-down Z stack of slices
* @param {*} opts
* @param {Slice[]} slices top-down Z stack of slices
* @param {boolean} cutdir true=CW false=CCW
* @param {boolean} depthFirst prioritize cut depth in pockets by nesting
*/
function sliceOutput(sliceOut, opts = {}) {
const { cutdir, depthFirst, easeDown, progress } = opts;
function pocket({ slices, cutdir, depthFirst, easeDown, progress }) {
let total = 0;
let depthData = [];
for (let slice of sliceOut) {
for (let slice of slices) {
let polys = [], t = [], c = [];
POLY.flatten(slice.camLines).forEach(function (poly) {
let child = poly.parent;
@ -569,7 +589,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
}, { swapdir: false });
newLayer();
}
progress(++total, sliceOut.length);
progress(++total, slices.length);
}
// crucially returns true for -0 as well as other negative #s
@ -663,44 +683,32 @@ export function prepEach(widget, settings, print, firstPoint, update) {
// make top start offset configurable
printPoint = firstPoint || origin;
// accumulated data for depth-first optimizations
// let depthData = {
// rough: [],
// outline: [],
// roughDiam: 0,
// outlineDiam: 0,
// contourx: [],
// contoury: [],
// trace: [],
// drill: [],
// layer: 0,
// };
let ops = {
stock,
setTool,
setDrill,
setSpindle,
setTolerance,
setPrintPoint,
setLasering,
getPrintPoint() { return printPoint },
printPoint,
newLayer,
addGCode,
bounds,
camOut,
polyEmit,
poly2polyEmit,
tip2tipEmit,
depthRoughPath,
depthOutlinePath,
sliceOutput,
depthRoughPath,
emitDrills,
emitTrace,
bounds,
getPrintPoint() { return printPoint },
lastPoint: () => { return lastPoint },
newLayer,
pocket,
poly2polyEmit,
polyEmit,
printPoint,
setDrill,
setLasering,
setPrintPoint,
setSpindle,
setTolerance,
setTool,
stock,
tip2tipEmit,
zclear,
zmax,
lastPoint: () => { return lastPoint }
zsafe,
};
let opSum = 0;
@ -973,8 +981,8 @@ export function prepEach(widget, settings, print, firstPoint, update) {
let arcPoints = arcToPath(from, to, arcPreviewRes, { clockwise, center })
.map(applyWidgetMovement);
.map(applyWidgetMovement);
// console.log({arcPoints})
camOut(from, 1);
@ -1096,7 +1104,10 @@ export function prepEach(widget, settings, print, firstPoint, update) {
if (lastPoint && newOutput.length) {
let lastLayer = newOutput.filter(layer => Array.isArray(layer)).peek();
if (Array.isArray(lastLayer)) {
print.addOutput(lastLayer, printPoint = lastPoint.clone().setZ(zmax_outer), 0, 0, tool);
printPoint = lastPoint.clone();
if (printPoint.z < zmax_outer) printPoint.setZ(zmax_outer);
print.addOutput(lastLayer, printPoint, 0, 0, tool);
// print.addOutput(lastLayer, printPoint = lastPoint.clone().setZ(zmax_outer), 0, 0, tool);
}
}
// console.log("prepare output", newOutput);

View file

@ -119,6 +119,10 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
ondone(msg);
}
function alert(msg) {
onupdate(null,null,msg);
}
if (unsafe) {
console.log("disabling overhang safeties");
}
@ -135,20 +139,20 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
let maxDelta = 1e-3;
if (stock.x + maxDelta < (bounds.max.x - bounds.min.x)) {
return error('stock X too small for part. resize stock or use offset stock');
alert('stock X too small for part');
}
if (stock.y + maxDelta < (bounds.max.y - bounds.min.y)) {
return error('stock Y too small for part. resize stock or use offset stock');
alert('stock Y too small for part');
}
if (stock.z + maxDelta < (bounds.max.z - bounds.min.z)) {
return error('stock Z too small for part. resize stock or use offset stock');
alert('stock Z too small for part');
}
}
if (zMin >= bounds.max.z) {
return error(`invalid z bottom ${(zMin / units).round(3)} >= bounds z max ${(zMax / units).round(3)}`);
alert(`invalid z bottom ${(zMin / units).round(3)} >= bounds z max ${(zMax / units).round(3)}`);
}
let mark = Date.now();
@ -158,31 +162,31 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
let shadows = {};
let slicer;
let state = {
settings,
widget,
addSlices,
bounds,
tabs,
cutTabs,
cutPolys,
color,
computeShadows,
contourPolys,
cutPolys,
cutTabs,
dark,
healPolys,
isIndexed,
ops: opList,
setAxisIndex,
settings,
shadowAt,
slicer,
addSlices,
isIndexed,
setAxisIndex,
updateToolDiams,
tabs,
unsafe,
updateSlicer,
computeShadows,
updateToolDiams,
widget,
zBottom,
zMax,
zThru,
ztOff,
zMax,
zTop,
unsafe,
color,
dark,
ops: opList
zTop
};
let tracker = setSliceTracker({ rotation: 0 });
@ -310,8 +314,11 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
workover.bottom_z = isIndexed ? valz.ov_botz : bottom_stock + valz.ov_botz;
workover.bottom_cut = Math.max(workover.bottom_z, -zThru);
}
// state.workarea = workover;
let { note, type } = op.op;
let named = note ? note.split(' ').filter(v => v.charAt(0) === '#') : [];
let layername = named.length ? named : (note ? `${type} (${note})` : type);
Object.assign(state, {
layername,
zBottom,
zThru,
ztOff,
@ -688,7 +695,7 @@ export async function holes(settings, widget, individual, rec, onProgress) {
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)
@ -698,13 +705,13 @@ export async function holes(settings, widget, individual, rec, onProgress) {
continue;
}
for (let poly of inner) {
if (poly.points.length < 7) continue;
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.98) {
// if not circular, don't add to holes
continue;
@ -717,8 +724,7 @@ export async function holes(settings, widget, individual, rec, onProgress) {
let overlap = false;
for (let [i, circle] of circles.entries()) {
let dist = circle.distTo2D(center);
// //if on the same xy point,
// if on the same xy point,
if (dist <= centerDiff) {
// console.log("overlap",center,circle);
circle.overlapping.push(center);
@ -732,12 +738,10 @@ export async function holes(settings, widget, individual, rec, onProgress) {
}
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();
@ -775,14 +779,33 @@ export async function holes(settings, widget, individual, rec, onProgress) {
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 cutTabs(tabs, offset) {
let out = [];
for (let tab of tabs) {
tab.top = tab.pos.z + tab.dim.z / 2;
}
for (let poly of offset) {
let polyZ = poly.getZ();
let cut = tabs.filter(tab => tab.top >= polyZ);
let lo = poly.cut(cut.map(tab => tab.off).flat(), false);
let hi = [];
for (let tab of cut) {
hi.appendAll(POLY.setZ(poly.cut(tab.off, true), tab.top));
}
if (hi.length) {
let heal = healPolys([ ...lo, ...hi ], false);
POLY.setWinding(heal, poly.isClockwise());
out.push(...heal);
} else {
out.push(poly);
}
}
return out;
}
function cutPolys(polys, offset, z, inter) {
function cutPolys(polys, offset) {
let noff = [];
offset.forEach(op => noff.appendAll(op.cut(POLY.union(polys, 0, true), inter)));
offset.forEach(op => noff.appendAll( op.cut(POLY.union(polys, 0, true), true) ));
return healPolys(noff);
}
@ -799,7 +822,17 @@ function contourPolys(widget, polys) {
}
}
function healPolys(noff) {
function dedup(arr, same) {
const out = [];
let prev;
for (const x of arr) {
if (!prev || !same(prev, x)) out.push(x);
prev = x;
}
return out;
}
function healPolys(noff, sameZ = true) {
for (let p of noff) {
if (p.appearsClosed()) {
p.points.pop();
@ -818,29 +851,29 @@ function healPolys(noff) {
let s2 = ntmp[j];
if (!s2) continue;
// require polys at same Z to heal
if (Math.abs(s1.getZ() - s2.getZ()) > 0.01) {
if (sameZ && 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));
s1.addPoints(s2.points);
ntmp[j] = null;
continue outer;
}
if (s2.last().isMergable2D(s1.first())) {
s2.addPoints(s1.points.slice(1));
s2.addPoints(s1.points);
ntmp[i] = null;
continue outer;
}
if (s1.first().isMergable2D(s2.first())) {
s1.reverse();
s1.addPoints(s2.points.slice(1));
s1.addPoints(s2.points);
ntmp[j] = null;
continue outer;
}
if (s1.last().isMergable2D(s2.last())) {
s2.reverse();
s1.addPoints(s2.points.slice(1));
s1.addPoints(s2.points);
ntmp[j] = null;
continue outer;
}
@ -854,9 +887,14 @@ function healPolys(noff) {
}
// 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;
poly.points = dedup(poly.points, (a,b) => a.isMergable3D(b));
if (poly.open) {
if (poly.first().isMergable3D(poly.last())) {
poly.points.pop();
poly.open = false;
} else if (poly.first().isMergable2D(poly.last())) {
poly.open = false;
}
}
}
}

View file

@ -12,6 +12,7 @@ const { sliceConnect, sliceDedup } = slicer;
const { config } = base;
const timing = false;
const zDecimal = 3;
const epsilon = 10e-5;
const begin = function () {
if (timing) console.time(...arguments);
@ -94,7 +95,7 @@ export class Slicer {
countZ(p3.z);
}
// use co-flat and co-line detection to adjust slice Z
if (p1.z === p2.z && p2.z === p3.z) {
if (Math.abs(p1.z - p2.z) < epsilon && Math.abs(p2.z - p3.z) < epsilon) {
// detect zFlat faces to avoid slicing directly on them
let zkey = p1.z.toFixed(zDecimal),
area = Math.abs(util.area2(p1, p2, p3)) / 2;

View file

@ -16,9 +16,10 @@ export class Topo {
async generate(opt = {}) {
let { state, contour, onupdate, ondone } = opt;
let { widget, settings, tshadow, center, tabs, color } = opt.state;
let { widget, settings, tshadow, tabs } = opt.state;
let { controller, process } = settings,
{ webGPU } = controller,
animesh = parseInt(controller.animesh || 100) * 2500,
axis = contour.axis.toLowerCase(),
contourX = axis === "x",
@ -37,16 +38,15 @@ export class Topo {
boundsY = maxY - minY,
inside = contour.inside,
density = 1 + (contour.reduction || 0),
resolution = tolerance ? tolerance : 1 / Math.sqrt(animesh / (boundsX * boundsY)),
resolution = (tolerance ? tolerance : 1 / Math.sqrt(animesh / (boundsX * boundsY))).round(5),
tool = new Tool(settings, contour.tool),
toolOffset = tool.generateProfile(resolution).profile,
toolDiameter = tool.fluteDiameter(),
toolStep = toolDiameter * contour.step, //tool.contourOffset(contour.step),
toolStep = toolDiameter * 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,
@ -75,12 +75,7 @@ export class Topo {
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;
debug_clips = false;
if (tolerance === 0 && !topoCache) {
console.log(widget.id, 'topo auto tolerance', resolution.round(4));
@ -90,7 +85,7 @@ export class Topo {
// used by Pocket -> Contour
this.tolerance = resolution;
if (tabs) {
if (tabsOn) {
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);
@ -107,6 +102,190 @@ export class Topo {
newslices.push(debug);
}
if (webGPU && !contour.nogpu) {
// invert tool Z offset for gpu code
let toolBounds = new THREE.Box3()
.expandByPoint({ x: -toolDiameter/2, y: -toolDiameter/2, z: 0 })
.expandByPoint({ x: toolDiameter/2, y: toolDiameter/2, z: 0 });
let toolPos = tool.profile.slice();
for (let i=0; i<toolPos.length; i+= 3) {
// toolPos[i+2] = -toolPos[i+2];
toolBounds.expandByPoint({ x: toolPos[i], y: toolPos[i+1], z: toolPos[i+2] });
}
let toolData = { positions: toolPos, bounds: toolBounds };
const vertices = widget.getGeoVertices({ unroll: true, translate: true });
const wbounds = widget.getBoundingBox();
if (!inside) {
wbounds.expandByVector({ x: toolDiameter/2, y: toolDiameter/2, z: 0 });
}
if (contourY) {
// console.time('swap XY vertices');
// swap XY vertices
for (let i=0; i<vertices.length; i+= 3) {
let tmp = vertices[i+1];
vertices[i+1] = vertices[i+0];
vertices[i+0] = tmp;
}
['min','max'].forEach(ext => {
ext = wbounds[ext];
let tmp = ext.x;
ext.x = ext.y;
ext.y = tmp;
});
// console.timeEnd('swap XY vertices');
}
let trace = contour.trace;
let gpu = await self.get_raster_gpu({
mode: trace ? "tracing" : "planar",
resolution
});
let xStep = density;
let yStep = Math.ceil(toolStep / resolution);
let epsilon = 10e-4;
await gpu.loadTool({
sparseData: toolData
});
let terrain = await gpu.loadTerrain({
triangles: vertices,
boundsOverride: wbounds
});
let { gridWidth, positions } = terrain;
let output = await gpu.generateToolpaths({
xStep,
yStep,
zFloor: zBottom - 1,
onProgress(pct) { console.log({ pct }); onupdate(pct/100, 100) }
});
gpu.terminate();
let { numScanlines, pointsPerLine, pathData } = output;
let xmult = xStep * resolution;
let ymult = yStep * resolution;
let xoff = wbounds.min.x;
let yoff = wbounds.min.y;
let slices = [];
function calcPoint(x, y, z, tx, ty) {
let p = newPoint(x, y, z);
p.tx = tx;
p.ty = ty;
return p;
}
for (let i=0; i<numScanlines; i++) {
let lineStart = i * pointsPerLine;
let lineData = pathData.slice(lineStart, lineStart + pointsPerLine);
let points = contourY
? Array.from(lineData).map((v,j) => calcPoint(i * ymult + yoff, j * xmult + xoff, v, i * xStep, j * yStep))
: Array.from(lineData).map((v,j) => calcPoint(j * xmult + xoff, i * ymult + yoff, v, j * xStep, i * yStep))
;
let slice = newSlice(i);
let poly = newPolygon().setOpen();
let lines = [ ];
let skip = 0;
let lp;
for (let p of points) {
if (inside && positions[p.ty * gridWidth + p.tx] < zBottom - epsilon) {
if (poly.length > 1) {
lines.push(poly);
poly = newPolygon().setOpen();
} else if (poly.length === 1) {
poly = newPolygon().setOpen();
}
lp = undefined;
skip = 0;
continue;
}
if (p.z < zBottom - epsilon) {
lp = p;
if (poly.length === 0) {
skip++;
continue;
}
if (poly.length > 1) {
skip = 0;
p.z = zBottom;
poly.push(p);
lines.push(poly);
poly = newPolygon().setOpen();
continue;
}
} else if (lp && skip && poly.length === 0) {
lp.z = zBottom;
poly.push(lp);
}
poly.push(lp = p);
skip = 0;
}
// require two points or more
if (poly.length > 1) {
lines.push(poly);
}
// raize output points when inside tab boundaries
if (!inside && tabsOn && clipTab.length)
for (let poly of lines) {
for (let p of poly.points) {
for (let clip of clipTab) {
if (p.z < clip.z && p.isInPolygon(clip)) {
p.z = clip.z;
break;
}
}
}
}
// drop interior points along a continuous slope
for (let poly of lines) {
let { points } = poly;
let merged = [ points[0] ];
let lp = points[1];
let dz = points[0].z - lp.z;
for (let i=2; i<points.length; i++) {
let np = points[i];
let ndz = np.z - lp.z;
if (Math.abs(dz - ndz) > epsilon) {
// slope changed
merged.push(lp);
dz = ndz;
} else if (curvesOnly && Math.abs(ndz) < epsilon) {
// add empty points as path separators
merged.push(undefined);
}
lp = np;
}
if (merged.peek() !== lp) {
merged.push(lp);
}
poly.points = merged;
}
// drop flat regions when enabled
if (curvesOnly) {
let nulines = [];
for (let poly of lines) {
let { points } = poly;
let newp = [];
for (let p of points) {
if (p) {
newp.push(p);
} else if (newp.length) {
nulines.push(newPolygon(newp).setOpen());
newp = [];
}
}
nulines.push(newPolygon(newp).setOpen());
}
lines = nulines.filter(p => p.perimeter() > toolStep);
}
slice.camLines = lines;
slices.push(slice);
if (inside) {
onupdate(i, numScanlines);
}
}
ondone(slices);
return this;
}
const probe = this.probe = new Probe({
profile: toolOffset,
data,
@ -177,8 +356,7 @@ export class Topo {
clipTabZ: clipTab ? clipTab.map(t => t.z) : undefined,
tabHeight,
newslices,
leave,
color
leave
}, (i, l, p) => {
onupdate(l / 2 + i / 2, l, p);
});
@ -304,7 +482,7 @@ export class Topo {
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;
const { clipTo, clipTab, clipTabZ, tabHeight, newslices, leave } = params;
let stepsTaken = 0,
stepsTotal = 0;
@ -370,11 +548,8 @@ export class Topo {
gridy
}, segments => {
if (segments.length > 0) {
let slice = newSlice(gridx);
let slice = newSlice(x);
slice.camLines = segments;
slice.output()
.setLayer("contour y", { face: color, line: color })
.addPolys(segments);
slicesY.push(slice);
}
onupdate(++stepsTaken, stepsTotal, "contour y");
@ -399,11 +574,8 @@ export class Topo {
gridy
}, segments => {
if (segments.length > 0) {
let slice = newSlice(gridy);
let slice = newSlice(y);
slice.camLines = segments;
slice.output()
.setLayer("contour x", { face: color, line: color })
.addPolys(segments);
slicesX.push(slice);
}
onupdate(++stepsTaken, stepsTotal, "contour x");
@ -534,9 +706,9 @@ export class Trace {
console.log(...arguments);
}
const push_point = this.push_point = function (x, y, z) {
this.push_point = function (x, y, z) {
const newP = newPoint(x, y, z);
const lastP = lastPP;//trace.last();
const lastP = lastPP;
if (lastP) {
const dl = (x - lastP.x) || (y - lastP.y);
@ -553,7 +725,6 @@ export class Trace {
}
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();

View file

@ -22,8 +22,9 @@ export class Topo {
async generate(opt = {}) {
let { state, op, onupdate, ondone } = opt;
let { widget, settings, tabs } = opt.state;
let { widget, settings, tabs, color } = state;
let { controller, process } = settings;
let { webGPU } = controller;
let axis = op.axis.toLowerCase(),
tool = new Tool(settings, op.tool),
@ -38,9 +39,8 @@ export class Topo {
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)),
resolution = (tolerance ? tolerance : 1 / Math.sqrt(density / (span.x * span.y))).round(5),
step = this.step = (tool.traceOffset() * 2) * op.step,
angle = this.angle = op.angle || 1;
@ -52,31 +52,36 @@ export class Topo {
console.log(widget.id, 'topo4 auto tolerance', resolution.round(4));
}
this.zBottom = state.zBottom;
this.zBottom = state.zBottom ?? 0;
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.unit = tool.unitScale();
this.units = controller.units === 'in' ? 25.4 : 1
this.zoff = widget.track.top || 0;
this.leave = op.leave || 0;
this.linear = op.linear || false;
this.lineColor = state.settings.controller.dark ? 0xffff00 : 0x555500;
this.lineColor = color;//controller.dark ? 0xffff00 : 0x555500;
this.offStart = op.offStart ?? 0;
this.offEnd = op.offEnd ?? 0;
this.bounds = bounds;
this.gpu = webGPU ?? false;
onupdate(0, "lathe");
const parts = webGPU ? [ 0.8, 0.2 ] : [ 0.25, 0.75 ];
const range = this.range = { min: Infinity, max: -Infinity };
const slices = this.sliced = await this.slice(scale(onupdate, 0.25, 0));
const slices = this.sliced = await this.slice(scale(onupdate, parts[0], 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));
const lathe = await this.lathe(scale(onupdate, parts[1], parts[0]));
onupdate(1, "lathe");
ondone(lathe);
@ -86,10 +91,9 @@ export class Topo {
}
async slice(onupdate) {
const { vertices, resolution, tabverts, zoff } = this;
const { vertices, resolution, tabverts, zoff, offStart, offEnd, units } = this;
const { minions } = self.kiri_worker;
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) {
@ -101,9 +105,10 @@ export class Topo {
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;
// add tool diameter to slice min/max range to fully carve part
range.min += offStart * units;
range.max -= offEnd * units;
range.max += resolution * 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) {
@ -116,24 +121,36 @@ export class Topo {
// 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++;
// rp.rasterizeMesh(vertices, resolution).then(rpo => console.log({ rpo }));
if (this.gpu) {
return await this.sliceGPU(onupdate);
}
slices.push(slice);
// console.log({ shards, range, step, slices });
const { minions } = self.kiri_worker;
const zSpan = range.max - range.min;
const shards = Math.ceil(Math.min(25, vertices.length / 27000));
const totalSteps = Math.ceil(zSpan / resolution);
const stardSteps = Math.ceil(totalSteps / shards);
const stepWidth = stardSteps * resolution;
let slices = this.slices = [];
for (let i=0; i<shards; i++) {
let s;
slices.push(s = {
min: range.min + i * stepWidth,
max: Math.min(range.max, range.min + (i+1) * stepWidth),
index: i * stardSteps
});
}
// console.log({
// shards,
// step: stepWidth,
// range,
// resolution,
// slices: slices.slice(),
// minions
// });
if (minions?.running > 1) {
return await this.sliceMinions(onupdate);
} else {
@ -141,6 +158,96 @@ export class Topo {
}
}
async sliceGPU(onupdate) {
const { angle, diam, linear, offStart, offEnd, resolution, tool, units, vertices, zBottom } = this;
// invert tool Z offset for gpu code
let toolBounds = new THREE.Box3()
.expandByPoint({ x: -this.diam/2, y: -diam/2, z: 0 })
.expandByPoint({ x: this.diam/2, y: diam/2, z: 0 });
let toolPos = tool.slice();
let minz = Infinity;
for (let i=0; i<toolPos.length; i += 3) {
// toolPos[i+2] = -toolPos[i+2];
toolBounds.expandByPoint({ x: 0, y: 0, z: toolPos[i+2] });
minz = Math.min(minz,toolPos[i+2]);
}
let toolData = { positions: toolPos, bounds: toolBounds };
// console.time('swap XZ vertices');
// swap XZ vertices for gpu code
for (let i=0; i<vertices.length; i+= 3) {
let tmp = vertices[i+2];
vertices[i+2] = vertices[i+0];
vertices[i+1] = -vertices[i+1];
vertices[i+0] = tmp;
}
// console.timeEnd('swap XZ vertices');
let gpu = await self.get_raster_gpu({
mode: "radial",
resolution,
rotationStep: angle,
radialRotationOffset: 90
});
let xStep = Math.max(1, Math.round(this.step / resolution));
let boundsOverride = this.bounds.clone();
boundsOverride.min.x += offStart * units;
boundsOverride.max.x -= offEnd * units;
await gpu.loadTool({
sparseData: toolData
});
await gpu.loadTerrain({
triangles: vertices,
boundsOverride,
onProgress(pct) { onupdate(pct/100) }
});
let output = await gpu.generateToolpaths({
xStep,
yStep: 1,
zFloor: zBottom,
onProgress(i,j) { onupdate(i/j) }
});
gpu.terminate();
let { numStrips, strips } = output;
let degPerRow = 360 / numStrips;
let slices = this.gpu_slices = [];
let xmult = resolution * xStep;
let xoff = boundsOverride.min.x;
let rows = [];
for (let i=0; i<numStrips; i++) {
let points = Array.from(strips[i].pathData).map((v,j) => newPoint(j * xmult + xoff, 0, v).setA(-i * degPerRow));
rows.push(points);
}
if (linear) {
for (let i=0; i<rows.length; i++) {
let slice = newSlice(i);
slice.index = i;
slice.camLines = [ newPolygon(rows[i]).setOpen() ];
if (i % 2 === 1) slice.camLines[0].reverse();
slices.push(slice);
}
} else {
let { pointsPerLine } = strips[0];
for (let i=0; i<pointsPerLine; i++) {
let slice = newSlice(i);
let points = rows.map(row => row[i]);
if (i % 2 === 1) points.reverse();
slice.index = i;
slice.camLines = [ newPolygon(points).setOpen() ];
slices.push(slice);
}
}
for (let slice of slices) {
slice.output()
.setLayer("lathe", { line: this.lineColor })
.addPoly(slice.camLines[0].clone().applyRotations());
}
// console.log({ webGPU: output, slices });
return slices;
}
async sliceWorker(onupdate) {
const { vertices, slices, resolution } = this;
@ -205,22 +312,28 @@ export class Topo {
async lathe(onupdate) {
const { minions } = self.kiri_worker;
if (minions?.running > 1) {
if (this.gpu) {
return await this.latheGPU(onupdate);
} else if (minions?.running > 1) {
return await this.latheMinions(onupdate);
} else {
return await this.latheWorker(onupdate);
}
}
async latheGPU(onupdate) {
return this.gpu_slices;
}
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)
// console.log({ tlen, slen, sinc, slices: slices.map(s => s.z) });
// cull slice lines to only the ones in range (~5x faster)
const oslices = [];
for (let slice of slices) {
const lines = slice.lines;
@ -244,7 +357,9 @@ export class Topo {
// 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) {
for (let sz = 0; ; sz += step) {
let si = Math.ceil(sz / resolution);
if (si >= oslices.length) break;
const rx = oslices[si].z;
let mz = -Infinity;
// iterate over tool offsets
@ -281,12 +396,12 @@ export class Topo {
}
if (mz === -Infinity && xo === 0 && yo === 0) {
// tool tip is off the model
continue;
// continue;
}
}
if (mz === -Infinity) {
mz = zBottom; // Math.min(0, lz)
} else if (zBottom && mz < zBottom) {
mz = zBottom;
} else if (mz < zBottom) {
mz = zBottom;
}
heights.push(rx, 0, lz = mz);