grid-apps-cmms/src/kiri/mode/cam/anim-3d-be.js
2025-12-12 22:26:58 -05:00

410 lines
12 KiB
JavaScript

/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
// WORKER BACK END ANIMATION CODE for 3D (indexed)
import { CSG } from '../../../geo/csg.js';
import { Tool } from './tool.js';
let nextMeshID = 1,
stock,
path,
pathIndex,
tool,
tools,
last,
settings,
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, minions } = worker;
dispatch.animate_setup2 = function (data, send) {
settings = data.settings;
const { controller, process } = settings;
const { print } = worker.current;
const isIndexed = process.camStockIndexed;
pathIndex = 0;
path = print.output.flat();
tools = settings.tools;
stock = settings.stock;
tool = null;
last = null;
animating = false;
animateClear = false;
stockIndex = 0;
indexCount = 0;
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.data({ mesh_move: { id: slice.id, pos: { x:0, y:0, z: stock.z/2} } });
}
send.done();
};
dispatch.animate2 = function (data, send) {
renderSpeed = data.speed || 0;
if (animating) {
return send.done();
}
renderSteps = data.steps || 1;
renderDone = false;
animating = renderSpeed > 0;
startTime = startTime || Date.now();
renderPath(send);
};
dispatch.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++;
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 = tool ? Math.min(tool.maxDiameter(), 1) / 4 : 1;
// 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 = (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); // 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, 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;
}
// 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;
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, da, sp, md } = pos;
toolMove(pos);
// console.log('renderMoves', {id, moves, seed});
let subs = 0;
if (dx || dy || dz < 0 || da)
for (let slice of stockSlices) {
if (slice.bounds.intersectsBox(toolMesh.bounds)) {
slice.subtractTool(toolMesh);
subs++;
}
}
// pause renderer at specified offsets
if (renderSpeed) {
let time = sp ? (md / sp) * 60000 : 10;
// console.log({ time, ...pos });
renderUpdate(send);
setTimeout(() => {
renderMoves(id, moves, send, index + 1);
}, time / renderSpeed);
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 Tool(settings, 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.isTaperBall()) {
const { b, r } = tool.getBallTaperParams();
// taper length excludes most of ball
const taperLen = flen - b;
// shaft at top
mesh = cylinder(slen, srad, srad, 20, true).translate(0, 0, slen / 2)
// taper cone in middle
.add(cylinder(taperLen, b, frad, 20, true).translate(0, 0, -taperLen / 2))
// exposed ball at bottom
.add(sphere(r, 20).translate(0, 0, -taperLen));
} 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 } });
}
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;
}
}