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

@ -2,6 +2,8 @@ name: 'docs-publish'
on: on:
push: push:
branches: [ master ] branches: [ master ]
paths:
- 'docs/**'
workflow_dispatch: workflow_dispatch:

View file

@ -3,6 +3,8 @@ name: Test Electron App Production Build
on: on:
# Runs on pushes targeting the default branch # Runs on pushes targeting the default branch
push: push:
paths:
- '!docs/**'
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:

View file

@ -2,7 +2,7 @@ name: 'Docs Fomatting Check'
on: on:
push: push:
paths: paths:
- './docs/**' - 'docs/**'
workflow_dispatch: workflow_dispatch:
jobs: jobs:

View file

@ -3,6 +3,8 @@ name: Test NPM Production Build
on: on:
# Runs on pushes targeting the default branch # Runs on pushes targeting the default branch
push: push:
paths:
- '!docs/**'
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
@ -24,4 +26,4 @@ jobs:
run: npm run setup run: npm run setup
- name: dry run production build - name: dry run production build
run: npm run prod-dryrun run: npm run dryrun-prod

5
.gitignore vendored
View file

@ -1,6 +1,8 @@
.DS_Store .DS_Store
.bache
.docusaurus .docusaurus
.env .env
alt
app.json app.json
build build
data data
@ -9,7 +11,8 @@ jszip-esm.js
logs logs
mod mod
node_modules node_modules
pack
package-lock.json package-lock.json
three.js three.js
tmp tmp
pack web/boot/bundle*

View file

