/** Copyright Stewart Allen -- All Rights Reserved */ "use strict"; // dep: kiri-mode.cam.driver gapp.register("kiri-mode.cam.animate", [], (root, exports) => { const { kiri } = root; const { driver } = kiri; const { CAM } = driver; let meshes = {}, unitScale = 1, progress, speedValues = [ 25, 12, 6, 3 ], speedNames = [ "1x", "2x", "4x", "8x" ], speedMax = speedValues.length - 1, speedIndex = 0, speedLabel, speed, color = 0, pauseButton, playButton, posOffset = { x:0, y:0, z:0 }; // ---( CLIENT FUNCTIONS )--- kiri.load(() => { if (!kiri.client) { return; } const { moto } = root; const { space } = moto; const { api } = kiri; function animate_clear(api) { kiri.client.animate_cleanup(); $('layer-animate').innerHTML = ''; $('layer-toolpos').innerHTML = ''; Object.keys(meshes).forEach(id => deleteMesh(id)); } function animate(api, delay) { kiri.client.animate_setup(api.conf.get(), data => { checkMeshCommands(data); if (!(data && data.mesh_add)) { return; } const UC = api.uc; const layer = $('layer-animate'); layer.innerHTML = ''; UC.setGroup(layer); UC.newRow([ UC.newButton(null,replay,{icon:'',title:"restart"}), playButton = UC.newButton(null,play,{icon:'',title:"play"}), pauseButton = UC.newButton(null,pause,{icon:'',title:"pause"}), UC.newButton(null,step,{icon:'',title:"single step"}), UC.newButton(null,fast,{icon:'',title:"toggle speed"}), UC.newButton(null,skip,{icon:'',title:"skip forward without animation"}), speedLabel = UC.newLabel("speed", {class:"speed"}), progress = UC.newLabel('0%', {class:"progress"}) ]); updateSpeed(); setTimeout(() => { play({steps: 1}); }, delay || 0); const toolpos = $('layer-toolpos'); toolpos.innerHTML = ''; UC.setGroup(toolpos); playButton.style.display = ''; pauseButton.style.display = 'none'; api.event.emit('animate', 'CAM'); }); } gapp.overlay(kiri.client, { animate(data, ondone) { kiri.client.send("animate", data, ondone); }, animate_setup(settings, ondone) { color = settings.controller.dark ? 0x888888 : 0; unitScale = settings.controller.units === 'in' ? 1/25.4 : 1; 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) { const geo = new THREE.BufferGeometry(); geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); geo.setIndex(new THREE.BufferAttribute(new Uint32Array(ind), 1)); const mat = new THREE.LineBasicMaterial({ transparent: true, opacity: 0.75, color }); const mesh = new THREE.LineSegments(geo, mat); space.world.add(mesh); meshes[id] = mesh; } function meshUpdates(id, updates) { const mesh = meshes[id]; if (!mesh) { return; // animate cancelled } const mpos = mesh.geometry.attributes.position; for (let i=0, il=updates.length; i 0) { speedIndex = (speedIndex + inc) % speedValues.length; } speed = speedValues[speedIndex]; speedLabel.innerText = speedNames[speedIndex]; } function replay() { CAM.animate_clear(api); setTimeout(() => { CAM.animate(api, 50); }, 250); } function checkMeshCommands(data) { if (!data) { return; } if (data.mesh_add) { const { id, ind, pos, offset } = data.mesh_add; meshAdd(id, ind, pos); if (offset) { posOffset = offset; } } if (data.mesh_del) { deleteMesh(data.mesh_del); } if (data.mesh_move) { const { id, pos } = data.mesh_move; const mesh = meshes[id]; if (mesh) { mesh.position.x = pos.x; mesh.position.y = pos.y; mesh.position.z = pos.z; space.update(); } } if (data.mesh_update) { meshUpdates(data.id, data.mesh_update); } } }); // ---( 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)); const step = rez; const stepsX = Math.floor(stock.x / step); const stepsY = Math.floor(stock.y / step); const { pos, ind } = createGrid(stepsX, stepsY, stock, step); const offset = { x: process.outputOriginCenter ? 0 : stock.x / 2, y: process.outputOriginCenter ? 0 : stock.y / 2, z: process.camOriginTop ? -stock.z : 0 } grid = pos; gridX = stepsX; gridY = stepsY; tool = null; last = null; animating = false; animateClear = false; center = Object.assign({}, stock.center); center.z -= stock.z / 2; send.data({ mesh_add: { id: 0, pos, ind, offset } }); send.data({ mesh_move: { id: 0, pos: center } }); send.done(); }; kiri.worker.animate = function(data, send) { if (data.speed >= 0) { renderSpeed = data.speed; } if (data.steps > 0) { stepsRemain = data.steps; } if (data.toend) { skipMode = !skipMode; } else { skipMode = false; } checkStash(send); if (animating) { return send.done(); } renderPath(send); }; kiri.worker.animate_cleanup = function(data, send) { if (animating) { animateClear = true; } }; function createGrid(stepsX, stepsY, size, step) { const gridPoints = stepsX * stepsY; const pos = new Float32Array(gridPoints * 3); const ind = []; const ox = size.x / 2; const oy = size.y / 2; const b = { mx: 0, my: 0, Mx: 0, My: 0}; // initialize grid points for (let x=0, ai=0; x 0) ind.appendAll([ (stepsY * x) + (y - 1), (stepsY * x) + (y ) ]); if (x > 0) ind.appendAll([ (stepsY * (x - 1)) + y, (stepsY * (x )) + y ]); b.mx = Math.min(b.mx, px); b.my = Math.min(b.my, py); b.Mx = Math.max(b.Mx, px); b.My = Math.max(b.My, py); } } return { pos, ind }; } let animateClear = false; let animating = false; let renderSpeed = 25; let stepsRemain = 0; let skipMode = false; let skipMove = null; let skipStash = []; function checkStash(send) { if (!skipMode && skipStash.length) { // merge & compress stash stack skipStash.forEach(mesh_update => { send.data({ id: 0, mesh_update }, [ mesh_update.buffer ]); }); skipStash = []; send.data({ mesh_move: skipMove }); } } function renderPath(send) { if (animateClear) { animateClear = false; animating = false; send.done(); return; } if (stepsRemain <= 0 || renderSpeed === 0) { skipMode = false; checkStash(send); animating = false; send.done(); return; } const next = path[pathIndex++]; if (!next) { skipMode = false; checkStash(send); animating = false; stepsRemain = 0; send.done(); return; } animating = true; stepsRemain--; if (next.tool >= 0 && (!tool || tool.getNumber() !== next.tool)) { // on real tool change, go to safe Z first if (tool) { let pos = last.point = { x: last.point.x, y: last.point.y, z: stock.z }; send.data({ mesh_move: { toolID, pos }}); } updateTool(next.tool, send); } const id = toolID; if (last) { const lp = last.point, np = next.point; // dwell ops have no point if (!np) { setTimeout(() => { renderPath(send) }, 0); return; } const dx = np.x - lp.x, dy = np.y - lp.y, dz = np.z - lp.z; // skip moves that are closer than resolution if (Math.sqrt(dx*dx +dy*dy + dz*dz) < rez) { setTimeout(() => { renderPath(send) }, 0); return; } const md = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz)); const st = Math.ceil(md / rez); const mx = dx / st, my = dy / st, mz = dz / st; const moves = []; for (let i=0, x=lp.x, y=lp.y, z=lp.z; i { renderPath(send) }, 0); } last = next; } function renderMoves(id, moves, send) { let index = 0; function update() { if (animateClear) { return renderPath(send); } if (renderSpeed > 0 && index < moves.length) { const pos = moves[index++]; if (!pos) throw `no pos @ ${index} of ${moves.length}`; tool.pos = pos; updateMesh(pos, send); if (skipMode) { skipMove = { id, pos }; } else { send.data({ mesh_move: { id, pos } }); } } let pauseTime = skipMode ? 0 : renderSpeed; if (index < moves.length) { setTimeout(update, pauseTime); } else { setTimeout(() => { renderPath(send) }, pauseTime); } } update(0); } function updateMesh(pos, send) { const prof = tool.profile; const { size, pix } = tool.profileDim; const mid = Math.floor(pix / 2); const update = new Float32Array(Math.round(prof.length * 0.8)); const rx = Math.floor((pos.x + stock.x / 2 - size / 2 - center.x) / rez); const ry = Math.floor((pos.y + stock.y / 2 - size / 2 - center.y) / rez); let upos = 0; // deform mesh to lowest point on tool profile for (let i=0, il=prof.length; i < il; ) { const dx = mid + prof[i++]; const dy = mid + prof[i++]; const dz = prof[i++]; const gx = rx + dx; const gy = ry + dy; if (gx < 0|| gy < 0 || gx > gridX-1 || gy > gridY-1) { continue; } const gi = gx * gridY + gy; const iz = gi * 3 + 2; const cz = grid[iz]; const tz = tool.pos.z - dz; if (tz < cz) { update[upos++] = iz; update[upos++] = tz; grid[iz] = tz; } } if (upos > 0) { const mesh_update = update.slice(0,upos); if (skipMode) { skipStash.push(mesh_update); } else { send.data({ id: 0, mesh_update }, [ mesh_update.buffer ]); } } } function updateTool(toolnum, send) { if (tool) { send.data({ mesh_del: toolID }); } tool = new CAM.Tool({ tools }, undefined, toolnum); tool.generateProfile(rez); const flen = tool.fluteLength() || 15; const slen = tool.shaftLength() || 15; // const frad = tool.fluteDiameter() / 2; const prof = tool.profile; const { size, pix } = tool.profileDim; const { pos, ind } = createGrid(pix, pix, {x:size, y:size, z:flen+slen}, rez); const mid = Math.floor(pix/2); // deform mesh to fit tool profile for (let i=0, il=prof.length; i < il; ) { const dx = mid + prof[i++]; const dy = mid + prof[i++]; const dz = prof[i++]; pos[(dx * pix + dy) * 3 + 2] = -dz; } send.data({ mesh_add: { id:++toolID, pos, ind }}); } // load renderer code in worker context only if (kiri.worker && false) fetch('/wasm/kiri-ani.wasm') .then(response => response.arrayBuffer()) .then(bytes => WebAssembly.instantiate(bytes, { env: { reportf: (a,b) => { console.log('[f]',a,b) }, reporti: (a,b) => { console.log('[i]',a,b) } } })) .then(results => { let {module, instance} = results; let {exports} = instance; let heap = new Uint8Array(exports.memory.buffer); let wasm = self.wasm = { heap, memory: exports.memory, updateMesh: exports.updateMesh }; // heap[0] = 5; // heap[100] = 6; // heap[200] = 8; // let rv = self.wasm.updateMesh(0, 0, 100, 200); }); }); });