diff --git a/.gitignore b/.gitignore index c66e12fd..3132d9dc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,8 @@ jszip-esm.js logs mod node_modules +pack package-lock.json three.js tmp -pack +web/boot/bundle.bin diff --git a/app.js b/app.js index f5e629f4..57b03227 100644 --- a/app.js +++ b/app.js @@ -37,6 +37,7 @@ let util; let dir; let log; let pre; +let alt; const EventEmitter = require('events'); class AppEmitter extends EventEmitter {} @@ -64,6 +65,7 @@ function init(mod) { startTime = time(); lastmod = mod.util.lastmod; logger = mod.log; + alt = ENV.alt; pre = ENV.pre || mod.meta.pre; debug = ENV.debug || mod.meta.debug; oversion = ENV.over || mod.meta.over; @@ -191,6 +193,19 @@ function init(mod) { append[key] = minify(append[key]); } } + + if (alt) { + for (let [ key, val ] of Object.entries(append)) { + let src = `src/main/${key}.js`; + if (!fs.existsSync(src)) { + logger.log('missing:', src); + continue; + } + fs.mkdirSync(`alt/main`, { recursive: true }); + let body = fs.readFileSync(src); + fs.writeFileSync(`alt/main/${key}.js`, body + val); + } + } }; // either add module assets to path or require(init.js) @@ -288,14 +303,14 @@ function initModule(mod, file, dir) { const path = mod.dir + '/' + dir + '/' + file; try { const body = fs.readFileSync(path); - // console.log({ inject: code, file, opt }); + logger.log({ inject: code, file, opt }); if (opt.first) { append[code] = body.toString() + '\n' + append[code]; } else { append[code] += body.toString() + '\n'; } } catch (e) { - console.log({ missing_file: path, dir, mod }); + logger.log({ missing_file: path, dir, mod }); } }, onload(fn) { @@ -430,6 +445,10 @@ function generateDevices() { let dstr = JSON.stringify(devs); util.mkdir(PATH.join(dir,"src","pack")); fs.writeFileSync(PATH.join(dir,"src","pack","kiri-devs.js"), `export const devices = ${dstr};`); + if (alt) { + fs.mkdirSync('alt/pack', { recursive: true }); + fs.writeFileSync(PATH.join(dir,"alt","pack","kiri-devs.js"), `export const devices = ${dstr};`); + } } function minify(code) { @@ -526,7 +545,13 @@ function fixedmap(prefix, map) { // HTTP 307 redirect function redir(path, type) { return (req, res, next) => { - http.redirect(res, path, type); + let query = req.url.split('?')[1]; + let rpath = path; + if (query && path.indexOf('?') < 0) { + rpath += `?${query}`; + } + // console.log({ redir: req.url, to: path, query, new_path: rpath }); + http.redirect(res, rpath, type); } } diff --git a/bin/bundle.config.json b/bin/bundle.config.json index 655fa67e..e7bd3566 100644 --- a/bin/bundle.config.json +++ b/bin/bundle.config.json @@ -1,11 +1,17 @@ { + "compress": false, "outputs": { - "bundle": "./web/kiri/bundle.bin" + "bundle": "./web/boot/bundle.bin" }, "inputs": [ - { "root": "./web", "prefix": "" }, - { "root": "./src", "prefix": "lib" }, - { "root": "./src/wasm", "prefix": "wasm" }, - { "root": "./src/wasm", "prefix": "lib/kiri/wasm" } + { "root": "web", "prefix": "" }, + { "root": "alt", "prefix": "lib"}, + { "root": "src", "prefix": "lib"}, + { "root": "src/wasm", "prefix": "wasm" }, + { "root": "src/wasm", "prefix": "lib/kiri/wasm" } + ], + "excludes": [ + "src/pack", + "web/boot/bundle.bin" ] } diff --git a/bin/bundler.mjs b/bin/bundler.mjs index 21cec2bd..e63aba3e 100644 --- a/bin/bundler.mjs +++ b/bin/bundler.mjs @@ -3,11 +3,21 @@ import path from 'path'; import uglify from 'uglify-js'; import { Buffer } from 'buffer'; -const cfg = JSON.parse(fs.readFileSync(process.argv[2] || './bundle.config.json', 'utf8')); -const compress = process.env.UGLIFY ? true : false +const cfg = JSON.parse(fs.readFileSync(process.argv[2] || './bin/bundle.config.json', 'utf8')); +const { inputs, outputs, excludes, compress } = cfg; +const envcomp = process.env.COMPRESS ? JSON.parse(process.env.COMPRESS) : undefined; +const debug = process.env.DEBUG; // --- recursive directory walker (follows symlinks safely) --- function* walk(dir, seen = new Set()) { + dir = path.normalize(dir); + if (excludes.indexOf(dir) >= 0) { + return; + } + if (!fs.existsSync(dir)) { + return; + } + const real = fs.realpathSync(dir); if (seen.has(real)) { return; @@ -25,13 +35,19 @@ function* walk(dir, seen = new Set()) { // --- collect all files according to config --- let entries = []; -for (const input of cfg.inputs) { +let virtMap = new Set(); +for (const input of inputs) { const root = input.root; const prefix = input.prefix || ''; for (const file of walk(root)) { const rel = path.relative(root, file).replace(/\\/g, '/'); const virt = prefix ? `${prefix}/${rel}` : rel; - entries.push({ file, virt }); + if (virtMap.has(virt)) { + console.log('pre-existing', virt, rel, root); + } else { + entries.push({ file, virt }); + virtMap.add(virt); + } } } @@ -40,28 +56,54 @@ const dataParts = []; const entryMeta = []; let offset = 0; let cache = new Map(); +let compEnable = (envcomp ?? compress); +let cacheDir = cfg.cacheDir ?? ".bcache"; + +if (compEnable) { + fs.mkdirSync(cacheDir, { recursive: true }); +} + +function fileTime(path) { + try { + return (fs.statSync(path).ctimeMs/1000) | 0; + } catch (e) { + return 0; + } +} for (const { file, virt } of entries) { + if (excludes.indexOf(file) >= 0) { + continue; + } let record = cache.get(file); if (record) { - // console.log({ dup: file }); + if (debug) console.log({ alias: virt, from: file }); entryMeta.push({ ...record, virt }); } else { + if (debug) console.log({ write: virt, from: file }); let data = fs.readFileSync(file); - if (compress && file.endsWith("js")) { - console.log('compress', file); - data = uglify.minify(data.toString(), { - compress: { - merge_vars: false, - unused: false + if (compEnable && file.endsWith("js")) { + let cpath = path.resolve(cacheDir, file); + if (fileTime(cpath) > fileTime(file)) { + data = fs.readFileSync(cpath); + console.log('cached', file); + } else { + console.log('compress', file); + data = uglify.minify(data.toString(), { + compress: { + merge_vars: false, + unused: false + } + }); + // console.log({ to: typeof(data), data }); + if (!data.code) { + if (debug) console.log({ skip: file }); + continue; } - }); - // console.log({ to: typeof(data), data }); - if (!data.code) { - console.log({ skip: file }); - continue; + data = Buffer.from(data.code); + fs.mkdirSync(path.dirname(cpath), { recursive: true }); + fs.writeFileSync(cpath, data); } - data = Buffer.from(data.code); } record = { virt, offset, length: data.length }; cache.set(file, record); @@ -99,7 +141,7 @@ const header = Buffer.concat([count, ...headerParts]); const full = Buffer.concat([header, ...dataParts]); // --- write final bundle --- -const outPath = cfg.outputs.bundle || './bundle.bin'; +const outPath = outputs.bundle || './bundle.bin'; fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, full); // ⚡ write raw binary diff --git a/notes.md b/notes.md index 56c74577..e39f50d8 100644 --- a/notes.md +++ b/notes.md @@ -32,7 +32,6 @@ * `B` multi-extruder rendering of raft fails to offset the rest of the print * `B` multi-extruder purge blocks fail to generate properly for rafts -* `F` new parameter to cap output flow rate (on printer device) * `F` new parameters for bridging speed / bridge fan control * `F` convert ranges to z offsets while continuing to show layer # * `F` support pillar top/bottom should conform to part diff --git a/package.json b/package.json index b0880a91..88a094d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grid-apps", - "version": "4.3.3", + "version": "4.3.4", "description": "grid.space 3d slicing & modeling tools", "author": "Stewart Allen ", "license": "MIT", @@ -61,7 +61,7 @@ "three": "^0.178.0", "three-mesh-bvh": "^0.7.6", "uglify-js": "3.14.5", - "validator": ">=13.7.0", + "validator": ">=13.15.15", "ws": "^8.18.2" }, "devDependencies": { @@ -82,7 +82,7 @@ "webpack-cli": "^5.1.4" }, "scripts": { - "setup": "npm run setup-mods && npm i && npm run clean && npm run dev -- --dryrun && npm run webpack-ext", + "setup": "npm run setup-mods && npm i && npm run clean && npm run dev -- --alt --dryrun && npm run webpack-ext", "setup-mods": "cd mods && npm i", "build": "npm run prebuild && electron-builder", "build-nopublish": "npm run prebuild && electron-builder --publish never", @@ -93,13 +93,13 @@ "build-win-arm": "npm run build -- --win --arm64", "build-mac": "npm run build -- --mac --arm64", "build-mac-intel": "npm run build -- --mac --x64", - "clean": "rm -rf build data dist src/pack tmp src.old web.old", + "clean": "rm -rf build data dist src/pack tmp src.old web.old .bcache", "docs-dev": "docusaurus start --config conf/docusaurus.config.js", "docs-build": "docusaurus build --config conf/docusaurus.config.js", "docs-serve": "docusaurus serve --config conf/docusaurus.config.js", "docs-check": "prettier --config ./conf/prettier.config.js ./docs --check", "dev": "npm run webpack-ext && gs-app-server --debug --single", - "prod": "npm run webpack-ext && npm run webpack-src prod && gs-app-server", + "prod": "npm run webpack-ext && npm run webpack-src prod && COMPRESS=1 node bin/bundler.mjs && gs-app-server", "prod-pack": "npm run webpack-ext && npm run webpack-src prod", "prod-dryrun": "gs-app-server --dryrun", "start": "npm run prebuild prod && electron .", diff --git a/src/kiri/core/codec.js b/src/kiri/core/codec.js index 3547fb6f..1ad844ac 100644 --- a/src/kiri/core/codec.js +++ b/src/kiri/core/codec.js @@ -225,7 +225,9 @@ Widget.prototype.encode = function(state) { json: json, // safe for JSON (float32array mess) group: this.group.id, track: this.track, - geo: json ? Array.from(geo) : geo + geo: json ? Array.from(geo) : geo, + meta: this.meta, + anno: this.anno, }; return coded; }; @@ -234,8 +236,12 @@ registerDecoder(TYPE.WIDGET, function(v, state) { const id = v.id, group = v.group || id, track = v.track || undefined, - widget = newWidget(id, Widget.Groups.forid(group)); + widget = newWidget(id, Widget.Groups.forid(group)), + meta= v.meta || widget.meta, + anno = v.anno || widget.anno; + widget.meta = meta; + widget.anno = anno; widget.loadVertices(v.json ? v.geo.toFloat32() : v.geo); widget.saved = Date.now(); if (track && track.pos) { diff --git a/src/kiri/core/conf.js b/src/kiri/core/conf.js index affad216..ae5512d5 100644 --- a/src/kiri/core/conf.js +++ b/src/kiri/core/conf.js @@ -270,9 +270,7 @@ export const conf = { firstLayerFanSpeed: 0, firstLayerFillRate: 35, firstLayerFlatten: 0, - firstLayerLineMult: 1.0, firstLayerNozzleTemp: 0, - firstLayerPrintMult: 1.0, firstLayerRate: 30, firstLayerYOffset: 0, firstSliceHeight: 0.25, @@ -293,6 +291,7 @@ export const conf = { outputInvertY: false, outputLayerRetract: false, outputLoops: 0, + outputMaxFlowrate: 15, outputMinLayerTime: 10, outputMinSpeed: 5.0, outputPurgeTower: 0, @@ -302,7 +301,7 @@ export const conf = { outputRetractDwell: 20, outputRetractSpeed: 40, outputRetractWipe: 0, - outputScarfLength: 5, + outputScarfLength: 0, outputSeekrate: 80, outputShellMult: 1.25, outputShortPoly: 0.0, @@ -319,7 +318,6 @@ export const conf = { sliceFillAngle: 45, sliceFillGrow: 0, sliceFillOverlap: 0.35, - sliceFillRate: 0, sliceFillRepeat: 1, sliceFillSparse: 0.25, sliceFillType: "hex", @@ -333,7 +331,6 @@ export const conf = { sliceShellOrder: "in-out", sliceShells: 3, sliceSolidMinArea: 1, - sliceSolidRate: 0, sliceSupportAngle: 50, sliceSupportArea: 0.1, sliceSupportDensity: 0.1, @@ -500,6 +497,8 @@ export const conf = { camLatheSpeed: 500, camLatheSpindle: 1000, camLatheTool: 1000, + camLatheOffStart: 0, + camLatheOffEnd: 0, camLevelDown: 0, camLevelInset: 0.5, camLevelOver: 0.75, diff --git a/src/kiri/mode/cam/anim-3d-be.js b/src/kiri/mode/cam/anim-3d-be.js index 204e5b2a..13df6084 100644 --- a/src/kiri/mode/cam/anim-3d-be.js +++ b/src/kiri/mode/cam/anim-3d-be.js @@ -5,31 +5,31 @@ 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, + showStock = false, + animateClear = false, + animating = false, + renderDone = false, + renderSteps = 0, + renderSpeed = 0, + indexCount = 0, + updates = 0, + maxR; export function init(worker) { const { dispatch } = worker; @@ -45,8 +45,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,11 +54,14 @@ 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 (showStock) 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); @@ -74,7 +75,6 @@ export function init(worker) { }; dispatch.animate2 = function (data, send) { - renderPause = data.pause || renderPause; renderSpeed = data.speed || 0; if (animating) { return send.done(); @@ -126,7 +126,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 +142,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, 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,7 +202,7 @@ 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, sp, md } = pos; toolMove(pos); // console.log('renderMoves', {id, moves, seed}); let subs = 0; @@ -208,15 +213,14 @@ function renderMoves(id, moves, send, seed = 0) { 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; } } diff --git a/src/kiri/mode/cam/anim-3d-fe.js b/src/kiri/mode/cam/anim-3d-fe.js index 3f31d97f..2300fdab 100644 --- a/src/kiri/mode/cam/anim-3d-fe.js +++ b/src/kiri/mode/cam/anim-3d-fe.js @@ -10,24 +10,26 @@ 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, + 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) { @@ -86,8 +88,9 @@ Object.assign(client, { }, animate_setup2(settings, ondone) { + initPathMesh(); color = settings.controller.dark ? 0x888888 : 0; - material = new THREE.MeshPhongMaterial({ + material = material ?? new THREE.MeshPhongMaterial({ flatShading: true, transparent: false, opacity: 0.5, @@ -102,6 +105,66 @@ 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: 0xffff00 }), + 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); + 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 (bool !== lines.visible) { + 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 +195,7 @@ function meshUpdate(id, ind, pos, ilen, plen) { function deleteMesh(id) { space.world.remove(meshes[id]); + meshes[id].geometry.dispose(); delete meshes[id]; } @@ -165,8 +229,7 @@ function play(opts) { } client.animate2({ speed, - steps: steps || Infinity, - pause: speedPauses[speedIndex] + steps: steps || Infinity }, handleUpdate); } @@ -177,8 +240,7 @@ function fast(opts) { button.pause.style.display = ''; client.animate2({ speed, - steps: steps || Infinity, - pause: speedPauses[speedIndex] + steps: steps || Infinity }, handleUpdate); } @@ -216,6 +278,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 +292,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) { diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index 944f8d2e..5d05ed31 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -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; @@ -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" }), diff --git a/src/kiri/mode/cam/client.js b/src/kiri/mode/cam/client.js index 8f6af172..65eba6db 100644 --- a/src/kiri/mode/cam/client.js +++ b/src/kiri/mode/cam/client.js @@ -29,6 +29,7 @@ const { widgets: WIDGETS } = api; class Client { animVer = 0; camStock; + showStock = true; current; currentIndex; flipping; @@ -640,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); diff --git a/src/kiri/mode/cam/export.js b/src/kiri/mode/cam/export.js index 84afee08..41280974 100644 --- a/src/kiri/mode/cam/export.js +++ b/src/kiri/mode/cam/export.js @@ -257,17 +257,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; diff --git a/src/kiri/mode/cam/prepare.js b/src/kiri/mode/cam/prepare.js index cbeefb87..9f3b19b4 100644 --- a/src/kiri/mode/cam/prepare.js +++ b/src/kiri/mode/cam/prepare.js @@ -412,7 +412,7 @@ export function prepEach(widget, settings, print, firstPoint, update) { } // 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 diff --git a/src/kiri/mode/cam/topo4.js b/src/kiri/mode/cam/topo4.js index 5bd4f528..f29934c2 100644 --- a/src/kiri/mode/cam/topo4.js +++ b/src/kiri/mode/cam/topo4.js @@ -38,9 +38,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,17 +51,20 @@ 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.zoff = widget.track.top || 0; this.leave = op.leave || 0; this.linear = op.linear || false; this.lineColor = state.settings.controller.dark ? 0xffff00 : 0x555500; + this.offStart = op.offStart ?? 0; + this.offEnd = op.offEnd ?? 0; onupdate(0, "lathe"); @@ -86,8 +88,8 @@ export class Topo { } async slice(onupdate) { - const { vertices, resolution, tabverts, zoff } = this; - + const { vertices, resolution, tabverts, zoff, offStart, offEnd, tool, unit } = this; + const { minions } = self.kiri_worker; const range = this.range = { min: Infinity, max: -Infinity }; const box = this.box = new THREE.Box2(); @@ -101,9 +103,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 * unit; + range.max -= offEnd * unit; + 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 +119,31 @@ export class Topo { // re-create shared vertex array for workers this.vertices = [].appendAll(vertices).appendAll(tabverts).toFloat32().toShared(); + const zSpan = range.max - range.min; const shards = Math.ceil(Math.min(25, vertices.length / 27000)); - const step = (range.max - range.min) / shards; + const totalSteps = Math.ceil(zSpan / resolution); + const stardSteps = Math.ceil(totalSteps / shards); + const stepWidth = stardSteps * resolution; - 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++; + for (let i=0; i 1) { return await this.sliceMinions(onupdate); } else { @@ -217,10 +227,10 @@ export class Topo { 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 +254,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 +293,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); diff --git a/src/kiri/mode/fdm/client.js b/src/kiri/mode/fdm/client.js index 7bb6d5cf..466856f9 100644 --- a/src/kiri/mode/fdm/client.js +++ b/src/kiri/mode/fdm/client.js @@ -34,14 +34,11 @@ export function init() { "sliceFillType": LANG.fi_type, "sliceFillWidth": LANG.fi_wdth_s, "sliceFillSparse": LANG.fi_pcnt_s, - "sliceFillRate": LANG.ou_feed_s, "sliceFillGrow": LANG.fi_grow_s, - "sliceSolidRate": LANG.ou_fini_s, "sliceSolidMinArea": LANG.ad_msol_s, "sliceTopLayers": LANG.sl_ltop_s, "sliceBottomLayers": LANG.sl_lbot_s, "firstLayerRate": LANG.fl_rate_s, - "firstLayerPrintMult": LANG.fl_mult_s, "firstLayerFanSpeed": LANG.ou_fans_s, // prepare "sliceShellOrder": LANG.sl_ordr_s, diff --git a/src/kiri/mode/fdm/driver-be.js b/src/kiri/mode/fdm/driver-be.js index f1164acc..2efb734d 100644 --- a/src/kiri/mode/fdm/driver-be.js +++ b/src/kiri/mode/fdm/driver-be.js @@ -9,6 +9,7 @@ import { fdm_export } from './export.js'; // fil = filament diameter // slice = slice height function extrudePerMM(noz, fil, slice) { + // (ratio of nozzle to filament) * (ratio of slice to nozzle) return ((Math.PI * util.sqr(noz / 2)) / (Math.PI * util.sqr(fil / 2))) * (slice / noz); }; diff --git a/src/kiri/mode/fdm/export.js b/src/kiri/mode/fdm/export.js index 6b359f56..a65d6ca9 100644 --- a/src/kiri/mode/fdm/export.js +++ b/src/kiri/mode/fdm/export.js @@ -26,7 +26,6 @@ export function fdm_export(print, online, ondone, ondebug) { { extrudeAbs } = device, { exportThumb } = controller, { extrudeMM, extrudePerMM } = FDM, - extused = Object.keys(print.extruders).map(v => parseInt(v)), timeFactor = (device.gcodeTime || 1) * 1.5, decimals = config.gcode_decimals || 4, zMoveMax = device.deviceZMax || 0, @@ -49,7 +48,6 @@ export function fdm_export(print, online, ondone, ondebug) { last = null, zpos = 0, bmax = 0, - blastz = 0, belt_add_y = (process.firstLayerYOffset || 0) - (belty || 0), oloops = process.outputLoops || 0, zhop = process.zHopDistance || 0, // range @@ -58,10 +56,8 @@ export function fdm_export(print, online, ondone, ondebug) { retDist = process.outputRetractDist || 0, // range retSpeed = process.outputRetractSpeed * 60 || 1, // range retDwell = process.outputRetractDwell || 0, // range - timeDwell = retDwell / 1000, scarfZ = process.outputScarfLength ? true : false, arcDist = isBelt || scarfZ ? 0 : (process.arcTolerance || 0), - arcMin = 1, arcRes = 20, arcDev = 0.5, arcMax = 40, @@ -85,7 +81,6 @@ export function fdm_export(print, online, ondone, ondebug) { fanSpeedSave = undefined, fanSpeedBase = 0, fanSpeed = 0, - lastType = undefined, lastNozzleTemp = nozzleTemp, lastBedTemp = bedTemp, lastFanSpeed = fanSpeed, @@ -169,7 +164,6 @@ export function fdm_export(print, online, ondone, ondebug) { retDist = params.outputRetractDist || 0; // range retSpeed = params.outputRetractSpeed * 60 || 1; // range retDwell = params.outputRetractDwell || 0; // range - timeDwell = retDwell / 1000; nozzleTemp = layer === 0 ? params.firstLayerNozzleTemp || params.outputTemp : params.outputTemp || params.firstLayerNozzleTemp; @@ -470,7 +464,7 @@ export function fdm_export(print, online, ondone, ondebug) { if (isBelt) { let zheight = path ? path.height || 0 : 0; epos.x = originCenter ? -pos.x : device.bedWidth - pos.x; - epos.z = blastz = pos.z * icos; + epos.z = pos.z * icos; epos.y = -pos.y + epos.z * bcos + belt_add_y; lout = epos; } @@ -635,7 +629,7 @@ export function fdm_export(print, online, ondone, ondebug) { // emit comment on output type chage if (last && out.type !== last.type) { - lastType = subst.feature = out.type; + subst.feature = out.type; if (gcodeFeature && gcodeFeature.length) { appendAllSub(gcodeFeature); } else { @@ -752,7 +746,6 @@ export function fdm_export(print, online, ondone, ondebug) { } // if new point is off the arc - // if (deem || depm || desp || dc > arcDist || cc.r < arcMin || cc.r > arcMax || dist > cc.r) { if (deem || depm || desp || dc * arcQ.center.length / arcQ.rSum > arcDist || dist > cc.r || cc.r > arcMax || radFault || !arcValid()) { // let debug = [deem, depm, desp, dc * arcQ.center.length / arcQ.rSum > arcDist, dist > cc.r, cc.r > arcMax, radFault]; if (arcQ.length === 4) { diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index 07571f0b..453d83da 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -79,6 +79,7 @@ export function menu() { _____: newGroup(LANG.sw_menu, $('fdm-walls'), { modes:FDM, driven, hideable, separator, group:"fdm-walls" }), sliceShells: newInput(LANG.sl_shel_s, { title:LANG.sl_shel_l, convert:toFloat }), sliceLineWidth: newInput(LANG.sl_line_s, { title:LANG.sl_line_l, convert:toFloat, bound:bound(0,5) }), + sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,1.0)}), separator: newBlank({ class:"set-sep", driven }), sliceShellOrder: newSelect(LANG.sl_ordr_s, { title:LANG.sl_ordr_l}, "shell"), sliceDetectThin: newSelect(LANG.ad_thin_s, { title: LANG.ad_thin_l, action: thinWallSave }, "thin"), @@ -86,19 +87,17 @@ export function menu() { sliceZInterleave: newBoolean(LANG.ad_zint_s, onBooleanClick, {title:LANG.ad_zint_l, show:zIntShow}), _____: newGroup(LANG.fs_menu, $('fdm-solid'), { modes:FDM, driven, hideable, separator, group:"fdm-solid" }), sliceFillAngle: newInput(LANG.fi_angl_s, {title:LANG.fi_angl_l, convert:toFloat}), - sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,2.0)}), sliceFillGrow: newInput(LANG.fi_grow_s, {title:LANG.fi_grow_l, convert:toFloat}), sliceSolidMinArea: newInput(LANG.ad_msol_s, {title:LANG.ad_msol_l, convert:toFloat}), _____: newGroup(LANG.fi_menu, $('fdm-fill'), { modes:FDM, driven, hideable, separator, group:"fdm-fill" }), sliceFillType: newSelect(LANG.fi_type, {trigger}, "infill"), sliceFillSparse: newInput(LANG.fi_pcnt_s, {title:LANG.fi_pcnt_l, convert:toFloat, bound:bound(0.0,1.0), show:hasInfill}), sliceFillRepeat: newInput(LANG.fi_rept_s, {title:LANG.fi_rept_l, convert:toInt, bound:bound(1,10), show:fillIsLinear}), - separator: newBlank({ class:"set-sep", driven }), - sliceFillRate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt, bound:bound(0,500)}), - sliceSolidRate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt, bound:bound(0,500)}), _____: newGroup(LANG.fh_menu, $('fdm-heat'), { modes:FDM, driven, hideable, separator, group:"fdm-heat" }), outputTemp: newInput(LANG.ou_nozl_s, {title:LANG.ou_nozl_l, convert:toInt}), outputBedTemp: newInput(LANG.ou_bedd_s, {title:LANG.ou_bedd_l, convert:toInt}), + separator: newBlank({ class:"set-sep", driven }), + outputDraftShield: newBoolean(LANG.fr_draf_s, onBooleanClick, {title:LANG.fr_draf_l, trigger, show:() => isNotBelt()}), _____: newGroup(LANG.fc_menu, $('fdm-cool'), { modes:FDM, driven, hideable, separator, group:"fdm-cool" }), outputFanLayer: newInput(LANG.ou_fanl_s, { title:LANG.ou_fanl_l, convert:toInt, bound:bound(0,255) }), outputFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255)}), @@ -137,9 +136,6 @@ export function menu() { firstLayerFlatten: newInput(LANG.fl_flat_s, {title:LANG.fl_flat_l, convert:toFloat, show:isBelt}), firstLayerRate: newInput(LANG.fl_rate_s, {title:LANG.fl_rate_l, convert:toFloat}), firstLayerFillRate: newInput(LANG.fl_frat_s, {title:LANG.fl_frat_l, convert:toFloat, show:isNotBelt}), - separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), - firstLayerLineMult: newInput(LANG.fl_sfac_s, {title:LANG.fl_sfac_l, convert:toFloat, bound:bound(0.5,2), show:isNotBelt}), - firstLayerPrintMult: newInput(LANG.fl_mult_s, {title:LANG.fl_mult_l, convert:toFloat}), separator: newBlank({ class:"set-sep", driven, show:isBelt }), firstLayerBrim: newInput(LANG.fl_brim_s, {title:LANG.fl_brim_l, convert:toInt, show:isBelt}), firstLayerBrimIn: newInput(LANG.fl_brin_s, {title:LANG.fl_brin_l, convert:toInt, show:isBelt}), @@ -154,7 +150,6 @@ export function menu() { outputRaftSpacing: newInput(LANG.fr_spac_s, {title:LANG.fr_spac_l, convert:toFloat, bound:bound(0.0,3.0), show: () => ui.outputRaft.checked && isNotBelt() }), separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), outputRaft: newBoolean(LANG.fr_nabl_s, onBooleanClick, {title:LANG.fr_nabl_l, trigger, show:() => isNotBelt()}), - outputDraftShield: newBoolean(LANG.fr_draf_s, onBooleanClick, {title:LANG.fr_draf_l, trigger, show:() => isNotBelt()}), _____: newGroup(LANG.ou_menu, $('fdm-output'), { modes:FDM, driven, hideable, separator, group:"fdm-out" }), outputFeedrate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt}), outputFinishrate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt}), @@ -177,6 +172,7 @@ export function menu() { arcTolerance: newInput(LANG.ad_arct_s, {title:LANG.ad_arct_l, convert:toFloat, bound:bound(0,1.0), show:() => { return isNotBelt() }}), outputRetractDwell: newInput(LANG.ad_rdwl_s, {title:LANG.ad_rdwl_l, convert:toInt}), outputMinSpeed: newInput(LANG.ad_mins_s, {title:LANG.ad_mins_l, convert:toFloat, bound:bound(1,200)}), + outputMaxFlowrate: newInput(LANG.ad_maxf_s, {title:LANG.ad_maxf_l, convert:toFloat, bound:bound(1,200)}), outputPurgeTower: newInput(LANG.ad_purg_s, {title:LANG.ad_purg_l, convert:toInt, bound:bound(0,1000)}), outputScarfLength: newInput(LANG.ad_scar_s, {title:LANG.ad_scar_l, convert:toFloat, bound:bound(0,1000)}), outputShortPoly: newInput(LANG.ad_spol_s, {title:LANG.ad_spol_l, convert:toFloat, bound:bound(0,10000)}), diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index b910958a..88168a09 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -82,12 +82,9 @@ export function slicePost(data, options) { let trg = shellFrac > 0.5 ? 1 : parts - 1; sliceShells += rem >= trg ? 1 : 0; } - const isFirst = index === 0; - const height = process.sliceHeight; - const spaceMult = isFirst ? process.firstLayerLineMult || 1 : 1; const count = isSynth ? 1 : sliceShells; - const offset = shellOffset * spaceMult; - const fillOff = fillOffset * spaceMult; + const offset = shellOffset; + const fillOff = fillOffset; const thinType = isSynth ? undefined : process.sliceDetectThin; const nutops = []; // co-locate shell processing with top generation in slicer diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index 301d47fb..680cc075 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -1,5 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +import { FDM } from './driver-be.js'; import { base, util } from '../../../geo/base.js'; import { poly2polyEmit, tip2tipEmit } from '../../../geo/paths.js'; import { newBounds } from '../../../geo/bounds.js'; @@ -682,7 +683,6 @@ export async function fdm_prepare(widgets, settings, update) { for (let rec of layer) { overate = rec.overate >= 0 ? rec.overate : overate; let brate = params.firstLayerRate || firstLayerRate; - let bmult = params.firstLayerPrintMult || params.firstLayerPrintMult; let point = rec.point; let belty = rec.belty = -point.y + (point.z * belt.slope); let lowrate = belty <= thresh ? brate : overate || brate; @@ -694,7 +694,6 @@ export async function fdm_prepare(widgets, settings, update) { if (rec.emit && belty <= thresh && lastout && Math.abs(lastout.belty - belty) < 0.01) { // apply base speed to segments touching belt rec.speed = params.firstLayerRate || Math.min(rec.speed, lowrate); - rec.emit *= bmult; rec.fan = params.firstLayerFanSpeed; minx = Math.min(minx, point.x, lastout.point.x); maxx = Math.max(maxx, point.x, lastout.point.x); @@ -794,10 +793,22 @@ export async function fdm_prepare(widgets, settings, update) { } }; + +/** + * Output and route Slice (shell,fill) paths for a single Slice + * + * @param {*} print Print output Object + * @param {*} slice current slice being Output + * @param {*} startPoint starting point for this layer + * @param {*} offset start point offset + * @param {*} output output prebuffer + * @param {*} opt options + */ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { const { settings } = print; const { device } = settings; const { bedWidth, bedDepth, bedRound } = device; + const { extrudePerMM } = FDM; let preout = [], process = opt.params || settings.process, @@ -806,6 +817,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { scarfLength = process.outputScarfLength || 0, minLayerTime = process.outputMinLayerTime || 0, nozzleSize = process.sliceLineWidth || device.extruders[extruder].extNozzle, + nozzles = device.extruders.map(e => e.extNozzle), firstLayer = (opt.first || false) && !opt.support, thinWall = nozzleSize * (opt.thinWall || 1.75), retractDist = opt.retractOver || (nozzleSize * 5), @@ -817,17 +829,17 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { fanSpeed = process.outputFanSpeed, firstShellSpeed = process.firstLayerRate, firstFillSpeed = process.firstLayerFillRate, - firstPrintMult = process.firstLayerPrintMult, finishSpeed = firstLayer ? firstShellSpeed : (opt.speed || process.outputFinishrate), printSpeed = opt.speed || (firstLayer ? firstShellSpeed : process.outputFeedrate), fillSpeed = opt.speed || opt.fillSpeed || (firstLayer ? firstFillSpeed || firstShellSpeed : process.outputFeedrate), - infillSpeed = process.sliceFillRate || opt.infillSpeed || fillSpeed || printSpeed, + infillSpeed = opt.infillSpeed || fillSpeed || printSpeed, moveSpeed = process.outputSeekrate, minSpeed = process.outputMinSpeed, origin = startPoint.add(offset), zhop = process.zHopDistance || 0, zmax = opt.zmax, antiBacklash = process.antiBacklash, + maxFlowRate = process.outputMaxFlowrate, wipeDist = process.outputRetractWipe || 0, isBelt = device.bedBelt, bedOffset = originCenter ? { @@ -851,11 +863,6 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { shellOrder = -shellOrder; } - // update fill speed when solids detected - if (slice.finishSolids) { - fillSpeed = process.sliceSolidRate || finishSpeed; - } - // on flats layers also slow down shell print speed if (slice.isFlatsLayer) { printSpeed = finishSpeed || printSpeed; @@ -867,13 +874,6 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { printSpeed /= 2; } - // apply first layer extrusion multipliers - if (firstLayer) { - fillMult *= firstPrintMult; - shellMult *= firstPrintMult; - sparseMult *= firstPrintMult; - } - function retract() { let array = preout.length ? preout : output; if (array.length) { @@ -1595,16 +1595,34 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { if (!p.emit) continue; let mms = p.speed ?? 1; let dst = lp?.distTo2D(p.point) ?? 0; + lp = p.point; if (dst) { tt += (dst/mms); td += dst; + p.dst = dst; } - lp = p.point; } if (minLayerTime && tt < minLayerTime) { let fact = tt / minLayerTime; for (let p of preout) { - if (p.emit) p.speed = Math.max(p.speed * fact, minSpeed); + if (p.emit) { + p.speed = Math.max(p.speed * fact, minSpeed); + } + } + } + + // apply speed damping when output exceeds volumetric flowrate cap + if (maxFlowRate) { + for (let p of preout) { + if (p.emit && p.dst) { + let vfr = nozzles[p.tool] * slice.height * p.speed; + if (vfr > maxFlowRate) { + let old = p.speed; + let damp = maxFlowRate / vfr; + p.speed = p.speed * damp; + // console.log({ vfr, damp, old, new: p.speed }); + } + } } } @@ -1617,7 +1635,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { print.addPrintPoints(preout, output); // bridge layer fan change - if (slice.isBridgeLayer) { + if (preout.length && slice.isBridgeLayer) { preout[0].fan = Math.round(fanSpeed / 3); preout.peek().fan = fanSpeed; } diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index 9bda3df8..5bc215da 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -498,7 +498,6 @@ export function fdm_slice(settings, widget, onupdate, ondone) { // just the top/bottom special solid layers or range defined solid layers forSlices(0.15, 0.2, slice => { let range = slice.params; - let spaceMult = slice.index === 0 ? process.firstLayerLineMult || 1 : 1; let isBottom = slice.index < bottomLayers; let isTop = topLayers && slice.index > slices.length - topLayers - 1; let isTopBase = isTop && slice.index === slices.length - topLayers; @@ -506,7 +505,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)) && !isSynth; let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; if (solidWidth) { - let fillSpace = fillSpacing * spaceMult * solidWidth; + let fillSpace = fillSpacing * solidWidth; doSolidLayerFill(slice, fillSpace, sliceFillAngle); } if (slice.index === slices.length - 1) { @@ -755,8 +754,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { let params = slice.params || process; let first = slice.index === 0; let solidWidth = params.sliceFillWidth || 1; - let spaceMult = first ? params.firstLayerLineMult || 1 : 1; - let fillSpace = fillSpacing * spaceMult * solidWidth; + let fillSpace = fillSpacing * solidWidth; let solidMinArea = params.sliceSolidMinArea; doSolidsFill(slice, fillSpace, sliceFillAngle, solidMinArea, promises); sliceFillAngle = (sliceFillAngle + 90.0) % 360; diff --git a/src/kiri/run/minion.js b/src/kiri/run/minion.js index ae1c2920..412da396 100644 --- a/src/kiri/run/minion.js +++ b/src/kiri/run/minion.js @@ -315,7 +315,6 @@ const funcs = self.minion = { const topo4 = Object.assign(new Topo4(), cache.lathe); const heights = topo4.lathePath(stmp, tool); - reply({ seq, heights }); } }; diff --git a/src/moto/license.js b/src/moto/license.js index f3975212..669e0eb7 100644 --- a/src/moto/license.js +++ b/src/moto/license.js @@ -3,7 +3,7 @@ const terms = { COPYRIGHT: "Copyright (C) Stewart Allen - All Rights Reserved", LICENSE: "See the license.md file included with the source distribution", - VERSION: "4.3.3" + VERSION: "4.3.4" }; export const beta = 0; diff --git a/src/moto/space.js b/src/moto/space.js index 8aa5047d..45101885 100644 --- a/src/moto/space.js +++ b/src/moto/space.js @@ -98,7 +98,6 @@ let WIN = self.window || {}, fontColor = '#333333', fontScale = 1.4, // computed relative to grid size rulerColor, - axisColor, axesOn = true, volumeOn = true, viewControl, @@ -388,7 +387,6 @@ function setPlatformColor(color) { function setFont(options) { if (options.color) fontColor = options.color; if (options.scale) fontScale = options.scale; - if (options.axisColor) axisColor = options.axisColor; if (options.rulerColor) rulerColor = options.rulerColor; updateRulers(); } diff --git a/web/boot/index.html b/web/boot/index.html new file mode 100644 index 00000000..8f7c9381 --- /dev/null +++ b/web/boot/index.html @@ -0,0 +1,67 @@ + + + + + + Kiri:Moto (loading) + + + + +Booting Grid.Space + + \ No newline at end of file diff --git a/web/boot/service.js b/web/boot/service.js new file mode 100644 index 00000000..8d0f405a --- /dev/null +++ b/web/boot/service.js @@ -0,0 +1,203 @@ +// --- configurable --- +const CACHE_VERSION = 'boot-001'; +const BUNDLE_URL = '/boot/bundle.bin'; +const VERSION_KEY = '/__version__'; + +let loaded = 0; +let mode = 'cached'; +let currentVersion = null; + +// --- logging --- +const log = (...a) => console.log('[SW]', ...a); +self.addEventListener('install', e => log('install')); +self.addEventListener('activate', e => log('activate')); +// self.addEventListener('fetch', e => log('fetch', e.request.url)); + +// --- lifecycle --- +self.skipWaiting(); +self.addEventListener('activate', e => e.waitUntil(clients.claim())); + +// --- mode control --- +self.addEventListener('message', e => { + const data = e.data || {}; + const { clear, disable, version } = data; + if (clear) { + clearCache(); + } + if (disable) { + mode = 'transparent'; + broadcast('transparent mode'); + unregister(); + } else if (version !== undefined) { + mode = 'cached'; + log('version', version); + ensureVersion(version); + } +}); + +// --- unregister current service worker --- +async function unregister() { + await self.registration.unregister(); +} + +// --- erase existing cache --- +async function clearCache() { + log('clearing cache'); + await caches.delete(CACHE_VERSION); +} + +// --- version check and preload --- +async function ensureVersion(incomingVersion) { + const cache = await caches.open(CACHE_VERSION); + let needPreload = false; + + const existing = await cache.match(VERSION_KEY); + if (!existing) { + log('no version stored → preload'); + needPreload = true; + } else { + const stored = await existing.text(); + if (stored !== incomingVersion) { + log(`mismatch ${stored} ≠ ${incomingVersion} → preload`); + needPreload = true; + } + } + + if (needPreload) { + await preloadBundle(); + await cache.put( + VERSION_KEY, + new Response(String(incomingVersion), { headers: { 'Content-Type': 'text/plain' } }) + ); + broadcast(`preload added ${loaded} files`); + } else { + broadcast('preload cached'); + } +} + +// --- fetch with redirect handler --- +async function fetch_safe(req) { + const res = await fetch(req); + if (res.redirected) { + log({ was_redirected_cloning: res.url }); + // clone into a fresh non-redirect response + const clone = res.clone(); + return new Response(await clone.blob(), { + headers: clone.headers, + status: 200, + statusText: 'OK' + }); + } + return res; +} + +// --- fetch handler --- +self.addEventListener('fetch', e => { + const { request } = e; + const url = new URL(request.url); + + if (request.method !== 'GET') return; + + if (mode == 'transparent') { + e.respondWith(fetch_safe(e.request)); + return; + } + + if (url.pathname.endsWith("/")) { + return e.respondWith(redirectOr404(appendURL(url, 'index.html').pathname)); + } else if (url.pathname.indexOf(".") < 0) { + return e.respondWith(redirectOr404(appendURL(url, '/index.html').pathname)); + } else { + e.respondWith(fromCacheOrNetwork(request)); + } +}); + +// --- helpers --- +function appendURL(url, append) { + return new URL(url.origin + url.pathname + append + url.search); +} + +async function redirectOr404(path) { + const cache = await caches.open(CACHE_VERSION); + const hit = await cache.match(path, { ignoreSearch: true }); + if (hit) { + return Response.redirect(path, 302); + } else { + return new Response('Not Found', { status: 404, statusText: 'Not Found' }); + } +} + +async function fromCacheOrNetwork(req) { + const cache = await caches.open(CACHE_VERSION); + const hit = await cache.match(req, { ignoreSearch: true }); + if (hit) { + return hit; + } + const net = await fetch_safe(req); + cache.put(req, net.clone()); + log(`put: ${net.url}`); + return net; +} + +async function broadcast(message) { + const clientsList = await clients.matchAll({ type: 'window' }); + for (const client of clientsList) { + client.postMessage(message); + } +} + +const sec_headers = { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp' +}; + +// --- preload & unpack bundle --- +async function preloadBundle() { + loaded = 0; + const cache = await caches.open(CACHE_VERSION); + const res = await fetch(BUNDLE_URL, { cache: 'no-store' }); + const buf = await res.arrayBuffer(); + const files = await unpackBundle(buf); + await Promise.all( + Object.entries(files).map(([path, blob]) => { + const ext = path.split('.').pop(); + const type = + ext === 'html' ? 'text/html' : + ext === 'js' ? 'application/javascript' : + ext === 'css' ? 'text/css' : + ext === 'json' ? 'application/json' : + ext === 'wasm' ? 'application/wasm' : + ext === 'svg' ? 'image/svg+xml' : + 'application/octet-stream'; + const headers = { 'Content-Type': type, ...sec_headers }; + const resp = new Response(blob, { headers }); + loaded++; + return cache.put('/' + path, resp); + }) + ); +} + +// --- simple bundle format --- +// [count:uint32][entries...][file data...] +// entry: [nlen:uint16][name][offset:uint32][length:uint32] +async function unpackBundle(buf) { + const view = new DataView(buf); + let pos = 0; + const count = view.getUint32(pos, true); pos += 4; + const decoder = new TextDecoder(); + const table = []; + + for (let i = 0; i < count; i++) { + const nlen = view.getUint16(pos, true); pos += 2; + const name = decoder.decode(new Uint8Array(buf, pos, nlen)); pos += nlen; + const offset = view.getUint32(pos, true); pos += 4; + const len = view.getUint32(pos, true); pos += 4; + table.push({ name, offset, len }); + } + + const files = {}; + for (const { name, offset, len } of table) + files[name] = buf.slice(offset, offset + len); + + return files; +} diff --git a/web/kiri/index.css b/web/kiri/index.css index ad3b7705..efc8c86d 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -1378,7 +1378,7 @@ details[open] summary::after { padding: 0 0 0 0; } #oplist { - max-height: 70vh; + max-height: calc(70vh - 200px); min-height: 0; overflow-y: auto; padding-top: 1px; diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index 39eb200d..ea00c48b 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -356,7 +356,7 @@ self.lang['en-us'] = { ad_thin_l: ["detect and fill gaps","between shell walls"], // FDM SHELLS - sl_line_s: "shell width", + sl_line_s: "line width", sl_line_l: ["extrusion width in millimeters","0 = nozzle diameter"], sl_ordr_s: "shell order", sl_ordr_l: ["output shell order","inside to outside","or outside to inside","affects surface quality"], @@ -369,8 +369,8 @@ self.lang['en-us'] = { // FDM SOLID fs_menu: "solid fill", - fi_over_s: "shell overlap", - fi_over_l: ["overlap with shell and other fill","for improved extrusion bonding","as fraction of nozzle diameter","0.0 - 2.0"], + fi_over_s: "fill overlap", + fi_over_l: ["overlap between shells and solid or sparse infill. can improve bonding to shells","as fraction of line width","0.0 - 1.0"], fi_angl_s: "start angle", fi_angl_l: ["starting angle in degrees","90 degrees added to ","each following layer","applies only to solid layers"], fi_grow_s: "area expand", @@ -654,6 +654,10 @@ self.lang['en-us'] = { ci_face_l: "select face to rotate facing up", ci_line_s: "linear", ci_line_l: "make linear passes along X then rotate Y", + ci_laso_s: "offset start", + ci_laso_l: ["x offet from left of part. negative values are outside of the part. in workspace units"], + ci_laeo_s: "offset end", + ci_laeo_l: ["x offet from right of part. negative values are outside of the part. in workspace units"], // CNC LASER On/Off Operations (Carvera) cl_powr_s: "power", @@ -675,10 +679,6 @@ self.lang['en-us'] = { cl_maxz_s: "max z height", cl_maxz_l: ["highest Z band position. when set to 0, uses the highest Z point of the workspace"], - - - - // CNC CUTOUT TABS ct_menu: "tabs", ct_angl_s: "angle", @@ -864,10 +864,12 @@ self.lang['en-us'] = { ad_rdwl_l: ["time between re-engaging","filament and movement","in milliseconds"], ad_scst_s: "shell coast", ad_scst_l: ["non-printing end","of perimeter shells","in millimeters"], - ad_msol_s: "min area", - ad_msol_l: ["minimum area (mm^2)","required to keep solid","must be > 0.1"], + ad_msol_s: "area filter", + ad_msol_l: ["minimum area (mm^2)","bridges and flats less than this are culled"], ad_mins_s: "min speed", ad_mins_l: ["minimum speed","for short segments"], + ad_maxf_s: "max flowrate", + ad_maxf_l: ["maximum volumetric flowrate supported by this print head and filament","in millimeters^3 / second"], ad_scar_s: "scarf length", ad_scar_l: ["ease up length for scarf joint. disables arc detection","0 = disabled"], ad_spol_s: "short path", diff --git a/web/kiri/manifest.json b/web/kiri/manifest.json index 0f24310a..5d913d11 100644 --- a/web/kiri/manifest.json +++ b/web/kiri/manifest.json @@ -1,5 +1,5 @@ { - "name": "Kiri:Moto 4.3.3", + "name": "Kiri:Moto 4.3.4", "short_name": "Kiri:Moto", "description": "Slicer for 3D printers, CNC mills, laser cutters and more", "start_url": "/kiri/",