@ -9,20 +9,11 @@ const appDir = path.join(usrDir, 'gapp');
const cnfDir = path.join(appDir, 'conf'); const cnfDir = path.join(appDir, 'conf');
const logDir = path.join(appDir, 'logs'); const logDir = path.join(appDir, 'logs');
const datDir = path.join(appDir, 'data'); const datDir = path.join(appDir, 'data');
const debug = process.argv.slice(2).map(v => v.replaceAll('-', '')).contains('debugg'); const debug = process.argv.slice(2).map(v => v.replaceAll('-', '')).indexOf('debugg') >= 0;
const devel = process.argv.slice(2).map(v => v.replaceAll('-', '')).contains('devel'); const devel = process.argv.slice(2).map(v => v.replaceAll('-', '')).indexOf('devel') >= 0;
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true; process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true;
// console.log({ appDir, usrDir, logDir, datDir, basDir });
// console.log({ argv: process.argv, debug, devel });
// console.log({
// cwd: process.cwd(),
// dir: require('fs').readdirSync('.'),
// out: require('fs').readdirSync(appDir),
// ___: require('fs').readdirSync(basDir)
// });
server({ server({
port: 5309, port: 5309,
apps: basDir, apps: basDir,
@ -40,23 +31,31 @@ function createWindow() {
width: 1600, width: 1600,
height: 900, height: 900,
webPreferences: { webPreferences: {
// contextIsolation: true,
// nodeIntegration: true,
// sandbox: true,
// preload: undefined
// nodeIntegration: true,
// contextIsolation: false
// preload: path.join(__dirname, 'preload.js') // preload: path.join(__dirname, 'preload.js')
} }
}); });
const { webContents } = mainWindow;
mainWindow.loadURL('http://localhost:5309/kiri'); mainWindow.loadURL('http://localhost:5309/kiri');
// default normal url navigation or page opens to happen outside Electron // default normal url navigation or page opens to happen outside Electron
mainWindow.webContents.setWindowOpenHandler((details) => { webContents.setWindowOpenHandler((details) => {
// console.log('EXTERNAL', details); // console.log('EXTERNAL', details);
shell.openExternal(details.url); shell.openExternal(details.url);
return { action: 'deny' } return { action: 'deny' }
}); });
// prevent "other" urls from opening inside Electron (alerts are problematic) // prevent "other" urls from opening inside Electron (alerts are problematic)
mainWindow.webContents.on('will-navigate', (event, url) => { webContents.on('will-navigate', (event, url) => {
// console.log('DIVERT', url); // console.log('DIVERT', url);
if (url.endsWith('/kiri') || url.endsWith('/kiri/')) { if (url.endsWith('/kiri') || url.endsWith('/kiri/')) {
return; return;
} }
if (url.endsWith('/mesh') || url.endsWith('/mesh/')) { if (url.endsWith('/mesh') || url.endsWith('/mesh/')) {
@ -66,9 +65,13 @@ function createWindow() {
shell.openExternal(url); shell.openExternal(url);
}); });
webContents.on('did-finish-load', () => {
mainWindow.webContents.executeJavaScript(`{ let x = document.getElementById('app-quit'); if (x) { x.onclick = () => window.close() } }; null;`);
});
if (devel) { if (devel) {
console.log("opening developer tools"); console.log("opening developer tools");
mainWindow.webContents.openDevTools(); webContents.openDevTools();
} }
} }
@ -77,9 +80,7 @@ app.on('ready', () => {
}); });
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
if (process.platform !== 'darwin') { app.quit();
app.quit();
}
}); });
app.on('activate', () => { app.on('activate', () => {

131
app.js
View file

@ -15,6 +15,10 @@ const license = require_fresh('./src/moto/license.js');
const version = license.version || "rogue"; const version = license.version || "rogue";
const netdb = require('@gridspace/net-level-client'); const netdb = require('@gridspace/net-level-client');
const PATH = require('path'); const PATH = require('path');
const isElectron = !!process.versions.electron ||
process.env.ELECTRON_RUN_AS_NODE === '1' ||
process.env.ELECTRON_NO_ATTACH === '1' ||
process.env.ELECTRON_BUILD === '1';
const append = { mesh:'', kiri:'' }; const append = { mesh:'', kiri:'' };
const code = {}; const code = {};
@ -31,6 +35,8 @@ let oversion;
let dversion; let dversion;
let lastmod; let lastmod;
let logger; let logger;
let single;
let dryrun;
let debug; let debug;
let http; let http;
let util; let util;
@ -60,12 +66,15 @@ netdb.create = async function(map = {}) {
function init(mod) { function init(mod) {
const ENV = mod.env; const ENV = mod.env;
const inElectron = isElectron || ENV.electron;
startTime = time(); startTime = time();
lastmod = mod.util.lastmod; lastmod = mod.util.lastmod;
logger = mod.log; logger = mod.log;
pre = ENV.pre || mod.meta.pre; pre = ENV.pre || mod.meta.pre;
debug = ENV.debug || mod.meta.debug; debug = ENV.debug || mod.meta.debug;
dryrun = ENV.dryrun;
single = ENV.single;
oversion = ENV.over || mod.meta.over; oversion = ENV.over || mod.meta.over;
crossOrigin = ENV.xorigin || mod.meta.xorigin || false; crossOrigin = ENV.xorigin || mod.meta.xorigin || false;
serviceWorker = (ENV.service || mod.meta.service) !== false; serviceWorker = (ENV.service || mod.meta.service) !== false;
@ -74,14 +83,10 @@ function init(mod) {
dir = mod.dir; dir = mod.dir;
log = mod.log; log = mod.log;
if (ENV.single) console.log({ cwd: process.cwd(), env: ENV }); if (single) console.log({ cwd: process.cwd(), env: ENV });
dversion = debug ? `_${version}` : version; dversion = debug ? `_${version}` : version;
forceUseCache = ENV.cache ? true : false; forceUseCache = ENV.cache ? true : false;
if (!ENV.electron) {
generateDevices();
}
if (pre) { if (pre) {
for (let [k,v] of Object.entries(productionMap)) { for (let [k,v] of Object.entries(productionMap)) {
productionMap[`${pre}${k}`] = `${pre}${v}`; productionMap[`${pre}${k}`] = `${pre}${v}`;
@ -93,6 +98,7 @@ function init(mod) {
req.url = req.url.substring(pre.length); req.url = req.url.substring(pre.length);
req.app.path = req.app.path.substring(pre.length); req.app.path = req.app.path.substring(pre.length);
req.app.ispre = true; req.app.ispre = true;
// console.log('MATCH1', dir, cookie, req.url);
return true; return true;
} }
let cookie = cookieValue(req.headers.cookie, "version") || ''; let cookie = cookieValue(req.headers.cookie, "version") || '';
@ -101,9 +107,12 @@ function init(mod) {
vmatch = [ vmatch ]; vmatch = [ vmatch ];
} }
if (vmatch.indexOf("*") >= 0) { if (vmatch.indexOf("*") >= 0) {
// console.log('MATCH2', dir, cookie, req.url);
return true; return true;
} }
return vmatch.indexOf(cookie) >= 0; let match = vmatch.indexOf(cookie) >= 0;
// console.log('MATCH3', dir, cookie, req.url);
return match;
}); });
mod.on.testv((version) => { mod.on.testv((version) => {
@ -122,6 +131,7 @@ function init(mod) {
mod.add(serveWasm); mod.add(serveWasm);
mod.add(serveCode); mod.add(serveCode);
mod.add(fullpath({ mod.add(fullpath({
"/boot" : redir((pre??"") + "/boot/", 301),
"/kiri" : redir((pre??"") + "/kiri/", 301), "/kiri" : redir((pre??"") + "/kiri/", 301),
"/mesh" : redir((pre??"") + "/mesh/", 301), "/mesh" : redir((pre??"") + "/mesh/", 301),
"/meta" : redir((pre??"") + "/meta/", 301), "/meta" : redir((pre??"") + "/meta/", 301),
@ -139,7 +149,8 @@ function init(mod) {
return "reload"; return "reload";
}); });
} }
mod.add(rewriteHtmlVersion); mod.add(addAppHeaders);
mod.static("/lib/", "alt");
mod.static("/lib/", "src"); mod.static("/lib/", "src");
mod.static("/obj/", "web/obj"); mod.static("/obj/", "web/obj");
mod.static("/font/", "web/font"); mod.static("/font/", "web/font");
@ -147,19 +158,26 @@ function init(mod) {
mod.static("/mesh/", "web/mesh"); mod.static("/mesh/", "web/mesh");
mod.static("/moto/", "web/moto"); mod.static("/moto/", "web/moto");
mod.static("/kiri/", "web/kiri"); mod.static("/kiri/", "web/kiri");
mod.static("/boot/", "web/boot");
// module loader // module loader
function load_modules(root, force) { function load_modules(root, force) {
lastmod(`${dir}/${root}`) && fs.readdirSync(`${dir}/${root}`).forEach(mdir => { lastmod(`${dir}/${root}`) && fs.readdirSync(`${dir}/${root}`).forEach(mdir => {
const modpath = `${root}/${mdir}`; const modpath = `${root}/${mdir}`;
if (dir.charAt(0) === '.' && !ENV.single) return; if (dir.charAt(0) === '.' && !single) return;
const stats = fs.lstatSync(`${mod.dir}/${modpath}`); const stats = fs.lstatSync(`${mod.dir}/${modpath}`);
if (!(stats.isDirectory() || stats.isSymbolicLink())) return; if (!(stats.isDirectory() || stats.isSymbolicLink())) return;
if (util.isfile(PATH.join(mod.dir,modpath,".disable"))) return; if (util.isfile(PATH.join(mod.dir,modpath,".disable"))) return;
const isDebugMod = util.isfile(PATH.join(mod.dir,modpath,".debug")); const isDebugMod = util.isfile(PATH.join(mod.dir,modpath,".debug"));
const isElectronMod = util.isfile(PATH.join(mod.dir,modpath,".electron")); const isElectronMod = util.isfile(PATH.join(mod.dir,modpath,".electron"));
if (force || (ENV.electron && !isElectronMod)) return; if (inElectron && !isElectronMod) {
if (force || (!ENV.electron && isElectronMod && !isDebugMod)) return; logger.log({ skip_non_electron_mod: modpath });
return;
}
if (!inElectron && isElectronMod && !isDebugMod) {
logger.log({ skip_electron_mod: modpath });
return;
}
try { try {
loadModule(mod, modpath); loadModule(mod, modpath);
} catch (error) { } catch (error) {
@ -189,6 +207,33 @@ function init(mod) {
append[key] = minify(append[key]); append[key] = minify(append[key]);
} }
} }
// create alt artifacts with module extensions
if (dryrun || !isElectron) {
logger.log('creating artifacts', Object.keys(append));
for (let [ key, val ] of Object.entries(append)) {
// append mains
let src = `${dir}/src/main/${key}.js`;
if (!fs.existsSync(src)) {
logger.log('missing', src);
continue;
}
fs.mkdirSync(`${dir}/alt/main`, { recursive: true });
let body = fs.readFileSync(src);
fs.writeFileSync(`${dir}/alt/main/${key}.js`, body + val);
// append packed mains
src= `${dir}/src/pack/${key}-main.js`;
if (!fs.existsSync(src)) {
logger.log('missing', src);
continue;
}
fs.mkdirSync(`${dir}/alt/pack`, { recursive: true });
body = fs.readFileSync(src);
fs.writeFileSync(`${dir}/alt/pack/${key}-main.js`, body + val);
}
} else {
logger.log('skipping artifacts');
}
}; };
// either add module assets to path or require(init.js) // either add module assets to path or require(init.js)
@ -253,11 +298,9 @@ function initModule(mod, file, dir) {
const [ path, file ] = [ ...arguments ]; const [ path, file ] = [ ...arguments ];
if (lastmod(file)) { if (lastmod(file)) {
code[path] = fs.readFileSync(file); code[path] = fs.readFileSync(file);
// console.log({ CODE: path, file });
} else if (lastmod(mod.dir + '/' + file)) { } else if (lastmod(mod.dir + '/' + file)) {
const alt = mod.dir + '/' + file; const alt = mod.dir + '/' + file;
code[path] = fs.readFileSync(alt); code[path] = fs.readFileSync(alt);
// console.log({ CODE: path, alt });
} else { } else {
console.log({ MISSING_CODE: path, file }); console.log({ MISSING_CODE: path, file });
} }
@ -286,14 +329,14 @@ function initModule(mod, file, dir) {
const path = mod.dir + '/' + dir + '/' + file; const path = mod.dir + '/' + dir + '/' + file;
try { try {
const body = fs.readFileSync(path); const body = fs.readFileSync(path);
// console.log({ inject: code, file, opt }); if (debug && !single) logger.log({ inject: code, file, opt });
if (opt.first) { if (opt.first) {
append[code] = body.toString() + '\n' + append[code]; append[code] = body.toString() + '\n' + append[code];
} else { } else {
append[code] += body.toString() + '\n'; append[code] += body.toString() + '\n';
} }
} catch (e) { } catch (e) {
console.log({ missing_file: path, dir, mod }); logger.log({ missing_file: path, dir, mod });
} }
}, },
onload(fn) { onload(fn) {
@ -412,24 +455,6 @@ function serveWasm(req, res, next) {
} }
} }
// pack/concat device script strings to inject into /code/ scripts
function generateDevices() {
let root = PATH.join(dir,"src","kiri","dev");
let devs = {};
fs.readdirSync(root).forEach(type => {
let map = devs[type] = devs[type] || {};
fs.readdirSync(PATH.join(root,type)).forEach(device => {
let deviceName = device.endsWith('.json')
? device.substring(0,device.length-5)
: device;
map[deviceName] = JSON.parse(fs.readFileSync(PATH.join(root,type,device)));
});
});
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};`);
}
function minify(code) { function minify(code) {
let mini = uglify.minify(code.toString(), { let mini = uglify.minify(code.toString(), {
compress: { compress: {
@ -524,7 +549,13 @@ function fixedmap(prefix, map) {
// HTTP 307 redirect // HTTP 307 redirect
function redir(path, type) { function redir(path, type) {
return (req, res, next) => { 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);
} }
} }
@ -547,10 +578,12 @@ function cookieValue(cookie,key) {
} }
} }
function rewriteHtmlVersion(req, res, next) { function addAppHeaders(req, res, next) {
if ([ if ([
"/kiri/", "/kiri/",
"/mesh/", "/mesh/",
"/lib/gpu/raster.js",
"/lib/gpu/raster-worker.js",
"/lib/mesh/work.js", "/lib/mesh/work.js",
"/lib/kiri/run/worker.js", "/lib/kiri/run/worker.js",
"/lib/kiri/run/minion.js", "/lib/kiri/run/minion.js",
@ -559,36 +592,6 @@ function rewriteHtmlVersion(req, res, next) {
].indexOf(req.app.path) >= 0) { ].indexOf(req.app.path) >= 0) {
addCorsHeaders(req, res); addCorsHeaders(req, res);
} }
if ([
"/lib/main/kiri.js",
"/lib/main/mesh.js"
].indexOf(req.app.path) >= 0) {
addCorsHeaders(req, res);
// const data = append[req.app.path.split('/')[3].split('.')[0]];
const data = append[req.app.path.split('.')[0].split('/').pop()];
if (!data) return next();
if (debug) logger.log({ append: req.app.path, data: data.length });
const real_write = res.write;
const real_end = res.end;
let body = '';
res.write = function (chunk, encoding) {
body += chunk.toString(encoding);
};
res.end = function (chunk, encoding) {
if (chunk) {
body += chunk.toString(encoding);
}
body += data;
res.setHeader('Content-Length', Buffer.byteLength(body));
real_write.call(res, body);
real_end.call(res);
};
}
next(); next();
} }

View file

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
tag=${1:-latest} tag=${1:-latest}
npm run clear-cache npm run clean
./bin/build-upload win ${tag} && \ ./bin/build-upload win ${tag} && \
./bin/build-upload linux ${tag} && \ ./bin/build-upload linux ${tag} && \
./bin/build-upload mac ${tag} ./bin/build-upload mac ${tag}

View file

@ -0,0 +1,23 @@
{
"compress": true,
"outputs": {
"bundle": "./web/boot/bundle-{version}.bin"
},
"inputs": [
{ "root": "web/moto", "prefix": "moto" },
{ "root": "web/kiri", "prefix": "kiri" },
{ "root": "web/font", "prefix": "font" },
{ "root": "src/wasm", "prefix": "wasm" },
{ "root": "src/wasm", "prefix": "lib/kiri/wasm" },
{ "src": "alt/pack/kiri-main.js", "dst": "lib/main/kiri.js" },
{ "src": "src/pack/kiri-main.js", "dst": "lib/main/kiri.js" },
{ "src": "src/pack/kiri-work.js", "dst": "lib/kiri/run/worker.js" },
{ "src": "src/pack/kiri-pool.js", "dst": "lib/kiri/run/minion.js" },
{ "src": "src/gpu/raster-worker.js", "dst": "lib/gpu/raster-worker.js" },
{ "src": "src/ext/tween.js", "dst": "lib/ext/tween.js" }
],
"excludes": [
"src/pack",
"web/boot/bundle*"
]
}

17
bin/bundle.config.json Normal file
View file

@ -0,0 +1,17 @@
{
"compress": false,
"outputs": {
"bundle": "./web/boot/bundle-{version}.bin"
},
"inputs": [
{ "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*"
]
}

166
bin/bundler.mjs Normal file
View file

@ -0,0 +1,166 @@
import fs from 'fs';
import path from 'path';
import uglify from 'uglify-js';
import { Buffer } from 'buffer';
const version = JSON.parse(fs.readFileSync('package.json', 'utf8')).version;
const cfg = JSON.parse(fs.readFileSync(process.argv[2] || './bin/bundle.config.json', 'utf8'));
const { inputs, outputs, excludes, compress } = cfg;
const excludePre = (excludes ?? []).filter(s => s.indexOf('*') >= 0).map(s => s.replaceAll('*',''));
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;
}
seen.add(real);
for (const name of fs.readdirSync(dir)) {
const p = path.join(dir, name);
let st;
try { st = fs.statSync(p); } catch { continue; }
if (st.isDirectory()) yield* walk(p, seen);
else yield p;
}
}
// --- collect all files according to config ---
let entries = [];
let virtMap = new Set();
for (const input of inputs) {
const { root, prefix, src, dst } = input;
if (src && dst) {
if (!fs.existsSync(src)) {
console.log({ src_missing: src });
} else if (virtMap.has(dst)) {
console.log('pre-existing', dst);
} else {
entries.push({ file: src, virt: dst });
virtMap.add(dst);
}
}
if (root)
for (const file of walk(root)) {
const rel = path.relative(root, file).replace(/\\/g, '/');
const virt = prefix ? `${prefix}/${rel}` : rel;
if (virtMap.has(virt)) {
console.log('pre-existing', virt, rel, root);
} else {
entries.push({ file, virt });
virtMap.add(virt);
}
}
}
// --- build header and data ---
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;
}
if (excludePre.filter(ex => file.startsWith(ex)).length) {
continue;
}
let record = cache.get(file);
if (record) {
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 (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;
}
data = Buffer.from(data.code);
fs.mkdirSync(path.dirname(cpath), { recursive: true });
fs.writeFileSync(cpath, data);
}
}
record = { virt, offset, length: data.length };
cache.set(file, record);
entryMeta.push(record);
dataParts.push(data);
offset += data.length;
if (debug) console.log('write', virt, data.length);
}
}
// temporary header without offsets to measure total size
let tempHeaderParts = [];
for (const { virt, offset, length } of entryMeta) {
const name = Buffer.from(virt, 'utf8');
const nlen = Buffer.alloc(2); nlen.writeUInt16LE(name.length);
const offb = Buffer.alloc(4); offb.writeUInt32LE(0); // placeholder
const lenb = Buffer.alloc(4); lenb.writeUInt32LE(length);
tempHeaderParts.push(nlen, name, offb, lenb);
}
const count = Buffer.alloc(4); count.writeUInt32LE(entries.length);
const tempHeader = Buffer.concat([count, ...tempHeaderParts]);
const headerSize = tempHeader.length; // true data offset base
// now rebuild header with correct absolute offsets
let headerParts = [];
for (const { virt, offset, length } of entryMeta) {
const name = Buffer.from(virt, 'utf8');
const nlen = Buffer.alloc(2); nlen.writeUInt16LE(name.length);
const offb = Buffer.alloc(4); offb.writeUInt32LE(headerSize + offset);
const lenb = Buffer.alloc(4); lenb.writeUInt32LE(length);
headerParts.push(nlen, name, offb, lenb);
// console.log(virt);
}
const header = Buffer.concat([count, ...headerParts]);
const full = Buffer.concat([header, ...dataParts]);
// --- write final bundle ---
const outPath = (outputs.bundle || './bundle.bin').replace('{version}', version);
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, full); // ⚡ write raw binary
console.log(`✅ Bundled ${entries.length} files → ${outPath} (${full.length} bytes)`);

View file

@ -5,6 +5,7 @@ exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context; const { electronPlatformName, appOutDir } = context;
if (process.env.SKIPNOT || electronPlatformName !== 'darwin') { if (process.env.SKIPNOT || electronPlatformName !== 'darwin') {
console.log(' ** skip notarizing:', electronPlatformName);
return; return;
} }

View file

@ -2,30 +2,30 @@ const fs = require('fs-extra');
const path = require('path'); const path = require('path');
const server = require('@gridspace/app-server'); const server = require('@gridspace/app-server');
const altTmp = path.join('tmp','alt');
fs.copySync("alt", altTmp, { dereference: true });
// deref src, web, mod for windows // deref src, web, mod for windows
const srcTmp = path.join('tmp','src'); const srcTmp = path.join('tmp','src');
fs.copySync("src", srcTmp, { dereference: true }); fs.copySync("src", srcTmp, { dereference: true, filter:(src) => {
return true || src.indexOf('src/main') < 0;
} });
const webTmp = path.join('tmp','web'); const webTmp = path.join('tmp','web');
fs.copySync("web", webTmp, { dereference: true }); fs.copySync("web", webTmp, { dereference: true, filter:(src) => {
return src.indexOf('web/boot') < 0;
} });
const modTmp = path.join('tmp','mod'); const modTmp = path.join('tmp','mod');
if (fs.existsSync("mod")) if (fs.existsSync("mod"))
fs.copySync("mod", modTmp, { dereference: true, filter:(src,dst) => { fs.copySync("mod", modTmp, { dereference: true, filter:(src,dst) => {
const ok = const ok = src === 'mod' || src.indexOf('mod/standalone') === 0;
src === 'mod' ||
src.indexOf('mod/standalone') === 0 ||
src.indexOf('mod/node_modules') === 0;
return ok; return ok;
} }); } });
const modsTmp = path.join('tmp','mods'); const modsTmp = path.join('tmp','mods');
fs.copySync("mods", modsTmp, { dereference: true, filter:(src,dst) => { fs.copySync("mods", modsTmp, { dereference: true, filter:(src,dst) => {
const ok = const ok = src === 'mods' || src.indexOf('mods/bambu') === 0;
src === 'mods' ||
src.indexOf('mods/bambu') === 0 ||
src.indexOf('mods/electron') === 0 ||
src.indexOf('mods/node_modules') === 0;
return ok; return ok;
} }); } });

View file

@ -1,5 +1,6 @@
import { build, transform } from 'esbuild'; import { build, transform } from 'esbuild';
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path';
import glob from 'fast-glob'; import glob from 'fast-glob';
// Get build mode from command line argument // Get build mode from command line argument
@ -38,6 +39,30 @@ async function appendExtraModules(extras, outfile, minify = false) {
console.log(`Appended ${files.length} module(s) to ${outfile}`); console.log(`Appended ${files.length} module(s) to ${outfile}`);
} }
async function generateDevices(dir = '') {
let root = path.join(dir, "src", "kiri", "dev");
let devs = {};
let files = await fs.readdir(root);
for (let type of files) {
let map = devs[type] = devs[type] || {};
let devices = await fs.readdir(path.join(root, type));
for (let device of devices) {
let deviceName = device.endsWith('.json')
? device.substring(0, device.length - 5)
: device;
map[deviceName] = JSON.parse(await fs.readFile(path.join(root, type, device)));
}
}
let dstr = JSON.stringify(devs);
fs.mkdir(path.join(dir, "src", "pack"), { recursive: true });
await fs.writeFile(path.join(dir, "src", "pack", "kiri-devs.js"), `export const devices = ${dstr};`);
if (true) {
// create alt artifacts
await fs.mkdir('alt/pack', { recursive: true });
await fs.writeFile(path.join(dir, "alt", "pack", "kiri-devs.js"), `export const devices = ${dstr};`);
}
}
const logOverride = { const logOverride = {
'duplicate-class-member': 'silent', 'duplicate-class-member': 'silent',
'duplicate-object-key': 'silent', 'duplicate-object-key': 'silent',
@ -66,12 +91,15 @@ const rec = {
logOverride, logOverride,
minify: isProd, // false for dev, true for prod minify: isProd, // false for dev, true for prod
platform: 'browser', platform: 'browser',
sourcemap: !isProd, // true for dev, false for prod sourcemap: false,
target: 'es2020', target: 'es2020',
}; };
async function buildApp() { async function buildApp() {
try { try {
// Concatenate kiri devices
generateDevices();
// Bundle mesh main app // Bundle mesh main app
await build(Object.assign({}, rec, { await build(Object.assign({}, rec, {
entryPoints: [ 'src/main/mesh.js' ], entryPoints: [ 'src/main/mesh.js' ],

23
bin/update-version.sh Executable file
View file

@ -0,0 +1,23 @@
#!/bin/bash
if [ -z "$1" ]; then
echo "Usage: $0 <version>"
echo "Example: $0 4.5.0"
exit 1
fi
VERSION=$1
# Update package.json (line 3, no indentation)
awk -v ver="$VERSION" '/"version":/ {print " \"version\": \"" ver "\","; next} 1' package.json > package.json.tmp && mv package.json.tmp package.json
# Update src/moto/license.js (line 6, VERSION field)
awk -v ver="$VERSION" '/VERSION:/ {print " VERSION: \"" ver "\""; next} 1' src/moto/license.js > src/moto/license.js.tmp && mv src/moto/license.js.tmp src/moto/license.js
# Update web/kiri/manifest.json (line 2, preserves "Kiri:Moto" prefix)
awk -v ver="$VERSION" '/"name":/ {print " \"name\": \"Kiri:Moto " ver "\","; next} 1' web/kiri/manifest.json > web/kiri/manifest.json.tmp && mv web/kiri/manifest.json.tmp web/kiri/manifest.json
echo "Updated version to $VERSION in:"
echo " - package.json"
echo " - src/moto/license.js"
echo " - web/kiri/manifest.json"

View file

@ -7,7 +7,28 @@ module.exports = {
favicon: 'img/gs-logo.png', favicon: 'img/gs-logo.png',
staticDirectories: ['./docs/static/'], staticDirectories: ['./docs/static/'],
// ... // ...
plugins: [require.resolve('docusaurus-lunr-search')], plugins: [
require.resolve('docusaurus-lunr-search'),
['@docusaurus/plugin-client-redirects',
{
redirects: [
// /docs/oldDoc -> /docs/newDoc
{
to: '/kiri-moto',
from: '/projects/kiri-moto',
},
// Redirect from multiple old paths to the new path
{
to: '/gridbot',
from: '/projects/gridbot',
},
{
to: '/mesh-tool',
from: '/projects/mesh-tool',
},
],
}],
],
presets: [ presets: [
[ [
'@docusaurus/preset-classic', '@docusaurus/preset-classic',

View file

@ -8,12 +8,12 @@ const config = {
useTabs: false, useTabs: false,
bracketSpacing: true, bracketSpacing: true,
objectWrap: "preserve", objectWrap: "preserve",
tabWidth: 4, tabWidth: 2,
semi: false, semi: true,
singleQuote: true, singleQuote: false,
bracketSameLine: true, bracketSameLine: true,
arrowParens: "always", arrowParens: "always",
endOfLine: "crlf" endOfLine: "lf"
} }
module.exports = config module.exports = config

View file

@ -7,16 +7,10 @@ This is a community driven project, and we welcome any contributions you'd like
## Running Locally ## Running Locally
- Ensure you have [node](https://nodejs.org/en/download/), or an equivalent installed - Ensure you have [node](https://nodejs.org/en/download/), or an equivalent installed
- Clone the [repository](https://github.com/GridSpace/grid-apps) - Clone the [repository](https://github.com/GridSpace/grid-apps) from https://github.com/GridSpace/grid-apps
- Run `npm run setup` - `npm run setup`
- Run `npm run dev` - `npm run dev`
- Open browser [http://localhost:8080/kiri](http://localhost:8080/kiri)
## Running Docs Locally
- Ensure you have [node](https://nodejs.org/en/download/), or an equivalent installed
- Clone the [repository](https://github.com/GridSpace/grid-apps)
- Run `npm run setup`
- Run `npm run docs-dev`
## How to add a new machine ## How to add a new machine

View file

@ -0,0 +1,29 @@
---
description: Overview of CAM mode
---
import { ImageCarousel } from "@site/docs/src/components/carousel.js";
# Interface
The CAM interface is, in most respects, the same as the [general interface](/kiri-moto/interface). It is highly recommended to familiarize yourself with the general interface before using the CAM interface.
With that in mind, here are the key differences:
## Operation List
The Operation List defines the order in which operations are
performed on the model. Selecting the **plus** button will open a
popup menu for choosing an operation to add. The operations are
draggable, enabling easy re-ordering.
- An operation can be disabled by **Ctrl+clicking** it. Disabled operations will be skipped in the slice/preview/export process.
- The thin blue bar in the Operation List is also draggable. Moving the bar automatically disables all operations below it. This allows you to slice, preview, and export up to a specific point in the timeline.
<ImageCarousel base="/img/CAM/interface/" images="camInterface" sml />
## Origin Selection
Origin Selection (introduced in v4.3) enables automatically setting an origin to points of interest on the model, including corners and arc centers.
![](/img/CAM/interface/originSelect.png)

View file

@ -39,7 +39,6 @@ Customize how your G-code is formatted for your controller or post-processor:
- **Enable Laser** — Enables [Laser](./ops#laser-operations) operations (typically uses `M3/M5`; adjusts movement strategy) - **Enable Laser** — Enables [Laser](./ops#laser-operations) operations (typically uses `M3/M5`; adjusts movement strategy)
- **File Extension** — Optional override for exported file type (e.g., `.nc`, `.ngc`, `.cnc`, `.cam`, `.gcode`, `.txt`) - **File Extension** — Optional override for exported file type (e.g., `.nc`, `.ngc`, `.cnc`, `.cam`, `.gcode`, `.txt`)
--- ---
## G-code Macros ## G-code Macros

View file

@ -35,6 +35,7 @@ description: Variable Substitutions and Expressions in GCode Macros
- \{pos_y\} = last output Y position - \{pos_y\} = last output Y position
- \{pos_z\} = last output Z position - \{pos_z\} = last output Z position
- \{progress\} = 0-100% state of print output - \{progress\} = 0-100% state of print output
- \{remain_time\} = print time remaining in minutes
- \{tool\} | \{nozzle\} = current tool in use - \{tool\} | \{nozzle\} = current tool in use
- \{tool_count\} = number of tools used in print - \{tool_count\} = number of tools used in print
- \{tool_used\__n_\} = whether tool # _n_ is used in print - \{tool_used\__n_\} = whether tool # _n_ is used in print

View file

@ -1,29 +0,0 @@
---
description: A tour of Kiri:Moto's web-based interface
---
# Interface
Kiri:Moto's interface is designed for maximum access to the workspace and to present only the options that are necessary and relevant to the current action.
The left menus control workspace, work mode, and objects while.
The right menus control the behavior and settings for slicing, path routing, and path to code exports.
[OBJ](https://www.cs.cmu.edu/~mbz/personal/graphics/obj.html), [STL](<https://en.wikipedia.org/wiki/STL_(file_format)>), and [3MF](https://docs.fileformat.com/3d/3mf/) files import natively. [SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics), [JPG](https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format), and [PNG](https://en.wikipedia.org/wiki/Portable_Network_Graphics) are auto-extruded into a 3D parts.
![A minimalist approach](/img/kiri-moto.png)
In CNC mode, there is an additional storyboard menu at the bottom where you can plan and re-order milling operations.
![](/img/kiri-moto-cnc.png)
The Setup menu offers access to Device selection and customization.&#x20;
![Device Dialog](/img/device-dialog.png)
Application preferences are mode-dependent. For example, CNC preferences show animation controls.
![CNC Mode Preferences](/img/preferences-cam.png)
![FDM Mode Preferences](/img/preferences-fdm.png)

View file

@ -0,0 +1,54 @@
---
description: A tour of Kiri:Moto's web-based interface
---
import { ImageCarousel } from "@site/docs/src/components/carousel.js";
# Interface
Kiri:Moto's interface is designed for maximum access to the workspace, showing only the options necessary and relevant to the current action.
[OBJ](https://www.cs.cmu.edu/~mbz/personal/graphics/obj.html), [STL](<https://en.wikipedia.org/wiki/STL_(file_format)>), and [3MF](https://docs.fileformat.com/3d/3mf/) files import natively. [SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics), [JPG](https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format), and [PNG](https://en.wikipedia.org/wiki/Portable_Network_Graphics) files are auto-extruded into 3D parts.
For the CAM interface, click [here](/kiri-moto/CAM).
![A minimalist approach](/img/interface/main.png)
## Top Menu
- **Left:** Buttons to move through the `Arrange → Slice → Preview → Export` workflow. These steps are also available as [keyboard shortcuts](/kiri-moto/controls). Selecting an option here will automatically run prior steps as needed.
- **Center:** Clicking the [logo](/kiri-moto) opens a dialog with version selection, helpful links, and your current Kiri:Moto version. If you are unsure which version to use, check the [release notes](https://github.com/GridSpace/grid-apps/releases/) on GitHub.
- **Right:** Options for importing/exporting the workspace, manipulating the view, changing modes, and setting preferences. Kiri:Moto currently supports CAM, FDM, SLA, and LASER modes. Some other modes are extensions of the LASER workflow, but do not have first-class support. The [preferences menu](#preferences) will be covered later in this section.
- Changing the mode updates the left menu and enables/disables mode-specific options such as the `CAM Operation` menu and the `CAM Animate` step.
- Learn about K:M file types [here](/kiri-moto/faq#what-are-the-kmz-and-km-file-formats-short-for).
## Left Menu
The left menu contains options for the current mode. These vary widely:
- CAM has ample documentation on [Process Options](/kiri-moto/CAM/processOpts).
- Other modes are less documented but many options are self-explanatory if you're familiar with the manufacturing process.
- Hovering over an option provides tooltips. When in doubt, a quick search usually explains the option in detail.
## Right Menu
The right menu contains controls for objects in the workspace.
- Hovering over the arrow next to an object reveals options for deleting, renaming, disabling, or replacing it.
- Disabled objects remain visible but are ignored during slicing and exporting.
## Machines Dialog
![Device Dialog](/img/interface/CAMDevice.png)
The device dialog is accessed through the settings dropdown. It allows you to configure a device or select a predefined one.
- The dropdown only shows devices available for the current mode.
- Stock device settings are greyed out. To modify them, click `Customize`.
- Learn more about how K:M handles CAM devices [here](/kiri-moto/CAM/machines).
## Preferences
Application preferences are also accessed through the settings dropdown. These are mode-dependent but share many common options. For example, CNC preferences include animation controls.
<ImageCarousel base="/img/interface/" images="prefs" />

View file

@ -35,6 +35,7 @@ const sidebars = {
id: "kiri-moto/CAM/index", id: "kiri-moto/CAM/index",
}, },
items: [ items: [
"kiri-moto/CAM/interface",
"kiri-moto/CAM/processOpts", "kiri-moto/CAM/processOpts",
"kiri-moto/CAM/machines", "kiri-moto/CAM/machines",
"kiri-moto/CAM/tools", "kiri-moto/CAM/tools",

View file

@ -4,59 +4,69 @@ import "react-responsive-carousel/lib/styles/carousel.min.css";
const imageDescripts = { const imageDescripts = {
outline: [ outline: [
["/1.png", "outline of a complex part"], ["/1.png", "Outline of a complex part"],
["/2.png", "outline of a non-flat part with inside holes"], ["/2.png", "Outline of a non-flat part with inside holes"],
["/3.png", "outside-only outline"], ["/3.png", "Outside-only outline"],
["/4.png", "inside-only outline"], ["/4.png", "Inside-only outline"],
], ],
level: [ level: [
["/1.png", "level of a large part"], ["/1.png", "Level of a large part"],
["/2.png", `level of a square part's stock`], ["/2.png", `Level of a square part's stock`],
["/3.png", `level of a part with a non-uniform top surface`], ["/3.png", `Level of a part with a non-uniform top surface`],
], ],
rough: [ rough: [
["/1.png", "rough cut of two parts"], ["/1.png", "Rough cut of two parts"],
["/2.png", "side-view of a rough cut"], ["/2.png", "Side-view of a rough cut"],
["/3.png", "rough cut of a part with a complex top surface"], ["/3.png", "Rough cut of a part with a complex top surface"],
["/4.gif", "animate of complex rough cut"], ["/4.gif", "Animate of complex rough cut"],
], ],
contour: [ contour: [
["/1.png", "close-up of a precise contour"], ["/1.png", "Close-up of a precise contour"],
["/2.png", "contour of multiple parts at once"], ["/2.png", "Contour of multiple parts at once"],
["/3.png", "low precision contour"], ["/3.png", "Low precision contour"],
], ],
register: [ register: [
["/1.png", "2-drill register"], ["/1.png", "2-drill register"],
["/2.png", "3-drill register"], ["/2.png", "3-drill register"],
["/3.png", "jigsaw register"], ["/3.png", "Jigsaw register"],
], ],
drill: [ drill: [
["/1.png", "drills for a robot baseplate"], ["/1.png", "Drills for a robot baseplate"],
["/2.png", "drills for a pegboard"], ["/2.png", "Drills for a pegboard"],
["/3.png", "selecting only matching-size holes"], ["/3.png", "Selecting only matching-size holes"],
], ],
trace: [ trace: [
["/1.png", "toolpath tracing a design into a part"], ["/1.png", "Toolpath tracing a design into a part"],
["/2.png", "selecting loops to trace"], ["/2.png", "Selecting loops to trace"],
["/3.png", "tracing edges for deburring"], ["/3.png", "Tracing edges for deburring"],
["/4.png", "trace to clear a pocket"], ["/4.png", "Trace to clear a pocket"],
["/5.png", "arbitrary offset outline"], ["/5.png", "Arbitrary offset outline"],
], ],
pocket: [ pocket: [
["/1.png", "selecting areas to pocket"], ["/1.png", "Selecting areas to pocket"],
["/2.png", "toolpath for pocket"], ["/2.png", "Toolpath for pocket"],
["/3.gif", "pocket animation"], ["/3.gif", "Pocket animation"],
["/4.png", "toolpath for v-bit carve"], ["/4.png", "Toolpath for v-bit carve"],
["/5.png", "many-pocketed part"], ["/5.png", "Many-pocketed part"],
], ],
gcode: [["/1.png", "the gcode editor popup"]], gcode: [["/1.png", "The gcode editor popup"]],
laserOn: [], laserOn: [],
laserOff: [], laserOff: [],
index: [], index: [],
lathe: [], lathe: [],
tabs: [ tabs: [
["/1.png", "tabs added to part"], ["/1.png", "Tabs added to part"],
["/2.png", "sliced part with tabs"], ["/2.png", "Sliced part with tabs"],
],
camInterface: [
["/1.png", "Empty Ops list"],
["/2.png", "Sample Ops list"],
["/3.png", "Ops list with history bar"],
],
prefs: [
["/prefs-CAM.png", "Preferences in CAM mode"],
["/prefs-FDM.png", "Preferences in FDM mode"],
["/prefs-LASER.png", "Preferences in laser mode"],
], ],
}; };
@ -66,15 +76,17 @@ const imageDescripts = {
* @param {keyof typeof imageDescripts} images - The key to an array of images and descriptions. * @param {keyof typeof imageDescripts} images - The key to an array of images and descriptions.
* @returns {ReactElement} - A carousel of the given images. * @returns {ReactElement} - A carousel of the given images.
*/ */
export function ImageCarousel({ base, images }) { export function ImageCarousel({ base, images, sml }) {
return ( return (
<Carousel showThumbs={false}> <div className={sml ? "carouselWrapper" : ""}>
{imageDescripts[images].map(([image, caption], index) => ( <Carousel showThumbs={false}>
<div> {imageDescripts[images].map(([image, caption], index) => (
<img src={base + image} alt="" /> <div>
<p className="caption">{caption}</p> <img src={base + image} alt="" />
</div> <p className="caption">{caption}</p>
))} </div>
</Carousel> ))}
</Carousel>
</div>
); );
} }

View file

@ -5,3 +5,14 @@
.navbar .navbar__items { .navbar .navbar__items {
height: 4.5rem; height: 4.5rem;
} }
.carouselWrapper {
width: 100%;
display: flex;
justify-content: center;
}
.carouselWrapper .carousel-root {
width: fit-content;
max-width: 20rem;
}

BIN
docs/static/img/CAM/interface/1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
docs/static/img/CAM/interface/2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
docs/static/img/CAM/interface/3.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

BIN
docs/static/img/interface/CAMDevice.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
docs/static/img/interface/main.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

BIN
docs/static/img/interface/prefs-CAM.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
docs/static/img/interface/prefs-FDM.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

View file

@ -34,7 +34,7 @@ self.kiri.load(api => {
}).map(v => `${v.substring(1)}FF`); }).map(v => `${v.substring(1)}FF`);
let local_port = 5309; let local_port = 5309;
let local_url = `http://localhost:${local_port}`; let local_host = `localhost:${local_port}`;
let sequence_id = (Math.random() * 0xfff) | 0; let sequence_id = (Math.random() * 0xfff) | 0;
let user_id = ((Math.random() * 0xfffffff) | 0).toString(); let user_id = ((Math.random() * 0xfffffff) | 0).toString();
let init = false; let init = false;
@ -53,7 +53,8 @@ self.kiri.load(api => {
if (socket.ws) { if (socket.ws) {
return; return;
} }
let ws_url = api.onshape ? `${local_url}/bambu` : "/bambu"; let ws_url = api.onshape ? `ws://${local_host}/bambu` : "/bambu";
console.log({ bambu_ws_url: ws_url });
let ws = socket.ws = new WebSocket(ws_url); let ws = socket.ws = new WebSocket(ws_url);
ws.onerror = (error) => { ws.onerror = (error) => {
socket.open = false; socket.open = false;
@ -1288,7 +1289,7 @@ self.kiri.load(api => {
function send(filename, gcode, start) { function send(filename, gcode, start) {
const spool = print_ams_select; const spool = print_ams_select;
const baseUrl = api.onshape ? `${local_url}/api/bambu_send` : "/api/bambu_send"; const baseUrl = api.onshape ? `http://${local_host}/api/bambu_send` : "/api/bambu_send";
const url = new URL(baseUrl, window.location.origin); const url = new URL(baseUrl, window.location.origin);
url.searchParams.append('host', host); url.searchParams.append('host', host);
url.searchParams.append('code', password); url.searchParams.append('code', password);

25
mods/devel/init.js Normal file
View file

@ -0,0 +1,25 @@
// back end for device export in dev mode
module.exports = function(server) {
server.inject("kiri", "kiri.js");
if (!server.env.debug) {
return;
}
const fs = require('fs');
server.api.postDevice = (req, res, next) => {
server.handler.decodePost(req, res, () => {
// console.log({decodePost: req.app.post});
const { device, process, profiles } = JSON.parse(req.app.post);
if (profiles) {
device.profiles = Object.entries(profiles).filter(v => v[0] !== 'default').map(v => v[1]);
} else {
device.profiles = [ process ];
}
const { mode } = device;
const { deviceName } = device;
const path = `${server.const.rootdir}/src/kiri-dev/${mode.toLowerCase()}/${deviceName.replace(/\ /g,'.')}`;
// console.log({ mode, device, process, path });
fs.writeFileSync(path, JSON.stringify(device, undefined, 4));
res.end('profile saved');
});
};
};

23
mods/devel/kiri.js Normal file
View file

@ -0,0 +1,23 @@
// provides a device export to local disk option
self.kiri.load(api => {
api.event.on('load-done', () => {
let deviceExport = api.device.export;
api.device.export = (exp, name, opt = {}) => {
const { event, record } = opt;
if (event && event.shiftKey & api.const.LOCAL) {
const { code, process, profiles } = record;
fetch("/api/postDevice", {
method: "POST",
body: JSON.stringify({ device: code, process, profiles })
})
.then(r => r.text())
.then(t => {
// console.log({server_said: t});
api.show.alert('profile saved to server');
});
} else {
deviceExport(exp, name);
}
};
});
});

View file

@ -1,15 +0,0 @@
if (self.kiri && !self.kiri.electron) {
self.kiri.electron = {};
self.kiri.load(api => {
console.log('ELECTRON MODULE RUNNING');
api.event.on('load-done', () => {
$('app-name-text').innerText = "More Info";
$('top-sep').style.display = 'flex';
});
api.stats.set('kiri', api.version + 'e');
});
}
if (self.mesh && self.mesh) {
self.mesh.electron = {};
}

View file

@ -1,9 +0,0 @@
/**
* for electron standalone build support
*/
module.exports = function(server) {
// insert script before all others in kiri client
server.inject("kiri", "electron.js");
// insert scripts into mesh client
server.inject("mesh", "electron.js");
};

View file

@ -1,16 +0,0 @@
{
"name": "grid-apps-mods",
"version": "4.1.0",
"description": "grid.space app modules",
"author": "Stewart Allen <sa@grid.space>",
"repository": {
"type": "git",
"url": "https://github.com/gridspace/apps.git"
},
"dependencies": {
"@gridspace/basic-ftp": "^5.0.5",
"aedes": "^0.51.3",
"hexer": "^1.5.0",
"mqtt": "^5.10.3"
}
}

View file

@ -15,6 +15,7 @@
* `P` move all persisted / workspace settings/data to IndexedDB (LS limitations) * `P` move all persisted / workspace settings/data to IndexedDB (LS limitations)
* `P` faster ray intersect https://github.com/gkjohnson/three-mesh-bvh/ * `P` faster ray intersect https://github.com/gkjohnson/three-mesh-bvh/
* `P` try material clipping planes for slice range selection * `P` try material clipping planes for slice range selection
* `P` switch png lib to https://github.com/photopea/UPNG.js
* `F` edit in Mesh:Tool * `F` edit in Mesh:Tool
* `F` custom device vars for profiles / ranges / gcode * `F` custom device vars for profiles / ranges / gcode
@ -31,10 +32,7 @@
* `B` multi-extruder rendering of raft fails to offset the rest of the print * `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 * `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` new parameters for bridging speed / bridge fan control
* `F` new base paremeter for hole and perimeter compensation
* `F` new parameters for inner / outer perimeter compensation
* `F` convert ranges to z offsets while continuing to show layer # * `F` convert ranges to z offsets while continuing to show layer #
* `F` support pillar top/bottom should conform to part * `F` support pillar top/bottom should conform to part
* `F` more explicit line width control with ranges and min/max adaptive * `F` more explicit line width control with ranges and min/max adaptive
@ -61,9 +59,7 @@
* `B` rapid moves should be max of terrain zmax and last cut layer height (roughing) * `B` rapid moves should be max of terrain zmax and last cut layer height (roughing)
* `B` feed rate for next tool set before tool change (push/pop feed rates?) * `B` feed rate for next tool set before tool change (push/pop feed rates?)
* `B` tab cuts cause loss of direction, parenting, depth, and it's hard to fix
* `B` tabs do not properly track widget mirror events * `B` tabs do not properly track widget mirror events
* `B` tabs are not cut to exact height
* `B` contour does not honor clip to stock * `B` contour does not honor clip to stock
* `F` add lathe step down to eliminate the need for roughing * `F` add lathe step down to eliminate the need for roughing

View file

@ -1,6 +1,6 @@
{ {
"name": "grid-apps", "name": "grid-apps",
"version": "4.3.1", "version": "4.5.0",
"description": "grid.space 3d slicing & modeling tools", "description": "grid.space 3d slicing & modeling tools",
"author": "Stewart Allen <sa@grid.space>", "author": "Stewart Allen <sa@grid.space>",
"license": "MIT", "license": "MIT",
@ -35,12 +35,15 @@
"node": ">=22.0.0" "node": ">=22.0.0"
}, },
"dependencies": { "dependencies": {
"@docusaurus/plugin-client-redirects": "^3.9.0",
"@fortawesome/fontawesome-free": "^6.1.1", "@fortawesome/fontawesome-free": "^6.1.1",
"@gridspace/app-server": "^0.0.18", "@gridspace/app-server": "^0.0.20",
"@gridspace/basic-ftp": "^5.0.5", "@gridspace/basic-ftp": "github:gridspace/basic-ftp#v5.0.5-gridspace",
"@gridspace/net-level-client": "^0.2.3", "@gridspace/net-level-client": "^0.2.3",
"@gridspace/raster-path": "^1.0.7",
"@tracespace/parser": "^5.0.0-next.0", "@tracespace/parser": "^5.0.0-next.0",
"@tweenjs/tween.js": "^16.6.0", "@tweenjs/tween.js": "^16.6.0",
"aedes": "^0.51.3",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"buffer-crc32": "^0.2.13", "buffer-crc32": "^0.2.13",
@ -49,25 +52,28 @@
"earcut": "^3.0.1", "earcut": "^3.0.1",
"express-useragent": "^1.0.13", "express-useragent": "^1.0.13",
"fast-glob": "^3.3.3", "fast-glob": "^3.3.3",
"hexer": "^1.5.0",
"i": "^0.3.7", "i": "^0.3.7",
"jszip": "^3.7.1", "jszip": "^3.7.1",
"manifold-3d": "^3.2.1", "manifold-3d": "^3.2.1",
"moment": "^2.29.4", "moment": "^2.29.4",
"mqtt": "^5.10.3",
"npm": "^11.4.2", "npm": "^11.4.2",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"react-responsive-carousel": "^3.2.23", "react-responsive-carousel": "^3.2.23",
"serve-static": "^1.14.1", "serve-static": "^2.2.0",
"three": "^0.178.0", "three": "^0.178.0",
"three-mesh-bvh": "^0.7.6", "three-mesh-bvh": "^0.7.6",
"uglify-js": "3.14.5", "uglify-js": "3.14.5",
"validator": ">=13.7.0", "validator": ">=13.15.15",
"ws": "^8.18.2" "ws": "^8.18.2"
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/core": "^3.7.0", "@docusaurus/core": "^3.9.0",
"@docusaurus/preset-classic": "^3.7.0", "@docusaurus/preset-classic": "^3.9.0",
"@electron/notarize": "^3.0.0", "@electron/notarize": "^3.0.0",
"copy-webpack-plugin": "^12.0.2", "copy-webpack-plugin": "^12.0.2",
"cross-env": "^10.1.0",
"docusaurus-lunr-search": "^3.6.0", "docusaurus-lunr-search": "^3.6.0",
"dotenv": "latest", "dotenv": "latest",
"electron": "^35.0.1", "electron": "^35.0.1",
@ -81,34 +87,39 @@
"webpack-cli": "^5.1.4" "webpack-cli": "^5.1.4"
}, },
"scripts": { "scripts": {
"setup": "npm i && npm run clean && npm run dev -- --dryrun && npm run webpack-ext", "build-debug": "npm run prebuild && cross-env DEBUG=electron-builder electron-builder",
"build": "npm run prebuild && electron-builder",
"build-nopublish": "npm run prebuild && electron-builder --publish never",
"build-debug": "npm run prebuild && DEBUG=electron-builder electron-builder",
"build-linux": "npm run build -- --linux --x64",
"build-linux-arm": "npm run build -- --linux --arm64", "build-linux-arm": "npm run build -- --linux --arm64",
"build-win": "npm run build -- --win --x64", "build-linux": "npm run build -- --linux --x64",
"build-win-arm": "npm run build -- --win --arm64",
"build-mac": "npm run build -- --mac --arm64",
"build-mac-intel": "npm run build -- --mac --x64", "build-mac-intel": "npm run build -- --mac --x64",
"clean": "rm -rf build data dist src/pack tmp src.old web.old", "build-mac": "npm run build -- --mac --arm64",
"docs-dev": "docusaurus start --config conf/docusaurus.config.js", "build-nopublish": "npm run prebuild && electron-builder --publish never",
"build-win-arm": "npm run build -- --win --arm64",
"build-win": "npm run build -- --win --x64",
"build": "npm run prebuild && electron-builder",
"bundle:dev": "node bin/bundler.mjs bin/bundle.config.json",
"bundle:prod": "node bin/bundler.mjs bin/bundle-prod.config.json",
"bundle": "cross-env COMPRESS=1 npm run bundle:prod",
"clean": "rm -rf alt build data dist src/pack src.old tmp web.old .bcache",
"dev": "npm run pack-dev && gs-app-server --debug --single",
"docs-build": "docusaurus build --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", "docs-check": "prettier --config ./conf/prettier.config.js ./docs --check",
"dev": "npm run webpack-ext && gs-app-server --debug --single", "docs-dev": "docusaurus start --config conf/docusaurus.config.js",
"prod": "npm run webpack-ext && npm run webpack-src prod && gs-app-server", "docs-serve": "docusaurus serve --config conf/docusaurus.config.js",
"prod-pack": "npm run webpack-ext && npm run webpack-src prod", "dryrun": "gs-app-server --debug --single --dryrun",
"prod-dryrun": "gs-app-server --dryrun", "dryrun-prod": "gs-app-server --single --dryrun",
"start": "npm run prebuild prod && electron .", "mac-verify": "spctl --assess -vv --type install dist/*/*.app",
"start-dev": "npm run prebuild prod && electron . --devel", "mk-links": "find src web -type l | xargs -I{} sh -c 'echo \"{},$(readlink {})\"' > conf/links.csv",
"pack-dev": "npm run webpack-src && npm run dryrun && npm run bundle:dev",
"pack-prod": "npm run webpack-src prod && npm run dryrun && npm run bundle",
"postbuild": "node bin/electron-post.js",
"prebuild": "npm run setup && cross-env ELECTRON_BUILD=1 npm run pack-dev && node bin/electron-pre.js",
"preinstall": "node bin/install-pre.js",
"prod": "npm run pack-prod && gs-app-server",
"setup": "npm run clean && npm i && npm run webpack-ext",
"start-dbg": "npm run prebuild && electron . --debugg", "start-dbg": "npm run prebuild && electron . --debugg",
"start-ddb": "npm run prebuild && electron . --devel --debugg", "start-ddb": "npm run prebuild && electron . --devel --debugg",
"mk-links": "find src web -type l | xargs -I{} sh -c 'echo \"{},$(readlink {})\"' > conf/links.csv", "start-dev": "npm run prebuild prod && electron . --devel",
"mac-verify": "spctl --assess -vv --type install dist/*/*.app", "start": "npm run prebuild && electron .",
"prebuild": "npm run setup && npm run webpack-src prod && node bin/electron-pre.js",
"postbuild": "node bin/electron-post.js",
"preinstall": "node bin/install-pre.js",
"webpack-ext": "npm run webpack-three && npm run webpack-zip", "webpack-ext": "npm run webpack-three && npm run webpack-zip",
"webpack-src": "node bin/esbuild.config.mjs", "webpack-src": "node bin/esbuild.config.mjs",
"webpack-three": "npx webpack --config bin/webpack-three-esm.js", "webpack-three": "npx webpack --config bin/webpack-three-esm.js",
@ -122,52 +133,17 @@
"artifactName": "KiriMoto-${os}-${arch}.${ext}", "artifactName": "KiriMoto-${os}-${arch}.${ext}",
"files": [ "files": [
{ {
"from": "tmp/src", "from": "tmp",
"to": "src", "to": "",
"filter": [
"**/*"
]
},
{
"from": "tmp/web",
"to": "web",
"filter": [
"**/*"
]
},
{
"from": "tmp/mod",
"to": "mod",
"filter": [
"**/*"
]
},
{
"from": "tmp/mod/node_modules",
"to": "mod/node_modules",
"filter": [
"**/*"
]
},
{
"from": "tmp/mods",
"to": "mods",
"filter": [
"**/*"
]
},
{
"from": "tmp/mods/node_modules",
"to": "mods/node_modules",
"filter": [ "filter": [
"**/*" "**/*"
] ]
}, },
"app-el.js",
"app.js",
"bin/*", "bin/*",
"conf/**/*", "conf/**/*",
"data/cache/**/*", "data/cache/**/*",
"app-el.js",
"app.js",
"package.json" "package.json"
], ],
"extraFiles": [], "extraFiles": [],

View file

@ -9,27 +9,27 @@
![GitHub last commit](https://img.shields.io/github/last-commit/GridSpace/grid-apps) ![GitHub last commit](https://img.shields.io/github/last-commit/GridSpace/grid-apps)
![GitHub](https://img.shields.io/github/license/GridSpace/grid-apps) ![GitHub](https://img.shields.io/github/license/GridSpace/grid-apps)
# Community # Community
[Discord](https://discord.com/invite/suyCCgr) -- Live Chat [Discord](https://discord.com/invite/suyCCgr) -- Live Chat
[Forums](https://forum.grid.space/) -- Long Form and Archival Discussion [Forums](https://forum.grid.space/) -- Long Form and Archival Discussion
[BlueSky](https://bsky.app/profile/grid.space) -- Like the Good 'Ol Days
[YouTube](https://www.youtube.com/c/gridspace) -- Content when I have time [YouTube](https://www.youtube.com/c/gridspace) -- Content when I have time
# Free and Open Source # Free and Open Source
Kiri:Moto and Mesh:Tool are completely open source and free for use without restriction. Over 12 years in development, this passion project has grown well beyond its original scope. It has consumed most of my free time for many years. Please consider donating to support continued development GitHub sponsorship or PayPal. Kiri:Moto and Mesh:Tool are completely open source and free for use without restriction. Over 13 years in development, this passion project has grown well beyond its original scope. It has consumed most of my free time for many years. Please consider donating to support continued development GitHub sponsorship or PayPal.
[![GitHub Sponsors](https://img.shields.io/github/sponsors/GridSpace)](https://github.com/sponsors/GridSpace) [![GitHub Sponsors](https://img.shields.io/github/sponsors/GridSpace)](https://github.com/sponsors/GridSpace)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/gridspace3d?locale.x=en_US) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/gridspace3d?locale.x=en_US)
# Documentation # Documentation
[Documentation](https://docs.grid.space/) -- Could really use help with this [Documentation](https://docs.grid.space/) -- Could really use help with this
Docs are build with [Docusaurus](https://docusaurus.io/) and served using [GitHub Pages](https://pages.github.com/). Docs are build with [Docusaurus](https://docusaurus.io/) and served using [GitHub Pages](https://pages.github.com/).
You can build and view the docs locally with: You can build and view the docs locally with:
``` ```
git clone git@github.com:GridSpace/grid-apps.git git clone git@github.com:GridSpace/grid-apps.git
cd grid-apps cd grid-apps
@ -45,7 +45,6 @@ npm run docs-dev
[`Mesh:Tool`](https://grid.space/mesh) is a browser-based mesh repair and editing tool [`Mesh:Tool`](https://grid.space/mesh) is a browser-based mesh repair and editing tool
# Electron Builds (Desktop Binaries) # Electron Builds (Desktop Binaries)
https://github.com/GridSpace/grid-apps/releases/ https://github.com/GridSpace/grid-apps/releases/
@ -62,7 +61,6 @@ chmod 755 KiriMoto-linux-x86_64.AppImage
The Windows and Mac binaries are not signed, so you will need to jump through a few safety hoops to get them to open the first time. The Windows and Mac binaries are not signed, so you will need to jump through a few safety hoops to get them to open the first time.
# Linux / Mac Developers # Linux / Mac Developers
## Testing Locally (with Docker) ## Testing Locally (with Docker)

View file

@ -2,6 +2,162 @@
Full docs @ https://docs.grid.space/projects/kiri-moto Full docs @ https://docs.grid.space/projects/kiri-moto
# Release 4.4.0
## General
* add version numbering utility script
* split out help/info menu and language menus
* move install/uninstall/quit menu to center app menu
* add help menu visual callout
* improve language translation coverage
* improve build and serve workflow for alternate builds
* update service worker configuration
* add install/uninstall options in setup menu
## CAM
* implement tab hopping - toolpath goes up and over tab instead of interrupting cut (fixes #207)
* add GPU accelerated contouring for faster operations
* add radial GPU raster support (experimental)
* add new register mode that marks bottom cutout tabs
* add curves-only support to GPU contour
* add zsafe and move to safe z at start of new operations
* fix 'safe' moves during operation changes to be moves, not cuts
* fix tolerance/resolution for GPU raster mesh calculations
* fix NullPointerException with tabs in contour
* improve merge/co-planar point handling
* add slice layer naming using operation notes (fixes #447)
* fix level step down units (fixes #446)
## FDM
* document remain_time macro variable (refs #339)
* re-add base flow rate multiplier for belt mode (fixes #444)
* fix belt fan control ordering (fixes #438)
* exclude belt from bridge fan layer
* add centering feature for fill areas (fixes #448)
## Mesh:Tool
* add STEP export capability
* add STEP import with face generation
* improve face generation with inner holes
* add mesh scripting tool and persistence
* add plane and plane.loft operations
* add global edge map and edge reuse for better topology
* add devel device export option
## Electron
* clean up electron build process
* merge electron mod into core
* add service routes when available
* set proper content-type for appended code bodies
## Dependencies
* update @gridspace/raster-path to latest version with GPU acceleration
* update @gridspace/app-server for proper fallthrough handling
* switch from npmjs.org to git repos for @gridspace packages
* update WebGPU implementation
## Bug Fixes
* fix Bambu Lab local URL and module load order
* fix progress reporting for raster operations
* fix GPU lathe progress and contour API usage
* fix tool offsets when using GPU acceleration
* fix indexed rotations epsilon value for accurate zflat calculation
* fix traceload debug option
* sanitize topo3 resolution to avoid GPU floating-point errors
# Release 4.3
## General
* add lathe mode for CAM operations
* add volumetric flow calculations for FDM
* improve bundler with better dependency tracking
* migrate to ESM module format across codebase
* improve arc support in gcode generation
## FDM
* add scarf seams for better surface finish
* add hole compensation feature
* add spiral layer start option
* overhaul thin wall detection and handling
* improve volumetric flow rate controls
* add retraction tuning for better print quality
## CAM
* add lathe operation support with threading
* improve arc support for smoother toolpaths
* add drill from stock top option
* improve tab positioning and generation
* enhance surface selection and filtering
* add 4th axis lathe debug and testing mode
* improve tool path optimization
## Devices
* add new machine profiles
* improve device profile management
# Release 4.2
## CAM
* add arc support to gcode output for smoother paths
* add drill from stock top feature
* improve contour operations
* fix animation issues with shared tool numbers
* enhance trace operations with arc support
* improve pocket smoothing algorithms
## FDM
* improve support generation
* add new retraction options
* enhance layer time controls
## General
* improve gcode arc import and export
* fix file loading edge cases
* update device profiles
# Release 4.1
## CAM
* improve drill operations and positioning
* fix multi-part drilling bugs
* enhance trace line selection
* add dogbone support to trace ops
* improve pocket operation reliability
* fix trace selection with flip operations
## FDM
* add RatRig machine profiles
* improve Bambu Lab device control integration
* enhance support placement algorithms
* fix belt mode support generation
## General
* improve file import handling
* fix workspace restore for complex projects
* enhance mobile touch interactions
* update machine profiles for popular devices
# Release 4.0 (2024-01-21) # Release 4.0 (2024-01-21)
* major UI refactor * major UI refactor

View file

@ -168,7 +168,7 @@ SP.init = function(options = {}) {
request = IDB.open(name, this.version); request = IDB.open(name, this.version);
request.onupgradeneeded = function(event) { request.onupgradeneeded = function(event) {
let db = storage.db = request.result; let db = request.result;
let current = [...db.objectStoreNames]; let current = [...db.objectStoreNames];
let stores = storage.stores; let stores = storage.stores;
// add missing stores // add missing stores
@ -184,13 +184,12 @@ SP.init = function(options = {}) {
// remove obsolete stores // remove obsolete stores
for (let curr of current) { for (let curr of current) {
if (stores.indexOf(curr) < 0) { if (stores.indexOf(curr) < 0) {
storage.db.deleteObjectStore(curr); db.deleteObjectStore(curr);
console.log({index_dropped: curr}); console.log({index_dropped: curr});
} }
} }
event.target.transaction.oncomplete = function(event) { // allow transactions
storage.runQueue(); storage.db = db;
};
}; };
request.onsuccess = function(event) { request.onsuccess = function(event) {
@ -217,6 +216,7 @@ SP.runQueue = function() {
let i = 0, q = this.queue, e; let i = 0, q = this.queue, e;
while (i < q.length) { while (i < q.length) {
e = q[i++]; e = q[i++];
// console.log('runQ', i, ...e);
switch (e.shift()) { switch (e.shift()) {
case 'iterate': this.iterate(...e); break; case 'iterate': this.iterate(...e); break;
case 'keys': this.keys(...e); break; case 'keys': this.keys(...e); break;
@ -252,7 +252,11 @@ SP.put = function(key, value, callback, store = this.current) {
req.onerror = function(event) { callback(false) }; req.onerror = function(event) { callback(false) };
} }
} catch (e) { } catch (e) {
console.log(e); if (e.name === 'InvalidStateError') {
console.log({ putRetry: key });
return this.queue.push(['put', key, value, callback, store]);
}
console.log(e.name, e);
if (callback) callback(false); if (callback) callback(false);
} }
}; };
@ -275,6 +279,10 @@ SP.get = function(key, callback, store = this.current) {
req.onerror = function(event) { callback(null) }; req.onerror = function(event) { callback(null) };
} }
} catch (e) { } catch (e) {
if (e.name === 'InvalidStateError') {
console.log({ getRetry: key });
return this.queue.push(['get', key, callback, store]);
}
console.log(e); console.log(e);
if (callback) callback(null); if (callback) callback(null);
} }

View file

@ -4,4 +4,4 @@
import './pngjs.js'; import './pngjs.js';
// Re-export the PNG object // Re-export the PNG object
export const PNG = self.png ? self.png.PNG : null; export const PNG = globalThis.png ? globalThis.png.PNG : null;

View file

@ -1,4 +1,4 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.png = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ (function(f){globalThis.png = f();if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.png = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
(function (Buffer){ (function (Buffer){
"use strict"; "use strict";

View file

@ -127,6 +127,12 @@ class Point {
return p; return p;
} }
// annotate instance with other field data (see clone())
annotate(obj = {}) {
Object.assign(this, obj);
return this;
}
slopeTo(p) { slopeTo(p) {
return newSlope(this, p); return newSlope(this, p);
} }
@ -441,10 +447,6 @@ class Point {
return this === p || (this.x === p.x && this.y === p.y && this.z === p.z); return this === p || (this.x === p.x && this.y === p.y && this.z === p.z);
} }
isEqual2D(p) {
return this === p || (this.x === p.x && this.y === p.y);
}
/** /**
* returns true if points are close enough to be considered equivalent * returns true if points are close enough to be considered equivalent
*/ */

View file

@ -908,6 +908,7 @@ export class Polygon {
* todo make more efficient * todo make more efficient
* *
* @param {Boolean} deep * @param {Boolean} deep
* @param {String[]} fields to copy beyond class fields
* @returns {Polygon} * @returns {Polygon}
*/ */
clone(deep, fields) { clone(deep, fields) {
@ -2145,6 +2146,7 @@ export class Polygon {
return null; return null;
} }
// annotate instance with other field data (see clone())
annotate(obj = {}) { annotate(obj = {}) {
Object.assign(this, obj); Object.assign(this, obj);
return this; return this;

View file

@ -12,6 +12,8 @@ import { polygons } from '../geo/polygons.js';
import { newPoint } from '../geo/point.js'; import { newPoint } from '../geo/point.js';
import { config } from '../geo/base.js'; import { config } from '../geo/base.js';
const epsilon = 10e-5;
function dval(v, dv) { function dval(v, dv) {
return v !== undefined ? v : dv; return v !== undefined ? v : dv;
} }
@ -75,7 +77,7 @@ export async function slice(points, options = {}) {
// used to calculate buckets (rough sum of z span) // used to calculate buckets (rough sum of z span)
zSum += (Math.abs(p1.z - p2.z) + Math.abs(p2.z - p3.z) + Math.abs(p3.z - p1.z)); zSum += (Math.abs(p1.z - p2.z) + Math.abs(p2.z - p3.z) + Math.abs(p3.z - p1.z));
// use co-flat and co-line detection to adjust slice Z // use co-flat and co-line detection to adjust slice Z
if (p1.z === p2.z && p2.z === p3.z && p1.z >= zMin) { if (Math.abs(p1.z - p2.z) < epsilon && Math.abs(p2.z - p3.z) < epsilon && p1.z >= zMin) {
// detect faces co-planar with Z and sum the enclosed area // detect faces co-planar with Z and sum the enclosed area
let zkey = p1.z, let zkey = p1.z,
area = Math.abs(base.util.area2(p1,p2,p3)) / 2; area = Math.abs(base.util.area2(p1,p2,p3)) / 2;

View file

@ -60,13 +60,6 @@ export class Slope {
return this; return this;
} }
invert() {
this.dx = -this.dx;
this.dy = -this.dy;
this.angle = 180 - this.angle;
return this;
}
angleDiff(s2,sign) { angleDiff(s2,sign) {
const n1 = this.angle; const n1 = this.angle;
const n2 = s2.angle; const n2 = s2.angle;

1
src/gpu/raster-worker.js Symbolic link
View file

@ -0,0 +1 @@
../../node_modules/@gridspace/raster-path/build/raster-worker.js

1
src/gpu/raster.js Symbolic link
View file

@ -0,0 +1 @@
../../node_modules/@gridspace/raster-path/build/raster-path.js

View file

@ -9,7 +9,7 @@ import { client as work } from './client.js';
import { consts, COLOR as color, LISTS as lists } from './consts.js'; import { consts, COLOR as color, LISTS as lists } from './consts.js';
import { device, devices } from './devices.js'; import { device, devices } from './devices.js';
import { catalog, dialog, event, group, help, hide, image } from './main.js'; import { catalog, dialog, event, group, help, hide, image } from './main.js';
import { modal, mode, probe, process, show, space, util, view } from './main.js'; import { modal, mode, process, show, space, util, view } from './main.js';
import { local as dataLocal } from '../../data/local.js'; import { local as dataLocal } from '../../data/local.js';
import { noop, ajax, o2js, js2o } from './utils.js'; import { noop, ajax, o2js, js2o } from './utils.js';
import { functions } from './function.js'; import { functions } from './function.js';
@ -96,6 +96,7 @@ let UC = UI.prefix('kiri').inputAction(settings.conf.update),
put: (key, val) => localSet(key, val), put: (key, val) => localSet(key, val),
set: (key, val) => localSet(key, val), set: (key, val) => localSet(key, val),
setItem: (key, val) => localSet(key, val), setItem: (key, val) => localSet(key, val),
removeItem: (key) => localRemove(key)
}, },
tweak = { tweak = {
line_precision(v) { api.work.config({ base: { clipperClean: v } }) }, line_precision(v) { api.work.config({ base: { clipperClean: v } }) },
@ -119,6 +120,11 @@ function localSet(key, val) {
return val; return val;
} }
function localRemove(key) {
let sloc = api.conf.get().local;
return delete sloc[key];
}
export const api = { export const api = {
ajax, ajax,
beta, beta,
@ -137,6 +143,7 @@ export const api = {
dialog, dialog,
doit, doit,
event, event,
electron: navigator.userAgent.includes('Electron'),
feature, feature,
function: functions, function: functions,
group, group,
@ -156,7 +163,6 @@ export const api = {
o2js, o2js,
onkey, onkey,
platform, platform,
probe,
process, process,
sdb: dataLocal, sdb: dataLocal,
selection, selection,

View file

@ -225,7 +225,9 @@ Widget.prototype.encode = function(state) {
json: json, // safe for JSON (float32array mess) json: json, // safe for JSON (float32array mess)
group: this.group.id, group: this.group.id,
track: this.track, track: this.track,
geo: json ? Array.from(geo) : geo geo: json ? Array.from(geo) : geo,
meta: this.meta,
anno: this.anno,
}; };
return coded; return coded;
}; };
@ -234,8 +236,12 @@ registerDecoder(TYPE.WIDGET, function(v, state) {
const id = v.id, const id = v.id,
group = v.group || id, group = v.group || id,
track = v.track || undefined, 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.loadVertices(v.json ? v.geo.toFloat32() : v.geo);
widget.saved = Date.now(); widget.saved = Date.now();
if (track && track.pos) { if (track && track.pos) {

View file

@ -182,33 +182,33 @@ function normalize(settings) {
// auto field renaming on import // auto field renaming on import
const renamed = { const renamed = {
roughingTool: "camRoughTool", camWideCutout: "camOutlineWide",
roughingSpindle: "camRoughSpindle", drillDown: "camDrillDown",
roughingDown: "camRoughDown", drillDownSpeed: "camDrillDownSpeed",
roughingOver: "camRoughOver", drillDwell: "camDrillDwell",
roughingSpeed: "camRoughSpeed", drillingOn: "camDrillingOn",
roughingPlunge: "camRoughPlunge", drillLift: "camDrillLift",
roughingStock: "camRoughStock", drillSpindle: "camDrillSpindle",
roughingPocket: "camRoughVoid", drillTool: "camDrillTool",
roughingOn: "camRoughOn",
finishingTool: "camOutlineTool",
finishingSpindle: "camOutlineSpindle",
finishingDown: "camOutlineDown", finishingDown: "camOutlineDown",
finishingOver: "camContourOver",
finishingSpeed: "camOutlineSpeed",
finishingPlunge: "camOutlinePlunge",
finishingOn: "camOutlineOn", finishingOn: "camOutlineOn",
finishingOver: "camContourOver",
finishingPlunge: "camOutlinePlunge",
finishingSpeed: "camOutlineSpeed",
finishingSpindle: "camOutlineSpindle",
finishingTool: "camOutlineTool",
finishingXOn: "camContourXOn", finishingXOn: "camContourXOn",
finishingYOn: "camContourYOn", finishingYOn: "camContourYOn",
drillTool: "camDrillTool", outputClockwise: "camConventional",
drillSpindle: "camDrillSpindle", roughingDown: "camRoughDown",
drillDownSpeed: "camDrillDownSpeed", roughingOn: "camRoughOn",
drillDown: "camDrillDown", roughingOver: "camRoughOver",
drillDwell: "camDrillDwell", roughingPlunge: "camRoughPlunge",
drillLift: "camDrillLift", roughingPocket: "camRoughVoid",
drillingOn: "camDrillingOn", roughingSpeed: "camRoughSpeed",
camWideCutout: "camOutlineWide", roughingSpindle: "camRoughSpindle",
outputClockwise: "camConventional" roughingStock: "camRoughStock",
roughingTool: "camRoughTool",
}; };
export const conf = { export const conf = {
@ -258,95 +258,95 @@ export const conf = {
}, },
// process defaults FDM:Process // process defaults FDM:Process
p:{ p:{
antiBacklash: 0,
arcTolerance: 0,
firstLayerBedTemp: 0,
firstLayerBeltBump: 0,
firstLayerBeltLead: 3,
firstLayerBeltFact: 1,
firstLayerBrim: 0,
firstLayerBrimGap: 0,
firstLayerBrimIn: 0,
firstLayerBrimTrig: 0,
firstLayerFanSpeed: 0,
firstLayerFillRate: 35,
firstLayerFlatten: 0,
firstLayerNozzleTemp: 0,
firstLayerRate: 30,
firstLayerYOffset: 0,
firstSliceHeight: 0.25,
outputAlternating: false,
outputAvoidGaps: true,
outputBedTemp: 60,
outputBeltFirst: false,
outputBrimCount: 2,
outputBrimOffset: 2,
outputCoastDist: 0,
outputDraftShield: false,
outputFanLayer: 1,
outputFanSpeed: 255,
outputFeedrate: 50,
outputFillMult: 1.25,
outputFinishrate: 50,
outputInvertX: false,
outputInvertY: false,
outputLayerRetract: false,
outputLoops: 0,
outputMaxFlowrate: 15,
outputMinLayerTime: 10,
outputMinSpeed: 5.0,
outputPurgeTower: 0,
outputRaft: false,
outputRaftSpacing: 0.2,
outputRetractDist: 1.5,
outputRetractDwell: 20,
outputRetractSpeed: 40,
outputRetractWipe: 0,
outputScarfLength: 0,
outputSeekrate: 80,
outputShellMult: 1.25,
outputShortPoly: 0.0,
outputSparseMult: 1.25,
outputTemp: 200,
processName: "default", processName: "default",
ranges: [],
sliceAdaptive: false,
sliceAngle: 45, sliceAngle: 45,
sliceBottomLayers: 3,
sliceCompInner: 0,
sliceCompOuter: 0,
sliceDetectThin: "basic",
sliceFillAngle: 45,
sliceFillGrow: 0,
sliceFillOverlap: 0.35,
sliceFillRepeat: 1,
sliceFillSparse: 0.25,
sliceFillType: "hex",
sliceFillWidth: 1,
sliceHeight: 0.25, sliceHeight: 0.25,
sliceShells: 3,
sliceShellOrder: "in-out",
sliceLayerStart: "last", sliceLayerStart: "last",
sliceLayerStartX: 0, sliceLayerStartX: 0,
sliceLayerStartY: 0, sliceLayerStartY: 0,
sliceLineWidth: 0, sliceLineWidth: 0,
sliceFillGrow: 0,
sliceFillAngle: 45,
sliceFillWidth: 1,
sliceFillOverlap: 0.35,
sliceFillSparse: 0.25,
sliceFillRepeat: 1,
sliceFillRate: 0,
sliceSolidRate: 0,
sliceFillType: "hex",
sliceSupportDensity: 0.1,
sliceSupportOffset: 1.0,
sliceSupportGap: 1,
sliceSupportSize: 5,
sliceSupportArea: 0.1,
sliceSupportSpan: 5,
sliceSupportGrow: 0,
sliceSupportExtra: 0,
sliceSupportAngle: 50,
sliceSupportNozzle: 0,
sliceSupportEnable: false,
sliceSupportOutline: true,
sliceZInterleave: false,
sliceSolidMinArea: 1,
sliceBottomLayers: 3,
sliceTopLayers: 3,
firstSliceHeight: 0.25,
firstLayerRate: 30,
firstLayerFillRate: 35,
firstLayerPrintMult: 1.0,
firstLayerLineMult: 1.0,
firstLayerYOffset: 0,
firstLayerNozzleTemp: 0,
firstLayerBedTemp: 0,
firstLayerBrim: 0,
firstLayerBrimIn: 0,
firstLayerBrimTrig: 0,
firstLayerBrimGap: 0,
firstLayerBeltLead: 3,
firstLayerBeltBump: 0,
firstLayerFanSpeed: 0,
firstLayerFlatten: 0,
outputRaft: false,
outputRaftSpacing: 0.2,
outputDraftShield: false,
outputTemp: 200,
outputBedTemp: 60,
outputFeedrate: 50,
outputFinishrate: 50,
outputSeekrate: 80,
outputShellMult: 1.25,
outputFillMult: 1.25,
outputSparseMult: 1.25,
outputFanLayer: 1,
outputFanSpeed: 255,
outputRetractDist: 1.5,
outputRetractSpeed: 40,
outputRetractWipe: 0,
outputRetractDwell: 20,
outputBrimCount: 2,
outputBrimOffset: 2,
outputScarfLength: 0,
outputShortPoly: 100.0,
outputMinSpeed: 10.0,
outputMinLayerTime: 0,
outputCoastDist: 0,
outputPurgeTower: 0,
outputBeltFirst: false,
outputAvoidGaps: true,
outputAlternating: false,
outputLayerRetract: false,
outputLoops: 0,
outputInvertX: false,
outputInvertY: false,
sliceDetectThin: "off",
sliceMinHeight: 0, sliceMinHeight: 0,
sliceAdaptive: false, sliceShellOrder: "in-out",
sliceShells: 3,
sliceSolidMinArea: 1,
sliceSupportAngle: 50,
sliceSupportArea: 0.1,
sliceSupportDensity: 0.1,
sliceSupportEnable: false,
sliceSupportExtra: 0,
sliceSupportGap: 1,
sliceSupportGrow: 0,
sliceSupportNozzle: 0,
sliceSupportOffset: 1.0,
sliceSupportOutline: true,
sliceSupportSize: 5,
sliceSupportSpan: 5,
sliceTopLayers: 3,
sliceZInterleave: false,
zHopDistance: 0.2, zHopDistance: 0.2,
arcTolerance: 0,
antiBacklash: 0,
ranges: []
} }
}, },
sla:{ sla:{
@ -420,186 +420,187 @@ export const conf = {
}, },
// process defaults CAM:Process // process defaults CAM:Process
p:{ p:{
processName: "default", camArcEnabled: false,
camLevelTool: 1000, camArcResolution: 5,
camLevelSpindle: 1000, camArcTolerance: 0.15,
camLevelOver: 0.75,
camLevelStepZ: 0,
camLevelSpeed: 1000,
camLevelDown: 0,
camLevelInset: 0.5,
camLevelStock: true,
camRoughTool: 1000,
camRoughSpindle: 1000,
camRoughDown: 2,
camRoughOver: 0.4,
camRoughSpeed: 1000,
camRoughPlunge: 250,
camRoughStock: 0,
camRoughStockZ: 0,
camRoughAll: true,
camRoughVoid: false,
camRoughFlat: true,
camRoughTop: true,
camRoughIn: true,
camRoughOn: true,
camRoughOmitThru: false,
camRoughOmitVoid: false,
camOutlineTool: 1000,
camOutlineSpindle: 1000,
camOutlineTop: true,
camOutlineDown: 3,
camOutlineOver: 0.4,
camOutlineOverCount: 1,
camOutlineSpeed: 800,
camOutlinePlunge: 250,
camOutlineWide: false,
camOutlineDogbone: false,
camOutlineOmitThru: false,
camOutlineOmitVoid: false,
camOutlineOut: true,
camOutlineIn: false,
camOutlineOn: true,
camContourTool: 1000,
camContourSpindle: 1000,
camContourOver: 0.5,
camContourSpeed: 1000,
camContourAngle: 85, camContourAngle: 85,
camContourLeave: 0,
camContourReduce: 2,
camContourBottom: false, camContourBottom: false,
camContourBridge: 0,
camContourCurves: false, camContourCurves: false,
camContourIn: false, camContourIn: false,
camContourLeave: 0,
camContourOver: 0.5,
camContourReduce: 2,
camContourSpeed: 1000,
camContourSpindle: 1000,
camContourTool: 1000,
camContourXOn: true, camContourXOn: true,
camContourYOn: true, camContourYOn: true,
camLatheTool: 1000, camConventional: false, // outputClockwise
camLatheSpindle: 1000, camDepthFirst: false,
camLatheOver: 0.1,
camLatheAngle: 1,
camLatheSpeed: 500,
camLatheLinear: true,
camTolerance: 0,
camTraceTool: 1000,
camTraceSpindle: 1000,
camTraceType: "follow",
camTraceOver: 0.5,
camTraceDown: 0,
camTraceThru: 0,
camTraceSpeed: 250,
camTracePlunge: 200,
camTraceOffOver: 0,
camTraceDogbone: false,
camTraceMerge: false,
camTraceLines: false,
camTraceZTop: 0,
camTraceZBottom: 0,
camPocketSpindle: 1000,
camPocketTool: 1000,
camPocketOver: 0.25,
camPocketDown: 1,
camPocketSpeed: 250,
camPocketPlunge: 200,
camPocketExpand: 0,
camPocketSmooth: 0,
camPocketRefine: 20,
camPocketFollow: 5,
camPocketContour: false,
camPocketEngrave: false,
camPocketOutline: false,
camPocketZTop: 0,
camPocketZBottom: 0,
camDrillTool: 1006,
camDrillSpindle: 1000,
camDrillDownSpeed: 250,
camDrillDown: 5, camDrillDown: 5,
camDrillDownSpeed: 250,
camDrillDwell: 250, camDrillDwell: 250,
camDrillFromStockTop: false,
camDrillingOn: false,
camDrillLift: 2, camDrillLift: 2,
camDrillMark: false, camDrillMark: false,
camDrillFromStockTop: false,
camDrillThru: 5,
camDrillPrecision: 1, camDrillPrecision: 1,
camDrillingOn: false, camDrillSpindle: 1000,
camRegisterSpeed: 1000, camDrillThru: 5,
camRegisterThru: 5, camDrillTool: 1006,
camRegisterOffset: 10,
camHelicalTool: 1000,
camHelicalSpindle: 1000,
camHelicalDownSpeed: 250,
camHelicalSpeed: 1000,
camHelicalDown: 5,
camHelicalBottomFinish: true,
camHelicalThru: 0,
camHelicalOffset: "auto",
camHelicalForceStartAngle: false,
camHelicalStartAngle: 0,
camHelicalOffsetOverride: 0,
camHelicalEntry: false,
camHelicalEntryOffset:0,
camHelicalReverse: false,
camHelicalClockwise: true,
camFlipAxis: "X",
camFlipOther: "",
camLaserEnable: ["M321"],
camLaserDisable: ["M322"],
camLaserOn: ["M3"],
camLaserOff: ["M5"],
camLaserSpeed: 100,
camLaserPower: 1,
camLaserAdaptive: false,
camLaserAdaptMod: false,
camLaserFlatten: false,
camLaserFlatZ: 0,
camLaserPowerMin: 0,
camLaserPowerMax: 1,
camLaserZMin: 0,
camLaserZMax: 0,
camTabsWidth: 5,
camTabsHeight: 5,
camTabsDepth: 5,
camTabsMidline: false,
camDepthFirst: false,
camInnerFirst: false,
camEaseDown: false,
camEaseAngle: 10, camEaseAngle: 10,
camOriginTop: true, camEaseDown: false,
camZAnchor: "middle", camExpertFast: false,
camZOffset: 0,
camZTop: 0,
camZBottom: 0,
camZClearance: 1,
camZThru: 0,
camFastFeed: 6000, camFastFeed: 6000,
camFastFeedZ: 300, camFastFeedZ: 300,
camTolerance: 0, camFirstZMax: false,
camFlatness: 0.001, camFlatness: 0.001,
camContourBridge: 0, camFlipAxis: "X",
camStockX: 5, camFlipOther: "",
camStockY: 5, camForceZMax: false,
camStockZ: 5, camFullEngage: 0.8,
camStockOffset: true, camHelicalBottomFinish: true,
camStockClipTo: false, camHelicalClockwise: true,
camStockIndexed: false, camHelicalDown: 5,
camStockIndexGrid: true, camHelicalDownSpeed: 250,
camIndexAxis: 0, camHelicalEntry: false,
camHelicalEntryOffset:0,
camHelicalForceStartAngle: false,
camHelicalOffset: "auto",
camHelicalOffsetOverride: 0,
camHelicalReverse: false,
camHelicalSpeed: 1000,
camHelicalSpindle: 1000,
camHelicalStartAngle: 0,
camHelicalThru: 0,
camHelicalTool: 1000,
camIndexAbs: true, camIndexAbs: true,
camConventional: false, // outputClockwise camIndexAxis: 0,
camInnerFirst: false,
camLaserAdaptive: false,
camLaserAdaptMod: false,
camLaserDisable: ["M322"],
camLaserEnable: ["M321"],
camLaserFlatten: false,
camLaserFlatZ: 0,
camLaserOff: ["M5"],
camLaserOn: ["M3"],
camLaserPower: 1,
camLaserPowerMax: 1,
camLaserPowerMin: 0,
camLaserSpeed: 100,
camLaserZMax: 0,
camLaserZMin: 0,
camLatheAngle: 1,
camLatheLinear: true,
camLatheOver: 0.1,
camLatheSpeed: 500,
camLatheSpindle: 1000,
camLatheTool: 1000,
camLatheOffStart: 0,
camLatheOffEnd: 0,
camLevelDown: 0,
camLevelInset: 0.5,
camLevelOver: 0.75,
camLevelSpeed: 1000,
camLevelSpindle: 1000,
camLevelStepZ: 0,
camLevelStock: true,
camLevelTool: 1000,
camOriginCenter: false, camOriginCenter: false,
camOriginOffX: 0, camOriginOffX: 0,
camOriginOffY: 0, camOriginOffY: 0,
camOriginOffZ: 0, camOriginOffZ: 0,
camOriginTop: true,
camOutlineDogbone: false,
camOutlineDown: 3,
camOutlineIn: false,
camOutlineOmitThru: false,
camOutlineOmitVoid: false,
camOutlineOn: true,
camOutlineOut: true,
camOutlineOver: 0.4,
camOutlineOverCount: 1,
camOutlinePlunge: 250,
camOutlineSpeed: 800,
camOutlineSpindle: 1000,
camOutlineTool: 1000,
camOutlineTop: true,
camOutlineWide: false,
camPocketContour: false,
camPocketDown: 1,
camPocketEngrave: false,
camPocketExpand: 0,
camPocketFollow: 5,
camPocketOutline: false,
camPocketOver: 0.25,
camPocketPlunge: 200,
camPocketRefine: 20,
camPocketSmooth: 1,
camPocketSpeed: 250,
camPocketSpindle: 1000,
camPocketTool: 1000,
camPocketZBottom: 0,
camPocketZTop: 0,
camRegisterOffset: 10,
camRegisterSpeed: 1000,
camRegisterThru: 5,
camRoughAll: true,
camRoughDown: 2,
camRoughFlat: true,
camRoughIn: true,
camRoughOmitThru: false,
camRoughOmitVoid: false,
camRoughOn: true,
camRoughOver: 0.4,
camRoughPlunge: 250,
camRoughSpeed: 1000,
camRoughSpindle: 1000,
camRoughStock: 0,
camRoughStockZ: 0,
camRoughTool: 1000,
camRoughTop: true,
camRoughVoid: false,
camStockClipTo: false,
camStockIndexed: false,
camStockIndexGrid: true,
camStockOffset: true,
camStockX: 5,
camStockY: 5,
camStockZ: 5,
camTabsDepth: 5,
camTabsHeight: 5,
camTabsMidline: false,
camTabsWidth: 5,
camTolerance: 0,
camToolInit: true,
camTraceDogbone: false,
camTraceDown: 0,
camTraceLines: false,
camTraceMerge: false,
camTraceOffOver: 0,
camTraceOver: 0.5,
camTracePlunge: 200,
camTraceSpeed: 250,
camTraceSpindle: 1000,
camTraceThru: 0,
camTraceTool: 1000,
camTraceType: "follow",
camTraceZBottom: 0,
camTraceZTop: 0,
camTrueShadow: false,
camZAnchor: "middle",
camZBottom: 0,
camZClearance: 1,
camZOffset: 0,
camZThru: 0,
camZTop: 0,
op2: [], // flip ops
ops: [], // current ops
outputInvertX: false, outputInvertX: false,
outputInvertY: false, outputInvertY: false,
camExpertFast: false, processName: "default",
camTrueShadow: false,
camArcEnabled: false,
camArcTolerance: 0.15,
camArcResolution: 5,
camForceZMax: false,
camFirstZMax: false,
camToolInit: true,
camFullEngage: 0.8,
ops: [], // current ops
op2: [] // flip ops
} }
}, },
laser: { laser: {
@ -915,37 +916,37 @@ export const conf = {
devproc: {}, devproc: {},
// application ui and control preferences (Q menu) // application ui and control preferences (Q menu)
controller:{ controller:{
view: null, animesh: "800",
dark: false,
shiny: false,
drawer: false,
scrolls: true,
zoomSpeed: 1.0,
lineType: "path",
autoSave: true,
antiAlias: true, antiAlias: true,
assembly: false,
autoLayout: true,
autoSave: true,
dark: false,
detail: "50",
devel: false,
drawer: false,
edgeangle: 20,
exportOcto: false,
exportPreview: false,
exportThumb: false,
freeLayout: true,
healMesh: false,
lineType: "path",
manifold: false,
ortho: false,
reverseZoom: true, reverseZoom: true,
scrolls: true,
shiny: true,
showOrigin: false, showOrigin: false,
showRulers: true, showRulers: true,
showSpeeds: true, showSpeeds: true,
freeLayout: true,
autoLayout: true,
spaceRandoX: false,
spaceLayout: 1, spaceLayout: 1,
edgeangle: 20, spaceRandoX: false,
units: "mm",
exportOcto: false,
exportGhost: false,
exportLocal: false,
exportThumb: false,
exportPreview: false,
detail: "50",
animesh: "800",
healMesh: false,
threaded: true, threaded: true,
assembly: false, units: "mm",
ortho: false, view: null,
devel: false webGPU: false,
zoomSpeed: 1.0,
}, },
// default hidden ui groups // default hidden ui groups
hidden: { hidden: {

View file

@ -108,7 +108,8 @@ const LISTS = {
regaxis: [ regaxis: [
{ name: "X" }, { name: "X" },
{ name: "Y" }, { name: "Y" },
{ name: "-" } { name: "-" },
{ name: "=" }
], ],
regpoints: [ regpoints: [
{ name: "2" }, { name: "2" },

View file

@ -218,207 +218,6 @@ function exportGCodeDialog(gcode, sections, info, names) {
ajax.send(form); ajax.send(form);
} }
function gridhost_tracker(host,key) {
ajax(host+"/api/check?key="+key, function(data) {
data = js2o(data);
console.log(data);
if (!(data.done || data.error)) {
setTimeout(function() { gridhost_tracker(host,key) }, 1000);
}
});
}
function gridlocal_probe(ev, devs) {
if (ev && ev.code !== 'Enter') return;
if (!devs && api.probe.local(gridlocal_probe)) return;
grid_local = devs;
let gdev = localGet('grid-local');
let gloc = $('grid-local');
let html = [];
for (let uuid in devs) {
gdev = gdev || uuid;
let dev = devs[uuid];
let sel = uuid === gdev ? ' selected' : '';
html.push(`<option id="gl-${uuid}" value="${uuid}" ${sel}>${dev.stat.device.name}</option>`);
}
if (html.length) {
gloc.innerHTML = html.join('\n');
gloc.onchange = (ev) => {
localSet('grid-local', gloc.options[gloc.selectedIndex].value);
};
$('send-to-gridhead').style.display = 'flex';
$('send-to-gridspool').style.display = 'flex';
} else {
$('send-to-gridhead').style.display = '';
$('send-to-gridspool').style.display = '';
}
}
function sendto_gridlocal() {
let uuid = $('grid-local').value;
let dev = grid_local[uuid];
if (dev) {
let file = $('print-filename').value;
fetch(
`${api.probe.live}/api/grid_send?uuid=${uuid}&file=${encodeURIComponent(file + "." + fileext)}`,
{method: "POST", body: gcode}
)
.then(t => t.text())
.then(t => {
api.stats.add(`ua_${api.mode.get_lower()}_print_local_ok`);
console.log({grid_spool_said: t});
})
.catch(e => {
api.stats.add(`ua_${api.mode.get_lower()}_print_local_err`);
console.log({grid_local_spool_error: e});
})
.finally(() => {
api.modal.hide();
});
}
}
function admin_gridlocal() {
let dev = grid_local[$('grid-local').value];
if (dev && dev.stat && dev.stat.device) {
let dsd = dev.stat.device;
window.open(`http://${dsd.addr[0]}:${dsd.port || 4080}`, "_grid_admin");
}
}
function gridhost_probe(ev, set_host) {
if (ev && ev.code !== 'Enter') return;
if (!(grid_host && grid_apik)) return;
if (set_host) grid_host.value = set_host;
let xhtr = new XMLHttpRequest(),
host = grid_host.value,
apik = grid_apik.value,
target = grid_target.value;
if (!apik) $('gpapik').style.display = 'none';
if (!host && api.probe.grid(gridhost_probe)) return;
if (!host) return;
xhtr.onreadystatechange = function() {
if (xhtr.readyState === 4) {
if (xhtr.status >= 200 && xhtr.status < 300) {
localSet('grid-host', host);
localSet('grid-apik', apik);
let res = JSON.parse(xhtr.responseText);
let sel = false;
let match = false;
let first = null;
let html = [];
grid_targets = {};
for (let key in res) {
first = first || key;
if (!localGet('grid-target')) {
localSet('grid-target', key);
sel = true;
} else {
sel = localGet('grid-target') === key;
}
match = match || sel;
grid_targets[html.length] = key;
html.push(
"<option id='gpo-'" + key + " value='" +key + "'" +
(sel ? " selected" : "") +
">" +
(res[key].comment || key) +
"</option>"
);
}
if (!match) {
localSet('grid-target', first);
}
grid_target.innerHTML = html.join('\n');
} else if (xhtr.status === 401) {
$('gpapik').style.display = '';
} else {
local.removeItem('grid-host');
local.removeItem('grid-apik');
console.log("invalid grid:host url");
}
}
};
xhtr.open("GET", host + "/api/active?key=" + apik);
xhtr.send();
}
function sendto_gridhost() {
if (!(grid_host && grid_apik)) return;
let xhtr = new XMLHttpRequest(),
host = grid_host.value,
apik = grid_apik.value,
target = localGet('grid-target') || '';
if (target === '') {
api.show.alert('invalid or missing target');
return;
}
if (host.indexOf("http") !== 0) {
api.show.alert("host missing protocol (http:// or https://)");
return;
}
if (host.indexOf("://") < 0) {
api.show.alert("host:port malformed");
return;
}
if (api.const.SECURE && !api.util.isSecure(host)) {
api.show.alert("host must begin with 'https' on a secure site");
return;
}
localSet('grid-host', host.trim());
localSet('grid-apik', apik.trim());
xhtr.onreadystatechange = function() {
if (xhtr.readyState === 4) {
let status = xhtr.status;
api.stats.add(`ua_${api.mode.get_lower()}_print_grid_${status}`);
if (status >= 200 && status < 300) {
let json = js2o(xhtr.responseText);
gridhost_tracker(host,json.key);
api.ajax(host+"/api/wait?key="+json.key, function(data) {
data = js2o(data);
console.log(data);
api.show.alert("print to "+target+": "+data.status, 600);
});
} else {
api.show.alert("grid:host error\nstatus: "+status+"\nmessage: "+xhtr.responseText, 10000);
}
api.show.progress(0);
}
};
xhtr.upload.addEventListener('progress', function(evt) {
api.show.progress(evt.loaded/evt.total, "sending");
});
filename = $('print-filename').value;
xhtr.open("POST",
host + "/api/print?" +
"filename=" + filename +
"&target=" + target +
"&key=" + apik +
"&time=" + Math.round(info.time) +
"&length=" + Math.round(info.distance) +
"&image=" + filename
);
xhtr.setRequestHeader("Content-Type", "text/plain");
let snapshot = api.view.snapshot;
xhtr.send(snapshot ? [gcode,snapshot].join("\0") : gcode);
api.modal.hide();
}
function download() { function download() {
filename = $('print-filename').value; filename = $('print-filename').value;
api.util.download(gcode, filename + "." + fileext); api.util.download(gcode, filename + "." + fileext);
@ -463,8 +262,6 @@ function exportGCodeDialog(gcode, sections, info, names) {
let set = api.conf.get(); let set = api.conf.get();
let fdm = MODE === MODES.FDM; let fdm = MODE === MODES.FDM;
let octo = set.controller.exportOcto && MODE !== MODES.CAM; let octo = set.controller.exportOcto && MODE !== MODES.CAM;
let ghost = set.controller.exportGhost;
let local = set.controller.exportLocal;
let preview = set.controller.exportPreview; let preview = set.controller.exportPreview;
$('code-preview-head').style.display = preview ? '' : 'none'; $('code-preview-head').style.display = preview ? '' : 'none';
$('code-preview').style.display = preview ? '' : 'none'; $('code-preview').style.display = preview ? '' : 'none';
@ -484,8 +281,6 @@ function exportGCodeDialog(gcode, sections, info, names) {
// persist fields when changed // persist fields when changed
bindField('octo-host', 'octo-host'); bindField('octo-host', 'octo-host');
bindField('octo-apik', 'octo-apik'); bindField('octo-apik', 'octo-apik');
bindField('grid-host', 'grid-host');
bindField('grid-apik', 'grid-apik');
// in cam mode, show zip file option // in cam mode, show zip file option
let downloadZip = $('print-zip'); let downloadZip = $('print-zip');
@ -699,37 +494,11 @@ function exportGCodeDialog(gcode, sections, info, names) {
$('ophost').style.display = 'none'; $('ophost').style.display = 'none';
$('opapik').style.display = 'none'; $('opapik').style.display = 'none';
$('oph1nt').style.display = 'none'; $('oph1nt').style.display = 'none';
$('send-to-gridhost').style.display = 'none';
} }
octo_host.value = localGet('octo-host') || ''; octo_host.value = localGet('octo-host') || '';
octo_apik.value = localGet('octo-apik') || ''; octo_apik.value = localGet('octo-apik') || '';
} catch (e) { console.log(e) } } catch (e) { console.log(e) }
// grid:host setup
$('send-to-hosthead').style.display = ghost ? '' : 'none';
$('send-to-gridhost').style.display = ghost ? '' : 'none';
if (ghost) try {
$('print-gridhost').onclick = sendto_gridhost;
$('grid-host').onkeyup = gridhost_probe;
$('grid-apik').onkeyup = gridhost_probe;
grid_host = $('grid-host');
grid_apik = $('grid-apik');
grid_target = $('grid-target');
grid_target.onchange = function(ev) {
localSet('grid-target', grid_targets[grid_target.selectedIndex]);
};
grid_host.value = localGet('grid-host') || '';
grid_apik.value = localGet('grid-apik') || '';
gridhost_probe();
} catch (e) { console.log(e) }
// grid:local setup
if (local) try {
gridlocal_probe();
$('print-gridlocal').onclick = sendto_gridlocal;
$('admin-gridlocal').onclick = admin_gridlocal;
} catch (e) { console.log(e) }
// preview of the generated GCODE (first 64k max) // preview of the generated GCODE (first 64k max)
if (preview && gcode) $('code-preview-textarea').value = gcode.substring(0,65535); if (preview && gcode) $('code-preview-textarea').value = gcode.substring(0,65535);
} }

View file

@ -142,28 +142,28 @@ function booleanSave() {
if (control.antiAlias != ui.antiAlias.checked) { if (control.antiAlias != ui.antiAlias.checked) {
api.show.alert('Page Reload Required to Change Aliasing'); api.show.alert('Page Reload Required to Change Aliasing');
} }
control.shiny = ui.shiny.checked; control.antiAlias = ui.antiAlias.checked;
control.assembly = ui.assembly.checked;
control.autoLayout = ui.autoLayout.checked;
control.autoSave = ui.autoSave.checked;
control.dark = ui.dark.checked;
control.devel = ui.devel.checked;
control.drawer = ui.drawer.checked; control.drawer = ui.drawer.checked;
control.exportOcto = ui.exportOcto.checked;
control.exportPreview = ui.exportPreview.checked;
control.exportThumb = ui.exportThumb.checked;
control.freeLayout = ui.freeLayout.checked;
control.healMesh = ui.healMesh.checked;
control.ortho = ui.ortho.checked;
control.manifold = ui.manifold.checked;
control.reverseZoom = ui.reverseZoom.checked;
control.scrolls = ui.scrolls.checked; control.scrolls = ui.scrolls.checked;
control.shiny = ui.shiny.checked;
control.showOrigin = ui.showOrigin.checked; control.showOrigin = ui.showOrigin.checked;
control.showRulers = ui.showRulers.checked; control.showRulers = ui.showRulers.checked;
control.autoLayout = ui.autoLayout.checked;
control.freeLayout = ui.freeLayout.checked;
control.spaceRandoX = ui.spaceRandoX.checked; control.spaceRandoX = ui.spaceRandoX.checked;
control.autoSave = ui.autoSave.checked;
control.antiAlias = ui.antiAlias.checked;
control.reverseZoom = ui.reverseZoom.checked;
control.dark = ui.dark.checked;
control.exportOcto = ui.exportOcto.checked;
control.exportGhost = ui.exportGhost.checked;
control.exportLocal = ui.exportLocal.checked;
control.exportThumb = ui.exportThumb.checked;
control.exportPreview = ui.exportPreview.checked;
control.healMesh = ui.healMesh.checked;
control.threaded = setThreaded(ui.threaded.checked); control.threaded = setThreaded(ui.threaded.checked);
control.assembly = ui.assembly.checked; control.webGPU = ui.webGPU.checked;
control.ortho = ui.ortho.checked;
control.devel = ui.devel.checked;
space.view.setZoom(control.reverseZoom, control.zoomSpeed); space.view.setZoom(control.reverseZoom, control.zoomSpeed);
// platform.layout(); // platform.layout();
api.conf.save(); api.conf.save();
@ -904,7 +904,6 @@ function init_one() {
xany: $('mod-x-any'), xany: $('mod-x-any'),
xsla: $('mod-x-sla'), xsla: $('mod-x-sla'),
xlaser: $('mod-x-laser'), xlaser: $('mod-x-laser'),
local: $('mod-local'),
don8: $('mod-don8'), don8: $('mod-don8'),
any: $('mod-any'), any: $('mod-any'),
}, },
@ -1048,6 +1047,7 @@ function init_one() {
showSpeeds: newBoolean(LANG.op_sped_s, speedSave, {title:LANG.op_sped_l}), showSpeeds: newBoolean(LANG.op_sped_s, speedSave, {title:LANG.op_sped_l}),
shiny: newBoolean(LANG.op_shny_s, booleanSave, {title:LANG.op_shny_l, modes:FDM}), shiny: newBoolean(LANG.op_shny_s, booleanSave, {title:LANG.op_shny_l, modes:FDM}),
lineType: newSelect(LANG.op_line_s, {title: LANG.op_line_l, action: lineTypeSave, modes:FDM}, "linetype"), lineType: newSelect(LANG.op_line_s, {title: LANG.op_line_l, action: lineTypeSave, modes:FDM}, "linetype"),
manifold: newBoolean(LANG.op_mani_s, booleanSave, {title: LANG.op_mani_l, modes:CAM}, "manifold"),
animesh: newSelect(LANG.op_anim_s, {title: LANG.op_anim_l, action: aniMeshSave, modes:CAM}, "animesh"), animesh: newSelect(LANG.op_anim_s, {title: LANG.op_anim_l, action: aniMeshSave, modes:CAM}, "animesh"),
units: newSelect(LANG.op_unit_s, {title: LANG.op_unit_l, action: unitsSave, modes:CAM}, "units"), units: newSelect(LANG.op_unit_s, {title: LANG.op_unit_l, action: unitsSave, modes:CAM}, "units"),
edgeangle: newInput(LANG.op_spoa_s, {title:LANG.op_spoa_l, convert:toFloat, size:3}), edgeangle: newInput(LANG.op_spoa_s, {title:LANG.op_spoa_l, convert:toFloat, size:3}),
@ -1058,8 +1058,6 @@ function init_one() {
spaceRandoX: newBoolean(LANG.op_spcx_s, booleanSave, {title:LANG.op_spcx_l, show:isBelt}), spaceRandoX: newBoolean(LANG.op_spcx_s, booleanSave, {title:LANG.op_spcx_l, show:isBelt}),
spaceLayout: newInput(LANG.op_spcr_s, {title:LANG.op_spcr_l, convert:toFloat, size:3, units}), spaceLayout: newInput(LANG.op_spcr_s, {title:LANG.op_spcr_l, convert:toFloat, size:3, units}),
_____: newGroup(LANG.xp_menu, $('prefs-xpo'), {inline: true}), _____: newGroup(LANG.xp_menu, $('prefs-xpo'), {inline: true}),
exportLocal: newBoolean(`Grid:Local`, booleanSave, {title:LANG.op_exgl_l}),
exportGhost: newBoolean(`Grid:Host`, booleanSave, {title:LANG.op_exgh_l}),
exportOcto: newBoolean(`OctoPrint`, booleanSave, {title:LANG.op_exop_l}), exportOcto: newBoolean(`OctoPrint`, booleanSave, {title:LANG.op_exop_l}),
exportThumb: newBoolean(`Thumbnail`, booleanSave, {modes:FDM}), exportThumb: newBoolean(`Thumbnail`, booleanSave, {modes:FDM}),
exportPreview: newBoolean(`Code Preview`, booleanSave), exportPreview: newBoolean(`Code Preview`, booleanSave),
@ -1068,6 +1066,7 @@ function init_one() {
healMesh: newBoolean(LANG.pt_heal_s, booleanSave, {title: LANG.pt_heal_l}), healMesh: newBoolean(LANG.pt_heal_s, booleanSave, {title: LANG.pt_heal_l}),
threaded: newBoolean(LANG.pt_thrd_s, booleanSave, {title: LANG.pt_thrd_l, modes:THREED}), threaded: newBoolean(LANG.pt_thrd_s, booleanSave, {title: LANG.pt_thrd_l, modes:THREED}),
assembly: newBoolean(LANG.pt_assy_s, booleanSave, {title: LANG.pt_assy_l, modes:THREED}), assembly: newBoolean(LANG.pt_assy_s, booleanSave, {title: LANG.pt_assy_l, modes:THREED}),
webGPU: newBoolean(LANG.pt_wgpu_s, booleanSave, {title: LANG.pt_wgpu_l, modes:THREED}),
prefadd: uc.checkpoint($('prefs-add')), prefadd: uc.checkpoint($('prefs-add')),
@ -1635,20 +1634,23 @@ function init_two() {
space.platform.setZOff(0.05); space.platform.setZOff(0.05);
// restore UI state from settings // restore UI state from settings
ui.antiAlias.checked = control.antiAlias;
ui.assembly.checked = control.assembly;
ui.autoLayout.checked = control.autoLayout;
ui.autoSave.checked = control.autoSave;
ui.devel.checked = control.devel;
ui.freeLayout.checked = control.freeLayout;
ui.healMesh.checked = control.healMesh;
ui.manifold.checked = control.manifold;
ui.ortho.checked = control.ortho;
ui.reverseZoom.checked = control.reverseZoom;
ui.showOrigin.checked = control.showOrigin; ui.showOrigin.checked = control.showOrigin;
ui.showRulers.checked = control.showRulers; ui.showRulers.checked = control.showRulers;
ui.showSpeeds.checked = control.showSpeeds; ui.showSpeeds.checked = control.showSpeeds;
ui.freeLayout.checked = control.freeLayout;
ui.autoLayout.checked = control.autoLayout;
ui.spaceRandoX.checked = control.spaceRandoX; ui.spaceRandoX.checked = control.spaceRandoX;
ui.antiAlias.checked = control.antiAlias;
ui.reverseZoom.checked = control.reverseZoom;
ui.autoSave.checked = control.autoSave;
ui.healMesh.checked = control.healMesh;
ui.threaded.checked = setThreaded(control.threaded); ui.threaded.checked = setThreaded(control.threaded);
ui.assembly.checked = control.assembly; ui.webGPU.checked = control.webGPU;
ui.ortho.checked = control.ortho;
ui.devel.checked = control.devel;
lineTypeSave(); lineTypeSave();
detailSave(); detailSave();
updateStats(); updateStats();
@ -1789,6 +1791,7 @@ function init_two() {
$('context-duplicate').onclick = duplicateSelection; $('context-duplicate').onclick = duplicateSelection;
$('context-mirror').onclick = mirrorSelection; $('context-mirror').onclick = mirrorSelection;
$('context-layflat').onclick = () => { api.event.emit("tool.mesh.lay-flat") }; $('context-layflat').onclick = () => { api.event.emit("tool.mesh.lay-flat") };
$('context-lefty').onclick = () => { api.event.emit("tool.mesh.lefty") };
$('context-setfocus').onclick = () => { $('context-setfocus').onclick = () => {
api.event.emit( api.event.emit(
"tool.camera.focus", "tool.camera.focus",

View file

@ -114,12 +114,6 @@ export const mode = {
} }
}; };
export const probe = {
live: "https://live.grid.space",
grid: noop,
local: noop
};
export const process = { export const process = {
code: currentProcessCode, code: currentProcessCode,
get: currentProcessName get: currentProcessName
@ -784,43 +778,7 @@ function showHelpFile(local,then) {
} }
function showLocal() { function showLocal() {
showModal('local'); // deprecated
api.probe.local((err,data) => {
let devc = 0;
let bind = [];
let html = ['<table>'];
html.push(`<thead><tr><th>device</th><th>type</th><th>status</th><th></th></tr></thead>`);
html.push(`<tbody>`);
let recs = [];
for (let k in data) {
recs.push(data[k].stat);
}
recs.sort((a,b) => {
return a.device.name < b.device.name ? -1 : 1;
});
for (let r of recs) {
bind.push({uuid: r.device.uuid, host: r.device.addr[0], port: r.device.port});
html.push(`<tr>`);
html.push(`<td>${r.device.name}</td>`);
html.push(`<td>${r.device.mode}</td>`);
html.push(`<td>${r.state}</td>`);
html.push(`<td><button id="${r.device.uuid}">admin</button></td>`);
html.push(`</tr>`);
devc++;
}
html.push(`</tbody>`);
html.push(`</table>`);
if (devc) {
$('mod-local').innerHTML = html.join('');
} else {
$('mod-local').innerHTML = `<br><b>no local devices</b>`;
}
bind.forEach(rec => {
$(rec.uuid).onclick = () => {
window.open(`http://${rec.host}:${rec.port||4080}/`);
};
});
});
} }
function setViewMode(mode) { function setViewMode(mode) {

View file

@ -163,19 +163,19 @@ function platformUpdateTopZ() {
const wzmax = bounds.max.z; const wzmax = bounds.max.z;
const zdelta = process.camZOffset || 0; const zdelta = process.camZOffset || 0;
switch (process.camZAnchor) { switch (process.camZAnchor) {
case 'top': case 'top':
widget.setTopZ(stock.z - zdelta); widget.setTopZ(stock.z - zdelta, true);
break; break;
case 'middle': case 'middle':
if (widget.isIndexed) { if (widget.isIndexed) {
widget.setTopZ(zdelta); widget.setTopZ(zdelta, true);
} else { } else {
widget.setTopZ(stock.z - (stock.z - wzmax) / 2 + zdelta); widget.setTopZ(stock.z - (stock.z - wzmax) / 2 + zdelta, true);
} }
break; break;
case 'bottom': case 'bottom':
widget.setTopZ(wzmax + zdelta); widget.setTopZ(wzmax + zdelta, true);
break; break;
} }
} else { } else {
widget.setTopZ(0); widget.setTopZ(0);

View file

@ -56,8 +56,8 @@ class Print {
// drop duplicates (usually intruced by FDM bisections) // drop duplicates (usually intruced by FDM bisections)
if (lastPoint && point && !arc && type !== 'lerp') { if (lastPoint && point && !arc && type !== 'lerp') {
// nested due to uglify confusing browser // nested due to uglify confusing browser
const { x, y, z } = lastPoint; const { x, y, z, a } = lastPoint;
if (point.x == x && point.y == y && point.z == z && lastEmit == emit) { if (point.x == x && point.y == y && point.z == z && point.a == z && lastEmit == emit) {
return lastOut; return lastOut;
} }
} }
@ -129,6 +129,7 @@ class Print {
close = !options.open, close = !options.open,
scarf = !poly.open ? (options.scarf ?? 0) : false, scarf = !poly.open ? (options.scarf ?? 0) : false,
tool = options.tool, tool = options.tool,
zmax = options.zmax,
last = startPoint, last = startPoint,
first = true; first = true;

View file

@ -189,7 +189,7 @@ function rotate(x, y, z) {
space.update(); space.update();
} }
function merge() { function merge({ deleteMerged = true }) {
let sel = widgets(); let sel = widgets();
if (sel.length === 0) { if (sel.length === 0) {
sel = api.widgets.all(); sel = api.widgets.all();
@ -207,7 +207,7 @@ function merge() {
} }
}); });
let w = api.platform.load_verts([], obj, "merged"); let w = api.platform.load_verts([], obj, "merged");
api.platform.delete(sel); if (deleteMerged) api.platform.delete(sel);
api.platform.select(w); api.platform.select(w);
} }
@ -234,7 +234,7 @@ function isolateBodies(){
if(bodies.length ==1){ if(bodies.length ==1){
api.platform.select(widget); api.platform.select(widget);
}else{ }else{
console.log(bodies,Array.from(bodies.entries())) // console.log(bodies,Array.from(bodies.entries()))
for(let [i,verts] of bodies.entries()){ for(let [i,verts] of bodies.entries()){
let w = api.platform.load_verts([], verts, `isolate ${i} of ${ widget.anno.file}`); let w = api.platform.load_verts([], verts, `isolate ${i} of ${ widget.anno.file}`);
api.platform.select(w) api.platform.select(w)

View file

@ -49,6 +49,24 @@ function onLayFlatSelect() {
// endIt(); // endIt();
} }
function onLeftySelect() {
// Project the face normal onto the XY plane (ignore Z component)
let projectedNormal = new Vector3(lastface.normal.x, lastface.normal.y, 0).normalize();
// Target direction on XY plane (negative Y axis)
let targetDir = new Vector3(0, -1, 0);
// Calculate angle between projected normal and target
let angle = Math.atan2(
projectedNormal.x * targetDir.y - projectedNormal.y * targetDir.x, // cross product Z component
projectedNormal.x * targetDir.x + projectedNormal.y * targetDir.y // dot product
);
// Create quaternion for Z-axis rotation only
let q = new Quaternion().setFromAxisAngle(ZAXIS, angle);
api.selection.rotate(q);
}
function onFaceUpSelect() { function onFaceUpSelect() {
api.event.emit('tool.mesh.face-normal', lastface.normal); api.event.emit('tool.mesh.face-normal', lastface.normal);
// endIt(); // endIt();
@ -105,6 +123,12 @@ api.event.on('tool.mesh.lay-flat', () => {
startIt(); startIt();
}); });
api.event.on('tool.mesh.lefty', () => {
opName = 'align y axis';
onDone = onLeftySelect;
startIt();
});
function scale() { function scale() {
let cam = space.internals().camera; let cam = space.internals().camera;
let dist = cam.position.distanceTo(pmesh.position); let dist = cam.position.distanceTo(pmesh.position);

View file

@ -473,10 +473,12 @@ class Widget {
/** /**
* moves top of widget to given Z * moves top of widget to given Z
* only non-zero in CAM mode
* *
* @param {number} z position * @param {number} z position
* @param {boolean} cam mode
*/ */
setTopZ(z) { setTopZ(z, cam) {
let mesh = this.mesh, let mesh = this.mesh,
track = this.track, track = this.track,
mbb = mesh.getBoundingBox(), mbb = mesh.getBoundingBox(),
@ -488,7 +490,7 @@ class Widget {
track.top = mbz; track.top = mbz;
} }
// difference between top of stock/bounds and top of widget (cam mode) // difference between top of stock/bounds and top of widget (cam mode)
track.tzoff = mbz - z; track.tzoff = cam ? mbz - z : 0;
this._updateMeshPosition(); this._updateMeshPosition();
} }
@ -503,8 +505,7 @@ class Widget {
} }
_move(x, y, z, abs) { _move(x, y, z, abs) {
let mesh = this.mesh, let mat = this.getMaterial(),
mat = this.getMaterial(),
pos = this.track.pos; pos = this.track.pos;
// do not allow moves in pure slice view // do not allow moves in pure slice view
if (!mat.visible) return; if (!mat.visible) return;
@ -528,13 +529,10 @@ class Widget {
track = this.track, track = this.track,
tzoff = track.tzoff, tzoff = track.tzoff,
top = track.top, top = track.top,
tz = tzoff !== top ? -tzoff : 0, tz = -tzoff,
{ x, y, z } = track.pos; { x, y, z } = track.pos;
if (track.indexed) { if (track.indexed) {
let rad = track.indexRad; let rad = track.indexRad;
let ln = track.box.d / 2;
let dx = Math.sin(rad) * ln;
let dy = ln - Math.cos(rad) * ln;
this.mesh.rotation.x = -rad; this.mesh.rotation.x = -rad;
z = top; z = top;
} else { } else {
@ -782,9 +780,11 @@ class Widget {
} }
if (set) { if (set) {
let dark = this.api.space.is_dark(); let dark = this.api.space.is_dark();
let cam = this.api.mode.is_cam();
let color = dark && !cam ? 0xffffff : 0;
let angle = this.api.conf.get().controller.edgeangle || 20; let angle = this.api.conf.get().controller.edgeangle || 20;
let edges = new THREE.EdgesGeometry(mesh.geometry, angle); let edges = new THREE.EdgesGeometry(mesh.geometry, angle);
let material = new THREE.LineBasicMaterial({ color: 0 }); let material = new THREE.LineBasicMaterial({ color });
this.outline = new THREE.LineSegments(edges, material); this.outline = new THREE.LineSegments(edges, material);
this.outline.renderOrder = -20; this.outline.renderOrder = -20;
mesh.add(this.outline); mesh.add(this.outline);

View file

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

View file

@ -10,29 +10,35 @@ const { client } = api;
let meshes = {}, let meshes = {},
button = {}, button = {},
label = {}, label = {},
lineTracker,
material, material,
speedPauses = [0, 0, 0, 0, 0],
speedValues = [1, 2, 4, 8, 32], speedValues = [1, 2, 4, 8, 32],
speedNames = ["1x", "2x", "4x", "8x", "!!"], speedNames = ["1x", "2x", "4x", "8x", "!!"],
speedMax = speedValues.length - 1, speedMax = speedValues.length - 1,
speedIndex = 0, speedIndex = 0,
speed, speed,
origin, origin,
color = 0; color = 0,
dark = false,
manifold = false,
A2R = Math.PI / 180;
export function animate_clear2(api) { export function animate_clear2(api) {
let { anim } = api.ui; let { anim } = api.ui;
client.animate_cleanup2(); lineTracker?.clear();
Object.keys(meshes).forEach(id => deleteMesh(id)); Object.keys(meshes).forEach(id => deleteMesh(id));
api.widgets.setAxisIndex(0); api.widgets.setAxisIndex(0);
api.uc.setVisible(anim.laba, true); api.uc.setVisible(anim.laba, true);
api.uc.setVisible(anim.vala, true); api.uc.setVisible(anim.vala, true);
anim.vala.value = "0.0"; anim.vala.value = "0.0";
client.animate_cleanup2();
} }
export function animate2(api, delay) { export function animate2(api, delay) {
let alert = api.alerts.show("building animation"); let alert = api.alerts.show("building animation");
let settings = api.conf.get(); let settings = api.conf.get();
dark = settings.controller.dark;
manifold = settings.controller.manifold;
client.animate_setup2(settings, data => { client.animate_setup2(settings, data => {
handleUpdate(data); handleUpdate(data);
if (data) { if (data) {
@ -48,7 +54,8 @@ export function animate2(api, delay) {
speed: anim.speed, speed: anim.speed,
trans: anim.trans, trans: anim.trans,
model: anim.model, model: anim.model,
shade: anim.shade shade: anim.shade,
path: anim.path
}); });
Object.assign(label, { Object.assign(label, {
progress: anim.progress, progress: anim.progress,
@ -59,6 +66,14 @@ export function animate2(api, delay) {
a: anim.vala 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); updateSpeed(0);
setTimeout(step, delay || 0); setTimeout(step, delay || 0);
toggleTrans(undefined, false); toggleTrans(undefined, false);
@ -72,6 +87,7 @@ export function animate2(api, delay) {
button.trans.onclick = toggleTrans; button.trans.onclick = toggleTrans;
button.model.onclick = toggleModel; button.model.onclick = toggleModel;
button.shade.onclick = toggleStock; button.shade.onclick = toggleStock;
button.path.onclick = togglePath;
button.play.style.display = ''; button.play.style.display = '';
button.pause.style.display = 'none'; button.pause.style.display = 'none';
@ -86,8 +102,9 @@ Object.assign(client, {
}, },
animate_setup2(settings, ondone) { animate_setup2(settings, ondone) {
color = settings.controller.dark ? 0x888888 : 0; initPathMesh();
material = new THREE.MeshPhongMaterial({ color = dark ? 0x888888 : 0;
material = material ?? new THREE.MeshPhongMaterial({
flatShading: true, flatShading: true,
transparent: false, transparent: false,
opacity: 0.5, 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) { function meshAdd(id, ind, pos, ilen, plen) {
const geo = new THREE.BufferGeometry(); const geo = new THREE.BufferGeometry();
const pa = plen ? pos.subarray(0, plen * 3) : pos; const pa = plen ? pos.subarray(0, plen * 3) : pos;
@ -132,6 +216,7 @@ function meshUpdate(id, ind, pos, ilen, plen) {
function deleteMesh(id) { function deleteMesh(id) {
space.world.remove(meshes[id]); space.world.remove(meshes[id]);
meshes[id].geometry.dispose();
delete meshes[id]; delete meshes[id];
} }
@ -141,8 +226,15 @@ function toggleModel(ev, bool) {
} }
function toggleStock(ev, bool, set) { function toggleStock(ev, bool, set) {
set !== false && api.local.toggle('cam.anim.stock', bool); bool = api.local.toggle('cam.anim.stock', bool);
return api.event.emit('cam.stock.toggle', bool ?? undefined); 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) { function toggleTrans(ev, bool) {
@ -165,8 +257,7 @@ function play(opts) {
} }
client.animate2({ client.animate2({
speed, speed,
steps: steps || Infinity, steps: steps || Infinity
pause: speedPauses[speedIndex]
}, handleUpdate); }, handleUpdate);
} }
@ -177,8 +268,7 @@ function fast(opts) {
button.pause.style.display = ''; button.pause.style.display = '';
client.animate2({ client.animate2({
speed, speed,
steps: steps || Infinity, steps: steps || Infinity
pause: speedPauses[speedIndex]
}, handleUpdate); }, handleUpdate);
} }
@ -216,6 +306,7 @@ function handleUpdate(data) {
if (data.stock_index !== undefined) { if (data.stock_index !== undefined) {
api.widgets.setAxisIndex(data.stock_index); api.widgets.setAxisIndex(data.stock_index);
label.a.value = -data.stock_index.toFixed(1); label.a.value = -data.stock_index.toFixed(1);
lineTracker.rotate(-data.stock_index);
} }
if (data.mesh_index) { if (data.mesh_index) {
const { id, index } = 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; const { id, ind, pos, ilen, plen } = data.mesh_update;
meshUpdate(id, ind, pos, ilen, plen); meshUpdate(id, ind, pos, ilen, plen);
} }
if (data && data.progress) { if (data.progress) {
label.progress.value = (data.progress * 100).toFixed(1); label.progress.value = (data.progress * 100).toFixed(1);
} }
if (data.line) {
lineTracker.add(...data.line);
}
} }
function updateSpeed(inc = 0) { function updateSpeed(inc = 0) {

View file

@ -9,6 +9,7 @@ import { selectHoles } from './cl-hole.js';
import { selectHelical } from './cl-helical.js'; import { selectHelical } from './cl-helical.js';
import { surfaceAdd } from './cl-surface.js'; import { surfaceAdd } from './cl-surface.js';
import { traceAdd } from './cl-trace.js'; import { traceAdd } from './cl-trace.js';
import { opRender } from './client.js';
const { MODES } = api.const; const { MODES } = api.const;
const { uc: UC } = api; const { uc: UC } = api;
@ -206,7 +207,7 @@ export function createPopOps() {
sep: UC.newBlank({ class: "pop-sep" }), sep: UC.newBlank({ class: "pop-sep" }),
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: UC.toInt, show: hasSpindle }), 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) }), 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 }), 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 }), 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 }), 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 }), 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 }), 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 }), 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" }), sep: UC.newBlank({ class: "pop-sep" }),
top: UC.newBoolean(LANG.co_clrt_s, undefined, { title: LANG.co_clrt_l }), 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 } }), 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', tolerance: 'camTolerance',
filter: 'camContourFilter', filter: 'camContourFilter',
leave: 'camContourLeave', leave: 'camContourLeave',
linear: 'camLatheLinear' linear: 'camLatheLinear',
offStart: 'camLatheOffStart',
offEnd: 'camLatheOffEnd'
}).inputs = { }).inputs = {
tool: UC.newSelect(LANG.cc_tool, {}, "tools"), tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
// axis: UC.newSelect(LANG.cd_axis, {}, "xyaxis"), // 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) }), 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) }), 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" }), 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 }), 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) }), 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" }), sep: UC.newBlank({ class: "pop-sep" }),

View file

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

View file

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

View file

@ -10,7 +10,7 @@ import { Layers } from '../../core/layers.js';
import { Stack } from '../../core/stack.js'; import { Stack } from '../../core/stack.js';
import { updateStock } from './cl-stock.js'; import { updateStock } from './cl-stock.js';
import { createPopOps } from './cl-ops.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 { traceOn, traceDone, unselectTraces } from './cl-trace.js';
import { holeSelOn, selectHolesDone, clearHolesRec } from './cl-hole.js'; import { holeSelOn, selectHolesDone, clearHolesRec } from './cl-hole.js';
import { surfaceOn, surfaceDone } from './cl-surface.js'; import { surfaceOn, surfaceDone } from './cl-surface.js';
@ -29,6 +29,7 @@ const { widgets: WIDGETS } = api;
class Client { class Client {
animVer = 0; animVer = 0;
camStock; camStock;
showStock = true;
current; current;
currentIndex; currentIndex;
flipping; flipping;
@ -320,6 +321,7 @@ export function opRender() {
el.classList.add("disabled"); el.classList.add("disabled");
} }
bounds.push(el); bounds.push(el);
let parent = $('camops');
let timer = null; let timer = null;
let inside = true; let inside = true;
let popped = false; let popped = false;
@ -329,14 +331,14 @@ export function opRender() {
} }
el.rec = rec; el.rec = rec;
el.unpop = () => { el.unpop = () => {
let pos = [...el.childNodes].indexOf(poprec.div); let pos = [...parent.childNodes].indexOf(poprec.div);
if (pos >= 0) { if (pos >= 0) {
el.removeChild(poprec.div); parent.removeChild(poprec.div);
} }
popped = false; popped = false;
}; };
function onEnter(ev) { function onEnter(ev) {
if ((surfaceOn || traceOn) && env.poppedRec != rec) { if ((clock || surfaceOn || traceOn) && env.poppedRec != rec) {
return; return;
} }
if (popped && env.poppedRec != rec) { if (popped && env.poppedRec != rec) {
@ -351,22 +353,24 @@ export function opRender() {
popped = true; popped = true;
poprec.use(rec); poprec.use(rec);
env.hoveredOp = el; env.hoveredOp = el;
if (!clock) { // offset Y position of pop div by % of Y screen location of button
// offset Y position of pop div by % of Y screen location of button parent.appendChild(poprec.div);
el.appendChild(poprec.div); poprec.addNote();
poprec.addNote(); const { innerHeight } = window;
const { innerHeight } = window; const brect = ev.target.getBoundingClientRect();
const brect = ev.target.getBoundingClientRect(); const prect = parent.getBoundingClientRect();
const prect = poprec.div.getBoundingClientRect(); const Prect = poprec.div.getBoundingClientRect();
const pcty = (brect.top / innerHeight) * 0.9; const tdiff = prect.top - brect.top;
const offpx = -pcty * prect.height; const botoff = innerHeight - (brect.top + Prect.height);
poprec.div.style.transform = `translateY(${offpx}px)`; 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 // option click event appears latent
// and overides the sticky settings // and overides the sticky settings
setTimeout(() => { setTimeout(() => {
UC.setSticky(false); UC.setSticky(false);
}, 0); }, 10);
} }
function onLeave(ev) { function onLeave(ev) {
inside = false; inside = false;
@ -637,7 +641,8 @@ export function init() {
}); });
api.event.on("cam.stock.toggle", (bool) => { 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); 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]; const widget = print.widgets[0];
if (!widget) return; if (!widget) return;
console.log({ cam_export: print });
const { settings } = print; const { settings } = print;
const { device, tools } = settings; const { device } = settings;
let i, let i,
time = 0, time = 0,
@ -257,17 +255,18 @@ export function cam_export(print, online) {
points--; points--;
return; return;
} }
let gn;
if (out.emit >= 0 && out.emit <= 3) gn = `G${out.emit}`; let dx = opt.dx || newpos.x - pos.x,
let speed = out.speed,
arc = out.emit == 2 || out.emit == 3,
center = out.center,
nl = (compact_output && lastGn === gn) ? [] : [gn],
dx = opt.dx || newpos.x - pos.x,
dy = opt.dy || newpos.y - pos.y, dy = opt.dy || newpos.y - pos.y,
dz = opt.dz || newpos.z - pos.z, dz = opt.dz || newpos.z - pos.z,
da = newpos.a != pos.a, 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), feed = Math.min(speed || maxf, maxf),
dist = Math.sqrt(dx * dx + dy * dy + dz * dz), dist = Math.sqrt(dx * dx + dy * dy + dz * dz),
newFeed = feed && feed !== pos.f; newFeed = feed && feed !== pos.f;
@ -424,7 +423,9 @@ export function cam_export(print, online) {
toolz[out.tool] = ctool; toolz[out.tool] = ctool;
} }
point = out.point; point = out.point;
if (!point || point.mod) return; if (!point || point.mod) {
return;
}
// ensure not point is modified twice // ensure not point is modified twice
point.mod = 1; point.mod = 1;
if (isOffset) { 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.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.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.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 }), 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}), 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)}), 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}), camOriginTop: newBoolean(LANG.or_topp_s, onBooleanClick, {title:LANG.or_topp_l}),
camOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, {title:LANG.or_cntr_l}), camOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, {title:LANG.or_cntr_l}),
separator: newBlank({ class:"set-sep", driven }), separator: newBlank({ class:"set-sep", driven }),

View file

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

View file

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

View file

@ -25,7 +25,7 @@ export class OpIndex extends CamOp {
} }
prepare(ops, progress) { prepare(ops, progress) {
const { lastPoint, zmax, zclear, camOut, stock } = ops; const { lastPoint, zmax, zclear, camOut, stock, newLayer } = ops;
let last = lastPoint(); let last = lastPoint();
if (last) { if (last) {
// max point of stock corner radius when rotating (safe z when indexing) // 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 // move above rotating stock
camOut(last = last.clone().setZ(zmove), 0); camOut(last = last.clone().setZ(zmove), 0);
// issue rotation command // 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'; import { newPoint } from '../../../geo/point.js';
function createFilter(op) { function createFilter(op) {
let ok = () => true;
let filter = slices => slices; let filter = slices => slices;
if (op.filter) { let filterString = op.filter?.map(l => l.trim()).join('\n');
if (filterString) {
try { try {
const obj = eval(`( ${op.filter.join('\n')} )`); const obj = eval(`( ${filterString} )`);
let idx = 0; const accept = [];
if (obj && obj.slices) { let index = 0;
const nadd = []; let sl_fn = obj?.slices ?? ok;
filter = function (slices) { filter = function (slices) {
for (let slice of slices) { for (let slice of slices) {
if (obj.slices(slice, idx++)) { if (sl_fn(slice, index++)) {
nadd.push(slice); accept.push(slice);
}
} }
return nadd; }
}; return accept;
} };
} catch (e) { } catch (e) {
console.log('filter parse error', e, op.filter); console.log('filter parse error', e, op.filter);
} }
@ -36,9 +37,8 @@ class OpLathe extends CamOp {
async slice(progress) { async slice(progress) {
let { op, state } = this; let { op, state } = this;
let { addSlices } = state; let { addSlices, color } = state;
let filter = createFilter(op); let filter = createFilter(op);
this.topo = await topo4_generate({ this.topo = await topo4_generate({
op, op,
state, state,

View file

@ -28,10 +28,10 @@ class OpLevel extends CamOp {
let points = []; let points = [];
let clear = op.stock ? 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.outer(POLY.offset(tshadow, toolDiam * (op.inset || 0)));
POLY.fillArea(clear, 90, stepOver, points); POLY.fillArea(clear, 1090, stepOver, points);
let layers = this.layers = []; let layers = this.layers = [];
for (let z of zList) { for (let z of zList) {

View file

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

View file

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

View file

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

View file

@ -15,11 +15,12 @@ class OpRough extends CamOp {
async slice(progress) { async slice(progress) {
let { op, state } = this; 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 { updateToolDiams, thruHoles, tabs, cutTabs, cutPolys } = state;
let { ztOff, zMax, shadowAt, isIndexed} = state; let { ztOff, zMax, shadowAt, isIndexed} = state;
let { workarea } = 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) { if (op.down <= 0) {
throw `invalid step down "${op.down}"`; throw `invalid step down "${op.down}"`;
@ -36,6 +37,12 @@ class OpRough extends CamOp {
updateToolDiams(toolDiam); 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 // clear the stock above the area to be roughed out
if (workarea.top_z > workarea.top_part && !cutThruBypass) { if (workarea.top_z > workarea.top_part && !cutThruBypass) {
let shadow = state.shadow.base.clone(); let shadow = state.shadow.base.clone();
@ -58,14 +65,11 @@ class OpRough extends CamOp {
slice.z = z; slice.z = z;
let polys = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ); let polys = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ);
if (tabs) { if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
polys = cutTabs(tabs, polys, slice.z); polys = cutTabs(tabs, polys, slice.z);
} }
slice.camLines = polys; slice.camLines = polys;
slice.output() slice.output()
.setLayer("roughing", {face: color, line: color}) .setLayer(state.layername, {face: color, line: color})
.addPolys(slice.camLines); .addPolys(slice.camLines);
addSlices(slice); addSlices(slice);
camFaces.push(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 // elimate double inset on inners
offset.forEach(op => { offset.forEach(op => {
if (op.inner) { 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; slice.camLines = offset;
if (roughLeaveZ) { if (roughLeaveZ) {
// offset roughing in Z as well to minimize // offset roughing in Z as well to minimize tool marks on curved surfaces
// tool marks on curved surfaces
// const roughLeaveZ = 1 * Math.min(roughDown, roughLeave / 2); // const roughLeaveZ = 1 * Math.min(roughDown, roughLeave / 2);
slice.camLines.forEach(p => { for (let poly of slice.camLines) {
p.setZ(p.getZ() + roughLeaveZ); 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)); progress(0.5 + 0.5 * (index / slices.length));
}); });
@ -278,8 +282,11 @@ class OpRough extends CamOp {
} }
slices.forEach(slice => { slices.forEach(slice => {
if (slice.camLines && tabs) {
slice.camLines = cutTabs(tabs, slice.camLines);
}
slice.output() slice.output()
.setLayer("roughing", {face: color, line: color}) .setLayer(state.layername, {face: color, line: color})
.addPolys(slice.camLines); .addPolys(slice.camLines);
}); });
this.sliceOut = slices.filter(slice => slice.camLines); this.sliceOut = slices.filter(slice => slice.camLines);
@ -289,15 +296,13 @@ class OpRough extends CamOp {
prepare(ops, progress) { prepare(ops, progress) {
let { op, state, sliceOut, camFaces } = this; let { op, state, sliceOut, camFaces } = this;
let { setTool, setSpindle, setPrintPoint, sliceOutput, polyEmit } = ops; let { setTool, setSpindle, setPrintPoint, pocket, polyEmit, newLayer, printPoint } = ops;
let { camOut, newLayer, printPoint } = ops;
let { settings } = state; let { settings } = state;
let { process } = settings; let { process } = settings;
let easeDown = process.camEaseDown; let easeDown = process.camEaseDown;
let cutdir = op.ov_conv; let cutdir = op.ov_conv;
let depthFirst = process.camDepthFirst && !state.isIndexed; let depthFirst = process.camDepthFirst && !state.isIndexed;
let depthData = [];
setTool(op.tool, op.rate, op.plunge); setTool(op.tool, op.rate, op.plunge);
setSpindle(op.spindle); setSpindle(op.spindle);
@ -325,11 +330,12 @@ class OpRough extends CamOp {
// output the roughing passes // output the roughing passes
setPrintPoint(printPoint); setPrintPoint(printPoint);
sliceOutput(sliceOut, { pocket({
cutdir, cutdir,
depthFirst, depthFirst,
easeDown: op.down && easeDown ? 0.001 : 0, 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); POLY.setWinding(slice.camLines, offset === "outside" ? !cutdir : cutdir, false);
slice.output() slice.output()
.setLayer("trace follow", {line: color}, false) .setLayer(state.layername, {line: color}, false)
.addPolys(slice.camLines) .addPolys(slice.camLines)
} }
function clearZnew(polys, z, down) { function clearZnew(polys, z, down) {
@ -154,7 +154,7 @@ class OpTrace extends CamOp {
.setLayer("trace poly", {line: 0x1188ff}, false) .setLayer("trace poly", {line: 0x1188ff}, false)
.addPolys([ poly ]) .addPolys([ poly ])
slice.output() slice.output()
.setLayer("trace", {line: color}, false) .setLayer(state.layername, {line: color}, false)
.addPolys(slice.camLines) .addPolys(slice.camLines)
progress(zpro, "trace"); progress(zpro, "trace");
zpro += zinc; zpro += zinc;
@ -295,7 +295,6 @@ class OpTrace extends CamOp {
prepare(ops, progress) { prepare(ops, progress) {
let { op, state } = this; let { op, state } = this;
let { settings } = state;
let { setTool, setSpindle } = ops; let { setTool, setSpindle } = ops;
setTool(op.tool, op.rate); 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; if (widget.camops.length === 0 || widget.meta.disabled) return;
let device = settings.device, let { device, process } = settings,
process = settings.process,
isIndexed = process.camStockIndexed, isIndexed = process.camStockIndexed,
startCenter = process.camOriginCenter, startCenter = process.camOriginCenter,
alignTop = settings.controller.alignTop, alignTop = settings.controller.alignTop,
@ -65,7 +64,6 @@ export function prepEach(widget, settings, print, firstPoint, update) {
stockz = stock.z * (isIndexed ? 0.5 : 1), stockz = stock.z * (isIndexed ? 0.5 : 1),
outer = settings.bounds || widget.getPositionBox(), outer = settings.bounds || widget.getPositionBox(),
outerz = outer.max.z, outerz = outer.max.z,
slices = widget.slices,
zclear = (process.camZClearance || 1), zclear = (process.camZClearance || 1),
zmax_force = process.camForceZMax || false, zmax_force = process.camForceZMax || false,
zmax_outer = stockz + zclear, 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, boundsz = isIndexed ? stock.z / 2 : bounds.max.z + ztOff,
zadd = !isIndexed ? stock.z - boundsz : alignTop ? outerz - boundsz : 0, zadd = !isIndexed ? stock.z - boundsz : alignTop ? outerz - boundsz : 0,
zmax = outerz + zclear + (process.camOriginOffZ || 0), zmax = outerz + zclear + (process.camOriginOffZ || 0),
zsafe = isIndexed ? Math.hypot(bounds.dim.y, bounds.dim.z) / 2 + zclear : zmax,
wmpos = widget.track.pos, wmpos = widget.track.pos,
wmx = wmpos.x, wmx = wmpos.x,
wmy = wmpos.y, wmy = wmpos.y,
originx = (startCenter ? 0 : -stock.x / 2) + (process.camOriginOffX || 0), originx = (startCenter ? 0 : -stock.x / 2) + (process.camOriginOffX || 0),
originy = (startCenter ? 0 : -stock.y / 2) + (process.camOriginOffY || 0), originy = (startCenter ? 0 : -stock.y / 2) + (process.camOriginOffY || 0),
origin = newPoint(originx + wmx, originy + wmy, zmax), origin = newPoint(originx + wmx, originy + wmy, zmax),
output = print.output,
easeDown = process.camEaseDown, easeDown = process.camEaseDown,
easeAngle = process.camEaseAngle, easeAngle = process.camEaseAngle,
depthFirst = process.camDepthFirst, depthFirst = process.camDepthFirst,
@ -94,13 +92,11 @@ export function prepEach(widget, settings, print, firstPoint, update) {
drillDown = 0, drillDown = 0,
drillLift = 0, drillLift = 0,
drillDwell = 0, drillDwell = 0,
drillThru = 0,
lasering = false, lasering = false,
laserPower = 0, laserPower = 0,
newOutput = print.output || [], newOutput = print.output || [],
layerOut = [], layerOut = [],
printPoint, printPoint,
isNewMode,
isPocket, isPocket,
isContour, isContour,
isRough, isRough,
@ -112,6 +108,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
toolDiamMove, toolDiamMove,
plungeRate = process.camFastFeedZ, plungeRate = process.camFastFeedZ,
feedRate, feedRate,
lastOp,
lastTool, lastTool,
lastPush, lastPush,
lastPoint, lastPoint,
@ -187,11 +184,10 @@ export function prepEach(widget, settings, print, firstPoint, update) {
laserPower = power; laserPower = power;
} }
function setDrill(down, lift, dwell, thru) { function setDrill(down, lift, dwell) {
drillDown = down; drillDown = down;
drillLift = lift; drillLift = lift;
drillDwell = dwell; drillDwell = dwell;
drillThru = thru;
} }
function emitDrills(polys) { function emitDrills(polys) {
@ -264,10 +260,11 @@ export function prepEach(widget, settings, print, firstPoint, update) {
*/ */
function layerPush(point, emit, speed, tool, options) { function layerPush(point, emit, speed, tool, options) {
const { type, center, arcPoints } = options ?? {}; const { type, center, arcPoints } = options ?? {};
const dz = (point && lastPush && lastPush.point) ? point.z - lastPush.point.z : 0; const dz = (point && lastPush?.point) ? point.z - lastPush.point.z : 0;
if (dz < 0 && speed > plungeRate) { if (dz < -0.05 && speed > plungeRate) {
speed = plungeRate; speed = plungeRate;
} }
// if (options?.type !== 'lerp') console.log( point, currentOp.type );
layerOut.mode = currentOp; layerOut.mode = currentOp;
if (lasering) { if (lasering) {
let power = emit ? laserPower : 0; let power = emit ? laserPower : 0;
@ -320,7 +317,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
p.x + wmx, p.x + wmx,
p.y + wmy, p.y + wmy,
p.z + zadd 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 * @param {number} opts.factor speed scale factor
*/ */
function camOut(point, emit = 1, opts) { 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 { let {
center = {}, center = {},
clockwise = true, clockwise = true,
@ -342,14 +358,12 @@ export function prepEach(widget, settings, print, firstPoint, update) {
factor = 1, factor = 1,
} = opts ?? {} } = opts ?? {}
// console.log({radius})
const isArc = emit == 2 || emit == 3; const isArc = emit == 2 || emit == 3;
const pointA = point.a;
// apply widget movement pos // apply widget movement pos
const pointA = point.a;
point = applyWidgetMovement(point); point = applyWidgetMovement(point);
// console.log(point.z);
if (nextIsMove) { if (nextIsMove) {
emit = 0; emit = 0;
nextIsMove = false; nextIsMove = false;
@ -370,7 +384,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
for (let a of lerp) { for (let a of lerp) {
let lp = point.clone().setA(a); let lp = point.clone().setA(a);
// console.log(lp.a, lp.x, lp.y, lp.z); // console.log(lp.a, lp.x, lp.y, lp.z);
lastPoint = layerPush( layerPush(
lp, lp,
emit, emit,
rate, rate,
@ -403,16 +417,20 @@ export function prepEach(widget, settings, print, firstPoint, update) {
layerPush(point.clone().setZ(stockz + 1), 0, 0, tool); layerPush(point.clone().setZ(stockz + 1), 0, 0, tool);
// change to cutting move for remainder of plunge // change to cutting move for remainder of plunge
emit = 1; emit = 1;
isMove = false;
} }
// drop points too close together // 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]); // console.trace(["drop dup",lastPoint,point]);
return; 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 // 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 iscontour = tolerance > 0;
let isflat = absDeltaZ < 0.001; let isflat = absDeltaZ < 0.001;
// restrict this to contouring // restrict this to contouring
@ -431,7 +449,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
} else if (point.z < lastPoint.z) { } else if (point.z < lastPoint.z) {
layerPush(point.clone().setZ(lastPoint.z), 0, 0, tool); 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 // for longer moves, check the terrain to see if we need to go up and over
const bigXY = (deltaXY > moveLen && !lasering); const bigXY = (deltaXY > moveLen && !lasering);
const bigZ = (deltaZ > toolDiam / 2 && deltaXY > tolerance); const bigZ = (deltaZ > toolDiam / 2 && deltaXY > tolerance);
@ -478,13 +496,15 @@ export function prepEach(widget, settings, print, firstPoint, update) {
} }
// set new plunge rate // set new plunge rate
let tmprate;
if (false)
if (!lasering && !isLathe && deltaZ < -tolerance) { if (!lasering && !isLathe && deltaZ < -tolerance) {
let threshold = Math.min(deltaXY / 2, absDeltaZ), let threshold = Math.min(deltaXY / 2, absDeltaZ),
modifier = threshold / absDeltaZ; modifier = threshold / absDeltaZ;
if (synthPlunge && threshold && modifier && deltaXY > tolerance) { if (synthPlunge && threshold && modifier && deltaXY > tolerance) {
// use modifier to speed up long XY move plunge rates // use modifier to speed up long XY move plunge rates
// console.log('modifier', modifier); // console.log('modifier', modifier);
rate = Math.max( tmprate = Math.max(
plungeRate, plungeRate,
Math.round(plungeRate + ((feedRate - plungeRate) * modifier)) 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 L = Math.hypot(deltaXY, absDeltaZ);
let limXY = feedRate * L / deltaXY; let limXY = feedRate * L / deltaXY;
let limZ = plungeRate * L / Math.abs(deltaZ); 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; // let zps = len / absDeltaZ;
// rate = Math.max( // rate = Math.max(
// plungeRate, // plungeRate,
@ -508,7 +529,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
lastPoint = layerPush( lastPoint = layerPush(
point, point,
clockwise ? 2 : 3, clockwise ? 2 : 3,
rate, tmprate ?? rate,
tool, tool,
{ {
center, center,
@ -523,7 +544,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
lastPoint = layerPush( lastPoint = layerPush(
point, point,
emit, emit,
rate, tmprate ?? rate,
tool tool
); );
} }
@ -533,16 +554,15 @@ export function prepEach(widget, settings, print, firstPoint, update) {
* output an array of slices that form a pocket * output an array of slices that form a pocket
* used by rough and pocket ops, does not support arcs * used by rough and pocket ops, does not support arcs
* *
* @param {Slice[]} top-down Z stack of slices * @param {Slice[]} slices top-down Z stack of slices
* @param {*} opts * @param {boolean} cutdir true=CW false=CCW
* @param {boolean} depthFirst prioritize cut depth in pockets by nesting
*/ */
function sliceOutput(sliceOut, opts = {}) { function pocket({ slices, cutdir, depthFirst, easeDown, progress }) {
const { cutdir, depthFirst, easeDown, progress } = opts;
let total = 0; let total = 0;
let depthData = []; let depthData = [];
for (let slice of sliceOut) { for (let slice of slices) {
let polys = [], t = [], c = []; let polys = [], t = [], c = [];
POLY.flatten(slice.camLines).forEach(function (poly) { POLY.flatten(slice.camLines).forEach(function (poly) {
let child = poly.parent; let child = poly.parent;
@ -569,7 +589,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
}, { swapdir: false }); }, { swapdir: false });
newLayer(); newLayer();
} }
progress(++total, sliceOut.length); progress(++total, slices.length);
} }
// crucially returns true for -0 as well as other negative #s // 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 // make top start offset configurable
printPoint = firstPoint || origin; 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 = { let ops = {
stock,
setTool,
setDrill,
setSpindle,
setTolerance,
setPrintPoint,
setLasering,
getPrintPoint() { return printPoint },
printPoint,
newLayer,
addGCode, addGCode,
bounds,
camOut, camOut,
polyEmit,
poly2polyEmit,
tip2tipEmit,
depthRoughPath,
depthOutlinePath, depthOutlinePath,
sliceOutput, depthRoughPath,
emitDrills, emitDrills,
emitTrace, emitTrace,
bounds, getPrintPoint() { return printPoint },
lastPoint: () => { return lastPoint },
newLayer,
pocket,
poly2polyEmit,
polyEmit,
printPoint,
setDrill,
setLasering,
setPrintPoint,
setSpindle,
setTolerance,
setTool,
stock,
tip2tipEmit,
zclear, zclear,
zmax, zmax,
lastPoint: () => { return lastPoint } zsafe,
}; };
let opSum = 0; let opSum = 0;
@ -973,7 +981,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
let arcPoints = arcToPath(from, to, arcPreviewRes, { clockwise, center }) let arcPoints = arcToPath(from, to, arcPreviewRes, { clockwise, center })
.map(applyWidgetMovement); .map(applyWidgetMovement);
// console.log({arcPoints}) // console.log({arcPoints})
@ -1096,7 +1104,10 @@ export function prepEach(widget, settings, print, firstPoint, update) {
if (lastPoint && newOutput.length) { if (lastPoint && newOutput.length) {
let lastLayer = newOutput.filter(layer => Array.isArray(layer)).peek(); let lastLayer = newOutput.filter(layer => Array.isArray(layer)).peek();
if (Array.isArray(lastLayer)) { 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); // console.log("prepare output", newOutput);

View file

@ -119,6 +119,10 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
ondone(msg); ondone(msg);
} }
function alert(msg) {
onupdate(null,null,msg);
}
if (unsafe) { if (unsafe) {
console.log("disabling overhang safeties"); console.log("disabling overhang safeties");
} }
@ -135,20 +139,20 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
let maxDelta = 1e-3; let maxDelta = 1e-3;
if (stock.x + maxDelta < (bounds.max.x - bounds.min.x)) { 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)) { 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)) { 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) { 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(); let mark = Date.now();
@ -158,31 +162,31 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
let shadows = {}; let shadows = {};
let slicer; let slicer;
let state = { let state = {
settings, addSlices,
widget,
bounds, bounds,
tabs, color,
cutTabs, computeShadows,
cutPolys,
contourPolys, contourPolys,
cutPolys,
cutTabs,
dark,
healPolys, healPolys,
isIndexed,
ops: opList,
setAxisIndex,
settings,
shadowAt, shadowAt,
slicer, slicer,
addSlices, tabs,
isIndexed, unsafe,
setAxisIndex,
updateToolDiams,
updateSlicer, updateSlicer,
computeShadows, updateToolDiams,
widget,
zBottom, zBottom,
zMax,
zThru, zThru,
ztOff, ztOff,
zMax, zTop
zTop,
unsafe,
color,
dark,
ops: opList
}; };
let tracker = setSliceTracker({ rotation: 0 }); 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_z = isIndexed ? valz.ov_botz : bottom_stock + valz.ov_botz;
workover.bottom_cut = Math.max(workover.bottom_z, -zThru); 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, { Object.assign(state, {
layername,
zBottom, zBottom,
zThru, zThru,
ztOff, 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") }; let opts = { each: onEach, progress: (num, total) => onProgress(num / total * 0.5, "slice") };
await slicer.slice(indices, opts); await slicer.slice(indices, opts);
let shadowedDrills = false; let shadowedDrills = false;
// console.log("slices",slices)
for (let [i, slice] of slices.entries()) { for (let [i, slice] of slices.entries()) {
for (let top of slice.tops) { for (let top of slice.tops) {
// console.log("slicing",slice.z,top) // console.log("slicing",slice.z,top)
@ -698,13 +705,13 @@ export async function holes(settings, widget, individual, rec, onProgress) {
continue; continue;
} }
for (let poly of inner) { for (let poly of inner) {
if (poly.points.length < 7) continue; if (poly.points.length < 7) {
continue;
}
let center = poly.calcCircleCenter(); let center = poly.calcCircleCenter();
center.area = poly.area(); center.area = poly.area();
center.overlapping = [center]; center.overlapping = [center];
center.depth = 0; center.depth = 0;
// console.log("center",center)
if (poly.circularity() < 0.98) { if (poly.circularity() < 0.98) {
// if not circular, don't add to holes // if not circular, don't add to holes
continue; continue;
@ -717,8 +724,7 @@ export async function holes(settings, widget, individual, rec, onProgress) {
let overlap = false; let overlap = false;
for (let [i, circle] of circles.entries()) { for (let [i, circle] of circles.entries()) {
let dist = circle.distTo2D(center); let dist = circle.distTo2D(center);
// if on the same xy point,
// //if on the same xy point,
if (dist <= centerDiff) { if (dist <= centerDiff) {
// console.log("overlap",center,circle); // console.log("overlap",center,circle);
circle.overlapping.push(center); 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"); onProgress(0.5 + (i / slices.length * 0.25), "recognize circles");
} }
let drills = []; let drills = [];
for (let [i, c] of circles.entries()) { for (let [i, c] of circles.entries()) {
let overlapping = c.overlapping; let overlapping = c.overlapping;
let last = overlapping.shift(); let last = overlapping.shift();
while (overlapping.length) { while (overlapping.length) {
let circ = overlapping.shift(); let circ = overlapping.shift();
@ -775,14 +779,33 @@ export async function holes(settings, widget, individual, rec, onProgress) {
return drills; return drills;
} }
function cutTabs(tabs, offset, z, inter) { function cutTabs(tabs, offset) {
tabs = tabs.filter(tab => z < tab.pos.z + tab.dim.z / 2).map(tab => tab.off).flat(); let out = [];
return cutPolys(tabs, offset, z, false); 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 = []; 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); 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) { for (let p of noff) {
if (p.appearsClosed()) { if (p.appearsClosed()) {
p.points.pop(); p.points.pop();
@ -818,29 +851,29 @@ function healPolys(noff) {
let s2 = ntmp[j]; let s2 = ntmp[j];
if (!s2) continue; if (!s2) continue;
// require polys at same Z to heal // 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; continue;
} }
if (!(s1.open && s2.open)) continue; if (!(s1.open && s2.open)) continue;
if (s1.last().isMergable2D(s2.first())) { if (s1.last().isMergable2D(s2.first())) {
s1.addPoints(s2.points.slice(1)); s1.addPoints(s2.points);
ntmp[j] = null; ntmp[j] = null;
continue outer; continue outer;
} }
if (s2.last().isMergable2D(s1.first())) { if (s2.last().isMergable2D(s1.first())) {
s2.addPoints(s1.points.slice(1)); s2.addPoints(s1.points);
ntmp[i] = null; ntmp[i] = null;
continue outer; continue outer;
} }
if (s1.first().isMergable2D(s2.first())) { if (s1.first().isMergable2D(s2.first())) {
s1.reverse(); s1.reverse();
s1.addPoints(s2.points.slice(1)); s1.addPoints(s2.points);
ntmp[j] = null; ntmp[j] = null;
continue outer; continue outer;
} }
if (s1.last().isMergable2D(s2.last())) { if (s1.last().isMergable2D(s2.last())) {
s2.reverse(); s2.reverse();
s1.addPoints(s2.points.slice(1)); s1.addPoints(s2.points);
ntmp[j] = null; ntmp[j] = null;
continue outer; continue outer;
} }
@ -854,9 +887,14 @@ function healPolys(noff) {
} }
// close poly if head meets tail // close poly if head meets tail
for (let poly of noff) { for (let poly of noff) {
if (poly.open && poly.first().isMergable2D(poly.last())) { poly.points = dedup(poly.points, (a,b) => a.isMergable3D(b));
poly.points.pop(); if (poly.open) {
poly.open = false; 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 { config } = base;
const timing = false; const timing = false;
const zDecimal = 3; const zDecimal = 3;
const epsilon = 10e-5;
const begin = function () { const begin = function () {
if (timing) console.time(...arguments); if (timing) console.time(...arguments);
@ -94,7 +95,7 @@ export class Slicer {
countZ(p3.z); countZ(p3.z);
} }
// use co-flat and co-line detection to adjust slice 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 // detect zFlat faces to avoid slicing directly on them
let zkey = p1.z.toFixed(zDecimal), let zkey = p1.z.toFixed(zDecimal),
area = Math.abs(util.area2(p1, p2, p3)) / 2; area = Math.abs(util.area2(p1, p2, p3)) / 2;

View file

@ -16,9 +16,10 @@ export class Topo {
async generate(opt = {}) { async generate(opt = {}) {
let { state, contour, onupdate, ondone } = 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, let { controller, process } = settings,
{ webGPU } = controller,
animesh = parseInt(controller.animesh || 100) * 2500, animesh = parseInt(controller.animesh || 100) * 2500,
axis = contour.axis.toLowerCase(), axis = contour.axis.toLowerCase(),
contourX = axis === "x", contourX = axis === "x",
@ -37,16 +38,15 @@ export class Topo {
boundsY = maxY - minY, boundsY = maxY - minY,
inside = contour.inside, inside = contour.inside,
density = 1 + (contour.reduction || 0), 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), tool = new Tool(settings, contour.tool),
toolOffset = tool.generateProfile(resolution).profile, toolOffset = tool.generateProfile(resolution).profile,
toolDiameter = tool.fluteDiameter(), toolDiameter = tool.fluteDiameter(),
toolStep = toolDiameter * contour.step, //tool.contourOffset(contour.step), toolStep = toolDiameter * contour.step,
leave = contour.leave || 0, leave = contour.leave || 0,
maxangle = contour.angle, maxangle = contour.angle,
curvesOnly = contour.curves, curvesOnly = contour.curves,
bridge = contour.bridging || 0, bridge = contour.bridging || 0,
// R2A = 180 / Math.PI,
stepsX = Math.ceil(boundsX / resolution), stepsX = Math.ceil(boundsX / resolution),
stepsY = Math.ceil(boundsY / resolution), stepsY = Math.ceil(boundsY / resolution),
widtopo = widget.topo, widtopo = widget.topo,
@ -75,12 +75,7 @@ export class Topo {
clipTo = inside ? shadow : POLY.expand(shadow, toolDiameter / 2 + resolution * 3), clipTo = inside ? shadow : POLY.expand(shadow, toolDiameter / 2 + resolution * 3),
partOff = inside ? 0 : toolDiameter / 2 + resolution, partOff = inside ? 0 : toolDiameter / 2 + resolution,
gridDelta = Math.floor(partOff / resolution), gridDelta = Math.floor(partOff / resolution),
debug = false, debug_clips = false;
debug_clips = debug && true,
debug_topo = debug && true,
debug_topo_lines = debug && true,
debug_topo_shells = debug && true,
time = Date.now;
if (tolerance === 0 && !topoCache) { if (tolerance === 0 && !topoCache) {
console.log(widget.id, 'topo auto tolerance', resolution.round(4)); console.log(widget.id, 'topo auto tolerance', resolution.round(4));
@ -90,7 +85,7 @@ export class Topo {
// used by Pocket -> Contour // used by Pocket -> Contour
this.tolerance = resolution; this.tolerance = resolution;
if (tabs) { if (tabsOn) {
clipTab.appendAll(tabs.map(tab => { clipTab.appendAll(tabs.map(tab => {
let ctab = POLY.expand([tab.poly], toolDiameter / 2); let ctab = POLY.expand([tab.poly], toolDiameter / 2);
ctab.forEach(ct => ct.z = tab.dim.z / 2 + tab.pos.z); ctab.forEach(ct => ct.z = tab.dim.z / 2 + tab.pos.z);
@ -107,6 +102,190 @@ export class Topo {
newslices.push(debug); 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({ const probe = this.probe = new Probe({
profile: toolOffset, profile: toolOffset,
data, data,
@ -177,8 +356,7 @@ export class Topo {
clipTabZ: clipTab ? clipTab.map(t => t.z) : undefined, clipTabZ: clipTab ? clipTab.map(t => t.z) : undefined,
tabHeight, tabHeight,
newslices, newslices,
leave, leave
color
}, (i, l, p) => { }, (i, l, p) => {
onupdate(l / 2 + i / 2, 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 { minX, maxX, minY, maxY, boundsX, boundsY, stepsX, stepsY } = params;
const { gridDelta, resolution, density, partOff, toolStep, contourX, contourY } = 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, let stepsTaken = 0,
stepsTotal = 0; stepsTotal = 0;
@ -370,11 +548,8 @@ export class Topo {
gridy gridy
}, segments => { }, segments => {
if (segments.length > 0) { if (segments.length > 0) {
let slice = newSlice(gridx); let slice = newSlice(x);
slice.camLines = segments; slice.camLines = segments;
slice.output()
.setLayer("contour y", { face: color, line: color })
.addPolys(segments);
slicesY.push(slice); slicesY.push(slice);
} }
onupdate(++stepsTaken, stepsTotal, "contour y"); onupdate(++stepsTaken, stepsTotal, "contour y");
@ -399,11 +574,8 @@ export class Topo {
gridy gridy
}, segments => { }, segments => {
if (segments.length > 0) { if (segments.length > 0) {
let slice = newSlice(gridy); let slice = newSlice(y);
slice.camLines = segments; slice.camLines = segments;
slice.output()
.setLayer("contour x", { face: color, line: color })
.addPolys(segments);
slicesX.push(slice); slicesX.push(slice);
} }
onupdate(++stepsTaken, stepsTotal, "contour x"); onupdate(++stepsTaken, stepsTotal, "contour x");
@ -534,9 +706,9 @@ export class Trace {
console.log(...arguments); 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 newP = newPoint(x, y, z);
const lastP = lastPP;//trace.last(); const lastP = lastPP;
if (lastP) { if (lastP) {
const dl = (x - lastP.x) || (y - lastP.y); const dl = (x - lastP.x) || (y - lastP.y);
@ -553,7 +725,6 @@ export class Trace {
} }
if (curvesOnly) { if (curvesOnly) {
const dv = contourX ? Math.abs(lastP.x - x) : Math.abs(lastP.y - y); 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; const angle = Math.atan2(Math.abs(dz), dv) * RAD2DEG;
if (angle > maxangle) { if (angle > maxangle) {
end_poly(); end_poly();

View file

@ -22,8 +22,9 @@ export class Topo {
async generate(opt = {}) { async generate(opt = {}) {
let { state, op, onupdate, ondone } = opt; let { state, op, onupdate, ondone } = opt;
let { widget, settings, tabs } = opt.state; let { widget, settings, tabs, color } = state;
let { controller, process } = settings; let { controller, process } = settings;
let { webGPU } = controller;
let axis = op.axis.toLowerCase(), let axis = op.axis.toLowerCase(),
tool = new Tool(settings, op.tool), tool = new Tool(settings, op.tool),
@ -38,9 +39,8 @@ export class Topo {
x: axis === "x", x: axis === "x",
y: axis === "y" y: axis === "y"
}, },
zMin = min.z + 0.0001,
tolerance = op.tolerance, 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, step = this.step = (tool.traceOffset() * 2) * op.step,
angle = this.angle = op.angle || 1; angle = this.angle = op.angle || 1;
@ -52,31 +52,36 @@ export class Topo {
console.log(widget.id, 'topo4 auto tolerance', resolution.round(4)); console.log(widget.id, 'topo4 auto tolerance', resolution.round(4));
} }
this.zBottom = state.zBottom; this.zBottom = state.zBottom ?? 0;
this.resolution = resolution; this.resolution = resolution;
this.vertices = widget.getGeoVertices({ unroll: true, translate: true }); this.vertices = widget.getGeoVertices({ unroll: true, translate: true });
this.tabverts = widget.getTabVertices(); this.tabverts = widget.getTabVertices();
this.tool = tool.generateProfile(resolution).profile; this.tool = tool.generateProfile(resolution).profile;
this.maxo = tool.profileDim.maxo * resolution; this.maxo = tool.profileDim.maxo * resolution;
this.diam = tool.fluteDiameter(); this.diam = tool.fluteDiameter();
this.unit = tool.unitScale();
this.units = controller.units === 'in' ? 25.4 : 1
this.zoff = widget.track.top || 0; this.zoff = widget.track.top || 0;
this.leave = op.leave || 0; this.leave = op.leave || 0;
this.linear = op.linear || false; 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"); onupdate(0, "lathe");
const parts = webGPU ? [ 0.8, 0.2 ] : [ 0.25, 0.75 ];
const range = this.range = { min: Infinity, max: -Infinity }; 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) { for (let slice of slices) {
range.min = Math.min(range.min, slice.z); range.min = Math.min(range.min, slice.z);
range.max = Math.max(range.max, 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"); onupdate(1, "lathe");
ondone(lathe); ondone(lathe);
@ -86,10 +91,9 @@ export class Topo {
} }
async slice(onupdate) { 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 range = this.range = { min: Infinity, max: -Infinity };
const box = this.box = new THREE.Box2();
// swap XZ in shared array // swap XZ in shared array
for (let i = 0, l = vertices.length; i < l; i += 3) { 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); range.max = Math.max(range.max, x);
} }
// add tool radius to slice min/max range ot fully carve part // add tool diameter to slice min/max range to fully carve part
range.min -= this.diam / 2; range.min += offStart * units;
range.max += this.diam / 2; range.max -= offEnd * units;
range.max += resolution * 2;
// merge in tab vertices here so they don't affect slice range / dimensions // merge in tab vertices here so they don't affect slice range / dimensions
for (let i = 0, l = tabverts.length; i < l; i += 3) { 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 // re-create shared vertex array for workers
this.vertices = [].appendAll(vertices).appendAll(tabverts).toFloat32().toShared(); this.vertices = [].appendAll(vertices).appendAll(tabverts).toFloat32().toShared();
const shards = Math.ceil(Math.min(25, vertices.length / 27000)); // rp.rasterizeMesh(vertices, resolution).then(rpo => console.log({ rpo }));
const step = (range.max - range.min) / shards; if (this.gpu) {
return await this.sliceGPU(onupdate);
let index = 0;
let slices = this.slices = [];
let slice = { min: range.min, max: range.min + step, index };
for (let z = range.min; z < range.max; z += resolution) {
if (z > slice.max) {
slices.push(slice);
slice = { min: z, max: z + step, index };
}
index++;
} }
slices.push(slice);
// console.log({ shards, range, step, slices });
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) { if (minions?.running > 1) {
return await this.sliceMinions(onupdate); return await this.sliceMinions(onupdate);
} else { } 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) { async sliceWorker(onupdate) {
const { vertices, slices, resolution } = this; const { vertices, slices, resolution } = this;
@ -205,22 +312,28 @@ export class Topo {
async lathe(onupdate) { async lathe(onupdate) {
const { minions } = self.kiri_worker; 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); return await this.latheMinions(onupdate);
} else { } else {
return await this.latheWorker(onupdate); return await this.latheWorker(onupdate);
} }
} }
async latheGPU(onupdate) {
return this.gpu_slices;
}
lathePath(slices, tool) { lathePath(slices, tool) {
const { resolution, step, zBottom, maxo } = this; const { resolution, step, zBottom, maxo } = this;
const tlen = tool.length; const tlen = tool.length;
const slen = slices.length; const slen = slices.length;
const sinc = Math.max(1, Math.ceil(step / resolution));
const heights = []; 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 = []; const oslices = [];
for (let slice of slices) { for (let slice of slices) {
const lines = slice.lines; const lines = slice.lines;
@ -244,7 +357,9 @@ export class Topo {
// iterate over all slices (real x = z) // iterate over all slices (real x = z)
// find max real z using z ray intersect from tool point to slice lines + offset // find max real z using z ray intersect from tool point to slice lines + offset
let lz = 0; 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; const rx = oslices[si].z;
let mz = -Infinity; let mz = -Infinity;
// iterate over tool offsets // iterate over tool offsets
@ -281,12 +396,12 @@ export class Topo {
} }
if (mz === -Infinity && xo === 0 && yo === 0) { if (mz === -Infinity && xo === 0 && yo === 0) {
// tool tip is off the model // tool tip is off the model
continue; // continue;
} }
} }
if (mz === -Infinity) { if (mz === -Infinity) {
mz = zBottom; // Math.min(0, lz) mz = zBottom;
} else if (zBottom && mz < zBottom) { } else if (mz < zBottom) {
mz = zBottom; mz = zBottom;
} }
heights.push(rx, 0, lz = mz); heights.push(rx, 0, lz = mz);

View file

@ -34,14 +34,11 @@ export function init() {
"sliceFillType": LANG.fi_type, "sliceFillType": LANG.fi_type,
"sliceFillWidth": LANG.fi_wdth_s, "sliceFillWidth": LANG.fi_wdth_s,
"sliceFillSparse": LANG.fi_pcnt_s, "sliceFillSparse": LANG.fi_pcnt_s,
"sliceFillRate": LANG.ou_feed_s,
"sliceFillGrow": LANG.fi_grow_s, "sliceFillGrow": LANG.fi_grow_s,
"sliceSolidRate": LANG.ou_fini_s,
"sliceSolidMinArea": LANG.ad_msol_s, "sliceSolidMinArea": LANG.ad_msol_s,
"sliceTopLayers": LANG.sl_ltop_s, "sliceTopLayers": LANG.sl_ltop_s,
"sliceBottomLayers": LANG.sl_lbot_s, "sliceBottomLayers": LANG.sl_lbot_s,
"firstLayerRate": LANG.fl_rate_s, "firstLayerRate": LANG.fl_rate_s,
"firstLayerPrintMult": LANG.fl_mult_s,
"firstLayerFanSpeed": LANG.ou_fans_s, "firstLayerFanSpeed": LANG.ou_fans_s,
// prepare // prepare
"sliceShellOrder": LANG.sl_ordr_s, "sliceShellOrder": LANG.sl_ordr_s,

View file

@ -9,6 +9,7 @@ import { fdm_export } from './export.js';
// fil = filament diameter // fil = filament diameter
// slice = slice height // slice = slice height
function extrudePerMM(noz, fil, slice) { 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); return ((Math.PI * util.sqr(noz / 2)) / (Math.PI * util.sqr(fil / 2))) * (slice / noz);
}; };

View file

@ -26,7 +26,6 @@ export function fdm_export(print, online, ondone, ondebug) {
{ extrudeAbs } = device, { extrudeAbs } = device,
{ exportThumb } = controller, { exportThumb } = controller,
{ extrudeMM, extrudePerMM } = FDM, { extrudeMM, extrudePerMM } = FDM,
extused = Object.keys(print.extruders).map(v => parseInt(v)),
timeFactor = (device.gcodeTime || 1) * 1.5, timeFactor = (device.gcodeTime || 1) * 1.5,
decimals = config.gcode_decimals || 4, decimals = config.gcode_decimals || 4,
zMoveMax = device.deviceZMax || 0, zMoveMax = device.deviceZMax || 0,
@ -49,7 +48,6 @@ export function fdm_export(print, online, ondone, ondebug) {
last = null, last = null,
zpos = 0, zpos = 0,
bmax = 0, bmax = 0,
blastz = 0,
belt_add_y = (process.firstLayerYOffset || 0) - (belty || 0), belt_add_y = (process.firstLayerYOffset || 0) - (belty || 0),
oloops = process.outputLoops || 0, oloops = process.outputLoops || 0,
zhop = process.zHopDistance || 0, // range zhop = process.zHopDistance || 0, // range
@ -58,10 +56,8 @@ export function fdm_export(print, online, ondone, ondebug) {
retDist = process.outputRetractDist || 0, // range retDist = process.outputRetractDist || 0, // range
retSpeed = process.outputRetractSpeed * 60 || 1, // range retSpeed = process.outputRetractSpeed * 60 || 1, // range
retDwell = process.outputRetractDwell || 0, // range retDwell = process.outputRetractDwell || 0, // range
timeDwell = retDwell / 1000,
scarfZ = process.outputScarfLength ? true : false, scarfZ = process.outputScarfLength ? true : false,
arcDist = isBelt || scarfZ ? 0 : (process.arcTolerance || 0), arcDist = isBelt || scarfZ ? 0 : (process.arcTolerance || 0),
arcMin = 1,
arcRes = 20, arcRes = 20,
arcDev = 0.5, arcDev = 0.5,
arcMax = 40, arcMax = 40,
@ -85,7 +81,6 @@ export function fdm_export(print, online, ondone, ondebug) {
fanSpeedSave = undefined, fanSpeedSave = undefined,
fanSpeedBase = 0, fanSpeedBase = 0,
fanSpeed = 0, fanSpeed = 0,
lastType = undefined,
lastNozzleTemp = nozzleTemp, lastNozzleTemp = nozzleTemp,
lastBedTemp = bedTemp, lastBedTemp = bedTemp,
lastFanSpeed = fanSpeed, lastFanSpeed = fanSpeed,
@ -169,7 +164,6 @@ export function fdm_export(print, online, ondone, ondebug) {
retDist = params.outputRetractDist || 0; // range retDist = params.outputRetractDist || 0; // range
retSpeed = params.outputRetractSpeed * 60 || 1; // range retSpeed = params.outputRetractSpeed * 60 || 1; // range
retDwell = params.outputRetractDwell || 0; // range retDwell = params.outputRetractDwell || 0; // range
timeDwell = retDwell / 1000;
nozzleTemp = layer === 0 ? nozzleTemp = layer === 0 ?
params.firstLayerNozzleTemp || params.outputTemp : params.firstLayerNozzleTemp || params.outputTemp :
params.outputTemp || params.firstLayerNozzleTemp; params.outputTemp || params.firstLayerNozzleTemp;
@ -297,8 +291,9 @@ export function fdm_export(print, online, ondone, ondebug) {
append(`; Bed type: ${bedType}`); append(`; Bed type: ${bedType}`);
append(`; Target: ${filter[mode]}`); append(`; Target: ${filter[mode]}`);
// inject thumbnail preview // inject thumbnail preview
if (exportThumb && worker.snap) { let { current } = self.kiri_worker ?? {};
let { width, height, url } = worker.snap; if (exportThumb && current?.snap) {
let { width, height, url } = current.snap;
let data = url.substring(url.indexOf(',') + 1); let data = url.substring(url.indexOf(',') + 1);
append(`; thumbnail begin ${width} ${height} ${data.length}`); append(`; thumbnail begin ${width} ${height} ${data.length}`);
for (let i=0; i<data.length; i += 78) { for (let i=0; i<data.length; i += 78) {
@ -470,7 +465,7 @@ export function fdm_export(print, online, ondone, ondebug) {
if (isBelt) { if (isBelt) {
let zheight = path ? path.height || 0 : 0; let zheight = path ? path.height || 0 : 0;
epos.x = originCenter ? -pos.x : device.bedWidth - pos.x; 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; epos.y = -pos.y + epos.z * bcos + belt_add_y;
lout = epos; lout = epos;
} }
@ -613,6 +608,9 @@ export function fdm_export(print, online, ondone, ondebug) {
for (pidx=0; pidx<path.length; pidx++) { for (pidx=0; pidx<path.length; pidx++) {
out = path[pidx]; out = path[pidx];
speedMMM = (out.speed || process.outputFeedrate) * 60; // range speedMMM = (out.speed || process.outputFeedrate) * 60; // range
if (!isBelt) {
fanSpeed = out.fan ?? fanSpeed;
}
// hint to controller that we're working on a specific object // hint to controller that we're working on a specific object
// so that gcode between start/stop comments can be cancelled // so that gcode between start/stop comments can be cancelled
@ -634,7 +632,7 @@ export function fdm_export(print, online, ondone, ondebug) {
// emit comment on output type chage // emit comment on output type chage
if (last && out.type !== last.type) { if (last && out.type !== last.type) {
lastType = subst.feature = out.type; subst.feature = out.type;
if (gcodeFeature && gcodeFeature.length) { if (gcodeFeature && gcodeFeature.length) {
appendAllSub(gcodeFeature); appendAllSub(gcodeFeature);
} else { } else {
@ -687,10 +685,10 @@ export function fdm_export(print, online, ondone, ondebug) {
// in belt mode, fan can change per segment (for base) // in belt mode, fan can change per segment (for base)
if (isBelt) { if (isBelt) {
setTempFanSpeed(out.fan); setTempFanSpeed(out.fan);
if (fanSpeed !== lastFanSpeed) { }
appendAllSub(gcodeFan); if (fanSpeed !== lastFanSpeed) {
lastFanSpeed = fanSpeed; appendAllSub(gcodeFan);
} lastFanSpeed = fanSpeed;
} }
if (lastp && out.emit) { if (lastp && out.emit) {
@ -751,7 +749,6 @@ export function fdm_export(print, online, ondone, ondebug) {
} }
// if new point is off the arc // 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()) { 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]; // let debug = [deem, depm, desp, dc * arcQ.center.length / arcQ.rSum > arcDist, dist > cc.r, cc.r > arcMax, radFault];
if (arcQ.length === 4) { if (arcQ.length === 4) {

View file

@ -79,6 +79,7 @@ export function menu() {
_____: newGroup(LANG.sw_menu, $('fdm-walls'), { modes:FDM, driven, hideable, separator, group:"fdm-walls" }), _____: 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 }), 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) }), 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 }), separator: newBlank({ class:"set-sep", driven }),
sliceShellOrder: newSelect(LANG.sl_ordr_s, { title:LANG.sl_ordr_l}, "shell"), 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"), 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}), 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" }), _____: 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}), 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}), 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}), 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" }), _____: newGroup(LANG.fi_menu, $('fdm-fill'), { modes:FDM, driven, hideable, separator, group:"fdm-fill" }),
sliceFillType: newSelect(LANG.fi_type, {trigger}, "infill"), 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}), 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}), 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" }), _____: 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}), 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}), 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" }), _____: 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) }), 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)}), outputFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255)}),
@ -135,11 +134,9 @@ export function menu() {
firstLayerFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255), show:isBelt}), firstLayerFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255), show:isBelt}),
firstLayerYOffset: newInput(LANG.fl_zoff_s, {title:LANG.fl_zoff_l, convert:toFloat, show:isBelt}), firstLayerYOffset: newInput(LANG.fl_zoff_s, {title:LANG.fl_zoff_l, convert:toFloat, show:isBelt}),
firstLayerFlatten: newInput(LANG.fl_flat_s, {title:LANG.fl_flat_l, convert:toFloat, show:isBelt}), firstLayerFlatten: newInput(LANG.fl_flat_s, {title:LANG.fl_flat_l, convert:toFloat, show:isBelt}),
firstLayerBeltFact: newInput(LANG.fl_mult_s, {title:LANG.fl_mult_l, convert:toFloat, bound:bound(0, 2), show:isBelt}),
firstLayerRate: newInput(LANG.fl_rate_s, {title:LANG.fl_rate_l, convert:toFloat}), 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}), 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 }), separator: newBlank({ class:"set-sep", driven, show:isBelt }),
firstLayerBrim: newInput(LANG.fl_brim_s, {title:LANG.fl_brim_l, convert:toInt, 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}), firstLayerBrimIn: newInput(LANG.fl_brin_s, {title:LANG.fl_brin_l, convert:toInt, show:isBelt}),
@ -154,7 +151,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() }), 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 }), separator: newBlank({ class:"set-sep", driven, show:isNotBelt }),
outputRaft: newBoolean(LANG.fr_nabl_s, onBooleanClick, {title:LANG.fr_nabl_l, trigger, 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" }), _____: 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}), 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}), outputFinishrate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt}),
@ -177,10 +173,13 @@ export function menu() {
arcTolerance: newInput(LANG.ad_arct_s, {title:LANG.ad_arct_l, convert:toFloat, bound:bound(0,1.0), show:() => { return isNotBelt() }}), 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}), 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)}), 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)}), 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)}), 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)}), outputShortPoly: newInput(LANG.ad_spol_s, {title:LANG.ad_spol_l, convert:toFloat, bound:bound(0,10000)}),
outputCoastDist: newInput(LANG.ad_scst_s, {title:LANG.ad_scst_l, convert:toFloat, bound:bound(0.0,10)}), outputCoastDist: newInput(LANG.ad_scst_s, {title:LANG.ad_scst_l, convert:toFloat, bound:bound(0.0,10)}),
sliceCompInner: newInput(LANG.sl_ofin_s, {title:LANG.sl_ofin_l, convert:toFloat, bound:bound(-10.0,10.0)}),
sliceCompOuter: newInput(LANG.sl_ofot_s, {title:LANG.sl_ofot_l, convert:toFloat, bound:bound(-10.0,10.0)}),
zHopDistance: newInput(LANG.ad_zhop_s, {title:LANG.ad_zhop_l, convert:toFloat, bound:bound(0,3.0)}), zHopDistance: newInput(LANG.ad_zhop_s, {title:LANG.ad_zhop_l, convert:toFloat, bound:bound(0,3.0)}),
outputLoops: newInput(LANG.ag_loop_s, {title:LANG.ag_loop_l, convert:toInt, bound:bound(-1,1000), show:isBelt}), outputLoops: newInput(LANG.ag_loop_s, {title:LANG.ag_loop_l, convert:toInt, bound:bound(-1,1000), show:isBelt}),

View file

@ -17,12 +17,13 @@ slicer.slicePost.FDM = slicePost;
* data object. return is ignored. * data object. return is ignored.
*/ */
export function slicePost(data, options) { export function slicePost(data, options) {
const { z, lines, groups } = data; const { groups, z } = data;
const { useAssembly, post_args, zIndexes } = options; const { post_args, useAssembly, zIndexes } = options;
const { process, isSynth, vaseMode } = post_args; const { isSynth, process, vaseMode } = post_args;
const { shellOffset, fillOffset, clipOffset } = post_args; const { compInner, compOuter, pump } = post_args;
const { clipOffset, fillOffset, shellOffset } = post_args;
if (options.pump) { if (pump) {
let nugroups = []; let nugroups = [];
let pump = shellOffset / 10; let pump = shellOffset / 10;
for (let p of groups) { for (let p of groups) {
@ -41,6 +42,19 @@ export function slicePost(data, options) {
data.tops = POLY.nest(groups); data.tops = POLY.nest(groups);
} }
// perimeter and inner hole compensation offsets
if (compInner || compOuter) {
let inner = POLY.inner(data.tops).flat();
let outer = POLY.outer(data.tops);
if (compInner) {
inner = POLY.flatten(POLY.offset(inner, compInner/2));
}
if (compOuter) {
outer = POLY.flatten(POLY.offset(outer, -compOuter/2));
}
data.tops = POLY.nest([...inner, ...outer]);
}
if (isSynth) { if (isSynth) {
const process = post_args.process; const process = post_args.process;
if (process.sliceSupportGrow > 0) { if (process.sliceSupportGrow > 0) {
@ -54,6 +68,7 @@ export function slicePost(data, options) {
delete data.groups; delete data.groups;
return; return;
} }
const index = zIndexes.indexOf(z); const index = zIndexes.indexOf(z);
const range = getRangeParameters(process, index); const range = getRangeParameters(process, index);
// calculate fractional shells // calculate fractional shells
@ -67,12 +82,9 @@ export function slicePost(data, options) {
let trg = shellFrac > 0.5 ? 1 : parts - 1; let trg = shellFrac > 0.5 ? 1 : parts - 1;
sliceShells += rem >= trg ? 1 : 0; 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 count = isSynth ? 1 : sliceShells;
const offset = shellOffset * spaceMult; const offset = shellOffset;
const fillOff = fillOffset * spaceMult; const fillOff = fillOffset;
const thinType = isSynth ? undefined : process.sliceDetectThin; const thinType = isSynth ? undefined : process.sliceDetectThin;
const nutops = []; const nutops = [];
// co-locate shell processing with top generation in slicer // co-locate shell processing with top generation in slicer
@ -129,7 +141,7 @@ function offset_default(params) {
} }
const deltaBig = 160; const deltaBig = 160;
const deltSmall = 20; const deltaSmall = 20;
// look for close points on adjacent segmented polys and merge // look for close points on adjacent segmented polys and merge
let test_polys = first !== last ? [ ...first, ...last ] : last; let test_polys = first !== last ? [ ...first, ...last ] : last;
@ -139,6 +151,8 @@ function offset_default(params) {
for (let poly of test_flats) for (let poly of test_flats)
poly.forEachSegment((p0, p1) => { poly.forEachSegment((p0, p1) => {
p0.angle = new Slope(p0, p1); p0.angle = new Slope(p0, p1);
// todo calc vertex angle
// exclude segment vertex from merge when angle < deltaSmall
}); });
// segment each poly to be tested // segment each poly to be tested

View file

@ -1,5 +1,6 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { FDM } from './driver-be.js';
import { base, util } from '../../../geo/base.js'; import { base, util } from '../../../geo/base.js';
import { poly2polyEmit, tip2tipEmit } from '../../../geo/paths.js'; import { poly2polyEmit, tip2tipEmit } from '../../../geo/paths.js';
import { newBounds } from '../../../geo/bounds.js'; import { newBounds } from '../../../geo/bounds.js';
@ -46,8 +47,6 @@ export async function fdm_prepare(widgets, settings, update) {
output = [], output = [],
layerout = []; layerout = [];
let lastLayerStart = null;
// compute bounds if missing // compute bounds if missing
if (!bounds) { if (!bounds) {
bounds = new THREE.Box3(); bounds = new THREE.Box3();
@ -553,12 +552,9 @@ export async function fdm_prepare(widgets, settings, update) {
offset, offset,
layerout, layerout,
{ {
routeAround: process.outputAvoidGaps,
seedPoint: printPoint.sub(offset),
params, // range parameters
first: slice.index === 0, first: slice.index === 0,
support: slice.widget.support,
onBelt: beltStart, onBelt: beltStart,
params, // range parameters
pretract: (wipeDist) => { pretract: (wipeDist) => {
if (!(lastLayer && lastLayer.length)) { if (!(lastLayer && lastLayer.length)) {
return; return;
@ -573,7 +569,11 @@ export async function fdm_prepare(widgets, settings, update) {
print.addOutput(lastLayer, endpoint, null, null, lastOut.tool); print.addOutput(lastLayer, endpoint, null, null, lastOut.tool);
} }
} }
} },
routeAround: process.outputAvoidGaps,
seedPoint: printPoint.sub(offset),
support: slice.widget.support,
zmax,
} }
); );
layerout.print_time = printPoint.layer_time; layerout.print_time = printPoint.layer_time;
@ -682,8 +682,8 @@ export async function fdm_prepare(widgets, settings, update) {
for (let rec of layer) { for (let rec of layer) {
overate = rec.overate >= 0 ? rec.overate : overate; overate = rec.overate >= 0 ? rec.overate : overate;
let bmult = params.firstLayerBeltFact ?? 1;
let brate = params.firstLayerRate || firstLayerRate; let brate = params.firstLayerRate || firstLayerRate;
let bmult = params.firstLayerPrintMult || params.firstLayerPrintMult;
let point = rec.point; let point = rec.point;
let belty = rec.belty = -point.y + (point.z * belt.slope); let belty = rec.belty = -point.y + (point.z * belt.slope);
let lowrate = belty <= thresh ? brate : overate || brate; let lowrate = belty <= thresh ? brate : overate || brate;
@ -695,8 +695,9 @@ export async function fdm_prepare(widgets, settings, update) {
if (rec.emit && belty <= thresh && lastout && Math.abs(lastout.belty - belty) < 0.01) { if (rec.emit && belty <= thresh && lastout && Math.abs(lastout.belty - belty) < 0.01) {
// apply base speed to segments touching belt // apply base speed to segments touching belt
rec.speed = params.firstLayerRate || Math.min(rec.speed, lowrate); rec.speed = params.firstLayerRate || Math.min(rec.speed, lowrate);
rec.emit *= bmult;
rec.fan = params.firstLayerFanSpeed; rec.fan = params.firstLayerFanSpeed;
// apply base flowrate multiplier to segments touching belt
rec.emit *= bmult;
minx = Math.min(minx, point.x, lastout.point.x); minx = Math.min(minx, point.x, lastout.point.x);
maxx = Math.max(maxx, point.x, lastout.point.x); maxx = Math.max(maxx, point.x, lastout.point.x);
maxy = Math.max(maxy, point.y); maxy = Math.max(maxy, point.y);
@ -795,10 +796,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 = {}) { function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
const { settings } = print; const { settings } = print;
const { device } = settings; const { device } = settings;
const { bedWidth, bedDepth, bedRound } = device; const { bedWidth, bedDepth, bedRound } = device;
const { extrudePerMM } = FDM;
let preout = [], let preout = [],
process = opt.params || settings.process, process = opt.params || settings.process,
@ -807,6 +820,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
scarfLength = process.outputScarfLength || 0, scarfLength = process.outputScarfLength || 0,
minLayerTime = process.outputMinLayerTime || 0, minLayerTime = process.outputMinLayerTime || 0,
nozzleSize = process.sliceLineWidth || device.extruders[extruder].extNozzle, nozzleSize = process.sliceLineWidth || device.extruders[extruder].extNozzle,
nozzles = device.extruders.map(e => e.extNozzle),
firstLayer = (opt.first || false) && !opt.support, firstLayer = (opt.first || false) && !opt.support,
thinWall = nozzleSize * (opt.thinWall || 1.75), thinWall = nozzleSize * (opt.thinWall || 1.75),
retractDist = opt.retractOver || (nozzleSize * 5), retractDist = opt.retractOver || (nozzleSize * 5),
@ -815,17 +829,20 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
shellOrder = {"out-in":-1,"in-out":1,"alternate":-2}[process.sliceShellOrder] || -1, shellOrder = {"out-in":-1,"in-out":1,"alternate":-2}[process.sliceShellOrder] || -1,
sparseMult = process.outputSparseMult, sparseMult = process.outputSparseMult,
coastDist = process.outputCoastDist || 0, coastDist = process.outputCoastDist || 0,
fanSpeed = process.outputFanSpeed,
firstShellSpeed = process.firstLayerRate, firstShellSpeed = process.firstLayerRate,
firstFillSpeed = process.firstLayerFillRate, firstFillSpeed = process.firstLayerFillRate,
firstPrintMult = process.firstLayerPrintMult,
finishSpeed = firstLayer ? firstShellSpeed : (opt.speed || process.outputFinishrate), finishSpeed = firstLayer ? firstShellSpeed : (opt.speed || process.outputFinishrate),
printSpeed = opt.speed || (firstLayer ? firstShellSpeed : process.outputFeedrate), printSpeed = opt.speed || (firstLayer ? firstShellSpeed : process.outputFeedrate),
fillSpeed = opt.speed || opt.fillSpeed || (firstLayer ? firstFillSpeed || 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, moveSpeed = process.outputSeekrate,
minSpeed = process.outputMinSpeed,
origin = startPoint.add(offset), origin = startPoint.add(offset),
zhop = process.zHopDistance || 0, zhop = process.zHopDistance || 0,
zmax = opt.zmax,
antiBacklash = process.antiBacklash, antiBacklash = process.antiBacklash,
maxFlowRate = process.outputMaxFlowrate,
wipeDist = process.outputRetractWipe || 0, wipeDist = process.outputRetractWipe || 0,
isBelt = device.bedBelt, isBelt = device.bedBelt,
bedOffset = originCenter ? { bedOffset = originCenter ? {
@ -849,11 +866,6 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
shellOrder = -shellOrder; shellOrder = -shellOrder;
} }
// update fill speed when solids detected
if (slice.finishSolids) {
fillSpeed = process.sliceSolidRate || finishSpeed;
}
// on flats layers also slow down shell print speed // on flats layers also slow down shell print speed
if (slice.isFlatsLayer) { if (slice.isFlatsLayer) {
printSpeed = finishSpeed || printSpeed; printSpeed = finishSpeed || printSpeed;
@ -862,13 +874,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
// override: adapt bridge layer speed // override: adapt bridge layer speed
if (slice.isBridgeLayer) { if (slice.isBridgeLayer) {
fillSpeed /= 2; fillSpeed /= 2;
} printSpeed /= 2;
// apply first layer extrusion multipliers
if (firstLayer) {
fillMult *= firstPrintMult;
shellMult *= firstPrintMult;
sparseMult *= firstPrintMult;
} }
function retract() { function retract() {
@ -1100,9 +1106,6 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
coast: firstLayer ? 0 : coastDist, coast: firstLayer ? 0 : coastDist,
extrude: numOrDefault(opt.extrude, shellMult), extrude: numOrDefault(opt.extrude, shellMult),
nozzleSize, nozzleSize,
rate: finishShell ? finishSpeed : printSpeed,
scarf,
tool: extruder,
onfirst: (firstPoint, preout) => { onfirst: (firstPoint, preout) => {
let from = seedPoint || startPoint; let from = seedPoint || startPoint;
if (from.distTo2D(firstPoint) > retractDist) { if (from.distTo2D(firstPoint) > retractDist) {
@ -1111,7 +1114,11 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
} }
} }
seedPoint = null; seedPoint = null;
} },
rate: finishShell ? finishSpeed : printSpeed,
scarf,
tool: extruder,
zmax
}); });
lastPoly = slice.lastPoly = poly; lastPoly = slice.lastPoly = poly;
} }
@ -1591,16 +1598,34 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
if (!p.emit) continue; if (!p.emit) continue;
let mms = p.speed ?? 1; let mms = p.speed ?? 1;
let dst = lp?.distTo2D(p.point) ?? 0; let dst = lp?.distTo2D(p.point) ?? 0;
lp = p.point;
if (dst) { if (dst) {
tt += (dst/mms); tt += (dst/mms);
td += dst; td += dst;
p.dst = dst;
} }
lp = p.point;
} }
if (minLayerTime && tt < minLayerTime) { if (minLayerTime && tt < minLayerTime) {
let fact = tt / minLayerTime; let fact = tt / minLayerTime;
for (let p of preout) { for (let p of preout) {
if (p.emit) p.speed *= fact; 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 });
}
}
} }
} }
@ -1612,6 +1637,12 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
// add offset points to total print // add offset points to total print
print.addPrintPoints(preout, output); print.addPrintPoints(preout, output);
// bridge layer fan change
if (preout.length && slice.isBridgeLayer) {
preout[0].fan = Math.round(fanSpeed / 3);
preout.peek().fan = fanSpeed;
}
// create next layer start point and annotate // create next layer start point and annotate
// with print time for previous layer (hack to // with print time for previous layer (hack to
// avoid changing return signature rn) // avoid changing return signature rn)

View file

@ -140,7 +140,8 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
fillOffset = lineWidth * fillOffsetMult, fillOffset = lineWidth * fillOffsetMult,
clipOffset = process.sliceSupportOffset, clipOffset = process.sliceSupportOffset,
sliceFillAngle = process.sliceFillAngle, sliceFillAngle = process.sliceFillAngle,
supportDensity = process.sliceSupportDensity; supportDensity = process.sliceSupportDensity,
{ sliceCompInner, sliceCompOuter } = process;
// override globals used by vopt() // override globals used by vopt()
isFlat = controller.lineType === "flat"; isFlat = controller.lineType === "flat";
@ -230,6 +231,8 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
useAssembly, useAssembly,
post: 'FDM', post: 'FDM',
post_args: { post_args: {
compInner: sliceCompInner,
compOuter: sliceCompOuter,
shellOffset, shellOffset,
fillOffset, fillOffset,
clipOffset, clipOffset,
@ -495,7 +498,6 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
// just the top/bottom special solid layers or range defined solid layers // just the top/bottom special solid layers or range defined solid layers
forSlices(0.15, 0.2, slice => { forSlices(0.15, 0.2, slice => {
let range = slice.params; let range = slice.params;
let spaceMult = slice.index === 0 ? process.firstLayerLineMult || 1 : 1;
let isBottom = slice.index < bottomLayers; let isBottom = slice.index < bottomLayers;
let isTop = topLayers && slice.index > slices.length - topLayers - 1; let isTop = topLayers && slice.index > slices.length - topLayers - 1;
let isTopBase = isTop && slice.index === slices.length - topLayers; let isTopBase = isTop && slice.index === slices.length - topLayers;
@ -503,7 +505,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)) && !isSynth; let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)) && !isSynth;
let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0;
if (solidWidth) { if (solidWidth) {
let fillSpace = fillSpacing * spaceMult * solidWidth; let fillSpace = fillSpacing * solidWidth;
doSolidLayerFill(slice, fillSpace, sliceFillAngle); doSolidLayerFill(slice, fillSpace, sliceFillAngle);
} }
if (slice.index === slices.length - 1) { if (slice.index === slices.length - 1) {
@ -752,8 +754,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
let params = slice.params || process; let params = slice.params || process;
let first = slice.index === 0; let first = slice.index === 0;
let solidWidth = params.sliceFillWidth || 1; let solidWidth = params.sliceFillWidth || 1;
let spaceMult = first ? params.firstLayerLineMult || 1 : 1; let fillSpace = fillSpacing * solidWidth;
let fillSpace = fillSpacing * spaceMult * solidWidth;
let solidMinArea = params.sliceSolidMinArea; let solidMinArea = params.sliceSolidMinArea;
doSolidsFill(slice, fillSpace, sliceFillAngle, solidMinArea, promises); doSolidsFill(slice, fillSpace, sliceFillAngle, solidMinArea, promises);
sliceFillAngle = (sliceFillAngle + 90.0) % 360; sliceFillAngle = (sliceFillAngle + 90.0) % 360;

Some files were not shown because too many files have changed in this diff Show more