Merge branch 'rel-4.3' into improved-register

This commit is contained in:
bakedpotatolord 2025-07-21 09:23:53 -06:00
commit 333a965566
336 changed files with 19582 additions and 25184 deletions

View file

@ -22,8 +22,8 @@ jobs:
with: with:
node-version: '20' node-version: '20'
- name: Install dependencies - name: Setup Build Environment
run: npm install run: npm run setup
- name: Build Electron app - name: Build Electron app
env: env:

View file

@ -24,4 +24,4 @@ jobs:
run: npm run setup run: npm run setup
- name: dry run production build - name: dry run production build
run: npm run prod-dryrun run: npm run prod-dryrun

8
.gitignore vendored
View file

@ -1,13 +1,15 @@
.DS_Store .DS_Store
.docusaurus
.env .env
app.json app.json
build
data data
dist dist
jszip-esm.js
logs logs
mod mod
node_modules node_modules
package-lock.json package-lock.json
src/ext/three.js three.js
tmp tmp
.docusaurus pack
build

View file

@ -2,6 +2,7 @@ const { app, shell, session, BrowserWindow } = require('electron');
const path = require('path'); const path = require('path');
const server = require('@gridspace/app-server'); const server = require('@gridspace/app-server');
const pkgd = app.isPackaged;
const basDir = __dirname; const basDir = __dirname;
const usrDir = app.getPath("userData"); const usrDir = app.getPath("userData");
const appDir = path.join(usrDir, 'gapp'); const appDir = path.join(usrDir, 'gapp');
@ -11,6 +12,8 @@ const datDir = path.join(appDir, 'data');
const debug = process.argv.slice(2).map(v => v.replaceAll('-', '')).contains('debugg'); const debug = process.argv.slice(2).map(v => v.replaceAll('-', '')).contains('debugg');
const devel = process.argv.slice(2).map(v => v.replaceAll('-', '')).contains('devel'); const devel = process.argv.slice(2).map(v => v.replaceAll('-', '')).contains('devel');
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true;
// console.log({ appDir, usrDir, logDir, datDir, basDir }); // console.log({ appDir, usrDir, logDir, datDir, basDir });
// console.log({ argv: process.argv, debug, devel }); // console.log({ argv: process.argv, debug, devel });
// console.log({ // console.log({
@ -26,9 +29,9 @@ server({
data: datDir, data: datDir,
conf: cnfDir, conf: cnfDir,
logs: logDir, logs: logDir,
cache: path.join(basDir, "data", "cache"),
single: true, single: true,
electron: true, electron: true,
pkgd,
debug debug
}); });

709
app.js
View file

@ -12,35 +12,31 @@ const uglify = require('uglify-js');
const moment = require('moment'); const moment = require('moment');
const agent = require('express-useragent'); const agent = require('express-useragent');
const license = require_fresh('./src/moto/license.js'); const license = require_fresh('./src/moto/license.js');
const version = license.VERSION || "rogue"; const version = license.version || "rogue";
const netdb = require('@gridspace/net-level-client'); const netdb = require('@gridspace/net-level-client');
const PATH = require('path'); const PATH = require('path');
const fileCache = {}; const append = { mesh:'', kiri:'' };
const code_src = {};
const code = {}; const code = {};
const mods = {}; const mods = {};
const load = []; const load = [];
const synth = {};
const api = {}; const api = {};
const wrap = {};
let forceUseCache = false; let forceUseCache = false;
let serviceWorker = true; let serviceWorker = true;
let crossOrigin = false; let crossOrigin = false;
let setupFn; let setupFn;
let cacheDir;
let startTime; let startTime;
let oversion; let oversion;
let dversion; let dversion;
let lastmod; let lastmod;
let logger; let logger;
let debug; let debug;
let over;
let http; let http;
let util; let util;
let dir; let dir;
let log; let log;
let pre;
const EventEmitter = require('events'); const EventEmitter = require('events');
class AppEmitter extends EventEmitter {} class AppEmitter extends EventEmitter {}
@ -51,38 +47,24 @@ netdb.create = async function(map = {}) {
Object.assign(map, JSON.parse(fs.readFileSync(map.conf))); Object.assign(map, JSON.parse(fs.readFileSync(map.conf)));
} }
const client = new netdb(); const client = new netdb();
if (map.host && map.port) await client.open(map.host, map.port); try {
if (map.user && map.pass) await client.auth(map.user, map.pass); if (map.host && map.port) await client.open(map.host, map.port);
if (map.base) await client.use(map.base); if (map.user && map.pass) await client.auth(map.user, map.pass);
if (map.base) await client.use(map.base);
} catch (e) {
logger.log({ netdb_setup_error: e });
}
logger.log({ netdb: map.host, user: map.user, base: map.base }); logger.log({ netdb: map.host, user: map.user, base: map.base });
return client; return client;
}; };
// experimental wrapping of esm modules using embedded data urls
function wrap_module(buf, as) {
const base64 = Buffer.from(buf).toString('base64');
const path = as.split('.');
const exp = path.pop();
return [
`gapp.register("${path.join('.')}", [], (root, exports) => {`,
`const url = "data:text/javascript;base64,${base64}";`,
`exports({ ["${exp}"]: import(url) });`,
"});"
].join('\n');
}
function addonce(array, v) {
if (array.indexOf(v) < 0) {
array.push(v);
}
};
function init(mod) { function init(mod) {
const ENV = mod.env; const ENV = mod.env;
startTime = time(); startTime = time();
lastmod = mod.util.lastmod; lastmod = mod.util.lastmod;
logger = mod.log; logger = mod.log;
pre = ENV.pre || mod.meta.pre;
debug = ENV.debug || mod.meta.debug; debug = ENV.debug || mod.meta.debug;
oversion = ENV.over || mod.meta.over; oversion = ENV.over || mod.meta.over;
crossOrigin = ENV.xorigin || mod.meta.xorigin || false; crossOrigin = ENV.xorigin || mod.meta.xorigin || false;
@ -94,185 +76,26 @@ function init(mod) {
if (ENV.single) console.log({ cwd: process.cwd(), env: ENV }); if (ENV.single) console.log({ cwd: process.cwd(), env: ENV });
dversion = debug ? `_${version}` : version; dversion = debug ? `_${version}` : version;
cacheDir = ENV.cache || mod.util.datadir("cache");
if (ENV.single) logger.log({ cacheDir });
forceUseCache = ENV.cache ? true : false; forceUseCache = ENV.cache ? true : false;
const approot = PATH.join("main","gapp"); if (!ENV.electron) {
const refcache = {}; generateDevices();
const callstack = [];
let xxxx = false;
function find_refs(cache, path, ismod) {
let rec = refcache[path];
if (rec) {
let crec = cache[path];
if (!crec) {
cache[path] = rec;
for (let d of rec.deps) find_refs(cache, d);
for (let u of rec.uses) find_refs(cache, u);
}
return;
}
callstack.push(path);
rec = cache[path] = refcache[path] = {
uses: [],
deps: [ approot ]
};
let full = PATH.join(dir,"src",`${path}.js`);
try {
fs.lstatSync(full);
} catch (e) {
console.log({missing: full, callstack});
throw e;
}
// skip interrogating file if it's a module (external compacted)
if (ismod) {
wrap[`src/${path}.js`] = path.replaceAll('/','.');
return;
}
let lines = fs.readFileSync(full)
.toString()
.split('\n');
for (let line of lines) {
let arr, pos;
let upos = line.indexOf('// use:');
if (upos >= 0) {
arr = rec.uses;
pos = upos + 7;
}
let dpos = line.indexOf('// dep:');
if (dpos >= 0) {
arr = rec.deps;
pos = dpos + 7;
}
let mpos = line.indexOf('// mod:');
if (mpos >= 0) {
arr = rec.deps;
pos = mpos + 7;
}
if (upos >= 0 && dpos >= 0) {
console.log(`invalid line: ${line}`);
process.exit();
}
if (arr && pos >= 0) {
let path = line.substring(pos).trim().replace(/\./g,'/').trim();
addonce(arr, path);
find_refs(cache, path, mpos >= 0);
}
}
// if (xxxx) console.log({path, ...rec});
if (false) {
let seek = 'mesh/api';
if (rec.uses.indexOf(seek) >= 0 || rec.deps.indexOf(seek) >= 0) {
console.log({PULLS:seek, path});
}
}
callstack.pop();
} }
// return record position indicated by path if (pre) {
function pos(path, list) { for (let [k,v] of Object.entries(productionMap)) {
for (let i=0; i<list.length; i++) { productionMap[`${pre}${k}`] = `${pre}${v}`;
if (list[i].path === path) {
return i;
}
} }
console.trace(`not found: ${path}`);
process.exit();
}
function order_refs(cache) {
const recs = Object.entries(cache).map(entry => {
return { path: entry[0], deps: entry[1].deps }
}).sort((a,b) => {
return a.path === b.path ? 0 : a.path < b.path ? -1 : 1;
});
if (xxxx) console.log({ordering: recs});
// for each rec, ensure that dependencies are inserted before it
let lrec = recs.slice();
for (let rec of lrec) {
let { path, deps } = rec;
for (let dep of deps) {
let rpos = pos(path, recs);
let dpos = pos(dep, recs);
if (dpos > rpos) {
let drec = recs[dpos];
// remove old dep record
recs.splice(dpos, 1);
// insert dep before
recs.splice(rpos, 0, drec);
let nrpos = pos(path, recs);
let ndpos = pos(dep, recs);
let fail = nrpos != ndpos + 1;
// if (xxxx) { console.log({move: dep, dpos, before: path, rpos}); }
if (fail) {
console.log({move: dep, dpos, before: path, rpos, ndpos, nrpos, recs: recs.slice(0,10)});
process.exit();
}
}
}
}
if (xxxx) console.log({recs});
return recs.map(rec => rec.path);
}
// process script dependencies, expand paths
for (let [ key, val ] of Object.entries(script)) {
if (val.indexOf(approot) < 0) {
val = [ approot, ...val ];
}
const list = val.map(p => p.charAt(0) === '&' ? p.substring(1) : p);
const cache = {};
const roots = [];
// xxxx = key === "kiri_work";
// for each path in the list, find deps and add to list
for (let path of val) {
let fc = path.charAt(0);
if (fc === '@') {
continue;
}
if (fc === '#') {
continue;
}
if (fc === '&') {
path = path.substring(1);
addonce(roots, path);
}
find_refs(cache, path);
}
if (xxxx) console.log({ processing: key, val });
let refs = order_refs(cache).filter(p => roots.indexOf(p) < 0);
// remove paths that are in refs
let paths = list.filter(p => {
if (p.charAt(0) === '&') {
p = p.substring(1);
}
return refs.indexOf(p) < 0 && roots.indexOf(p) < 0;
});
// when dependency roots exist, re-write val array
if (roots.length) {
val = [...refs, ...paths, ...roots];
}
// val.splice(1, 0, ...roots);
if (xxxx) console.log({key, cache, refs, paths, roots, val});
script[key] = val.map(p => {
let fc = p.charAt(0);
if (fc === '@') return p;
if (fc === '#') {
fc = p.split('#');
let nupath = `src/${fc[1]}.js`;
wrap[nupath] = fc[1].replaceAll('/','.');
return nupath;
}
return `src/${p}.js`;
});
// console.log({script: key, files: script[key]});
} }
mod.on.test((req) => { mod.on.test((req) => {
let cookie = cookieValue(req.headers.cookie, "version") || undefined; if (req.app?.path?.startsWith(pre)) {
req.url = req.url.substring(pre.length);
req.app.path = req.app.path.substring(pre.length);
req.app.ispre = true;
return true;
}
let cookie = cookieValue(req.headers.cookie, "version") || '';
let vmatch = mod.meta.version || "*"; let vmatch = mod.meta.version || "*";
if (!Array.isArray(vmatch)) { if (!Array.isArray(vmatch)) {
vmatch = [ vmatch ]; vmatch = [ vmatch ];
@ -283,22 +106,30 @@ function init(mod) {
return vmatch.indexOf(cookie) >= 0; return vmatch.indexOf(cookie) >= 0;
}); });
mod.on.testv((version) => {
let vmatch = mod.meta.version || "*";
if (!Array.isArray(vmatch)) {
vmatch = [ vmatch ];
}
if (vmatch.indexOf("*") >= 0) {
return true;
}
return vmatch.indexOf(version) >= 0;
});
mod.add(handleSetup); mod.add(handleSetup);
mod.add(handleOptions); mod.add(handleOptions);
mod.add(handleWasm); mod.add(serveWasm);
mod.add(serveCode);
mod.add(fullpath({ mod.add(fullpath({
"/kiri" : redir("/kiri/", 301), "/kiri" : redir((pre??"") + "/kiri/", 301),
"/mesh" : redir("/mesh/", 301), "/mesh" : redir((pre??"") + "/mesh/", 301),
"/meta" : redir("/meta/", 301), "/meta" : redir((pre??"") + "/meta/", 301),
"/kiri/index.html" : redir("/kiri/", 301), "/kiri/index.html" : redir((pre??"") + "/kiri/", 301),
"/mesh/index.html" : redir("/mesh/", 301), "/mesh/index.html" : redir((pre??"") + "/mesh/", 301),
"/meta/index.html" : redir("/meta/", 301) "/meta/index.html" : redir((pre??"") + "/meta/", 301)
})); }));
mod.add(handleVersion); mod.add(handleVersion);
mod.add(prepath([
[ "/code/", handleCode ],
// [ "/wasm/", handleWasm ]
]));
mod.add(fixedmap("/api/", api)); mod.add(fixedmap("/api/", api));
if (debug) { if (debug) {
mod.static("/mod/", "mod"); mod.static("/mod/", "mod");
@ -309,29 +140,16 @@ function init(mod) {
}); });
} }
mod.add(rewriteHtmlVersion); mod.add(rewriteHtmlVersion);
mod.add((req, res, next) => { mod.static("/lib/", "src");
const path = req.gs.path.substring(1);
if (wrap[path]) {
const data = getCachedFile(path, file => {
console.log({ hot_wrap: file });
return fs.readFileSync(file);
});
res.setHeader('Content-Type', 'application/javascript; charset=UTF-8');
return res.end(data);
}
next();
});
mod.static("/src/", "src");
mod.static("/obj/", "web/obj"); mod.static("/obj/", "web/obj");
mod.static("/font/", "web/font"); mod.static("/font/", "web/font");
mod.static("/fon2/", "web/fon2"); mod.static("/fon2/", "web/fon2");
mod.static("/mesh/", "web/mesh"); mod.static("/mesh/", "web/mesh");
mod.static("/moto/", "web/moto"); mod.static("/moto/", "web/moto");
mod.static("/meta/", "web/meta");
mod.static("/kiri/", "web/kiri"); mod.static("/kiri/", "web/kiri");
// module loader
function load_modules(root, force) { function load_modules(root, force) {
// load modules
lastmod(`${dir}/${root}`) && fs.readdirSync(`${dir}/${root}`).forEach(mdir => { lastmod(`${dir}/${root}`) && fs.readdirSync(`${dir}/${root}`).forEach(mdir => {
const modpath = `${root}/${mdir}`; const modpath = `${root}/${mdir}`;
if (dir.charAt(0) === '.' && !ENV.single) return; if (dir.charAt(0) === '.' && !ENV.single) return;
@ -356,7 +174,7 @@ function init(mod) {
// load optional local modules // load optional local modules
load_modules('mods'); load_modules('mods');
// run loads injected by modules // run load functions injected by modules
while (load.length) { while (load.length) {
try { try {
load.shift()(); load.shift()();
@ -365,8 +183,12 @@ function init(mod) {
} }
} }
// runs after module loads / injects // minify appends in production mode
prepareScripts(); if (!debug) {
for (let key of Object.keys(append)) {
append[key] = minify(append[key]);
}
}
}; };
// either add module assets to path or require(init.js) // either add module assets to path or require(init.js)
@ -377,6 +199,9 @@ function loadModule(mod, dir) {
if (lastmod(`${mod.dir}/${dir}/.ignore`)) { if (lastmod(`${mod.dir}/${dir}/.ignore`)) {
return; return;
} }
// either load the module, or if it's not a module
// directory, serve the module as static content
// bound to the root
lastmod(`${mod.dir}/${dir}/init.js`) ? lastmod(`${mod.dir}/${dir}/init.js`) ?
initModule(mod, `./${dir}/init.js`, dir) : initModule(mod, `./${dir}/init.js`, dir) :
mod.static("/", `${mod.dir}/${dir}`); mod.static("/", `${mod.dir}/${dir}`);
@ -384,21 +209,19 @@ function loadModule(mod, dir) {
// load module and call returned function with helper object // load module and call returned function with helper object
function initModule(mod, file, dir) { function initModule(mod, file, dir) {
logger.log({module: file}); logger.log({ module: file, dir });
require_fresh(file)({ require_fresh(file)({
// express functions added here show up at "/api/" url root // express functions added here show up at "/api/" url root
api: api, api: api,
adm: { adm: {
reload: prepareScripts, setver(ver) { oversion = ver },
setver: (ver) => { oversion = ver }, crossOrigin(bool) { crossOrigin = bool }
crossOrigin: (bool) => { crossOrigin = bool }
}, },
events, events,
const: { const: {
args: {}, args: {},
meta: mod.meta, meta: mod.meta,
debug: debug, debug,
script: script,
moddir: dir, moddir: dir,
rootdir: mod.dir, rootdir: mod.dir,
version: oversion || version version: oversion || version
@ -411,54 +234,43 @@ function initModule(mod, file, dir) {
}, },
mod: mods, mod: mods,
util: { util: {
guid,
time,
log: logger.log, log: logger.log,
time: time, logger: log.new,
guid: guid,
mkdirs: util.mkdir, mkdirs: util.mkdir,
isfile: util.isfile,
confdir: util.confdir, confdir: util.confdir,
datadir: util.datadir, datadir: util.datadir,
lastmod: lastmod, lastmod: lastmod,
obj2string: obj2string, isfile: util.isfile,
string2obj: string2obj,
getCookieValue: cookieValue, getCookieValue: cookieValue,
logger: log.new obj2string(o) { return JSON.stringify(o) },
}, string2obj(s) { return JSON.parse(s) },
inject: (code, file, opt = {}) => {
let codelist = script[code];
if (!codelist) {
return logger.log(`inject missing target "${code}"`);
}
const path = `${dir}/${file}`;
codelist.push(path);
if (opt.cachever) {
cachever[path] = opt.cachever;
}
}, },
path: { path: {
any: arg => { mod.add(arg) }, any(arg) { mod.add(arg) },
pre: arg => { mod.add(prepath(arg)) }, code() {
map: arg => { mod.add(fixedmap(arg)) }, const [ path, file ] = [ ...arguments ];
full: arg => { mod.add(fullpath(arg)) }, if (lastmod(file)) {
static: (root, pre) => { code[path] = fs.readFileSync(file);
mod.static(pre || "/", root); // console.log({ CODE: path, file });
}, } else if (lastmod(mod.dir + '/' + file)) {
code: (endpoint, path) => { const alt = mod.dir + '/' + file;
let fpath = PATH.join(mod.dir, path); code[path] = fs.readFileSync(alt);
if (debug) { // console.log({ CODE: path, alt });
code[endpoint] = fs.readFileSync(fpath);
} else { } else {
code[endpoint] = minify(fpath); console.log({ MISSING_CODE: path, file });
} }
code_src[endpoint] = {
endpoint,
path: path,
mod: lastmod(fpath)
};
}, },
full(arg) { mod.add(fullpath(arg)) },
map(arg) { mod.add(fixedmap(arg)) },
pre(arg) { mod.add(prepath(arg)) },
redir: redir, redir: redir,
remap: remap, remap: remap,
setup: fn => { setupFn = fn } setup(fn) { setupFn = fn },
static(root, pre) {
mod.static(pre || "/", root);
},
}, },
handler: { handler: {
addCORS: addCorsHeaders, addCORS: addCorsHeaders,
@ -470,66 +282,29 @@ function initModule(mod, file, dir) {
ws: { ws: {
register: mod.wss register: mod.wss
}, },
onload: (fn) => { inject(code, file, opt = {}) {
const path = mod.dir + '/' + dir + '/' + file;
try {
const body = fs.readFileSync(path);
// console.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 });
}
},
onload(fn) {
load.push(fn); load.push(fn);
}, },
onexit: (fn) => { onexit(fn) {
mod.on.exit(fn); mod.on.exit(fn);
} }
}); });
} }
const script = {
kiri : [
"@devices",
"kiri/ui",
"&main/kiri",
"&kiri/lang-en"
],
kiri_work : [
"kiri-run/worker",
"&main/kiri",
],
kiri_pool : [
"&kiri-run/minion",
"&main/kiri",
],
engine : [
"&kiri-run/engine",
"&main/kiri",
],
frame : [
"kiri-run/frame"
],
meta : [
"main/meta",
],
mesh : [
"&main/mesh"
],
mesh_work : [
"&mesh/work"
],
mesh_pool : [
"&mesh/pool"
],
cache : [
"moto/license",
"main/service",
],
service : [
"moto/license",
"moto/service"
]
};
// prevent caching of specified modules
const cachever = {};
function promise(resolve, reject) {
return new Promise(resolve, reject);
}
function rval() { function rval() {
return (Math.round(Math.random()*0xffffffff)).toString(36); return (Math.round(Math.random()*0xffffffff)).toString(36);
} }
@ -542,14 +317,6 @@ function time() {
return Date.now(); return Date.now();
} }
function obj2string(o) {
return JSON.stringify(o);
}
function string2obj(s) {
return JSON.parse(s);
}
function handleSetup(req, res, next) { function handleSetup(req, res, next) {
if (setupFn) { if (setupFn) {
setupFn(req, res, next); setupFn(req, res, next);
@ -558,14 +325,37 @@ function handleSetup(req, res, next) {
} }
} }
const productionMap = {
'/lib/mesh/work.js' : '/lib/pack/mesh-work.js',
'/lib/main/mesh.js' : '/lib/pack/mesh-main.js',
'/lib/main/kiri.js' : '/lib/pack/kiri-main.js',
'/lib/kiri/run/engine.js' : '/lib/pack/kiri-eng.js',
'/lib/kiri/run/minion.js' : '/lib/pack/kiri-pool.js',
'/lib/kiri/run/worker.js' : '/lib/pack/kiri-work.js',
};
const redirList = [
"/kiri/",
"/mesh/"
];
function handleVersion(req, res, next) { function handleVersion(req, res, next) {
let vstr = oversion || dversion || version; let vstr = oversion || dversion || version;
if (["/kiri/","/mesh/","/meta/"].indexOf(req.app.path) >= 0 && req.url.indexOf(vstr) < 0) { if (!req.app.ispre && redirList.indexOf(req.app.path) === 0 && req.url.indexOf(vstr) < 0) {
if (req.url.indexOf("?") > 0) { if (req.url.indexOf("?") > 0) {
return http.redirect(res, `${req.url},ver:${vstr}`); return http.redirect(res, `${req.url},ver:${vstr}`);
} else { } else {
return http.redirect(res, `${req.url}?ver:${vstr}`); return http.redirect(res, `${req.url}?ver:${vstr}`);
} }
} else if (!debug) {
// in production serve packed bundles
let { path } = req.app;
let mapped = productionMap[path];
if (mapped) {
req.url = mapped;
addCorsHeaders(req, res);
}
next();
} else { } else {
next(); next();
} }
@ -586,7 +376,20 @@ function handleOptions(req, res, next) {
} }
} }
function handleWasm(req, res, next) { function serveCode(req, res, next) {
let { path } = req.app;
if (path.startsWith("/code/")) {
path = path.split('/')[2].split('.')[0];
if (code[path]) {
res.setHeader('Content-Type', 'application/javascript');
res.setHeader('Cache-Control', 'public, max-age=600');
return res.end(code[path]);
}
}
next();
}
function serveWasm(req, res, next) {
let file = req.app.path.split('/').pop(); let file = req.app.path.split('/').pop();
let ext = (file || '').split('.')[1]; let ext = (file || '').split('.')[1];
let path = PATH.join(dir,"src","wasm",file); let path = PATH.join(dir,"src","wasm",file);
@ -609,61 +412,9 @@ function handleWasm(req, res, next) {
} }
} }
function handleCode(req, res, next) {
let key = req.app.path.split('/')[2].split('.')[0],
ck = code_src[key],
js = code[key];
if (!js) {
return http.reply404(req, res);
}
if (ck) {
let mpath = `${dir}/${ck.path}`;
let mod = lastmod(mpath);
if (mod > ck.mod) {
if (debug) {
js = code[ck.endpoint] = fs.readFileSync(mpath);
} else {
js = code[ck.endpoint] = minify(mpath);
}
ck.mod = mod;
}
}
addCorsHeaders(req, res);
serveCode(req, res, {
code: js,
mtime: startTime
});
}
function serveCode(req, res, code) {
if (code.deny) {
return http.reply404(req, res);
}
let imd = ifModifiedDate(req);
if (imd && code.mtime <= imd && !code.nocache) {
res.writeHead(304, "Not Modified");
res.end();
return;
}
let cacheControl = code.nocache ?
'private, max-age=0' :
'public, max-age=600';
res.writeHead(200, {
'Content-Type': 'application/javascript',
'Cache-Control': cacheControl,
'Last-Modified': new Date(code.mtime).toGMTString(),
});
res.end(code.code);
}
// pack/concat device script strings to inject into /code/ scripts // pack/concat device script strings to inject into /code/ scripts
function generateDevices() { function generateDevices() {
let root = PATH.join(dir,"src","kiri-dev"); let root = PATH.join(dir,"src","kiri","dev");
let devs = {}; let devs = {};
fs.readdirSync(root).forEach(type => { fs.readdirSync(root).forEach(type => {
let map = devs[type] = devs[type] || {}; let map = devs[type] = devs[type] || {};
@ -674,130 +425,12 @@ function generateDevices() {
map[deviceName] = JSON.parse(fs.readFileSync(PATH.join(root,type,device))); map[deviceName] = JSON.parse(fs.readFileSync(PATH.join(root,type,device)));
}); });
}); });
synth.devices = `self.devices = ${JSON.stringify(devs)};`; 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};`);
} }
// pack/concat code modules served under "/code/" function minify(code) {
function prepareScripts() {
generateDevices();
for (let key of Object.keys(script)) {
code[key] = concatCode(script[key]);
}
}
function concatCode(array) {
let code = [];
let direct = array.filter(f => f.charAt(0) !== '@');
let inject = array.filter(f => f.charAt(0) === '@').map(f => f.substring(1));
synth.inject = "/* injection point */";
// in debug mode, the script should load dependent
// scripts instead of serving a complete bundle
if (debug) {
const code = [
oversion ? `self.debug_version='${oversion}';self.enable_service=${serviceWorker};` : '',
'self.debug=true;',
'(function() { let load = [ '
];
direct.forEach(file => {
const vers = cachever[file] || oversion || dversion || version;
code.push(`"/${file.replace(/\\/g,'/')}?${vers}",`);
});
code.push([
']; function load_next() {',
'let file = load.shift();',
'if (!file) return;',
'if (!self.document) { importScripts(file); return load_next() }',
'let s = document.createElement("script");',
's.type = "text/javascript";',
's.src = file;',
's.onload = load_next;',
'document.head.appendChild(s);',
'} load_next(); })();'
].join('\n'));
inject.forEach(key => {
code.push(synth[key]);
});
return code.join('\n');
}
direct.forEach(file => {
let cached = getCachedFile(file, path => {
return minify(PATH.join(dir,file));
});
if (oversion) {
cached = `self.debug_version='${oversion}';self.enable_service=${serviceWorker};` + cached;
}
code.push(cached);
});
inject.forEach(key => {
code.push(synth[key]);
});
return code.join('');
}
function getCachedFile(file, fn) {
let filePath = PATH.join(dir,file);
let cachePath = cacheDir + PATH.sep + file
.replace(/\//g,'_')
.replace(/\\/g,'_')
.replace(/:/g,'_'),
cached = fileCache[filePath],
now = time();
if (cached) {
if (now - cached.lastcheck > 60000) {
let smod = lastmod(filePath),
cmod = cached.mtime;
if (!smod) {
throw "missing source file";
}
if (smod > cmod) {
cached = null;
} else {
cached.lastcheck = now;
}
}
}
if (!cached) {
let smod = lastmod(filePath),
cmod = lastmod(cachePath),
cacheData;
if (cmod >= smod || (forceUseCache && cmod)) {
cacheData = fs.readFileSync(cachePath);
} else {
logger.log({update_cache:filePath});
cacheData = fn(filePath);
// console.log(`NEW_CACHE_FILE: ${cachePath}`);
fs.writeFileSync(cachePath, cacheData);
}
if (wrap[file]) {
// console.log('WRAP', filePath, cacheData.length);
cacheData = wrap_module(cacheData, wrap[file]);
}
cached = {
data: cacheData,
mtime: cmod || now,
lastcheck: now
};
fileCache[filePath] = cached;
}
// console.log('[*]', filePath, cached.data.length);
return cached.data;
}
function minify(path) {
let code = fs.readFileSync(path);
let mini = uglify.minify(code.toString(), { let mini = uglify.minify(code.toString(), {
compress: { compress: {
merge_vars: false, merge_vars: false,
@ -834,10 +467,10 @@ function addCorsHeaders(req, res) {
if (req.headers['access-control-request-private-network'] === 'true') { if (req.headers['access-control-request-private-network'] === 'true') {
res.setHeader('Access-Control-Allow-Private-Network', 'true'); res.setHeader('Access-Control-Allow-Private-Network', 'true');
} }
if (!crossOrigin) { // if (!crossOrigin) {
res.setHeader("Cross-Origin-Opener-Policy", 'same-origin'); res.setHeader("Cross-Origin-Opener-Policy", 'same-origin');
res.setHeader("Cross-Origin-Embedder-Policy", 'require-corp'); res.setHeader("Cross-Origin-Embedder-Policy", 'require-corp');
} // }
res.setHeader("Allow", "GET,POST,OPTIONS"); res.setHeader("Allow", "GET,POST,OPTIONS");
} }
@ -890,7 +523,9 @@ function fixedmap(prefix, map) {
// HTTP 307 redirect // HTTP 307 redirect
function redir(path, type) { function redir(path, type) {
return (req, res, next) => http.redirect(res, path, type); return (req, res, next) => {
http.redirect(res, path, type);
}
} }
// mangle request path // mangle request path
@ -913,26 +548,44 @@ function cookieValue(cookie,key) {
} }
function rewriteHtmlVersion(req, res, next) { function rewriteHtmlVersion(req, res, next) {
if (["/kiri/","/mesh/","/meta/","/kiri/engine.html","/kiri/frame.html"].indexOf(req.app.path) >= 0) { if ([
"/kiri/",
"/mesh/",
"/lib/mesh/work.js",
"/lib/kiri/run/worker.js",
"/lib/kiri/run/minion.js",
"/lib/kiri/run/engine.js",
"/lib/kiri/run/frame.js"
].indexOf(req.app.path) >= 0) {
addCorsHeaders(req, res); addCorsHeaders(req, res);
let real_write = res.write; }
let real_end = res.end; if ([
let mlen = '{{version}}'.length; "/lib/main/kiri.js",
let vstr = oversion || dversion || version; "/lib/main/mesh.js"
if (vstr.length < mlen) { ].indexOf(req.app.path) >= 0) {
vstr = vstr.padStart(mlen,0); addCorsHeaders(req, res);
} else if (vstr.length > mlen) { // const data = append[req.app.path.split('/')[3].split('.')[0]];
vstr = vstr.substring(0,mlen); const data = append[req.app.path.split('.')[0].split('/').pop()];
}
res.write = function() { if (!data) return next();
arguments[0] = arguments[0].toString().replace(/{{version}}/g,vstr); if (debug) logger.log({ append: req.app.path, data: data.length });
real_write.apply(res, arguments);
const real_write = res.write;
const real_end = res.end;
let body = '';
res.write = function (chunk, encoding) {
body += chunk.toString(encoding);
}; };
res.end = function() {
if (arguments[0]) { res.end = function (chunk, encoding) {
arguments[0] = arguments[0].toString().replace(/{{version}}/g,vstr); if (chunk) {
body += chunk.toString(encoding);
} }
real_end.apply(res, arguments); body += data;
res.setHeader('Content-Length', Buffer.byteLength(body));
real_write.call(res, body);
real_end.call(res);
}; };
} }

View file

@ -30,8 +30,8 @@ fs.copySync("mods", modsTmp, { dereference: true, filter:(src,dst) => {
} }); } });
// create minified asset cache // create minified asset cache
server({ // server({
electron: true, // electron: true,
dryrun: true, // dryrun: true,
single: true // single: true
}); // });

106
bin/esbuild.config.mjs Normal file
View file

@ -0,0 +1,106 @@
import { build, transform } from 'esbuild';
import fs from 'fs/promises';
import glob from 'fast-glob';
// Get build mode from command line argument
const mode = process.argv[2] || 'dev';
const isProd = mode === 'prod';
console.log(`Building in ${mode} mode...`);
const MESH_OUTFILE = 'src/pack/mesh-main.js';
const MESH_EXTRAS = [ ];
const KIRI_OUTFILE = 'src/pack/kiri-main.js';
const KIRI_EXTRAS = [ ];
async function appendExtraModules(extras, outfile, minify = false) {
const files = await glob(extras);
if (files.length === 0) return;
const transformed = await Promise.all(
files.map(async (file) => {
const code = await fs.readFile(file, 'utf8');
if (!minify) return code;
const result = await transform(code, {
minify: true,
loader: 'js',
target: 'es2020',
});
return result.code;
})
);
await fs.appendFile(outfile, transformed.join(''));
console.log(`Appended ${files.length} module(s) to ${outfile}`);
}
const logOverride = {
'duplicate-class-member': 'silent',
'duplicate-object-key': 'silent',
'direct-eval': 'silent'
};
const rec = {
bundle: true,
define: { 'process.env.NODE_ENV': `"${mode}"` },
external: ['module'],
format: 'esm',
logOverride,
minify: isProd, // false for dev, true for prod
platform: 'browser',
sourcemap: !isProd, // true for dev, false for prod
target: 'es2020',
};
async function buildApp() {
try {
// Bundle mesh main app
await build(Object.assign({}, rec, {
entryPoints: [ 'src/main/mesh.js' ],
outfile: MESH_OUTFILE,
}));
appendExtraModules(MESH_EXTRAS, MESH_OUTFILE, isProd);
// Bundle mesh worker
await build(Object.assign({}, rec, {
entryPoints: [ 'src/mesh/work.js' ],
outfile: 'src/pack/mesh-work.js',
}));
// Bundle kiri main app
await build(Object.assign({}, rec, {
entryPoints: [ 'src/main/kiri.js' ],
outfile: KIRI_OUTFILE,
}));
appendExtraModules(KIRI_EXTRAS, KIRI_OUTFILE, isProd);
// Bundle kiri worker
await build(Object.assign({}, rec, {
entryPoints: [ 'src/kiri/run/worker.js' ],
outfile: 'src/pack/kiri-work.js',
}));
// Bundle kiri minion
await build(Object.assign({}, rec, {
entryPoints: [ 'src/kiri/run/minion.js' ],
outfile: 'src/pack/kiri-pool.js',
}));
// Bundle kiri engine
await build(Object.assign({}, rec, {
entryPoints: [ 'src/kiri/run/engine.js' ],
outfile: 'src/pack/kiri-eng.js',
}));
console.log(`Build completed successfully in ${mode} mode!`);
} catch (error) {
console.error('Build failed:', error);
process.exit(1);
}
}
buildApp();

View file

@ -5,14 +5,14 @@ const path = require('path');
async function main() { async function main() {
console.log('npm pre running'); console.log('npm pre running');
const links = fs.readFileSync("links.csv") const links = fs.readFileSync("conf/links.csv")
.toString() .toString()
.trim() .trim()
.split('\n') .split('\n')
.map(line => line.trim()) .map(line => line.trim())
.map(line => line.split(',').map(v => v.trim())); .map(line => line.split(',').map(v => v.trim()));
if (os.platform() === 'win32') if (os.platform() === 'win32') {
// convert links to the contents of the files/directories they reference // convert links to the contents of the files/directories they reference
for (let [link, target] of links) { for (let [link, target] of links) {
const absoluteTarget = path.resolve(path.dirname(link), target); const absoluteTarget = path.resolve(path.dirname(link), target);
@ -24,6 +24,9 @@ async function main() {
console.error(`Error creating symlink: ${link} -> ${absoluteTarget}`, err); console.error(`Error creating symlink: ${link} -> ${absoluteTarget}`, err);
} }
} }
} else {
console.log('skipping symlink conversion');
}
} }
main().catch(err => console.error('Error', err)); main().catch(err => console.error('Error', err));

3
bin/mk-links.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/bash
find src/ web/ -type l | while read link; do echo "$link,$(readlink "$link")"; done > conf/links.csv

View file

@ -0,0 +1,7 @@
import * as JSZip from "../node_modules/jszip/dist/jszip.js";
export default JSZip;
export {
JSZip
};

38
bin/webpack-jszip-esm.js Normal file
View file

@ -0,0 +1,38 @@
const path = require('path');
module.exports = {
mode: 'production',
entry: path.resolve(__dirname, './webpack-jszip-bundle.js'),
output: {
path: path.resolve(__dirname, '../src/ext'),
filename: 'jszip-esm.js',
library: {
type: 'module'
},
module: true
},
experiments: {
outputModule: true
},
resolve: {
extensions: ['.mjs', '.js'],
},
module: {
rules: [
{
test: /\.m?js$/,
resolve: {
fullySpecified: false,
},
},
],
},
optimization: {
minimize: false
},
performance: {
hints: false,
maxAssetSize: 2 * 1024 * 1024,
maxEntrypointSize: 2 * 1024 * 1024,
},
};

View file

@ -8,9 +8,16 @@ import { LineGeometry } from '../node_modules/three/examples/jsm/lines/LineGeome
import { LineSegments2 } from '../node_modules/three/examples/jsm/lines/LineSegments2.js'; import { LineSegments2 } from '../node_modules/three/examples/jsm/lines/LineSegments2.js';
import { LineSegmentsGeometry } from '../node_modules/three/examples/jsm/lines/LineSegmentsGeometry.js'; import { LineSegmentsGeometry } from '../node_modules/three/examples/jsm/lines/LineSegmentsGeometry.js';
import * as MeshBVHLib from '../node_modules/three-mesh-bvh/build/index.module.js';
export { export {
THREE, SVGLoader, BufferGeometryUtils, THREE,
SVGLoader,
BufferGeometryUtils,
LineMaterial, LineMaterial,
Line2, LineGeometry, Line2,
LineSegments2, LineSegmentsGeometry LineGeometry,
}; LineSegments2,
LineSegmentsGeometry,
MeshBVHLib
};

View file

@ -1,15 +1,18 @@
const TerserPlugin = require("terser-webpack-plugin");
const path = require('path'); const path = require('path');
module.exports = { module.exports = {
mode: 'production', mode: 'production',
entry: "./bin/webpack-three-bundle.js", entry: path.resolve(__dirname, './webpack-three-bundle.js'),
output: { output: {
path: path.resolve('src/ext'), path: path.resolve(__dirname, '../src/ext'),
filename: 'three.js', filename: 'three.js',
library: 'ThreeBundle', library: {
libraryTarget: 'umd', type: 'module'
globalObject: 'this' },
module: true
},
experiments: {
outputModule: true
}, },
resolve: { resolve: {
extensions: ['.mjs', '.js'], extensions: ['.mjs', '.js'],
@ -25,21 +28,11 @@ module.exports = {
], ],
}, },
optimization: { optimization: {
minimize: false, minimize: false
minimizer: [
new TerserPlugin({
extractComments: false,
terserOptions: {
format: {
comments: false,
},
},
}),
],
}, },
performance: { performance: {
hints: false, hints: false,
maxAssetSize: 2 * 1024 * 1024, maxAssetSize: 2 * 1024 * 1024,
maxEntrypointSize: 2 * 1024 * 1024, maxEntrypointSize: 2 * 1024 * 1024,
}, },
}; };

View file

@ -5,7 +5,6 @@ src/ext/earcut.js,../../node_modules/earcut/src/earcut.js
src/ext/tween.js,../../node_modules/@tweenjs/tween.js/src/Tween.js src/ext/tween.js,../../node_modules/@tweenjs/tween.js/src/Tween.js
src/ext/jszip.js,../../node_modules/jszip/dist/jszip.js src/ext/jszip.js,../../node_modules/jszip/dist/jszip.js
src/wasm/manifold.wasm,../../node_modules/manifold-3d/manifold.wasm src/wasm/manifold.wasm,../../node_modules/manifold-3d/manifold.wasm
src/kiri-dev/fdm/GridBot.Two.json,GridBot.One.json
src/kiri/lang-en.js,../../web/kiri/lang/en.js src/kiri/lang-en.js,../../web/kiri/lang/en.js
web/fon2,../node_modules/bootstrap-icons/font/ web/fon2,../node_modules/bootstrap-icons/font/
web/kiri/lang/pl.js,pl-pl.js web/kiri/lang/pl.js,pl-pl.js
1 src/ext/gerber.js ../../node_modules/@tracespace/parser/umd/parser.js
5 src/ext/tween.js ../../node_modules/@tweenjs/tween.js/src/Tween.js
6 src/ext/jszip.js ../../node_modules/jszip/dist/jszip.js
7 src/wasm/manifold.wasm ../../node_modules/manifold-3d/manifold.wasm
src/kiri-dev/fdm/GridBot.Two.json GridBot.One.json
8 src/kiri/lang-en.js ../../web/kiri/lang/en.js
9 web/fon2 ../node_modules/bootstrap-icons/font/
10 web/kiri/lang/pl.js pl-pl.js

View file

@ -25,7 +25,7 @@ This is a community driven project, and we welcome any contributions you'd like
- Open the developer console - Open the developer console
- Run the following code: `kiri.api.conf.get().device` - Run the following code: `kiri.api.conf.get().device`
- Right click on the object and select `Copy object` - Right click on the object and select `Copy object`
- Make a new file in the `src/kiri-dev/<mode>` directory, with the name of your machine, no spaces or special characters, and a `.json` extension. - Make a new file in the `src/kiri/dev/<mode>` directory, with the name of your machine, no spaces or special characters, and a `.json` extension.
- Paste the copied object into the new file, and save it. - Paste the copied object into the new file, and save it.
- Publish your changes to a git repo - Publish your changes to a git repo
- Submit a [pull request](https://github.com/GridSpace/grid-apps/compare) - Submit a [pull request](https://github.com/GridSpace/grid-apps/compare)

View file

@ -2,9 +2,8 @@ self.kiri.load(api => {
console.log('BAMBU MODULE RUNNING'); console.log('BAMBU MODULE RUNNING');
const { kiri, moto } = self; const { uc: ui } = api;
const { ui } = kiri; const { $, h } = api.web;
const h = moto.webui;
const defams = ";; DEFINE BAMBU-AMS "; const defams = ";; DEFINE BAMBU-AMS ";
const readonly = true; const readonly = true;
const stock_colors = Object.values({ const stock_colors = Object.values({
@ -565,7 +564,7 @@ self.kiri.load(api => {
function monitoring() { function monitoring() {
let mon = selected?.rec?.serial ?? ''; let mon = selected?.rec?.serial ?? '';
ui.setVisible($('bbl_connect'), select.value !== '' && monitors.indexOf(mon) < 0); ui.setVisible($('bbl_connect'), select?.value && monitors.indexOf(mon) < 0);
return mon ? true : false; return mon ? true : false;
} }
@ -661,7 +660,7 @@ self.kiri.load(api => {
} }
}); });
api.event.on("init-done", function() { api.event.on("load-done", function() {
if (init) { if (init) {
return; return;
} }

View file

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

View file

@ -2,8 +2,6 @@
## `C` cosmetic, `F` functional, `P` performance, `B` bug fix ## `C` cosmetic, `F` functional, `P` performance, `B` bug fix
* `F` experiment with structuredClone(), SharedArrayBuffer, createImageBitmap()
* `F` test #private fields, array.at() for negative indices
* `F` option to hide platform when viewing from below * `F` option to hide platform when viewing from below
# Kiri:Moto # Kiri:Moto
@ -14,7 +12,6 @@
* `P` refactor vertex replacement and widget update matrix tracking * `P` refactor vertex replacement and widget update matrix tracking
* `P` duplicate objects should share same slice data unless rotated or scaled * `P` duplicate objects should share same slice data unless rotated or scaled
* `P` allow selection to be decimated on demand (context menu?)
* `P` move all persisted / workspace settings/data to IndexedDB (LS limitations) * `P` move all persisted / workspace settings/data to IndexedDB (LS limitations)
* `P` faster ray intersect https://github.com/gkjohnson/three-mesh-bvh/ * `P` faster ray intersect https://github.com/gkjohnson/three-mesh-bvh/
* `P` try material clipping planes for slice range selection * `P` try material clipping planes for slice range selection
@ -26,7 +23,7 @@
* `F` complete and expose grouping feature * `F` complete and expose grouping feature
* `F` add svgnest-like arrange algorithm * `F` add svgnest-like arrange algorithm
* `F` date column and sorting in recent files list * `F` date column and sorting in recent files list
* `F` warn if part hanging in negative Z space or off bed in general * `F` highlight part outside workspace bounds (like can neg Z shader)
# FDM # FDM
@ -34,6 +31,7 @@
* `B` multi-extruder rendering of raft fails to offset the rest of the print * `B` multi-extruder rendering of raft fails to offset the rest of the print
* `B` multi-extruder purge blocks fail to generate properly for rafts * `B` multi-extruder purge blocks fail to generate properly for rafts
* `F` better thin-wall (like arachne)
* `F` add scarf seams * `F` add scarf seams
* `F` add 'random' and 'seam' start point options * `F` add 'random' and 'seam' start point options
* `F` convert ranges to z offsets while continuing to show layer # * `F` convert ranges to z offsets while continuing to show layer #
@ -52,12 +50,6 @@
* `P` extruder + filament max flow rate cap in planner * `P` extruder + filament max flow rate cap in planner
* `P` solid fill the tops of supports for down facing flats * `P` solid fill the tops of supports for down facing flats
# FDM - SLA
# `P` common support area detection (fork new fdm code)
* `P` prioritize supports by length of unsupported span. mandatory when circularity > X
* or % area of inner to outer poly is high (making it a thin shell)
# FDM - BELT # FDM - BELT
* `B` auto bed resizing leaves the origin in the wrong place at first * `B` auto bed resizing leaves the origin in the wrong place at first
@ -69,7 +61,6 @@
# CAM # CAM
* `B` roughing leave stock should also increase Z slightly (sqrt(2) * leave)
* `B` rapid moves should be max of terrain zmax and last cut layer height (roughing) * `B` rapid moves should be max of terrain zmax and last cut layer height (roughing)
* `B` feed rate for next tool set before tool change (push/pop feed rates?) * `B` feed rate for next tool set before tool change (push/pop feed rates?)
* `B` tab cuts cause loss of direction, parenting, depth, and it's hard to fix * `B` tab cuts cause loss of direction, parenting, depth, and it's hard to fix
@ -84,18 +75,14 @@
* `F` add `match faces` option in `outline` operation * `F` add `match faces` option in `outline` operation
* `F` add {progress} substitution and maybe {time-remaining} if can be calc'd * `F` add {progress} substitution and maybe {time-remaining} if can be calc'd
* `F` import and follow 2D paths (conformed like pocket contours) * `F` import and follow 2D paths (conformed like pocket contours)
* `F` add user-defined origin (issue #28)
* `F` intelligently turn circle hole pocket clear into spiral down
* `F` add `plunge max` to contouring that can override z feed limit * `F` add `plunge max` to contouring that can override z feed limit
* `F` add lead-in milling (requires adding clamp / no go areas) * `F` add lead-in milling (requires adding clamp / no go areas)
* `F` add linear clearing strategy * `F` add linear clearing strategy
* `F` add adaptive clearing strategy * `F` add adaptive clearing strategy
* `F` add ease-down support to trace op
* `F` add support for tapered ball mills * `F` add support for tapered ball mills
* `F` change color of line selection in trace op when not a closed poly * `F` change color of line selection in trace op when not a closed poly
* `F` limit cut depth to flute length of selected tool (or warn) * `F` limit cut depth to flute length of selected tool (or warn)
* `F` validate muti-part layout and spacing exceeds largest outside tool diameter * `F` validate muti-part layout and spacing exceeds largest outside tool diameter
* `F` parameterize dropping close points in prep.js. ensure long segments remain straight
* `F` trapezoidal tabs in Z * `F` trapezoidal tabs in Z
* `F` ease-in and ease-out especially on tab cut-out start/stop * `F` ease-in and ease-out especially on tab cut-out start/stop
* `F` maintain several part orientations + op chains in a single profile * `F` maintain several part orientations + op chains in a single profile
@ -104,14 +91,8 @@
* `P` outer corner arc moves * `P` outer corner arc moves
* `P` log Z interpolation for contour XYZ moves * `P` log Z interpolation for contour XYZ moves
* `P` option to start with the smallest poly by area on layer change * `P` option to start with the smallest poly by area on layer change
* `P` refactor slicing engine to add minion threading
* `P` port arc code from FDM export to CAM export
* `P` redo all path route / planning in prepare to account for terrain before camOut * `P` redo all path route / planning in prepare to account for terrain before camOut
# SLA
* `P` blit layers w/ 3JS ortho camera to canvas
# Laser # Laser
* `F` add PLT / HP-GL output format (https://en.wikipedia.org/wiki/HP-GL) * `F` add PLT / HP-GL output format (https://en.wikipedia.org/wiki/HP-GL)
@ -124,8 +105,9 @@
# Mesh:Tool # Mesh:Tool
* font to path with https://github.com/paulzi/svg-text-to-path
* add undo/redo * add undo/redo
* add TextGeometry
* font to path with https://github.com/paulzi/svg-text-to-path
* click to repair normals based on a known good (selected) face * click to repair normals based on a known good (selected) face
* send to Kiri:Moto workspace (or update model vertices in place) * send to Kiri:Moto workspace (or update model vertices in place)
* better z snap using just vertexes from face intersected * better z snap using just vertexes from face intersected
@ -134,6 +116,5 @@
* add flatten/crush op: for z bottoms (or surfaces?) * add flatten/crush op: for z bottoms (or surfaces?)
* allow setting model/group origin for scale/rotate * allow setting model/group origin for scale/rotate
* fix mirror to work with groups (just models currently) * fix mirror to work with groups (just models currently)
* bounding box toggle should be global, not selection
* add analyze results dialog * add analyze results dialog
* remove/hide auto-repair function * remove/hide auto-repair function

View file

@ -1,6 +1,6 @@
{ {
"name": "grid-apps", "name": "grid-apps",
"version": "4.2.0", "version": "4.3.0",
"description": "grid.space 3d slicing & modeling tools", "description": "grid.space 3d slicing & modeling tools",
"author": "Stewart Allen <sa@grid.space>", "author": "Stewart Allen <sa@grid.space>",
"license": "MIT", "license": "MIT",
@ -32,11 +32,12 @@
"slicer" "slicer"
], ],
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=22.0.0"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.1.1", "@fortawesome/fontawesome-free": "^6.1.1",
"@gridspace/app-server": "^0.0.17", "@gridspace/app-server": "^0.0.18",
"@gridspace/basic-ftp": "^5.0.5",
"@gridspace/net-level-client": "^0.2.3", "@gridspace/net-level-client": "^0.2.3",
"@tracespace/parser": "^5.0.0-next.0", "@tracespace/parser": "^5.0.0-next.0",
"@tweenjs/tween.js": "^16.6.0", "@tweenjs/tween.js": "^16.6.0",
@ -45,15 +46,18 @@
"buffer-crc32": "^0.2.13", "buffer-crc32": "^0.2.13",
"compression": "^1.7.4", "compression": "^1.7.4",
"connect": "^3.7.0", "connect": "^3.7.0",
"earcut": "^2.2.3", "earcut": "^3.0.1",
"express-useragent": "^1.0.13", "express-useragent": "^1.0.13",
"fast-glob": "^3.3.3",
"i": "^0.3.7",
"jszip": "^3.7.1", "jszip": "^3.7.1",
"manifold-3d": "^2.5.1", "manifold-3d": "^3.1.1",
"moment": "^2.29.4", "moment": "^2.29.4",
"npm": "^11.4.2",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"react-responsive-carousel": "^3.2.23", "react-responsive-carousel": "^3.2.23",
"serve-static": "^1.14.1", "serve-static": "^1.14.1",
"three": "^0.174.0", "three": "^0.178.0",
"three-mesh-bvh": "^0.7.6", "three-mesh-bvh": "^0.7.6",
"uglify-js": "3.14.5", "uglify-js": "3.14.5",
"validator": ">=13.7.0", "validator": ">=13.7.0",
@ -63,25 +67,21 @@
"@docusaurus/core": "^3.7.0", "@docusaurus/core": "^3.7.0",
"@docusaurus/preset-classic": "^3.7.0", "@docusaurus/preset-classic": "^3.7.0",
"@electron/notarize": "^3.0.0", "@electron/notarize": "^3.0.0",
"copy-webpack-plugin": "^12.0.2",
"docusaurus-lunr-search": "^3.6.0", "docusaurus-lunr-search": "^3.6.0",
"dotenv": "latest", "dotenv": "latest",
"electron": "^35.0.1", "electron": "^35.0.1",
"electron-builder": "^24.9.1", "electron-builder": "^24.9.1",
"esbuild": "^0.25.5",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"imports-loader": "^5.0.0",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"terser-webpack-plugin": "^5.3.10", "terser-webpack-plugin": "^5.3.10",
"webpack": "^5.92.1", "webpack": "^5.92.1",
"webpack-cli": "^5.1.4" "webpack-cli": "^5.1.4"
}, },
"scripts": { "scripts": {
"setup": "npm i && cd mods && npm i", "setup": "npm i && npm run clean && npm run dev -- --dryrun && npm run webpack-ext",
"dev": "gs-app-server --debug",
"prod": "gs-app-server",
"prod-dryrun": "gs-app-server --dryrun",
"start": "npm run prebuild && electron .",
"start-dev": "npm run prebuild && electron . --devel",
"start-dbg": "npm run prebuild && electron . --debugg",
"start-ddb": "npm run prebuild && electron . --devel --debugg",
"build": "npm run prebuild && electron-builder", "build": "npm run prebuild && electron-builder",
"build-nopublish": "npm run prebuild && electron-builder --publish never", "build-nopublish": "npm run prebuild && electron-builder --publish never",
"build-debug": "npm run prebuild && DEBUG=electron-builder electron-builder", "build-debug": "npm run prebuild && DEBUG=electron-builder electron-builder",
@ -91,16 +91,28 @@
"build-win-arm": "npm run build -- --win --arm64", "build-win-arm": "npm run build -- --win --arm64",
"build-mac": "npm run build -- --mac --arm64", "build-mac": "npm run build -- --mac --arm64",
"build-mac-intel": "npm run build -- --mac --x64", "build-mac-intel": "npm run build -- --mac --x64",
"mklinks": "find src web -type l | xargs -I{} sh -c 'echo \"{},$(readlink {})\"' > links.csv", "clean": "rm -rf build data dist src/pack tmp src.old web.old",
"docs-dev": "docusaurus start --config conf/docusaurus.config.js",
"docs-build": "docusaurus build --config conf/docusaurus.config.js",
"docs-serve": "docusaurus serve --config conf/docusaurus.config.js",
"docs-check": "prettier --config ./conf/prettier.config.js ./docs --check",
"dev": "npm run webpack-ext && gs-app-server --debug --single",
"prod": "npm run webpack-ext && npm run webpack-src prod && gs-app-server",
"prod-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",
"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", "mac-verify": "spctl --assess -vv --type install dist/*/*.app",
"clear-cache": "rm -rf data/cache/* dist/ tmp/*", "prebuild": "npm run setup && npm run webpack-src prod && node bin/electron-pre.js",
"prebuild": "node bin/electron-pre.js",
"postbuild": "node bin/electron-post.js", "postbuild": "node bin/electron-post.js",
"preinstall": "node bin/install-pre.js && npx webpack --config bin/webpack-three.js", "preinstall": "node bin/install-pre.js",
"docs-dev": "docusaurus start", "webpack-ext": "npm run webpack-three && npm run webpack-zip",
"docs-build": "docusaurus build", "webpack-src": "node bin/esbuild.config.mjs",
"docs-serve": "docusaurus serve", "webpack-three": "npx webpack --config bin/webpack-three-esm.js",
"docs-check": "prettier ./docs --check" "webpack-zip": "npx webpack --config bin/webpack-jszip-esm.js"
}, },
"main": "app-el.js", "main": "app-el.js",
"build": { "build": {
@ -172,8 +184,12 @@
"nsis": { "nsis": {
"oneClick": false, "oneClick": false,
"allowToChangeInstallationDirectory": true, "allowToChangeInstallationDirectory": true,
"differentialPackage": false,
"allowElevation": true "allowElevation": true
}, },
"dmg": {
"writeUpdateInfo": false
},
"mac": { "mac": {
"icon": "bin/GS.icns", "icon": "bin/GS.icns",
"category": "public.app-category.utilities", "category": "public.app-category.utilities",
@ -182,8 +198,7 @@
"entitlements": "bin/electron-entitlements.plist", "entitlements": "bin/electron-entitlements.plist",
"entitlementsInherit": "bin/electron-entitlements.plist", "entitlementsInherit": "bin/electron-entitlements.plist",
"target": [ "target": [
"dmg", "dmg"
"zip"
] ]
}, },
"linux": { "linux": {
@ -193,5 +208,6 @@
] ]
}, },
"afterSign": "bin/electron-notarize.js" "afterSign": "bin/electron-notarize.js"
} },
"sideEffects": true
} }

View file

@ -92,6 +92,22 @@ npm run setup
npm run dev npm run dev
``` ```
## TrueNAS Via YAML
`Apps > Discover Apps > Install via YAML`
```
services:
kirimoto:
build:
context: https://github.com/GridSpace/grid-apps.git#refs/tags/latest
dockerfile: ./src/dock/Dockerfile
ports:
- "8080:8080"
```
## For any default install...
Then open a browser to [localhost:8080/kiri](http://localhost:8080/kiri) Then open a browser to [localhost:8080/kiri](http://localhost:8080/kiri)
# Windows Developers # Windows Developers

View file

@ -1,18 +1,14 @@
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */ /** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
"use strict";
// extend Array, String, Number, Math // extend Array, String, Number, Math
gapp.register("add.array", [], (root, exports) => { const AP = {};
let AP = {};
if (!AP.flat) { if (!AP.flat) {
AP.flat = function() { AP.flat = function() {
try { try {
return [].concat.apply([], this); return [].concat.apply([], this);
} catch (e) { } catch (e) {
// console.log({flat_error: e}); // console.log({ flat_error: e, array: this });
let out = []; let out = [];
for (let e of this.slice()) { for (let e of this.slice()) {
out.push(...e); out.push(...e);
@ -216,12 +212,11 @@ Array.handle = function(fn) {
} }
}; };
for (let i in AP) { // Apply all methods to Array.prototype
Object.defineProperty(Array.prototype, i, { Object.assign(Array.prototype, AP);
value: AP[i],
enumerable: false // Export for ES modules
}); export { AP as arrayPrototype };
}
Float32Array.prototype.toShared = function() { Float32Array.prototype.toShared = function() {
const newvert = new Float32Array(new SharedArrayBuffer(this.buffer.byteLength)); const newvert = new Float32Array(new SharedArrayBuffer(this.buffer.byteLength));
@ -265,5 +260,3 @@ Number.prototype.round = function(digits) {
const pow = Math.pow(10, digits || 3); const pow = Math.pow(10, digits || 3);
return Math.round(this.valueOf() * pow) / pow; return Math.round(this.valueOf() * pow) / pow;
}; };
});

View file

@ -1,11 +1,10 @@
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */ /** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
"use strict";
// use: add.array // use: add.array
gapp.register("add.class", [], (root, exports) => { import { arrayPrototype } from './array.js';
self.ArrayWriter = class ArrayWriter { // Make classes available globally
class ArrayWriter {
constructor() { constructor() {
this.pos = 0; this.pos = 0;
this.array = []; this.array = [];
@ -48,7 +47,7 @@ self.ArrayWriter = class ArrayWriter {
} }
}; };
self.DataWriter = class DataWriter { class DataWriter {
constructor(view, pos) { constructor(view, pos) {
this.view = view; this.view = view;
this.pos = pos || 0; this.pos = pos || 0;
@ -106,7 +105,7 @@ self.DataWriter = class DataWriter {
} }
} }
self.DataReader = class DataReader { class DataReader {
constructor(view, pos) { constructor(view, pos) {
this.view = view; this.view = view;
this.pos = pos || 0; this.pos = pos || 0;
@ -171,4 +170,9 @@ self.DataReader = class DataReader {
} }
} }
}); self.ArrayWriter = ArrayWriter;
self.DataWriter = DataWriter;
self.DataReader = DataReader;
// Export for ES modules
export { ArrayWriter, DataWriter, DataReader };

View file

@ -1,16 +1,13 @@
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */ /** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
"use strict";
// dep: ext.three // dep: ext.three
gapp.register("add.three", [], (root, exports) => { import {
const {
THREE, SVGLoader, BufferGeometryUtils, THREE, SVGLoader, BufferGeometryUtils,
LineMaterial, Line2, LineGeometry, LineSegments2, LineSegmentsGeometry LineMaterial, Line2, LineGeometry, LineSegments2,
} = ThreeBundle; LineSegmentsGeometry, MeshBVHLib } from '../ext/three.js';
root.THREE = THREE; // Make THREE available globally
self.THREE = THREE;
THREE.BufferGeometryUtils = BufferGeometryUtils; THREE.BufferGeometryUtils = BufferGeometryUtils;
THREE.SVGLoader = SVGLoader.SVGLoader; THREE.SVGLoader = SVGLoader.SVGLoader;
THREE.LineMaterial = LineMaterial; THREE.LineMaterial = LineMaterial;
@ -147,4 +144,5 @@ THREE.Object3D.prototype.removeAll = function() {
this.children = []; this.children = [];
}; };
}); // Export for ES modules
export { THREE };

View file

@ -54,6 +54,21 @@
"camDrillLift": 2, "camDrillLift": 2,
"camDrillingOn": false, "camDrillingOn": false,
"camRegisterSpeed": 1000, "camRegisterSpeed": 1000,
"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", "camFlipAxis": "X",
"camFlipOther": "", "camFlipOther": "",
"camTabsWidth": 20, "camTabsWidth": 20,

View file

@ -23,37 +23,37 @@
"geo/wasm", "geo/wasm",
"data/local", "data/local",
"load/stl", "load/stl",
"kiri/conf", "kiri/core/conf",
"kiri/utils", "kiri/core/utils",
"kiri/slice", "kiri/core/slice",
"kiri/consts", "kiri/core/consts",
"kiri/client", "kiri/core/client",
"kiri/widget", "kiri/core/widget",
"kiri/layers", "kiri/core/layers",
"kiri-mode/cam/driver", "kiri/mode/cam/driver",
"kiri-mode/cam/export", "kiri/mode/cam/export",
"kiri-mode/cam/ops", "kiri/mode/cam/ops",
"kiri-mode/cam/prepare", "kiri/mode/cam/prepare",
"kiri-mode/cam/slice", "kiri/mode/cam/slice",
"kiri-mode/cam/slicer", "kiri/mode/cam/slicer",
"kiri-mode/cam/tool", "kiri/mode/cam/tool",
"kiri-mode/cam/topo", "kiri/mode/cam/topo",
"kiri-mode/fdm/driver", "kiri/mode/fdm/driver",
"kiri-mode/fdm/export", "kiri/mode/fdm/export",
"kiri-mode/fdm/fill", "kiri/mode/fdm/fill",
"kiri-mode/fdm/post", "kiri/mode/fdm/post",
"kiri-mode/fdm/prepare", "kiri/mode/fdm/prepare",
"kiri-mode/fdm/slice", "kiri/mode/fdm/slice",
"kiri-mode/laser/driver", "kiri/mode/laser/driver",
"kiri-mode/sla/driver", "kiri/mode/sla/driver",
"kiri-mode/sla/x_cxdlp", "kiri/mode/sla/x_cxdlp",
"kiri-mode/sla/x_photon", "kiri/mode/sla/x_photon",
"kiri-mode/sla/export", "kiri/mode/sla/export",
"kiri-mode/sla/slice", "kiri/mode/sla/slice",
"kiri/codec", "kiri/core/codec",
"kiri/print", "kiri/core/print",
"kiri/pack", "kiri/core/pack",
"kiri-run/worker", "kiri/run/worker",
"kiri-run/engine", "kiri/run/engine",
"main/kiri" "main/kiri"
] ]

View file

@ -1,19 +1,14 @@
/** Copyright Stewart Allen -- All Rights Reserved */ /** Copyright Stewart Allen -- All Rights Reserved */
"use strict";
gapp.register("data.index", [], (root, exports) => {
const { data } = root;
data.Index = IDBStore;
data.open = function() {
return new IDBStore(...arguments);
};
// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API // https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
export const data = {
Index: IDBStore,
open() {
return new IDBStore(...arguments);
}
};
let IDB, IRR, local = null; let IDB, IRR, local = null;
try { try {
@ -319,4 +314,5 @@ SP.clear = function(key, store = this.current) {
.objectStore(store).clear(); .objectStore(store).clear();
}; };
}); export const open = data.open;
export const Index = IDBStore;

View file

@ -1,47 +1,44 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; class Local {
__data__ = {};
__mem__ = true;
gapp.register("data.local", [], (root, exports) => { getItem(key) {
return this[key];
}
const { data } = root; setItem(key, val) {
this.__data__[key] = val;
this[key] = val;
}
function Local() { removeItem(key) {
this.__data__ = {}; delete this.__data__[key];
this.__mem__ = true; }
clear() {
this.__data__ = {};
}
} }
var LS = Local.prototype; let setLocal;
LS.getItem = function(key) {
return this[key];
};
LS.setItem = function(key, val) {
this.__data__[key] = val;
this[key] = val;
};
LS.removeItem = function(key) {
delete this.__data__[key];
};
LS.clear = function() {
this.__data__ = {};
};
try { try {
// deprecate 'Local' at some point if (typeof window === 'undefined') {
let local = data.local =self.localStorage; setLocal = new Local();
} else {
// deprecate 'Local' at some point
setLocal = self.localStorage;
}
let testkey = '__test'; let testkey = '__test';
local.setItem(testkey, 1); setLocal.setItem(testkey, 1);
local.getItem(testkey); setLocal.getItem(testkey);
local.removeItem(testkey); setLocal.removeItem(testkey);
} catch (e) { } catch (e) {
data.local = new Local(); setLocal = new Local();
let msg = "localStorage disabled: application may not function properly"; let msg = "localStorage disabled: application may not function properly";
console.log(msg);
// alert(msg); // alert(msg);
} }
}); export const local = setLocal;

File diff suppressed because it is too large Load diff

7
src/ext/clip2.esm.js Normal file
View file

@ -0,0 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
// Import the original Clipper library
import './clip2.js';
// Re-export the ClipperLib object
export const ClipperLib = self.ClipperLib;

View file

@ -1,270 +0,0 @@
/* FileSaver.js
* A saveAs() FileSaver implementation.
* 1.1.20151003
*
* By Eli Grey, http://eligrey.com
* License: MIT
* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
*/
/*global self */
/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
var saveAs = saveAs || (function(view) {
"use strict";
// IE <10 is explicitly unsupported
if (typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
return;
}
var
doc = view.document
// only get URL when necessary in case Blob.js hasn't overridden it yet
, get_URL = function() {
return view.URL || view.webkitURL || view;
}
, save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
, can_use_save_link = "download" in save_link
, click = function(node) {
var event = new MouseEvent("click");
node.dispatchEvent(event);
}
, is_safari = /Version\/[\d\.]+.*Safari/.test(navigator.userAgent)
, webkit_req_fs = view.webkitRequestFileSystem
, req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
, throw_outside = function(ex) {
(view.setImmediate || view.setTimeout)(function() {
throw ex;
}, 0);
}
, force_saveable_type = "application/octet-stream"
, fs_min_size = 0
// See https://code.google.com/p/chromium/issues/detail?id=375297#c7 and
// https://github.com/eligrey/FileSaver.js/commit/485930a#commitcomment-8768047
// for the reasoning behind the timeout and revocation flow
, arbitrary_revoke_timeout = 500 // in ms
, revoke = function(file) {
var revoker = function() {
if (typeof file === "string") { // file is an object URL
get_URL().revokeObjectURL(file);
} else { // file is a File
file.remove();
}
};
if (view.chrome) {
revoker();
} else {
setTimeout(revoker, arbitrary_revoke_timeout);
}
}
, dispatch = function(filesaver, event_types, event) {
event_types = [].concat(event_types);
var i = event_types.length;
while (i--) {
var listener = filesaver["on" + event_types[i]];
if (typeof listener === "function") {
try {
listener.call(filesaver, event || filesaver);
} catch (ex) {
throw_outside(ex);
}
}
}
}
, auto_bom = function(blob) {
// prepend BOM for UTF-8 XML and text/* types (including HTML)
if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
return new Blob(["\ufeff", blob], {type: blob.type});
}
return blob;
}
, FileSaver = function(blob, name, no_auto_bom) {
if (!no_auto_bom) {
blob = auto_bom(blob);
}
// First try a.download, then web filesystem, then object URLs
var
filesaver = this
, type = blob.type
, blob_changed = false
, object_url
, target_view
, dispatch_all = function() {
dispatch(filesaver, "writestart progress write writeend".split(" "));
}
// on any filesys errors revert to saving with object URLs
, fs_error = function() {
if (target_view && is_safari && typeof FileReader !== "undefined") {
// Safari doesn't allow downloading of blob urls
var reader = new FileReader();
reader.onloadend = function() {
var base64Data = reader.result;
target_view.location.href = "data:attachment/file" + base64Data.slice(base64Data.search(/[,;]/));
filesaver.readyState = filesaver.DONE;
dispatch_all();
};
reader.readAsDataURL(blob);
filesaver.readyState = filesaver.INIT;
return;
}
// don't create more object URLs than needed
if (blob_changed || !object_url) {
object_url = get_URL().createObjectURL(blob);
}
if (target_view) {
target_view.location.href = object_url;
} else {
var new_tab = view.open(object_url, "_blank");
if (new_tab == undefined && is_safari) {
//Apple do not allow window.open, see http://bit.ly/1kZffRI
view.location.href = object_url
}
}
filesaver.readyState = filesaver.DONE;
dispatch_all();
revoke(object_url);
}
, abortable = function(func) {
return function() {
if (filesaver.readyState !== filesaver.DONE) {
return func.apply(this, arguments);
}
};
}
, create_if_not_found = {create: true, exclusive: false}
, slice
;
filesaver.readyState = filesaver.INIT;
if (!name) {
name = "download";
}
if (can_use_save_link) {
object_url = get_URL().createObjectURL(blob);
setTimeout(function() {
save_link.href = object_url;
save_link.download = name;
click(save_link);
dispatch_all();
revoke(object_url);
filesaver.readyState = filesaver.DONE;
});
return;
}
// Object and web filesystem URLs have a problem saving in Google Chrome when
// viewed in a tab, so I force save with application/octet-stream
// http://code.google.com/p/chromium/issues/detail?id=91158
// Update: Google errantly closed 91158, I submitted it again:
// https://code.google.com/p/chromium/issues/detail?id=389642
if (view.chrome && type && type !== force_saveable_type) {
slice = blob.slice || blob.webkitSlice;
blob = slice.call(blob, 0, blob.size, force_saveable_type);
blob_changed = true;
}
// Since I can't be sure that the guessed media type will trigger a download
// in WebKit, I append .download to the filename.
// https://bugs.webkit.org/show_bug.cgi?id=65440
if (webkit_req_fs && name !== "download") {
name += ".download";
}
if (type === force_saveable_type || webkit_req_fs) {
target_view = view;
}
if (!req_fs) {
fs_error();
return;
}
fs_min_size += blob.size;
req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
var save = function() {
dir.getFile(name, create_if_not_found, abortable(function(file) {
file.createWriter(abortable(function(writer) {
writer.onwriteend = function(event) {
target_view.location.href = file.toURL();
filesaver.readyState = filesaver.DONE;
dispatch(filesaver, "writeend", event);
revoke(file);
};
writer.onerror = function() {
var error = writer.error;
if (error.code !== error.ABORT_ERR) {
fs_error();
}
};
"writestart progress write abort".split(" ").forEach(function(event) {
writer["on" + event] = filesaver["on" + event];
});
writer.write(blob);
filesaver.abort = function() {
writer.abort();
filesaver.readyState = filesaver.DONE;
};
filesaver.readyState = filesaver.WRITING;
}), fs_error);
}), fs_error);
};
dir.getFile(name, {create: false}, abortable(function(file) {
// delete file if it already exists
file.remove();
save();
}), abortable(function(ex) {
if (ex.code === ex.NOT_FOUND_ERR) {
save();
} else {
fs_error();
}
}));
}), fs_error);
}), fs_error);
}
, FS_proto = FileSaver.prototype
, saveAs = function(blob, name, no_auto_bom) {
return new FileSaver(blob, name, no_auto_bom);
}
;
// IE 10+ (native saveAs)
if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
return function(blob, name, no_auto_bom) {
if (!no_auto_bom) {
blob = auto_bom(blob);
}
return navigator.msSaveOrOpenBlob(blob, name || "download");
};
}
FS_proto.abort = function() {
var filesaver = this;
filesaver.readyState = filesaver.DONE;
dispatch(filesaver, "abort");
};
FS_proto.readyState = FS_proto.INIT = 0;
FS_proto.WRITING = 1;
FS_proto.DONE = 2;
FS_proto.error =
FS_proto.onwritestart =
FS_proto.onprogress =
FS_proto.onwrite =
FS_proto.onabort =
FS_proto.onerror =
FS_proto.onwriteend =
null;
return saveAs;
}(
typeof self !== "undefined" && self
|| typeof window !== "undefined" && window
|| this.content
));
// `self` is undefined in Firefox for Android content script context
// while `this` is nsIContentFrameMessageManager
// with an attribute `content` that corresponds to the window
if (typeof module !== "undefined" && module.exports) {
module.exports.saveAs = saveAs;
} else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) {
define([], function() {
return saveAs;
});
}

View file

@ -1,9 +1,6 @@
// SOURCE: https://stackoverflow.com/questions/1655769/fastest-md5-implementation-in-javascript // SOURCE: https://stackoverflow.com/questions/1655769/fastest-md5-implementation-in-javascript
gapp.register("ext.md5", [], (root, exports) => {
exports({ hash }); export function hash(e) {
function hash(e) {
function h(a, b) { function h(a, b) {
var c, d, e, f, g; var c, d, e, f, g;
e = a & 2147483648; e = a & 2147483648;
@ -67,6 +64,4 @@ function hash(e) {
d = 271733878; d = 271733878;
for (e = 0; e < f.length; e += 16) q = a, r = b, s = c, t = d, a = k(a, b, c, d, f[e + 0], 7, 3614090360), d = k(d, a, b, c, f[e + 1], 12, 3905402710), c = k(c, d, a, b, f[e + 2], 17, 606105819), b = k(b, c, d, a, f[e + 3], 22, 3250441966), a = k(a, b, c, d, f[e + 4], 7, 4118548399), d = k(d, a, b, c, f[e + 5], 12, 1200080426), c = k(c, d, a, b, f[e + 6], 17, 2821735955), b = k(b, c, d, a, f[e + 7], 22, 4249261313), a = k(a, b, c, d, f[e + 8], 7, 1770035416), d = k(d, a, b, c, f[e + 9], 12, 2336552879), c = k(c, d, a, b, f[e + 10], 17, 4294925233), b = k(b, c, d, a, f[e + 11], 22, 2304563134), a = k(a, b, c, d, f[e + 12], 7, 1804603682), d = k(d, a, b, c, f[e + 13], 12, 4254626195), c = k(c, d, a, b, f[e + 14], 17, 2792965006), b = k(b, c, d, a, f[e + 15], 22, 1236535329), a = l(a, b, c, d, f[e + 1], 5, 4129170786), d = l(d, a, b, c, f[e + 6], 9, 3225465664), c = l(c, d, a, b, f[e + 11], 14, 643717713), b = l(b, c, d, a, f[e + 0], 20, 3921069994), a = l(a, b, c, d, f[e + 5], 5, 3593408605), d = l(d, a, b, c, f[e + 10], 9, 38016083), c = l(c, d, a, b, f[e + 15], 14, 3634488961), b = l(b, c, d, a, f[e + 4], 20, 3889429448), a = l(a, b, c, d, f[e + 9], 5, 568446438), d = l(d, a, b, c, f[e + 14], 9, 3275163606), c = l(c, d, a, b, f[e + 3], 14, 4107603335), b = l(b, c, d, a, f[e + 8], 20, 1163531501), a = l(a, b, c, d, f[e + 13], 5, 2850285829), d = l(d, a, b, c, f[e + 2], 9, 4243563512), c = l(c, d, a, b, f[e + 7], 14, 1735328473), b = l(b, c, d, a, f[e + 12], 20, 2368359562), a = m(a, b, c, d, f[e + 5], 4, 4294588738), d = m(d, a, b, c, f[e + 8], 11, 2272392833), c = m(c, d, a, b, f[e + 11], 16, 1839030562), b = m(b, c, d, a, f[e + 14], 23, 4259657740), a = m(a, b, c, d, f[e + 1], 4, 2763975236), d = m(d, a, b, c, f[e + 4], 11, 1272893353), c = m(c, d, a, b, f[e + 7], 16, 4139469664), b = m(b, c, d, a, f[e + 10], 23, 3200236656), a = m(a, b, c, d, f[e + 13], 4, 681279174), d = m(d, a, b, c, f[e + 0], 11, 3936430074), c = m(c, d, a, b, f[e + 3], 16, 3572445317), b = m(b, c, d, a, f[e + 6], 23, 76029189), a = m(a, b, c, d, f[e + 9], 4, 3654602809), d = m(d, a, b, c, f[e + 12], 11, 3873151461), c = m(c, d, a, b, f[e + 15], 16, 530742520), b = m(b, c, d, a, f[e + 2], 23, 3299628645), a = n(a, b, c, d, f[e + 0], 6, 4096336452), d = n(d, a, b, c, f[e + 7], 10, 1126891415), c = n(c, d, a, b, f[e + 14], 15, 2878612391), b = n(b, c, d, a, f[e + 5], 21, 4237533241), a = n(a, b, c, d, f[e + 12], 6, 1700485571), d = n(d, a, b, c, f[e + 3], 10, 2399980690), c = n(c, d, a, b, f[e + 10], 15, 4293915773), b = n(b, c, d, a, f[e + 1], 21, 2240044497), a = n(a, b, c, d, f[e + 8], 6, 1873313359), d = n(d, a, b, c, f[e + 15], 10, 4264355552), c = n(c, d, a, b, f[e + 6], 15, 2734768916), b = n(b, c, d, a, f[e + 13], 21, 1309151649), a = n(a, b, c, d, f[e + 4], 6, 4149444226), d = n(d, a, b, c, f[e + 11], 10, 3174756917), c = n(c, d, a, b, f[e + 2], 15, 718787259), b = n(b, c, d, a, f[e + 9], 21, 3951481745), a = h(a, q), b = h(b, r), c = h(c, s), d = h(d, t); for (e = 0; e < f.length; e += 16) q = a, r = b, s = c, t = d, a = k(a, b, c, d, f[e + 0], 7, 3614090360), d = k(d, a, b, c, f[e + 1], 12, 3905402710), c = k(c, d, a, b, f[e + 2], 17, 606105819), b = k(b, c, d, a, f[e + 3], 22, 3250441966), a = k(a, b, c, d, f[e + 4], 7, 4118548399), d = k(d, a, b, c, f[e + 5], 12, 1200080426), c = k(c, d, a, b, f[e + 6], 17, 2821735955), b = k(b, c, d, a, f[e + 7], 22, 4249261313), a = k(a, b, c, d, f[e + 8], 7, 1770035416), d = k(d, a, b, c, f[e + 9], 12, 2336552879), c = k(c, d, a, b, f[e + 10], 17, 4294925233), b = k(b, c, d, a, f[e + 11], 22, 2304563134), a = k(a, b, c, d, f[e + 12], 7, 1804603682), d = k(d, a, b, c, f[e + 13], 12, 4254626195), c = k(c, d, a, b, f[e + 14], 17, 2792965006), b = k(b, c, d, a, f[e + 15], 22, 1236535329), a = l(a, b, c, d, f[e + 1], 5, 4129170786), d = l(d, a, b, c, f[e + 6], 9, 3225465664), c = l(c, d, a, b, f[e + 11], 14, 643717713), b = l(b, c, d, a, f[e + 0], 20, 3921069994), a = l(a, b, c, d, f[e + 5], 5, 3593408605), d = l(d, a, b, c, f[e + 10], 9, 38016083), c = l(c, d, a, b, f[e + 15], 14, 3634488961), b = l(b, c, d, a, f[e + 4], 20, 3889429448), a = l(a, b, c, d, f[e + 9], 5, 568446438), d = l(d, a, b, c, f[e + 14], 9, 3275163606), c = l(c, d, a, b, f[e + 3], 14, 4107603335), b = l(b, c, d, a, f[e + 8], 20, 1163531501), a = l(a, b, c, d, f[e + 13], 5, 2850285829), d = l(d, a, b, c, f[e + 2], 9, 4243563512), c = l(c, d, a, b, f[e + 7], 14, 1735328473), b = l(b, c, d, a, f[e + 12], 20, 2368359562), a = m(a, b, c, d, f[e + 5], 4, 4294588738), d = m(d, a, b, c, f[e + 8], 11, 2272392833), c = m(c, d, a, b, f[e + 11], 16, 1839030562), b = m(b, c, d, a, f[e + 14], 23, 4259657740), a = m(a, b, c, d, f[e + 1], 4, 2763975236), d = m(d, a, b, c, f[e + 4], 11, 1272893353), c = m(c, d, a, b, f[e + 7], 16, 4139469664), b = m(b, c, d, a, f[e + 10], 23, 3200236656), a = m(a, b, c, d, f[e + 13], 4, 681279174), d = m(d, a, b, c, f[e + 0], 11, 3936430074), c = m(c, d, a, b, f[e + 3], 16, 3572445317), b = m(b, c, d, a, f[e + 6], 23, 76029189), a = m(a, b, c, d, f[e + 9], 4, 3654602809), d = m(d, a, b, c, f[e + 12], 11, 3873151461), c = m(c, d, a, b, f[e + 15], 16, 530742520), b = m(b, c, d, a, f[e + 2], 23, 3299628645), a = n(a, b, c, d, f[e + 0], 6, 4096336452), d = n(d, a, b, c, f[e + 7], 10, 1126891415), c = n(c, d, a, b, f[e + 14], 15, 2878612391), b = n(b, c, d, a, f[e + 5], 21, 4237533241), a = n(a, b, c, d, f[e + 12], 6, 1700485571), d = n(d, a, b, c, f[e + 3], 10, 2399980690), c = n(c, d, a, b, f[e + 10], 15, 4293915773), b = n(b, c, d, a, f[e + 1], 21, 2240044497), a = n(a, b, c, d, f[e + 8], 6, 1873313359), d = n(d, a, b, c, f[e + 15], 10, 4264355552), c = n(c, d, a, b, f[e + 6], 15, 2734768916), b = n(b, c, d, a, f[e + 13], 21, 1309151649), a = n(a, b, c, d, f[e + 4], 6, 4149444226), d = n(d, a, b, c, f[e + 11], 10, 3174756917), c = n(c, d, a, b, f[e + 2], 15, 718787259), b = n(b, c, d, a, f[e + 9], 21, 3951481745), a = h(a, q), b = h(b, r), c = h(c, s), d = h(d, t);
return (p(a) + p(b) + p(c) + p(d)).toLowerCase() return (p(a) + p(b) + p(c) + p(d)).toLowerCase()
}; }
});

7
src/ext/pngjs.esm.js Normal file
View file

@ -0,0 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
// Import the original PNG library
import './pngjs.js';
// Re-export the PNG object
export const PNG = self.png ? self.png.PNG : null;

View file

@ -1,12 +1,8 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { newPoint } from './point.js';
import earcut from '../ext/earcut.js';
// use: add.array
// use: add.class
gapp.register("geo.base", [], (root, exports) => {
const base = root.base = {};
const round_decimal_precision = 5; const round_decimal_precision = 5;
function time() { function time() {
@ -219,7 +215,7 @@ function intersect(p1, p2, p3, p4, test, parallelok) {
if (test === keys.SEGINT && !segint) return null; if (test === keys.SEGINT && !segint) return null;
if (test === keys.RAYINT && !rayint) return null; if (test === keys.RAYINT && !rayint) return null;
let ip = base.newPoint( let ip = newPoint(
p1x + (a * d1x), // x p1x + (a * d1x), // x
p1y + (a * d1y), // y p1y + (a * d1y), // y
p3.z || p4.z, // z p3.z || p4.z, // z
@ -264,7 +260,7 @@ function intersectRayLine(ro, s1, p1, p2, infinite) {
b = n2 / d; b = n2 / d;
if (infinite || (inCloseRange(b, 0, 1) && a >= 0)) { if (infinite || (inCloseRange(b, 0, 1) && a >= 0)) {
let ip = base.newPoint( let ip = newPoint(
p1x + (a * s1x), p1x + (a * s1x),
p1y + (a * s1y), p1y + (a * s1y),
p2.z || ro.z, p2.z || ro.z,
@ -528,7 +524,7 @@ function comma(v) {
* Connect to base * Connect to base
******************************************************************* */ ******************************************************************* */
base.key = { export const key = {
NONE: "", NONE: "",
PROJECT: "project", PROJECT: "project",
SEGINT: "segint", SEGINT: "segint",
@ -536,7 +532,7 @@ base.key = {
PARALLEL: "parallel" PARALLEL: "parallel"
}; };
base.config = { export const config = {
// size of gcode debug arrow head // size of gcode debug arrow head
debug_arrow: 0.25, debug_arrow: 0.25,
// default # of decimal places in generated gcode // default # of decimal places in generated gcode
@ -602,7 +598,7 @@ base.config = {
clipperClean: 250 clipperClean: 250
}; };
base.util = { export const util = {
sqr, sqr,
lerp, lerp,
time, time,
@ -639,4 +635,10 @@ base.util = {
zInPlane zInPlane
}; };
}); export const base = {
config,
key,
util
};
export { earcut };

View file

@ -1,14 +1,9 @@
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */ /** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
"use strict"; import { config, util } from './base.js';
import { newPoint } from './point.js';
// dep: geo.base export class Bounds {
gapp.register("geo.bounds", [], (root, exports) => {
const { base } = root;
const { config, util } = base;
class Bounds {
constructor() { constructor() {
this.minx = 10e7; this.minx = 10e7;
this.miny = 10e7; this.miny = 10e7;
@ -121,7 +116,7 @@ class Bounds {
} }
center(z = 0) { center(z = 0) {
return base.newPoint(this.centerx(), this.centery(), z); return newPoint(this.centerx(), this.centery(), z);
} }
centerx() { centerx() {
@ -133,9 +128,6 @@ class Bounds {
} }
} }
gapp.overlay(base, { export function newBounds() {
Bounds, return new Bounds();
newBounds() { return new Bounds() } }
});
});

View file

@ -2,24 +2,17 @@
"use strict"; "use strict";
// dep: geo.base import { THREE } from '../ext/three.js';
// mod: ext.manifold import manifold from '../ext/manifold.js';
gapp.register("geo.csg", [], (root, exports) => {
const { base, ext } = root;
const debug = true;
const precision = 0.001; const precision = 0.001;
const factor = 1/precision; const factor = 1/precision;
function log() { function log() {
if (root?.mesh?.log) { console.log(...arguments);
root.mesh.log(...arguments)
} else if (root.debug === true) {
console.log(...arguments);
}
} }
const CSG = { export const CSG = {
// accepts 2 or more arguments with threejs vertex position arrays // accepts 2 or more arguments with threejs vertex position arrays
union() { union() {
return CSG.moduleOp('manifold.union', 'union', ...arguments); return CSG.moduleOp('manifold.union', 'union', ...arguments);
@ -123,19 +116,10 @@ function indexVertices(pos) {
} }
let Instance; let Instance;
manifold({
ext.manifold.then(mod => { locateFile() { return "../wasm/manifold.wasm" }
mod.default({ }).then(inst => {
locateFile() { return "/wasm/manifold.wasm" }
}).then(inst => {
inst.setup(); inst.setup();
Instance = inst; Instance = inst;
CSG.Instance = () => { return Instance }; CSG.Instance = () => { return Instance };
});
});
gapp.overlay(base, {
CSG
});
}); });

View file

@ -1,11 +1,5 @@
/** Copyright Stewart Allen -- All Rights Reserved */ /** Copyright Stewart Allen -- All Rights Reserved */
"use strict";
// dep: geo.base
gapp.register("geo.gyroid", [], (root, exports) => {
const { base } = root;
const PI2 = Math.PI * 2; const PI2 = Math.PI * 2;
let cache = {}; let cache = {};
@ -17,7 +11,7 @@ let lastSlice = 0;
* @param off {number} z offset value from 0-1 * @param off {number} z offset value from 0-1
* @param res {number} resolution (pixels/slices per side) * @param res {number} resolution (pixels/slices per side)
*/ */
function slice(off, res, val) { export function slice(off, res, val) {
// auto clear cach if it hasn't been hit in the last 20 seconds // auto clear cach if it hasn't been hit in the last 20 seconds
// or the requested resolution or tip values have changed // or the requested resolution or tip values have changed
let now = Date.now(); let now = Date.now();
@ -170,7 +164,7 @@ function slice(off, res, val) {
} }
// merge co-linear and distance threshold // merge co-linear and distance threshold
function filter(poly, inc) { export function filter(poly, inc) {
if (poly.length <= 2) { if (poly.length <= 2) {
return poly; return poly;
} }
@ -200,11 +194,10 @@ function filter(poly, inc) {
} }
e1 = el; e1 = el;
} }
nupoly.push(poly[poly.length-1]);
return nupoly; return nupoly;
} }
function distTo(a, b, dir) { export function distTo(a, b, dir) {
let dx = a.x - b.x; let dx = a.x - b.x;
let dy = a.y - b.y; let dy = a.y - b.y;
// bias distance by prevailing direction of discovery to join stragglers // bias distance by prevailing direction of discovery to join stragglers
@ -212,9 +205,3 @@ function distTo(a, b, dir) {
if (dir === 'td') dy = dy / 2; if (dir === 'td') dy = dy / 2;
return Math.sqrt(dx * dx + dy * dy); return Math.sqrt(dx * dx + dy * dy);
} }
gapp.overlay(base, {
gyroid: { slice }
})
});

View file

@ -1,13 +1,6 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; export class Line {
// dep: geo.base
gapp.register("geo.line", [], (root, exports) => {
const { base } = root;
class Line {
constructor(p1, p2, key) { constructor(p1, p2, key) {
if (!key) key = [p1.key, p2.key].join(';'); if (!key) key = [p1.key, p2.key].join(';');
this.p1 = p1; this.p1 = p1;
@ -54,18 +47,10 @@ class Line {
} }
} }
function newLine(p1, p2, key) { export function newLine(p1, p2, key) {
return new Line(p1, p2, key); return new Line(p1, p2, key);
} }
function newOrderedLine(p1, p2, key) { export function newOrderedLine(p1, p2, key) {
return p1.key < p2.key ? newLine(p1,p2,key) : newLine(p2,p1,key); return p1.key < p2.key ? newLine(p1,p2,key) : newLine(p2,p1,key);
} }
gapp.overlay(base, {
Line,
newLine,
newOrderedLine
});
});

View file

@ -1,19 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { base } from './base.js';
import { newPoint } from './point.js';
// path & routing output utilities
// dep: geo.base
// dep: geo.point
gapp.register("geo.paths", [], (root, exports) => {
const { base } = root;
const { util, config, newPoint } = base;
const { sqr, numOrDefault } = util;
const DEG2RAD = Math.PI / 180;
/** /**
* emit each element in an array based on * emit each element in an array based on
@ -21,21 +9,21 @@ const DEG2RAD = Math.PI / 180;
* elements with { first, last } points and * elements with { first, last } points and
* may be open polys, unlike poly2polyEmit * may be open polys, unlike poly2polyEmit
*/ */
function tip2tipEmit(array, startPoint, emitter) { export function tip2tipEmit(array, startPoint, emitter) {
let mindist, dist, found, count = 0; let mindist, dist, found, count = 0;
for (;;) { for (;;) {
found = null; found = null;
mindist = Infinity; mindist = Infinity;
array.forEach(function(el) { array.forEach(function (el) {
if (el.delete) return; if (el.delete) return;
dist = startPoint.distTo2D(el.first); dist = startPoint.distTo2D(el.first);
if (dist < mindist) { if (dist < mindist) {
found = {el:el, first:el.first, last:el.last}; found = { el: el, first: el.first, last: el.last };
mindist = dist; mindist = dist;
} }
dist = startPoint.distTo2D(el.last); dist = startPoint.distTo2D(el.last);
if (dist < mindist) { if (dist < mindist) {
found = {el:el, first:el.last, last:el.first}; found = { el: el, first: el.last, last: el.first };
mindist = dist; mindist = dist;
} }
}); });
@ -56,7 +44,7 @@ function tip2tipEmit(array, startPoint, emitter) {
* to be more like outputOrderClosest() and have the option to account for * to be more like outputOrderClosest() and have the option to account for
* depth in determining distance * depth in determining distance
*/ */
function poly2polyEmit(array, startPoint, emitter, opt = {}) { export function poly2polyEmit(array, startPoint, emitter, opt = {}) {
let marker = opt.mark || 'delete'; let marker = opt.mark || 'delete';
let mindist, dist, found, count = 0; let mindist, dist, found, count = 0;
for (;;) { for (;;) {
@ -74,21 +62,21 @@ function poly2polyEmit(array, startPoint, emitter, opt = {}) {
} }
if (d2l < mindist && d2l < d2f && opt.swapdir !== false) { if (d2l < mindist && d2l < d2f && opt.swapdir !== false) {
poly.reverse(); poly.reverse();
found = {poly:poly, index:0, point:poly.first()}; found = { poly: poly, index: 0, point: poly.first() };
mindist = d2l; mindist = d2l;
} else if (d2f < mindist) { } else if (d2f < mindist) {
found = {poly:poly, index:0, point:poly.first()}; found = { poly: poly, index: 0, point: poly.first() };
mindist = d2f; mindist = d2f;
} }
continue; continue;
} }
let area = poly.open ? 1 : poly.area(); let area = poly.open ? 1 : poly.area();
poly.forEachPoint(function(point, index) { poly.forEachPoint(function (point, index) {
dist = opt.weight ? dist = opt.weight ?
startPoint.distTo3D(point) * area * area : startPoint.distTo3D(point) * area * area :
startPoint.distTo2D(point); startPoint.distTo2D(point);
if (dist < mindist) { if (dist < mindist) {
found = {poly:poly, index:index, point:point}; found = { poly: poly, index: index, point: point };
mindist = dist; mindist = dist;
} }
}); });
@ -102,23 +90,23 @@ function poly2polyEmit(array, startPoint, emitter, opt = {}) {
// undo delete marks // undo delete marks
if (opt.perm !== true) { if (opt.perm !== true) {
array.forEach(function(poly) { poly[marker] = false }); array.forEach(function (poly) { poly[marker] = false });
} }
return startPoint; return startPoint;
} }
function calc_normal(p1, p2) { export function calc_normal(p1, p2) {
let dx = p2.x - p1.x; let dx = p2.x - p1.x;
let dy = p2.y - p1.y; let dy = p2.y - p1.y;
let len = Math.sqrt(dx * dx + dy * dy); let len = Math.sqrt(dx * dx + dy * dy);
let mn = (1 / len); let mn = (1 / len);
dx *= mn; dx *= mn;
dy *= mn; dy *= mn;
return({ dx: dy, dy: -dx, p1, p2, len }); return ({ dx: dy, dy: -dx, p1, p2, len });
} }
function end_vertex(n1, n2, off, start) { export function end_vertex(n1, n2, off, start) {
let dx, dy; let dx, dy;
if (start) { if (start) {
dx = n2.dx * off; dx = n2.dx * off;
@ -130,7 +118,7 @@ function end_vertex(n1, n2, off, start) {
return { dx, dy, vp: n1.p2 }; return { dx, dy, vp: n1.p2 };
} }
function calc_vertex(n1, n2, off, vp) { export function calc_vertex(n1, n2, off, vp) {
let dx, dy, io, vl, q, r; let dx, dy, io, vl, q, r;
r = 1 + (n1.dx * n2.dx + n1.dy * n2.dy); r = 1 + (n1.dx * n2.dx + n1.dy * n2.dy);
q = off / r; q = off / r;
@ -148,7 +136,7 @@ function calc_vertex(n1, n2, off, vp) {
return { dx, dy, vp: vp || n1.p2, io, vl }; return { dx, dy, vp: vp || n1.p2, io, vl };
} }
function v2pl(rec) { export function v2pl(rec) {
let p = rec.vp.clone(); let p = rec.vp.clone();
p.x += rec.dx; p.x += rec.dx;
p.y += rec.dy; p.y += rec.dy;
@ -156,7 +144,7 @@ function v2pl(rec) {
return p; return p;
} }
function v2pr(rec) { export function v2pr(rec) {
let p = rec.vp.clone(); let p = rec.vp.clone();
p.x -= rec.dx; p.x -= rec.dx;
p.y -= rec.dy; p.y -= rec.dy;
@ -164,17 +152,17 @@ function v2pr(rec) {
return p; return p;
} }
function pointsToPath(points, offset, open, miter = 1.5) { export function pointsToPath(points, offset, open, miter = 1.5) {
const absoff = Math.abs(offset); const absoff = Math.abs(offset);
// calculate segment normals which are used to calculate vertex normals // calculate segment normals which are used to calculate vertex normals
// next segment info is associated with the current point // next segment info is associated with the current point
const nupoints = []; const nupoints = [];
const length = points.length; const length = points.length;
if (length === 2 && points[0].isEqual(points[1])) { if (length === 2 && points[0].isEqual(points[1])) {
return { }; return {};
} }
const dedup = (open && length > 2) || (!open && length > 3); const dedup = (open && length > 2) || (!open && length > 3);
for (let i=0; i<length; i++) { for (let i = 0; i < length; i++) {
let p1 = points[i]; let p1 = points[i];
let p2 = points[(i + 1) % length]; let p2 = points[(i + 1) % length];
p1.normal = calc_normal(p1, p2); p1.normal = calc_normal(p1, p2);
@ -191,7 +179,7 @@ function pointsToPath(points, offset, open, miter = 1.5) {
nupoints.push(p1); nupoints.push(p1);
} }
if (nupoints.length === 1) { if (nupoints.length === 1) {
console.log({points, nupoints}); console.log({ points, nupoints });
} }
// when points are dropped, we need the new array // when points are dropped, we need the new array
points = nupoints; points = nupoints;
@ -204,10 +192,10 @@ function pointsToPath(points, offset, open, miter = 1.5) {
// calculate vertex normals from segments normals // calculate vertex normals from segments normals
// vertex info is associated with the origin point // vertex info is associated with the origin point
let fl, fr; let fl, fr;
for (let i=0, l=points.length; i<l; i++) { for (let i = 0, l = points.length; i < l; i++) {
let n1 = points[(i+l-1)%l].normal; let n1 = points[(i + l - 1) % l].normal;
let n2 = points[(i+l)%l].normal; let n2 = points[(i + l) % l].normal;
let vn = open && (i === 0 || i === l-1) ? let vn = open && (i === 0 || i === l - 1) ?
end_vertex(n1, n2, offset, i === 0) : end_vertex(n1, n2, offset, i === 0) :
calc_vertex(n1, n2, offset); calc_vertex(n1, n2, offset);
let { p1, p2 } = n2; let { p1, p2 } = n2;
@ -229,7 +217,7 @@ function pointsToPath(points, offset, open, miter = 1.5) {
// shorten each leg and insert new point // shorten each leg and insert new point
let delta = 0.1; let delta = 0.1;
let np1 = p1.clone().move({ x: n1.dy * delta, y: -n1.dx * delta, z: 0 }); let np1 = p1.clone().move({ x: n1.dy * delta, y: -n1.dx * delta, z: 0 });
let np2 = p1.clone().move({ x:-n2.dy * delta, y: n2.dx * delta, z: 0 }); let np2 = p1.clone().move({ x: -n2.dy * delta, y: n2.dx * delta, z: 0 });
let sn1 = np1.normal = calc_normal(np1, np2); let sn1 = np1.normal = calc_normal(np1, np2);
let sn2 = np2.normal = p1.normal; let sn2 = np2.normal = p1.normal;
let nv1 = calc_vertex(n1.p1.normal, sn1, offset, np1); let nv1 = calc_vertex(n1.p1.normal, sn1, offset, np1);
@ -336,7 +324,7 @@ function pointsToPath(points, offset, open, miter = 1.5) {
return { left, right, faces, normals, open }; return { left, right, faces, normals, open };
} }
function pathTo3D(path, height, z) { export function pathTo3D(path, height, z) {
const { faces, normals, left, right, open } = path; const { faces, normals, left, right, open } = path;
const out = []; const out = [];
const nrm = []; const nrm = [];
@ -356,14 +344,14 @@ function pathTo3D(path, height, z) {
} }
nrm.appendAll(normals); nrm.appendAll(normals);
// reverse normals to match faces, but underside so reverse Z as well // reverse normals to match faces, but underside so reverse Z as well
for (let i=normals.length-1; i>0; i-=3) { for (let i = normals.length - 1; i > 0; i -= 3) {
nrm.push(normals[i-2]); nrm.push(normals[i - 2]);
nrm.push(normals[i-1]); nrm.push(normals[i - 1]);
nrm.push(-normals[i-0]); nrm.push(-normals[i - 0]);
} }
for (let i=0, l=left.length, tl = open ? l-1 : l; i<tl; i++) { for (let i = 0, l = left.length, tl = open ? l - 1 : l; i < tl; i++) {
let p0 = left[i]; let p0 = left[i];
let p1 = left[(i+1)%l]; let p1 = left[(i + 1) % l];
out.push(p0.x, p0.y, p0.z + height); out.push(p0.x, p0.y, p0.z + height);
out.push(p0.x, p0.y, p0.z - height); out.push(p0.x, p0.y, p0.z - height);
out.push(p1.x, p1.y, p1.z - height); out.push(p1.x, p1.y, p1.z - height);
@ -372,15 +360,15 @@ function pathTo3D(path, height, z) {
out.push(p0.x, p0.y, p0.z + height); out.push(p0.x, p0.y, p0.z + height);
let ln = p0.vp.normal; let ln = p0.vp.normal;
nrm.push(ln.dx, ln.dy, -1); nrm.push(ln.dx, ln.dy, -1);
nrm.push(ln.dx, ln.dy, 1); nrm.push(ln.dx, ln.dy, 1);
nrm.push(ln.dx, ln.dy, 1); nrm.push(ln.dx, ln.dy, 1);
nrm.push(ln.dx, ln.dy, 1); nrm.push(ln.dx, ln.dy, 1);
nrm.push(ln.dx, ln.dy, -1); nrm.push(ln.dx, ln.dy, -1);
nrm.push(ln.dx, ln.dy, -1); nrm.push(ln.dx, ln.dy, -1);
} }
for (let i=0, l=right.length, tl = open ? l-1 : l; i<tl; i++) { for (let i = 0, l = right.length, tl = open ? l - 1 : l; i < tl; i++) {
let p0 = right[i]; let p0 = right[i];
let p1 = right[(i+1)%l]; let p1 = right[(i + 1) % l];
out.push(p0.x, p0.y, p0.z + height); out.push(p0.x, p0.y, p0.z + height);
out.push(p1.x, p1.y, p1.z - height); out.push(p1.x, p1.y, p1.z - height);
out.push(p0.x, p0.y, p0.z - height); out.push(p0.x, p0.y, p0.z - height);
@ -388,12 +376,12 @@ function pathTo3D(path, height, z) {
out.push(p0.x, p0.y, p0.z + height); out.push(p0.x, p0.y, p0.z + height);
out.push(p1.x, p1.y, p1.z + height); out.push(p1.x, p1.y, p1.z + height);
let rn = p0.vp.normal; let rn = p0.vp.normal;
nrm.push(-rn.dy, rn.dx, 1); nrm.push(-rn.dy, rn.dx, 1);
nrm.push(-rn.dy, rn.dx, -1); nrm.push(-rn.dy, rn.dx, -1);
nrm.push(-rn.dy, rn.dx, -1); nrm.push(-rn.dy, rn.dx, -1);
nrm.push(-rn.dy, rn.dx, -1); nrm.push(-rn.dy, rn.dx, -1);
nrm.push(-rn.dy, rn.dx, 1); nrm.push(-rn.dy, rn.dx, 1);
nrm.push(-rn.dy, rn.dx, 1); nrm.push(-rn.dy, rn.dx, 1);
} }
if (open) { if (open) {
// begin cap // begin cap
@ -406,12 +394,12 @@ function pathTo3D(path, height, z) {
out.push(r0.x, r0.y, r0.z - height); out.push(r0.x, r0.y, r0.z - height);
out.push(l0.x, l0.y, l0.z + height); out.push(l0.x, l0.y, l0.z + height);
let ln = l0.vp.normal; let ln = l0.vp.normal;
nrm.push(-ln.dy, ln.dx, 1); nrm.push(-ln.dy, ln.dx, 1);
nrm.push(-ln.dy, ln.dx, -1); nrm.push(-ln.dy, ln.dx, -1);
nrm.push(-ln.dy, ln.dx, -1); nrm.push(-ln.dy, ln.dx, -1);
nrm.push(-ln.dy, ln.dx, 1); nrm.push(-ln.dy, ln.dx, 1);
nrm.push(-ln.dy, ln.dx, -1); nrm.push(-ln.dy, ln.dx, -1);
nrm.push(-ln.dy, ln.dx, 1); nrm.push(-ln.dy, ln.dx, 1);
// end cap // end cap
let le = left.peek(); let le = left.peek();
let re = right.peek(); let re = right.peek();
@ -422,11 +410,11 @@ function pathTo3D(path, height, z) {
out.push(le.x, le.y, le.z + height); out.push(le.x, le.y, le.z + height);
out.push(re.x, re.y, re.z - height); out.push(re.x, re.y, re.z - height);
ln = re.vp.normal; ln = re.vp.normal;
nrm.push(-ln.dy, ln.dx, 1); nrm.push(-ln.dy, ln.dx, 1);
nrm.push(-ln.dy, ln.dx, -1); nrm.push(-ln.dy, ln.dx, -1);
nrm.push(-ln.dy, ln.dx, -1); nrm.push(-ln.dy, ln.dx, -1);
nrm.push(-ln.dy, ln.dx, 1); nrm.push(-ln.dy, ln.dx, 1);
nrm.push(-ln.dy, ln.dx, 1); nrm.push(-ln.dy, ln.dx, 1);
nrm.push(-ln.dy, ln.dx, -1); nrm.push(-ln.dy, ln.dx, -1);
} }
return { faces: out, normals: nrm }; return { faces: out, normals: nrm };
@ -434,7 +422,7 @@ function pathTo3D(path, height, z) {
// produces indexed geometry which isn't ideal for rendering because // produces indexed geometry which isn't ideal for rendering because
// the default threejs generated vertex normals aren't accurate // the default threejs generated vertex normals aren't accurate
function shapeToPath(shape, points, closed) { export function shapeToPath(shape, points, closed) {
closed = closed !== undefined ? closed : true; closed = closed !== undefined ? closed : true;
const profileGeometry = new THREE.ShapeGeometry(shape); const profileGeometry = new THREE.ShapeGeometry(shape);
@ -451,9 +439,9 @@ function shapeToPath(shape, points, closed) {
let hA = halfAngle; let hA = halfAngle;
let tA = v2.angle() + Math.PI * .5; let tA = v2.angle() + Math.PI * .5;
if (!closed){ if (!closed) {
if (i == 0 || i == points.length - 1) {hA = Math.PI * .5;} if (i == 0 || i == points.length - 1) { hA = Math.PI * .5; }
if (i == points.length - 1) {tA = v1.angle() - Math.PI * .5;} if (i == points.length - 1) { tA = v1.angle() - Math.PI * .5; }
} }
const shift = Math.tan(hA - Math.PI * .5); const shift = Math.tan(hA - Math.PI * .5);
@ -488,7 +476,7 @@ function shapeToPath(shape, points, closed) {
} }
const index = []; const index = [];
const lastCorner = closed == false ? points.length - 1: points.length; const lastCorner = closed == false ? points.length - 1 : points.length;
for (let i = 0; i < lastCorner; i++) { for (let i = 0; i < lastCorner; i++) {
for (let j = 0; j < profile.count; j++) { for (let j = 0; j < profile.count; j++) {
@ -524,7 +512,7 @@ function shapeToPath(shape, points, closed) {
index.push(p8, p7, p5); index.push(p8, p7, p5);
} }
return {index, faces}; return { index, faces };
} }
/** /**
@ -539,31 +527,31 @@ function shapeToPath(shape, points, closed) {
* *
* @return {Array<Point>} an array of points representing the arc. * @return {Array<Point>} an array of points representing the arc.
*/ */
function arcToPath( start, end,arcdivs=24,opts) { export function arcToPath(start, end, arcdivs = 24, opts) {
let { clockwise, center, radius } = opts; let { clockwise, center, radius } = opts;
// @type {Point} // @type {Point}
if (end.x === undefined && end.x === undefined && center === undefined) { if (end.x === undefined && end.x === undefined && center === undefined) {
// bambu generates loop z or wipe loop arcs in place // bambu generates loop z or wipe loop arcs in place
// console.log({ skip_empty_arc: rec }); // console.log({ skip_empty_arc: rec });
return; return;
}
if(arcdivs <= 2){
return [start.clone(), end.clone()];
} }
if (center) { if (center) {
// center = center.add(start); // center = center.add(start);
center.r = center.distTo2D(start); center.r = center.distTo2D(start);
} else if (radius !== undefined) { } else if (radius !== undefined) {
let pd = { x: end.x - start.x, y: end.y - start.y }; //position delta let pd = { x: end.x - start.x, y: end.y - start.y }; //position delta
let dst = Math.sqrt(pd.x * pd.x + pd.y * pd.y) / 2; // distance let dst = Math.sqrt(pd.x * pd.x + pd.y * pd.y) / 2; // distance
let pr2; let pr2;
if (Math.abs(dst - radius) < 0.001) { if (Math.abs(dst - radius) < 0.001) {
// center point radius // center point radius
pr2 = { x: (end.x + start.x) / 2, y: (end.y + start.y) / 2}; pr2 = { x: (end.x + start.x) / 2, y: (end.y + start.y) / 2 };
} else { } else {
// triangulate // triangulate
pr2 = base.util.center2pr(start, end, radius, clockwise); pr2 = base.util.center2pr(start, end, radius, clockwise);
@ -572,7 +560,7 @@ function arcToPath( start, end,arcdivs=24,opts) {
center.y = pr2.y; center.y = pr2.y;
center.r = radius; center.r = radius;
} else { } else {
console.log({malfomed_arc: {radius,center, clockwise, start, end}}); console.log({ malfomed_arc: { radius, center, clockwise, start, end } });
} }
//deltas //deltas
@ -587,29 +575,19 @@ function arcToPath( start, end,arcdivs=24,opts) {
let samePoint = Math.abs(ad) < 0.001 let samePoint = Math.abs(ad) < 0.001
let ofFull = Math.abs(ad)/(2*Math.PI); let ofFull = Math.abs(ad)/(2*Math.PI);
let steps = samePoint? arcdivs : Math.max(Math.floor( arcdivs * ofFull),4); let steps = samePoint? arcdivs : Math.max(Math.floor( arcdivs * ofFull),4);
let step = (samePoint? (Math.PI*2) : ad) / steps; let step = (samePoint? (Math.PI*2)*(clockwise? -1 : 1) : ad) / steps;
let numPoints = steps/ step;
let zStart = start.z; let zStart = start.z;
let zStep = dz / numPoints; let zStep = -dz / steps;
let rot = a1 + step; let rot = a1;
//unused deltas //unused deltas
let da = Math.abs(a1 - a2); let da = Math.abs(a1 - a2);
let dd = Math.sqrt(dx * dx + dy * dy); let dd = Math.sqrt(dx * dx + dy * dy);
// LOG({index, da, dd, first: pos, last: rec, center, a1, a2, ad, step, steps, rot, line});
// G0G1(false, [`X${center.x}`, `Y${center.y}`, `E1`]);
// under 1 degree arc and 5mm, convert to straight line
// if (da < 0.005 && dd < 5) {
// G0G1(false, [`X${end.x}`, `Y${end.y}`, `E1`]);
// return ;
// }
let arr = [] // point accumulator let arr = [] // point accumulator
for (let i=0; i<=steps-2; i++) { for (let i = 0; i <= steps - 2; i++) {
if (isNaN(center.r) || isNaN(center.x) || isNaN(center.y)) { if (isNaN(center.r) || isNaN(center.x) || isNaN(center.y)) {
console.log({malfomed_arc: {radius, clockwise, start, end}}); console.log({ malfomed_arc: { radius, clockwise, start, end } });
} }
arr.push(newPoint( arr.push(newPoint(
center.x + Math.cos(rot) * center.r, center.x + Math.cos(rot) * center.r,
@ -623,7 +601,7 @@ function arcToPath( start, end,arcdivs=24,opts) {
return arr return arr
} }
class FloatPacker { export class FloatPacker {
constructor(size, factor) { constructor(size, factor) {
this.size = size; this.size = size;
this.factor = Math.min(factor || 1.2, 1.1); this.factor = Math.min(factor || 1.2, 1.1);
@ -642,7 +620,7 @@ class FloatPacker {
this.array = nuarray; this.array = nuarray;
this.size = nusize; this.size = nusize;
} }
for (let i=0; i<args; i++) { for (let i = 0; i < args; i++) {
array[this.pos++] = arguments[i]; array[this.pos++] = arguments[i];
} }
} }
@ -656,15 +634,11 @@ class FloatPacker {
} }
} }
base.paths = { export const paths = {
poly2polyEmit,
tip2tipEmit, tip2tipEmit,
poly2polyEmit,
shapeToPath, shapeToPath,
pointsToPath, pointsToPath,
pathTo3D, pathTo3D,
arcToPath, FloatPacker
vertexNormal: calc_vertex, }
segmentNormal: calc_normal
};
});

View file

@ -1,13 +1,10 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { util, config, key } from './base.js';
import { newLine } from './line.js';
import { newSlope } from './slope.js';
// dep: geo.base const { Vector3 } = THREE;
gapp.register("geo.point", [], (root, exports) => {
const { base } = root;
const { util, config, key } = base;
const { round } = util;
class Point { class Point {
constructor(x = 0, y = 0, z = 0, key) { constructor(x = 0, y = 0, z = 0, key) {
@ -42,7 +39,7 @@ class Point {
} }
toVector3() { toVector3() {
return new THREE.Vector3(this.x, this.y, this.z); return new Vector3(this.x, this.y, this.z);
} }
set(x, y, z) { set(x, y, z) {
@ -126,11 +123,11 @@ class Point {
} }
slopeTo(p) { slopeTo(p) {
return base.newSlope(this, p); return newSlope(this, p);
} }
lineTo(p, k) { lineTo(p, k) {
return base.newLine(this, p, k); return newLine(this, p, k);
} }
isNear(p, dist) { isNear(p, dist) {
@ -296,7 +293,7 @@ class Point {
np2 = newPoint(p2.x - oy, p2.y + ox, p2.z, key.NONE); np2 = newPoint(p2.x - oy, p2.y + ox, p2.z, key.NONE);
np1.op = p1; np1.op = p1;
np2.op = p2; np2.op = p2;
return base.newLine(np1, np2, key.NONE); return newLine(np1, np2, key.NONE);
} }
offset(x, y, z) { offset(x, y, z) {
@ -652,10 +649,8 @@ function pointFromClipper(cp, z) {
return newPoint(cp.X / config.clipper, cp.Y / config.clipper, z); return newPoint(cp.X / config.clipper, cp.Y / config.clipper, z);
} }
gapp.overlay(base, { export {
Point, Point,
newPoint, newPoint,
pointFromClipper pointFromClipper
}); };
});

View file

@ -2,23 +2,14 @@
"use strict"; "use strict";
// dep: geo.base import { config } from './base.js';
// dep: geo.point import { newPoint } from '../geo/point.js';
gapp.register("geo.points", [], (root, exports) => {
const { base } = root;
const { config } = base;
gapp.overlay(base, {
verticesToPoints,
pointsToVertices
});
/** /**
* converts a geometry point array into a kiri point array * converts a geometry point array into a kiri point array
* with optional decimation * with optional decimation
*/ */
function verticesToPoints(array, options) { export function verticesToPoints(array, options) {
let parr = new Array(array.length / 3), let parr = new Array(array.length / 3),
i = 0, i = 0,
j = 0, j = 0,
@ -41,7 +32,7 @@ function verticesToPoints(array, options) {
// replace point objects with their equivalents // replace point objects with their equivalents
if (maxpass) { if (maxpass) {
while (i < array.length) { while (i < array.length) {
let p = base.newPoint(array[i++], array[i++], array[i++]), let p = newPoint(array[i++], array[i++], array[i++]),
k = p.key, k = p.key,
m = hash[k]; m = hash[k];
if (!m) { if (!m) {
@ -53,7 +44,7 @@ function verticesToPoints(array, options) {
} }
} else { } else {
while (i < array.length) { while (i < array.length) {
parr[j++] = base.newPoint(array[i++], array[i++], array[i++]); parr[j++] = newPoint(array[i++], array[i++], array[i++]);
} }
} }
@ -124,7 +115,7 @@ function verticesToPoints(array, options) {
return parr; return parr;
} }
function pointsToVertices(points) { export function pointsToVertices(points) {
let vertices = new Float32Array(points.length * 3), let vertices = new Float32Array(points.length * 3),
i = 0, vi = 0; i = 0, vi = 0;
while (i < points.length) { while (i < points.length) {
@ -134,5 +125,3 @@ function pointsToVertices(points) {
} }
return vertices; return vertices;
} }
});

View file

@ -2,22 +2,17 @@
"use strict"; "use strict";
// dep: geo.base import { base, config, earcut, util } from './base.js';
// dep: geo.paths import { ClipperLib } from '../ext/clip2.esm.js';
// dep: ext.clip2 import { newBounds } from './bounds.js';
// dep: ext.earcut import { paths } from './paths.js';
// dep: geo.point import { newPoint, pointFromClipper } from './point.js';
// dep: geo.bounds import { polygons as POLY } from './polygons.js';
// dep: geo.polygons
gapp.register("geo.polygon", [], (root, exports) => {
const { base } = root; const { Vector3 } = THREE;
const { config, util, polygons, newBounds, newPoint } = base;
const POLY = polygons, let XAXIS = new Vector3(1,0,0),
XAXIS = new THREE.Vector3(1,0,0),
DEG2RAD = Math.PI / 180, DEG2RAD = Math.PI / 180,
ClipperLib = self.ClipperLib,
Clipper = ClipperLib.Clipper, Clipper = ClipperLib.Clipper,
ClipType = ClipperLib.ClipType, ClipType = ClipperLib.ClipType,
PolyType = ClipperLib.PolyType, PolyType = ClipperLib.PolyType,
@ -39,7 +34,7 @@ const POLY = polygons,
let seqid = Math.round(Math.random() * 0xffffffff); let seqid = Math.round(Math.random() * 0xffffffff);
class Polygon { export class Polygon {
constructor(points) { constructor(points) {
this.id = seqid++; // polygon unique id this.id = seqid++; // polygon unique id
this.open = false; this.open = false;
@ -76,11 +71,11 @@ class Polygon {
} }
toPath2D(offset) { toPath2D(offset) {
return base.paths.pointsToPath(this.points, offset, this.open); return paths.pointsToPath(this.points, offset, this.open);
} }
toPath3D(offset, height, z) { toPath3D(offset, height, z) {
return base.paths.pathTo3D(this.toPath2D(offset), height, z); return paths.pathTo3D(this.toPath2D(offset), height, z);
} }
toString(verbose) { toString(verbose) {
@ -235,7 +230,7 @@ class Polygon {
} }
// perform earcut() // perform earcut()
let cut = self.earcut(out, holes, 3); let cut = earcut(out, holes, 3);
let ret = []; let ret = [];
// preserve swaps in new polys // preserve swaps in new polys
@ -467,7 +462,7 @@ class Polygon {
return polys return polys
.filter(poly => poly.length > 1) .filter(poly => poly.length > 1)
.map(poly => { .map(poly => {
let np = base.newPolygon().setOpen(); let np = newPolygon().setOpen();
for (let p of poly) { for (let p of poly) {
np.push(p); np.push(p);
} }
@ -848,6 +843,7 @@ class Polygon {
* scale polygon around origin * scale polygon around origin
*/ */
scale(scale, round) { scale(scale, round) {
this.area2 = undefined;
let x, y, z; let x, y, z;
if (typeof(scale) === 'number') { if (typeof(scale) === 'number') {
x = y = z = scale; x = y = z = scale;
@ -1078,6 +1074,7 @@ class Polygon {
* append point to polygon and return point * append point to polygon and return point
*/ */
push(p) { push(p) {
this.area2 = undefined;
// clone any point belonging to another polygon // clone any point belonging to another polygon
if (p.poly) p = p.clone(); if (p.poly) p = p.clone();
p.poly = this; p.poly = this;
@ -1122,6 +1119,14 @@ class Polygon {
return this.first().isEqual(this.last()); return this.first().isEqual(this.last());
} }
fixClosed() {
if (this.appearsClosed()) {
this.points.pop();
this.open = false;
}
return this;
}
setClockwise() { setClockwise() {
if (!this.isClockwise()) this.reverse(); if (!this.isClockwise()) this.reverse();
return this; return this;
@ -1143,7 +1148,7 @@ class Polygon {
applyRotations() { applyRotations() {
for (let point of this.points) { for (let point of this.points) {
if (point.a) { if (point.a) {
let p2 = new THREE.Vector3(point.x, point.y, point.z) let p2 = new Vector3(point.x, point.y, point.z)
.applyAxisAngle(XAXIS, point.a * DEG2RAD); .applyAxisAngle(XAXIS, point.a * DEG2RAD);
point.x = p2.x; point.x = p2.x;
point.y = p2.y; point.y = p2.y;
@ -1205,15 +1210,76 @@ class Polygon {
return false; return false;
} }
/** /**
* calls function for each point in this polygon * Ease down along the polygonal path.
* @param {Function} fn to call for each point *
* @param {boolean} [close=true] whether to close the loop (last point to first) * 1. Travel from fromPoint to closest point on polygon, to rampZ above that that point,
* @param {number} [start=0] starting index * 2. ease-down starts, following the polygonal path, decreasing Z at a fixed slope until target Z is hit,
* 3. then the rest of the path is completed and repeated at target Z until touchdown point is reached.
* 4. this function should probably move to CAM prepare since it's only called from there
*/ */
forEachPoint(fn, close, start) { forEachPointEaseDown(fn, fromPoint, degrees = 45) {
let index = this.findClosestPointTo(fromPoint).index,
fromZ = fromPoint.z,
offset = 0,
points = this.points,
length = points.length,
touch = -1, // first point to touch target z
targetZ = points[0].z,
dist2next,
last,
next,
done;
// Slope for computations.
const slope = Math.tan((degrees * Math.PI) / 180);
// Z height above polygon Z from which to start the ease-down.
// Machine will travel from "fromPoint" to "nearest point x, y, z' => with z' = point z + rampZ",
// then start the ease down along path.
const rampZ = 2.0;
while (true) {
next = points[index % length];
if (last && next.z < fromZ) {
// When "in Ease-Down" (ie. while target Z not yet reached) - follow path while slowly decreasing Z.
let deltaZ = fromZ - next.z;
dist2next = last.distTo2D(next);
let deltaZFullMove = dist2next * slope;
if (deltaZFullMove > deltaZ) {
// Too long: easing along full path would overshoot depth, synth intermediate point at target Z.
//
// XXX: please check my super basic trig - this should follow from `last` to `next` up until the
// intersect at the target Z distance.
fn(last.followTo(next, dist2next * deltaZ / deltaZFullMove).setZ(next.z), offset++);
} else {
// Ok: execute full move at desired slope.
next = next.clone().setZ(fromZ - deltaZFullMove);
}
fromZ = next.z;
} else if (offset === 0 && next.z < fromZ) {
// First point, move to rampZ height above next.
let deltaZ = fromZ - next.z;
fromZ = next.z + Math.min(deltaZ, rampZ)
next = next.clone().setZ(fromZ);
}
last = next;
fn(next, offset++);
if ((index % length) === touch) {
break;
}
if (touch < 0 && next.z <= targetZ) {
// Save touch-down index so as to be able to "complete" the full cut at target Z,
// i.e. keep following the path loop until the touch down point is reached again.
touch = ((index + length) % length);
}
index++;
}
return last;
}
forEachPoint(fn, close, start) {
let index = start || 0, let index = start || 0,
points = this.points, points = this.points,
length = points.length, length = points.length,
@ -1496,7 +1562,12 @@ class Polygon {
* @returns {number} 0.0 - 1.0 from flat to perfectly circular * @returns {number} 0.0 - 1.0 from flat to perfectly circular
*/ */
circularity() { circularity() {
return (4 * Math.PI * this.area()) / util.sqr(this.perimeter()); try {
return (4 * Math.PI * this.area()) / util.sqr(this.perimeter());
} catch (e) {
console.log(this.perimeter(), e);
return 0;
}
} }
circularityDeep() { circularityDeep() {
@ -2003,7 +2074,7 @@ class Polygon {
return res.map(array => { return res.map(array => {
let poly = newPolygon(); let poly = newPolygon();
for (let pt of array) { for (let pt of array) {
poly.push(base.pointFromClipper(pt, z)); poly.push(pointFromClipper(pt, z));
} }
return poly; return poly;
}); });
@ -2318,8 +2389,7 @@ class Polygon {
} }
// use Slope.angleDiff() then re-test path mitering / rendering export function slopeDiff(s1, s2) {
function slopeDiff(s1, s2) {
const n1 = s1.angle; const n1 = s1.angle;
const n2 = s2.angle; const n2 = s2.angle;
let diff = n2 - n1; let diff = n2 - n1;
@ -2328,28 +2398,17 @@ function slopeDiff(s1, s2) {
return Math.abs(diff); return Math.abs(diff);
} }
function fromClipperPath(path, z) { export function fromClipperPath(path, z) {
let poly = newPolygon(), let poly = newPolygon(),
i = 0, i = 0,
l = path.length; l = path.length;
while (i < l) { while (i < l) {
// poly.push(newPoint(null,null,z,null,path[i++])); // poly.push(newPoint(null,null,z,null,path[i++]));
poly.push(base.pointFromClipper(path[i++], z)); poly.push(pointFromClipper(path[i++], z));
} }
return poly; return poly;
} }
function newPolygon(points) { export function newPolygon(points) {
return new Polygon(points); return new Polygon(points);
} }
Polygon.fromArray = function(array) {
return newPolygon().fromArray(array);
};
gapp.overlay(base, {
Polygon,
newPolygon
});
});

View file

@ -2,45 +2,45 @@
"use strict"; "use strict";
// dep: geo.base import { base, util, config } from './base.js';
// dep: geo.point import { newPoint, pointFromClipper } from './point.js';
// use: ext.clip2 import { newPolygon } from './polygon.js';
// use: geo.slope import { newSlope } from './slope.js';
// use: geo.polygon import { paths } from './paths.js';
gapp.register("geo.polygons", [], (root, exports) => { import { ClipperLib } from '../ext/clip2.esm.js';
const { base } = root; const geo = base;
const { util, paths, config, newPoint } = base; const { numOrDefault } = util;
const { sqr, numOrDefault } = util;
const DEG2RAD = Math.PI / 180, const DEG2RAD = Math.PI / 180,
SQRT = Math.sqrt, SQRT = Math.sqrt,
SQR = util.sqr, SQR = util.sqr,
ABS = Math.abs; ABS = Math.abs;
const ClipperLib = self.ClipperLib, const {
Clipper = ClipperLib.Clipper, Clipper,
ClipType = ClipperLib.ClipType, ClipType,
PolyType = ClipperLib.PolyType, PolyType,
PolyFillType = ClipperLib.PolyFillType, PolyFillType,
CleanPolygon = Clipper.CleanPolygon, EndType,
JoinType,
PolyTree,
ClipperOffset
} = ClipperLib;
const CleanPolygon = Clipper.CleanPolygon,
CleanPolygons = Clipper.CleanPolygons, CleanPolygons = Clipper.CleanPolygons,
SimplifyPolygons = Clipper.SimplifyPolygons, SimplifyPolygons = Clipper.SimplifyPolygons,
FillNonZero = PolyFillType.pftNonZero, FillNonZero = PolyFillType.pftNonZero,
FillEvenOdd = PolyFillType.pftEvenOdd, FillEvenOdd = PolyFillType.pftEvenOdd,
PathSubject = PolyType.ptSubject, PathSubject = PolyType.ptSubject,
PathClip = PolyType.ptClip, PathClip = PolyType.ptClip,
EndType = ClipperLib.EndType,
JoinType = ClipperLib.JoinType,
PolyTree = ClipperLib.PolyTree,
ClipXOR = ClipType.ctXor, ClipXOR = ClipType.ctXor,
ClipDiff = ClipType.ctDifference, ClipDiff = ClipType.ctDifference,
ClipUnion = ClipType.ctUnion, ClipUnion = ClipType.ctUnion,
ClipIntersect = ClipType.ctIntersection, ClipIntersect = ClipType.ctIntersection;
ClipperOffset = ClipperLib.ClipperOffset
;
const POLYS = base.polygons = { const POLYS = {
clearInner, clearInner,
rayIntersect, rayIntersect,
alignWindings, alignWindings,
@ -73,14 +73,16 @@ const POLYS = base.polygons = {
fingerprint fingerprint
}; };
function outer(polys) { export { POLYS };
export function outer(polys) {
for (let p of polys) { for (let p of polys) {
p.inner = undefined; p.inner = undefined;
} }
return polys; return polys;
} }
function inner(polys) { export function inner(polys) {
const ret = []; const ret = [];
for (let p of polys) { for (let p of polys) {
if (p.inner) { if (p.inner) {
@ -90,7 +92,7 @@ function inner(polys) {
return ret; return ret;
} }
function length(polys) { export function length(polys) {
let length = 0; let length = 0;
for (let p of polys) { for (let p of polys) {
length += p.deepLength; length += p.deepLength;
@ -98,20 +100,20 @@ function length(polys) {
return length; return length;
} }
function setZ(polys, z) { export function setZ(polys, z) {
for (let poly of polys) { for (let poly of polys) {
poly.setZ(z); poly.setZ(z);
} }
return polys; return polys;
} }
function clearInner(polys) { export function clearInner(polys) {
for (let p of polys) { for (let p of polys) {
p.clearInner(); p.clearInner();
} }
} }
function toClipper(polys = []) { export function toClipper(polys = []) {
let out = []; let out = [];
for (let poly of polys) { for (let poly of polys) {
poly.toClipper(out); poly.toClipper(out);
@ -119,16 +121,16 @@ function toClipper(polys = []) {
return out; return out;
} }
function fromClipperNode(tnode, z) { export function fromClipperNode(tnode, z) {
let poly = base.newPolygon(); let poly = newPolygon();
for (let point of tnode.m_polygon) { for (let point of tnode.m_polygon) {
poly.push(base.pointFromClipper(point, z)); poly.push(pointFromClipper(point, z));
} }
poly.open = tnode.IsOpen; poly.open = tnode.IsOpen;
return poly; return poly;
}; }
function fromClipperTree(tnode, z, tops, parent, minarea) { export function fromClipperTree(tnode, z, tops, parent, minarea) {
let poly, let poly,
polys = tops || [], polys = tops || [],
min = numOrDefault(minarea, 0.1); min = numOrDefault(minarea, 0.1);
@ -150,9 +152,9 @@ function fromClipperTree(tnode, z, tops, parent, minarea) {
} }
return polys; return polys;
}; }
function fromClipperTreeUnion(tnode, z, minarea, tops, parent) { export function fromClipperTreeUnion(tnode, z, minarea, tops, parent) {
let polys = tops || [], poly; let polys = tops || [], poly;
for (let child of tnode.m_Childs) { for (let child of tnode.m_Childs) {
@ -171,9 +173,9 @@ function fromClipperTreeUnion(tnode, z, minarea, tops, parent) {
} }
return polys; return polys;
}; }
function cleanClipperTree(tree) { export function cleanClipperTree(tree) {
if (tree.m_Childs) if (tree.m_Childs)
for (let child of tree.m_Childs) { for (let child of tree.m_Childs) {
child.m_polygon = CleanPolygon(child.m_polygon, config.clipperClean); child.m_polygon = CleanPolygon(child.m_polygon, config.clipperClean);
@ -181,9 +183,9 @@ function cleanClipperTree(tree) {
} }
return tree; return tree;
}; }
function filter(array, output, fn) { export function filter(array, output, fn) {
for (let poly of array) { for (let poly of array) {
poly = fn(poly); poly = fn(poly);
if (poly) { if (poly) {
@ -197,14 +199,14 @@ function filter(array, output, fn) {
return output; return output;
} }
function points(polys) { export function points(polys) {
return polys.length ? polys.map(p => p.deepLength).reduce((a,v) => a+v) : 0; return polys.length ? polys.map(p => p.deepLength).reduce((a,v) => a+v) : 0;
} }
/** /**
* redo nesting of polygons that might already have inners * redo nesting of polygons that might already have inners
*/ */
function renest(polygons, deep) { export function renest(polygons, deep) {
return nest(flatten(polygons, [], true), deep); return nest(flatten(polygons, [], true), deep);
} }
@ -220,7 +222,7 @@ function renest(polygons, deep) {
* @param {boolean} opentop prevent open polygons from having inners * @param {boolean} opentop prevent open polygons from having inners
* @returns {Polygon[]} top level parent polygons * @returns {Polygon[]} top level parent polygons
*/ */
function nest(polygons, deep, opentop) { export function nest(polygons, deep, opentop) {
if (!polygons) { if (!polygons) {
return polygons; return polygons;
} }
@ -284,7 +286,7 @@ function nest(polygons, deep, opentop) {
* @param {boolean} CW * @param {boolean} CW
* @param {boolean} [recurse] * @param {boolean} [recurse]
*/ */
function setWinding(array, CW, recurse) { export function setWinding(array, CW, recurse) {
if (!array) return; if (!array) return;
let poly, i = 0; let poly, i = 0;
while (i < array.length) { while (i < array.length) {
@ -302,7 +304,7 @@ function setWinding(array, CW, recurse) {
* @param {Polygon[]} polys * @param {Polygon[]} polys
* @return {boolean} true if aligned clockwise * @return {boolean} true if aligned clockwise
*/ */
function alignWindings(polys) { export function alignWindings(polys) {
let len = polys.length, let len = polys.length,
fwd = 0, fwd = 0,
pts = 0, pts = 0,
@ -323,21 +325,14 @@ function alignWindings(polys) {
return setCW; return setCW;
} }
function setContains(setA, poly) { export function setContains(setA, poly) {
for (let i=0; i<setA.length; i++) { for (let i=0; i<setA.length; i++) {
if (setA[i].contains(poly)) return true; if (setA[i].contains(poly)) return true;
} }
return false; return false;
} }
/** export function flatten(polys, to, crush) {
* Flatten an array of polygons into a single array of polygons.
* @param {Polygon[]} polys - input array of polygons
* @param {Polygon[]} [to] - output array. if omitted, a new array will be created
* @param {boolean} [crush] - if true, remove the inner array after flattening
* @returns {Polygon[]} - the flattened array
*/
function flatten(polys, to, crush) {
to = to || []; to = to || [];
for (let poly of polys) { for (let poly of polys) {
poly.flattenTo(to); poly.flattenTo(to);
@ -358,13 +353,13 @@ function flatten(polys, to, crush) {
* @param {number} [minArea] * @param {number} [minArea]
* @returns {Polygon[]} out * @returns {Polygon[]} out
*/ */
function subtract(setA, setB, outA, outB, z, minArea, opt = {}) { export function subtract(setA, setB, outA, outB, z, minArea, opt = {}) {
let min = numOrDefault(minArea, 0.1), let min = numOrDefault(minArea, 0.1),
out = []; out = [];
function filter(from, to = []) { function filter(from, to = []) {
from.forEach(function(poly) { from.forEach(function(poly) {
if (poly.area() >= min) { if (poly.area() >= min && poly.length > 2) {
to.push(poly); to.push(poly);
out.push(poly); out.push(poly);
} }
@ -440,63 +435,63 @@ function subtract(setA, setB, outA, outB, z, minArea, opt = {}) {
* @param {Polygon[]} polys * @param {Polygon[]} polys
* @returns {Polygon[]} * @returns {Polygon[]}
*/ */
function union(polys, minarea, all, opt = {}) { export function union(polys, minarea, all, opt = {}) {
if (polys.length < 2) return polys; if (polys.length < 2) return polys;
let lpre = length(polys); let lpre = length(polys);
if (opt.wasm && geo.wasm) { if (opt.wasm && geo.wasm) {
let min = minarea ?? 0.01; let min = minarea ?? 0.01;
// let deepLength = polys.map(p => p.deepLength).reduce((a,v) => a+v); // let deepLength = polys.map(p => p.deepLength).reduce((a,v) => a+v);
// if (deepLength < 15000) // if (deepLength < 15000)
try { try {
let out = geo.wasm.js.union(polys, polys[0].getZ()).filter(p => p.area() > min); let out = geo.wasm.js.union(polys, polys[0].getZ()).filter(p => p.area() > min);
opt.changes = length(out) - lpre; opt.changes = length(out) - lpre;
return out; return out;
} catch (e) { } catch (e) {
console.log({union_fail: polys, minarea, all}); console.log({union_fail: polys, minarea, all});
} }
} }
let out = polys.slice(), i, j, union, uset = [], a, b; let out = polys.slice(), i, j, union, uset = [], a, b;
outer: for (i=0; i<out.length; i++) { outer: for (i=0; i<out.length; i++) {
if (!out[i]) continue; if (!out[i]) continue;
for (j=i+1; j<out.length; j++) { for (j=i+1; j<out.length; j++) {
if (!out[j]) continue; if (!out[j]) continue;
union = out[i].union(out[j], minarea, all); union = out[i].union(out[j], minarea, all);
if (union && union.length) { if (union && union.length) {
if (opt.onmerge) { if (opt.onmerge) {
a = out[i]; a = out[i];
b = out[j]; b = out[j];
} }
out[i] = null; out[i] = null;
out[j] = null; out[j] = null;
if (all) { if (all) {
out.appendAll(union); out.appendAll(union);
} else { } else {
out.push(union); out.push(union);
} }
if (opt.onmerge) { if (opt.onmerge) {
opt.onmerge(a, b, union); opt.onmerge(a, b, union);
} }
continue outer; continue outer;
} }
} }
} }
for (i=0; i<out.length; i++) { for (i=0; i<out.length; i++) {
if (out[i]) uset.push(out[i]); if (out[i]) uset.push(out[i]);
} }
opt.changes = length(uset) - lpre; opt.changes = length(uset) - lpre;
return uset; return uset;
} }
/** /**
* @param {Polygon} poly clipping mask * @param {Polygon} poly clipping mask
* @returns {?Polygon[]} * @returns {?Polygon[]}
*/ */
function diff(setA, setB, z) { export function diff(setA, setB, z) {
let clip = new Clipper(), let clip = new Clipper(),
tree = new PolyTree(), tree = new PolyTree(),
sp1 = toClipper(setA), sp1 = toClipper(setA),
@ -516,7 +511,7 @@ function diff(setA, setB, z) {
* @param {Polygon} poly clipping mask * @param {Polygon} poly clipping mask
* @returns {?Polygon[]} * @returns {?Polygon[]}
*/ */
function xor(set, z) { export function xor(set, z) {
z = z || set[0].getZ(); z = z || set[0].getZ();
outer: for (;;) { outer: for (;;) {
// sort largest to smallest area // sort largest to smallest area
@ -559,7 +554,7 @@ function diff(setA, setB, z) {
* @param {Polygon[]} setB mask set * @param {Polygon[]} setB mask set
* @returns {Polygon[]} * @returns {Polygon[]}
*/ */
function trimTo(setA, setB) { export function trimTo(setA, setB) {
// handle null/empty slices // handle null/empty slices
if (setA === setB || setA === null || setB === null) return null; if (setA === setB || setA === null || setB === null) return null;
@ -573,7 +568,7 @@ function trimTo(setA, setB) {
return out; return out;
} }
function sumCirc(polys) { export function sumCirc(polys) {
let sum = 0.0; let sum = 0.0;
polys.forEach(function(poly) { polys.forEach(function(poly) {
sum += poly.circularityDeep(); sum += poly.circularityDeep();
@ -591,7 +586,7 @@ function sumCirc(polys) {
* @param {Function} [collector] receives output of each pass * @param {Function} [collector] receives output of each pass
* @returns {Polygon[]} last offset * @returns {Polygon[]} last offset
*/ */
function expand(polys, distance, z, out, count, distance2, collector, min) { export function expand(polys, distance, z, out, count, distance2, collector, min) {
return offset(polys, [distance, distance2 || distance], { return offset(polys, [distance, distance2 || distance], {
z, outs: out, call: collector, minArea: min, count, flat: true z, outs: out, call: collector, minArea: min, count, flat: true
}); });
@ -602,11 +597,11 @@ function expand(polys, distance, z, out, count, distance2, collector, min) {
* and return resulting gaps from offsets for thin wall detection in * and return resulting gaps from offsets for thin wall detection in
* in FDM mode and uncleared areas in CAM mode. * in FDM mode and uncleared areas in CAM mode.
*/ */
function offset(polys, dist, opts = {}) { export function offset(polys, dist, opts = {}) {
let open = opts.open ? polys.filter(p => p.open) : []; let open = opts.open ? polys.filter(p => p.open) : [];
if (open.length) { if (open.length) {
open = open.map(p => paths.pointsToPath(p.points, dist, true)); open = open.map(p => paths.pointsToPath(p.points, dist, true));
open = open.map(p => base.newPolygon().setOpen().addPoints(p.right)); open = open.map(p => newPolygon().setOpen().addPoints(p.right));
} }
// do not use clipper to offset open lines // do not use clipper to offset open lines
@ -706,7 +701,7 @@ function offset(polys, dist, opts = {}) {
* as performing subtractive analysis between initial layer shell (ref) * as performing subtractive analysis between initial layer shell (ref)
* and last offset (cmp) to produce gap candidates (for thinfill) * and last offset (cmp) to produce gap candidates (for thinfill)
*/ */
function inset(polys, dist, count, z, wasm) { export function inset(polys, dist, count, z, wasm) {
let total = count; let total = count;
let layers = []; let layers = [];
let ref = polys; let ref = polys;
@ -749,7 +744,7 @@ function inset(polys, dist, count, z, wasm) {
* @param {number} [maxLen] * @param {number} [maxLen]
* @returns {Point[]} supplied output or new array * @returns {Point[]} supplied output or new array
*/ */
function fillArea(polys, angle, spacing, output, minLen, maxLen) { export function fillArea(polys, angle, spacing, output, minLen, maxLen) {
if (polys.length === 0) return; if (polys.length === 0) return;
let i = 1, let i = 1,
@ -771,7 +766,7 @@ function fillArea(polys, angle, spacing, output, minLen, maxLen) {
while (angle > 90) angle -= 180; while (angle > 90) angle -= 180;
// X,Y ray slope derived from angle // X,Y ray slope derived from angle
raySlope = base.newSlope(0,0, raySlope = newSlope(0,0,
Math.cos(angle * DEG2RAD) * spacing, Math.cos(angle * DEG2RAD) * spacing,
Math.sin(angle * DEG2RAD) * spacing Math.sin(angle * DEG2RAD) * spacing
); );
@ -843,8 +838,8 @@ function fillArea(polys, angle, spacing, output, minLen, maxLen) {
if (minlen && plen < minlen) continue; if (minlen && plen < minlen) continue;
if (maxlen && plen > maxlen) continue; if (maxlen && plen > maxlen) continue;
} }
let p1 = base.pointFromClipper(poly.m_polygon[0], zpos); let p1 = pointFromClipper(poly.m_polygon[0], zpos);
let p2 = base.pointFromClipper(poly.m_polygon[1], zpos); let p2 = pointFromClipper(poly.m_polygon[1], zpos);
let od = rayint.origin.distToLineNew(p1,p2) / spacing; let od = rayint.origin.distToLineNew(p1,p2) / spacing;
lines.push([p1, p2, od]); lines.push([p1, p2, od]);
} }
@ -875,7 +870,7 @@ function fillArea(polys, angle, spacing, output, minLen, maxLen) {
* @param {boolean} [for_fill] * @param {boolean} [for_fill]
* @returns {Point[]} * @returns {Point[]}
*/ */
function rayIntersect(start, slope, polygons, for_fill) { export function rayIntersect(start, slope, polygons, for_fill) {
let i = 0, let i = 0,
flat = [], flat = [],
points = [], points = [],
@ -1004,11 +999,11 @@ function rayIntersect(start, slope, polygons, for_fill) {
return points; return points;
} }
function pd(a,b) { export function pd(a,b) {
return a > b ? Math.abs(1-b/a) : Math.abs(1-a/b); return a > b ? Math.abs(1-b/a) : Math.abs(1-a/b);
} }
function fingerprint(polys) { export function fingerprint(polys) {
let recs = flatten(polys).map(p => { let recs = flatten(polys).map(p => {
return { return {
l: p.length, l: p.length,
@ -1037,7 +1032,7 @@ function fingerprint(polys) {
} }
// compare fingerprint arrays // compare fingerprint arrays
function fingerprintCompare(a, b) { export function fingerprintCompare(a, b) {
// true if array is the same object // true if array is the same object
if (a === b) { if (a === b) {
return true; return true;
@ -1079,7 +1074,7 @@ function fingerprintCompare(a, b) {
// plan a route through an array of polygon center points // plan a route through an array of polygon center points
// starting with the polygon center closest to "start" // starting with the polygon center closest to "start"
function route(polys, start) { export function route(polys, start) {
let centers = []; let centers = [];
let first, minDist = Infinity; let first, minDist = Infinity;
for (let poly of polys) { for (let poly of polys) {
@ -1116,4 +1111,4 @@ function route(polys, start) {
return routed.map(r => r.poly); return routed.map(r => r.poly);
} }
}); export const polygons = POLYS;

View file

@ -1,23 +1,16 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
/** /**
* basic slice and line connection. In future, replace kiri's fdm and cam slicers * basic slice and line connection. In future, replace kiri's fdm and cam slicers
* with wrappers on this one. * with wrappers on this one.
*/ */
// dep: geo.base
// dep: geo.line
// dep: geo.point
// dep: geo.polygon
// dep: geo.polygons
gapp.register("geo.slicer", [], (root, exports) => {
const { base } = root; import { base } from './base.js';
const { config, util, polygons } = base import { newOrderedLine } from './line.js';
const { newOrderedLine, newPolygon, newPoint } = base; import { newPolygon } from '../geo/polygon.js';
import { polygons } from '../geo/polygons.js';
const POLY = base.polygons; import { newPoint } from '../geo/point.js';
import { config } from '../geo/base.js';
function dval(v, dv) { function dval(v, dv) {
return v !== undefined ? v : dv; return v !== undefined ? v : dv;
@ -32,7 +25,7 @@ function dval(v, dv) {
* @param {Point[]} points vertex array * @param {Point[]} points vertex array
* @param {Object} options slicing parameters * @param {Object} options slicing parameters
*/ */
async function slice(points, options = {}) { export async function slice(points, options = {}) {
let zMin = options.zMin || 0, let zMin = options.zMin || 0,
zMax = options.zMax || 0, zMax = options.zMax || 0,
zInc = options.zInc || 0, zInc = options.zInc || 0,
@ -72,7 +65,6 @@ async function slice(points, options = {}) {
for (i = 0; i < points.length; i++) { for (i = 0; i < points.length; i++) {
points[i] = points[i].round(3); points[i] = points[i].round(3);
} }
} }
// gather z-index stats // gather z-index stats
@ -86,7 +78,7 @@ async function slice(points, options = {}) {
if (p1.z === p2.z && p2.z === p3.z && p1.z >= zMin) { if (p1.z === p2.z && p2.z === p3.z && p1.z >= zMin) {
// detect faces co-planar with Z and sum the enclosed area // detect faces co-planar with Z and sum the enclosed area
let zkey = p1.z, let zkey = p1.z,
area = Math.abs(util.area2(p1,p2,p3)) / 2; area = Math.abs(base.util.area2(p1,p2,p3)) / 2;
if (!zFlat[zkey]) { if (!zFlat[zkey]) {
zFlat[zkey] = area; zFlat[zkey] = area;
} else { } else {
@ -322,7 +314,7 @@ function makeZLine(phash, p1, p2, coplanar, edge) {
* *
* @param {number} z * @param {number} z
*/ */
async function sliceZ(z, points, options = {}) { export async function sliceZ(z, points, options = {}) {
if (Array.isArray(z)) { if (Array.isArray(z)) {
return Promise.all(z.map(z => sliceZ(z, points, options))); return Promise.all(z.map(z => sliceZ(z, points, options)));
} }
@ -388,14 +380,14 @@ async function sliceZ(z, points, options = {}) {
if (groupFn) { if (groupFn) {
let groups = groupFn(lines, z, options); let groups = groupFn(lines, z, options);
if (options.xor) { if (options.xor) {
groups = POLY.xor(groups); groups = polygons.xor(groups);
} }
if (options.union) { if (options.union) {
let points = groups.map(p => p.length); let points = groups.map(p => p.length);
if (points.length > 1) points = points.reduce((a,b) => a + b); if (points.length > 1) points = points.reduce((a,b) => a + b);
// simplistic healing of non-manifold meshes // simplistic healing of non-manifold meshes
let opt = { x: 1 }; let opt = { x: 1 };
let union = POLY.union(POLY.nest(groups), 0.1, true, opt); let union = polygons.union(polygons.nest(groups), 0.1, true, opt);
// fall back to xor'ing polygons that might overlap // fall back to xor'ing polygons that might overlap
// when one does not cleanly contain the other and we lose lots of points // when one does not cleanly contain the other and we lose lots of points
// trigger when 2 polygons and we lose > 40% of points in the union // trigger when 2 polygons and we lose > 40% of points in the union
@ -409,14 +401,14 @@ async function sliceZ(z, points, options = {}) {
} }
// track total poly length changes to determine if healed // track total poly length changes to determine if healed
rval.changes = opt.changes; rval.changes = opt.changes;
groups = POLY.flatten(union, null, true); groups = polygons.flatten(union, null, true);
} }
rval.groups = groups; rval.groups = groups;
} }
// look for driver-specific slice post-processor // look for driver-specific slice post-processor
if (options.post) { if (options.post) {
let fn = base.slicePost[options.post]; let fn = slicer.slicePost[options.post];
if (fn) fn(rval, options); if (fn) fn(rval, options);
} }
@ -458,7 +450,7 @@ async function sliceZ(z, points, options = {}) {
* @param {number} [index] * @param {number} [index]
* @returns {Array} * @returns {Array}
*/ */
function sliceConnect(input, z, opt = {}) { export function sliceConnect(input, z, opt = {}) {
let { debug, both } = opt; let { debug, both } = opt;
if (both) { if (both) {
@ -782,7 +774,7 @@ function sliceConnect(input, z, opt = {}) {
* @param {Line[]} lines * @param {Line[]} lines
* @returns {Line[]} * @returns {Line[]}
*/ */
function removeDuplicateLines(lines) { export function removeDuplicateLines(lines) {
let output = [], let output = [],
tmplines = [], tmplines = [],
points = [], points = [],
@ -873,12 +865,11 @@ function removeDuplicateLines(lines) {
return output; return output;
} }
gapp.overlay(base, { export const slicer = {
slice, slice,
sliceZ, sliceZ,
slicePost: {}, slicePost: {},
sliceDedup: removeDuplicateLines, sliceDedup: removeDuplicateLines,
sliceConnect sliceConnect
}); }
});

View file

@ -2,19 +2,13 @@
"use strict"; "use strict";
// dep: geo.base import { config } from './base.js';
// use: geo.line
// use: geo.point
gapp.register("geo.slope", [], (root, exports) => {
const { base } = root;
const { config } = base;
const ABS = Math.abs, const ABS = Math.abs,
DEG2RAD = Math.PI / 180, DEG2RAD = Math.PI / 180,
RAD2DEG = 180 / Math.PI; RAD2DEG = 180 / Math.PI;
class Slope { export class Slope {
constructor(p1, p2, dx, dy) { constructor(p1, p2, dx, dy) {
this.dx = p1 && p2 ? p2.x - p1.x : dx; this.dx = p1 && p2 ? p2.x - p1.x : dx;
this.dy = p1 && p2 ? p2.y - p1.y : dy; this.dy = p1 && p2 ? p2.y - p1.y : dy;
@ -94,21 +88,13 @@ function angleWithinDelta(a1, a2, delta) {
return (ABS(a1-a2) <= delta || 360-ABS(a1-a2) <= delta); return (ABS(a1-a2) <= delta || 360-ABS(a1-a2) <= delta);
} }
function newSlope(p1, p2, dx, dy) { export function newSlope(p1, p2, dx, dy) {
return new Slope(p1, p2, dx, dy); return new Slope(p1, p2, dx, dy);
} }
function newSlopeFromAngle(angle) { export function newSlopeFromAngle(angle) {
return newSlope(0,0, return newSlope(0,0,
Math.cos(angle * DEG2RAD), Math.cos(angle * DEG2RAD),
Math.sin(angle * DEG2RAD) Math.sin(angle * DEG2RAD)
); );
} }
gapp.overlay(base, {
Slope,
newSlope,
newSlopeFromAngle
});
});

View file

@ -1,16 +1,12 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { base } from '../geo/base.js';
import { newPolygon } from './polygon.js';
// dep: geo.base
// dep: ext.clip2
gapp.register("geo.wasm", [], (root, exports) => {
const { base } = root;
const { config } = base; const { config } = base;
const factor = config.clipper; const factor = config.clipper;
const wasm_ctrl = {
export const wasm_ctrl = {
enable, enable,
disable, disable,
count: { count: {
@ -20,8 +16,6 @@ const wasm_ctrl = {
} }
}; };
gapp.overlay(base, { wasm_ctrl });
function log() { function log() {
console.log(...arguments); console.log(...arguments);
} }
@ -60,7 +54,7 @@ function writePoly(view, poly, inner) {
function readPoly(view, z) { function readPoly(view, z) {
let points = view.readU16(true); let points = view.readU16(true);
if (points === 0) return; if (points === 0) return;
let poly = self.base.newPolygon(); let poly = newPolygon();
while (points-- > 0) { while (points-- > 0) {
poly.add(view.readI32(true)/factor, view.readI32(true)/factor, z || 0); poly.add(view.readI32(true)/factor, view.readI32(true)/factor, z || 0);
} }
@ -79,7 +73,7 @@ function readPolys(view, z, out = []) {
return out; return out;
} }
function polyOffset(polys, offset, z, clean, simple) { export function polyOffset(polys, offset, z, clean, simple) {
wasm_ctrl.count.offset++; wasm_ctrl.count.offset++;
let wasm = base.wasm, let wasm = base.wasm,
buffer = wasm.shared, buffer = wasm.shared,
@ -89,7 +83,7 @@ function polyOffset(polys, offset, z, clean, simple) {
return polyNest(out); return polyNest(out);
} }
function polyUnion(polys, z) { export function polyUnion(polys, z) {
wasm_ctrl.count.union++; wasm_ctrl.count.union++;
let wasm = base.wasm, let wasm = base.wasm,
buffer = wasm.shared, buffer = wasm.shared,
@ -99,7 +93,7 @@ function polyUnion(polys, z) {
return polyNest(out); return polyNest(out);
} }
function polyDiff(polysA, polysB, z, AB, BA) { export function polyDiff(polysA, polysB, z, AB, BA) {
wasm_ctrl.count.diff++; wasm_ctrl.count.diff++;
let wasm = base.wasm, let wasm = base.wasm,
buffer = wasm.shared, buffer = wasm.shared,
@ -153,7 +147,7 @@ function readString(pos, len) {
return out.join(''); return out.join('');
} }
function enable() { export function enable() {
if (base.wasm || base._wasm) { if (base.wasm || base._wasm) {
return; return;
} }
@ -200,11 +194,9 @@ function enable() {
}); });
} }
function disable() { export function disable() {
if (base.wasm) { if (base.wasm) {
base.wasm.free(base.wasm.shared);
delete base.wasm; delete base.wasm;
// console.log({disabled: geo});
} }
} }
});

View file

@ -1,648 +0,0 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: kiri-mode.cam.driver
// dep: kiri-mode.cam.animate2
gapp.register("kiri-mode.cam.animate", [], (root, exports) => {
const { kiri } = root;
const { driver } = kiri;
const { CAM } = driver;
const asLines = false;
const asPoints = false;
// ---( CLIENT FUNCTIONS )---
// tint points below z=0 with red
function add_red_neg_z(material) {
material.onBeforeCompile = (shader) => {
shader.vertexShader = shader.vertexShader.replace(
`#include <worldpos_vertex>`,
`
#include <worldpos_vertex>
vWorldPosition = vec3(transformed);
`
);
shader.vertexShader = `
varying vec3 vWorldPosition;
` + shader.vertexShader;
shader.fragmentShader = `
varying vec3 vWorldPosition;
` + shader.fragmentShader;
shader.fragmentShader = shader.fragmentShader.replace(
`#include <dithering_fragment>`,
`
#include <dithering_fragment>
if (vWorldPosition.z < 0.0) {
gl_FragColor.rgb += vec3(0.5, 0.0, 0.0); // Add red tint
}
`
);
};
return material;
}
kiri.load(() => {
if (!kiri.client) {
return;
}
let meshes = {},
button = {},
label = {},
unitScale = 1,
speedValues = [ 1, 2, 4, 8, 32 ],
speedPauses = [ 30, 20, 10, 5, 0 ],
speedNames = [ "1x", "2x", "4x", "8x", "!!" ],
speedMax = speedValues.length - 1,
speedIndex = 0,
speed,
color = 0,
material,
origin,
posOffset = { x:0, y:0, z:0 };
const { moto } = root;
const { space } = moto;
const { api } = kiri;
function animate_clear(api) {
let { anim } = api.ui;
moto.space.platform.showGridBelow(true);
kiri.client.animate_cleanup();
Object.keys(meshes).forEach(id => deleteMesh(id));
toggleStock(undefined,true,false);
api.uc.setVisible(anim.laba, false);
api.uc.setVisible(anim.vala, false);
}
function animate(api, delay) {
let alert = api.alerts.show("building animation");
let settings = api.conf.get();
kiri.client.animate_setup(settings, data => {
checkMeshCommands(data);
if (!(data && data.mesh_add)) {
return;
}
let { anim } = api.ui;
Object.assign(button, {
replay: anim.replay,
play: anim.play,
step: anim.step,
pause: anim.pause,
speed: anim.speed,
trans: anim.trans,
model: anim.model,
shade: anim.shade
});
Object.assign(label, {
progress: anim.progress,
speed: anim.labspd,
x: anim.valx,
y: anim.valy,
z: anim.valz,
});
origin = settings.origin;
speedIndex = api.local.getInt('cam.anim.speed') || 0;
updateSpeed();
setTimeout(step, delay || 0);
button.replay.onclick = replay;
button.play.onclick = play;
button.step.onclick = step;
button.pause.onclick = pause;
button.speed.onclick = fast;
button.trans.onclick = toggleTrans;
button.model.onclick = toggleModel;
button.shade.onclick = toggleStock;
button.play.style.display = '';
button.pause.style.display = 'none';
api.event.emit('animate', 'CAM');
api.alerts.hide(alert);
moto.space.platform.showGridBelow(false);
toggleTrans(0,api.local.getBoolean('cam.anim.trans', true));
toggleModel(0,api.local.getBoolean('cam.anim.model', false));
toggleStock(0,api.local.getBoolean('cam.anim.stock', false));
});
}
gapp.overlay(kiri.client, {
animate(data, ondone) {
kiri.client.send("animate", data, ondone);
},
animate_setup(settings, ondone) {
color = settings.controller.dark ? 0x48607B : 0x607FA4;
unitScale = settings.controller.units === 'in' ? 1/25.4 : 1;
let flatShading = true,
transparent = true,
opacity = 0.9,
side = THREE.DoubleSide;
material = new THREE.MeshPhongMaterial({
flatShading,
transparent,
opacity,
color,
side
});
add_red_neg_z(material);
kiri.client.send("animate_setup", {settings}, ondone);
},
animate_cleanup(data, ondone) {
kiri.client.send("animate_cleanup", data, ondone);
}
});
gapp.overlay(CAM, {
animate,
animate_clear
});
function meshAdd(id, ind, pos, sab) {
const geo = new THREE.BufferGeometry();
if (sab) {
// use array buffer shared with worker
pos = new Float32Array(sab);
}
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
if (ind.length) {
geo.setIndex(new THREE.BufferAttribute(new Uint32Array(ind), 1));
}
let mesh;
if (asPoints) {
const mat = new THREE.PointsMaterial({
transparent: true,
opacity: 0.75,
color: 0x888888,
size: 0.3
});
mesh = new THREE.Points(geo, mat);
} else if (asLines) {
const mat = new THREE.LineBasicMaterial({
transparent: true,
opacity: 0.75,
color
});
mesh = new THREE.LineSegments(geo, mat);
} else {
geo.computeVertexNormals();
mesh = new THREE.Mesh(geo, material);
mesh.renderOrder = -10;
}
space.world.add(mesh);
meshes[id] = mesh;
}
function meshUpdates(id) {
const mesh = meshes[id];
if (!mesh) {
return; // animate cancelled
}
mesh.geometry.attributes.position.needsUpdate = true;
space.update();
}
function deleteMesh(id) {
space.world.remove(meshes[id]);
delete meshes[id];
}
function toggleModel(ev,bool) {
api.local.toggle('cam.anim.model', bool);
api.widgets.all().forEach(w => w.toggleVisibility(bool));
}
function toggleStock(ev,bool,set) {
set !== false && api.local.toggle('cam.anim.stock', bool);
return api.event.emit('cam.stock.toggle', bool ?? undefined);
}
function toggleTrans(ev,bool) {
bool = api.local.toggle('cam.anim.trans', bool);
material.transparent = bool;
material.needsUpdate = true;
}
function step() {
updateSpeed();
kiri.client.animate({speed, steps: 1}, handleGridUpdate);
}
function play(opts) {
const { steps } = opts;
updateSpeed();
if (steps !== 1) {
button.play.style.display = 'none';
button.pause.style.display = '';
}
kiri.client.animate({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
}, handleGridUpdate);
}
function fast(opts) {
const { steps } = opts;
updateSpeed(1);
button.play.style.display = 'none';
button.pause.style.display = '';
kiri.client.animate({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
}, handleGridUpdate);
}
function pause() {
button.play.style.display = '';
button.pause.style.display = 'none';
kiri.client.animate({speed: 0}, handleGridUpdate);
}
function handleGridUpdate(data) {
checkMeshCommands(data);
if (data && data.progress) {
label.progress.value = (data.progress * 100).toFixed(1);
}
}
function updateSpeed(inc = 0) {
if (inc === Infinity) {
speedIndex = speedMax;
} else if (inc > 0) {
speedIndex = (speedIndex + inc) % speedValues.length;
}
api.local.set('cam.anim.speed', speedIndex);
speed = speedValues[speedIndex];
label.speed.value = speedNames[speedIndex];
}
function replay() {
animate_clear(api);
setTimeout(() => {
animate(api, 50);
}, 250);
}
function checkMeshCommands(data) {
if (!data) {
return;
}
if (data.mesh_add) {
const { id, ind, pos, offset, sab } = data.mesh_add;
meshAdd(id, ind, pos, sab);
space.refresh();
if (offset) {
posOffset = offset;
}
}
if (data.mesh_del) {
deleteMesh(data.mesh_del);
}
if (data.mesh_move) {
const { id, pos } = data.mesh_move;
const mesh = meshes[id];
if (mesh) {
mesh.position.x = pos.x;
mesh.position.y = pos.y;
mesh.position.z = pos.z;
space.update();
}
label.x.value = (pos.x - origin.x).toFixed(2);
label.y.value = (pos.y + origin.y).toFixed(2);
label.z.value = (pos.z - origin.z).toFixed(2);
}
if (data.mesh_update) {
meshUpdates(data.id);
}
}
});
// ---( WORKER FUNCTIONS )---
kiri.load(() => {
if (!kiri.worker) {
return;
}
let stock, center, grid, gridX, gridY, rez;
let path, pathIndex, tool, tools, last, toolID = 1;
kiri.worker.animate_setup = function(data, send) {
const { settings } = data;
const { process } = settings;
const print = worker.print;
const density = parseInt(settings.controller.animesh) * 1000;
pathIndex = 0;
path = print.output.flat();
tools = settings.tools;
stock = settings.stock;
rez = 1/Math.sqrt(density/(stock.x * stock.y));
//destructure arcs into path points
path = path.map(o=>
o.arcPoints
? [
...o.arcPoints.map(point=>
({
...o,
point
})
),
o
]
: o
)
.flat();
const step = rez;
const stepsX = Math.floor(stock.x / step);
const stepsY = Math.floor(stock.y / step);
const { pos, ind, sab } = createGrid(stepsX, stepsY, stock, step, true);
const offset = {
x: process.camOriginCenter ? 0 : stock.x / 2,
y: process.camOriginCenter ? 0 : stock.y / 2,
z: process.camOriginTop ? -stock.z : 0
}
grid = pos;
gridX = stepsX;
gridY = stepsY;
tool = null;
last = null;
animating = false;
animateClear = false;
center = Object.assign({}, stock.center);
center.z -= stock.z / 2;
send.data({ mesh_add: { id: 0, ind, offset, sab } }, [ ]); // sab not transferrable
send.data({ mesh_move: { id: 0, pos: center } });
send.done();
};
kiri.worker.animate = function(data, send) {
renderPause = data.pause || renderPause;
renderSpeed = data.speed || 0;
if (animating) {
return send.done();
}
renderSteps = data.steps || 1;
renderDone = false;
animating = renderSpeed > 0;
renderPath(send);
};
kiri.worker.animate_cleanup = function(data, send) {
if (animating) {
animateClear = true;
}
};
function createGrid(stepsX, stepsY, size, step, stock) {
const gridPoints = stepsX * stepsY;
const sab = new SharedArrayBuffer(gridPoints * 3 * 4)
const pos = new Float32Array(sab);
const ind = [];
const ox = size.x / 2;
const oy = size.y / 2;
let ex = stepsX - 1;
let ey = stepsY - 1;
// initialize grid points
for (let x=0, ai=0; x<stepsX; x++) {
for (let y=0; y<stepsY; y++) {
let px = pos[ai++] = x * step - ox + step / 2;
let py = pos[ai++] = y * step - oy + step / 2;
pos[ai++] = stock && (x * y === 0 || x === ex || y === ey) ? 0 : size.z;
if (asPoints) {
continue;
}
if (asLines) {
if (y > 0) ind.appendAll([
(stepsY * x) + (y - 1),
(stepsY * x) + (y )
]);
if (x > 0) ind.appendAll([
(stepsY * (x - 1)) + y,
(stepsY * (x )) + y
]);
} else {
if (x > 0 && y > 0) {
let v0 = stepsY * (x - 1) + y - 1;
let v1 = stepsY * (x - 0) + y - 1;
let v2 = stepsY * (x - 0) + y;
let v3 = stepsY * (x - 1) + y;
ind.appendAll([
v0, v1, v2, v0, v2, v3
]);
}
}
}
}
return { pos, ind, sab };
}
let animateClear = false;
let animating = false;
let renderDist = 0;
let renderDone = false;
let renderPause = 10;
let renderSteps = 0;
let renderSpeed = 0;
let skipMove = null;
let toolUpdate;
let depth = 0;
// send latest tool position and progress bar
function renderUpdate(send) {
if (toolUpdate) {
send.data(toolUpdate);
}
send.data({ progress: pathIndex / path.length, id: 0, mesh_update: 1 });
}
function renderPath(send) {
if (renderDone) {
return;
}
if (renderSteps-- === 0) {
animating = false;
renderPath(send);
return;
}
if (animating === false || animateClear || renderSpeed === 0) {
renderUpdate(send);
renderDone = true;
animating = false;
animateClear = false;
send.done();
return;
}
let next = path[pathIndex];
while (next && next.type === 'laser') {
last = next;
next = path[++pathIndex];
}
if (!next) {
animating = false;
renderPath(send);
return;
}
pathIndex++;
const firstTool = !tool && next.tool;
const toolChange = !firstTool && (tool.getID() !== next.tool.getID());
if (firstTool || toolChange) {
// on real tool change, go to safe Z first
if (tool && last.point) {
let pos = last.point = {
x: last.point.x,
y: last.point.y,
z: stock.z
};
send.data({ mesh_move: { toolID, pos }});
}
updateTool(next.tool, send);
}
const id = toolID;
const rezstep = rez;
if (last) {
const lp = last.point, np = next.point;
last = next;
// dwell ops have no point
if (!np || !lp) {
return renderPath(send);
}
const dx = np.x - lp.x, dy = np.y - lp.y, dz = np.z - lp.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
renderDist += dist;
// skip moves that are less than grid resolution
if (renderDist < rezstep) {
renderPath(send);
return;
}
const md = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz));
const st = Math.ceil(md / rezstep);
const mx = dx / st, my = dy / st, mz = dz / st;
const moves = [];
for (let i=0, x=lp.x, y=lp.y, z=lp.z; i<st; i++) {
moves.push({x,y,z});
x += mx;
y += my;
z += mz;
}
moves.push(next.point);
renderMoves(id, moves, send);
} else {
last = next;
if (tool) {
tool.pos = next.point;
toolUpdate = { mesh_move: { id, pos: next.point }};
}
renderPath(send);
}
}
function renderMoves(id, moves, send, seed = 0) {
for (let index = seed; index<moves.length; index++) {
const pos = moves[index];
if (!pos) {
throw `no pos @ ${index} of ${moves.length}`;
}
tool.pos = pos;
deformMesh(pos, send);
toolUpdate = { mesh_move: { id, pos }};
// pause renderer at specified offsets
if ((renderSpeed && renderDist >= renderSpeed) || (depth > 600)) {
renderDist = depth = 0;
renderUpdate(send);
setTimeout(() => {
renderMoves(id, moves, send, index);
}, renderPause);
return;
}
}
depth++;
renderPath(send);
}
// update stock mesh to reflect tool tip geometry at given XYZ position
function deformMesh(pos, send) {
const prof = tool.profile;
const { size, pix } = tool.profileDim;
const mid = Math.floor(pix / 2);
const rx = Math.floor((pos.x + stock.x / 2 - size / 2 - center.x) / rez);
const ry = Math.floor((pos.y + stock.y / 2 - size / 2 - center.y) / rez);
let upos = 0;
// deform mesh to lowest point on tool profile
for (let i=0, il=prof.length; i < il; ) {
const dx = mid + prof[i++];
const dy = mid + prof[i++];
const dz = prof[i++];
const gx = rx + dx;
const gy = ry + dy;
if (gx < 0|| gy < 0 || gx > gridX-1 || gy > gridY-1) {
continue;
}
const gi = gx * gridY + gy;
const iz = gi * 3 + 2;
const cz = grid[iz];
const tz = tool.pos.z - dz;
if (tz < cz) {
upos++;
grid[iz] = tz;
}
}
}
function updateTool(toolobj, send) {
if (tool) {
send.data({ mesh_del: toolID });
}
tool = new CAM.Tool({ tools }, toolobj.getID());
tool.generateProfile(rez);
const flen = tool.fluteLength() || 15;
const slen = tool.shaftLength() || 15;
// const frad = tool.fluteDiameter() / 2;
const prof = tool.profile;
const { size, pix } = tool.profileDim;
const { pos, ind, sab } = createGrid(pix, pix, {x:size, y:size, z:flen+slen}, rez);
const mid = Math.floor(pix/2);
// deform mesh to fit tool profile
for (let i=0, il=prof.length; i < il; ) {
const dx = mid + prof[i++];
const dy = mid + prof[i++];
const dz = prof[i++];
pos[(dx * pix + dy) * 3 + 2] = -dz;
}
send.data({ mesh_add: { id:++toolID, ind, sab }});
}
});
});

View file

@ -1,670 +0,0 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: geo.csg
// dep: kiri-mode.cam.driver
gapp.register("kiri-mode.cam.animate2", [], (root) => {
const { kiri } = root;
const { driver } = kiri;
const { CAM } = driver;
// ---( CLIENT FUNCTIONS )---
kiri.load(() => {
if (!kiri.client) {
return;
}
let meshes = {},
button = {},
label = {},
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;
const { moto } = root;
const { space } = moto;
const { api } = kiri;
function animate_clear2(api) {
let { anim } = api.ui;
kiri.client.animate_cleanup2();
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";
}
function animate2(api, delay) {
let alert = api.alerts.show("building animation");
let settings = api.conf.get();
kiri.client.animate_setup2(settings, data => {
handleUpdate(data);
if (data) {
return;
}
let { anim } = api.ui;
Object.assign(button, {
replay: anim.replay,
play: anim.play,
step: anim.step,
pause: anim.pause,
speed: anim.speed,
trans: anim.trans,
model: anim.model,
shade: anim.shade
});
Object.assign(label, {
progress: anim.progress,
speed: anim.labspd,
x: anim.valx,
y: anim.valy,
z: anim.valz,
a: anim.vala
});
updateSpeed(0);
setTimeout(step, delay || 0);
toggleTrans(undefined, false);
origin = settings.origin;
button.replay.onclick = replay;
button.play.onclick = play;
button.step.onclick = step;
button.pause.onclick = pause;
button.speed.onclick = fast;
button.trans.onclick = toggleTrans;
button.model.onclick = toggleModel;
button.shade.onclick = toggleStock;
button.play.style.display = '';
button.pause.style.display = 'none';
api.event.emit('animate', 'CAM');
api.alerts.hide(alert);
});
}
gapp.overlay(kiri.client, {
animate2(data, ondone) {
kiri.client.send("animate2", data, ondone);
},
animate_setup2(settings, ondone) {
color = settings.controller.dark ? 0x888888 : 0;
material = new THREE.MeshMatcapMaterial({
flatShading: true,
transparent: false,
opacity: 0.9,
color: 0x888888,
side: THREE.DoubleSide
});
kiri.client.send("animate_setup2", {settings}, ondone);
},
animate_cleanup2(data, ondone) {
kiri.client.send("animate_cleanup2", data, ondone);
}
});
gapp.overlay(CAM, {
animate2,
animate_clear2
});
function meshAdd(id, ind, pos, ilen, plen) {
const geo = new THREE.BufferGeometry();
const pa = plen ? pos.subarray(0, plen * 3) : pos;
const ia = ilen ? ind.subarray(0, ilen) : ind;
geo.setAttribute('position', new THREE.BufferAttribute(pa, 3));
geo.setIndex(new THREE.BufferAttribute(ia, 1));
const mesh = new THREE.Mesh(geo, material);
mesh.pos = pos;
mesh.ind = ind;
space.world.add(mesh);
meshes[id] = mesh;
}
function meshUpdate(id, ind, pos, ilen, plen) {
const mesh = meshes[id];
if (!mesh) {
return; // animate cancelled
}
const geo = mesh.geometry;
mesh.pos = pos || mesh.pos;
mesh.ind = ind || mesh.ind;
geo.setAttribute('position', new THREE.BufferAttribute(mesh.pos.subarray(0, plen * 3), 3));
geo.setIndex(new THREE.BufferAttribute(mesh.ind.subarray(0, ilen), 1));
geo.attributes.position.needsUpdate = true;
geo.index.needsUpdate = true;
space.update();
}
function deleteMesh(id) {
space.world.remove(meshes[id]);
delete meshes[id];
}
function toggleModel(ev,bool) {
api.local.toggle('cam.anim.model', bool);
api.widgets.all().forEach(w => w.toggleVisibility(bool));
}
function toggleStock(ev,bool,set) {
set !== false && api.local.toggle('cam.anim.stock', bool);
return api.event.emit('cam.stock.toggle', bool ?? undefined);
}
function toggleTrans(ev,bool) {
bool = api.local.toggle('cam.anim.trans', bool);
material.transparent = bool;
material.needsUpdate = true;
}
function step() {
updateSpeed();
kiri.client.animate2({speed, steps: 1}, handleUpdate);
}
function play(opts) {
const { steps } = opts;
updateSpeed();
if (steps !== 1) {
button.play.style.display = 'none';
button.pause.style.display = '';
}
kiri.client.animate2({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
}, handleUpdate);
}
function fast(opts) {
const { steps } = opts;
updateSpeed(1);
button.play.style.display = 'none';
button.pause.style.display = '';
kiri.client.animate2({
speed,
steps: steps || Infinity,
pause: speedPauses[speedIndex]
}, handleUpdate);
}
function pause() {
button.play.style.display = '';
button.pause.style.display = 'none';
kiri.client.animate2({speed: 0}, handleUpdate);
}
function handleUpdate(data) {
if (!data) {
return;
}
if (data.mesh_add) {
const { id, ind, pos, ilen, plen } = data.mesh_add;
meshAdd(id, ind, pos, ilen, plen);
space.refresh();
}
if (data.mesh_del) {
deleteMesh(data.mesh_del);
}
if (data.mesh_move) {
const { id, pos } = data.mesh_move;
const mesh = meshes[id];
if (mesh) {
mesh.position.x = pos.x;
mesh.position.y = pos.y;
mesh.position.z = pos.z;
space.refresh();
}
label.x.value = (pos.x - origin.x).toFixed(2);
label.y.value = (pos.y + origin.y).toFixed(2);
label.z.value = (pos.z - origin.z).toFixed(2);
}
if (data.stock_index !== undefined) {
api.widgets.setAxisIndex(data.stock_index);
label.a.value = -data.stock_index.toFixed(1);
}
if (data.mesh_index) {
const { id, index } = data.mesh_index;
const mesh = meshes[id];
if (mesh) {
mesh.rotation.x = (Math.PI / 180) * index;
space.refresh();
}
}
if (data.mesh_update) {
const { id, ind, pos, ilen, plen } = data.mesh_update;
meshUpdate(id, ind, pos, ilen, plen);
}
if (data && data.progress) {
label.progress.value = (data.progress * 100).toFixed(1);
}
}
function updateSpeed(inc = 0) {
if (inc === Infinity) {
speedIndex = speedMax;
} else if (inc > 0) {
speedIndex = (speedIndex + inc) % speedValues.length;
}
api.local.set('cam.anim.speed', speedIndex);
speed = speedValues[speedIndex];
label.speed.value = speedNames[speedIndex];
}
function replay() {
animate_clear2(api);
setTimeout(() => {
animate2(api, 50);
}, 250);
}
});
// ---( WORKER FUNCTIONS )---
const { CSG } = root.base;
let nextMeshID = 1;
class Stock {
constructor(x, y, z) {
this.id = nextMeshID++;
this.vbuf = undefined;
this.ibuf = undefined;
this.ilen = 0;
this.sends = 0;
this.newbuf = true;
this.subtracts = 0;
this.mesh = CSG.Instance().Manifold.cube([x, y, z], true);
}
send(send) {
const newbuf = this.newbuf;
const action = this.sends++ === 0 ? 'mesh_add' : 'mesh_update';
send.data({ [action]: {
id: this.id,
ind: newbuf ? this.ibuf : undefined,
pos: newbuf ? this.vbuf : undefined,
ilen: this.ilen,
plen: this.plen
} });
this.newbuf = false;
this.sends++;
// console.log({ send: this.id, newbuf, action });
}
translate(x, y, z) {
// console.log({ translate: this.id, x, y, z });
const oldmesh = this.mesh;
this.mesh = this.mesh.translate(x, y, z);
this.bounds = CSG.toBox3(this.mesh);
oldmesh.delete();
return this;
}
updateMesh(updates) {
if (this.sends > 0 && this.subtracts === 0) {
return;
}
const subs = this.subtracts;
this.subtracts = 0;
let start = Date.now();
let mesh = this.mesh.getMesh();
this.sharedVertexBuffer(mesh.numVert * 3);
this.sharedIndexBuffer(mesh.numTri * 3);
this.vbuf.set(mesh.vertProperties);
this.ibuf.set(mesh.triVerts);
let sub = Date.now();
updates.push(this);
if (false) console.log({
update: this.id,
time: sub - start,
subs,
mssub: ((sub - start) / subs).round(3)
});
}
subtractTool(toolMesh) {
const oldmesh = this.mesh;
this.mesh = this.mesh.subtract(toolMesh.mesh);
oldmesh.delete();
this.subtracts++;
}
sharedVertexBuffer(size) {
const old = this.vbuf;
this.plen = size;
if (old && old.length >= size) {
// console.log({svb: this.id, reuse: size, old: old.length});
return old;
}
// let buf = new Float32Array(new SharedArrayBuffer(size * 4));
let buf = new Float32Array(new SharedArrayBuffer(size * 4 + 1024 * 1024));
// console.log({new_svb: this.id, size, buf});
this.newbuf = true;
return this.vbuf = buf;
}
sharedIndexBuffer(size) {
const old = this.ibuf;
this.ilen = size;
if (old && old.length >= size) {
// console.log({sib: this.id, reuse: size, old: old.length});
return old;
}
// let buf = new Uint32Array(new SharedArrayBuffer(size * 4));
let buf = new Uint32Array(new SharedArrayBuffer(size * 4 + 1024 * 1024));
// console.log({new_sib: this.id, size, buf});
this.newbuf = true;
return this.ibuf = buf;
}
}
kiri.load(() => {
if (!kiri.worker) {
return;
}
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;
kiri.worker.animate_setup2 = function(data, send) {
const { settings } = data;
const { process } = settings;
const print = worker.print;
const density = parseInt(settings.controller.animesh) * 1000;
const isIndexed = process.camStockIndexed;
pathIndex = 0;
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;
animateClear = false;
stockIndex = 0;
indexCount = 0;
startTime = 0;
updates = 0;
stockZ = isIndexed ? 0 : stock.z;
stockSlices = [];
const { x, y, z } = stock;
const sliceCount = parseInt(settings.controller.animesh || 2000) / 100;
const sliceWidth = stock.x / sliceCount;
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.done();
};
kiri.worker.animate2 = function(data, send) {
renderPause = data.pause || renderPause;
renderSpeed = data.speed || 0;
if (animating) {
return send.done();
}
renderSteps = data.steps || 1;
renderDone = false;
animating = renderSpeed > 0;
startTime = startTime || Date.now();
renderPath(send);
};
kiri.worker.animate_cleanup2 = function(data, send) {
if (animating) {
animateClear = true;
}
};
function renderPath(send) {
if (renderDone) {
return;
}
if (renderSteps-- === 0) {
animating = false;
renderPath(send);
return;
}
if (animating === false || animateClear || renderSpeed === 0) {
renderUpdate(send);
renderDone = true;
animating = false;
animateClear = false;
send.done();
return;
}
let next = path[pathIndex];
while (next && next.type === 'laser') {
last = next;
next = path[++pathIndex];
}
if (!next) {
console.log('animation completed in ', ((Date.now() - startTime)/1000).round(2));
animating = false;
renderPath(send);
return;
}
pathIndex++;
// console.log(next.point.z);
if (next.tool && (!tool || tool.getID() !== next.tool.getID())) {
// on real tool change, go to safe Z first
if (tool && last.point) {
let pos = last.point = {
x: last.point.x,
y: last.point.y,
z: stock.z
};
toolMove(pos);
send.data(toolUpdateMsg);
}
toolUpdate(next.tool.getID(), send);
}
const id = toolID;
const rezstep = rez * 2;
if (last) {
const lp = last.point, np = next.point;
last = next;
// 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;
}
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 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 });
x += mx;
y += my;
z += mz;
}
moveDist = 0;
moves.push({...next.point, md:sd});
renderMoves(id, moves, send);
} else {
last = next;
if (tool) {
toolMove(next.point);
}
renderPath(send);
}
}
function renderMoves(id, moves, send, seed = 0) {
for (let index = seed; index<moves.length; index++) {
const pos = moves[index];
if (!pos) {
throw `no pos @ ${index} of ${moves.length}`;
}
const { dx, dy, dz } = pos;
toolMove(pos);
// console.log('renderMoves', {id, moves, seed});
let subs = 0;
if (dx || dy || dz < 0)
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;
renderUpdate(send);
setTimeout(() => {
renderMoves(id, moves, send, index + 1);
}, renderPause);
return;
}
}
renderPath(send);
}
// send latest tool position and progress bar
function renderUpdate(send) {
const updated = []
for (let slice of stockSlices) {
slice.updateMesh(updated);
}
for (let slice of updated) {
slice.send(send);
}
if (toolUpdateMsg) {
send.data(toolUpdateMsg);
}
if (stockIndexMsg) {
send.data({ stock_index: stockIndex });
for (let slice of stockSlices) {
send.data({ mesh_index: { id: slice.id, index: -stockIndex } });
}
stockIndexMsg = false;
}
send.data({ progress: pathIndex / path.length });
updates++;
}
// move tool mesh animation space, update client
function toolMove(pos) {
toolUpdateMsg = { mesh_move: { id: toolID, pos: { x: pos.x, y: pos.y, z: pos.z } } };
if (toolMesh.mesh) {
toolMesh.mesh.delete();
}
toolMesh.mesh = toolMesh.root.translate(pos.x, pos.y, pos.z);
if (pos.a !== undefined) {
let tmp = toolMesh.mesh.rotate([ pos.a, 0, 0 ]);
toolMesh.mesh.delete();
toolMesh.mesh = tmp;
if (pos.a !== stockIndex) {
stockIndexMsg = true;
stockIndex = pos.a;
}
}
toolMesh.bounds = CSG.toBox3(toolMesh.mesh);
}
// delete old tool mesh, generate tool mesh, send to client
function toolUpdate(toolid, send) {
if (tool) {
send.data({ mesh_del: toolID });
}
tool = new CAM.Tool({ tools }, toolid);
const Instance = CSG.Instance();
const slen = tool.shaftLength() || 15;
const srad = tool.shaftDiameter() / 2;
const flen = tool.fluteLength() || 15;
const frad = toolRadius = tool.fluteDiameter() / 2;
const tlen = slen + flen; // total tool length
let { cylinder, sphere } = Instance.Manifold;
let mesh;
if (tool.isBallMill()) {
mesh = cylinder(tlen - frad * 2, frad, frad, 20, true)
.add(sphere(frad, 20).translate(0, 0, -(tlen - frad * 2)/2))
.add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen/2));
} else if (tool.isTaperMill()) {
const trad = Math.max(tool.tipDiameter() / 2, 0.001);
mesh = cylinder(slen, srad, srad, 20, true).translate(0, 0, slen/2)
.add(cylinder(flen, trad, frad, 20, true).translate(0, 0, -flen/2));
} else {
mesh = cylinder(tlen, frad, frad, 20, true)
.add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen/2));
}
mesh = mesh.translate(0, 0, (tlen - stockZ) / 2);
const raw = mesh.getMesh();
const vertex = raw.vertProperties;
const index = raw.triVerts;
toolMesh = { root: mesh, index, vertex, bounds: CSG.toBox3(mesh) };
send.data({ mesh_add: { id:--toolID, ind: index, pos: vertex }});
}
});
});

File diff suppressed because it is too large Load diff

View file

@ -1,189 +0,0 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: main.kiri
// use: kiri.codec
// use: mesh.tool
gapp.register("kiri-mode.cam.driver", [], (root, exports) => {
const { kiri } = root;
const { driver } = kiri;
const CAM = driver.CAM = {};
CAM.process = {
LEVEL: 1,
ROUGH: 2,
OUTLINE: 3,
CONTOUR_X: 4,
CONTOUR_Y: 5,
TRACE: 6,
DRILL: 7
};
// defer loading until kiri.client and kiri.worker exist
kiri.load(api => {
if (kiri.client) {
CAM.surface_prep = function(index, ondone) {
kiri.client.sync();
const settings = api.conf.get();
kiri.client.send("cam_surfaces", { settings, index }, output => {
ondone(output);
});
};
CAM.surface_show = function(widget) {
widget.selectFaces(Object.values(widget._surfaces).flat());
};
CAM.surface_toggle = function(widget, face, radians, ondone) {
let surfaces = widget._surfaces = widget._surfaces || {};
for (let [root, faces] of Object.entries(surfaces)) {
if (faces.contains(face)) {
// delete this face group
delete surfaces[root];
CAM.surface_show(widget);
ondone(Object.keys(surfaces).map(v => parseInt(v)));
return;
}
}
kiri.client.send("cam_surface_find", { id: widget.id, face, radians }, faces => {
if (faces.length) {
surfaces[face] = faces;
CAM.surface_show(widget);
}
ondone(Object.keys(surfaces).map(v => parseInt(v)));
});
};
CAM.surface_clear = function(widget) {
widget.selectFaces([]);
widget._surfaces = {};
};
CAM.traces = function(ondone, single) {
kiri.client.sync();
const settings = api.conf.get();
const widgets = api.widgets.map();
kiri.client.send("cam_traces", { settings, single }, output => {
const ids = [];
kiri.codec.decode(output).forEach(rec => {
ids.push(rec.id);
widgets[rec.id].traces = rec.traces;
});
ondone(ids);
});
};
CAM.traces_clear = function(ondone) {
kiri.client.send("cam_traces_clear", {}, () => {
// console.log({clear_traces: true});
});
};
CAM.holes = function(indiv,rec,onProgress,onDone) {
kiri.client.sync();
const settings = api.conf.get();
return new Promise((res,rej)=>{
kiri.client.send("cam_holes", { settings, rec, indiv }, output => {
let out = kiri.codec.decode(output)
if(out.progress != undefined){
//if a progress message,
onProgress(out.progress,out.msg)
}else{
api.hide.alert(alert)
onDone(out)
res(out)
}
});
})
}
}
if (kiri.worker) {
CAM.surface_prep = function(widget, index) {
if (!widget.tool) {
let tool = widget.tool = new mesh.tool();
let translate = index ? true : false;
tool.index(widget.getGeoVertices({ unroll: true, translate }));
}
};
CAM.surface_find = function(widget, faces, radians) {
CAM.surface_prep(widget);
return widget.tool.findConnectedSurface(faces, radians || 0, 0.0);
};
kiri.worker.cam_surfaces = function(data, send) {
const { settings, index } = data;
const widgets = Object.values(kiri.worker.cache);
for (let widget of widgets) {
if (index) {
widget.setIndexed(true);
widget.setAxisIndex(-index);
} else {
widget.setIndexed(false);
widget.setAxisIndex(0);
}
CAM.surface_prep(widget, index);
}
send.done({});
};
kiri.worker.cam_surface_find = function(data, send) {
const { id, face, radians } = data;
const widget = kiri.worker.cache[id];
const faces = CAM.surface_find(widget, [face], radians);
send.done(faces);
}
kiri.worker.cam_traces = async function(data, send) {
const { settings, single } = data;
const widgets = Object.values(kiri.worker.cache);
const fresh = [];
for (let widget of widgets) {
if (await CAM.traces(settings, widget, single)) {
fresh.push(widget);
}
}
// const fresh = widgets.filter(widget => CAM.traces(settings, widget, single));
send.done(kiri.codec.encode(fresh.map(widget => { return {
id: widget.id,
traces: widget.traces,
} } )));
};
kiri.worker.cam_traces_clear = function(data, send) {
for (let widget of Object.values(kiri.worker.cache)) {
delete widget.traces;
delete widget.topo;
}
send.done({});
};
kiri.worker.cam_holes = async function(data, send) {
const { settings, indiv, rec } = data;
const widgets = Object.values(kiri.worker.cache);
const fresh = [];
for (let [i,widget] of widgets.entries() ) {
if (await CAM.holes(settings, widget, indiv, rec,
( prog, msg )=>{ send.data({progress: (i/widgets.length)+(prog/widgets.length),msg})}
)){
fresh.push(widget);
}
}
// const fresh = widgets.filter(widget => CAM.traces(settings, widget, single));
send.done(kiri.codec.encode(fresh.map(widget => { return {
id: widget.id,
holes: widget.drills,
shadowed:widget.shadowedDrills
} } )));
}
}
});
});

File diff suppressed because it is too large Load diff

View file

@ -1,353 +0,0 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: kiri.api
// dep: kiri.consts
// dep: kiri.settings
gapp.register("kiri-mode.cam.tools", (root, exports) => {
const DEG2RAD = Math.PI / 180;
let { kiri } = root,
{ api, consts } = kiri,
{ uc, ui } = api,
{ MODES } = consts,
DOC = document,
selectedTool = null,
editTools = null,
maxTool = 0,
toolNames = ['endmill','ballmill','tapermill','drill'];
api.show.tools = showTools;
// extend API
Object.assign(api.tool, {
update: updateTool
});
function settings() {
return api.conf.get();
}
function renderTools() {
ui.toolSelect.innerHTML = '';
maxTool = 0;
editTools.forEach(function(tool, index) {
maxTool = Math.max(maxTool, tool.number);
tool.order = index;
let opt = DOC.createElement('option');
opt.appendChild(DOC.createTextNode(tool.name));
opt.onclick = function() { selectTool(tool) };
ui.toolSelect.appendChild(opt);
});
}
function selectTool(tool) {
selectedTool = tool;
ui.toolName.value = tool.name;
ui.toolNum.value = tool.number;
ui.toolFluteDiam.value = tool.flute_diam;
ui.toolFluteLen.value = tool.flute_len;
ui.toolShaftDiam.value = tool.shaft_diam;
ui.toolShaftLen.value = tool.shaft_len;
ui.toolTaperTip.value = tool.taper_tip || 0;
ui.toolMetric.checked = tool.metric;
ui.toolType.selectedIndex = toolNames.indexOf(tool.type);
if (tool === 'tapermill') {
ui.toolTaperAngle.value = kiri.driver.CAM.calcTaperAngle(
(tool.flute_diam - tool.taper_tip) / 2, tool.flute_len
).round(1);
} else if(tool === 'drill'){
ui.toolTaperAngle.value = 118;
} else {
ui.toolTaperAngle.value = 0;
}
renderTool(tool);
}
function otag(o) {
if (Array.isArray(o)) {
let out = []
o.forEach(oe => out.push(otag(oe)));
return out.join('');
}
let tags = [];
Object.keys(o).forEach(key => {
let val = o[key];
let att = [];
Object.keys(val).forEach(tk => {
let tv = val[tk];
att.push(`${tk.replace(/_/g,'-')}="${tv}"`);
});
tags.push(`<${key} ${att.join(' ')}></${key}>`);
});
return tags.join('');
}
function renderTool(tool) {
let type = selectedTool.type;
let taper = type=== 'tapermill'
let drill = type === 'drill'
const drillAngleRad = 140 * Math.PI / 180
ui.toolTaperAngle.disabled = taper ? undefined : 'true';
ui.toolTaperTip.disabled = taper ? undefined : 'true';
$('tool-view').innerHTML = '<svg id="tool-svg" width="100%" height="100%"></svg>';
setTimeout(() => {
let svg = $('tool-svg'),
pad = 10,
dim = { w: svg.clientWidth, h: svg.clientHeight },
max = { w: dim.w - pad * 2, h: dim.h - pad * 2},
off = { x: pad, y: pad },
isBall = type === "ballmill",
shaft_fill = "#cccccc",
flute_fill = "#dddddd",
stroke = "#777777",
stroke_width = 3,
stroke_thin = stroke_width / 2,
shaft = tool.shaft_len || 1,
flute = tool.flute_len || 1,
drillTip = drill ? 0.5 * tool.flute_diam * Math.sin(drillAngleRad) : 0,
total_len = shaft + flute+drillTip,
units = dim.h / total_len,
shaft_len = (shaft / total_len) * max.h,
flute_len = (flute / total_len) * max.h,
drill_tip_len = (drillTip / total_len) * max.h,
shaft_diam = tool.shaft_diam * units,
flute_diam = tool.flute_diam * units,
// total_wid = Math.max(flute_diam, shaft_diam),
shaft_off = (max.w - shaft_diam) / 2,
flute_off = (max.w - flute_diam) / 2,
taper_off = (max.w - (tool.taper_tip || 0) * units) / 2,
parts = [
// shaft rectangle
{ rect: {
x: off.x + shaft_off,
y: off.y,
width: max.w - shaft_off * 2,
height: shaft_len,
fill: shaft_fill,
stroke_width,
stroke
} }
];
if (taper) {
let yoff = off.y + shaft_len;
// let mid = dim.w / 2;
parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[
`M ${off.x + flute_off} ${yoff}`,
`L ${off.x + taper_off} ${yoff + flute_len}`,
`L ${dim.w - off.x - taper_off} ${yoff + flute_len}`,
`L ${dim.w - off.x - flute_off} ${yoff}`,
`z`
].join('\n')}});
} else if(drill){
const x1 = off.x + flute_off,
y1 = off.y + shaft_len,
x2 = dim.w - off.x - flute_off,
y2 = y1 + flute_len,
xMid = dim.w / 2
parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[
`M ${x1} ${y1}`, //move to top left
`L ${x1} ${y2}`, //line to bottom left
`L ${xMid} ${y2+drill_tip_len}`, //line to bottom mid point
`L ${x2} ${y2}`, //line to bottom right
`L ${x2} ${y1}`, //line to top right
`z`
].join('\n')}});
//add drill flute lines
parts.push({ line: {
x1, y1, x2, y2: (y1 + y2) / 2,
stroke, stroke_width: stroke_thin
} });
parts.push({ line: {
x1, y1: (y1 + y2) / 2, x2, y2,
stroke, stroke_width: stroke_thin
} });
} else {
let fl = isBall ? flute_len - flute_diam/2 : flute_len;
let x1 = off.x + flute_off;
let y1 = off.y + shaft_len;
let x2 = x1 + max.w - flute_off * 2;
let y2 = y1 + fl;
// flute rectangle
parts.push({ rect: {
x: off.x + flute_off,
y: off.y + shaft_len,
width: max.w - flute_off * 2,
height: fl,
fill: flute_fill,
stroke_width,
stroke,
} });
// hatch "fill" flute
parts.push({ line: { x1, y1, x2, y2, stroke, stroke_width: stroke_thin } });
parts.push({ line: {
x1: (x1 + x2) / 2, y1, x2, y2: (y1 + y2) / 2,
stroke, stroke_width: stroke_thin
} });
parts.push({ line: {
x1, y1: (y1 + y2) / 2, x2: (x1 + x2) / 2, y2,
stroke, stroke_width: stroke_thin
} });
}
if (isBall) {
let rad = (max.w - flute_off * 2) / 2;
let xend = dim.w - off.x - flute_off;
let yoff = off.y + shaft_len + flute_len + stroke_width/2 - flute_diam/2;
parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[
`M ${off.x + flute_off} ${yoff}`,
`A ${rad} ${rad} 0 0 0 ${xend} ${yoff}`,
// `L ${off.x + flute_off} ${yoff}`
].join('\n')}})
}
svg.innerHTML = otag(parts);
}, 10);
}
function updateTool(ev) {
selectedTool.name = ui.toolName.value;
selectedTool.number = parseInt(ui.toolNum.value);
selectedTool.flute_diam = parseFloat(ui.toolFluteDiam.value);
selectedTool.flute_len = parseFloat(ui.toolFluteLen.value);
selectedTool.shaft_diam = parseFloat(ui.toolShaftDiam.value);
selectedTool.shaft_len = parseFloat(ui.toolShaftLen.value);
selectedTool.taper_tip = parseFloat(ui.toolTaperTip.value);
selectedTool.metric = ui.toolMetric.checked;
selectedTool.type = toolNames[ui.toolType.selectedIndex];
if (selectedTool.type === 'tapermill') {
const CAM = kiri.driver.CAM;
const rad = (selectedTool.flute_diam - selectedTool.taper_tip) / 2;
if (ev && ev.target === ui.toolTaperAngle) {
const angle = parseFloat(ev.target.value);
const len = CAM.calcTaperLength(rad, angle * DEG2RAD);
selectedTool.flute_len = len;
ui.toolTaperAngle.value = angle.round(1);
ui.toolFluteLen.value = selectedTool.flute_len.round(4);
} else {
ui.toolTaperAngle.value = CAM.calcTaperAngle(rad, selectedTool.flute_len).round(1);
}
} else {
ui.toolTaperAngle.value = 0;
}
renderTools();
ui.toolSelect.selectedIndex = selectedTool.order;
setToolChanged(true);
renderTool(selectedTool);
}
function setToolChanged(changed) {
editTools.changed = changed;
ui.toolsSave.disabled = !changed;
}
function showTools() {
if (api.mode.get_id() !== MODES.CAM) return;
api.settings.sync.get().then(_showTools);
}
function _showTools() {
let selectedIndex = null;
editTools = settings().tools.slice().sort((a,b) => {
return a.name > b.name ? 1 : -1;
});
setToolChanged(false);
ui.toolsClose.onclick = function() {
if (editTools.changed && !confirm("abandon changes?")) return;
api.dialog.hide();
};
ui.toolAdd.onclick = function() {
let metric = settings().controller.units === 'mm';
editTools.push(Object.assign({
id: Date.now(),
number: maxTool + 1,
name: "new tool",
type: "endmill",
taper_tip: 0,
metric
}, metric ? {
shaft_diam: 2,
shaft_len: 15,
flute_diam: 2,
flute_len: 20,
} : {
shaft_diam: 0.25,
shaft_len: 1.5,
flute_diam: 0.25,
flute_len: 2,
}));
setToolChanged(true);
renderTools();
ui.toolSelect.selectedIndex = editTools.length-1;
selectTool(editTools[editTools.length-1]);
};
ui.toolCopy.onclick = function() {
let clone = Object.assign({}, selectedTool);
let { name } = clone;
let split = name.split(' ');
let endv = parseInt(split.pop());
if (endv) {
name = split.join(' ') + ' ' + (endv + 1);
} else {
name = `${name} 2`;
}
clone.id = Date.now();
clone.number = maxTool + 1;
clone.name = name;
editTools.push(clone);
setToolChanged(true);
renderTools();
ui.toolSelect.selectedIndex = editTools.length-1;
selectTool(editTools[editTools.length-1]);
};
ui.toolDelete.onclick = function() {
editTools.remove(selectedTool);
setToolChanged(true);
renderTools();
};
ui.toolsSave.onclick = function() {
if (selectedTool) updateTool();
settings().tools = editTools.sort((a,b) => {
return a.name < b.name ? -1 : 1;
});
setToolChanged(false);
api.conf.save();
api.conf.update_fields();
api.event.settings();
api.settings.sync.put();
};
ui.toolsImport.onclick = (ev) => api.event.import(ev);
ui.toolsExport.onclick = () => {
uc.prompt("Export Tools Filename", "tools").then(name => {
if (!name) {
return;
}
const record = {
version: kiri.version,
tools: api.conf.get().tools,
time: Date.now()
};
api.util.download(api.util.b64enc(record), `${name}.km`);
});
};
renderTools();
if (editTools.length > 0) {
selectTool(editTools[0]);
ui.toolSelect.selectedIndex = 0;
} else {
ui.toolAdd.onclick();
}
api.dialog.show('tools');
ui.toolSelect.focus();
}
});

View file

@ -1,14 +0,0 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: kiri-mode.laser.driver
gapp.register("kiri-mode.drag.driver", [], (root, exports) => {
const DRIVERS = root.kiri.driver;
const { LASER } = DRIVERS;
const { TYPE } = LASER;
DRIVERS.DRAG = Object.assign({}, LASER, { name: "DragKnife", type: TYPE.DRAG });
});

View file

@ -1,85 +0,0 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
gapp.register("kiri-mode.fdm.driver", [], (root, exports) => {
const { base, kiri } = root;
const { driver } = kiri;
const { util } = base;
const FDM = driver.FDM = { getRangeParameters, extrudePerMM, extrudeMM };
// shared by client and worker contexts
function getRangeParameters(process, index) {
if (index === undefined || index === null || index < 0) {
return process;
}
let ranges = process.ranges;
if (!(ranges && ranges.length)) {
return process;
}
let params = Object.clone(process);
for (let range of ranges) {
if (index >= range.lo && index <= range.hi) {
for (let [key, value] of Object.entries(range.fields)) {
params[key] = value;
params._range = true;
}
}
}
return params;
}
// noz = nozzle diameter
// fil = filament diameter
// slice = slice height
function extrudePerMM(noz, fil, slice) {
return ((Math.PI * util.sqr(noz / 2)) / (Math.PI * util.sqr(fil / 2))) * (slice / noz);
};
// dist = distance between extrusion points
// perMM = amount extruded per MM (from extrudePerMM)
// factor = scaling factor (usually 1.0)
function extrudeMM(dist, perMM, factor) {
return dist * perMM * factor;
}
// defer loading until client and worker exist
kiri.load(api => {
const { client, worker } = kiri;
if (client) {
FDM.support_generate = function(ondone) {
client.clear();
client.sync();
const settings = api.conf.get();
const widgets = api.widgets.map();
client.send("fdm_support_generate", { settings }, (gen) => {
if (gen && gen.error) {
api.show.alert('support generation canceled');
return ondone([]);
}
for (let g of gen) {
g.widget = widgets[g.id];
}
ondone(gen);
});
};
}
if (worker) {
worker.fdm_support_generate = function(data, send) {
const { settings } = data;
const widgets = Object.values(worker.cache);
const fresh = widgets.filter(widget => FDM.supports(settings, widget));
send.done(kiri.codec.encode(fresh.map(widget => { return {
id: widget.id,
supports: widget.supports,
} } )));
};
}
});
});

View file

@ -1,101 +0,0 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: geo.base
// dep: kiri-mode.sla.driver
gapp.register("kiri-mode.sla.client", [], (root, exports) => {
const { base, kiri } = root;
const { driver } = kiri;
const { util } = base;
const { SLA } = driver;
gapp.overlay(SLA, {
init,
printDownload
});
function init(kiri, api) {
api.event.on("mode.set", (mode) => {
if (mode === 'SLA') {
api.ui.func.preview.classList.add('hide');
} else {
api.ui.func.preview.classList.remove('hide');
}
});
}
function printDownload(output, api, names) {
const { file, width, height, layers, volume } = output;
const fileroot = names[0] || "print";
const filename = `${fileroot}-${new Date().getTime().toString(36)}`;
api.modal.show('xsla');
// api.ajax("/kiri/output-sla.html", html => {
// api.ui.print.innerHTML = html;
let settings = api.conf.get(),
process = settings.process,
device = settings.device,
print_sec = (process.slaBaseLayers * process.slaBaseOn) +
(layers - process.slaBaseLayers) * process.slaLayerOn;
// add peel lift/drop times to total print time
for (let i=0; i<layers; i++) {
let dist = process.slaPeelDist,
lift = process.slaPeelLiftRate,
drop = process.slaPeelDropRate,
off = process.slaLayerOff;
if (i < process.slaBaseLayers) {
dist = process.slaBasePeelDist;
lift = process.slaBasePeelLiftRate;
off = process.slaBaseOff;
}
print_sec += (dist * lift) / 60;
print_sec += (dist * drop) / 60;
print_sec += off;
}
let print_min = Math.floor(print_sec/60),
print_hrs = Math.floor(print_min/60),
download = $('print-sla');
// add lift/drop time
print_sec -= (print_min * 60);
print_min -= (print_hrs * 60);
print_sec = Math.round(print_sec).toString().padStart(2,'0');
print_min = print_min.toString().padStart(2,'0');
print_hrs = print_hrs.toString().padStart(2,'0');
$('print-filename-sla').value = filename;
$('print-volume').value = (volume/1000).round(2);
$('print-layers').value = layers;
$('print-time').value = `${print_hrs}:${print_min}:${print_sec}`;
switch (device.deviceName) {
case 'Anycubic.Photon':
download.innerText += " .photon";
download.onclick = () => { saveFile(api, file, ".photon") };
break;
case 'Anycubic.Photon.S':
download.innerText += " .photons";
download.onclick = () => { saveFile(api, file, ".photons") };
break;
case 'Creality.Halot.Sky':
default:
download.innerText += " .cxdlp";
download.onclick = () => { saveFile(api, file, ".cxdlp") };
break;
}
// api.modal.show('print');
// });
}
function saveFile(api, file, ext) {
api.util.download(file, $('print-filename-sla').value + ext);
api.modal.hide();
}
});

View file

@ -1,14 +0,0 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: kiri-mode.laser.driver
gapp.register("kiri-mode.wjet.driver", [], (root, exports) => {
const DRIVERS = root.kiri.driver;
const { LASER } = DRIVERS;
const { TYPE } = LASER;
DRIVERS.WJET = Object.assign({}, LASER, { name: 'WaterJet', type: TYPE.WJET });
});

View file

@ -1,175 +0,0 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: moto.license
// dep: moto.broker
// dep: geo.base
// dep: geo.polygons
// dep: geo.slicer
// dep: geo.wasm
// dep: kiri.codec
// dep: kiri-mode.fdm.post
// dep: kiri-mode.cam.topo
// dep: kiri-mode.cam.topo4
// use: kiri-mode.cam.slicer
// dep: ext.clip2
gapp.register("kiri-run.minion", [], (root, exports) => {
const { base, kiri } = root;
const { polygons } = base;
const { codec } = kiri;
const POLY = polygons;
const clib = self.ClipperLib;
const ctyp = clib.ClipType;
const ptyp = clib.PolyType;
const cfil = clib.PolyFillType;
let cache = self.cache = {};
let name = "unknown";
// catch clipper alerts and convert to console messages
self.alert = function(o) {
console.log(o);
};
self.onmessage = function(msg) {
let data = msg.data;
let cmd = data.cmd;
(funcs[cmd] || funcs.bad)(data, data.seq, cmd);
};
function reply(msg, direct) {
self.postMessage(msg, direct);
}
function log() {
console.log(`[${name}]`, ...arguments);
}
const funcs = self.minion = {
label(data, seq) {
name = data.name;
},
config: data => {
if (data.base) {
Object.assign(base.config, data.base);
} else {
log({invalid: data});
}
},
union: (data, seq) => {
if (!(data.polys && data.polys.length)) {
reply({ seq, union: codec.encode([]) });
return;
}
let state = { zeros: [] };
let polys = codec.decode(data.polys);
let union = POLY.union(polys, data.minarea || 0, true);
reply({ seq, union: codec.encode(union) }, state.zeros);
},
topShells: (data, seq) => {
let top = codec.decode(data.top, {full: true});
let {z, count, offset1, offsetN, fillOffset, opt} = data;
kiri.driver.FDM.doTopShells(z, top, count, offset1, offsetN, fillOffset, opt);
let state = { zeros: [] };
reply({ seq, top: codec.encode(top, {full: true}) }, state.zeros);
},
fill: (data, seq) => {
let polys = codec.decode(data.polys);
let { angle, spacing, minLen, maxLen } = data;
let fill = POLY.fillArea(polys, angle, spacing, [], minLen, maxLen);
let arr = new Float32Array(fill.length * 4);
for (let i=0, p=0; p<fill.length; ) {
let pt = fill[p++];
arr[i++] = pt.x;
arr[i++] = pt.y;
arr[i++] = pt.z;
arr[i++] = pt.index;
}
reply({ seq, fill: arr }, [ arr.buffer ]);
},
clip: (data, seq) => {
const clip = new clib.Clipper();
const ctre = new clib.PolyTree();
const clips = [];
const M = base.config.clipper;
let lines = data.lines.map(array => {
return codec.decodePointArray2D(array, data.z, (X, Y) => { return {X: X*M, Y: Y*M} })
});
let polys = data.polys.map(array => {
return codec.decodePointArray2D(array, data.z, (X, Y) => { return {X: X*M, Y: Y*M} })
});
clip.AddPaths(lines, ptyp.ptSubject, false);
clip.AddPaths(polys, ptyp.ptClip, true);
const state = { zeros: [] };
if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) {
for (let node of ctre.m_AllPolys) {
clips.push(codec.encode(POLY.fromClipperNode(node, data.z), state.zeros));
}
}
reply({ seq, clips }, state.zeros);
},
sliceZ: (data, seq) => {
let { z, points, options } = data;
let i = 0, p = 0, realp = new Array(points.length / 3);
while (i < points.length) {
realp[p++] = base.newPoint(points[i++], points[i++], points[i++]).round(3);
}
let state = { zero: [] };
let output = [];
base.sliceZ(z, realp, {
...options,
each(out) { output.push(out) }
}).then(() => {
for (let rec of output) {
// lines do not pass codec properly (for now)
delete rec.lines;
}
reply({ seq, output: codec.encode(output) }, state.zeros);
});
},
putCache: msg => {
const { key, data } = msg;
// log({ minion_putCache: key, data });
if (data) {
cache[key] = data;
} else {
delete cache[key];
}
},
clearCache: msg => {
for (let key in cache) {
delete cache[key];
}
},
wasm: data => {
if (data.enable) {
base.wasm_ctrl.enable();
} else {
base.wasm_ctrl.disable();
}
},
bad: (data, seq, cmd) => {
reply({ seq, error: `invalid command (${cmd})` });
}
};
moto.broker.publish("minion.started", { funcs, cache, reply, log });
});

View file

@ -1,149 +0,0 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: moto.broker
// dep: data.local
// dep: kiri.utils
// dep: kiri.consts
gapp.register("kiri.api", (root, exports) => {
let { data, kiri, moto, noop } = root,
{ consts, utils } = kiri,
{ ajax, o2js, js2o } = utils,
lists = consts.LISTS,
clone = Object.clone,
isHover = false,
feature = {
seed: true, // seed profiles on first use
meta: true, // show selected widget metadata
frame: true, // receive frame events
alert_event: false, // emit alerts as events instead of display
controls: true, // show or not side menus
device_filter: undefined, // function to limit devices shown
drop_group: undefined, // optional array to group multi drop
drop_layout: true, // layout on new drop
hoverAdds: false, // when true only searches widget additions
on_key: undefined, // function override default key handlers
on_key2: [], // allows for multiple key handlers
on_load: undefined, // function override file drop loads
on_add_stl: undefined, // legacy override stl drop loads
on_mouse_up: undefined, // function intercepts mouse up select
on_mouse_down: undefined, // function intercepts mouse down
work_alerts: true, // allow disabling work progress alerts
pmode: consts.PMODES.SPEED, // preview modes
// hover: false, // when true fires mouse hover events
get hover() {
return isHover;
},
set hover(b) {
isHover = b;
moto.broker.publish("feature.hover", b);
}
},
onkey = (fn) => {
api.feature.on_key2.push(fn);
},
doit = {
undo: noop, // do.js
redo: noop // do.js
},
devel = {
xray(layers, raw) {
let proc = api.conf.get().process,
size = proc.sliceHeight || proc.slaSlice || 1,
base = (proc.firstSliceHeight || size);
layers = Array.isArray(layers) ? layers : [ layers ];
proc.xray = layers.map(l => raw ? l : base + l * size - size / 2);
proc.xrayi = layers.slice();
api.function.slice();
}
},
local = {
get: (key) => localGet(key),
getInt: (key) => parseInt(localGet(key)),
getFloat: (key) => parseFloat(localGet(key)),
getBoolean: (key, def = true) => {
let val = localGet(key);
return val === true || val === 'true' || val === def;
},
toggle: (key, val, def) => localSet(key, val ?? !api.local.getBoolean(key, def)),
put: (key, val) => localSet(key, val),
set: (key, val) => localSet(key, val),
},
tweak = {
line_precision(v) { api.work.config({base:{clipperClean: v}}) },
gcode_decimals(v) { api.work.config({base:{gcode_decimals: v}}) }
},
und = undefined,
api = exports({
ajax, // via utils
alerts: {}, // alerts.js
busy: {}, // main.js
catalog: und, // main.js
clip, // <--
clone, // <--
color: und, // main.js
conf: {}, // settings.js
const: {}, // main.js
devel, // <--
device: {}, // devices.js
devices: {}, // devices.js
dialog: {}, // main.js
doit, // <--
event: {}, // main.js
feature, // <--
function: {}, // function.js
group: {}, // main.js
help: {}, // main.js
hide: {}, // main.js
image: {}, // main.js
js2o, // via utils
language: und, // main.js
lists, // <--
local, // <--
modal: {}, // main.js
mode: {}, // main.js
o2js, // via utils
onkey, // <--
platform: {}, // platform.js
probe: {}, // main.js
process: {}, // main.js
sdb: data.local,
selection: {}, // selection.js
settings: {}, // settings.js
show: {}, // main.js
space: {}, // main.js
tool: {}, // kiri-mode/cam/tools.js
tweak, // <--
uc: {}, // main.js
ui: {}, // main.js
util: {}, // main.js
var: {
layer_lo: 0,
layer_hi: 0,
layer_max: 0
},
view: {}, // main.js
widgets: {}, // widgets.js
work: und, // main.js
});
function clip(text) {
navigator.clipboard
.writeText(text)
.catch(err => console.error('Clipboard Error:', err));
}
function localGet(key) {
let sloc = api.conf.get().local;
return sloc[key] || api.sdb[key];
}
function localSet(key, val) {
let sloc = api.conf.get().local;
sloc[key] = api.sdb[key] = val;
return val;
}
});

View file

@ -1,12 +1,6 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { api } from './api.js';
// dep: kiri.api
gapp.register("kiri.alerts", [], (root, exports) => {
const { kiri } = root;
const { api } = kiri;
let alerts = []; let alerts = [];
@ -68,11 +62,14 @@ function update(clear) {
} }
} }
// extend API export {
Object.assign(api.alerts, {
hide, hide,
show, show,
update update
}); };
}); export default {
hide,
show,
update
};

189
src/kiri/core/api.js Normal file
View file

@ -0,0 +1,189 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import alerts from './alerts.js';
import settings from './settings.js'
import STACKS from './stacks.js';
import { broker } from '../../moto/broker.js';
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 { local as dataLocal } from '../../data/local.js';
import { noop, ajax, o2js, js2o } from './utils.js';
import { functions } from './function.js';
import { platform } from './platform.js';
import { selection } from './selection.js';
import { stats } from './stats.js';
import { newWidget } from './widget.js';
import { widgets } from './widgets.js';
import { updateTool } from '../mode/cam/tools.js';
import { beta, version } from '../../moto/license.js';
import { space as SPACE } from '../../moto/space.js';
import { LANG } from './lang.js';
import { LOCAL, SETUP, SECURE } from './main.js';
import { UI } from './ui.js';
import web from '../../moto/webui.js';
let UC = UI.prefix('kiri').inputAction(settings.conf.update),
und = undefined,
clone = Object.clone,
isHover = false,
feature = {
seed: true, // seed profiles on first use
meta: true, // show selected widget metadata
frame: true, // receive frame events
alert_event: false, // emit alerts as events instead of display
controls: true, // show or not side menus
device_filter: und, // function to limit devices shown
drop_group: und, // optional array to group multi drop
drop_layout: true, // layout on new drop
hoverAdds: false, // when true only searches widget additions
on_key: und, // function override default key handlers
on_key2: [], // allows for multiple key handlers
on_load: und, // function override file drop loads
on_add_stl: und, // legacy override stl drop loads
on_mouse_up: und, // function intercepts mouse up select
on_mouse_down: und, // function intercepts mouse down
work_alerts: true, // allow disabling work progress alerts
pmode: consts.PMODES.SPEED, // preview modes
// hover: false, // when true fires mouse hover events
get hover() {
return isHover;
},
set hover(b) {
isHover = b;
broker.publish("feature.hover", b);
}
},
busyVal = 0,
busy = {
val() { return busyVal },
inc() { api.event.emit("busy", ++busyVal) },
dec() { api.event.emit("busy", --busyVal) }
},
onkey = (fn) => {
api.feature.on_key2.push(fn);
},
doit = {
undo: noop, // do.js
redo: noop // do.js
},
devel = {
xray(layers, raw) {
let proc = api.conf.get().process,
size = proc.sliceHeight || proc.slaSlice || 1,
base = (proc.firstSliceHeight || size);
layers = Array.isArray(layers) ? layers : [layers];
proc.xray = layers.map(l => raw ? l : base + l * size - size / 2);
proc.xrayi = layers.slice();
api.function.slice();
}
},
local = {
get: (key) => localGet(key),
getItem: (key) => localGet(key),
getInt: (key) => parseInt(localGet(key)),
getFloat: (key) => parseFloat(localGet(key)),
getBoolean: (key, def = true) => {
let val = localGet(key);
return val === true || val === 'true' || val === def;
},
toggle: (key, val, def) => localSet(key, val ?? !api.local.getBoolean(key, def)),
put: (key, val) => localSet(key, val),
set: (key, val) => localSet(key, val),
setItem: (key, val) => localSet(key, val),
},
tweak = {
line_precision(v) { api.work.config({ base: { clipperClean: v } }) },
gcode_decimals(v) { api.work.config({ base: { gcode_decimals: v } }) }
};
function clip(text) {
navigator.clipboard
.writeText(text)
.catch(err => console.error('Clipboard Error:', err));
}
function localGet(key) {
let sloc = api.conf.get().local;
return sloc[key] || api.sdb[key];
}
function localSet(key, val) {
let sloc = api.conf.get().local;
sloc[key] = api.sdb[key] = val;
return val;
}
export const api = {
ajax,
beta,
alerts,
busy,
catalog,
client: work,
clip,
clone,
color,
conf: settings.conf,
const: { LANG, LOCAL, SETUP, SECURE, STACKS, ...consts },
devel,
device,
devices,
dialog,
doit,
event,
feature,
function: functions,
group,
help,
hide,
image,
js2o,
language: LANG,
lists,
local,
modal,
mode,
new: {
widget: newWidget
},
noop,
o2js,
onkey,
platform,
probe,
process,
sdb: dataLocal,
selection,
settings,
show,
space,
SPACE,
stacks: STACKS,
stats,
tool: {
update: updateTool
},
tweak,
uc: UC,
ui: {},
util,
var: {
layer_lo: 0,
layer_hi: 0,
layer_max: 0
},
version,
view,
web,
widgets,
work,
};
// allow widget to straddle client / worker FOR NOW
self.kiri_api = api;

60
src/kiri/core/boxes.js Normal file
View file

@ -0,0 +1,60 @@
import { space } from '../../moto/space.js';
const { BoxGeometry, Matrix4, MeshPhongMaterial, Quaternion, Vector3, Mesh } = THREE;
const boxes = [];
let lastBox;
export function clearboxes() {
boxes.length = 0;
}
export function getlastbox() {
return lastBox;
}
export function delbox(name) {
const old = boxes[name];
if (old) {
old.groupTo.remove(old);
}
}
export function addbox(point, color, name, dim = {x:1,y:1,z:1,rz:0}, opt = {}) {
delbox(name);
const box = new Mesh(
new BoxGeometry(dim.x, dim.y, dim.z),
new MeshPhongMaterial({
transparent: true,
opacity: opt.opacity || 0.5,
color
})
);
if (name) {
boxes[name] = box;
}
box.position.x = point.x;
box.position.y = point.y;
box.position.z = point.z;
lastBox = { point, dim };
const group = opt.group || space.scene;
group.add(box);
box.groupTo = group;
if (dim.rz) {
opt.rotate = new Quaternion().setFromAxisAngle(new Vector3(0,0,1), dim.rz);
}
if (opt.rotate) {
opt.matrix = new Matrix4().makeRotationFromQuaternion(opt.rotate);
}
if (opt.matrix) {
box.geometry.applyMatrix4(opt.matrix);
}
return box;
}

View file

@ -1,27 +1,22 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { api } from './api.js';
import { noop } from './utils.js';
// use: kiri.api import { codec } from './codec.js';
gapp.register("kiri.client", [], (root, exports) => { import { Widget } from './widget.js';
import { newPrint } from './print.js';
const { kiri } = root;
// this code runs in kiri's main loop // this code runs in kiri's main loop
let loc = self.location, let debug = self.debug === true,
host = loc.hostname,
port = loc.port,
proto = loc.protocol,
debug = self.debug === true,
time = Date.now, time = Date.now,
seqid = 1, seqid = 1,
syncd = {}, syncd = {},
running = {}, running = {},
worker = null, worker = null,
minions = false, minions = false,
restarting = false restarting = false,
// occ = new Worker("/kiri/ext/occ-worker.js", {type:"module"}), workpath,
; poolpath;
/** /**
* @param {Function} fn name of function in kiri.worker * @param {Function} fn name of function in kiri.worker
@ -52,13 +47,23 @@ function send(fn, data, onreply, zerocopy) {
} }
// code is running in the browser / client context // code is running in the browser / client context
const client = exports({ export const client = {
send: send, send: send,
setWorkPath(path) {
workpath = path;
return client;
},
setPoolPath(path) {
poolpath = path;
return client;
},
pool: { pool: {
start() { start() {
minions = true; minions = true;
send("pool_start", {}, noop); send("pool_start", { url: poolpath }, noop);
}, },
stop() { stop() {
@ -70,9 +75,21 @@ const client = exports({
newWorker() { newWorker() {
if (self.createWorker) { if (self.createWorker) {
return self.createWorker(); return self.createWorker();
} else if (self.kiri_work) {
const work = atob(self.kiri_work);
const blob = new Blob([ work ], { type: 'application/javascript' });
return new Worker(URL.createObjectURL(blob));
} else { } else {
let _ = debug ? '_' : ''; let worker = new Worker(workpath || api.const.PATHS.work, { type: 'module' });
return new Worker(`/code/kiri_work.js?${_}${gapp.version}`); worker.onerror = (error) => {
console.log({ WORKER_ERROR: error });
error.preventDefault();
};
worker.onmessageerror = (error) => {
console.log({ WORKER_MESSAGE_ERROR: error });
error.preventDefault();
};
return worker;
} }
}, },
@ -85,6 +102,10 @@ const client = exports({
return current > 0; return current > 0;
}, },
start() {
client.restart();
},
restart() { restart() {
// prevent re-entry from cancel callback // prevent re-entry from cancel callback
if (restarting) { if (restarting) {
@ -108,7 +129,7 @@ const client = exports({
running = {}; running = {};
worker = client.newWorker(); worker = client.newWorker();
client.onmessage = worker.onmessage = function(e) { client.onmessage = worker.onmessage = (e) => {
let now = time(), let now = time(),
reply = e.data, reply = e.data,
record = running[reply.seq], record = running[reply.seq],
@ -157,7 +178,7 @@ const client = exports({
// widget sync // widget sync
sync(widgets) { sync(widgets) {
if (!widgets) { if (!widgets) {
widgets = kiri.api.widgets.all(); widgets = api.widgets.all();
} }
// sync any widget that has changed // sync any widget that has changed
for (let widget of widgets.filter(w => w.modified || !syncd[w.id])) { for (let widget of widgets.filter(w => w.modified || !syncd[w.id])) {
@ -181,7 +202,7 @@ const client = exports({
send("rotate", { settings }, reply => { send("rotate", { settings }, reply => {
if (reply.group) { if (reply.group) {
// collect post-rotation data for slice/preview renders // collect post-rotation data for slice/preview renders
for (let widget of kiri.Widget.Groups.forid(reply.group)) { for (let widget of Widget.Groups.forid(reply.group)) {
widget.belt = reply.belt; widget.belt = reply.belt;
} }
} else if (callback) { } else if (callback) {
@ -241,7 +262,7 @@ const client = exports({
ondone(null, reply.error); ondone(null, reply.error);
} }
if (reply.debug) { if (reply.debug) {
kiri.api.event.emit("export.debug", reply.debug); api.event.emit("export.debug", reply.debug);
} }
}); });
}, },
@ -262,10 +283,10 @@ const client = exports({
y: -origin.y, y: -origin.y,
z: origin.z z: origin.z
}; };
const output = kiri.newPrint().parseSVG(code, offset); const output = newPrint().parseSVG(code, offset);
send("parse_svg", output, reply => { send("parse_svg", output, reply => {
if (reply.parsed) { if (reply.parsed) {
done(kiri.codec.decode(reply.parsed)); done(codec.decode(reply.parsed));
} }
}); });
return; return;
@ -275,7 +296,7 @@ const client = exports({
progress(reply.progress); progress(reply.progress);
} }
if (reply.parsed) { if (reply.parsed) {
done(kiri.codec.decode(reply.parsed), reply.maxSpeed, reply.minSpeed); done(codec.decode(reply.parsed), reply.maxSpeed, reply.minSpeed);
} }
}); });
}, },
@ -330,9 +351,8 @@ const client = exports({
// console.log({ clearCache_reply: reply }); // console.log({ clearCache_reply: reply });
}); });
} }
}); };
// start worker self.kiri_client = client;
client.restart();
}); export default client;

View file

@ -1,32 +1,14 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { Layers } from './layers.js';
import { newPoint } from '../../geo/point.js';
import { Polygon, newPolygon } from '../../geo/polygon.js';
import { Slice, newSlice, Top, newTop } from './slice.js';
import { Widget, newWidget } from './widget.js';
// dep: main.kiri
// dep: geo.base
// dep: geo.polygon
// dep: kiri.widget
// dep: kiri.slice
// dep: kiri.layers
gapp.register("kiri.codec", [], (root, exports) => {
const { base, kiri } = root;
const decoders = {}; const decoders = {};
const freeMem = true; const freeMem = true;
const codec = exports({
undef: undefined,
encode: encode,
decode: decode,
registerDecoder: registerDecoder,
allocFloat32Array: allocFloat32Array,
encodePointArray,
decodePointArray,
encodePointArray2D,
decodePointArray2D,
toCodable
});
const TYPE = { const TYPE = {
WIDGET: 100, WIDGET: 100,
SLICE: 200, SLICE: 200,
@ -210,7 +192,7 @@ function decodePointArray(array) {
const points = new Array(length / 3); const points = new Array(length / 3);
for (let vid=0, pid=0; vid < length; ) { for (let vid=0, pid=0; vid < length; ) {
points[pid++] = base.newPoint(array[vid++], array[vid++], array[vid++]); points[pid++] = newPoint(array[vid++], array[vid++], array[vid++]);
} }
return points; return points;
@ -225,7 +207,7 @@ function decodePointArray2D(array, z, fn) {
for (let vid=0, pid=0; vid < length; ) { for (let vid=0, pid=0; vid < length; ) {
points[pid++] = fn ? points[pid++] = fn ?
fn(array[vid++], array[vid++]) : fn(array[vid++], array[vid++]) :
base.newPoint(array[vid++], array[vid++], z); newPoint(array[vid++], array[vid++], z);
} }
return points; return points;
@ -233,7 +215,7 @@ function decodePointArray2D(array, z, fn) {
// ----- Widget Codec ----- // ----- Widget Codec -----
kiri.Widget.prototype.encode = function(state) { Widget.prototype.encode = function(state) {
const json = state._json_; const json = state._json_;
const geo = this.getGeoVertices(); const geo = this.getGeoVertices();
const coded = { const coded = {
@ -252,7 +234,7 @@ registerDecoder(TYPE.WIDGET, function(v, state) {
const id = v.id, const id = v.id,
group = v.group || id, group = v.group || id,
track = v.track || undefined, track = v.track || undefined,
widget = kiri.newWidget(id, kiri.Widget.Groups.forid(group)); widget = newWidget(id, Widget.Groups.forid(group));
widget.loadVertices(v.json ? v.geo.toFloat32() : v.geo); widget.loadVertices(v.json ? v.geo.toFloat32() : v.geo);
widget.saved = Date.now(); widget.saved = Date.now();
@ -266,7 +248,7 @@ registerDecoder(TYPE.WIDGET, function(v, state) {
// ----- Slice Codec ----- // ----- Slice Codec -----
kiri.Slice.prototype.encode = function(state) { Slice.prototype.encode = function(state) {
const rv = { const rv = {
type: TYPE.SLICE, type: TYPE.SLICE,
z: this.z, z: this.z,
@ -279,7 +261,7 @@ kiri.Slice.prototype.encode = function(state) {
}; };
registerDecoder(TYPE.SLICE, function(v, state) { registerDecoder(TYPE.SLICE, function(v, state) {
let slice = kiri.newSlice(v.z, state.mesh ? state.mesh.newGroup() : null); let slice = newSlice(v.z, state.mesh ? state.mesh.newGroup() : null);
slice.index = v.index; slice.index = v.index;
slice.layers = decode(v.layers, state) slice.layers = decode(v.layers, state)
@ -289,7 +271,7 @@ registerDecoder(TYPE.SLICE, function(v, state) {
// ----- Slice.Top Codec ----- // ----- Slice.Top Codec -----
kiri.Top.prototype.encode = function(state) { Top.prototype.encode = function(state) {
let obj = { let obj = {
type: TYPE.TOP, type: TYPE.TOP,
poly: encode(this.poly, state) poly: encode(this.poly, state)
@ -307,7 +289,7 @@ kiri.Top.prototype.encode = function(state) {
}; };
registerDecoder(TYPE.TOP, function(v, state) { registerDecoder(TYPE.TOP, function(v, state) {
let top = kiri.newTop(decode(v.poly, state)); let top = newTop(decode(v.poly, state));
if (state.full) { if (state.full) {
// top.gaps = decode(v.gaps, state); // top.gaps = decode(v.gaps, state);
top.last = decode(v.last, state); top.last = decode(v.last, state);
@ -320,7 +302,7 @@ registerDecoder(TYPE.TOP, function(v, state) {
// ----- Polygon Codec ----- // ----- Polygon Codec -----
base.Polygon.prototype.encode = function(state) { Polygon.prototype.encode = function(state) {
if (!state.poly) state.poly = {}; if (!state.poly) state.poly = {};
let cached = state.poly[this.id]; let cached = state.poly[this.id];
@ -352,10 +334,10 @@ registerDecoder(TYPE.POLY, function(v, state) {
const array = v.array; const array = v.array;
const length = array.length; const length = array.length;
const poly = base.newPolygon(); const poly = newPolygon();
for (let vid = 0; vid < length; ) { for (let vid = 0; vid < length; ) {
poly.push(base.newPoint(array[vid++], array[vid++], array[vid++])); poly.push(newPoint(array[vid++], array[vid++], array[vid++]));
} }
state.poly[v.id] = poly; state.poly[v.id] = poly;
@ -382,7 +364,7 @@ function encodeLayerPolys(polys, state) {
}); });
} }
kiri.Layers.prototype.encode = function(state) { Layers.prototype.encode = function(state) {
let zeros = state.zeros; let zeros = state.zeros;
let enc = { let enc = {
type: TYPE.LAYERS, type: TYPE.LAYERS,
@ -419,7 +401,7 @@ kiri.Layers.prototype.encode = function(state) {
}; };
registerDecoder(TYPE.LAYERS, function(v, state) { registerDecoder(TYPE.LAYERS, function(v, state) {
const render = new kiri.Layers(); const render = new Layers();
const { layers } = v; const { layers } = v;
for (let i=0; i<layers.length; i++) { for (let i=0; i<layers.length; i++) {
@ -463,4 +445,28 @@ registerDecoder(TYPE.LAYERS, function(v, state) {
return render; return render;
}); });
}); export const codec = {
encode,
decode,
registerDecoder,
allocFloat32Array,
encodePointArray,
decodePointArray,
encodePointArray2D,
decodePointArray2D,
toCodable
};
export {
encode,
decode,
registerDecoder,
allocFloat32Array,
encodePointArray,
decodePointArray,
encodePointArray2D,
decodePointArray2D,
toCodable
};
self.kiri_codec = codec;

View file

@ -1,13 +1,5 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: add.array
// dep: data.local
gapp.register("kiri.conf", (root, exports) => {
const { data } = root;
const { local } = data;
const { clone } = Object; const { clone } = Object;
const CVER = 410; const CVER = 410;
@ -215,12 +207,11 @@ const renamed = {
drillDwell: "camDrillDwell", drillDwell: "camDrillDwell",
drillLift: "camDrillLift", drillLift: "camDrillLift",
drillingOn: "camDrillingOn", drillingOn: "camDrillingOn",
camPocketOnlyFinish: "camOutlinePocket",
camWideCutout: "camOutlineWide", camWideCutout: "camOutlineWide",
outputClockwise: "camConventional" outputClockwise: "camConventional"
}; };
const conf = exports({ export const conf = {
// --------------- helper functions // --------------- helper functions
normalize, normalize,
device_from_code, device_from_code,
@ -431,9 +422,10 @@ const conf = exports({
camLevelTool: 1000, camLevelTool: 1000,
camLevelSpindle: 1000, camLevelSpindle: 1000,
camLevelOver: 0.75, camLevelOver: 0.75,
camLevelStepZ: 0,
camLevelSpeed: 1000, camLevelSpeed: 1000,
camLevelDown: 0, camLevelDown: 0,
camLevelOver: 0.5, camLevelInset: 0.5,
camLevelStock: true, camLevelStock: true,
camRoughTool: 1000, camRoughTool: 1000,
camRoughSpindle: 1000, camRoughSpindle: 1000,
@ -494,7 +486,7 @@ const conf = exports({
camTracePlunge: 200, camTracePlunge: 200,
camTraceOffOver: 0, camTraceOffOver: 0,
camTraceDogbone: false, camTraceDogbone: false,
camTraceMerge: true, camTraceMerge: false,
camTraceLines: false, camTraceLines: false,
camTraceZTop: 0, camTraceZTop: 0,
camTraceZBottom: 0, camTraceZBottom: 0,
@ -527,6 +519,21 @@ const conf = exports({
camRegisterSpeed: 1000, camRegisterSpeed: 1000,
camRegisterThru: 5, camRegisterThru: 5,
camRegisterOffset: 10, 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", camFlipAxis: "X",
camFlipOther: "", camFlipOther: "",
camLaserEnable: ["M321"], camLaserEnable: ["M321"],
@ -580,7 +587,8 @@ const conf = exports({
outputInvertY: false, outputInvertY: false,
camExpertFast: false, camExpertFast: false,
camTrueShadow: false, camTrueShadow: false,
camArcTolerance: 0.006, camArcEnabled: false,
camArcTolerance: 0.15,
camArcResolution: 5, camArcResolution: 5,
camForceZMax: false, camForceZMax: false,
camFirstZMax: false, camFirstZMax: false,
@ -973,7 +981,7 @@ const conf = exports({
id: genID(), id: genID(),
ver: CVER ver: CVER
} }
}); };
const settings = conf.template; const settings = conf.template;
@ -992,5 +1000,3 @@ settings.cdev.LASER = clone(settings.device);
settings.cdev.DRAG = clone(settings.device); settings.cdev.DRAG = clone(settings.device);
settings.cdev.WJET = clone(settings.device); settings.cdev.WJET = clone(settings.device);
settings.cdev.WEDM = clone(settings.device); settings.cdev.WEDM = clone(settings.device);
});

View file

@ -1,9 +1,5 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
gapp.register("kiri.consts", (root, exports) => {
const COLOR = { const COLOR = {
wireframe: 0x444444, wireframe: 0x444444,
wireframe_opacity: 0.25, wireframe_opacity: 0.25,
@ -94,6 +90,11 @@ const LISTS = {
{ name: "inside" }, { name: "inside" },
{ name: "outside" } { name: "outside" }
], ],
helicaloff:[
{ name: "auto" },
{ name: "inside" },
{ name: "outside" },
],
zanchor: [ zanchor: [
{ name: "top" }, { name: "top" },
{ name: "middle" }, { name: "middle" },
@ -145,16 +146,22 @@ const PMODES = {
TOOLS: 2 TOOLS: 2
}; };
const PATHS = {
work: "../lib/kiri/run/worker.js",
pool: "./minion.js"
};
const SEED = 'kiri-seed'; const SEED = 'kiri-seed';
exports({ export let consts = {
PMODES,
COLOR, COLOR,
LISTS, LISTS,
MODES, MODES,
PATHS,
PMODES,
VIEWS, VIEWS,
SEED, SEED,
beltfact: Math.cos(Math.PI / 4) beltfact: Math.cos(Math.PI / 4)
}); };
}); export { COLOR, LISTS, MODES, VIEWS, PATHS, PMODES, SEED };

View file

@ -13,7 +13,7 @@ const gapp = self.gapp;
eval( fs.readFileSync("src/add/array.js").toString() ); eval( fs.readFileSync("src/add/array.js").toString() );
eval( fs.readFileSync("src/data/local.js").toString() ); eval( fs.readFileSync("src/data/local.js").toString() );
eval( fs.readFileSync("src/kiri/conf.js").toString() ); eval( fs.readFileSync("src/kiri/core/conf.js").toString() );
gapp.main(undefined, undefined, root => { gapp.main(undefined, undefined, root => {
let kiri = root.kiri; let kiri = root.kiri;

382
src/kiri/core/devices.js Normal file
View file

@ -0,0 +1,382 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { $, h } from '../../moto/webui.js';
import { api } from './api.js';
import { conf } from './conf.js';
import { space } from '../../moto/space.js';
import { devices as devlist } from '../../pack/kiri-devs.js';
import { settings, conf as setconf } from './settings.js';
export const device = {
clone: cloneDevice,
code: currentDeviceCode,
get: currentDeviceName,
set: selectDevice,
isBelt
};
export const devices = {
show: showDevices,
select: selectDevice,
refresh: updateDeviceList,
update_laser_state: updateLaserState
};
function isBelt() {
return setconf.get().device.bedBelt;
}
function currentDeviceName() {
return setconf.get().filter[api.mode.get()];
}
function currentDeviceCode() {
return setconf.get().devices[currentDeviceName()];
}
function getModeDevices() {
return Object.keys(devlist[ api.mode.get_lower() ]).sort();
}
export function showDevices() {
settings.sync.get().then(_showDevices);
}
function _showDevices() {
updateDeviceList();
api.modal.show('setup');
}
function updateDeviceList() {
renderDevices(getModeDevices());
}
function updateDeviceName(newname) {
let selected = api.device.get(),
devs = setconf.get().devices;
if (newname !== selected) {
devs[newname] = devs[selected];
delete devs[selected];
selectDevice(newname);
updateDeviceList();
}
}
function putLocalDevice(devicename, obj) {
setconf.get().devices[devicename] = obj;
setconf.save();
}
function removeLocalDevice(devicename) {
delete setconf.get().devices[devicename];
setconf.save();
settings.sync.put();
}
function isLocalDevice(devicename) {
return setconf.get().devices[devicename] ? true : false;
}
function getSelectedDevice() {
return api.device.get();
}
function selectDevice(devicename) {
if (isLocalDevice(devicename)) {
setDeviceCode(setconf.get().devices[devicename], devicename);
} else {
let code = devlist[api.mode.get_lower()][devicename];
if (code) {
setDeviceCode(code, devicename);
}
}
}
// only for local filters
function cloneDevice() {
let name = `${getSelectedDevice().replace(/\./g,' ')}`;
let code = api.clone(setconf.get().device);
code.mode = api.mode.get();
if (name.toLowerCase().indexOf('my ') >= 0) {
name = `${name} copy`;
} else {
name = `My ${name}`;
}
putLocalDevice(name, code);
setDeviceCode(code, name);
settings.sync.put();
}
function updateLaserState() {
const dev = setconf.get().device;
$('laser-on').style.display = dev.useLaser ? 'flex' : 'none';
$('laser-off').style.display = dev.useLaser ? 'flex' : 'none';
}
function setDeviceCode(code, devicename) {
api.event.emit('device.select', devicename);
try {
if (typeof(code) === 'string') code = js2o(code) || {};
let mode = api.mode.get(),
lmode = mode.toLowerCase(),
current = setconf.get(),
local = isLocalDevice(devicename),
dev = current.device = conf.device_from_code(code,mode),
dproc = current.devproc[devicename], // last process name for this device
newdev = dproc === undefined, // first time device is selected
predev = current.filter[mode], // previous device selection
chgdev = predev !== devicename; // device is changing
// fill missing device fields
conf.fill_cull_once(dev, conf.defaults[lmode].d);
// first time device use, add any print profiles and set to default if present
if (code.profiles) {
for (let profile of code.profiles) {
let profname = profile.processName;
// if no saved profile by that name for this mode...
if (!current.sproc[mode][profname]) {
console.log('adding profile', profname, 'to', mode);
current.sproc[mode][profname] = profile;
}
// if it's a new device, seed the new profile name as last profile
if (newdev && !current.devproc[devicename]) {
console.log('setting default profile for new device', devicename, 'to', profname);
current.devproc[devicename] = dproc = profname;
}
}
}
dev.new = false;
dev.deviceName = devicename;
let { platform, ui, uc } = api;
ui.deviceBelt.checked = dev.bedBelt;
ui.deviceRound.checked = dev.bedRound;
ui.deviceOrigin.checked = dev.ctOriginCenter || dev.originCenter || dev.bedRound;
ui.fwRetract.checked = dev.fwRetract;
// add extruder selection buttons
if (dev.extruders) {
let ext = api.lists.extruders = [];
dev.internal = 0;
for (let i=0; i<dev.extruders.length; i++) {
ext.push({id:i, name:i});
}
}
// disable editing for non-local devices
[
// ui.deviceName,
ui.gcodePre,
ui.gcodePost,
ui.bedDepth,
ui.bedWidth,
ui.maxHeight,
ui.useLaser,
ui.resolutionX,
ui.resolutionY,
ui.deviceOrigin,
ui.deviceRound,
ui.deviceBelt,
ui.fwRetract,
ui.deviceZMax,
ui.gcodeTime,
ui.gcodeFan,
ui.gcodeFeature,
ui.gcodeTrack,
ui.gcodeLayer,
ui.extFilament,
ui.extNozzle,
ui.spindleMax,
ui.gcodeSpindle,
ui.gcodeDwell,
ui.gcodeChange,
ui.gcodeFExt,
ui.gcodeSpace,
ui.gcodeStrip,
ui.gcodeLaserOn,
ui.gcodeLaserOff,
ui.laserMaxPower,
ui.extPrev,
ui.extNext,
ui.extAdd,
ui.extDel,
ui.extOffsetX,
ui.extOffsetY
].forEach(function(e) {
e.disabled = !local;
});
ui.deviceSave.disabled = !local;
ui.deviceDelete.disabled = !local;
ui.deviceRename.disabled = !local;
ui.deviceExport.disabled = !local;
ui.deviceAdd.style.display = mode === 'SLA' ? 'none' : '';
if (local) {
ui.deviceAdd.innerText = "copy";
ui.deviceDelete.style.display = '';
ui.deviceRename.style.display = '';
ui.deviceExport.style.display = '';
} else {
ui.deviceAdd.innerText = "customize";
ui.deviceDelete.style.display = 'none';
ui.deviceRename.style.display = 'none';
ui.deviceExport.style.display = 'none';
}
ui.deviceAdd.disabled = dev.noclone;
setconf.update_fields();
space.platform.setBelt(isBelt());
platform.update_size();
platform.update_origin();
platform.update();
updateLaserState();
// store current device name for this mode
current.filter[mode] = devicename;
// cache device record for this mode (restored in setMode)
current.cdev[mode] = dev;
if (dproc) {
// restore last process associated with this device
setconf.load(null, dproc);
} else {
setconf.update();
}
setconf.save();
if (isBelt()) {
// space.view.setHome(dev.bedBelt ? Math.PI/2 : 0, Math.PI / 2.5);
space.view.setHome(0, Math.PI / 2.5);
} else {
space.view.setHome(0);
}
// when changing devices, update focus on widgets
if (chgdev) {
setTimeout(api.space.set_focus, 0);
}
uc.refresh(1);
api.event.emit('device.selected', dev);
} catch (e) {
console.log({error:e, device:code, devicename});
api.show.alert(`invalid or deprecated device: "${devicename}"`, 10);
api.show.alert(`please select a new device`, 10);
throw e;
showDevices();
}
api.function.clear();
api.event.settings();
}
function renderDevices(devices) {
let selected = api.device.get() || devices[0],
features = api.feature,
devs = setconf.get().devices,
dfilter = typeof(features.device_filter) === 'function' ? features.device_filter : undefined;
for (let local in devs) {
if (!(devs.hasOwnProperty(local) && devs[local])) {
continue;
}
let dev = devs[local],
fdmCode = dev.cmd,
fdmMode = (api.mode.get() === 'FDM');
if (dev.mode ? (dev.mode === api.mode.get()) : (fdmCode ? fdmMode : !fdmMode)) {
devices.push(local);
}
};
devices = devices.sort();
let { event, ui } = api;
event.emit('devices.render', devices);
ui.deviceSave.onclick = function() {
event.emit('device.save');
api.function.clear();
setconf.save();
settings.sync.put();
showDevices();
api.modal.hide();
};
ui.deviceAdd.onclick = function() {
api.function.clear();
cloneDevice();
showDevices();
};
ui.deviceDelete.onclick = function() {
api.function.clear();
removeLocalDevice(getSelectedDevice());
selectDevice(getModeDevices()[0]);
showDevices();
};
ui.deviceRename.onclick = function() {
api.uc.prompt(`Rename "${selected}`, selected).then(newname => {
if (newname) {
updateDeviceName(newname);
setconf.save();
settings.sync.put();
showDevices();
} else {
showDevices();
}
});
};
ui.deviceExport.onclick = function(event) {
const record = {
version: kiri.version,
device: selected,
process: api.process.code(),
profiles: event.altKey ? settings.prof() : undefined,
code: devs[selected],
time: Date.now()
};
let exp = api.util.b64enc(record);
api.device.export(exp, selected, { event, record });
};
let dedup = {};
let list_cdev = [];
let list_mdev = [];
devices.forEach(function(device, index) {
// prevent device from appearing twice
// such as local name = standard device name
if (dedup[device]) {
return;
}
dedup[device] = device;
let loc = isLocalDevice(device);
if (dfilter && dfilter(device) === false) {
return;
}
if (loc) {
list_mdev.push(h.option(device));
} else {
list_cdev.push(h.option(device));
}
});
let dev_list = $('dev-list');
h.bind(dev_list, [
h.option({ _: '-- My Devices --', disabled: true }),
...list_mdev,
h.option({ _: '-- Stock Devices --', disabled: true }),
...list_cdev
]);
let dev_opts = [...dev_list.options].map(o => o.innerText);
dev_list.selectedIndex = dev_opts.indexOf(selected);
dev_list.onchange = ev => {
const seldev = dev_list.options[dev_list.selectedIndex];
selectDevice(seldev.innerText);
api.platform.layout();
}
selectDevice(selected);
}

View file

@ -1,13 +1,8 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
// dep: kiri.api import { api } from './api.js';
// use: kiri.selection import { space } from '../../moto/space.js';
// use: moto.space
gapp.register("kiri.do", [], (root, events) => {
const { kiri, moto } = root;
const { api } = kiri;
const { space } = moto;
const { event } = api; const { event } = api;
let stack = []; let stack = [];
@ -44,7 +39,7 @@ let clear = api.doit.clear = function() {
}; };
function updateButtons() { function updateButtons() {
let isArrange = api.view.get() === kiri.consts.VIEWS.ARRANGE; let isArrange = api.view.get() === api.consts.VIEWS.ARRANGE;
$('doit').style.display = isArrange && stack.length ? 'flex' : 'none'; $('doit').style.display = isArrange && stack.length ? 'flex' : 'none';
$('undo').disabled = stpos === 0; $('undo').disabled = stpos === 0;
$('redo').disabled = stpos == stack.length; $('redo').disabled = stpos == stack.length;
@ -175,5 +170,3 @@ event.on('mouse.drag.done', () => {
}); });
moved = {x:0, y:0}; moved = {x:0, y:0};
}); });
});

View file

@ -1,23 +1,13 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { $ } from '../../moto/webui.js';
import { api } from './api.js';
// dep: ext.md5 import { client } from './client.js';
// dep: geo.base import { local } from '../../data/local.js';
// dep: data.local import { util } from '../../geo/base.js';
// dep: kiri.consts import { MODES } from './consts.js';
// dep: kiri.api import { LASER as laser_driver } from '../mode/laser/driver.js';
// dep: kiri.main import { SLA as sla_client } from '../mode/sla/client.js';
gapp.register("kiri.export", [], (root, exports) => {
const { base, data, kiri } = root;
const { api, consts } = kiri;
const { local } = data;
const { util } = base;
const { stats, ui } = api;
const { MODES } = consts;
kiri.export = exportFile;
let printSeq = parseInt(local['kiri-print-seq'] || local['print-seq'] || "0") + 1; let printSeq = parseInt(local['kiri-print-seq'] || local['print-seq'] || "0") + 1;
@ -29,7 +19,7 @@ function localSet(key, val) {
return api.local.set(key, val); return api.local.set(key, val);
} }
function exportFile(options) { export function exportFile(options) {
let mode = api.mode.get(); let mode = api.mode.get();
let names = api.widgets.all().map(w => w.meta ? w.meta.file : undefined) let names = api.widgets.all().map(w => w.meta ? w.meta.file : undefined)
.filter(v => v) .filter(v => v)
@ -53,7 +43,7 @@ function callExport(callback, mode, names) {
let gcode = []; let gcode = [];
let section = []; let section = [];
let sections = { }; let sections = { };
kiri.client.export(api.conf.get(), (line) => { client.export(api.conf.get(), (line) => {
if (typeof line !== 'string') { if (typeof line !== 'string') {
if (line.section) { if (line.section) {
sections[line.section] = section = []; sections[line.section] = section = [];
@ -75,7 +65,7 @@ function callExport(callback, mode, names) {
} }
function callExportLaser(options, names) { function callExportLaser(options, names) {
kiri.client.export(api.conf.get(), (line) => { client.export(api.conf.get(), (line) => {
// engine export uses lines // engine export uses lines
// console.log({unexpected_line: line}); // console.log({unexpected_line: line});
}, (output, error) => { }, (output, error) => {
@ -89,14 +79,14 @@ function callExportLaser(options, names) {
} }
function callExportSLA(options, names) { function callExportSLA(options, names) {
kiri.client.export(api.conf.get(), (line) => { client.export(api.conf.get(), (line) => {
api.show.progress(line.progress, "exporting"); api.show.progress(line.progress, "exporting");
}, (output, error) => { }, (output, error) => {
api.show.progress(0); api.show.progress(0);
if (error) { if (error) {
api.show.alert(error, 5); api.show.alert(error, 5);
} else { } else {
kiri.driver.SLA.printDownload(output, api, names); sla_client.printDownload(output, api, names);
} }
}); });
} }
@ -107,7 +97,7 @@ function exportLaserDialog(data, names) {
const fileroot = names[0] || "laser"; const fileroot = names[0] || "laser";
const filename = `${fileroot}-${(printSeq.toString().padStart(3,"0"))}`; const filename = `${fileroot}-${(printSeq.toString().padStart(3,"0"))}`;
const settings = api.conf.get(); const settings = api.conf.get();
const driver = kiri.driver.LASER; const driver = laser_driver;
function download_svg() { function download_svg() {
api.util.download( api.util.download(
@ -207,7 +197,7 @@ function exportGCodeDialog(gcode, sections, info, names) {
ajax.onreadystatechange = function() { ajax.onreadystatechange = function() {
if (ajax.readyState === 4) { if (ajax.readyState === 4) {
let status = ajax.status; let status = ajax.status;
stats.add(`ua_${api.mode.get_lower()}_print_octo_${status}`); api.stats.add(`ua_${api.mode.get_lower()}_print_octo_${status}`);
if (status >= 200 && status < 300) { if (status >= 200 && status < 300) {
api.modal.hide(); api.modal.hide();
} else { } else {
@ -278,11 +268,11 @@ function exportGCodeDialog(gcode, sections, info, names) {
) )
.then(t => t.text()) .then(t => t.text())
.then(t => { .then(t => {
stats.add(`ua_${api.mode.get_lower()}_print_local_ok`); api.stats.add(`ua_${api.mode.get_lower()}_print_local_ok`);
console.log({grid_spool_said: t}); console.log({grid_spool_said: t});
}) })
.catch(e => { .catch(e => {
stats.add(`ua_${api.mode.get_lower()}_print_local_err`); api.stats.add(`ua_${api.mode.get_lower()}_print_local_err`);
console.log({grid_local_spool_error: e}); console.log({grid_local_spool_error: e});
}) })
.finally(() => { .finally(() => {
@ -394,7 +384,7 @@ function exportGCodeDialog(gcode, sections, info, names) {
xhtr.onreadystatechange = function() { xhtr.onreadystatechange = function() {
if (xhtr.readyState === 4) { if (xhtr.readyState === 4) {
let status = xhtr.status; let status = xhtr.status;
stats.add(`ua_${api.mode.get_lower()}_print_grid_${status}`); api.stats.add(`ua_${api.mode.get_lower()}_print_grid_${status}`);
if (status >= 200 && status < 300) { if (status >= 200 && status < 300) {
let json = js2o(xhtr.responseText); let json = js2o(xhtr.responseText);
gridhost_tracker(host,json.key); gridhost_tracker(host,json.key);
@ -481,7 +471,7 @@ function exportGCodeDialog(gcode, sections, info, names) {
$('print-filament').style.display = fdm ? '' : 'none'; $('print-filament').style.display = fdm ? '' : 'none';
$('print-filename').value = filename; $('print-filename').value = filename;
$('print-filesize').value = util.comma(info.bytes); $('print-filesize').value = util.comma(info.bytes);
$('print-filament').value = Math.round(info.distance); $('print-length').value = Math.round(info.distance);
if (set.controller.devel) { if (set.controller.devel) {
$('code-preview-textarea').style.height = "30em"; $('code-preview-textarea').style.height = "30em";
} }
@ -511,7 +501,7 @@ function exportGCodeDialog(gcode, sections, info, names) {
}) })
} }
} }
kiri.client.zip(files, progress => { client.zip(files, progress => {
api.show.progress(progress.percent/100, "generating zip files"); api.show.progress(progress.percent/100, "generating zip files");
}, output => { }, output => {
api.show.progress(0); api.show.progress(0);
@ -681,7 +671,7 @@ function exportGCodeDialog(gcode, sections, info, names) {
name: `Metadata/top_1.png`, name: `Metadata/top_1.png`,
data: api.view.bambu.s512.png data: api.view.bambu.s512.png
}]; }];
kiri.client.zip(files, progress => { client.zip(files, progress => {
api.show.progress(progress.percent/100, "generating 3mf"); api.show.progress(progress.percent/100, "generating 3mf");
}, output => { }, output => {
api.show.progress(0); api.show.progress(0);
@ -742,5 +732,3 @@ function exportGCodeDialog(gcode, sections, info, names) {
// preview of the generated GCODE (first 64k max) // preview of the generated GCODE (first 64k max)
if (preview && gcode) $('code-preview-textarea').value = gcode.substring(0,65535); if (preview && gcode) $('code-preview-textarea').value = gcode.substring(0,65535);
} }
});

View file

@ -1,13 +1,5 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: main.kiri
// use: data.index
gapp.register("kiri.files", [], (root, exports) => {
const { kiri } = self;
class Files { class Files {
constructor(indexdb) { constructor(indexdb) {
let store = this; let store = this;
@ -176,8 +168,8 @@ function notifyFileListeners(store) {
} }
} }
kiri.openFiles = function(indexdb) { export const openFiles = function(indexdb) {
return new Files(indexdb); return new Files(indexdb);
}; };
}); export { saveFileList, notifyFileListeners };

View file

@ -1,19 +1,12 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { api } from './api.js';
import { load } from '../../load/file.js';
// use: load.stl import { newWidget } from './widget.js';
// use: load.svg import { VIEWS } from './consts.js';
// use: kiri.api
// use: kiri.platform
// use: kiri.settings
// use: kiri.widget
gapp.register("kiri.frame", [], (root, exports) => {
// add frame message api listener // add frame message api listener
window.addEventListener('message', msg => { window.addEventListener('message', msg => {
const { load, kiri, moto } = self;
const { api, newWidget } = kiri;
const { conf, event, feature, platform, settings, show } = api; const { conf, event, feature, platform, settings, show } = api;
if (!feature.frame) return; if (!feature.frame) return;
@ -126,5 +119,3 @@ window.addEventListener('message', msg => {
show.progress(data.progress, data.message); show.progress(data.progress, data.message);
} }
}); });
});

View file

@ -1,30 +1,16 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { api } from './api.js';
import { client } from './client.js';
// dep: kiri.api import { codec } from './codec.js';
// dep: kiri.client import { space } from '../../moto/space.js';
// dep: kiri.export import { COLOR, PMODES } from './consts.js';
// dep: moto.space import { exportFile } from './export.js';
// use: kiri-mode.cam.client
// dep: kiri-mode.fdm.client
// dep: kiri-mode.sla.client
// dep: kiri-mode.laser.driver
// dep: kiri-mode.drag.driver
// dep: kiri-mode.wjet.driver
// dep: kiri-mode.wedm.driver
gapp.register("kiri.function", (root, exports) => {
const { kiri } = root;
const { api, client, consts, utils } = kiri;
const { space } = moto;
const { COLOR, PMODES } = consts;
let complete = {}; let complete = {};
function prepareSlices(callback, scale = 1, offset = 0) { function prepareSlices(callback, scale = 1, offset = 0) {
const { conf, event, feature, hide, mode, view, platform, show } = api; const { conf, event, feature, hide, mode, view, platform, show, stacks } = api;
const { stacks } = kiri;
if (view.is_arrange()) { if (view.is_arrange()) {
// in arrange mode, create a screenshot at the start slicing // in arrange mode, create a screenshot at the start slicing
@ -139,16 +125,43 @@ function prepareSlices(callback, scale = 1, offset = 0) {
} }
function sliceWidget(widget) { function sliceWidget(widget) {
// weight each widget progress % by their # vertices
let factor = (widget.getVertices().count / defvert);
widget.slice(settings, (sliced, error) => { function onupdate(update, msg, alert) {
if (alert) {
api.show.alert(alert);
}
if (msg && msg !== lastMsg) {
let mark = Date.now();
if (lastMsg) {
let key = slicing.length > 1 ?
`${widget.id}_${segNumber++}_${lastMsg}` :
`${segNumber++}_${lastMsg}`
segtimes[key] = mark - startTime;
}
lastMsg = msg;
startTime = mark;
}
// on update
if (update >= 0) {
track[widget.id] = (update || 0) * factor;
totalProgress = 0;
for (let w of slicing) {
totalProgress += (track[w.id] || 0);
}
show.progress(offset + (totalProgress / slicing.length) * scale, msg);
}
}
function ondone(sliced, error) {
let mark = Date.now(); let mark = Date.now();
// update UI info // update UI info
if (sliced) { if (sliced) {
// update segment time // update segment time
if (lastMsg) { if (lastMsg) {
segtimes[`${widget.id}_${segNumber++}_${lastMsg}`] = mark - startTime; let key = slicing.length > 1 ?
`${widget.id}_${segNumber++}_${lastMsg}` :
`${segNumber++}_${lastMsg}`
segtimes[`${key}`] = mark - startTime;
} }
event.emit('slice', settings.mode); event.emit('slice', settings.mode);
} }
@ -169,28 +182,55 @@ function prepareSlices(callback, scale = 1, offset = 0) {
// start next widget slice // start next widget slice
sliceNext(); sliceNext();
} }
}, (update, msg, alert) => { }
if (alert) {
api.show.alert(alert); // weight each widget progress % by their # vertices
let factor = (widget.getVertices().count / defvert);
widget.settings = settings;
widget.clearSlices();
onupdate(0.0001, "slicing");
// store slicing visuals
widget.stack = api.stacks.create(widget.id, widget.mesh);
// compensate for zcut (widget moved through floor)
widget.stack.obj.view.position.z = widget.track.zcut || 0;
// in case result of slice is nothing, do not preserve previous
widget.slices = []
// executed from kiri.js
client.slice(settings, widget, (reply) => {
if (reply.alert) {
onupdate(null, null, reply.alert);
} }
if (msg && msg !== lastMsg) { if (reply.update) {
let mark = Date.now(); onupdate(reply.update, reply.updateStatus);
if (lastMsg) {
segtimes[`${widget.id}_${segNumber++}_${lastMsg}`] = mark - startTime;
}
lastMsg = msg;
startTime = mark;
} }
// on update if (reply.send_start) {
if (update >= 0) { widget.xfer = {start: reply.send_start};
track[widget.id] = (update || 0) * factor; }
totalProgress = 0; if (reply.stats) {
for (let w of slicing) { widget.stats = reply.stats;
totalProgress += (track[w.id] || 0); }
} if (reply.send_end) {
show.progress(offset + (totalProgress / slicing.length) * scale, msg); widget.stats.load_time = widget.xfer.start - reply.send_end;
}
if (reply.slice) {
widget.slices.push(codec.decode(reply.slice, {mesh:widget.mesh}));
}
if (reply.done) {
ondone(true);
}
if (reply.error) {
ondone(false, reply.error);
} }
}); });
// discard point cache
widget.points = undefined;
} }
function sliceDone() { function sliceDone() {
@ -200,7 +240,10 @@ function prepareSlices(callback, scale = 1, offset = 0) {
}; };
for (let widget of slicing) { for (let widget of slicing) {
// on done // on done
segtimes[`${widget.id}_${segNumber++}_draw`] = widget.render(widget.stack); let key = slicing.length > 1 ?
`${widget.id}_${segNumber++}_draw` :
`${segNumber++}_draw`
segtimes[`${key}`] = widget.render(widget.stack);
// rotate stack for belt beds // rotate stack for belt beds
if (widget.belt) { if (widget.belt) {
widget.stack.obj.rotate(widget.belt); widget.stack.obj.rotate(widget.belt);
@ -247,9 +290,7 @@ function prepareSlices(callback, scale = 1, offset = 0) {
} }
function preparePreview(callback, scale = 1, offset = 0) { function preparePreview(callback, scale = 1, offset = 0) {
const { conf, event, feature, hide, mode, view, platform, show } = api; const { conf, event, feature, hide, mode, view, platform, show, stacks } = api;
const { stacks } = kiri;
const widgets = api.widgets.all(); const widgets = api.widgets.all();
const settings = conf.get(); const settings = conf.get();
const { device, process, controller } = settings; const { device, process, controller } = settings;
@ -305,7 +346,7 @@ function preparePreview(callback, scale = 1, offset = 0) {
client.prepare(settings, (progress, message, layer) => { client.prepare(settings, (progress, message, layer) => {
if (layer) { if (layer) {
output.push(kiri.codec.decode(layer)); output.push(codec.decode(layer));
} }
if (message && message !== lastMsg) { if (message && message !== lastMsg) {
const mark = Date.now(); const mark = Date.now();
@ -407,7 +448,7 @@ function prepareExport() {
} }
api.event.emit("function.export", {mode: settings.mode}); api.event.emit("function.export", {mode: settings.mode});
complete.export = true; complete.export = true;
kiri.export(...argsave); exportFile(...argsave);
} }
function cancelWorker() { function cancelWorker() {
@ -417,8 +458,7 @@ function cancelWorker() {
} }
function parseCode(code, type) { function parseCode(code, type) {
const { conf, event, show, widgets, view } = api; const { conf, event, show, stacks, widgets, view } = api;
const { stacks } = kiri;
const settings = conf.get(); const settings = conf.get();
event.emit("code.load", {code, type}); event.emit("code.load", {code, type});
@ -441,8 +481,12 @@ function parseCode(code, type) {
}); });
} }
function clear_progress() {
complete = {};
}
// extend API (api.function) // extend API (api.function)
const functions = Object.assign(api.function, { export const functions = {
slice: prepareSlices, slice: prepareSlices,
print: preparePreview, print: preparePreview,
prepare: preparePreview, prepare: preparePreview,
@ -451,7 +495,16 @@ const functions = Object.assign(api.function, {
cancel: cancelWorker, cancel: cancelWorker,
parse: parseCode, parse: parseCode,
clear: client.clear, clear: client.clear,
clear_progress() { complete = {} } clear_progress,
}); };
}); export {
prepareSlices as slice,
preparePreview as print,
preparePreview as prepare,
prepareAnimation as animate,
prepareExport as export,
cancelWorker as cancel,
parseCode as parse,
clear_progress,
};

1879
src/kiri/core/init.js Normal file

File diff suppressed because it is too large Load diff

1
src/kiri/core/lang-en.js Symbolic link
View file

@ -0,0 +1 @@
../../../web/kiri/lang/en.js

View file

@ -1,11 +1,6 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; const LANG = self.lang = { current: {} };
gapp.register("kiri.lang", [], (root, exports) => {
const { kiri } = root;
const LANG = kiri.lang = { current: {} };
const KDFL = 'en-us'; const KDFL = 'en-us';
let lset = navigator.language.toLocaleLowerCase(); let lset = navigator.language.toLocaleLowerCase();
@ -78,4 +73,6 @@ LANG.set = function() {
return undefined; return undefined;
} }
}); export default { LANG };
export { LANG };

View file

@ -1,25 +1,9 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { newPolygon, Polygon } from '../../geo/polygon.js';
import { polygons as POLY } from '../../geo/polygons.js';
/* export class Layers {
* provides the `output` abstraction for turning poly lines (open and closed)
* into primordial geometries (faces, lines, color ranges). used inside of
* workers as part of slicing and preview output.
*/
// dep: geo.base
// dep: geo.paths
// dep: geo.polygon
// dep: geo.polygons
gapp.register("kiri.layers", [], (root, exports) => {
const { base, kiri } = root;
const { polygons, newPolygon } = base;
const POLY = base.polygons;
class Layers {
constructor() { constructor() {
this.init(); this.init();
} }
@ -94,7 +78,7 @@ class Layers {
options.open = true; options.open = true;
const polys = []; const polys = [];
for (let i=0; i<lines.length-1; i += 2) { for (let i=0; i<lines.length-1; i += 2) {
polys.push(new base.Polygon() polys.push(new Polygon()
.append(lines[i]) .append(lines[i])
.append(lines[i+1]) .append(lines[i+1])
.setOpen()); .setOpen());
@ -110,11 +94,14 @@ class Layers {
// an open or closed polygon // an open or closed polygon
addPoly(poly, options) { addPoly(poly, options) {
return this.addPolys([poly], options); return this.addPolys([ poly ], options);
} }
// a polygon rendered as a webgl line // a polygon rendered as a webgl line
addPolys(polys, options) { addPolys(polys, options) {
if (!Array.isArray(polys)) {
throw "polys must be an array";
}
if (polys.length === 0) { if (polys.length === 0) {
return this; return this;
} }
@ -186,8 +173,7 @@ class Layers {
p1.setZ(poly.z); p1.setZ(poly.z);
p2.setZ(poly.z); p2.setZ(poly.z);
} }
this.addPolys(p1); this.addPolys([ p1, p2 ]);
this.addPolys(p2);
} }
const color = opts.color ? const color = opts.color ?
(typeof(opts.color) === 'number' ? { line: opts.color, face: opts.color } : opts.color) : (typeof(opts.color) === 'number' ? { line: opts.color, face: opts.color } : opts.color) :
@ -277,7 +263,3 @@ function flat(polys) {
return POLY.flatten([polys.clone(true)], [], true); return POLY.flatten([polys.clone(true)], [], true);
} }
} }
kiri.Layers = Layers;
});

959
src/kiri/core/main.js Normal file
View file

@ -0,0 +1,959 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import './frame.js';
import lang from './lang.js';
import STACKS from './stacks.js';
import { $ } from '../../moto/webui.js';
import { api } from './api.js';
import { broker } from '../../moto/broker.js';
import { consts } from './consts.js';
import { Index } from '../../data/index.js';
import { local as dataLocal } from '../../data/local.js';
import { openFiles } from './files.js';
import { platform } from './platform.js';
import { selection } from './selection.js';
import { settings } from './settings.js';
import { showDevices } from './devices.js';
import { space as SPACE } from '../../moto/space.js';
import { stats } from './stats.js';
import { noop, utils } from './utils.js';
import { version } from '../../moto/license.js';
import { Widget, newWidget } from './widget.js';
import { showTools } from '../mode/cam/tools.js';
let { parseOpt, o2js, js2o, ls2o } = utils,
{ COLOR, MODES, VIEWS } = consts,
LANG = lang.current,
WIN = self.window,
DOC = self.document,
LOC = self.location,
SETUP = parseOpt(LOC.search.substring(1)),
SECURE = isSecure(LOC.protocol),
LOCAL = self.debug && !SETUP.remote,
EVENT = broker,
SDB = dataLocal,
FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')),
clone = Object.clone,
MODE = MODES.FDM,
viewMode = VIEWS.ARRANGE,
autoSaveTimer = null,
inits = parseInt(SDB.getItem('kiri-init') || stats.get('init') || 0) + 1;
// update version and init count
SDB.setItem('kiri-init', inits);
stats.set('init', inits);
stats.set('kiri', version);
// allow widget to straddle client / worker FOR NOW
self.kiri_catalog = FILES;
export const dialog = {
show: showModal,
hide: hideModal,
update_process_list: updateProcessList
};
export const help = {
show: showHelp,
file: showHelpFile
};
export const event = {
on(t,l) { return EVENT.on(t,l) },
emit(t,m,o) { return EVENT.publish(t,m,o) },
bind(t,m,o) { return EVENT.bind(t,m,o) },
alerts(clr) { api.alerts.update(clr) },
import: loadFile,
settings: triggerSettingsEvent
};
export const group = {
merge: groupMerge,
split: groupSplit,
};
export const hide = {
alert(rec, recs) { api.alerts.hide(...arguments) },
import: noop,
slider: hideSlider
};
export const image = {
dialog: loadImageDialog,
convert: loadImageConvert
};
export const modal = {
show: showModal,
hide: hideModal,
visible: modalShowing
};
export const mode = {
get_id() { return MODE },
get_lower: getModeLower,
get: getMode,
set: setMode,
switch: switchMode,
set_expert: noop,
is_fdm() { return MODE === MODES.FDM },
is_cam() { return MODE === MODES.CAM },
is_sla() { return MODE === MODES.SLA },
is_drag() { return MODE === MODES.DRAG },
is_wedm() { return MODE === MODES.WEDM },
is_wjet() { return MODE === MODES.WJET },
is_laser() { return MODE === MODES.LASER },
is_2d() { return false ||
api.mode.is_drag() ||
api.mode.is_wedm() ||
api.mode.is_wjet() ||
api.mode.is_laser()
}
};
export const probe = {
live: "https://live.grid.space",
grid: noop,
local: noop
};
export const process = {
code: currentProcessCode,
get: currentProcessName
};
export const show = {
alert() { return api.alerts.show(...arguments) },
controls: setControlsVisible,
devices: showDevices,
import() { api.ui.import.style.display = '' },
layer: setVisibleLayer,
local: showLocal,
progress: setProgress,
slices: showSlices,
tools: showTools
};
export const space = {
reload,
auto_save,
restore: restoreWorkspace,
clear: clearWorkspace,
save: saveWorkspace,
set_focus: setFocus,
update: SPACE.update,
is_dark() { return settings.ctrl().dark }
};
export const util = {
isSecure,
download: downloadBlob,
ui2rec() { api.conf.update_from(...arguments) },
rec2ui() { api.conf.update_fields(...arguments) },
b64enc(obj) { return base64js.fromByteArray(new TextEncoder().encode(JSON.stringify(obj))) },
b64dec(obj) { return JSON.parse(new TextDecoder().decode(base64js.toByteArray(obj))) }
};
export const view = {
get() { return viewMode },
set() { setViewMode(...arguments) },
set_arrange() { api.view.set(VIEWS.ARRANGE) },
set_slice() { api.view.set(VIEWS.SLICE) },
set_preview() { api.view.set(VIEWS.PREVIEW) },
set_animate() { api.view.set(VIEWS.ANIMATE) },
is_arrange() { return viewMode === VIEWS.ARRANGE },
is_slice() { return viewMode === VIEWS.SLICE },
is_preview() { return viewMode === VIEWS.PREVIEW },
is_animate() { return viewMode === VIEWS.ANIMATE },
update_stack_labels: updateStackLabelState,
update_slider_max: updateSliderMax,
update_slider: updateSlider,
update_speeds: updateSpeeds,
hide_slices: hideSlices,
snapshot: null,
edges: setEdges,
unit_scale: unitScale,
wireframe: setWireframe
};
// add show() to catalog for API
FILES.show = showCatalog;
// patch broker for api backward compatibility
EVENT.on = (topic, listener) => {
EVENT.subscribe(topic, listener);
return EVENT;
};
function updateStackLabelState() {
const settings = api.conf.get();
// match label checkboxes to preference
for (let label of api.stacks.getLabels()) {
let check = `${settings.mode}-${api.view.get()}-${label}`;
api.stacks.setVisible(label, settings.labels[check] !== false);
}
}
function setFocus(sel, point) {
if (point) {
SPACE.platform.setCenter(point.x, point.z, point.y);
SPACE.view.setFocus(new THREE.Vector3(point.x, point.y, point.z));
return;
}
if (sel === undefined) {
sel = api.widgets.all();
} else if (!Array.isArray) {
sel = [ sel ];
} else if (sel.length === 0) {
sel = api.widgets.all();
}
let pos = { x:0, y:0, z:0 };
for (let widget of sel) {
pos.x += widget.track.pos.x;
pos.y += widget.track.pos.y;
pos.z += widget.track.pos.z;
}
if (sel.length) {
pos.x /= sel.length;
pos.y /= sel.length;
pos.z /= sel.length;
}
let cam_index = api.conf.get().process.camStockIndexed || false;
let focus_z = cam_index ? 0 : platform.top_z() / 2;
SPACE.platform.setCenter(pos.x, -pos.y, focus_z);
SPACE.view.setFocus(new THREE.Vector3(pos.x, focus_z, -pos.y));
}
function reload() {
api.event.emit('reload');
do_reload(100);
}
function do_reload(time) {
// allow time for async saves to complete and busy to to to zero
setTimeout(() => {
if (api.busy.val() === 0) {
LOC.reload();
} else {
console.log(`reload deferred on busy=${api.busy.val()}`);
do_reload(250);
}
}, time || 100);
}
function auto_save() {
if (!settings.ctrl().autoSave) {
return;
}
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(() => {
api.space.save(true);
}, 1000);
}
/** ******************************************************************
* Utility Functions
******************************************************************* */
function unitScale() {
return api.mode.is_cam() && settings.ctrl().units === 'in' ? 25.4 : 1;
}
function triggerSettingsEvent() {
api.event.emit('settings', settings.get());
}
function isSecure(proto) {
return proto.toLowerCase().indexOf("https") === 0;
}
function setProgress(value = 0, msg) {
value = (value * 100).round(4);
api.ui.progress.width = value+'%';
if (self.debug) {
// console.log(msg, value.round(2));
api.ui.prostatus.style.display = 'flex';
if (msg) {
api.ui.prostatus.innerHTML = msg;
} else {
api.ui.prostatus.innerHTML = '';
}
}
}
function bound(v,min,max) {
return Math.max(min,Math.min(max,v));
}
function updateSpeeds(maxSpeed, minSpeed) {
const { ui } = api;
ui.speeds.style.display =
maxSpeed &&
settings.mode !== 'SLA' &&
settings.mode !== 'LASER' &&
viewMode === VIEWS.PREVIEW &&
ui.showSpeeds.checked ? 'block' : '';
if (maxSpeed) {
const colors = [];
for (let i = 0; i <= maxSpeed; i += maxSpeed / 20) {
colors.push(Math.round(Math.max(i, 1)));
}
api.client.colors(colors, maxSpeed, speedColors => {
const list = [];
Object.keys(speedColors).map(v => parseInt(v)).sort((a, b) => b - a).forEach(speed => {
const color = speedColors[speed];
const hex = color.toString(16).padStart(6, 0);
const r = (color >> 16) & 0xff;
const g = (color >> 8) & 0xff;
const b = (color >> 0) & 0xff;
const style = `background-color:#${hex}`;
list.push(`<label style="${style}">${speed}</label>`);
});
ui.speedbar.innerHTML = list.join('');
});
api.event.emit('preview.speeds', {
min: minSpeed,
max: maxSpeed
});
}
}
function updateSlider() {
api.event.emit("slider.set", {
start: (api.var.layer_lo / api.var.layer_max),
end: (api.var.layer_hi / api.var.layer_max)
});
api.conf.update_fields_from_range();
}
function setVisibleLayer(h, l) {
h = h >= 0 ? h : api.var.layer_hi;
l = l >= 0 ? l : api.var.layer_lo;
api.var.layer_hi = bound(h, 0, api.var.layer_max);
api.var.layer_lo = bound(l, 0, h);
api.event.emit("slider.label");
updateSlider();
showSlices();
}
function setWireframe(bool, color, opacity) {
api.widgets.each(function(w) { w.setWireframe(bool, color, opacity) });
SPACE.update();
}
function setEdges(bool) {
if (bool && bool.toggle) {
api.local.toggle('model.edges');
} else {
api.local.set('model.edges', bool);
}
bool = api.local.getBoolean('model.edges');
api.widgets.each(w => w.setEdges(bool));
SPACE.update();
}
function updateSliderMax(set) {
let max = STACKS.getRange().tallest - 1;
api.var.layer_max = api.ui.sliderMax.innerText = max;
if (set || max < api.var.layer_hi) {
api.var.layer_hi = api.var.layer_max;
api.event.emit("slider.label");
updateSlider();
}
}
function hideSlices() {
STACKS.clear();
api.widgets.opacity(COLOR.model_opacity);
api.widgets.each(function(widget) {
widget.setWireframe(false);
});
}
function setWidgetVisibility(bool) {
api.widgets.each(w => {
if (bool) {
w.show();
} else {
w.hide();
}
});
}
/**
* hide or show slice-layers and their sub-elements
*
* @param {number} [layer]
*/
function showSlices(layer) {
if (viewMode === VIEWS.ARRANGE) {
return;
}
showSlider();
if (typeof(layer) === 'string' || typeof(layer) === 'number') {
layer = parseInt(layer);
} else {
layer = api.var.layer_hi;
}
layer = bound(layer, 0, api.var.layer_max);
if (layer < api.var.layer_lo) api.var.layer_lo = layer;
api.var.layer_hi = layer;
api.event.emit("slider.label");
updateSlider();
STACKS.setRange(api.var.layer_lo, api.var.layer_hi);
SPACE.update();
}
function showSlider() {
api.ui.layers.style.display = 'flex';
api.ui.slider.style.display = 'flex';
}
function hideSlider() {
api.ui.layers.style.display = 'none';
api.ui.slider.style.display = 'none';
api.ui.speeds.style.display = 'none';
}
function loadImageDialog(image, name, force) {
if (!force && image.byteLength > 2500000) {
return api.uc.confirm("Large images may fail to import<br>Consider resizing under 1000 x 1000<br>Proceed with import?").then(ok => {
if (ok) {
loadImageDialog(image, name, true);
}
});
}
const opt = {pre: [
"<div class='f-col a-center'>",
" <h3>Image Conversion</h3>",
" <p class='t-just' style='width:300px;line-height:1.5em'>",
" This will create a 3D model from a 2D PNG image. Photos must",
" be blurred to be usable. Values from 0=off to 50=high are suggested.",
" Higher values incur more processing time.",
" </p>",
" <div class='f-row t-right'><table>",
" <tr><th>blur value</th><td><input id='png-blur' value='0' size='3'></td>",
" <th>&nbsp;invert image</th><td><input id='png-inv' type='checkbox'></td></tr>",
" <tr><th>base size</th><td><input id='png-base' value='0' size='3'></td>",
" <th>&nbsp;invert alpha</th><td><input id='alpha-inv' type='checkbox'></td></tr>",
" <tr><th>border size</th><td><input id='png-border' value='0' size='3'></td>",
" <th></th><td></td></tr>",
" </table></div>",
"</div>"
]};
api.uc.confirm(undefined, {convert:true, cancel:false}, undefined, opt).then((ok) => {
if (ok) {
loadImage(image, {
file: name,
blur: parseInt($('png-blur').value) || 0,
base: parseInt($('png-base').value) || 0,
border: parseInt($('png-border').value) || 0,
inv_image: $('png-inv').checked,
inv_alpha: $('alpha-inv').checked
});
}
});
}
function loadImage(image, opt = {}) {
const info = Object.assign({settings: settings.get(), png:image}, opt);
api.client.image2mesh(info, progress => {
api.show.progress(progress, "converting");
}, vertices => {
api.show.progress(0);
const widget = newWidget().loadVertices(vertices);
widget.meta.file = opt.file;
platform.add(widget);
});
}
/** ******************************************************************
* Selection Functions
******************************************************************* */
function groupMerge() {
Widget.Groups.merge(selection.widgets(true));
}
function groupSplit() {
Widget.Groups.split(selection.widgets(false));
}
/** ******************************************************************
* Settings Functions
******************************************************************* */
// convert any image type to png
function loadImageConvert(res, name) {
let url = URL.createObjectURL(new Blob([res]));
$('mod-any').innerHTML = `<img id="xsrc" src="${url}"><canvas id="xdst"></canvas>`;
let img = $('xsrc');
let can = $('xdst');
img.onload = () => {
can.width = img.width;
can.height = img.height;
let ctx = can.getContext('2d');
ctx.drawImage(img, 0, 0);
fetch(can.toDataURL()).then(r => r.arrayBuffer()).then(data => {
loadImageDialog(data, name);
});
};
}
function loadFile(ev) {
// use modern Filesystem api when available
if (false && window.showOpenFilePicker) {
window.showOpenFilePicker().then(files => {
return Promise.all(files.map(fh => fh.getFile()))
}).then(files => {
if (files.length) {
api.platform.load_files(files);
}
}).catch(e => { /* ignore cancel */ });
return;
}
api.ui.load.click();
}
function saveWorkspace(quiet) {
api.conf.save();
const newWidgets = [];
const oldWidgets = js2o(SDB.getItem('ws-widgets'), []);
api.widgets.each(function(widget) {
if (widget.synth) return;
newWidgets.push(widget.id);
oldWidgets.remove(widget.id);
widget.saveState();
let ann = api.widgets.annotate(widget.id);
ann.file = widget.meta.file;
ann.url = widget.meta.url;
});
SDB.setItem('ws-widgets', o2js(newWidgets));
oldWidgets.forEach(wid => {
Widget.deleteFromState(wid);
});
// eliminate dangling saved widgets
FILES.deleteFilter(key => newWidgets.indexOf(key.substring(8)) < 0, "ws-save-", "ws-savf");
if (!quiet) {
api.show.alert("workspace saved", 1);
}
}
function restoreWorkspace(ondone, skip_widget_load) {
let newset = api.conf.restore(false),
camera = newset.controller.view,
toload = ls2o('ws-widgets',[]),
loaded = 0,
position = true;
api.conf.update_fields();
platform.update_size();
SPACE.view.reset();
if (camera) {
SPACE.view.load(camera);
} else {
SPACE.view.home();
}
if (skip_widget_load) {
if (ondone) {
ondone();
}
return;
}
// remove any widgets from platform
api.widgets.each(function(widget) {
platform.delete(widget);
});
// load any widget by name that was saved to the workspace
toload.forEach(function(widgetid) {
Widget.loadFromState(widgetid, function(widget) {
if (widget) {
platform.add(widget, 0, position, true);
let ann = api.widgets.annotate(widgetid);
widget.meta.file = ann.file;
widget.meta.url = ann.url;
}
if (++loaded === toload.length) {
platform.deselect();
if (ondone) {
ondone();
setTimeout(() => {
platform.update_bounds();
SPACE.update();
}, 1);
}
}
}, position);
});
return toload.length > 0;
}
function clearWorkspace() {
// free up worker cache/mem
api.client.clear();
platform.select_all();
platform.delete(selection.meshes());
}
function modalShowing() {
return api.ui.modal.style.display === 'flex';
}
function showModal(which) {
let mod = api.ui.modal,
style = mod.style,
visible = modalShowing(),
info = { pct: 0 };
// hide all modals befroe showing another
Object.keys(api.ui.modals).forEach(name => {
api.ui.modals[name].style.display = name === which ? 'flex' : '';
});
function ondone() {
api.event.emit('modal.show', which);
}
if (visible) {
return ondone();
}
style.height = '0';
style.display = 'flex';
new TWEEN.Tween(info).
easing(TWEEN.Easing.Quadratic.InOut).
to({ pct: 100 }, 100).
onUpdate(() => { style.height = `${info.pct}%` }).
onComplete(ondone).
start();
}
function hideModal() {
if (!modalShowing()) {
return;
}
let mod = api.ui.modal, style = mod.style, info={pct:100};
new TWEEN.Tween(info).
easing(TWEEN.Easing.Quadratic.InOut).
to({pct:0}, 100).
onUpdate(() => { style.height = `${info.pct}%` }).
onComplete(() => {
style.display = '';
api.event.emit('modal.hide');
}).
start();
}
function showCatalog() {
showModal("files");
}
function editSettings(e) {
let current = settings.get(),
mode = getMode(),
name = e.target.getAttribute("name"),
load = current.sproc[mode][name],
loadstr = JSON.stringify(load,null,4).split('\n');
api.uc.prompt(`settings for "${name}"`, loadstr).then(edit => {
if (edit) {
try {
current.sproc[mode][name] = JSON.parse(edit);
if (name === settings.proc().processName) {
api.conf.load(null, name);
}
api.conf.save();
api.settings.sync.put();
} catch (e) {
console.log({ malformed_settings: e });
api.uc.alert('malformed settings object');
}
}
});
}
function exportSettings(e) {
let current = settings.get(),
mode = getMode(),
name = e.target.getAttribute("name"),
data = api.util.b64enc({
process: current.sproc[mode][name],
version,
moto: moto.id,
time: Date.now(),
mode,
name
});
api.uc.prompt("Export Process Filename", name).then(name => {
if (name) {
api.util.download(data, `${name}.km`);
}
});
}
function deleteSettings(e) {
let current = settings.get();
let name = e.target.getAttribute("del");
delete current.sproc[getMode()][name];
api.settings.sync.put();
updateProcessList();
api.conf.save();
triggerSettingsEvent();
}
function updateProcessList() {
let current = settings.get();
let list = [], s = current, sp = s.sproc[getMode()] || {}, table = api.ui.settingsList;
table.innerHTML = '';
for (let k in sp) {
if (sp.hasOwnProperty(k)) list.push(k);
}
list.filter(n => n !=='default').sort().forEach(function(sk) {
let row = DOC.createElement('div'),
load = DOC.createElement('button'),
edit = DOC.createElement('button'),
xprt = DOC.createElement('button'),
del = DOC.createElement('button'),
name = sk;
load.setAttribute('load', sk);
load.onclick = (ev) => {
api.conf.load(undefined, sk);
updateProcessList();
hideModal();
}
load.appendChild(DOC.createTextNode(sk));
if (sk == settings.proc().processName) {
load.setAttribute('class', 'selected')
}
api.ui.settingsName.value = settings.proc().processName;
del.setAttribute('del', sk);
del.setAttribute('title', "remove '"+sk+"'");
del.innerHTML = '<i class="far fa-trash-alt"></i>';
del.onclick = deleteSettings;
edit.setAttribute('name', sk);
edit.setAttribute('title', 'edit');
edit.innerHTML = '<i class="far fa-edit"></i>';
edit.onclick = editSettings;
xprt.setAttribute('name', sk);
xprt.setAttribute('title', 'export');
xprt.innerHTML = '<i class="fas fa-download"></i>';
xprt.onclick = exportSettings;
row.setAttribute("class", "flow-row");
row.appendChild(edit);
row.appendChild(load);
row.appendChild(xprt);
row.appendChild(del);
table.appendChild(row);
});
}
function showHelp() {
showHelpFile(`local`,() => {});
}
function showHelpFile(local,then) {
if (!local) {
WIN.open("//docs.grid.space/", "_help");
return;
}
const LANG = api.language.current;
$('kiri-version').innerHTML = `${LANG.version} ${version}`;
showModal('help');
api.event.emit('help.show', local);
}
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}/`);
};
});
});
}
function setViewMode(mode) {
const isCAM = settings.mode() === 'CAM';
viewMode = mode;
platform.deselect();
selection.update_info();
// clear any bounds forced by, for example, WireEDM previews
api.platform.set_bounds();
// disable clear in non-arrange modes
['view-arrange','act-slice','act-preview','act-animate'].forEach(el => {
$(el).classList.remove('selected')
});
$('render-tools').classList.add('hide');
switch (mode) {
case VIEWS.ARRANGE:
$('view-arrange').classList.add('selected');
$('render-tools').classList.remove('hide');
api.function.clear_progress();
api.client.clear();
STACKS.clear();
hideSlider();
updateSpeeds();
setVisibleLayer();
setWidgetVisibility(true);
api.widgets.opacity(1);
api.view.edges(api.local.getBoolean('model.edges'));
break;
case VIEWS.SLICE:
$('act-slice').classList.add('selected');
updateSpeeds();
updateSliderMax();
setWidgetVisibility(true);
!isCAM && api.view.edges(false);
break;
case VIEWS.PREVIEW:
$('act-preview').classList.add('selected');
setWidgetVisibility(true);
!isCAM && api.view.edges(false);
break;
case VIEWS.ANIMATE:
$('act-animate').classList.add('selected');
!isCAM && api.view.edges(false);
break;
default:
console.log("invalid view mode: "+mode);
return;
}
api.event.emit('view.set', mode);
DOC.activeElement.blur();
}
function getMode() {
return settings.mode();
}
function getModeLower() {
return getMode().toLowerCase();
}
function switchMode(mode) {
setMode(mode, null, platform.update_size);
}
function setMode(mode, lock, then) {
if (!MODES[mode]) {
console.log("invalid mode: "+mode);
mode = 'FDM';
}
const current = settings.get();
// change mode constants
current.mode = mode;
MODE = MODES[mode];
// gcode edit area for any non-SLA mode
api.uc.setVisible($('gcode-edit'), mode !== 'SLA');
// highlight selected mode menu item
["FDM","CAM","SLA","LASER","DRAG","WJET","WEDM"].forEach(sm => {
const cl = $(`mode-${sm.toLowerCase()}`).classList;
if (sm === mode) {
cl.add('selected');
} else {
cl.remove('selected');
}
});
// restore cached device profile for this mode
if (current.cdev[mode]) {
current.device = clone(current.cdev[mode]);
api.event.emit('device.select', api.device.get());
}
// hide/show
api.uc.setVisible($('set-tools'), mode === 'CAM');
// updates right-hand menu by enabling/disabling fields
setViewMode(VIEWS.ARRANGE);
api.uc.setMode(MODE);
// sanitize and persist settings
api.conf.load();
api.conf.save();
// other housekeeping
triggerSettingsEvent();
platform.update_selected();
selection.update_bounds(api.widgets.all());
api.conf.update_fields();
// because device dialog, if showing, needs to be updated
if (modalShowing()) {
api.show.devices();
}
api.space.restore(null, true);
api.event.emit("mode.set", mode);
if (then) {
then();
}
}
function currentProcessName() {
return settings.get().cproc[getMode()];
}
function currentProcessCode() {
return settings.get().sproc[getMode()][currentProcessName()];
}
function setControlsVisible(show) {
// TODO fix
// $('mid-left').style.display = show ? 'flex' : 'none';
// $('mid-right').style.display = show ? 'flex' : 'none';
}
function downloadBlob(data, filename) {
let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"}));
$('mod-any').innerHTML = `<a id="_dexport_" href="${url}" download="${filename}">x</a>`;
$('_dexport_').click();
}
// prevent safari from exiting full screen mode
DOC.onkeydown = function (evt) { if (evt.keyCode == 27) evt.preventDefault() }
export { FILES as catalog, LOCAL, SETUP, SECURE };

View file

@ -1,17 +1,6 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; export class Packer {
/**
* adapted from
* http://codeincomplete.com/posts/2011/5/7/bin_packing/
*/
gapp.register("kiri.pack", [], (root, exports) => {
const { kiri } = root;
class Packer {
constructor(w, h, spacing, opt = {}) { constructor(w, h, spacing, opt = {}) {
this.w = w; this.w = w;
this.h = h; this.h = h;
@ -150,7 +139,3 @@ class Packer {
return node; return node;
} }
} }
kiri.Pack = Packer;
});

View file

@ -1,26 +1,16 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { $, h } from '../../moto/webui.js';
import { api } from './api.js';
import { ajax, js2o } from './utils.js';
import { base } from '../../geo/base.js';
import { load } from '../../load/file.js';
import { space } from '../../moto/space.js';
import { Widget, newWidget } from './widget.js';
import { Packer } from './pack.js';
// dep: moto.space import { COLOR, MODES } from './consts.js';
// dep: moto.webui import { THREE } from '../../ext/three.js';
// dep: kiri.api
// dep: kiri.consts
// dep: kiri.utils
// dep: kiri.widget
// dep: load.stl
// dep: load.obj
// dep: load.3mf
// dep: load.gbr
// use: kiri-mode.cam.tool
gapp.register("kiri.platform", [], (root, exports) => {
const { base, kiri, moto, load } = root;
const { api, consts, driver, utils, newWidget, Widget } = kiri;
const { ajax, js2o } = utils;
const { space } = moto;
const { util } = base;
const { COLOR, MODES } = consts;
const V0 = new THREE.Vector3(0,0,0); const V0 = new THREE.Vector3(0,0,0);
@ -525,7 +515,7 @@ function platformDelete(widget, defer) {
api.event.emit('widget.delete', widget); api.event.emit('widget.delete', widget);
return; return;
} }
kiri.client.clear(widget); api.client.clear(widget);
api.widgets.remove(widget); api.widgets.remove(widget);
api.selection.remove(widget); api.selection.remove(widget);
Widget.Groups.remove(widget); Widget.Groups.remove(widget);
@ -719,12 +709,12 @@ function platformLayout() {
mp = [sz.x, sz.y], mp = [sz.x, sz.y],
ms = [mp[0] / 2, mp[1] / 2], ms = [mp[0] / 2, mp[1] / 2],
c = Widget.Groups.blocks().sort(), c = Widget.Groups.blocks().sort(),
p = new kiri.Pack(ms[0], ms[1], gap).fit(c); p = new Packer(ms[0], ms[1], gap).fit(c);
while (!p.packed) { while (!p.packed) {
ms[0] *= 1.1; ms[0] *= 1.1;
ms[1] *= 1.1; ms[1] *= 1.1;
p = new kiri.Pack(ms[0], ms[1], gap).fit(c); p = new Packer(ms[0], ms[1], gap).fit(c);
} }
for (i = 0; i < c.length; i++) { for (i = 0; i < c.length; i++) {
@ -809,7 +799,7 @@ function platformLoadFiles(files, group) {
if (api.conf.get().controller.devel) { if (api.conf.get().controller.devel) {
api.event.emit('cam.parse.gerber', { data: text }); api.event.emit('cam.parse.gerber', { data: text });
} else { } else {
kiri.client.gerber2mesh(text, progress => { api.client.gerber2mesh(text, progress => {
api.show.progress(progress, "converting"); api.show.progress(progress, "converting");
}, vertices => { }, vertices => {
api.show.progress(0); api.show.progress(0);
@ -847,7 +837,7 @@ function platformLoadFiles(files, group) {
api.function.parse(data.textDecode('utf-8'), 'gcode'); api.function.parse(data.textDecode('utf-8'), 'gcode');
load_dec(); load_dec();
} else if (issvg) { } else if (issvg) {
loadSVGDialog(opt => { loadSVGDialog(opt => {
group = group || []; group = group || [];
let svg = load.SVG.parse(data.textDecode('utf-8'), opt); let svg = load.SVG.parse(data.textDecode('utf-8'), opt);
let ind = 0; let ind = 0;
@ -921,7 +911,7 @@ function fitDeviceToWidgets() {
} }
// extend API (api.platform) // extend API (api.platform)
const platform = Object.assign(api.platform, { export const platform = {
fit: fitDeviceToWidgets, fit: fitDeviceToWidgets,
add: platformAdd, add: platformAdd,
changed: platformChanged, changed: platformChanged,
@ -950,6 +940,4 @@ const platform = Object.assign(api.platform, {
show_volume: space.platform.showVolume, show_volume: space.platform.showVolume,
top_z() { return topZ }, top_z() { return topZ },
clear() { api.space.clear(); api.space.save(true) } clear() { api.space.clear(); api.space.save(true) }
}); };
});

View file

@ -1,19 +1,13 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { arcToPath } from '../../geo/paths.js';
import { consts } from './consts.js';
import { newPoint } from '../../geo/point.js';
import { util } from '../../geo/base.js';
// dep: geo.base
// dep: geo.point
// dep: geo.polygon
// dep: geo.paths
// dep: kiri.consts
gapp.register("kiri.print", [], (root, evets) => {
const { base, kiri } = self;
const { paths, util, newPoint } = base;
const { arcToPath } = paths;
const { numOrDefault } = util; const { numOrDefault } = util;
const { beltfact } = kiri.consts; const { beltfact } = consts;
const XAXIS = new THREE.Vector3(1,0,0); const XAXIS = new THREE.Vector3(1,0,0);
const DEG2RAD = Math.PI / 180; const DEG2RAD = Math.PI / 180;
@ -463,10 +457,10 @@ class Print {
let arcPoints = arcToPath( prevPoint, point, 64,{ clockwise:g2,center}) ?? [] let arcPoints = arcToPath( prevPoint, point, 64,{ clockwise:g2,center}) ?? []
let emit = g2 ? 2 : 3; let emit = g2 ? 2 : 3;
// console.log("clone point",structuredClone({point,prevPoint,center,arcPoints,emit})); // console.log("clone point",structuredClone({point,prevPoint,center,arcPoints,emit}));
// console.log("pointer point",{point,prevPoint,center,arcPoints,emit}); // console.log("pointer point",{point,prevPoint,center,arcPoints,emit});
outputPoint(point,prevPoint,emit,{center,arcPoints}); outputPoint(point,prevPoint,emit,{center,arcPoints});
// scope.addOutput(seq, point, emit, pos.F, tool,{center,arcPoints}); // scope.addOutput(seq, point, emit, pos.F, tool,{center,arcPoints});
} }
@ -619,10 +613,15 @@ class Print {
scope.belt = belt; scope.belt = belt;
if (scope.debugE) { if (scope.debugE) {
console.log({ bounds, print_time: time.round(2) }); console.log({
bounds,
minf,
maxf,
print_time: time.round(2),
output: scope.output
});
} }
console.log(scope.output)
done({ output: scope.output }); done({ output: scope.output });
} }
} }
@ -646,8 +645,8 @@ class Output {
const { type, center, arcPoints } = (options ?? {}); const { type, center, arcPoints } = (options ?? {});
//speed, tool, type, center, arcPoints //speed, tool, type, center, arcPoints
this.point = point; // point to emit this.point = point;
this.emit = emit; // emit (feed for printers, power for lasers, cut for cam) this.emit = Number(emit); //convert bools into 0/1
this.speed = speed; this.speed = speed;
this.tool = tool; this.tool = tool;
this.type = type; this.type = type;
@ -680,9 +679,7 @@ function newPrint(settings, widgets, id) {
return new Print(settings, widgets, id); return new Print(settings, widgets, id);
}; };
gapp.overlay(kiri, { export {
Print, Print,
newPrint newPrint
}); };
});

View file

@ -1,35 +1,24 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { Layers } from './layers.js';
import { newPoint } from '../../geo/point.js';
import { newPolygon } from '../../geo/polygon.js';
import { newSlopeFromAngle } from '../../geo/slope.js';
/*
* uses `layers.js` to convert paths (usually preview) into primordial geometries.
* the input is based on an array (layers) of arrays containing `Output` objects
*/
// dep: geo.base
// dep: geo.point
// dep: geo.polygon
gapp.register("kiri.render", [], (root, exports) => {
const { base, kiri } = root;
const { config, util, newPolygon } = base;
const hsV = 0.9; const hsV = 0.9;
const XAXIS = new THREE.Vector3(1,0,0); const XAXIS = new THREE.Vector3(1,0,0);
const DEG2RAD = Math.PI / 180; const DEG2RAD = Math.PI / 180;
exports({ function worker() {
path, return self.kiri_worker.current;
is_dark, }
rate_to_color
});
function is_cam() { function is_cam() {
return root.worker.print.settings.mode === 'CAM'; return worker().print.settings.mode === 'CAM';
} }
function is_dark() { function is_dark() {
return root.worker.print.settings.controller.dark ? true : false; return worker().print.settings.controller.dark ? true : false;
}; };
function rate_to_color(rate, max) { function rate_to_color(rate, max) {
@ -44,17 +33,17 @@ function rate_to_color(rate, max) {
} }
}; };
/** /**
* Generate visual representation of a gcode program. * Generate visual representation of a gcode program.
* @param {Output[][]} levels - Array of arrays containing `Output` objects. * @param {Output[][]} levels - Array of arrays containing `Output` objects.
* @param {function} update - Called with a completion percentage and the rendered layer. * @param {function} update - Called with a completion percentage and the rendered layer.
* @param {{tools: Object, flat: Boolean, thin: Boolean, speed: Boolean, lineWidth: Number, toolMode: Boolean, z: Number, action: String, other: String}} [opts] - Optional parameters. * @param {{tools: Object, flat: Boolean, thin: Boolean, speed: Boolean, lineWidth: Number, toolMode: Boolean, z: Number, action: String, other: String}} [opts] - Optional parameters.
* @returns {Promise.kiri.Layers[]} - Array of layers. * @returns {Promise.kiri.Layers[]} - Array of layers.
*/ */
async function path(levels, update, opts = {}) { async function path(levels, update, opts = {}) {
levels = levels.filter(level => level.length); levels = levels.filter(level => level.length);
if (levels.length === 0) { if (levels.length === 0) {
self.worker.print.maxSpeed = 0; worker().print.maxSpeed = 0;
return []; return [];
} }
@ -107,10 +96,11 @@ async function path(levels, update, opts = {}) {
// }).reduce((a, v) => Math.max(a, v)) + 1; // }).reduce((a, v) => Math.max(a, v)) + 1;
// for reporting // for reporting
self.worker.print.minSpeed = minspd; let print = worker().print;
self.worker.print.maxSpeed = maxspd; print.minSpeed = minspd;
self.worker.print.thinColor = thin; print.maxSpeed = maxspd;
self.worker.print.flatColor = flat; print.thinColor = thin;
print.flatColor = flat;
let lastTool = null; let lastTool = null;
let lastEnd = null; let lastEnd = null;
@ -138,7 +128,8 @@ async function path(levels, update, opts = {}) {
const engages = []; const engages = [];
const lasers = []; const lasers = [];
const sparks = []; const sparks = [];
const output = new kiri.Layers(); const output = new Layers();
layers.push(output); layers.push(output);
const pushPrint = (toolid, poly) => { const pushPrint = (toolid, poly) => {
@ -172,8 +163,8 @@ async function path(levels, update, opts = {}) {
let p2 = new THREE.Vector3(point.x, point.y, point.z) let p2 = new THREE.Vector3(point.x, point.y, point.z)
.applyAxisAngle(XAXIS, point.a * DEG2RAD); .applyAxisAngle(XAXIS, point.a * DEG2RAD);
// reconstruct point point for display without A axis // reconstruct point point for display without A axis
outPoint = base.newPoint(p2.x, p2.y, p2.z); outPoint = newPoint(p2.x, p2.y, p2.z);
// let sp = base.newPoint(outPoint.x * 1.1, outPoint.y * 1.1, outPoint.z * 1.1); // let sp = newPoint(outPoint.x * 1.1, outPoint.y * 1.1, outPoint.z * 1.1);
// sparks.push(outPoint, sp); // sparks.push(outPoint, sp);
} }
if (out.tool !== lastTool) { if (out.tool !== lastTool) {
@ -321,8 +312,8 @@ async function path(levels, update, opts = {}) {
.addAreas(heads.map(points => { .addAreas(heads.map(points => {
const {p1, p2} = points; const {p1, p2} = points;
const slope = p2.slopeTo(p1); const slope = p2.slopeTo(p1);
const s1 = base.newSlopeFromAngle(slope.angle + 20); const s1 = newSlopeFromAngle(slope.angle + 20);
const s2 = base.newSlopeFromAngle(slope.angle - 20); const s2 = newSlopeFromAngle(slope.angle - 20);
const p3 = points.p2.projectOnSlope(s1, arrowSize); const p3 = points.p2.projectOnSlope(s1, arrowSize);
const p4 = points.p2.projectOnSlope(s2, arrowSize); const p4 = points.p2.projectOnSlope(s2, arrowSize);
return newPolygon().addPoints([p2,p3,p4]).setZ(p2.z + 0.01); return newPolygon().addPoints([p2,p3,p4]).setZ(p2.z + 0.01);
@ -543,4 +534,14 @@ function color4(rgb, inc, seg) {
} }
} }
}); export const render = {
path,
is_dark,
rate_to_color
};
export {
path,
is_dark,
rate_to_color
};

View file

@ -1,16 +1,9 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { api } from './api.js';
import { space } from '../../moto/space.js';
// dep: moto.space import { THREE } from '../../ext/three.js';
// dep: kiri.api import { tool as MeshTool } from '../../mesh/tool.js';
// dep: kiri.consts
// dep: kiri.utils
gapp.register("kiri.selection", (root, exports) => {
const { kiri, moto, noop } = self;
const { api, consts, utils } = kiri;
const { space } = moto;
const selectedMeshes = []; const selectedMeshes = [];
@ -214,6 +207,39 @@ function merge() {
api.platform.select(w); api.platform.select(w);
} }
/**
* Isolates the bodies within the selected widgets. If no widgets are selected,
* it processes all available widgets. For each widget, it checks if a tool is
* associated; if not, it creates a new MeshTool and indexes the vertices.
* The function identifies isolated bodies within each widget. If a widget has
* a single body, it remains selected. Otherwise, it creates new widgets for
* each isolated body and selects them while deleting the original widget.
*/
function isolateBodies(){
let sel = widgets();
console.log('isolate', sel);
if (sel.length === 0) {
sel = api.widgets.all();
}
for(let widget of sel){
if (!widget.tool) {
let tool = widget.tool = new MeshTool();
tool.index(widget.getGeoVertices({ unroll: true, translate:true }));
}
let bodies = widget.tool.isolateBodies()
if(bodies.length ==1){
api.platform.select(widget);
}else{
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)
}
api.platform.delete(widget);
}
}
}
function exportWidgets(format = "stl") { function exportWidgets(format = "stl") {
let sel = widgets(); let sel = widgets();
if (sel.length === 0) { if (sel.length === 0) {
@ -296,9 +322,10 @@ function setDisabled(bool) {
} }
// extend API (api.selection) // extend API (api.selection)
const selection = Object.assign(api.selection, { export const selection = {
move, move,
merge, merge,
isolateBodies,
scale, scale,
rotate, rotate,
mirror, mirror,
@ -320,6 +347,4 @@ const selection = Object.assign(api.selection, {
disable() { setDisabled(true) }, disable() { setDisabled(true) },
opacity() { api.widgets.opacity(...arguments) }, opacity() { api.widgets.opacity(...arguments) },
meshes() { return selectedMeshes.slice() }, meshes() { return selectedMeshes.slice() },
}); };
});

View file

@ -1,23 +1,20 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { $, h } from '../../moto/webui.js';
import { api } from './api.js';
import { conf } from './conf.js';
import { codec } from './codec.js';
import { consts } from './consts.js';
import { space } from '../../moto/space.js';
import { local } from '../../data/local.js';
import { utils } from './utils.js';
import { version } from '../../moto/license.js';
import { JSZip } from '../../ext/jszip-esm.js';
// dep: kiri.api
// dep: kiri.conf
// dep: kiri.utils
// dep: moto.space
// dep: data.local
// use: kiri.widgets
// use: ext.base64
gapp.register("kiri.settings", (root, exports) => {
const { data, kiri, moto, noop } = self;
const { api, conf, consts, utils } = kiri;
const { space } = moto;
const { local } = data;
const { clone } = Object;
const { areEqual, ls2o, js2o } = utils; const { areEqual, ls2o, js2o } = utils;
const { COLOR } = consts; const { COLOR } = consts;
const { clone } = Object;
const localFilterKey ='kiri-gcode-filters'; const localFilterKey ='kiri-gcode-filters';
const localFilters = js2o(local.getItem(localFilterKey)) || []; const localFilters = js2o(local.getItem(localFilterKey)) || [];
@ -225,7 +222,7 @@ function updateSettingsFromFields(setrec, uirec = api.ui, changes) {
} }
// given a settings region, update values of matching bound UI fields // given a settings region, update values of matching bound UI fields
function updateFieldsFromSettings(setrec, uirec = api.ui, trace) { function updateFieldsFromSettings(setrec, uirec = api.ui, opt = {}) {
if (!setrec) { if (!setrec) {
updateFieldsFromSettings(settings.device); updateFieldsFromSettings(settings.device);
updateFieldsFromSettings(settings.process); updateFieldsFromSettings(settings.process);
@ -233,6 +230,9 @@ function updateFieldsFromSettings(setrec, uirec = api.ui, trace) {
updateExtruderFields(settings.device); updateExtruderFields(settings.device);
return; return;
} }
if (opt.trace) {
console.log({ setrec, uirec });
}
for (let key in setrec) { for (let key in setrec) {
if (!setrec.hasOwnProperty(key)) { if (!setrec.hasOwnProperty(key)) {
continue; continue;
@ -242,6 +242,9 @@ function updateFieldsFromSettings(setrec, uirec = api.ui, trace) {
continue; continue;
} }
let uie = uirec[key], typ = uie ? uie.type : null; let uie = uirec[key], typ = uie ? uie.type : null;
if (opt.trace) {
console.log({ key, uie, typ, val });
}
if (typ === 'text') { if (typ === 'text') {
if (uie.setv) { if (uie.setv) {
uie.setv(val); uie.setv(val);
@ -275,6 +278,8 @@ function updateFieldsFromSettings(setrec, uirec = api.ui, trace) {
} else { } else {
uie.value = ''; uie.value = '';
} }
} else if (opt.trace) {
console.log('skipped', { key, val, typ });
} }
} }
} }
@ -506,7 +511,7 @@ function settingsExport(opts = {}) {
const widgets = api.widgets.all(); const widgets = api.widgets.all();
const note = opts.node || undefined; const note = opts.node || undefined;
const shot = opts.work || opts.screen ? space.screenshot() : undefined; const shot = opts.work || opts.screen ? space.screenshot() : undefined;
const work = opts.work ? kiri.codec.encode(widgets,{_json_:true}) : undefined; const work = opts.work ? codec.encode(widgets,{_json_:true}) : undefined;
const view = opts.work ? space.view.save() : undefined; const view = opts.work ? space.view.save() : undefined;
const setn = Object.clone(settings); const setn = Object.clone(settings);
// stuff in legacy annotations for re-import // stuff in legacy annotations for re-import
@ -515,7 +520,7 @@ function settingsExport(opts = {}) {
} }
const xprt = { const xprt = {
settings: setn, settings: setn,
version: kiri.version, version: version,
screen: shot, screen: shot,
space: space.info, space: space.info,
note: note, note: note,
@ -600,7 +605,7 @@ function settingsImport(data, ask) {
work.type = 100; work.type = 100;
} }
} }
kiri.codec.decode(data.work).forEach(widget => { codec.decode(data.work).forEach(widget => {
api.platform.add(widget, 0, true, true); api.platform.add(widget, 0, true, true);
}); });
if (data.view) { if (data.view) {
@ -655,7 +660,7 @@ function settingsImportUrl(url, ask) {
function settingsImportZip(data, ask) { function settingsImportZip(data, ask) {
let alert = api.show.alert("Importing Workspace"); let alert = api.show.alert("Importing Workspace");
JSZip.loadAsync(data).then(zip => { new JSZip().loadAsync(data).then(zip => {
for (let [key,value] of Object.entries(zip.files)) { for (let [key,value] of Object.entries(zip.files)) {
if (key === "workspace.json") { if (key === "workspace.json") {
value.async("string").then(json => { value.async("string").then(json => {
@ -760,9 +765,14 @@ function setEnableWASM(bool) {
api.event.emit("set.assembly", bool); api.event.emit("set.assembly", bool);
} }
// extend API (api.conf) // merged api for backward compatibility
Object.assign(api.conf, { const apiSet = {
dbo: () => { return ls2o('ws-settings') }, dbo() { return ls2o('ws-settings') },
dev() { return settings.device },
proc() { return settings.process },
ctrl() { return settings.controller },
mode() { return settings.mode },
prof() { return settings.sproc[settings.mode] },
get: getSettings, get: getSettings,
put: putSettings, put: putSettings,
load: loadSettings, load: loadSettings,
@ -777,25 +787,26 @@ Object.assign(api.conf, {
restore: restoreSettings, restore: restoreSettings,
export: settingsExport, export: settingsExport,
import: settingsImport, import: settingsImport,
});
// extend API (api.settings)
Object.assign(api.settings, {
get: getSettings,
import: settingsImport,
import_zip: settingsImportZip, import_zip: settingsImportZip,
import_url: settingsImportUrl, import_url: settingsImportUrl,
import_prusa: settingsPrusaConvert, import_prusa: settingsPrusaConvert,
dev() { return settings.device },
proc() { return settings.process },
ctrl() { return settings.controller },
mode() { return settings.mode },
prof() { return settings.sproc[settings.mode] },
sync: { sync: {
async get() {}, async get() {},
async put() {}, async put() {},
status: false status: false
} }
}); };
}); // for backward compatibility
apiSet.conf = apiSet;
apiSet.set = apiSet;
apiSet.settings = apiSet;
export default apiSet;
// for backward compatibility
export {
apiSet as conf,
apiSet as set,
apiSet as settings
};

View file

@ -1,14 +1,8 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { Layers } from './layers.js';
import { newPoint } from '../../geo/point.js';
// dep: geo.base import { polygons as POLY } from '../../geo/polygons.js';
// dep: geo.polygons
gapp.register("kiri.slice", [], (root, exports) => {
const { base, kiri } = root;
const POLY = base.polygons;
let tracker; let tracker;
@ -41,7 +35,7 @@ class Slice {
*/ */
output() { output() {
if (this.layers) return this.layers; if (this.layers) return this.layers;
let layers = this.layers = new kiri.Layers(); let layers = this.layers = new Layers();
if (tracker) { if (tracker) {
layers.setRotation(-tracker.rotation || 0); layers.setRotation(-tracker.rotation || 0);
} }
@ -150,7 +144,7 @@ class Slice {
} else { } else {
// create top object from object bundle passed back by slicePost() // create top object from object bundle passed back by slicePost()
let top = new Top(data.poly); let top = new Top(data.poly);
top.thin_fill = data.thin_fill ? data.thin_fill.map(p => base.newPoint(p.x,p.y,p.z)) : undefined; top.thin_fill = data.thin_fill ? data.thin_fill.map(p => newPoint(p.x,p.y,p.z)) : undefined;
top.fill_lines = data.fill_lines; top.fill_lines = data.fill_lines;
top.fill_sparse = data.fill_sparse; top.fill_sparse = data.fill_sparse;
top.fill_off = data.fill_off; top.fill_off = data.fill_off;
@ -251,12 +245,10 @@ function newSlice(z, view) {
return new Slice(z, view); return new Slice(z, view);
} }
gapp.overlay(kiri, { export {
Top, Top,
Slice, Slice,
newTop, newTop,
newSlice, newSlice,
setSliceTracker setSliceTracker
}); };
});

View file

@ -1,15 +1,9 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
gapp.register("kiri.stack", [], (root, exports) => {
const { kiri } = root;
/* /*
* converts `layers.js` output data structures into three.js meshes for display * converts `layers.js` output data structures into three.js meshes for display
*/ */
class Stack { export class Stack {
constructor(view, freeMem, shiny) { constructor(view, freeMem, shiny) {
this._view = view; this._view = view;
this.view = view.newGroup(); this.view = view.newGroup();
@ -245,8 +239,6 @@ class Stack {
} }
} }
kiri.Stack = Stack;
let shininess = 15, let shininess = 15,
specular = 0x444444, specular = 0x444444,
emissive = 0x101010, emissive = 0x101010,
@ -301,5 +293,3 @@ function createLambertMaterial(color, flat) {
side: flat ? THREE.DoubleSide : THREE.FrontSide side: flat ? THREE.DoubleSide : THREE.FrontSide
}); });
} }
});

View file

@ -1,13 +1,8 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { $ } from '../../moto/webui.js';
import { api } from './api.js';
// dep: kiri.stack import { Stack } from './stack.js';
// use: kiri.api
// use: kiri.ui
gapp.register("kiri.stacks", [], (root, exports) => {
const { kiri } = root;
let freeMem = true, let freeMem = true,
stacks = {}, stacks = {},
@ -18,7 +13,7 @@ let freeMem = true,
function init() { function init() {
labels = $("layers"); labels = $("layers");
API = kiri.api, API = api,
UC = API.uc, UC = API.uc,
UI = API.ui; UI = API.ui;
} }
@ -80,7 +75,7 @@ function create(name, view) {
} }
const stack = stacks[name] = { const stack = stacks[name] = {
layers: [ ], layers: [ ],
obj: new kiri.Stack(view, freeMem, kiri.api.conf.get().controller.shiny), obj: new Stack(view, freeMem, api.conf.get().controller.shiny),
add: function(layers) { add: function(layers) {
let lmap = layers.layers; let lmap = layers.layers;
let map = stack.obj.addLayers(layers); let map = stack.obj.addLayers(layers);
@ -93,9 +88,9 @@ function create(name, view) {
mat.visible = ctrl.toggle.checked; mat.visible = ctrl.toggle.checked;
}); });
if (ctrl.toggle.checked) { if (ctrl.toggle.checked) {
kiri.api.event.emit("stack.show", label); api.event.emit("stack.show", label);
} else { } else {
kiri.api.event.emit("stack.hide", label) api.event.emit("stack.hide", label)
} }
}) })
}; };
@ -155,7 +150,7 @@ function setFraction(frac) {
}); });
} }
kiri.stacks = { export default {
clear, clear,
create, create,
rotate, rotate,
@ -170,4 +165,17 @@ kiri.stacks = {
setFreeMem setFreeMem
}; };
}); export {
clear,
create,
rotate,
remove,
getStack,
getStacks,
getRange,
setRange,
getLabels,
setVisible,
setFraction,
setFreeMem
};

View file

@ -1,20 +1,12 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { local } from '../../data/local.js';
import { broker } from '../../moto/broker.js';
// dep: moto.broker import { js2o, o2js } from './utils.js';
// dep: kiri.api
// dep: kiri.utils
gapp.register("kiri.stats", [], (root, exports) => {
const { data, kiri, gapp } = root;
const { broker } = gapp;
const { api, utils } = kiri;
const { js2o, o2js } = utils;
class Stats { class Stats {
constructor(db) { constructor(db) {
this.db = db || data.local; this.db = db || local;
this.obj = js2o(this.db['stats'] || '{}'); this.obj = js2o(this.db['stats'] || '{}');
let o = this.obj, k; let o = this.obj, k;
for (k in o) { for (k in o) {
@ -56,6 +48,6 @@ class Stats {
} }
} }
kiri.stats = new Stats(); export const stats = new Stats();
}); export { stats as Stats };

View file

@ -1,12 +1,10 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
// dep: add.three import { api } from './api.js';
// dep: kiri.api import { THREE } from '../../ext/three.js';
gapp.register("kiri.tools", (root, exports) => {
const { Vector3, Quaternion } = THREE; const { Vector3, Quaternion } = THREE;
const { kiri, moto } = root; const { kiri, moto } = root;
const { api } = kiri;
const XAXIS = new THREE.Vector3(1,0,0); const XAXIS = new THREE.Vector3(1,0,0);
const ZAXIS = new THREE.Vector3(0,0,1); const ZAXIS = new THREE.Vector3(0,0,1);
@ -164,4 +162,5 @@ api.event.on('mouse.hover.up', (ev) => {
endIt(); endIt();
}); });
});
export { onLayFlatSelect, onFaceUpSelect, startIt, endIt, cleanup, scale, onDone };

865
src/kiri/core/ui.js Normal file
View file

@ -0,0 +1,865 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { $ } from '../../moto/webui.js';
import { api } from './api.js';
let DOC = self.document,
inputAction = null,
lastAddTo = null,
lastGroup = null,
lastDiv = null,
addTo = null,
bindTo = null,
groups = {},
groupSticky = false,
groupName = undefined,
heads = {}, // hideable group heads (clickable label)
hidden = {}, // hidden groups (by name)
hasModes = [],
setters = [],
lastMode = null,
lastExpert = true,
prefix = "tab",
units = 1,
lastChange = null,
lastBtn = null,
lastTxt = null,
lastPop = null;
export const UI = {
prefix: function(pre) { prefix = pre; return UI },
inputAction: function(fn) { inputAction = fn; return UI },
lastChange: function() { return lastChange },
checkpoint,
restore,
refresh,
setHidden,
setMode,
bound,
toInt,
toFloat,
toDegsFloat,
isSticky,
setSticky,
newBoolean,
newButton,
newBlank,
newDiv,
newElement,
newExpand,
endExpand,
newGCode,
newGroup,
newLabel,
newInput,
newValue,
newRange,
newRow,
newSelect,
newText,
setGroup,
addUnits,
setUnits,
confirm,
prompt,
alert,
onBlur,
setEnabled(el, bool) {
if (bool) {
el.removeAttribute('disabled');
} else {
el.setAttribute('disabled','');
}
},
setVisible(el, bool) {
UI.setClass(el, 'hide', !bool);
},
setClass(el, clazz, bool) {
if (bool) {
el.classList.add(clazz);
} else {
el.classList.remove(clazz);
}
}
};
function setHidden(map) {
hidden = map;
refresh();
for (let ctrl of Object.values(heads)) {
ctrl.update();
}
}
function onBlur(obj, fn) {
if (Array.isArray(obj)) {
for (let o of obj) onBlur(o, fn);
return;
}
obj.addEventListener('blur', fn);
}
function alert(message) {
return confirm(message, {ok:true});
}
function prompt(message, value) {
return confirm(message, {ok:true, cancel:undefined}, value);
}
function confirm(message, buttons, input, opt = {}) {
return new Promise((resolve, reject) => {
let { feature } = api;
let onkey_save = feature.on_key;
feature.on_key = key => {
// console.log({ eat_key: key });
return true;
};
let dialog = $('dialog');
let btns = buttons || {
"yes": true,
"no": false
};
let rnd = Date.now().toString(36);
let html = [
`<div class="confirm f-col a-stretch" style="padding:5px !important">`
];
if (message) {
html.push(`<label style="user-select:text">${message}</label>`);
}
if (opt.pre) {
html = opt.pre.appendAll(html);
}
let iid;
if (Array.isArray(input)) {
iid = `confirm-input-${rnd}`;
html.append(`<div><textarea rows="15" cols="40" class="grow" type="text" spellcheck="false" id="${iid}"></textarea></div>`);
} else if (input !== undefined) {
iid = `confirm-input-${rnd}`;
html.append(`<div><input class="grow" type="text" spellcheck="false" id="${iid}"/></div>`);
}
html.append(`<div class="f-row j-end">`);
Object.entries(btns).forEach((row,i) => {
html.append(`<button id="confirm-${i}-${rnd}">${row[0]}</button>`);
});
html.append(`</div></div>`);
if (opt.post) {
html.appendAll(opt.post);
}
dialog.innerHTML = html.join('');
function done(value) {
dialog.close();
feature.on_key = onkey_save;
if (value !== undefined) {
setTimeout(() => { resolve(value) }, 150);
}
}
if (iid) {
let array = Array.isArray(input);
iid = $(iid);
iid.value = array ? input.join('\n') : input;
if (!array)
iid.onkeypress = (ev) => {
if (ev.key === 'Enter' || ev.charCode === 13) {
done(ev.target.value);
}
};
}
Object.entries(btns).forEach((row,i) => {
$(`confirm-${i}-${rnd}`).onclick = (ev) => {
let value = iid && row[1] ? iid.value : row[1];
ev.preventDefault();
ev.stopPropagation();
done(value);
}
});
setTimeout(() => {
dialog.showModal();
if (iid) {
iid.focus();
iid.selectionStart = 0;
iid.selectionEnd = iid.value.length;
}
}, 150);
});
}
function refresh() {
setMode(lastMode);
setters.forEach(input => {
if (input.setv) {
input.setv(input.real);
}
});
}
function setMode(mode) {
lastMode = mode;
hasModes.forEach(div => div.setMode(mode));
}
function checkpoint(at) {
return { addTo: at || addTo, lastDiv: at || lastDiv, lastGroup, groupName };
}
function restore(opt = {}) {
addTo = opt.addTo || addTo;
bindTo = opt.bindTo || null;
lastDiv = opt.lastDiv || lastDiv;
lastGroup = opt.lastGroup || lastGroup;
groupName = opt.groupName || groupName;
}
// at present only used by the layers popup menu
function setGroup(div) {
addTo = lastDiv = div;
groupName = undefined;
lastGroup = [];
return div;
}
function newElement(type, opt = {}) {
let el = DOC.createElement(type);
if (opt.id) {
el.setAttribute("id", opt.id);
}
if (opt.class) {
for (let cl of opt.class.split(' ')) {
el.classList.add(cl);
}
}
if (opt.attr) {
for (let [key, val] of Object.entries(opt.attr)) {
el.setAttribute(key, val);
}
}
return el;
}
function newGroup(label, div, opt = {}) {
lastDiv = div = (div || lastDiv);
let group = opt.group || label,
row = DOC.createElement('div'),
dbkey = `beta-${prefix}-show-${group}`,
link;
if (opt.class) {
opt.class.split(' ').forEach(ce => {
row.classList.add(ce);
});
} else {
row.setAttribute("class", "set-header");
}
if (div && opt.driven) {
addModeControls(div, opt);
}
addTo = lastDiv;
div.appendChild(row);
if (label) {
link = DOC.createElement('a');
link.appendChild(DOC.createTextNode(label));
row.appendChild(link);
}
addModeControls(row, opt);
lastGroup = groups[group] = [];
lastGroup.key = dbkey;
groupName = group;
if (opt.hideable) {
let pad = DOC.createElement('i');
let arr = DOC.createElement('span');
pad.setAttribute('class','grow');
row.appendChild(pad);
row.appendChild(arr);
const ctrl = heads[group] = {
row,
arr,
update() {
if (!hidden[group]) {
arr.innerHTML = '<i class="fa-solid fa-caret-down"></i>';
row.classList.add('hidden');
} else {
arr.innerHTML = '<i class="fa-solid fa-caret-up"></i>';
row.classList.remove('hidden');
}
}
};
row.onclick = () => {
hidden[group] = !hidden[group];
refresh();
ctrl.update();
};
}
return row;
}
function addCollapsableElement(parent, options = {}) {
let row = newDiv(options);
if (parent) parent.appendChild(row);
if (lastGroup) lastGroup.push(row);
return row;
}
function bound(low,high) {
return function(v) {
if (isNaN(v)) return low;
return v < low ? low : v > high ? high : v;
};
}
function toInt() {
let nv = this.value !== '' ? parseInt(this.value) : null;
if (isNaN(nv)) nv = 0;
if (nv !== null && this.bound) nv = this.bound(nv);
this.value = nv;
if (this.setv) {
return this.real = Math.round(nv * units);
};
return nv;
}
function toFloat() {
let nv = this.value !== '' ? parseFloat(this.value) : null;
if (nv !== null && this.bound) nv = this.bound(nv);
if (this.setv) {
return this.setv(nv * units);
} else {
this.value = nv;
}
return nv;
}
function toDegsFloat(){
let nv = this.value !== '' ? parseFloat(this.value) : null;
if (nv !== null && this.bound) nv = this.bound(nv);
nv = (nv+360)% 360; // bound the val to 0-359.99
if (this.setv) {
return this.setv(nv);
} else {
this.value = nv;
}
return nv;
}
function raw() {
return this.value !== '' ? this.value : null;
}
function setUnits(v) {
if (v !== units) {
units = v;
refresh();
}
}
function newLabel(text, opt = {}) {
let label = DOC.createElement('label');
label.appendChild(DOC.createTextNode(text));
label.setAttribute("class", "noselect");
if (opt.class) opt.class.split(' ').forEach(cl => {
label.classList.add(cl);
})
return label;
}
function newValue(size = 6, opt = {}) {
let value = DOC.createElement('input');
value.setAttribute("size", size);
value.setAttribute("readonly", '');
value.setAttribute("class", "nooutline noselect");
if (opt.class) opt.class.split(' ').forEach(cl => {
value.classList.add(cl);
})
return value;
}
function addId(el, opt = {}) {
if (opt.id) {
el.setAttribute("id", opt.id);
}
}
function safecall(fn) {
try {
return fn();
} catch (e) {
// console.log({ safecall_error: e });
return false;
}
}
function addModeControls(el, opt = {}) {
el.__opt = opt;
el.showMe = function() {
if (opt.trace) console.log({ showMe: el });
el.classList.remove('hide');
};
el.hideMe = function() {
if (opt.trace) console.log({ hideMe: el });
el.classList.add('hide');
};
el.setVisible = function(show) {
if (opt.trace) console.log({ setVisible: show });
if (show) el.showMe();
else el.hideMe();
};
el.setMode = function(mode) {
let hidn = hidden[el._group] === true;
let xprt = opt.expert === undefined || (opt.expert === lastExpert);
let show = opt.show ? safecall(opt.show) : true;
let disp = opt.visible ? opt.visible() : true;
let hmod = el.hasMode(mode);
if (opt.trace) console.log({ setMode: mode, xprt, show, disp, hmod, modes:el.modes });
if (opt.manual) return;
el.setVisible(!hidn && hmod && show && xprt && disp);
}
el.hasMode = function(mode) {
return (el.modes.length === 0) ||
(el.modes.contains && el.modes.contains(mode)) ||
(el.modes === mode);
}
el.modes = opt.modes || [];
hasModes.push(el);
}
function newDiv(opt = {}) {
let div = DOC.createElement(opt.tag || 'div');
addModeControls(div, opt);
(opt.addto || addTo).appendChild(div);
if (opt.addto) lastDiv = addTo = div;
if (opt.class) div.setAttribute('class', opt.class);
if (opt.group !== false) {
lastGroup?.push(div);
div._group = groupName;
} else {
lastGroup = undefined;
groupName = undefined;
}
return div;
}
function newExpand(label, opt = {}, opteach = {}) {
let div = DOC.createElement('details');
div.setAttribute('class', opt.class || 'f-col');
addModeControls(div, opt);
let summary = DOC.createElement('summary');
summary.setAttribute('class', opt.class || 'var-row');
summary.innerHTML = `<label>${label}</label>`;
div.appendChild( summary );
div.collapse = () => {
div.removeAttribute('open');
};
lastAddTo = addTo;
addTo.appendChild(div);
addTo = div;
return div;
}
function endExpand() {
addTo = lastAddTo;
return addTo;
}
function isSticky() {
return groupSticky;
}
function setSticky(bool) {
groupSticky = bool;
}
function newGCode(label, options) {
let opt = options || {},
btn = DOC.createElement("button"),
txt = DOC.createElement("textarea"),
area = opt.area;
txt.setAttribute("wrap", "off");
txt.setAttribute("spellcheck", "false");
txt.setAttribute("style", "resize: none");
txt.onblur = bindTo || inputAction;
txt.button = btn;
btn.setAttribute("class", "basis-50");
btn.appendChild(DOC.createTextNode(label));
btn.setAttribute("title", opt.title || undefined);
btn.onclick = function(ev) {
ev.stopPropagation();
if (ev.target === txt) {
// drop clicks on TextArea
ev.target.focus();
} else {
let fc = area.firstChild;
if (fc) area.removeChild(fc);
area.appendChild(txt);
txt.scrollTop = 0;
txt.scrollLeft = 0;
txt.selectionEnd = 0;
let rows = txt.value.split('\n');
let cols = 0;
rows.forEach(row => {
cols = Math.max(cols, row.length);
});
let showing = btn === lastBtn;
if (lastTxt) {
lastTxt.classList.remove('txt-sel');
}
if (lastBtn) {
lastBtn.classList.remove('btn-sel');
}
if (!showing) {
btn.classList.add('btn-sel');
lastTxt = btn;
lastBtn = btn;
txt.focus();
} else {
inputAction();
}
}
};
addModeControls(btn, opt);
return txt;
}
function newText(label, options) {
let opt = options || {},
inline = opt.row,
row = inline ? lastDiv : newDiv(options),
btn = DOC.createElement("button"),
pop = DOC.createElement("div"),
txt = DOC.createElement("textarea"),
area = opt.area;
txt.setAttribute("wrap", "off");
txt.setAttribute("spellcheck", "false");
txt.setAttribute("style", "resize: none");
txt.onblur = inputAction;
btn.appendChild(DOC.createTextNode(inline ? label : "edit"));
btn.onclick = function(ev) {
ev.stopPropagation();
if (ev.target === txt) {
// drop clicks on TextArea
ev.target.focus();
} else {
let fc = area.firstChild;
if (fc) area.removeChild(fc);
area.appendChild(txt);
// first time, button click / show
btn.parentNode.onclick = btn.onclick;
txt.scrollTop = 0;
txt.scrollLeft = 0;
txt.selectionEnd = 0;
let rows = txt.value.split('\n');
let cols = 0;
rows.forEach(row => {
cols = Math.max(cols, row.length);
});
let showing = pop === lastPop;
if (lastBtn) {
lastBtn.classList.remove('btn-sel');
}
if (lastTxt) {
lastTxt.classList.remove('txt-sel');
}
if (!showing) {
row.classList.add('txt-sel');
pop.style.display = "flex";
lastPop = pop;
lastTxt = row;
txt.focus();
} else {
inputAction();
}
}
};
addModeControls(btn, opt);
addId(btn, opt);
if (!inline) {
row.appendChild(newLabel(label));
row.setAttribute("class", "var-row");
}
row.appendChild(btn);
if (opt.title) row.setAttribute("title", options.title);
if (row.setVisible) {
btn.setVisible = row.setVisible;
}
return txt;
}
function newInput(label, opt = {}) {
let row = newDiv(opt),
hide = opt.hide,
size = opt.size || 5,
height = opt.height || 0,
ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'),
action = opt.action || bindTo || inputAction;
row.appendChild(newLabel(label));
row.appendChild(ip);
row.setAttribute("class", opt.class || "var-row");
if (height > 1) {
ip.setAttribute("cols", size);
ip.setAttribute("rows", height);
ip.setAttribute("wrap", "off");
} else {
if (Number.isInteger(size)) {
ip.setAttribute("size", size);
} else {
ip.setAttribute("style", `width:${size}`);
}
}
ip.setAttribute("type", "text");
ip.setAttribute("spellcheck", "false");
row.style.display = hide ? 'none' : '';
if (opt.disabled) ip.setAttribute("disabled", "true");
if (opt.title) row.setAttribute("title", opt.title);
if (opt.convert) ip.convert = opt.convert.bind(ip);
if (opt.bound) ip.bound = opt.bound;
if (opt.action) action = opt.action;
ip.addEventListener('focus', function(event) {
setSticky(true);
});
if (action) {
ip.addEventListener('keydown', function(event) {
let key = event.key;
if (
opt.text ||
(key >= '0' && key <= '9') ||
key === '.' ||
key === '-' ||
key === 'Backspace' ||
key === 'Delete' ||
key === 'ArrowLeft' ||
key === 'ArrowRight' ||
key === 'Tab' ||
event.metaKey ||
event.ctrlKey ||
(key === ',' && options.comma)
) {
return;
}
event.preventDefault();
event.stopPropagation();
});
ip.addEventListener('keyup', function(event) {
if (event.keyCode === 13) {
ip.blur();
}
});
ip.addEventListener('blur', function(event) {
setSticky(false);
lastChange = ip;
action(event);
if (opt.trigger) {
refresh(opt.trigger === 1 || opt.trigger === true);
}
});
if (opt.units) {
addUnits(ip, opt.round || 3);
}
}
if (!ip.convert) ip.convert = raw.bind(ip);
ip.setVisible = row.setVisible;
return ip;
}
function addUnits(input, round) {
setters.push(input);
input.setv = function(value) {
if (typeof(value) === 'number') {
input.real = value;
input.value = (value / units).round(round);
} else {
input.value = value;
}
// console.log({ value }, input.real, input.value );
return input.real;
};
return input;
}
function newRange(label, options) {
let row = newDiv(options),
ip = DOC.createElement('input'),
hide = options && options.hide,
action = bindTo || inputAction;
if (label) row.appendChild(newLabel(label));
row.appendChild(ip);
row.setAttribute("class", "var-row");
ip.setAttribute("type", "range");
ip.setAttribute("min", (options && options.min ? options.min : 0));
ip.setAttribute("max", (options && options.max ? options.max : 100));
ip.setAttribute("value", 0);
row.style.display = hide ? 'none' : '';
if (options) {
if (options.title) {
ip.setAttribute("title", options.title);
row.setAttribute("title", options.title);
}
if (options.action) action = options.action;
}
ip.setVisible = row.setVisible;
return ip;
}
function newSelect(label, options = {}, source) {
let row = newDiv(options),
ip = DOC.createElement('select'),
hide = options && options.hide,
action = bindTo || inputAction;
row.appendChild(newLabel(label));
row.appendChild(ip);
if (Array.isArray(source)) {
ip._source = source;
} else {
row.setAttribute("source", source || "tools");
}
row.setAttribute("class", "var-row");
row.style.display = hide ? 'none' : '';
if (options.convert) ip.convert = options.convert.bind(ip);
if (options.disabled) ip.setAttribute("disabled", "true");
if (options.title) row.setAttribute("title", options.title);
if (options.action) action = options.action;
ip.setVisible = row.setVisible;
ip.onchange = function(ev) {
lastChange = ip;
action();
if (options.trigger) {
refresh();
}
};
ip.onclick = (ev) => {
groupSticky = true;
};
// because firefox
ip.onmouseenter = (ev) => {
groupSticky = true;
};
return ip;
}
function newBoolean(label, action = bindTo, opt = {}) {
let row = newDiv(opt),
ip = DOC.createElement('input'),
hide = opt.hide;
if (label) {
row.appendChild(newLabel(label));
}
row.appendChild(ip);
row.setAttribute("class", "var-row");
row.style.display = hide ? 'none' : '';
ip.setAttribute("type", "checkbox");
ip.checked = false;
if (opt.disabled) {
ip.setAttribute("disabled", "true");
}
if (opt.title) {
ip.setAttribute("title", opt.title);
row.setAttribute("title", opt.title);
}
if (action) {
ip.onclick = function(ev) {
action(ip);
if (opt.trigger) {
refresh();
}
};
}
ip.setVisible = row.setVisible;
return ip;
}
function newBlank(options) {
let opt = options || {},
row = newDiv(opt),
hide = opt.hide;
row.isBlank = true;
row.style.display = hide ? 'none' : '';
if (!opt.driven) {
row.setAttribute("class", "var-row");
}
if (opt.class) {
opt.class.split(' ').forEach(ce => {
row.classList.add(ce);
});
}
return row;
}
// unlike other elements, does not auto-add to a row
function newButton(label, action, opt = {}) {
let b = DOC.createElement('button');
b.onclick = function() {
switch (typeof action) {
case "string":
api.event.emit(action);
break;
case "function":
action(...arguments);
break;
}
};
if (opt.class) {
opt.class.split(' ').forEach(ce => {
b.classList.add(ce);
});
}
if (opt.icon) {
let d = DOC.createElement('div');
d.innerHTML = opt.icon;
b.appendChild(d);
}
if (opt.title) {
b.setAttribute('title', opt.title);
}
if (label) {
b.appendChild(DOC.createTextNode(label));
}
addModeControls(b, opt);
addId(b, opt);
return b;
}
function newRow(children, options) {
let row = addCollapsableElement((options && options.noadd) ? null : addTo);
if (children) children.forEach(function (c) { row.appendChild(c) });
addModeControls(row, options);
if (options && options.class) {
options.class.split(' ').forEach(ce => {
row.classList.add(ce);
});
}
return row;
}

View file

@ -1,8 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { ajax as moto_ajax } from '../../moto/ajax.js';
import { local as dataLocal } from '../../data/local.js';
gapp.register("kiri.utils", [], (root, exports) => {
function parseOpt(ov) { function parseOpt(ov) {
let opt = {}, kv, kva; let opt = {}, kv, kva;
@ -28,7 +27,7 @@ function encodeOpt(opt) {
} }
function ajax(url, fn, rt, po, hd) { function ajax(url, fn, rt, po, hd) {
return moto.ajax.new(fn, rt).request(url, po, hd); return moto_ajax.new(fn, rt).request(url, po, hd);
} }
function o2js(o,def) { function o2js(o,def) {
@ -46,7 +45,7 @@ function js2o(s,def) {
function ls2o(key,def) { function ls2o(key,def) {
// defer ref b/c it may run in a worker // defer ref b/c it may run in a worker
return js2o(root.data.local.getItem(key),def); return js2o(dataLocal.getItem(key),def);
} }
// split 24 bit color into [ r, g, b ] // split 24 bit color into [ r, g, b ]
@ -99,11 +98,14 @@ function trackFn(fn, name) {
} }
} }
exports({ function noop() {}
export {
trackFn, trackFn,
areEqual, areEqual,
parseOpt, parseOpt,
encodeOpt, encodeOpt,
noop,
ajax, ajax,
o2js, o2js,
js2o, js2o,
@ -111,6 +113,19 @@ exports({
avgc, avgc,
rgb, rgb,
a2c a2c
}); };
}); export let utils = {
trackFn,
areEqual,
parseOpt,
encodeOpt,
noop,
ajax,
o2js,
js2o,
ls2o,
avgc,
rgb,
a2c
};

View file

@ -1,23 +1,11 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { base } from '../../geo/base.js';
import { avgc } from './utils.js';
// dep: geo.base import { verticesToPoints } from '../../geo/points.js';
// dep: geo.point import { util as mesh_util } from '../../mesh/util.js';
// dep: geo.points
// dep: geo.polygons
// dep: kiri.utils
// use: kiri.codec
// use: mesh.util
gapp.register("kiri.widget", [], (root, exports) => {
const { base, kiri } = root;
const { api, driver, utils } = kiri;
const { util, polygons } = base;
const { Mesh, newPoint, verticesToPoints } = base;
const { inRange, time } = util;
const { avgc, trackFn } = utils;
const { inRange, time } = base.util;
const solid_opacity = 1.0; const solid_opacity = 1.0;
const groups = []; const groups = [];
@ -28,12 +16,14 @@ let nextId = 0;
function newWidget(id,group) { return new Widget(id,group) } function newWidget(id,group) { return new Widget(id,group) }
function catalog() { return kiri.catalog } function catalog() { return self.kiri_catalog };
function index() { return catalog().index } function index() { return self.kiri_catalog.index }
class Widget { class Widget {
constructor(id, group) { constructor(id, group) {
this.api = self.kiri_api;
this.id = id || Date.now().toString(36)+(nextId++); this.id = id || Date.now().toString(36)+(nextId++);
this.grouped = group ? true : false; this.grouped = group ? true : false;
this.group = group || []; this.group = group || [];
@ -347,7 +337,7 @@ class Widget {
} }
selectFaces(faces) { selectFaces(faces) {
let groups = mesh.util.facesToGroups(faces || []); let groups = mesh_util.facesToGroups(faces || []);
let geo = this.mesh.geometry; let geo = this.mesh.geometry;
geo.clearGroups(); geo.clearGroups();
for (let group of groups) { for (let group of groups) {
@ -503,8 +493,8 @@ class Widget {
w._move(x, y, z, abs); w._move(x, y, z, abs);
}); });
// allow for use in engine / cli // allow for use in engine / cli
if ((x || y || z) && api && api.event) { if ((x || y || z) && this.api && this.api.event) {
api.event.emit('widget.move', {widget: this, pos: {x, y, z}, abs}); this.api.event.emit('widget.move', {widget: this, pos: {x, y, z}, abs});
} }
} }
@ -555,8 +545,8 @@ class Widget {
w._scale(x, y, z); w._scale(x, y, z);
}); });
this.center(false); this.center(false);
if (api && api.event) { if (this.api && this.api.event) {
api.event.emit('widget.scale', {widget: this, x, y, z}); this.api.event.emit('widget.scale', {widget: this, x, y, z});
} }
} }
@ -586,8 +576,8 @@ class Widget {
if (this.outline) { if (this.outline) {
this.setEdges(true); this.setEdges(true);
} }
if ((x || y || z) && api && api.event) { if ((x || y || z) && this.api && this.api.event) {
api.event.emit('widget.rotate', {widget: this, x, y, z}); this.api.event.emit('widget.rotate', {widget: this, x, y, z});
} }
} }
@ -630,8 +620,8 @@ class Widget {
w._mirror(); w._mirror();
}); });
this.center(); this.center();
if (api && api.event) { if (this.api && this.api.event) {
api.event.emit('widget.mirror', {widget: this}); this.api.event.emit('widget.mirror', {widget: this});
} }
} }
@ -758,113 +748,21 @@ class Widget {
return this; return this;
} }
/**
* processes points into facets, then into slices
*
* once upon a time there were multiple slicers. this was the fastest in most cases.
* lines are added to all the buckets they cross. then buckets are processed in order.
* buckets are contiguous ranges of z slicers. the advantage of this method is that
* as long as a large percentage of lines do not cross large z distances, this reduces
* the number of lines each slice has to consider thus improving speed.
*
* @params {Object} settings
* @params {Function} [ondone]
* @params {Function} [onupdate]
*/
slice(settings, ondone, onupdate) {
let widget = this;
let startTime = time();
widget.settings = settings;
widget.clearSlices();
onupdate(0.0001, "slicing");
if (kiri.client && !widget.inWorker) {
// store slicing visuals
widget.stack = kiri.stacks.create(widget.id, widget.mesh);
// compensate for zcut (widget moved through floor)
widget.stack.obj.view.position.z = widget.track.zcut || 0;
// in case result of slice is nothing, do not preserve previous
widget.slices = []
// executed from kiri.js
kiri.client.slice(settings, this, function(reply) {
if (reply.alert) {
onupdate(null, null, reply.alert);
}
if (reply.update) {
onupdate(reply.update, reply.updateStatus);
}
if (reply.send_start) {
widget.xfer = {start: reply.send_start};
}
if (reply.stats) {
widget.stats = reply.stats;
}
if (reply.send_end) {
widget.stats.load_time = widget.xfer.start - reply.send_end;
}
if (reply.slice) {
widget.slices.push(kiri.codec.decode(reply.slice, {mesh:widget.mesh}));
}
if (reply.done) {
ondone(true);
}
if (reply.error) {
ondone(false, reply.error);
}
});
}
if (kiri.server) {
// executed from kiri-worker.js
let catchdone = function(error) {
if (error) {
return ondone(error);
}
onupdate(1.0, "transfer");
widget.stats.slice_time = time() - startTime;
ondone();
};
let catchupdate = function(progress, message, alert) {
onupdate(progress, message, alert);
};
let drv = driver[settings.mode.toUpperCase()];
if (drv) {
let promise = drv.slice(settings, widget, catchupdate, catchdone);
if (promise) promise.catch(error => ondone(error));
} else {
console.log('invalid mode: '+settings.mode);
ondone('invalid mode: '+settings.mode);
}
}
// discard point cache
widget.points = undefined;
}
/** /**
* render to provided stack * render to provided stack
*/ */
render(stack) { render(stack) {
const mark = Date.now(); const mark = Date.now();
if (this.slices) for (let slice of this.slices || []) {
this.slices.forEach(slice => {
if (slice.layers) { if (slice.layers) {
stack.add(slice.layers); stack.add(slice.layers);
} }
}); }
return Date.now() - mark; return Date.now() - mark;
} }
setEdges(set) { setEdges(set) {
if (!(api && api.conf)) { if (!(this.api && this.api.conf)) {
// missing api features in engine mode // missing api features in engine mode
return; return;
} }
@ -878,8 +776,8 @@ class Widget {
} }
if (set) { if (set) {
let dark = api.space.is_dark(); let dark = this.api.space.is_dark();
let angle = api.conf.get().controller.edgeangle || 20; let angle = this.api.conf.get().controller.edgeangle || 20;
let edges = new THREE.EdgesGeometry(mesh.geometry, angle); let edges = new THREE.EdgesGeometry(mesh.geometry, angle);
let material = new THREE.LineBasicMaterial({ color: 0 }); let material = new THREE.LineBasicMaterial({ color: 0 });
this.outline = new THREE.LineSegments(edges, material); this.outline = new THREE.LineSegments(edges, material);
@ -889,7 +787,7 @@ class Widget {
} }
setWireframe(set, color, opacity) { setWireframe(set, color, opacity) {
if (!(api && api.conf)) { if (!(this.api && this.api.conf)) {
// missing api features in engine mode // missing api features in engine mode
return; return;
} }
@ -901,7 +799,7 @@ class Widget {
this.wire = null; this.wire = null;
} }
if (set) { if (set) {
let dark = api.space.is_dark(); let dark = this.api.space.is_dark();
let mat = new THREE.MeshBasicMaterial({ let mat = new THREE.MeshBasicMaterial({
wireframe: true, wireframe: true,
color: dark ? 0xaaaaaa : 0, color: dark ? 0xaaaaaa : 0,
@ -911,7 +809,7 @@ class Widget {
let wire = widget.wire = new THREE.Mesh(mesh.geometry.shallowClone(), mat); let wire = widget.wire = new THREE.Mesh(mesh.geometry.shallowClone(), mat);
mesh.add(wire); mesh.add(wire);
} }
if (api.view.is_arrange()) { if (this.api.view.is_arrange()) {
this.setColor(this.color); this.setColor(this.color);
} else { } else {
this.setColor(0x888888,undefined,false); this.setColor(0x888888,undefined,false);
@ -1066,7 +964,4 @@ Widget.deleteFromState = function(id,ondone) {
index().remove('ws-save-'+id, ondone); index().remove('ws-save-'+id, ondone);
}; };
kiri.Widget = Widget; export { Widget, newWidget };
kiri.newWidget = newWidget;
});

View file

@ -1,14 +1,9 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict"; import { $ } from '../../moto/webui.js';
import { api } from './api.js';
// use: load.file import { load } from '../../load/file.js';
// use: kiri.selection import { Widget, newWidget } from './widget.js';
// use: kiri.platform
gapp.register("kiri.widgets", (root, exports) => {
const { data, kiri, moto, noop } = root;
const { api, consts, utils, newWidget, Widget } = kiri;
let WIDGETS = []; let WIDGETS = [];
@ -36,7 +31,7 @@ function rename(sel) {
return; return;
} }
let widget = widgets[0]; let widget = widgets[0];
kiri.ui.prompt("new widget name", widget.meta.file || "no name").then(newname => { api.uc.prompt("new widget name", widget.meta.file || "no name").then(newname => {
if (newname) { if (newname) {
widget.meta.file = newname; widget.meta.file = newname;
api.platform.changed(); api.platform.changed();
@ -89,7 +84,7 @@ function meshes() {
function opacity(value) { function opacity(value) {
api.widgets.each(w => w.setOpacity(value)); api.widgets.each(w => w.setOpacity(value));
moto.space.update(); api.space.update();
} }
function setIndexed(value) { function setIndexed(value) {
@ -101,7 +96,7 @@ function setAxisIndex(value) {
} }
// extend API (api.widgets) // extend API (api.widgets)
const widgets = Object.assign(api.widgets, { export const widgets = {
load: Widget.loadFromCatalog, load: Widget.loadFromCatalog,
new: newWidget, new: newWidget,
map, map,
@ -120,6 +115,4 @@ const widgets = Object.assign(api.widgets, {
each(fn) { WIDGETS.slice().forEach(widget => fn(widget)) }, each(fn) { WIDGETS.slice().forEach(widget => fn(widget)) },
for(fn) { widgets.each(fn) }, for(fn) { widgets.each(fn) },
forid(id) { return WIDGETS.filter(w => w.id === id)[0] } forid(id) { return WIDGETS.filter(w => w.id === id)[0] }
}); };
});

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