Merge branch 'rel-4.3' into helical-op

This commit is contained in:
bakedpotatolord 2025-07-10 12:57:56 -06:00
commit dfc2cd7f17
325 changed files with 18349 additions and 25230 deletions

View file

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

View file

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

7
.gitignore vendored
View file

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

View file

@ -2,6 +2,7 @@ const { app, shell, session, BrowserWindow } = require('electron');
const path = require('path');
const server = require('@gridspace/app-server');
const pkgd = app.isPackaged;
const basDir = __dirname;
const usrDir = app.getPath("userData");
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 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({ argv: process.argv, debug, devel });
// console.log({
@ -26,9 +29,9 @@ server({
data: datDir,
conf: cnfDir,
logs: logDir,
cache: path.join(basDir, "data", "cache"),
single: true,
electron: true,
pkgd,
debug
});

611
app.js
View file

@ -12,31 +12,26 @@ const uglify = require('uglify-js');
const moment = require('moment');
const agent = require('express-useragent');
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 PATH = require('path');
const fileCache = {};
const code_src = {};
const append = { mesh:'', kiri:'' };
const code = {};
const mods = {};
const load = [];
const synth = {};
const api = {};
const wrap = {};
let forceUseCache = false;
let serviceWorker = true;
let crossOrigin = false;
let setupFn;
let cacheDir;
let startTime;
let oversion;
let dversion;
let lastmod;
let logger;
let debug;
let over;
let http;
let util;
let dir;
@ -58,25 +53,6 @@ netdb.create = async function(map = {}) {
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) {
const ENV = mod.env;
@ -94,8 +70,6 @@ function init(mod) {
if (ENV.single) console.log({ cwd: process.cwd(), env: ENV });
dversion = debug ? `_${version}` : version;
cacheDir = ENV.cache || mod.util.datadir("cache");
if (ENV.single) logger.log({ cacheDir });
forceUseCache = ENV.cache ? true : false;
const approot = PATH.join("main","gapp");
@ -103,172 +77,8 @@ function init(mod) {
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
function pos(path, list) {
for (let i=0; i<list.length; i++) {
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]});
if (!ENV.electron) {
generateDevices();
}
mod.on.test((req) => {
@ -285,7 +95,8 @@ function init(mod) {
mod.add(handleSetup);
mod.add(handleOptions);
mod.add(handleWasm);
mod.add(serveWasm);
mod.add(serveCode);
mod.add(fullpath({
"/kiri" : redir("/kiri/", 301),
"/mesh" : redir("/mesh/", 301),
@ -295,10 +106,6 @@ function init(mod) {
"/meta/index.html" : redir("/meta/", 301)
}));
mod.add(handleVersion);
mod.add(prepath([
[ "/code/", handleCode ],
// [ "/wasm/", handleWasm ]
]));
mod.add(fixedmap("/api/", api));
if (debug) {
mod.static("/mod/", "mod");
@ -309,25 +116,14 @@ function init(mod) {
});
}
mod.add(rewriteHtmlVersion);
mod.add((req, res, next) => {
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");
// example of how to use the new middleware
// mod.add(appendContent('/kiri/index.html', '<script>console.log("hello")</script>'));
mod.static("/lib/", "src");
mod.static("/obj/", "web/obj");
mod.static("/font/", "web/font");
mod.static("/fon2/", "web/fon2");
mod.static("/mesh/", "web/mesh");
mod.static("/moto/", "web/moto");
mod.static("/meta/", "web/meta");
mod.static("/kiri/", "web/kiri");
function load_modules(root, force) {
@ -365,8 +161,12 @@ function init(mod) {
}
}
// runs after module loads / injects
prepareScripts();
// minify appends
if (!debug) {
for (let key of Object.keys(append)) {
append[key] = minify(append[key]);
}
}
};
// either add module assets to path or require(init.js)
@ -384,12 +184,11 @@ function loadModule(mod, dir) {
// load module and call returned function with helper object
function initModule(mod, file, dir) {
logger.log({module: file});
logger.log({ module: file, dir });
require_fresh(file)({
// express functions added here show up at "/api/" url root
api: api,
adm: {
reload: prepareScripts,
setver: (ver) => { oversion = ver },
crossOrigin: (bool) => { crossOrigin = bool }
},
@ -398,7 +197,6 @@ function initModule(mod, file, dir) {
args: {},
meta: mod.meta,
debug: debug,
script: script,
moddir: dir,
rootdir: mod.dir,
version: oversion || version
@ -425,40 +223,43 @@ function initModule(mod, file, dir) {
logger: log.new
},
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;
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 });
}
},
path: {
any: arg => { mod.add(arg) },
pre: arg => { mod.add(prepath(arg)) },
map: arg => { mod.add(fixedmap(arg)) },
code() {
const [ path, file ] = [ ...arguments ];
if (lastmod(file)) {
code[path] = fs.readFileSync(file);
// console.log({ CODE: path, file });
} else if (lastmod(mod.dir + '/' + file)) {
const alt = mod.dir + '/' + file;
code[path] = fs.readFileSync(alt);
// console.log({ CODE: path, alt });
} else {
console.log({ MISSING_CODE: path, file });
}
},
full: arg => { mod.add(fullpath(arg)) },
map: arg => { mod.add(fixedmap(arg)) },
pre: arg => { mod.add(prepath(arg)) },
redir: redir,
remap: remap,
setup: fn => { setupFn = fn },
static: (root, pre) => {
mod.static(pre || "/", root);
},
code: (endpoint, path) => {
let fpath = PATH.join(mod.dir, path);
if (debug) {
code[endpoint] = fs.readFileSync(fpath);
} else {
code[endpoint] = minify(fpath);
}
code_src[endpoint] = {
endpoint,
path: path,
mod: lastmod(fpath)
};
},
redir: redir,
remap: remap,
setup: fn => { setupFn = fn }
},
handler: {
addCORS: addCorsHeaders,
@ -479,51 +280,6 @@ function initModule(mod, file, dir) {
});
}
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_work",
"&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 = {};
@ -559,14 +315,33 @@ 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',
// '/lib/kiri/run/frame.js' : '/lib/pack/kiri-frame.js',
};
function handleVersion(req, res, next) {
let vstr = oversion || dversion || version;
if (["/kiri/","/mesh/","/meta/"].indexOf(req.app.path) >= 0 && req.url.indexOf(vstr) < 0) {
if (["/kiri/","/mesh/"].indexOf(req.app.path) >= 0 && req.url.indexOf(vstr) < 0) {
if (req.url.indexOf("?") > 0) {
return http.redirect(res, `${req.url},ver:${vstr}`);
} else {
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 {
next();
}
@ -587,7 +362,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 ext = (file || '').split('.')[1];
let path = PATH.join(dir,"src","wasm",file);
@ -610,61 +398,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
function generateDevices() {
let root = PATH.join(dir,"src","kiri-dev");
let root = PATH.join(dir,"src","kiri","dev");
let devs = {};
fs.readdirSync(root).forEach(type => {
let map = devs[type] = devs[type] || {};
@ -675,132 +411,12 @@ function generateDevices() {
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 prepareScripts() {
generateDevices();
for (let key of Object.keys(script)) {
code[key] = concatCode(key);
}
}
function concatCode(key) {
let array = script[key];
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) {
inject.forEach(key => {
code.push(synth[key]);
});
code.push(...[
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;',
// 'console.log("loading", file);',
'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'));
code = code.join('\n');
} else {
inject.forEach(key => {
code.push(synth[key]);
});
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);
});
code = code.join('');
synth[key] = `self.${key} = "${Buffer.from(code).toString('base64')}";\n`;
}
return code;
}
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);
function minify(code) {
let mini = uglify.minify(code.toString(), {
compress: {
merge_vars: false,
@ -837,10 +453,10 @@ function addCorsHeaders(req, res) {
if (req.headers['access-control-request-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-Embedder-Policy", 'require-corp');
}
// }
res.setHeader("Allow", "GET,POST,OPTIONS");
}
@ -893,7 +509,9 @@ function fixedmap(prefix, map) {
// HTTP 307 redirect
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
@ -916,26 +534,43 @@ function cookieValue(cookie,key) {
}
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);
let real_write = res.write;
let real_end = res.end;
let mlen = '{{version}}'.length;
let vstr = oversion || dversion || version;
if (vstr.length < mlen) {
vstr = vstr.padStart(mlen,0);
} else if (vstr.length > mlen) {
vstr = vstr.substring(0,mlen);
}
res.write = function() {
arguments[0] = arguments[0].toString().replace(/{{version}}/g,vstr);
real_write.apply(res, arguments);
}
if ([
"/lib/main/kiri.js",
"/lib/main/mesh.js"
].indexOf(req.app.path) >= 0) {
addCorsHeaders(req, res);
const data = append[req.app.path.split('/')[3].split('.')[0]];
if (!data) return next();
if (debug) logger.log({ append: req.app.path, data: data.length });
const real_write = res.write;
const real_end = res.end;
let body = '';
res.write = function (chunk, encoding) {
body += chunk.toString(encoding);
};
res.end = function() {
if (arguments[0]) {
arguments[0] = arguments[0].toString().replace(/{{version}}/g,vstr);
res.end = function (chunk, encoding) {
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
server({
electron: true,
dryrun: true,
single: true
});
// server({
// electron: true,
// dryrun: 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() {
console.log('npm pre running');
const links = fs.readFileSync("links.csv")
const links = fs.readFileSync("conf/links.csv")
.toString()
.trim()
.split('\n')
.map(line => line.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
for (let [link, target] of links) {
const absoluteTarget = path.resolve(path.dirname(link), target);
@ -24,6 +24,9 @@ async function main() {
console.error(`Error creating symlink: ${link} -> ${absoluteTarget}`, err);
}
}
} else {
console.log('skipping symlink conversion');
}
}
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

@ -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 { LineSegmentsGeometry } from '../node_modules/three/examples/jsm/lines/LineSegmentsGeometry.js';
import * as MeshBVHLib from '../node_modules/three-mesh-bvh/build/index.module.js';
export {
THREE, SVGLoader, BufferGeometryUtils,
THREE,
SVGLoader,
BufferGeometryUtils,
LineMaterial,
Line2, LineGeometry,
LineSegments2, LineSegmentsGeometry
};
Line2,
LineGeometry,
LineSegments2,
LineSegmentsGeometry,
MeshBVHLib
};

View file

@ -1,15 +1,18 @@
const TerserPlugin = require("terser-webpack-plugin");
const path = require('path');
module.exports = {
mode: 'production',
entry: "./bin/webpack-three-bundle.js",
entry: path.resolve(__dirname, './webpack-three-bundle.js'),
output: {
path: path.resolve('src/ext'),
path: path.resolve(__dirname, '../src/ext'),
filename: 'three.js',
library: 'ThreeBundle',
libraryTarget: 'umd',
globalObject: 'this'
library: {
type: 'module'
},
module: true
},
experiments: {
outputModule: true
},
resolve: {
extensions: ['.mjs', '.js'],
@ -25,21 +28,11 @@ module.exports = {
],
},
optimization: {
minimize: false,
minimizer: [
new TerserPlugin({
extractComments: false,
terserOptions: {
format: {
comments: false,
},
},
}),
],
minimize: false
},
performance: {
hints: false,
maxAssetSize: 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/jszip.js,../../node_modules/jszip/dist/jszip.js
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
web/fon2,../node_modules/bootstrap-icons/font/
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
- Run the following code: `kiri.api.conf.get().device`
- 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.
- Publish your changes to a git repo
- 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');
const { kiri, moto } = self;
const { ui } = kiri;
const h = moto.webui;
const { uc: ui } = api;
const { $, h } = api.web;
const defams = ";; DEFINE BAMBU-AMS ";
const readonly = true;
const stock_colors = Object.values({
@ -565,7 +564,7 @@ self.kiri.load(api => {
function monitoring() {
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;
}
@ -661,7 +660,7 @@ self.kiri.load(api => {
}
});
api.event.on("init-done", function() {
api.event.on("load-done", function() {
if (init) {
return;
}

View file

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

View file

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

View file

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

View file

@ -1,11 +1,10 @@
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
"use strict";
// 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() {
this.pos = 0;
this.array = [];
@ -48,7 +47,7 @@ self.ArrayWriter = class ArrayWriter {
}
};
self.DataWriter = class DataWriter {
class DataWriter {
constructor(view, pos) {
this.view = view;
this.pos = pos || 0;
@ -106,7 +105,7 @@ self.DataWriter = class DataWriter {
}
}
self.DataReader = class DataReader {
class DataReader {
constructor(view, pos) {
this.view = view;
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 */
"use strict";
// dep: ext.three
gapp.register("add.three", [], (root, exports) => {
const {
import {
THREE, SVGLoader, BufferGeometryUtils,
LineMaterial, Line2, LineGeometry, LineSegments2, LineSegmentsGeometry
} = ThreeBundle;
root.THREE = THREE;
LineMaterial, Line2, LineGeometry, LineSegments2,
LineSegmentsGeometry, MeshBVHLib } from '../ext/three.js';
// Make THREE available globally
self.THREE = THREE;
THREE.BufferGeometryUtils = BufferGeometryUtils;
THREE.SVGLoader = SVGLoader.SVGLoader;
THREE.LineMaterial = LineMaterial;
@ -147,4 +144,5 @@ THREE.Object3D.prototype.removeAll = function() {
this.children = [];
};
});
// Export for ES modules
export { THREE };

View file

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

View file

@ -1,19 +1,14 @@
/** 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
export const data = {
Index: IDBStore,
open() {
return new IDBStore(...arguments);
}
};
let IDB, IRR, local = null;
try {
@ -319,4 +314,5 @@ SP.clear = function(key, store = this.current) {
.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 */
"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() {
this.__data__ = {};
this.__mem__ = true;
removeItem(key) {
delete this.__data__[key];
}
clear() {
this.__data__ = {};
}
}
var LS = Local.prototype;
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__ = {};
};
let setLocal;
try {
// deprecate 'Local' at some point
let local = data.local =self.localStorage;
if (typeof window === 'undefined') {
setLocal = new Local();
} else {
// deprecate 'Local' at some point
setLocal = self.localStorage;
}
let testkey = '__test';
local.setItem(testkey, 1);
local.getItem(testkey);
local.removeItem(testkey);
setLocal.setItem(testkey, 1);
setLocal.getItem(testkey);
setLocal.removeItem(testkey);
} catch (e) {
data.local = new Local();
setLocal = new Local();
let msg = "localStorage disabled: application may not function properly";
console.log(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;
});
}

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

@ -0,0 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
//import original library
import * as JSZip from "./jszip.js";
//export object attached to the window object
export default self.JSZip;

View file

@ -1,9 +1,6 @@
// SOURCE: https://stackoverflow.com/questions/1655769/fastest-md5-implementation-in-javascript
gapp.register("ext.md5", [], (root, exports) => {
exports({ hash });
function hash(e) {
export function hash(e) {
function h(a, b) {
var c, d, e, f, g;
e = a & 2147483648;
@ -67,6 +64,4 @@ function hash(e) {
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);
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 */
"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;
function time() {
@ -219,7 +215,7 @@ function intersect(p1, p2, p3, p4, test, parallelok) {
if (test === keys.SEGINT && !segint) return null;
if (test === keys.RAYINT && !rayint) return null;
let ip = base.newPoint(
let ip = newPoint(
p1x + (a * d1x), // x
p1y + (a * d1y), // y
p3.z || p4.z, // z
@ -264,7 +260,7 @@ function intersectRayLine(ro, s1, p1, p2, infinite) {
b = n2 / d;
if (infinite || (inCloseRange(b, 0, 1) && a >= 0)) {
let ip = base.newPoint(
let ip = newPoint(
p1x + (a * s1x),
p1y + (a * s1y),
p2.z || ro.z,
@ -528,7 +524,7 @@ function comma(v) {
* Connect to base
******************************************************************* */
base.key = {
export const key = {
NONE: "",
PROJECT: "project",
SEGINT: "segint",
@ -536,7 +532,7 @@ base.key = {
PARALLEL: "parallel"
};
base.config = {
export const config = {
// size of gcode debug arrow head
debug_arrow: 0.25,
// default # of decimal places in generated gcode
@ -602,7 +598,7 @@ base.config = {
clipperClean: 250
};
base.util = {
export const util = {
sqr,
lerp,
time,
@ -639,4 +635,10 @@ base.util = {
zInPlane
};
});
export const base = {
config,
key,
util
};
export { earcut };

View file

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

View file

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

View file

@ -1,11 +1,5 @@
/** 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;
let cache = {};
@ -17,7 +11,7 @@ let lastSlice = 0;
* @param off {number} z offset value from 0-1
* @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
// or the requested resolution or tip values have changed
let now = Date.now();
@ -170,7 +164,7 @@ function slice(off, res, val) {
}
// merge co-linear and distance threshold
function filter(poly, inc) {
export function filter(poly, inc) {
if (poly.length <= 2) {
return poly;
}
@ -200,11 +194,10 @@ function filter(poly, inc) {
}
e1 = el;
}
nupoly.push(poly[poly.length-1]);
return nupoly;
}
function distTo(a, b, dir) {
export function distTo(a, b, dir) {
let dx = a.x - b.x;
let dy = a.y - b.y;
// 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;
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 */
"use strict";
// dep: geo.base
gapp.register("geo.line", [], (root, exports) => {
const { base } = root;
class Line {
export class Line {
constructor(p1, p2, key) {
if (!key) key = [p1.key, p2.key].join(';');
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);
}
function newOrderedLine(p1, p2, key) {
export function newOrderedLine(p1, p2, 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 */
"use strict";
// 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;
import { base } from './base.js';
import { newPoint } from './point.js';
/**
* emit each element in an array based on
@ -21,21 +9,21 @@ const DEG2RAD = Math.PI / 180;
* elements with { first, last } points and
* may be open polys, unlike poly2polyEmit
*/
function tip2tipEmit(array, startPoint, emitter) {
export function tip2tipEmit(array, startPoint, emitter) {
let mindist, dist, found, count = 0;
for (;;) {
found = null;
mindist = Infinity;
array.forEach(function(el) {
array.forEach(function (el) {
if (el.delete) return;
dist = startPoint.distTo2D(el.first);
if (dist < mindist) {
found = {el:el, first:el.first, last:el.last};
found = { el: el, first: el.first, last: el.last };
mindist = dist;
}
dist = startPoint.distTo2D(el.last);
if (dist < mindist) {
found = {el:el, first:el.last, last:el.first};
found = { el: el, first: el.last, last: el.first };
mindist = dist;
}
});
@ -56,7 +44,7 @@ function tip2tipEmit(array, startPoint, emitter) {
* to be more like outputOrderClosest() and have the option to account for
* depth in determining distance
*/
function poly2polyEmit(array, startPoint, emitter, opt = {}) {
export function poly2polyEmit(array, startPoint, emitter, opt = {}) {
let marker = opt.mark || 'delete';
let mindist, dist, found, count = 0;
for (;;) {
@ -74,21 +62,21 @@ function poly2polyEmit(array, startPoint, emitter, opt = {}) {
}
if (d2l < mindist && d2l < d2f && opt.swapdir !== false) {
poly.reverse();
found = {poly:poly, index:0, point:poly.first()};
found = { poly: poly, index: 0, point: poly.first() };
mindist = d2l;
} else if (d2f < mindist) {
found = {poly:poly, index:0, point:poly.first()};
found = { poly: poly, index: 0, point: poly.first() };
mindist = d2f;
}
continue;
}
let area = poly.open ? 1 : poly.area();
poly.forEachPoint(function(point, index) {
poly.forEachPoint(function (point, index) {
dist = opt.weight ?
startPoint.distTo3D(point) * area * area :
startPoint.distTo2D(point);
if (dist < mindist) {
found = {poly:poly, index:index, point:point};
found = { poly: poly, index: index, point: point };
mindist = dist;
}
});
@ -102,23 +90,23 @@ function poly2polyEmit(array, startPoint, emitter, opt = {}) {
// undo delete marks
if (opt.perm !== true) {
array.forEach(function(poly) { poly[marker] = false });
array.forEach(function (poly) { poly[marker] = false });
}
return startPoint;
}
function calc_normal(p1, p2) {
export function calc_normal(p1, p2) {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let len = Math.sqrt(dx * dx + dy * dy);
let mn = (1 / len);
dx *= 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;
if (start) {
dx = n2.dx * off;
@ -130,7 +118,7 @@ function end_vertex(n1, n2, off, start) {
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;
r = 1 + (n1.dx * n2.dx + n1.dy * n2.dy);
q = off / r;
@ -148,7 +136,7 @@ function calc_vertex(n1, n2, off, vp) {
return { dx, dy, vp: vp || n1.p2, io, vl };
}
function v2pl(rec) {
export function v2pl(rec) {
let p = rec.vp.clone();
p.x += rec.dx;
p.y += rec.dy;
@ -156,7 +144,7 @@ function v2pl(rec) {
return p;
}
function v2pr(rec) {
export function v2pr(rec) {
let p = rec.vp.clone();
p.x -= rec.dx;
p.y -= rec.dy;
@ -164,17 +152,17 @@ function v2pr(rec) {
return p;
}
function pointsToPath(points, offset, open, miter = 1.5) {
export function pointsToPath(points, offset, open, miter = 1.5) {
const absoff = Math.abs(offset);
// calculate segment normals which are used to calculate vertex normals
// next segment info is associated with the current point
const nupoints = [];
const length = points.length;
if (length === 2 && points[0].isEqual(points[1])) {
return { };
return {};
}
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 p2 = points[(i + 1) % length];
p1.normal = calc_normal(p1, p2);
@ -191,7 +179,7 @@ function pointsToPath(points, offset, open, miter = 1.5) {
nupoints.push(p1);
}
if (nupoints.length === 1) {
console.log({points, nupoints});
console.log({ points, nupoints });
}
// when points are dropped, we need the new array
points = nupoints;
@ -204,10 +192,10 @@ function pointsToPath(points, offset, open, miter = 1.5) {
// calculate vertex normals from segments normals
// vertex info is associated with the origin point
let fl, fr;
for (let i=0, l=points.length; i<l; i++) {
let n1 = points[(i+l-1)%l].normal;
let n2 = points[(i+l)%l].normal;
let vn = open && (i === 0 || i === l-1) ?
for (let i = 0, l = points.length; i < l; i++) {
let n1 = points[(i + l - 1) % l].normal;
let n2 = points[(i + l) % l].normal;
let vn = open && (i === 0 || i === l - 1) ?
end_vertex(n1, n2, offset, i === 0) :
calc_vertex(n1, n2, offset);
let { p1, p2 } = n2;
@ -229,7 +217,7 @@ function pointsToPath(points, offset, open, miter = 1.5) {
// shorten each leg and insert new point
let delta = 0.1;
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 sn2 = np2.normal = p1.normal;
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 };
}
function pathTo3D(path, height, z) {
export function pathTo3D(path, height, z) {
const { faces, normals, left, right, open } = path;
const out = [];
const nrm = [];
@ -356,14 +344,14 @@ function pathTo3D(path, height, z) {
}
nrm.appendAll(normals);
// reverse normals to match faces, but underside so reverse Z as well
for (let i=normals.length-1; i>0; i-=3) {
nrm.push(normals[i-2]);
nrm.push(normals[i-1]);
nrm.push(-normals[i-0]);
for (let i = normals.length - 1; i > 0; i -= 3) {
nrm.push(normals[i - 2]);
nrm.push(normals[i - 1]);
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 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(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);
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);
}
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 p1 = right[(i+1)%l];
let p1 = right[(i + 1) % l];
out.push(p0.x, p0.y, p0.z + height);
out.push(p1.x, p1.y, p1.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(p1.x, p1.y, p1.z + height);
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);
}
if (open) {
// begin cap
@ -406,12 +394,12 @@ function pathTo3D(path, height, z) {
out.push(r0.x, r0.y, r0.z - height);
out.push(l0.x, l0.y, l0.z + height);
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);
// end cap
let le = left.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(re.x, re.y, re.z - height);
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);
}
return { faces: out, normals: nrm };
@ -434,7 +422,7 @@ function pathTo3D(path, height, z) {
// produces indexed geometry which isn't ideal for rendering because
// 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;
const profileGeometry = new THREE.ShapeGeometry(shape);
@ -451,9 +439,9 @@ function shapeToPath(shape, points, closed) {
let hA = halfAngle;
let tA = v2.angle() + Math.PI * .5;
if (!closed){
if (i == 0 || i == points.length - 1) {hA = Math.PI * .5;}
if (i == points.length - 1) {tA = v1.angle() - Math.PI * .5;}
if (!closed) {
if (i == 0 || i == points.length - 1) { hA = Math.PI * .5; }
if (i == points.length - 1) { tA = v1.angle() - Math.PI * .5; }
}
const shift = Math.tan(hA - Math.PI * .5);
@ -488,7 +476,7 @@ function shapeToPath(shape, points, closed) {
}
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 j = 0; j < profile.count; j++) {
@ -524,7 +512,7 @@ function shapeToPath(shape, points, closed) {
index.push(p8, p7, p5);
}
return {index, faces};
return { index, faces };
}
/**
@ -539,18 +527,15 @@ function shapeToPath(shape, points, closed) {
*
* @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;
// @type {Point}
if (end.x === undefined && end.x === undefined && center === undefined) {
// bambu generates loop z or wipe loop arcs in place
// console.log({ skip_empty_arc: rec });
return;
}
if(arcdivs <= 2){
@ -560,14 +545,13 @@ function arcToPath( start, end,arcdivs=24,opts) {
if (center) {
// center = center.add(start);
center.r = center.distTo2D(start);
} else if (radius !== undefined) {
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 pr2;
if (Math.abs(dst - radius) < 0.001) {
// 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 {
// triangulate
pr2 = base.util.center2pr(start, end, radius, clockwise);
@ -576,7 +560,7 @@ function arcToPath( start, end,arcdivs=24,opts) {
center.y = pr2.y;
center.r = radius;
} else {
console.log({malfomed_arc: {radius,center, clockwise, start, end}});
console.log({ malfomed_arc: { radius, center, clockwise, start, end } });
}
//deltas
@ -601,9 +585,9 @@ function arcToPath( start, end,arcdivs=24,opts) {
let dd = Math.sqrt(dx * dx + dy * dy);
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)) {
console.log({malfomed_arc: {radius, clockwise, start, end}});
console.log({ malfomed_arc: { radius, clockwise, start, end } });
}
arr.push(newPoint(
center.x + Math.cos(rot) * center.r,
@ -618,7 +602,7 @@ function arcToPath( start, end,arcdivs=24,opts) {
return arr
}
class FloatPacker {
export class FloatPacker {
constructor(size, factor) {
this.size = size;
this.factor = Math.min(factor || 1.2, 1.1);
@ -637,7 +621,7 @@ class FloatPacker {
this.array = nuarray;
this.size = nusize;
}
for (let i=0; i<args; i++) {
for (let i = 0; i < args; i++) {
array[this.pos++] = arguments[i];
}
}
@ -651,15 +635,11 @@ class FloatPacker {
}
}
base.paths = {
poly2polyEmit,
export const paths = {
tip2tipEmit,
poly2polyEmit,
shapeToPath,
pointsToPath,
pathTo3D,
arcToPath,
vertexNormal: calc_vertex,
segmentNormal: calc_normal
};
});
FloatPacker
}

View file

@ -1,13 +1,10 @@
/** 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
gapp.register("geo.point", [], (root, exports) => {
const { base } = root;
const { util, config, key } = base;
const { round } = util;
const { Vector3 } = THREE;
class Point {
constructor(x = 0, y = 0, z = 0, key) {
@ -42,7 +39,7 @@ class Point {
}
toVector3() {
return new THREE.Vector3(this.x, this.y, this.z);
return new Vector3(this.x, this.y, this.z);
}
set(x, y, z) {
@ -126,11 +123,11 @@ class Point {
}
slopeTo(p) {
return base.newSlope(this, p);
return newSlope(this, p);
}
lineTo(p, k) {
return base.newLine(this, p, k);
return newLine(this, p, k);
}
isNear(p, dist) {
@ -296,7 +293,7 @@ class Point {
np2 = newPoint(p2.x - oy, p2.y + ox, p2.z, key.NONE);
np1.op = p1;
np2.op = p2;
return base.newLine(np1, np2, key.NONE);
return newLine(np1, np2, key.NONE);
}
offset(x, y, z) {
@ -652,10 +649,8 @@ function pointFromClipper(cp, z) {
return newPoint(cp.X / config.clipper, cp.Y / config.clipper, z);
}
gapp.overlay(base, {
export {
Point,
newPoint,
pointFromClipper
});
});
};

View file

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

View file

@ -2,22 +2,17 @@
"use strict";
// dep: geo.base
// dep: geo.paths
// dep: ext.clip2
// dep: ext.earcut
// dep: geo.point
// dep: geo.bounds
// dep: geo.polygons
gapp.register("geo.polygon", [], (root, exports) => {
import { base, config, earcut, util } from './base.js';
import { ClipperLib } from '../ext/clip2.esm.js';
import { newBounds } from './bounds.js';
import { paths } from './paths.js';
import { newPoint, pointFromClipper } from './point.js';
import { polygons as POLY } from './polygons.js';
const { base } = root;
const { config, util, polygons, newBounds, newPoint } = base;
const { Vector3 } = THREE;
const POLY = polygons,
XAXIS = new THREE.Vector3(1,0,0),
let XAXIS = new Vector3(1,0,0),
DEG2RAD = Math.PI / 180,
ClipperLib = self.ClipperLib,
Clipper = ClipperLib.Clipper,
ClipType = ClipperLib.ClipType,
PolyType = ClipperLib.PolyType,
@ -39,7 +34,7 @@ const POLY = polygons,
let seqid = Math.round(Math.random() * 0xffffffff);
class Polygon {
export class Polygon {
constructor(points) {
this.id = seqid++; // polygon unique id
this.open = false;
@ -76,11 +71,11 @@ class Polygon {
}
toPath2D(offset) {
return base.paths.pointsToPath(this.points, offset, this.open);
return paths.pointsToPath(this.points, offset, this.open);
}
toPath3D(offset, height, z) {
return base.paths.pathTo3D(this.toPath2D(offset), height, z);
return paths.pathTo3D(this.toPath2D(offset), height, z);
}
toString(verbose) {
@ -235,7 +230,7 @@ class Polygon {
}
// perform earcut()
let cut = self.earcut(out, holes, 3);
let cut = earcut(out, holes, 3);
let ret = [];
// preserve swaps in new polys
@ -467,7 +462,7 @@ class Polygon {
return polys
.filter(poly => poly.length > 1)
.map(poly => {
let np = base.newPolygon().setOpen();
let np = newPolygon().setOpen();
for (let p of poly) {
np.push(p);
}
@ -1143,7 +1138,7 @@ class Polygon {
applyRotations() {
for (let point of this.points) {
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);
point.x = p2.x;
point.y = p2.y;
@ -1205,15 +1200,76 @@ class Polygon {
return false;
}
/**
* calls function for each point in this polygon
* @param {Function} fn to call for each point
* @param {boolean} [close=true] whether to close the loop (last point to first)
* @param {number} [start=0] starting index
* Ease down along the polygonal path.
*
* 1. Travel from fromPoint to closest point on polygon, to rampZ above that that point,
* 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,
points = this.points,
length = points.length,
@ -2003,7 +2059,7 @@ class Polygon {
return res.map(array => {
let poly = newPolygon();
for (let pt of array) {
poly.push(base.pointFromClipper(pt, z));
poly.push(pointFromClipper(pt, z));
}
return poly;
});
@ -2318,8 +2374,7 @@ class Polygon {
}
// use Slope.angleDiff() then re-test path mitering / rendering
function slopeDiff(s1, s2) {
export function slopeDiff(s1, s2) {
const n1 = s1.angle;
const n2 = s2.angle;
let diff = n2 - n1;
@ -2328,28 +2383,17 @@ function slopeDiff(s1, s2) {
return Math.abs(diff);
}
function fromClipperPath(path, z) {
export function fromClipperPath(path, z) {
let poly = newPolygon(),
i = 0,
l = path.length;
while (i < l) {
// poly.push(newPoint(null,null,z,null,path[i++]));
poly.push(base.pointFromClipper(path[i++], z));
poly.push(pointFromClipper(path[i++], z));
}
return poly;
}
function newPolygon(points) {
export function newPolygon(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";
// dep: geo.base
// dep: geo.point
// use: ext.clip2
// use: geo.slope
// use: geo.polygon
gapp.register("geo.polygons", [], (root, exports) => {
import { base, util, config } from './base.js';
import { newPoint, pointFromClipper } from './point.js';
import { newPolygon } from './polygon.js';
import { newSlope } from './slope.js';
import { paths } from './paths.js';
import { ClipperLib } from '../ext/clip2.esm.js';
const { base } = root;
const { util, paths, config, newPoint } = base;
const { sqr, numOrDefault } = util;
const geo = base;
const { numOrDefault } = util;
const DEG2RAD = Math.PI / 180,
SQRT = Math.sqrt,
SQR = util.sqr,
ABS = Math.abs;
const ClipperLib = self.ClipperLib,
Clipper = ClipperLib.Clipper,
ClipType = ClipperLib.ClipType,
PolyType = ClipperLib.PolyType,
PolyFillType = ClipperLib.PolyFillType,
CleanPolygon = Clipper.CleanPolygon,
const {
Clipper,
ClipType,
PolyType,
PolyFillType,
EndType,
JoinType,
PolyTree,
ClipperOffset
} = ClipperLib;
const CleanPolygon = Clipper.CleanPolygon,
CleanPolygons = Clipper.CleanPolygons,
SimplifyPolygons = Clipper.SimplifyPolygons,
FillNonZero = PolyFillType.pftNonZero,
FillEvenOdd = PolyFillType.pftEvenOdd,
PathSubject = PolyType.ptSubject,
PathClip = PolyType.ptClip,
EndType = ClipperLib.EndType,
JoinType = ClipperLib.JoinType,
PolyTree = ClipperLib.PolyTree,
ClipXOR = ClipType.ctXor,
ClipDiff = ClipType.ctDifference,
ClipUnion = ClipType.ctUnion,
ClipIntersect = ClipType.ctIntersection,
ClipperOffset = ClipperLib.ClipperOffset
;
ClipIntersect = ClipType.ctIntersection;
const POLYS = base.polygons = {
const POLYS = {
clearInner,
rayIntersect,
alignWindings,
@ -73,14 +73,16 @@ const POLYS = base.polygons = {
fingerprint
};
function outer(polys) {
export { POLYS };
export function outer(polys) {
for (let p of polys) {
p.inner = undefined;
}
return polys;
}
function inner(polys) {
export function inner(polys) {
const ret = [];
for (let p of polys) {
if (p.inner) {
@ -90,7 +92,7 @@ function inner(polys) {
return ret;
}
function length(polys) {
export function length(polys) {
let length = 0;
for (let p of polys) {
length += p.deepLength;
@ -98,20 +100,20 @@ function length(polys) {
return length;
}
function setZ(polys, z) {
export function setZ(polys, z) {
for (let poly of polys) {
poly.setZ(z);
}
return polys;
}
function clearInner(polys) {
export function clearInner(polys) {
for (let p of polys) {
p.clearInner();
}
}
function toClipper(polys = []) {
export function toClipper(polys = []) {
let out = [];
for (let poly of polys) {
poly.toClipper(out);
@ -119,16 +121,16 @@ function toClipper(polys = []) {
return out;
}
function fromClipperNode(tnode, z) {
let poly = base.newPolygon();
export function fromClipperNode(tnode, z) {
let poly = newPolygon();
for (let point of tnode.m_polygon) {
poly.push(base.pointFromClipper(point, z));
poly.push(pointFromClipper(point, z));
}
poly.open = tnode.IsOpen;
return poly;
};
}
function fromClipperTree(tnode, z, tops, parent, minarea) {
export function fromClipperTree(tnode, z, tops, parent, minarea) {
let poly,
polys = tops || [],
min = numOrDefault(minarea, 0.1);
@ -150,9 +152,9 @@ function fromClipperTree(tnode, z, tops, parent, minarea) {
}
return polys;
};
}
function fromClipperTreeUnion(tnode, z, minarea, tops, parent) {
export function fromClipperTreeUnion(tnode, z, minarea, tops, parent) {
let polys = tops || [], poly;
for (let child of tnode.m_Childs) {
@ -171,9 +173,9 @@ function fromClipperTreeUnion(tnode, z, minarea, tops, parent) {
}
return polys;
};
}
function cleanClipperTree(tree) {
export function cleanClipperTree(tree) {
if (tree.m_Childs)
for (let child of tree.m_Childs) {
child.m_polygon = CleanPolygon(child.m_polygon, config.clipperClean);
@ -181,9 +183,9 @@ function cleanClipperTree(tree) {
}
return tree;
};
}
function filter(array, output, fn) {
export function filter(array, output, fn) {
for (let poly of array) {
poly = fn(poly);
if (poly) {
@ -197,14 +199,14 @@ function filter(array, output, fn) {
return output;
}
function points(polys) {
export function points(polys) {
return polys.length ? polys.map(p => p.deepLength).reduce((a,v) => a+v) : 0;
}
/**
* redo nesting of polygons that might already have inners
*/
function renest(polygons, deep) {
export function renest(polygons, deep) {
return nest(flatten(polygons, [], true), deep);
}
@ -220,7 +222,7 @@ function renest(polygons, deep) {
* @param {boolean} opentop prevent open polygons from having inners
* @returns {Polygon[]} top level parent polygons
*/
function nest(polygons, deep, opentop) {
export function nest(polygons, deep, opentop) {
if (!polygons) {
return polygons;
}
@ -284,7 +286,7 @@ function nest(polygons, deep, opentop) {
* @param {boolean} CW
* @param {boolean} [recurse]
*/
function setWinding(array, CW, recurse) {
export function setWinding(array, CW, recurse) {
if (!array) return;
let poly, i = 0;
while (i < array.length) {
@ -302,7 +304,7 @@ function setWinding(array, CW, recurse) {
* @param {Polygon[]} polys
* @return {boolean} true if aligned clockwise
*/
function alignWindings(polys) {
export function alignWindings(polys) {
let len = polys.length,
fwd = 0,
pts = 0,
@ -323,21 +325,14 @@ function alignWindings(polys) {
return setCW;
}
function setContains(setA, poly) {
export function setContains(setA, poly) {
for (let i=0; i<setA.length; i++) {
if (setA[i].contains(poly)) return true;
}
return false;
}
/**
* 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) {
export function flatten(polys, to, crush) {
to = to || [];
for (let poly of polys) {
poly.flattenTo(to);
@ -358,7 +353,7 @@ function flatten(polys, to, crush) {
* @param {number} [minArea]
* @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),
out = [];
@ -440,63 +435,63 @@ function subtract(setA, setB, outA, outB, z, minArea, opt = {}) {
* @param {Polygon[]} polys
* @returns {Polygon[]}
*/
function union(polys, minarea, all, opt = {}) {
if (polys.length < 2) return polys;
let lpre = length(polys);
export function union(polys, minarea, all, opt = {}) {
if (polys.length < 2) return polys;
let lpre = length(polys);
if (opt.wasm && geo.wasm) {
let min = minarea ?? 0.01;
// let deepLength = polys.map(p => p.deepLength).reduce((a,v) => a+v);
// if (deepLength < 15000)
try {
let out = geo.wasm.js.union(polys, polys[0].getZ()).filter(p => p.area() > min);
opt.changes = length(out) - lpre;
return out;
} catch (e) {
console.log({union_fail: polys, minarea, all});
}
}
if (opt.wasm && geo.wasm) {
let min = minarea ?? 0.01;
// let deepLength = polys.map(p => p.deepLength).reduce((a,v) => a+v);
// if (deepLength < 15000)
try {
let out = geo.wasm.js.union(polys, polys[0].getZ()).filter(p => p.area() > min);
opt.changes = length(out) - lpre;
return out;
} catch (e) {
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++) {
if (!out[i]) continue;
for (j=i+1; j<out.length; j++) {
if (!out[j]) continue;
union = out[i].union(out[j], minarea, all);
if (union && union.length) {
if (opt.onmerge) {
a = out[i];
b = out[j];
}
out[i] = null;
out[j] = null;
if (all) {
out.appendAll(union);
} else {
out.push(union);
}
if (opt.onmerge) {
opt.onmerge(a, b, union);
}
continue outer;
}
}
}
outer: for (i=0; i<out.length; i++) {
if (!out[i]) continue;
for (j=i+1; j<out.length; j++) {
if (!out[j]) continue;
union = out[i].union(out[j], minarea, all);
if (union && union.length) {
if (opt.onmerge) {
a = out[i];
b = out[j];
}
out[i] = null;
out[j] = null;
if (all) {
out.appendAll(union);
} else {
out.push(union);
}
if (opt.onmerge) {
opt.onmerge(a, b, union);
}
continue outer;
}
}
}
for (i=0; i<out.length; i++) {
if (out[i]) uset.push(out[i]);
}
for (i=0; i<out.length; i++) {
if (out[i]) uset.push(out[i]);
}
opt.changes = length(uset) - lpre;
return uset;
}
opt.changes = length(uset) - lpre;
return uset;
}
/**
* @param {Polygon} poly clipping mask
* @returns {?Polygon[]}
*/
function diff(setA, setB, z) {
export function diff(setA, setB, z) {
let clip = new Clipper(),
tree = new PolyTree(),
sp1 = toClipper(setA),
@ -516,7 +511,7 @@ function diff(setA, setB, z) {
* @param {Polygon} poly clipping mask
* @returns {?Polygon[]}
*/
function xor(set, z) {
export function xor(set, z) {
z = z || set[0].getZ();
outer: for (;;) {
// sort largest to smallest area
@ -559,7 +554,7 @@ function diff(setA, setB, z) {
* @param {Polygon[]} setB mask set
* @returns {Polygon[]}
*/
function trimTo(setA, setB) {
export function trimTo(setA, setB) {
// handle null/empty slices
if (setA === setB || setA === null || setB === null) return null;
@ -573,7 +568,7 @@ function trimTo(setA, setB) {
return out;
}
function sumCirc(polys) {
export function sumCirc(polys) {
let sum = 0.0;
polys.forEach(function(poly) {
sum += poly.circularityDeep();
@ -591,7 +586,7 @@ function sumCirc(polys) {
* @param {Function} [collector] receives output of each pass
* @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], {
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
* 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) : [];
if (open.length) {
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
@ -706,7 +701,7 @@ function offset(polys, dist, opts = {}) {
* as performing subtractive analysis between initial layer shell (ref)
* 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 layers = [];
let ref = polys;
@ -749,7 +744,7 @@ function inset(polys, dist, count, z, wasm) {
* @param {number} [maxLen]
* @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;
let i = 1,
@ -771,7 +766,7 @@ function fillArea(polys, angle, spacing, output, minLen, maxLen) {
while (angle > 90) angle -= 180;
// X,Y ray slope derived from angle
raySlope = base.newSlope(0,0,
raySlope = newSlope(0,0,
Math.cos(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 (maxlen && plen > maxlen) continue;
}
let p1 = base.pointFromClipper(poly.m_polygon[0], zpos);
let p2 = base.pointFromClipper(poly.m_polygon[1], zpos);
let p1 = pointFromClipper(poly.m_polygon[0], zpos);
let p2 = pointFromClipper(poly.m_polygon[1], zpos);
let od = rayint.origin.distToLineNew(p1,p2) / spacing;
lines.push([p1, p2, od]);
}
@ -875,7 +870,7 @@ function fillArea(polys, angle, spacing, output, minLen, maxLen) {
* @param {boolean} [for_fill]
* @returns {Point[]}
*/
function rayIntersect(start, slope, polygons, for_fill) {
export function rayIntersect(start, slope, polygons, for_fill) {
let i = 0,
flat = [],
points = [],
@ -1004,11 +999,11 @@ function rayIntersect(start, slope, polygons, for_fill) {
return points;
}
function pd(a,b) {
export function pd(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 => {
return {
l: p.length,
@ -1037,7 +1032,7 @@ function fingerprint(polys) {
}
// compare fingerprint arrays
function fingerprintCompare(a, b) {
export function fingerprintCompare(a, b) {
// true if array is the same object
if (a === b) {
return true;
@ -1079,7 +1074,7 @@ function fingerprintCompare(a, b) {
// plan a route through an array of polygon center points
// starting with the polygon center closest to "start"
function route(polys, start) {
export function route(polys, start) {
let centers = [];
let first, minDist = Infinity;
for (let poly of polys) {
@ -1116,4 +1111,4 @@ function route(polys, start) {
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 */
"use strict";
/**
* basic slice and line connection. In future, replace kiri's fdm and cam slicers
* 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;
const { config, util, polygons } = base
const { newOrderedLine, newPolygon, newPoint } = base;
const POLY = base.polygons;
import { base } from './base.js';
import { newOrderedLine } from './line.js';
import { newPolygon } from '../geo/polygon.js';
import { polygons } from '../geo/polygons.js';
import { newPoint } from '../geo/point.js';
import { config } from '../geo/base.js';
function dval(v, dv) {
return v !== undefined ? v : dv;
@ -32,7 +25,7 @@ function dval(v, dv) {
* @param {Point[]} points vertex array
* @param {Object} options slicing parameters
*/
async function slice(points, options = {}) {
export async function slice(points, options = {}) {
let zMin = options.zMin || 0,
zMax = options.zMax || 0,
zInc = options.zInc || 0,
@ -72,7 +65,6 @@ async function slice(points, options = {}) {
for (i = 0; i < points.length; i++) {
points[i] = points[i].round(3);
}
}
// 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) {
// detect faces co-planar with Z and sum the enclosed area
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]) {
zFlat[zkey] = area;
} else {
@ -322,7 +314,7 @@ function makeZLine(phash, p1, p2, coplanar, edge) {
*
* @param {number} z
*/
async function sliceZ(z, points, options = {}) {
export async function sliceZ(z, points, options = {}) {
if (Array.isArray(z)) {
return Promise.all(z.map(z => sliceZ(z, points, options)));
}
@ -388,14 +380,14 @@ async function sliceZ(z, points, options = {}) {
if (groupFn) {
let groups = groupFn(lines, z, options);
if (options.xor) {
groups = POLY.xor(groups);
groups = polygons.xor(groups);
}
if (options.union) {
let points = groups.map(p => p.length);
if (points.length > 1) points = points.reduce((a,b) => a + b);
// simplistic healing of non-manifold meshes
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
// 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
@ -409,14 +401,14 @@ async function sliceZ(z, points, options = {}) {
}
// track total poly length changes to determine if healed
rval.changes = opt.changes;
groups = POLY.flatten(union, null, true);
groups = polygons.flatten(union, null, true);
}
rval.groups = groups;
}
// look for driver-specific slice post-processor
if (options.post) {
let fn = base.slicePost[options.post];
let fn = slicer.slicePost[options.post];
if (fn) fn(rval, options);
}
@ -458,7 +450,7 @@ async function sliceZ(z, points, options = {}) {
* @param {number} [index]
* @returns {Array}
*/
function sliceConnect(input, z, opt = {}) {
export function sliceConnect(input, z, opt = {}) {
let { debug, both } = opt;
if (both) {
@ -782,7 +774,7 @@ function sliceConnect(input, z, opt = {}) {
* @param {Line[]} lines
* @returns {Line[]}
*/
function removeDuplicateLines(lines) {
export function removeDuplicateLines(lines) {
let output = [],
tmplines = [],
points = [],
@ -873,12 +865,11 @@ function removeDuplicateLines(lines) {
return output;
}
gapp.overlay(base, {
export const slicer = {
slice,
sliceZ,
slicePost: {},
sliceDedup: removeDuplicateLines,
sliceConnect
});
}
});

View file

@ -2,19 +2,13 @@
"use strict";
// dep: geo.base
// use: geo.line
// use: geo.point
gapp.register("geo.slope", [], (root, exports) => {
const { base } = root;
const { config } = base;
import { config } from './base.js';
const ABS = Math.abs,
DEG2RAD = Math.PI / 180,
RAD2DEG = 180 / Math.PI;
class Slope {
export class Slope {
constructor(p1, p2, dx, dy) {
this.dx = p1 && p2 ? p2.x - p1.x : dx;
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);
}
function newSlope(p1, p2, dx, dy) {
export function newSlope(p1, p2, dx, dy) {
return new Slope(p1, p2, dx, dy);
}
function newSlopeFromAngle(angle) {
export function newSlopeFromAngle(angle) {
return newSlope(0,0,
Math.cos(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 */
"use strict";
import { base } from '../geo/base.js';
// dep: geo.base
// dep: ext.clip2
gapp.register("geo.wasm", [], (root, exports) => {
const { base } = root;
const { config } = base;
const factor = config.clipper;
const wasm_ctrl = {
export const wasm_ctrl = {
enable,
disable,
count: {
@ -20,8 +16,6 @@ const wasm_ctrl = {
}
};
gapp.overlay(base, { wasm_ctrl });
function log() {
console.log(...arguments);
}
@ -60,7 +54,7 @@ function writePoly(view, poly, inner) {
function readPoly(view, z) {
let points = view.readU16(true);
if (points === 0) return;
let poly = self.base.newPolygon();
let poly = base.newPolygon();
while (points-- > 0) {
poly.add(view.readI32(true)/factor, view.readI32(true)/factor, z || 0);
}
@ -79,7 +73,7 @@ function readPolys(view, z, out = []) {
return out;
}
function polyOffset(polys, offset, z, clean, simple) {
export function polyOffset(polys, offset, z, clean, simple) {
wasm_ctrl.count.offset++;
let wasm = base.wasm,
buffer = wasm.shared,
@ -89,7 +83,7 @@ function polyOffset(polys, offset, z, clean, simple) {
return polyNest(out);
}
function polyUnion(polys, z) {
export function polyUnion(polys, z) {
wasm_ctrl.count.union++;
let wasm = base.wasm,
buffer = wasm.shared,
@ -99,7 +93,7 @@ function polyUnion(polys, z) {
return polyNest(out);
}
function polyDiff(polysA, polysB, z, AB, BA) {
export function polyDiff(polysA, polysB, z, AB, BA) {
wasm_ctrl.count.diff++;
let wasm = base.wasm,
buffer = wasm.shared,
@ -153,7 +147,7 @@ function readString(pos, len) {
return out.join('');
}
function enable() {
export function enable() {
if (base.wasm || base._wasm) {
return;
}
@ -200,11 +194,9 @@ function enable() {
});
}
function disable() {
export function disable() {
if (base.wasm) {
base.wasm.free(base.wasm.shared);
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,236 +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.cylinder_show = function(widget) {
widget.selectFaces(Object.values(widget._cylinders).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);
}
});
})
};
CAM.cylinderShow = function(onProgress,onDone){
kiri.client.sync();
const settings = api.conf.get();
}
CAM.cylinderToggle = (widget, face, onDone) => {
let cyls = widget._cylinders = widget._cylinders || {};
for (let [root, faces] of Object.entries(cyls)) {
if (faces.contains(face)) { // if a face group contains the selected face
// delete this face group
delete cyls[root];
CAM.cylinder_show(widget);
onDone(Object.keys(cyls).map(v => parseInt(v)));
return;
}
}
//send
kiri.client.send("cam_cylinder_find", { id: widget.id, face }, ({faces,error}) => {
if (faces?.length) {
cyls[face] = faces;
CAM.cylinder_show(widget);
}
onDone({faces:Object.keys(cyls).map(v => parseInt(v)),error});
});
}
CAM.cylinderClear = (widget) => {
widget.selectFaces([]);
widget._cylinders = {};
}
}
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 }));
}
};
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
} } )));
}
kiri.worker.cam_cylinder_find = async function(data,send){
const { id, face, settings } = data;
const widget = kiri.worker.cache[id];
try{
send.done(CAM.cylinder_poly_find(widget, face));
}catch(error){
send.done({error});
}
}
}
});
});

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 */
"use strict";
// dep: kiri.api
gapp.register("kiri.alerts", [], (root, exports) => {
const { kiri } = root;
const { api } = kiri;
import { api } from './api.js';
let alerts = [];
@ -68,11 +62,14 @@ function update(clear) {
}
}
// extend API
Object.assign(api.alerts, {
export {
hide,
show,
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, MODES, VIEWS, PATHS } 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;

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

@ -0,0 +1,52 @@
import { space } from '../../moto/space.js';
const { BoxGeometry, Matrix4, MeshPhongMaterial, Quaternion, Vector3, Mesh } = THREE;
const boxes = [];
let lastBox;
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 = boxes[name] = new Mesh(
new BoxGeometry(dim.x, dim.y, dim.z),
new MeshPhongMaterial({
transparent: true,
opacity: opt.opacity || 0.5,
color
})
);
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,19 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// use: kiri.api
gapp.register("kiri.client", [], (root, exports) => {
const { kiri } = root;
import { api } from './api.js';
import { noop } from './utils.js';
// this code runs in kiri's main loop
let loc = self.location,
host = loc.hostname,
port = loc.port,
proto = loc.protocol,
debug = self.debug === true,
let debug = self.debug === true,
time = Date.now,
seqid = 1,
syncd = {},
running = {},
worker = null,
minions = false,
restarting = false
// occ = new Worker("/kiri/ext/occ-worker.js", {type:"module"}),
;
restarting = false,
workpath,
poolpath;
/**
* @param {Function} fn name of function in kiri.worker
@ -52,13 +44,23 @@ function send(fn, data, onreply, zerocopy) {
}
// code is running in the browser / client context
const client = exports({
export const client = {
send: send,
setWorkPath(path) {
workpath = path;
return client;
},
setPoolPath(path) {
poolpath = path;
return client;
},
pool: {
start() {
minions = true;
send("pool_start", {}, noop);
send("pool_start", { url: poolpath }, noop);
},
stop() {
@ -75,8 +77,16 @@ const client = exports({
const blob = new Blob([ work ], { type: 'application/javascript' });
return new Worker(URL.createObjectURL(blob));
} else {
let _ = debug ? '_' : '';
return new Worker(`/code/kiri_work.js?${_}${gapp.version}`);
let worker = new Worker(workpath || api.const.PATHS.work, { type: 'module' });
worker.onerror = (error) => {
console.log({ WORKER_ERROR: error });
error.preventDefault();
};
worker.onmessageerror = (error) => {
console.log({ WORKER_MESSAGE_ERROR: error });
error.preventDefault();
};
return worker;
}
},
@ -89,6 +99,10 @@ const client = exports({
return current > 0;
},
start() {
client.restart();
},
restart() {
// prevent re-entry from cancel callback
if (restarting) {
@ -112,7 +126,7 @@ const client = exports({
running = {};
worker = client.newWorker();
client.onmessage = worker.onmessage = function(e) {
client.onmessage = worker.onmessage = (e) => {
let now = time(),
reply = e.data,
record = running[reply.seq],
@ -161,7 +175,7 @@ const client = exports({
// widget sync
sync(widgets) {
if (!widgets) {
widgets = kiri.api.widgets.all();
widgets = api.widgets.all();
}
// sync any widget that has changed
for (let widget of widgets.filter(w => w.modified || !syncd[w.id])) {
@ -245,7 +259,7 @@ const client = exports({
ondone(null, reply.error);
}
if (reply.debug) {
kiri.api.event.emit("export.debug", reply.debug);
api.event.emit("export.debug", reply.debug);
}
});
},
@ -334,9 +348,8 @@ const client = exports({
// console.log({ clearCache_reply: reply });
});
}
});
};
// start worker
client.restart();
self.kiri_client = client;
});
export default client;

View file

@ -1,32 +1,14 @@
/** 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 freeMem = true;
const codec = exports({
undef: undefined,
encode: encode,
decode: decode,
registerDecoder: registerDecoder,
allocFloat32Array: allocFloat32Array,
encodePointArray,
decodePointArray,
encodePointArray2D,
decodePointArray2D,
toCodable
});
const TYPE = {
WIDGET: 100,
SLICE: 200,
@ -210,7 +192,7 @@ function decodePointArray(array) {
const points = new Array(length / 3);
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;
@ -225,7 +207,7 @@ function decodePointArray2D(array, z, fn) {
for (let vid=0, pid=0; vid < length; ) {
points[pid++] = fn ?
fn(array[vid++], array[vid++]) :
base.newPoint(array[vid++], array[vid++], z);
newPoint(array[vid++], array[vid++], z);
}
return points;
@ -233,7 +215,7 @@ function decodePointArray2D(array, z, fn) {
// ----- Widget Codec -----
kiri.Widget.prototype.encode = function(state) {
Widget.prototype.encode = function(state) {
const json = state._json_;
const geo = this.getGeoVertices();
const coded = {
@ -252,7 +234,7 @@ registerDecoder(TYPE.WIDGET, function(v, state) {
const id = v.id,
group = v.group || id,
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.saved = Date.now();
@ -266,7 +248,7 @@ registerDecoder(TYPE.WIDGET, function(v, state) {
// ----- Slice Codec -----
kiri.Slice.prototype.encode = function(state) {
Slice.prototype.encode = function(state) {
const rv = {
type: TYPE.SLICE,
z: this.z,
@ -279,7 +261,7 @@ kiri.Slice.prototype.encode = function(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.layers = decode(v.layers, state)
@ -289,7 +271,7 @@ registerDecoder(TYPE.SLICE, function(v, state) {
// ----- Slice.Top Codec -----
kiri.Top.prototype.encode = function(state) {
Top.prototype.encode = function(state) {
let obj = {
type: TYPE.TOP,
poly: encode(this.poly, state)
@ -307,7 +289,7 @@ kiri.Top.prototype.encode = function(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) {
// top.gaps = decode(v.gaps, state);
top.last = decode(v.last, state);
@ -320,7 +302,7 @@ registerDecoder(TYPE.TOP, function(v, state) {
// ----- Polygon Codec -----
base.Polygon.prototype.encode = function(state) {
Polygon.prototype.encode = function(state) {
if (!state.poly) state.poly = {};
let cached = state.poly[this.id];
@ -352,10 +334,10 @@ registerDecoder(TYPE.POLY, function(v, state) {
const array = v.array;
const length = array.length;
const poly = base.newPolygon();
const poly = newPolygon();
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;
@ -382,7 +364,7 @@ function encodeLayerPolys(polys, state) {
});
}
kiri.Layers.prototype.encode = function(state) {
Layers.prototype.encode = function(state) {
let zeros = state.zeros;
let enc = {
type: TYPE.LAYERS,
@ -419,7 +401,7 @@ kiri.Layers.prototype.encode = function(state) {
};
registerDecoder(TYPE.LAYERS, function(v, state) {
const render = new kiri.Layers();
const render = new Layers();
const { layers } = v;
for (let i=0; i<layers.length; i++) {
@ -463,4 +445,28 @@ registerDecoder(TYPE.LAYERS, function(v, state) {
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 */
"use strict";
// dep: add.array
// dep: data.local
gapp.register("kiri.conf", (root, exports) => {
const { data } = root;
const { local } = data;
const { clone } = Object;
const CVER = 410;
@ -219,7 +211,7 @@ const renamed = {
outputClockwise: "camConventional"
};
const conf = exports({
export const conf = {
// --------------- helper functions
normalize,
device_from_code,
@ -981,7 +973,7 @@ const conf = exports({
id: genID(),
ver: CVER
}
});
};
const settings = conf.template;
@ -1000,5 +992,3 @@ settings.cdev.LASER = clone(settings.device);
settings.cdev.DRAG = clone(settings.device);
settings.cdev.WJET = clone(settings.device);
settings.cdev.WEDM = clone(settings.device);
});

View file

@ -1,9 +1,5 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
gapp.register("kiri.consts", (root, exports) => {
const COLOR = {
wireframe: 0x444444,
wireframe_opacity: 0.25,
@ -150,16 +146,22 @@ const PMODES = {
TOOLS: 2
};
const PATHS = {
work: "/lib/kiri/run/worker.js",
pool: "/lib/kiri/run/minion.js"
};
const SEED = 'kiri-seed';
exports({
PMODES,
export let consts = {
COLOR,
LISTS,
MODES,
PATHS,
PMODES,
VIEWS,
SEED,
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/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 => {
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 */
// dep: kiri.api
// use: kiri.selection
// use: moto.space
gapp.register("kiri.do", [], (root, events) => {
import { api } from './api.js';
import { space } from '../../moto/space.js';
const { kiri, moto } = root;
const { api } = kiri;
const { space } = moto;
const { event } = api;
let stack = [];
@ -44,7 +39,7 @@ let clear = api.doit.clear = function() {
};
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';
$('undo').disabled = stpos === 0;
$('redo').disabled = stpos == stack.length;
@ -175,5 +170,3 @@ event.on('mouse.drag.done', () => {
});
moved = {x:0, y:0};
});
});

View file

@ -1,23 +1,13 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: ext.md5
// dep: geo.base
// dep: data.local
// dep: kiri.consts
// dep: kiri.api
// dep: kiri.main
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;
import { $ } from '../../moto/webui.js';
import { api } from './api.js';
import { client } from './client.js';
import { local } from '../../data/local.js';
import { util } from '../../geo/base.js';
import { MODES } from './consts.js';
import { LASER as laser_driver } from '../mode/laser/driver.js';
import { SLA as sla_client } from '../mode/sla/client.js';
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);
}
function exportFile(options) {
export function exportFile(options) {
let mode = api.mode.get();
let names = api.widgets.all().map(w => w.meta ? w.meta.file : undefined)
.filter(v => v)
@ -53,7 +43,7 @@ function callExport(callback, mode, names) {
let gcode = [];
let section = [];
let sections = { };
kiri.client.export(api.conf.get(), (line) => {
client.export(api.conf.get(), (line) => {
if (typeof line !== 'string') {
if (line.section) {
sections[line.section] = section = [];
@ -75,7 +65,7 @@ function callExport(callback, mode, names) {
}
function callExportLaser(options, names) {
kiri.client.export(api.conf.get(), (line) => {
client.export(api.conf.get(), (line) => {
// engine export uses lines
// console.log({unexpected_line: line});
}, (output, error) => {
@ -89,14 +79,14 @@ function callExportLaser(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");
}, (output, error) => {
api.show.progress(0);
if (error) {
api.show.alert(error, 5);
} 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 filename = `${fileroot}-${(printSeq.toString().padStart(3,"0"))}`;
const settings = api.conf.get();
const driver = kiri.driver.LASER;
const driver = laser_driver;
function download_svg() {
api.util.download(
@ -207,7 +197,7 @@ function exportGCodeDialog(gcode, sections, info, names) {
ajax.onreadystatechange = function() {
if (ajax.readyState === 4) {
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) {
api.modal.hide();
} else {
@ -278,11 +268,11 @@ function exportGCodeDialog(gcode, sections, info, names) {
)
.then(t => t.text())
.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});
})
.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});
})
.finally(() => {
@ -394,7 +384,7 @@ function exportGCodeDialog(gcode, sections, info, names) {
xhtr.onreadystatechange = function() {
if (xhtr.readyState === 4) {
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) {
let json = js2o(xhtr.responseText);
gridhost_tracker(host,json.key);
@ -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");
}, output => {
api.show.progress(0);
@ -681,7 +671,7 @@ function exportGCodeDialog(gcode, sections, info, names) {
name: `Metadata/top_1.png`,
data: api.view.bambu.s512.png
}];
kiri.client.zip(files, progress => {
client.zip(files, progress => {
api.show.progress(progress.percent/100, "generating 3mf");
}, output => {
api.show.progress(0);
@ -742,5 +732,3 @@ function exportGCodeDialog(gcode, sections, info, names) {
// preview of the generated GCODE (first 64k max)
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 */
"use strict";
// dep: main.kiri
// use: data.index
gapp.register("kiri.files", [], (root, exports) => {
const { kiri } = self;
class Files {
constructor(indexdb) {
let store = this;
@ -176,8 +168,8 @@ function notifyFileListeners(store) {
}
}
kiri.openFiles = function(indexdb) {
export const openFiles = function(indexdb) {
return new Files(indexdb);
};
});
export { saveFileList, notifyFileListeners };

View file

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

View file

@ -1,30 +1,16 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: kiri.api
// dep: kiri.client
// dep: kiri.export
// dep: moto.space
// 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;
import { api } from './api.js';
import { client } from './client.js';
import { codec } from './codec.js';
import { space } from '../../moto/space.js';
import { COLOR, PMODES } from './consts.js';
import { exportFile } from './export.js';
let complete = {};
function prepareSlices(callback, scale = 1, offset = 0) {
const { conf, event, feature, hide, mode, view, platform, show } = api;
const { stacks } = kiri;
const { conf, event, feature, hide, mode, view, platform, show, stacks } = api;
if (view.is_arrange()) {
// in arrange mode, create a screenshot at the start slicing
@ -139,10 +125,34 @@ function prepareSlices(callback, scale = 1, offset = 0) {
}
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 = widgets.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();
// update UI info
if (sliced) {
@ -169,28 +179,55 @@ function prepareSlices(callback, scale = 1, offset = 0) {
// start next widget slice
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) {
let mark = Date.now();
if (lastMsg) {
segtimes[`${widget.id}_${segNumber++}_${lastMsg}`] = mark - startTime;
}
lastMsg = msg;
startTime = mark;
if (reply.update) {
onupdate(reply.update, reply.updateStatus);
}
// 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);
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(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() {
@ -247,9 +284,7 @@ function prepareSlices(callback, scale = 1, offset = 0) {
}
function preparePreview(callback, scale = 1, offset = 0) {
const { conf, event, feature, hide, mode, view, platform, show } = api;
const { stacks } = kiri;
const { conf, event, feature, hide, mode, view, platform, show, stacks } = api;
const widgets = api.widgets.all();
const settings = conf.get();
const { device, process, controller } = settings;
@ -305,7 +340,7 @@ function preparePreview(callback, scale = 1, offset = 0) {
client.prepare(settings, (progress, message, layer) => {
if (layer) {
output.push(kiri.codec.decode(layer));
output.push(codec.decode(layer));
}
if (message && message !== lastMsg) {
const mark = Date.now();
@ -407,7 +442,7 @@ function prepareExport() {
}
api.event.emit("function.export", {mode: settings.mode});
complete.export = true;
kiri.export(...argsave);
exportFile(...argsave);
}
function cancelWorker() {
@ -441,8 +476,12 @@ function parseCode(code, type) {
});
}
function clear_progress() {
complete = {};
}
// extend API (api.function)
const functions = Object.assign(api.function, {
export const functions = {
slice: prepareSlices,
print: preparePreview,
prepare: preparePreview,
@ -451,7 +490,16 @@ const functions = Object.assign(api.function, {
cancel: cancelWorker,
parse: parseCode,
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,
};

2171
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 */
"use strict";
gapp.register("kiri.lang", [], (root, exports) => {
const { kiri } = root;
const LANG = kiri.lang = { current: {} };
const LANG = self.lang = { current: {} };
const KDFL = 'en-us';
let lset = navigator.language.toLocaleLowerCase();
@ -78,4 +73,6 @@ LANG.set = function() {
return undefined;
}
});
export default { LANG };
export { LANG };

View file

@ -1,25 +1,9 @@
/** 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';
/*
* 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 {
export class Layers {
constructor() {
this.init();
}
@ -94,7 +78,7 @@ class Layers {
options.open = true;
const polys = [];
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+1])
.setOpen());
@ -110,11 +94,14 @@ class Layers {
// an open or closed polygon
addPoly(poly, options) {
return this.addPolys([poly], options);
return this.addPolys([ poly ], options);
}
// a polygon rendered as a webgl line
addPolys(polys, options) {
if (!Array.isArray(polys)) {
throw "polys must be an array";
}
if (polys.length === 0) {
return this;
}
@ -186,8 +173,7 @@ class Layers {
p1.setZ(poly.z);
p2.setZ(poly.z);
}
this.addPolys(p1);
this.addPolys(p2);
this.addPolys([ p1, p2 ]);
}
const 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);
}
}
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 */
"use strict";
/**
* adapted from
* http://codeincomplete.com/posts/2011/5/7/bin_packing/
*/
gapp.register("kiri.pack", [], (root, exports) => {
const { kiri } = root;
class Packer {
export class Packer {
constructor(w, h, spacing, opt = {}) {
this.w = w;
this.h = h;
@ -150,7 +139,3 @@ class Packer {
return node;
}
}
kiri.Pack = Packer;
});

View file

@ -1,26 +1,16 @@
/** 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
// dep: moto.webui
// 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;
import { COLOR, MODES } from './consts.js';
import { THREE } from '../../ext/three.js';
const V0 = new THREE.Vector3(0,0,0);
@ -525,7 +515,7 @@ function platformDelete(widget, defer) {
api.event.emit('widget.delete', widget);
return;
}
kiri.client.clear(widget);
api.client.clear(widget);
api.widgets.remove(widget);
api.selection.remove(widget);
Widget.Groups.remove(widget);
@ -719,12 +709,12 @@ function platformLayout() {
mp = [sz.x, sz.y],
ms = [mp[0] / 2, mp[1] / 2],
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) {
ms[0] *= 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++) {
@ -809,7 +799,7 @@ function platformLoadFiles(files, group) {
if (api.conf.get().controller.devel) {
api.event.emit('cam.parse.gerber', { data: text });
} else {
kiri.client.gerber2mesh(text, progress => {
api.client.gerber2mesh(text, progress => {
api.show.progress(progress, "converting");
}, vertices => {
api.show.progress(0);
@ -847,7 +837,7 @@ function platformLoadFiles(files, group) {
api.function.parse(data.textDecode('utf-8'), 'gcode');
load_dec();
} else if (issvg) {
loadSVGDialog(opt => {
loadSVGDialog(opt => {
group = group || [];
let svg = load.SVG.parse(data.textDecode('utf-8'), opt);
let ind = 0;
@ -921,7 +911,7 @@ function fitDeviceToWidgets() {
}
// extend API (api.platform)
const platform = Object.assign(api.platform, {
export const platform = {
fit: fitDeviceToWidgets,
add: platformAdd,
changed: platformChanged,
@ -950,6 +940,4 @@ const platform = Object.assign(api.platform, {
show_volume: space.platform.showVolume,
top_z() { return topZ },
clear() { api.space.clear(); api.space.save(true) }
});
});
};

View file

@ -1,19 +1,13 @@
/** 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 { beltfact } = kiri.consts;
const { beltfact } = consts;
const XAXIS = new THREE.Vector3(1,0,0);
const DEG2RAD = Math.PI / 180;
@ -463,10 +457,10 @@ class Print {
let arcPoints = arcToPath( prevPoint, point, 64,{ clockwise:g2,center}) ?? []
let emit = g2 ? 2 : 3;
// console.log("clone point",structuredClone({point,prevPoint,center,arcPoints,emit}));
// console.log("pointer point",{point,prevPoint,center,arcPoints,emit});
outputPoint(point,prevPoint,emit,{center,arcPoints});
// scope.addOutput(seq, point, emit, pos.F, tool,{center,arcPoints});
}
@ -646,8 +640,8 @@ class Output {
const { type, center, arcPoints } = (options ?? {});
//speed, tool, type, center, arcPoints
this.point = point; // point to emit
this.emit = emit; // emit (feed for printers, power for lasers, cut for cam)
this.point = point;
this.emit = Number(emit); //convert bools into 0/1
this.speed = speed;
this.tool = tool;
this.type = type;
@ -680,9 +674,7 @@ function newPrint(settings, widgets, id) {
return new Print(settings, widgets, id);
};
gapp.overlay(kiri, {
export {
Print,
newPrint
});
});
};

View file

@ -1,35 +1,24 @@
/** 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 XAXIS = new THREE.Vector3(1,0,0);
const DEG2RAD = Math.PI / 180;
exports({
path,
is_dark,
rate_to_color
});
function worker() {
return self.kiri_worker.current;
}
function is_cam() {
return root.worker.print.settings.mode === 'CAM';
return worker().print.settings.mode === 'CAM';
}
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) {
@ -44,17 +33,17 @@ function rate_to_color(rate, max) {
}
};
/**
* Generate visual representation of a gcode program.
* @param {Output[][]} levels - Array of arrays containing `Output` objects.
* @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.
* @returns {Promise.kiri.Layers[]} - Array of layers.
*/
/**
* Generate visual representation of a gcode program.
* @param {Output[][]} levels - Array of arrays containing `Output` objects.
* @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.
* @returns {Promise.kiri.Layers[]} - Array of layers.
*/
async function path(levels, update, opts = {}) {
levels = levels.filter(level => level.length);
if (levels.length === 0) {
self.worker.print.maxSpeed = 0;
worker().print.maxSpeed = 0;
return [];
}
@ -107,10 +96,11 @@ async function path(levels, update, opts = {}) {
// }).reduce((a, v) => Math.max(a, v)) + 1;
// for reporting
self.worker.print.minSpeed = minspd;
self.worker.print.maxSpeed = maxspd;
self.worker.print.thinColor = thin;
self.worker.print.flatColor = flat;
let print = worker().print;
print.minSpeed = minspd;
print.maxSpeed = maxspd;
print.thinColor = thin;
print.flatColor = flat;
let lastTool = null;
let lastEnd = null;
@ -138,7 +128,8 @@ async function path(levels, update, opts = {}) {
const engages = [];
const lasers = [];
const sparks = [];
const output = new kiri.Layers();
const output = new Layers();
layers.push(output);
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)
.applyAxisAngle(XAXIS, point.a * DEG2RAD);
// reconstruct point point for display without A axis
outPoint = base.newPoint(p2.x, p2.y, p2.z);
// let sp = base.newPoint(outPoint.x * 1.1, outPoint.y * 1.1, outPoint.z * 1.1);
outPoint = newPoint(p2.x, p2.y, p2.z);
// let sp = newPoint(outPoint.x * 1.1, outPoint.y * 1.1, outPoint.z * 1.1);
// sparks.push(outPoint, sp);
}
if (out.tool !== lastTool) {
@ -321,8 +312,8 @@ async function path(levels, update, opts = {}) {
.addAreas(heads.map(points => {
const {p1, p2} = points;
const slope = p2.slopeTo(p1);
const s1 = base.newSlopeFromAngle(slope.angle + 20);
const s2 = base.newSlopeFromAngle(slope.angle - 20);
const s1 = newSlopeFromAngle(slope.angle + 20);
const s2 = newSlopeFromAngle(slope.angle - 20);
const p3 = points.p2.projectOnSlope(s1, arrowSize);
const p4 = points.p2.projectOnSlope(s2, arrowSize);
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,8 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: moto.space
// dep: kiri.api
// 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;
import { api } from './api.js';
import { space } from '../../moto/space.js';
import { THREE } from '../../ext/three.js';
const selectedMeshes = [];
@ -296,7 +288,7 @@ function setDisabled(bool) {
}
// extend API (api.selection)
const selection = Object.assign(api.selection, {
export const selection = {
move,
merge,
scale,
@ -320,6 +312,4 @@ const selection = Object.assign(api.selection, {
disable() { setDisabled(true) },
opacity() { api.widgets.opacity(...arguments) },
meshes() { return selectedMeshes.slice() },
});
});
};

View file

@ -1,23 +1,19 @@
/** 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';
// 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 { COLOR } = consts;
const { clone } = Object;
const localFilterKey ='kiri-gcode-filters';
const localFilters = js2o(local.getItem(localFilterKey)) || [];
@ -506,7 +502,7 @@ function settingsExport(opts = {}) {
const widgets = api.widgets.all();
const note = opts.node || 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 setn = Object.clone(settings);
// stuff in legacy annotations for re-import
@ -515,7 +511,7 @@ function settingsExport(opts = {}) {
}
const xprt = {
settings: setn,
version: kiri.version,
version: version,
screen: shot,
space: space.info,
note: note,
@ -600,7 +596,7 @@ function settingsImport(data, ask) {
work.type = 100;
}
}
kiri.codec.decode(data.work).forEach(widget => {
codec.decode(data.work).forEach(widget => {
api.platform.add(widget, 0, true, true);
});
if (data.view) {
@ -760,9 +756,14 @@ function setEnableWASM(bool) {
api.event.emit("set.assembly", bool);
}
// extend API (api.conf)
Object.assign(api.conf, {
dbo: () => { return ls2o('ws-settings') },
// merged api for backward compatibility
const apiSet = {
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,
put: putSettings,
load: loadSettings,
@ -777,25 +778,26 @@ Object.assign(api.conf, {
restore: restoreSettings,
export: settingsExport,
import: settingsImport,
});
// extend API (api.settings)
Object.assign(api.settings, {
get: getSettings,
import: settingsImport,
import_zip: settingsImportZip,
import_url: settingsImportUrl,
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: {
async get() {},
async put() {},
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 */
"use strict";
// dep: geo.base
// dep: geo.polygons
gapp.register("kiri.slice", [], (root, exports) => {
const { base, kiri } = root;
const POLY = base.polygons;
import { Layers } from './layers.js';
import { newPoint } from '../../geo/point.js';
import { polygons as POLY } from '../../geo/polygons.js';
let tracker;
@ -41,7 +35,7 @@ class Slice {
*/
output() {
if (this.layers) return this.layers;
let layers = this.layers = new kiri.Layers();
let layers = this.layers = new Layers();
if (tracker) {
layers.setRotation(-tracker.rotation || 0);
}
@ -150,7 +144,7 @@ class Slice {
} else {
// create top object from object bundle passed back by slicePost()
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_sparse = data.fill_sparse;
top.fill_off = data.fill_off;
@ -251,12 +245,10 @@ function newSlice(z, view) {
return new Slice(z, view);
}
gapp.overlay(kiri, {
export {
Top,
Slice,
newTop,
newSlice,
setSliceTracker
});
});
};

View file

@ -1,15 +1,9 @@
/** 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
*/
class Stack {
export class Stack {
constructor(view, freeMem, shiny) {
this._view = view;
this.view = view.newGroup();
@ -245,8 +239,6 @@ class Stack {
}
}
kiri.Stack = Stack;
let shininess = 15,
specular = 0x444444,
emissive = 0x101010,
@ -301,5 +293,3 @@ function createLambertMaterial(color, flat) {
side: flat ? THREE.DoubleSide : THREE.FrontSide
});
}
});

View file

@ -1,13 +1,8 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
// dep: kiri.stack
// use: kiri.api
// use: kiri.ui
gapp.register("kiri.stacks", [], (root, exports) => {
const { kiri } = root;
import { $ } from '../../moto/webui.js';
import { api } from './api.js';
import { Stack } from './stack.js';
let freeMem = true,
stacks = {},
@ -18,7 +13,7 @@ let freeMem = true,
function init() {
labels = $("layers");
API = kiri.api,
API = api,
UC = API.uc,
UI = API.ui;
}
@ -80,7 +75,7 @@ function create(name, view) {
}
const stack = stacks[name] = {
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) {
let lmap = layers.layers;
let map = stack.obj.addLayers(layers);
@ -93,9 +88,9 @@ function create(name, view) {
mat.visible = ctrl.toggle.checked;
});
if (ctrl.toggle.checked) {
kiri.api.event.emit("stack.show", label);
api.event.emit("stack.show", label);
} 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,
create,
rotate,
@ -170,4 +165,17 @@ kiri.stacks = {
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 */
"use strict";
// dep: moto.broker
// 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;
import { local } from '../../data/local.js';
import { broker } from '../../moto/broker.js';
import { js2o, o2js } from './utils.js';
class Stats {
constructor(db) {
this.db = db || data.local;
this.db = db || local;
this.obj = js2o(this.db['stats'] || '{}');
let o = this.obj, k;
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 */
// dep: add.three
// dep: kiri.api
gapp.register("kiri.tools", (root, exports) => {
import { api } from './api.js';
import { THREE } from '../../ext/three.js';
const { Vector3, Quaternion } = THREE;
const { kiri, moto } = root;
const { api } = kiri;
const XAXIS = new THREE.Vector3(1,0,0);
const ZAXIS = new THREE.Vector3(0,0,1);
@ -164,4 +162,5 @@ api.event.on('mouse.hover.up', (ev) => {
endIt();
});
});
export { onLayFlatSelect, onFaceUpSelect, startIt, endIt, cleanup, scale, onDone };

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

@ -0,0 +1,859 @@
/** 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);
lastGroup?.push(div);
div._group = groupName;
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;
}
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 */
"use strict";
gapp.register("kiri.utils", [], (root, exports) => {
import { ajax as moto_ajax } from '../../moto/ajax.js';
import { local as dataLocal } from '../../data/local.js';
function parseOpt(ov) {
let opt = {}, kv, kva;
@ -28,7 +27,7 @@ function encodeOpt(opt) {
}
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) {
@ -46,7 +45,7 @@ function js2o(s,def) {
function ls2o(key,def) {
// 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 ]
@ -99,11 +98,14 @@ function trackFn(fn, name) {
}
}
exports({
function noop() {}
export {
trackFn,
areEqual,
parseOpt,
encodeOpt,
noop,
ajax,
o2js,
js2o,
@ -111,6 +113,19 @@ exports({
avgc,
rgb,
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 */
"use strict";
// dep: geo.base
// dep: geo.point
// 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;
import { base } from '../../geo/base.js';
import { avgc } from './utils.js';
import { verticesToPoints } from '../../geo/points.js';
import { util as mesh_util } from '../../mesh/util.js';
const { inRange, time } = base.util;
const solid_opacity = 1.0;
const groups = [];
@ -28,12 +16,14 @@ let nextId = 0;
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 {
constructor(id, group) {
this.api = self.kiri_api;
this.id = id || Date.now().toString(36)+(nextId++);
this.grouped = group ? true : false;
this.group = group || [];
@ -347,7 +337,7 @@ class Widget {
}
selectFaces(faces) {
let groups = mesh.util.facesToGroups(faces || []);
let groups = mesh_util.facesToGroups(faces || []);
let geo = this.mesh.geometry;
geo.clearGroups();
for (let group of groups) {
@ -503,8 +493,8 @@ class Widget {
w._move(x, y, z, abs);
});
// allow for use in engine / cli
if ((x || y || z) && api && api.event) {
api.event.emit('widget.move', {widget: this, pos: {x, y, z}, abs});
if ((x || y || z) && this.api && this.api.event) {
this.api.event.emit('widget.move', {widget: this, pos: {x, y, z}, abs});
}
}
@ -555,8 +545,8 @@ class Widget {
w._scale(x, y, z);
});
this.center(false);
if (api && api.event) {
api.event.emit('widget.scale', {widget: this, x, y, z});
if (this.api && this.api.event) {
this.api.event.emit('widget.scale', {widget: this, x, y, z});
}
}
@ -586,8 +576,8 @@ class Widget {
if (this.outline) {
this.setEdges(true);
}
if ((x || y || z) && api && api.event) {
api.event.emit('widget.rotate', {widget: this, x, y, z});
if ((x || y || z) && this.api && this.api.event) {
this.api.event.emit('widget.rotate', {widget: this, x, y, z});
}
}
@ -630,8 +620,8 @@ class Widget {
w._mirror();
});
this.center();
if (api && api.event) {
api.event.emit('widget.mirror', {widget: this});
if (this.api && this.api.event) {
this.api.event.emit('widget.mirror', {widget: this});
}
}
@ -758,113 +748,21 @@ class Widget {
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(stack) {
const mark = Date.now();
if (this.slices)
this.slices.forEach(slice => {
for (let slice of this.slices || []) {
if (slice.layers) {
stack.add(slice.layers);
}
});
}
return Date.now() - mark;
}
setEdges(set) {
if (!(api && api.conf)) {
if (!(this.api && this.api.conf)) {
// missing api features in engine mode
return;
}
@ -878,8 +776,8 @@ class Widget {
}
if (set) {
let dark = api.space.is_dark();
let angle = api.conf.get().controller.edgeangle || 20;
let dark = this.api.space.is_dark();
let angle = this.api.conf.get().controller.edgeangle || 20;
let edges = new THREE.EdgesGeometry(mesh.geometry, angle);
let material = new THREE.LineBasicMaterial({ color: 0 });
this.outline = new THREE.LineSegments(edges, material);
@ -889,7 +787,7 @@ class Widget {
}
setWireframe(set, color, opacity) {
if (!(api && api.conf)) {
if (!(this.api && this.api.conf)) {
// missing api features in engine mode
return;
}
@ -901,7 +799,7 @@ class Widget {
this.wire = null;
}
if (set) {
let dark = api.space.is_dark();
let dark = this.api.space.is_dark();
let mat = new THREE.MeshBasicMaterial({
wireframe: true,
color: dark ? 0xaaaaaa : 0,
@ -911,7 +809,7 @@ class Widget {
let wire = widget.wire = new THREE.Mesh(mesh.geometry.shallowClone(), mat);
mesh.add(wire);
}
if (api.view.is_arrange()) {
if (this.api.view.is_arrange()) {
this.setColor(this.color);
} else {
this.setColor(0x888888,undefined,false);
@ -1066,7 +964,4 @@ Widget.deleteFromState = function(id,ondone) {
index().remove('ws-save-'+id, ondone);
};
kiri.Widget = Widget;
kiri.newWidget = newWidget;
});
export { Widget, newWidget };

View file

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