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:
push:
branches: [ master ]
paths:
- 'docs/**'
workflow_dispatch:

View file

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

View file

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

View file

@ -3,6 +3,8 @@ name: Test NPM Production Build
on:
# Runs on pushes targeting the default branch
push:
paths:
- '!docs/**'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
@ -24,4 +26,4 @@ jobs:
run: npm run setup
- 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
.bache
.docusaurus
.env
alt
app.json
build
data
@ -9,7 +11,8 @@ jszip-esm.js
logs
mod
node_modules
pack
package-lock.json
three.js
tmp
pack
web/boot/bundle*

View file

@ -9,20 +9,11 @@ const appDir = path.join(usrDir, 'gapp');
const cnfDir = path.join(appDir, 'conf');
const logDir = path.join(appDir, 'logs');
const datDir = path.join(appDir, 'data');
const debug = process.argv.slice(2).map(v => v.replaceAll('-', '')).contains('debugg');
const devel = process.argv.slice(2).map(v => v.replaceAll('-', '')).contains('devel');
const debug = process.argv.slice(2).map(v => v.replaceAll('-', '')).indexOf('debugg') >= 0;
const devel = process.argv.slice(2).map(v => v.replaceAll('-', '')).indexOf('devel') >= 0;
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({
port: 5309,
apps: basDir,
@ -40,23 +31,31 @@ function createWindow() {
width: 1600,
height: 900,
webPreferences: {
// contextIsolation: true,
// nodeIntegration: true,
// sandbox: true,
// preload: undefined
// nodeIntegration: true,
// contextIsolation: false
// preload: path.join(__dirname, 'preload.js')
}
});
const { webContents } = mainWindow;
mainWindow.loadURL('http://localhost:5309/kiri');
// default normal url navigation or page opens to happen outside Electron
mainWindow.webContents.setWindowOpenHandler((details) => {
webContents.setWindowOpenHandler((details) => {
// console.log('EXTERNAL', details);
shell.openExternal(details.url);
return { action: 'deny' }
});
// 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);
if (url.endsWith('/kiri') || url.endsWith('/kiri/')) {
if (url.endsWith('/kiri') || url.endsWith('/kiri/')) {
return;
}
if (url.endsWith('/mesh') || url.endsWith('/mesh/')) {
@ -66,9 +65,13 @@ function createWindow() {
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) {
console.log("opening developer tools");
mainWindow.webContents.openDevTools();
webContents.openDevTools();
}
}
@ -77,9 +80,7 @@ app.on('ready', () => {
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
app.quit();
});
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 netdb = require('@gridspace/net-level-client');
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 code = {};
@ -31,6 +35,8 @@ let oversion;
let dversion;
let lastmod;
let logger;
let single;
let dryrun;
let debug;
let http;
let util;
@ -60,12 +66,15 @@ netdb.create = async function(map = {}) {
function init(mod) {
const ENV = mod.env;
const inElectron = isElectron || ENV.electron;
startTime = time();
lastmod = mod.util.lastmod;
logger = mod.log;
pre = ENV.pre || mod.meta.pre;
debug = ENV.debug || mod.meta.debug;
dryrun = ENV.dryrun;
single = ENV.single;
oversion = ENV.over || mod.meta.over;
crossOrigin = ENV.xorigin || mod.meta.xorigin || false;
serviceWorker = (ENV.service || mod.meta.service) !== false;
@ -74,14 +83,10 @@ function init(mod) {
dir = mod.dir;
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;
forceUseCache = ENV.cache ? true : false;
if (!ENV.electron) {
generateDevices();
}
if (pre) {
for (let [k,v] of Object.entries(productionMap)) {
productionMap[`${pre}${k}`] = `${pre}${v}`;
@ -93,6 +98,7 @@ function init(mod) {
req.url = req.url.substring(pre.length);
req.app.path = req.app.path.substring(pre.length);
req.app.ispre = true;
// console.log('MATCH1', dir, cookie, req.url);
return true;
}
let cookie = cookieValue(req.headers.cookie, "version") || '';
@ -101,9 +107,12 @@ function init(mod) {
vmatch = [ vmatch ];
}
if (vmatch.indexOf("*") >= 0) {
// console.log('MATCH2', dir, cookie, req.url);
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) => {
@ -122,6 +131,7 @@ function init(mod) {
mod.add(serveWasm);
mod.add(serveCode);
mod.add(fullpath({
"/boot" : redir((pre??"") + "/boot/", 301),
"/kiri" : redir((pre??"") + "/kiri/", 301),
"/mesh" : redir((pre??"") + "/mesh/", 301),
"/meta" : redir((pre??"") + "/meta/", 301),
@ -139,7 +149,8 @@ function init(mod) {
return "reload";
});
}
mod.add(rewriteHtmlVersion);
mod.add(addAppHeaders);
mod.static("/lib/", "alt");
mod.static("/lib/", "src");
mod.static("/obj/", "web/obj");
mod.static("/font/", "web/font");
@ -147,19 +158,26 @@ function init(mod) {
mod.static("/mesh/", "web/mesh");
mod.static("/moto/", "web/moto");
mod.static("/kiri/", "web/kiri");
mod.static("/boot/", "web/boot");
// module loader
function load_modules(root, force) {
lastmod(`${dir}/${root}`) && fs.readdirSync(`${dir}/${root}`).forEach(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}`);
if (!(stats.isDirectory() || stats.isSymbolicLink())) return;
if (util.isfile(PATH.join(mod.dir,modpath,".disable"))) return;
const isDebugMod = util.isfile(PATH.join(mod.dir,modpath,".debug"));
const isElectronMod = util.isfile(PATH.join(mod.dir,modpath,".electron"));
if (force || (ENV.electron && !isElectronMod)) return;
if (force || (!ENV.electron && isElectronMod && !isDebugMod)) return;
if (inElectron && !isElectronMod) {
logger.log({ skip_non_electron_mod: modpath });
return;
}
if (!inElectron && isElectronMod && !isDebugMod) {
logger.log({ skip_electron_mod: modpath });
return;
}
try {
loadModule(mod, modpath);
} catch (error) {
@ -189,6 +207,33 @@ function init(mod) {
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)
@ -253,11 +298,9 @@ function initModule(mod, file, dir) {
const [ path, file ] = [ ...arguments ];
if (lastmod(file)) {
code[path] = fs.readFileSync(file);
// console.log({ CODE: path, file });
} else if (lastmod(mod.dir + '/' + file)) {
const alt = mod.dir + '/' + file;
code[path] = fs.readFileSync(alt);
// console.log({ CODE: path, alt });
} else {
console.log({ MISSING_CODE: path, file });
}
@ -286,14 +329,14 @@ function initModule(mod, file, dir) {
const path = mod.dir + '/' + dir + '/' + file;
try {
const body = fs.readFileSync(path);
// console.log({ inject: code, file, opt });
if (debug && !single) logger.log({ inject: code, file, opt });
if (opt.first) {
append[code] = body.toString() + '\n' + append[code];
} else {
append[code] += body.toString() + '\n';
}
} catch (e) {
console.log({ missing_file: path, dir, mod });
logger.log({ missing_file: path, dir, mod });
}
},
onload(fn) {
@ -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) {
let mini = uglify.minify(code.toString(), {
compress: {
@ -524,7 +549,13 @@ function fixedmap(prefix, map) {
// HTTP 307 redirect
function redir(path, type) {
return (req, res, next) => {
http.redirect(res, path, type);
let query = req.url.split('?')[1];
let rpath = path;
if (query && path.indexOf('?') < 0) {
rpath += `?${query}`;
}
// console.log({ redir: req.url, to: path, query, new_path: rpath });
http.redirect(res, rpath, type);
}
}
@ -547,10 +578,12 @@ function cookieValue(cookie,key) {
}
}
function rewriteHtmlVersion(req, res, next) {
function addAppHeaders(req, res, next) {
if ([
"/kiri/",
"/mesh/",
"/lib/gpu/raster.js",
"/lib/gpu/raster-worker.js",
"/lib/mesh/work.js",
"/lib/kiri/run/worker.js",
"/lib/kiri/run/minion.js",
@ -559,36 +592,6 @@ function rewriteHtmlVersion(req, res, next) {
].indexOf(req.app.path) >= 0) {
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();
}

View file

@ -1,6 +1,6 @@
#!/bin/bash
tag=${1:-latest}
npm run clear-cache
npm run clean
./bin/build-upload win ${tag} && \
./bin/build-upload linux ${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;
if (process.env.SKIPNOT || electronPlatformName !== 'darwin') {
console.log(' ** skip notarizing:', electronPlatformName);
return;
}

View file

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

View file

@ -1,5 +1,6 @@
import { build, transform } from 'esbuild';
import fs from 'fs/promises';
import path from 'path';
import glob from 'fast-glob';
// 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}`);
}
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 = {
'duplicate-class-member': 'silent',
'duplicate-object-key': 'silent',
@ -66,12 +91,15 @@ const rec = {
logOverride,
minify: isProd, // false for dev, true for prod
platform: 'browser',
sourcemap: !isProd, // true for dev, false for prod
sourcemap: false,
target: 'es2020',
};
async function buildApp() {
try {
// Concatenate kiri devices
generateDevices();
// Bundle mesh main app
await build(Object.assign({}, rec, {
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',
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: [
[
'@docusaurus/preset-classic',

View file

@ -8,12 +8,12 @@ const config = {
useTabs: false,
bracketSpacing: true,
objectWrap: "preserve",
tabWidth: 4,
semi: false,
singleQuote: true,
tabWidth: 2,
semi: true,
singleQuote: false,
bracketSameLine: true,
arrowParens: "always",
endOfLine: "crlf"
endOfLine: "lf"
}
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
- 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 dev`
## 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`
- Clone the [repository](https://github.com/GridSpace/grid-apps) from https://github.com/GridSpace/grid-apps
- `npm run setup`
- `npm run dev`
- Open browser [http://localhost:8080/kiri](http://localhost:8080/kiri)
## 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)
- **File Extension** — Optional override for exported file type (e.g., `.nc`, `.ngc`, `.cnc`, `.cam`, `.gcode`, `.txt`)
---
## G-code Macros

View file

@ -35,6 +35,7 @@ description: Variable Substitutions and Expressions in GCode Macros
- \{pos_y\} = last output Y position
- \{pos_z\} = last output Z position
- \{progress\} = 0-100% state of print output
- \{remain_time\} = print time remaining in minutes
- \{tool\} | \{nozzle\} = current tool in use
- \{tool_count\} = number of tools 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",
},
items: [
"kiri-moto/CAM/interface",
"kiri-moto/CAM/processOpts",
"kiri-moto/CAM/machines",
"kiri-moto/CAM/tools",

View file

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

View file

@ -5,3 +5,14 @@
.navbar .navbar__items {
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`);
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 user_id = ((Math.random() * 0xfffffff) | 0).toString();
let init = false;
@ -53,7 +53,8 @@ self.kiri.load(api => {
if (socket.ws) {
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);
ws.onerror = (error) => {
socket.open = false;
@ -1288,7 +1289,7 @@ self.kiri.load(api => {
function send(filename, gcode, start) {
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);
url.searchParams.append('host', host);
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` faster ray intersect https://github.com/gkjohnson/three-mesh-bvh/
* `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` 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 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 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` support pillar top/bottom should conform to part
* `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` 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 are not cut to exact height
* `B` contour does not honor clip to stock
* `F` add lathe step down to eliminate the need for roughing

View file

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

View file

@ -9,27 +9,27 @@
![GitHub last commit](https://img.shields.io/github/last-commit/GridSpace/grid-apps)
![GitHub](https://img.shields.io/github/license/GridSpace/grid-apps)
# Community
[Discord](https://discord.com/invite/suyCCgr) -- Live Chat
[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
# 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)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/gridspace3d?locale.x=en_US)
# Documentation
[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/).
You can build and view the docs locally with:
```
git clone git@github.com:GridSpace/grid-apps.git
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
# Electron Builds (Desktop Binaries)
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.
# Linux / Mac Developers
## Testing Locally (with Docker)

View file

@ -2,6 +2,162 @@
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)
* major UI refactor

View file

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

View file

@ -4,4 +4,4 @@
import './pngjs.js';
// 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){
"use strict";

View file

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

View file

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

View file

@ -12,6 +12,8 @@ import { polygons } from '../geo/polygons.js';
import { newPoint } from '../geo/point.js';
import { config } from '../geo/base.js';
const epsilon = 10e-5;
function dval(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)
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
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
let zkey = p1.z,
area = Math.abs(base.util.area2(p1,p2,p3)) / 2;

View file

@ -60,13 +60,6 @@ export class Slope {
return this;
}
invert() {
this.dx = -this.dx;
this.dy = -this.dy;
this.angle = 180 - this.angle;
return this;
}
angleDiff(s2,sign) {
const n1 = this.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 { device, devices } from './devices.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 { noop, ajax, o2js, js2o } from './utils.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),
set: (key, val) => localSet(key, val),
setItem: (key, val) => localSet(key, val),
removeItem: (key) => localRemove(key)
},
tweak = {
line_precision(v) { api.work.config({ base: { clipperClean: v } }) },
@ -119,6 +120,11 @@ function localSet(key, val) {
return val;
}
function localRemove(key) {
let sloc = api.conf.get().local;
return delete sloc[key];
}
export const api = {
ajax,
beta,
@ -137,6 +143,7 @@ export const api = {
dialog,
doit,
event,
electron: navigator.userAgent.includes('Electron'),
feature,
function: functions,
group,
@ -156,7 +163,6 @@ export const api = {
o2js,
onkey,
platform,
probe,
process,
sdb: dataLocal,
selection,

View file

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

View file

@ -182,33 +182,33 @@ function normalize(settings) {
// auto field renaming on import
const renamed = {
roughingTool: "camRoughTool",
roughingSpindle: "camRoughSpindle",
roughingDown: "camRoughDown",
roughingOver: "camRoughOver",
roughingSpeed: "camRoughSpeed",
roughingPlunge: "camRoughPlunge",
roughingStock: "camRoughStock",
roughingPocket: "camRoughVoid",
roughingOn: "camRoughOn",
finishingTool: "camOutlineTool",
finishingSpindle: "camOutlineSpindle",
camWideCutout: "camOutlineWide",
drillDown: "camDrillDown",
drillDownSpeed: "camDrillDownSpeed",
drillDwell: "camDrillDwell",
drillingOn: "camDrillingOn",
drillLift: "camDrillLift",
drillSpindle: "camDrillSpindle",
drillTool: "camDrillTool",
finishingDown: "camOutlineDown",
finishingOver: "camContourOver",
finishingSpeed: "camOutlineSpeed",
finishingPlunge: "camOutlinePlunge",
finishingOn: "camOutlineOn",
finishingOver: "camContourOver",
finishingPlunge: "camOutlinePlunge",
finishingSpeed: "camOutlineSpeed",
finishingSpindle: "camOutlineSpindle",
finishingTool: "camOutlineTool",
finishingXOn: "camContourXOn",
finishingYOn: "camContourYOn",
drillTool: "camDrillTool",
drillSpindle: "camDrillSpindle",
drillDownSpeed: "camDrillDownSpeed",
drillDown: "camDrillDown",
drillDwell: "camDrillDwell",
drillLift: "camDrillLift",
drillingOn: "camDrillingOn",
camWideCutout: "camOutlineWide",
outputClockwise: "camConventional"
outputClockwise: "camConventional",
roughingDown: "camRoughDown",
roughingOn: "camRoughOn",
roughingOver: "camRoughOver",
roughingPlunge: "camRoughPlunge",
roughingPocket: "camRoughVoid",
roughingSpeed: "camRoughSpeed",
roughingSpindle: "camRoughSpindle",
roughingStock: "camRoughStock",
roughingTool: "camRoughTool",
};
export const conf = {
@ -258,95 +258,95 @@ export const conf = {
},
// process defaults FDM:Process
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",
ranges: [],
sliceAdaptive: false,
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,
sliceShells: 3,
sliceShellOrder: "in-out",
sliceLayerStart: "last",
sliceLayerStartX: 0,
sliceLayerStartY: 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,
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,
arcTolerance: 0,
antiBacklash: 0,
ranges: []
}
},
sla:{
@ -420,186 +420,187 @@ export const conf = {
},
// process defaults CAM:Process
p:{
processName: "default",
camLevelTool: 1000,
camLevelSpindle: 1000,
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,
camArcEnabled: false,
camArcResolution: 5,
camArcTolerance: 0.15,
camContourAngle: 85,
camContourLeave: 0,
camContourReduce: 2,
camContourBottom: false,
camContourBridge: 0,
camContourCurves: false,
camContourIn: false,
camContourLeave: 0,
camContourOver: 0.5,
camContourReduce: 2,
camContourSpeed: 1000,
camContourSpindle: 1000,
camContourTool: 1000,
camContourXOn: true,
camContourYOn: true,
camLatheTool: 1000,
camLatheSpindle: 1000,
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,
camConventional: false, // outputClockwise
camDepthFirst: false,
camDrillDown: 5,
camDrillDownSpeed: 250,
camDrillDwell: 250,
camDrillFromStockTop: false,
camDrillingOn: false,
camDrillLift: 2,
camDrillMark: false,
camDrillFromStockTop: false,
camDrillThru: 5,
camDrillPrecision: 1,
camDrillingOn: false,
camRegisterSpeed: 1000,
camRegisterThru: 5,
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,
camDrillSpindle: 1000,
camDrillThru: 5,
camDrillTool: 1006,
camEaseAngle: 10,
camOriginTop: true,
camZAnchor: "middle",
camZOffset: 0,
camZTop: 0,
camZBottom: 0,
camZClearance: 1,
camZThru: 0,
camEaseDown: false,
camExpertFast: false,
camFastFeed: 6000,
camFastFeedZ: 300,
camTolerance: 0,
camFirstZMax: false,
camFlatness: 0.001,
camContourBridge: 0,
camStockX: 5,
camStockY: 5,
camStockZ: 5,
camStockOffset: true,
camStockClipTo: false,
camStockIndexed: false,
camStockIndexGrid: true,
camIndexAxis: 0,
camFlipAxis: "X",
camFlipOther: "",
camForceZMax: false,
camFullEngage: 0.8,
camHelicalBottomFinish: true,
camHelicalClockwise: true,
camHelicalDown: 5,
camHelicalDownSpeed: 250,
camHelicalEntry: false,
camHelicalEntryOffset:0,
camHelicalForceStartAngle: false,
camHelicalOffset: "auto",
camHelicalOffsetOverride: 0,
camHelicalReverse: false,
camHelicalSpeed: 1000,
camHelicalSpindle: 1000,
camHelicalStartAngle: 0,
camHelicalThru: 0,
camHelicalTool: 1000,
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,
camOriginOffX: 0,
camOriginOffY: 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,
outputInvertY: false,
camExpertFast: false,
camTrueShadow: false,
camArcEnabled: false,
camArcTolerance: 0.15,
camArcResolution: 5,
camForceZMax: false,
camFirstZMax: false,
camToolInit: true,
camFullEngage: 0.8,
ops: [], // current ops
op2: [] // flip ops
processName: "default",
}
},
laser: {
@ -915,37 +916,37 @@ export const conf = {
devproc: {},
// application ui and control preferences (Q menu)
controller:{
view: null,
dark: false,
shiny: false,
drawer: false,
scrolls: true,
zoomSpeed: 1.0,
lineType: "path",
autoSave: true,
animesh: "800",
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,
scrolls: true,
shiny: true,
showOrigin: false,
showRulers: true,
showSpeeds: true,
freeLayout: true,
autoLayout: true,
spaceRandoX: false,
spaceLayout: 1,
edgeangle: 20,
units: "mm",
exportOcto: false,
exportGhost: false,
exportLocal: false,
exportThumb: false,
exportPreview: false,
detail: "50",
animesh: "800",
healMesh: false,
spaceRandoX: false,
threaded: true,
assembly: false,
ortho: false,
devel: false
units: "mm",
view: null,
webGPU: false,
zoomSpeed: 1.0,
},
// default hidden ui groups
hidden: {

View file

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

View file

@ -218,207 +218,6 @@ function exportGCodeDialog(gcode, sections, info, names) {
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() {
filename = $('print-filename').value;
api.util.download(gcode, filename + "." + fileext);
@ -463,8 +262,6 @@ function exportGCodeDialog(gcode, sections, info, names) {
let set = api.conf.get();
let fdm = MODE === MODES.FDM;
let octo = set.controller.exportOcto && MODE !== MODES.CAM;
let ghost = set.controller.exportGhost;
let local = set.controller.exportLocal;
let preview = set.controller.exportPreview;
$('code-preview-head').style.display = preview ? '' : 'none';
$('code-preview').style.display = preview ? '' : 'none';
@ -484,8 +281,6 @@ function exportGCodeDialog(gcode, sections, info, names) {
// persist fields when changed
bindField('octo-host', 'octo-host');
bindField('octo-apik', 'octo-apik');
bindField('grid-host', 'grid-host');
bindField('grid-apik', 'grid-apik');
// in cam mode, show zip file option
let downloadZip = $('print-zip');
@ -699,37 +494,11 @@ function exportGCodeDialog(gcode, sections, info, names) {
$('ophost').style.display = 'none';
$('opapik').style.display = 'none';
$('oph1nt').style.display = 'none';
$('send-to-gridhost').style.display = 'none';
}
octo_host.value = localGet('octo-host') || '';
octo_apik.value = localGet('octo-apik') || '';
} 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)
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) {
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.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.shiny = ui.shiny.checked;
control.showOrigin = ui.showOrigin.checked;
control.showRulers = ui.showRulers.checked;
control.autoLayout = ui.autoLayout.checked;
control.freeLayout = ui.freeLayout.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.assembly = ui.assembly.checked;
control.ortho = ui.ortho.checked;
control.devel = ui.devel.checked;
control.webGPU = ui.webGPU.checked;
space.view.setZoom(control.reverseZoom, control.zoomSpeed);
// platform.layout();
api.conf.save();
@ -904,7 +904,6 @@ function init_one() {
xany: $('mod-x-any'),
xsla: $('mod-x-sla'),
xlaser: $('mod-x-laser'),
local: $('mod-local'),
don8: $('mod-don8'),
any: $('mod-any'),
},
@ -1048,6 +1047,7 @@ function init_one() {
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}),
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"),
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}),
@ -1058,8 +1058,6 @@ function init_one() {
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}),
_____: 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}),
exportThumb: newBoolean(`Thumbnail`, booleanSave, {modes:FDM}),
exportPreview: newBoolean(`Code Preview`, booleanSave),
@ -1068,6 +1066,7 @@ function init_one() {
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}),
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')),
@ -1635,20 +1634,23 @@ function init_two() {
space.platform.setZOff(0.05);
// 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.showRulers.checked = control.showRulers;
ui.showSpeeds.checked = control.showSpeeds;
ui.freeLayout.checked = control.freeLayout;
ui.autoLayout.checked = control.autoLayout;
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.assembly.checked = control.assembly;
ui.ortho.checked = control.ortho;
ui.devel.checked = control.devel;
ui.webGPU.checked = control.webGPU;
lineTypeSave();
detailSave();
updateStats();
@ -1789,6 +1791,7 @@ function init_two() {
$('context-duplicate').onclick = duplicateSelection;
$('context-mirror').onclick = mirrorSelection;
$('context-layflat').onclick = () => { api.event.emit("tool.mesh.lay-flat") };
$('context-lefty').onclick = () => { api.event.emit("tool.mesh.lefty") };
$('context-setfocus').onclick = () => {
api.event.emit(
"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 = {
code: currentProcessCode,
get: currentProcessName
@ -784,43 +778,7 @@ function showHelpFile(local,then) {
}
function showLocal() {
showModal('local');
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}/`);
};
});
});
// deprecated
}
function setViewMode(mode) {

View file

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

View file

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

View file

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

View file

@ -49,6 +49,24 @@ function onLayFlatSelect() {
// 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() {
api.event.emit('tool.mesh.face-normal', lastface.normal);
// endIt();
@ -105,6 +123,12 @@ api.event.on('tool.mesh.lay-flat', () => {
startIt();
});
api.event.on('tool.mesh.lefty', () => {
opName = 'align y axis';
onDone = onLeftySelect;
startIt();
});
function scale() {
let cam = space.internals().camera;
let dist = cam.position.distanceTo(pmesh.position);

View file

@ -473,10 +473,12 @@ class Widget {
/**
* moves top of widget to given Z
* only non-zero in CAM mode
*
* @param {number} z position
* @param {boolean} cam mode
*/
setTopZ(z) {
setTopZ(z, cam) {
let mesh = this.mesh,
track = this.track,
mbb = mesh.getBoundingBox(),
@ -488,7 +490,7 @@ class Widget {
track.top = mbz;
}
// difference between top of stock/bounds and top of widget (cam mode)
track.tzoff = mbz - z;
track.tzoff = cam ? mbz - z : 0;
this._updateMeshPosition();
}
@ -503,8 +505,7 @@ class Widget {
}
_move(x, y, z, abs) {
let mesh = this.mesh,
mat = this.getMaterial(),
let mat = this.getMaterial(),
pos = this.track.pos;
// do not allow moves in pure slice view
if (!mat.visible) return;
@ -528,13 +529,10 @@ class Widget {
track = this.track,
tzoff = track.tzoff,
top = track.top,
tz = tzoff !== top ? -tzoff : 0,
tz = -tzoff,
{ x, y, z } = track.pos;
if (track.indexed) {
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;
z = top;
} else {
@ -782,9 +780,11 @@ class Widget {
}
if (set) {
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 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.renderOrder = -20;
mesh.add(this.outline);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,6 +9,7 @@ import { fdm_export } from './export.js';
// fil = filament diameter
// slice = slice height
function extrudePerMM(noz, fil, slice) {
// (ratio of nozzle to filament) * (ratio of slice to nozzle)
return ((Math.PI * util.sqr(noz / 2)) / (Math.PI * util.sqr(fil / 2))) * (slice / noz);
};

View file

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

View file

@ -79,6 +79,7 @@ export function menu() {
_____: newGroup(LANG.sw_menu, $('fdm-walls'), { modes:FDM, driven, hideable, separator, group:"fdm-walls" }),
sliceShells: newInput(LANG.sl_shel_s, { title:LANG.sl_shel_l, convert:toFloat }),
sliceLineWidth: newInput(LANG.sl_line_s, { title:LANG.sl_line_l, convert:toFloat, bound:bound(0,5) }),
sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,1.0)}),
separator: newBlank({ class:"set-sep", driven }),
sliceShellOrder: newSelect(LANG.sl_ordr_s, { title:LANG.sl_ordr_l}, "shell"),
sliceDetectThin: newSelect(LANG.ad_thin_s, { title: LANG.ad_thin_l, action: thinWallSave }, "thin"),
@ -86,19 +87,17 @@ export function menu() {
sliceZInterleave: newBoolean(LANG.ad_zint_s, onBooleanClick, {title:LANG.ad_zint_l, show:zIntShow}),
_____: newGroup(LANG.fs_menu, $('fdm-solid'), { modes:FDM, driven, hideable, separator, group:"fdm-solid" }),
sliceFillAngle: newInput(LANG.fi_angl_s, {title:LANG.fi_angl_l, convert:toFloat}),
sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,2.0)}),
sliceFillGrow: newInput(LANG.fi_grow_s, {title:LANG.fi_grow_l, convert:toFloat}),
sliceSolidMinArea: newInput(LANG.ad_msol_s, {title:LANG.ad_msol_l, convert:toFloat}),
_____: newGroup(LANG.fi_menu, $('fdm-fill'), { modes:FDM, driven, hideable, separator, group:"fdm-fill" }),
sliceFillType: newSelect(LANG.fi_type, {trigger}, "infill"),
sliceFillSparse: newInput(LANG.fi_pcnt_s, {title:LANG.fi_pcnt_l, convert:toFloat, bound:bound(0.0,1.0), show:hasInfill}),
sliceFillRepeat: newInput(LANG.fi_rept_s, {title:LANG.fi_rept_l, convert:toInt, bound:bound(1,10), show:fillIsLinear}),
separator: newBlank({ class:"set-sep", driven }),
sliceFillRate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt, bound:bound(0,500)}),
sliceSolidRate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt, bound:bound(0,500)}),
_____: newGroup(LANG.fh_menu, $('fdm-heat'), { modes:FDM, driven, hideable, separator, group:"fdm-heat" }),
outputTemp: newInput(LANG.ou_nozl_s, {title:LANG.ou_nozl_l, convert:toInt}),
outputBedTemp: newInput(LANG.ou_bedd_s, {title:LANG.ou_bedd_l, convert:toInt}),
separator: newBlank({ class:"set-sep", driven }),
outputDraftShield: newBoolean(LANG.fr_draf_s, onBooleanClick, {title:LANG.fr_draf_l, trigger, show:() => isNotBelt()}),
_____: newGroup(LANG.fc_menu, $('fdm-cool'), { modes:FDM, driven, hideable, separator, group:"fdm-cool" }),
outputFanLayer: newInput(LANG.ou_fanl_s, { title:LANG.ou_fanl_l, convert:toInt, bound:bound(0,255) }),
outputFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255)}),
@ -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}),
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}),
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}),
firstLayerFillRate: newInput(LANG.fl_frat_s, {title:LANG.fl_frat_l, convert:toFloat, show:isNotBelt}),
separator: newBlank({ class:"set-sep", driven, show:isNotBelt }),
firstLayerLineMult: newInput(LANG.fl_sfac_s, {title:LANG.fl_sfac_l, convert:toFloat, bound:bound(0.5,2), show:isNotBelt}),
firstLayerPrintMult: newInput(LANG.fl_mult_s, {title:LANG.fl_mult_l, convert:toFloat}),
separator: newBlank({ class:"set-sep", driven, show:isBelt }),
firstLayerBrim: newInput(LANG.fl_brim_s, {title:LANG.fl_brim_l, convert:toInt, show:isBelt}),
firstLayerBrimIn: newInput(LANG.fl_brin_s, {title:LANG.fl_brin_l, convert:toInt, show:isBelt}),
@ -154,7 +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() }),
separator: newBlank({ class:"set-sep", driven, show:isNotBelt }),
outputRaft: newBoolean(LANG.fr_nabl_s, onBooleanClick, {title:LANG.fr_nabl_l, trigger, show:() => isNotBelt()}),
outputDraftShield: newBoolean(LANG.fr_draf_s, onBooleanClick, {title:LANG.fr_draf_l, trigger, show:() => isNotBelt()}),
_____: newGroup(LANG.ou_menu, $('fdm-output'), { modes:FDM, driven, hideable, separator, group:"fdm-out" }),
outputFeedrate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt}),
outputFinishrate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt}),
@ -177,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() }}),
outputRetractDwell: newInput(LANG.ad_rdwl_s, {title:LANG.ad_rdwl_l, convert:toInt}),
outputMinSpeed: newInput(LANG.ad_mins_s, {title:LANG.ad_mins_l, convert:toFloat, bound:bound(1,200)}),
outputMaxFlowrate: newInput(LANG.ad_maxf_s, {title:LANG.ad_maxf_l, convert:toFloat, bound:bound(1,200)}),
outputPurgeTower: newInput(LANG.ad_purg_s, {title:LANG.ad_purg_l, convert:toInt, bound:bound(0,1000)}),
outputScarfLength: newInput(LANG.ad_scar_s, {title:LANG.ad_scar_l, convert:toFloat, bound:bound(0,1000)}),
outputShortPoly: newInput(LANG.ad_spol_s, {title:LANG.ad_spol_l, convert:toFloat, bound:bound(0,10000)}),
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)}),
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.
*/
export function slicePost(data, options) {
const { z, lines, groups } = data;
const { useAssembly, post_args, zIndexes } = options;
const { process, isSynth, vaseMode } = post_args;
const { shellOffset, fillOffset, clipOffset } = post_args;
const { groups, z } = data;
const { post_args, useAssembly, zIndexes } = options;
const { isSynth, process, vaseMode } = post_args;
const { compInner, compOuter, pump } = post_args;
const { clipOffset, fillOffset, shellOffset } = post_args;
if (options.pump) {
if (pump) {
let nugroups = [];
let pump = shellOffset / 10;
for (let p of groups) {
@ -41,6 +42,19 @@ export function slicePost(data, options) {
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) {
const process = post_args.process;
if (process.sliceSupportGrow > 0) {
@ -54,6 +68,7 @@ export function slicePost(data, options) {
delete data.groups;
return;
}
const index = zIndexes.indexOf(z);
const range = getRangeParameters(process, index);
// calculate fractional shells
@ -67,12 +82,9 @@ export function slicePost(data, options) {
let trg = shellFrac > 0.5 ? 1 : parts - 1;
sliceShells += rem >= trg ? 1 : 0;
}
const isFirst = index === 0;
const height = process.sliceHeight;
const spaceMult = isFirst ? process.firstLayerLineMult || 1 : 1;
const count = isSynth ? 1 : sliceShells;
const offset = shellOffset * spaceMult;
const fillOff = fillOffset * spaceMult;
const offset = shellOffset;
const fillOff = fillOffset;
const thinType = isSynth ? undefined : process.sliceDetectThin;
const nutops = [];
// co-locate shell processing with top generation in slicer
@ -129,7 +141,7 @@ function offset_default(params) {
}
const deltaBig = 160;
const deltSmall = 20;
const deltaSmall = 20;
// look for close points on adjacent segmented polys and merge
let test_polys = first !== last ? [ ...first, ...last ] : last;
@ -139,6 +151,8 @@ function offset_default(params) {
for (let poly of test_flats)
poly.forEachSegment((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

View file

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

View file

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

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