diff --git a/app.js b/app.js index c6e2f2f9..4c0c2f1b 100644 --- a/app.js +++ b/app.js @@ -12,7 +12,7 @@ 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'); @@ -58,25 +58,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; @@ -103,173 +84,7 @@ 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 { - 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]}); - } + generateDevices(); mod.on.test((req) => { let cookie = cookieValue(req.headers.cookie, "version") || undefined; @@ -295,10 +110,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,29 +120,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"); + 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"); - mod.static("/v2/", "web2"); - function load_modules(root, force) { // load modules lastmod(`${dir}/${root}`) && fs.readdirSync(`${dir}/${root}`).forEach(mdir => { @@ -353,10 +149,10 @@ function init(mod) { } // load development and 3rd party modules - load_modules('mod'); + // load_modules('mod'); // load optional local modules - load_modules('mods'); + // load_modules('mods'); // run loads injected by modules while (load.length) { @@ -366,9 +162,6 @@ function init(mod) { logger.log({on_load_fail: e}); } } - - // runs after module loads / injects - prepareScripts(); }; // either add module assets to path or require(init.js) @@ -391,7 +184,6 @@ function initModule(mod, file, dir) { // express functions added here show up at "/api/" url root api: api, adm: { - reload: prepareScripts, setver: (ver) => { oversion = ver }, crossOrigin: (bool) => { crossOrigin = bool } }, @@ -446,19 +238,6 @@ function initModule(mod, file, dir) { 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 } @@ -482,51 +261,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 = {}; @@ -564,28 +298,30 @@ function handleSetup(req, res, next) { 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; - if (path === '/v2/lib/mesh/work.js') { - req.url = req.app.path = '/v2/lib/pack/mesh-work.js'; - } else if (path === '/v2/lib/main/mesh.js') { - req.url = req.app.path = '/v2/lib/pack/mesh-main.js'; - } else if (path === '/v2/lib/kiri-run/minion.js') { - req.url = req.app.path = '/v2/lib/pack/kiri-pool.js'; - } else if (path === '/v2/lib/kiri-run/worker.js') { - req.url = req.app.path = '/v2/lib/pack/kiri-work.js'; - } else if (path === '/v2/lib/main/kiri.js') { - req.url = req.app.path = '/v2/lib/pack/kiri-main.js'; + if (path === '/lib/mesh/work.js') { + req.url = req.app.path = '/lib/pack/mesh-work.js'; + } else if (path === '/lib/main/mesh.js') { + req.url = req.app.path = '/lib/pack/mesh-main.js'; + } else if (path === '/lib/kiri-run/minion.js') { + req.url = req.app.path = '/lib/pack/kiri-pool.js'; + } else if (path === '/lib/kiri-run/worker.js') { + req.url = req.app.path = '/lib/pack/kiri-work.js'; + } else if (path === '/lib/main/kiri.js') { + req.url = req.app.path = '/lib/pack/kiri-main.js'; } + // add cors headers on rewrite if (path !== req.url) { + console.log('rewrite', path, req.url); addCorsHeaders(req, res); - // console.log('rewrite', path, req.url); } next(); } else { @@ -631,58 +367,6 @@ 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"); @@ -698,128 +382,7 @@ function generateDevices() { }); let dstr = JSON.stringify(devs); synth.devices = `self.devices = ${dstr};`; - fs.writeFileSync(PATH.join(dir,"src2","pack","devices.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; + fs.writeFileSync(PATH.join(dir,"src","pack","devices.js"), `export const devices = ${dstr};`); } function minify(path) { @@ -942,10 +505,11 @@ function cookieValue(cookie,key) { function rewriteHtmlVersion(req, res, next) { if ([ - "/v2/kiri/", - "/v2/lib/kiri-run/minion.js", - "/v2/lib/kiri-run/worker.js", - "/v2/lib/kiri-run/minion.js" + "/kiri/", + "/mesh/", + "/lib/mesh/work.js", + "/lib/kiri-run/worker.js", + "/lib/kiri-run/minion.js" ].indexOf(req.app.path) >= 0) { addCorsHeaders(req, res); } else if ([ diff --git a/package.json b/package.json index 58fd70a1..aa8b5530 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,7 @@ "webpack-cli": "^5.1.4" }, "scripts": { - "setup": "npm i && cd mods && npm i", - "setup2": "npm run setup && npm run dev -- --dryrun && npm run preinstall2", + "setup": "npm i && npm run dev -- --dryrun && npx webpack --config src/webpack/webpack-three-esm.js && node src/webpack/esbuild.config.mjs", "dev": "gs-app-server --debug", "prod": "gs-app-server", "prod-dryrun": "gs-app-server --dryrun", @@ -98,14 +97,12 @@ "build-mac-intel": "npm run build -- --mac --x64", "mesh-build": "webpack --config webpack.config.js", "mesh-build-dev": "webpack --config webpack.dev.config.js", - "serve-mesh": "npx serve dist/v2 -p 8181", "mklinks": "find src web -type l | xargs -I{} sh -c 'echo \"{},$(readlink {})\"' > 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", "postbuild": "node bin/electron-post.js", - "preinstall": "node bin/install-pre.js && npx webpack --config bin/webpack-three.js", - "preinstall2": "npx webpack --config src2/webpack/webpack-three-esm.js && node src2/webpack/esbuild.config.mjs", + "preinstall": "", "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", diff --git a/src2/add/array.js b/src.old/add/array.js similarity index 96% rename from src2/add/array.js rename to src.old/add/array.js index cd4d6c42..93c9b8d3 100644 --- a/src2/add/array.js +++ b/src.old/add/array.js @@ -1,7 +1,11 @@ /** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */ +"use strict"; + // extend Array, String, Number, Math -const AP = {}; +gapp.register("add.array", [], (root, exports) => { + +let AP = {}; if (!AP.flat) { AP.flat = function() { @@ -212,11 +216,12 @@ Array.handle = function(fn) { } }; -// Apply all methods to Array.prototype -Object.assign(Array.prototype, AP); - -// Export for ES modules -export { AP as arrayPrototype }; +for (let i in AP) { + Object.defineProperty(Array.prototype, i, { + value: AP[i], + enumerable: false + }); +} Float32Array.prototype.toShared = function() { const newvert = new Float32Array(new SharedArrayBuffer(this.buffer.byteLength)); @@ -260,3 +265,5 @@ Number.prototype.round = function(digits) { const pow = Math.pow(10, digits || 3); return Math.round(this.valueOf() * pow) / pow; }; + +}); diff --git a/src2/add/class.js b/src.old/add/class.js similarity index 91% rename from src2/add/class.js rename to src.old/add/class.js index 7d3b0ad6..2dd7d885 100644 --- a/src2/add/class.js +++ b/src.old/add/class.js @@ -1,10 +1,11 @@ /** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */ -// use: add.array -import { arrayPrototype } from './array.js'; +"use strict"; -// Make classes available globally -class ArrayWriter { +// use: add.array +gapp.register("add.class", [], (root, exports) => { + +self.ArrayWriter = class ArrayWriter { constructor() { this.pos = 0; this.array = []; @@ -47,7 +48,7 @@ class ArrayWriter { } }; -class DataWriter { +self.DataWriter = class DataWriter { constructor(view, pos) { this.view = view; this.pos = pos || 0; @@ -105,7 +106,7 @@ class DataWriter { } } -class DataReader { +self.DataReader = class DataReader { constructor(view, pos) { this.view = view; this.pos = pos || 0; @@ -170,9 +171,4 @@ class DataReader { } } -self.ArrayWriter = ArrayWriter; -self.DataWriter = DataWriter; -self.DataReader = DataReader; - -// Export for ES modules -export { ArrayWriter, DataWriter, DataReader }; +}); diff --git a/src2/add/three.js b/src.old/add/three.js similarity index 94% rename from src2/add/three.js rename to src.old/add/three.js index 889c119c..9f48f60a 100644 --- a/src2/add/three.js +++ b/src.old/add/three.js @@ -1,13 +1,16 @@ /** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */ +"use strict"; + // dep: ext.three -import { +gapp.register("add.three", [], (root, exports) => { + +const { THREE, SVGLoader, BufferGeometryUtils, - LineMaterial, Line2, LineGeometry, LineSegments2, - LineSegmentsGeometry, MeshBVHLib } from '../ext/three.js'; - -// Make THREE available globally -self.THREE = THREE; + LineMaterial, Line2, LineGeometry, LineSegments2, LineSegmentsGeometry +} = ThreeBundle; + +root.THREE = THREE; THREE.BufferGeometryUtils = BufferGeometryUtils; THREE.SVGLoader = SVGLoader.SVGLoader; THREE.LineMaterial = LineMaterial; @@ -144,5 +147,4 @@ THREE.Object3D.prototype.removeAll = function() { this.children = []; }; -// Export for ES modules -export { THREE }; +}); diff --git a/src2/cli/kiri-cam-device.json b/src.old/cli/kiri-cam-device.json similarity index 100% rename from src2/cli/kiri-cam-device.json rename to src.old/cli/kiri-cam-device.json diff --git a/src2/cli/kiri-cam-process.json b/src.old/cli/kiri-cam-process.json similarity index 100% rename from src2/cli/kiri-cam-process.json rename to src.old/cli/kiri-cam-process.json diff --git a/src2/cli/kiri-cam-tools.json b/src.old/cli/kiri-cam-tools.json similarity index 100% rename from src2/cli/kiri-cam-tools.json rename to src.old/cli/kiri-cam-tools.json diff --git a/src2/cli/kiri-controller.json b/src.old/cli/kiri-controller.json similarity index 100% rename from src2/cli/kiri-controller.json rename to src.old/cli/kiri-controller.json diff --git a/src2/cli/kiri-fdm-device.json b/src.old/cli/kiri-fdm-device.json similarity index 100% rename from src2/cli/kiri-fdm-device.json rename to src.old/cli/kiri-fdm-device.json diff --git a/src2/cli/kiri-fdm-process.json b/src.old/cli/kiri-fdm-process.json similarity index 100% rename from src2/cli/kiri-fdm-process.json rename to src.old/cli/kiri-fdm-process.json diff --git a/src2/cli/kiri-source.json b/src.old/cli/kiri-source.json similarity index 100% rename from src2/cli/kiri-source.json rename to src.old/cli/kiri-source.json diff --git a/src2/data/index.js b/src.old/data/index.js similarity index 97% rename from src2/data/index.js rename to src.old/data/index.js index e28217be..e102306b 100644 --- a/src2/data/index.js +++ b/src.old/data/index.js @@ -1,14 +1,19 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API +"use strict"; -export const data = { - Index: IDBStore, - open() { - return new IDBStore(...arguments); - } +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 + let IDB, IRR, local = null; try { @@ -314,5 +319,4 @@ SP.clear = function(key, store = this.current) { .objectStore(store).clear(); }; -export const open = data.open; -export const Index = IDBStore; +}); diff --git a/src.old/data/local.js b/src.old/data/local.js new file mode 100644 index 00000000..e2c849dd --- /dev/null +++ b/src.old/data/local.js @@ -0,0 +1,47 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +gapp.register("data.local", [], (root, exports) => { + +const { data } = root; + +function Local() { + this.__data__ = {}; + this.__mem__ = true; +} + +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__ = {}; +}; + +try { + // deprecate 'Local' at some point + let local = data.local =self.localStorage; + let testkey = '__test'; + local.setItem(testkey, 1); + local.getItem(testkey); + local.removeItem(testkey); +} catch (e) { + data.local = new Local(); + let msg = "localStorage disabled: application may not function properly"; + console.log(msg); + // alert(msg); +} + +}); diff --git a/src2/dock/Dockerfile b/src.old/dock/Dockerfile similarity index 100% rename from src2/dock/Dockerfile rename to src.old/dock/Dockerfile diff --git a/src2/dock/compose.yml b/src.old/dock/compose.yml similarity index 100% rename from src2/dock/compose.yml rename to src.old/dock/compose.yml diff --git a/src2/ext/base64.js b/src.old/ext/base64.js similarity index 100% rename from src2/ext/base64.js rename to src.old/ext/base64.js diff --git a/src/ext/clip.js b/src.old/ext/clip.js similarity index 100% rename from src/ext/clip.js rename to src.old/ext/clip.js diff --git a/src2/ext/clip2.js b/src.old/ext/clip2.js similarity index 100% rename from src2/ext/clip2.js rename to src.old/ext/clip2.js diff --git a/src2/ext/earcut.js b/src.old/ext/earcut.js similarity index 100% rename from src2/ext/earcut.js rename to src.old/ext/earcut.js diff --git a/src/ext/fsave.js b/src.old/ext/fsave.js similarity index 100% rename from src/ext/fsave.js rename to src.old/ext/fsave.js diff --git a/src2/ext/gerber.js b/src.old/ext/gerber.js similarity index 100% rename from src2/ext/gerber.js rename to src.old/ext/gerber.js diff --git a/src2/ext/jszip.js b/src.old/ext/jszip.js similarity index 100% rename from src2/ext/jszip.js rename to src.old/ext/jszip.js diff --git a/src2/ext/manifold.js b/src.old/ext/manifold.js similarity index 100% rename from src2/ext/manifold.js rename to src.old/ext/manifold.js diff --git a/src2/ext/md5.js b/src.old/ext/md5.js similarity index 98% rename from src2/ext/md5.js rename to src.old/ext/md5.js index e01a6b8e..88b36d68 100644 --- a/src2/ext/md5.js +++ b/src.old/ext/md5.js @@ -1,6 +1,9 @@ // SOURCE: https://stackoverflow.com/questions/1655769/fastest-md5-implementation-in-javascript +gapp.register("ext.md5", [], (root, exports) => { -export function hash(e) { +exports({ hash }); + +function hash(e) { function h(a, b) { var c, d, e, f, g; e = a & 2147483648; @@ -64,4 +67,6 @@ export 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() -} \ No newline at end of file +}; + +}); \ No newline at end of file diff --git a/src2/ext/pngjs.js b/src.old/ext/pngjs.js similarity index 100% rename from src2/ext/pngjs.js rename to src.old/ext/pngjs.js diff --git a/src2/ext/tween.js b/src.old/ext/tween.js similarity index 100% rename from src2/ext/tween.js rename to src.old/ext/tween.js diff --git a/src2/geo/base.js b/src.old/geo/base.js similarity index 98% rename from src2/geo/base.js rename to src.old/geo/base.js index d7ea18bf..e851e803 100644 --- a/src2/geo/base.js +++ b/src.old/geo/base.js @@ -1,8 +1,12 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { newPoint } from './point.js'; -import earcut from '../ext/earcut.js'; +"use strict"; +// use: add.array +// use: add.class +gapp.register("geo.base", [], (root, exports) => { + +const base = root.base = {}; const round_decimal_precision = 5; function time() { @@ -215,7 +219,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 = newPoint( + let ip = base.newPoint( p1x + (a * d1x), // x p1y + (a * d1y), // y p3.z || p4.z, // z @@ -260,7 +264,7 @@ function intersectRayLine(ro, s1, p1, p2, infinite) { b = n2 / d; if (infinite || (inCloseRange(b, 0, 1) && a >= 0)) { - let ip = newPoint( + let ip = base.newPoint( p1x + (a * s1x), p1y + (a * s1y), p2.z || ro.z, @@ -524,7 +528,7 @@ function comma(v) { * Connect to base ******************************************************************* */ -export const key = { +base.key = { NONE: "", PROJECT: "project", SEGINT: "segint", @@ -532,7 +536,7 @@ export const key = { PARALLEL: "parallel" }; -export const config = { +base.config = { // size of gcode debug arrow head debug_arrow: 0.25, // default # of decimal places in generated gcode @@ -598,7 +602,7 @@ export const config = { clipperClean: 250 }; -export const util = { +base.util = { sqr, lerp, time, @@ -635,10 +639,4 @@ export const util = { zInPlane }; -export const base = { - config, - key, - util -}; - -export { earcut }; +}); diff --git a/src2/geo/bounds.js b/src.old/geo/bounds.js similarity index 91% rename from src2/geo/bounds.js rename to src.old/geo/bounds.js index 05950d3e..3af6ef10 100644 --- a/src2/geo/bounds.js +++ b/src.old/geo/bounds.js @@ -1,9 +1,14 @@ /** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */ -import { config, util } from './base.js'; -import { newPoint } from './point.js'; +"use strict"; -export class Bounds { +// dep: geo.base +gapp.register("geo.bounds", [], (root, exports) => { + +const { base } = root; +const { config, util } = base; + +class Bounds { constructor() { this.minx = 10e7; this.miny = 10e7; @@ -116,7 +121,7 @@ export class Bounds { } center(z = 0) { - return newPoint(this.centerx(), this.centery(), z); + return base.newPoint(this.centerx(), this.centery(), z); } centerx() { @@ -128,6 +133,9 @@ export class Bounds { } } -export function newBounds() { - return new Bounds(); -} +gapp.overlay(base, { + Bounds, + newBounds() { return new Bounds() } +}); + +}); diff --git a/src2/geo/csg.js b/src.old/geo/csg.js similarity index 86% rename from src2/geo/csg.js rename to src.old/geo/csg.js index 0aec7c09..e93fa19d 100644 --- a/src2/geo/csg.js +++ b/src.old/geo/csg.js @@ -2,17 +2,24 @@ "use strict"; -import { THREE } from '../ext/three.js'; -import manifold from '../ext/manifold.js'; +// dep: geo.base +// mod: ext.manifold +gapp.register("geo.csg", [], (root, exports) => { +const { base, ext } = root; +const debug = true; const precision = 0.001; const factor = 1/precision; function log() { - console.log(...arguments); + if (root?.mesh?.log) { + root.mesh.log(...arguments) + } else if (root.debug === true) { + console.log(...arguments); + } } -export const CSG = { +const CSG = { // accepts 2 or more arguments with threejs vertex position arrays union() { return CSG.moduleOp('manifold.union', 'union', ...arguments); @@ -116,10 +123,23 @@ function indexVertices(pos) { } let Instance; -manifold({ - locateFile() { return "/wasm/manifold.wasm" } -}).then(inst => { + +ext.manifold.then(mod => { + mod.default({ + locateFile(a,b,c) { + 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 +}); + }); diff --git a/src2/geo/gyroid.js b/src.old/geo/gyroid.js similarity index 92% rename from src2/geo/gyroid.js rename to src.old/geo/gyroid.js index e333a37c..b56ca25c 100644 --- a/src2/geo/gyroid.js +++ b/src.old/geo/gyroid.js @@ -1,5 +1,11 @@ /** 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 = {}; @@ -11,7 +17,7 @@ let lastSlice = 0; * @param off {number} z offset value from 0-1 * @param res {number} resolution (pixels/slices per side) */ -export function slice(off, res, val) { +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(); @@ -164,7 +170,7 @@ export function slice(off, res, val) { } // merge co-linear and distance threshold -export function filter(poly, inc) { +function filter(poly, inc) { if (poly.length <= 2) { return poly; } @@ -194,16 +200,21 @@ export function filter(poly, inc) { } e1 = el; } + nupoly.push(poly[poly.length-1]); return nupoly; } -export function distTo(a, b, dir) { +function distTo(a, b, dir) { let dx = a.x - b.x; let dy = a.y - b.y; - if (dir === 'lr') { - return Math.abs(dx); - } else if (dir === 'td') { - return Math.abs(dy); - } + // bias distance by prevailing direction of discovery to join stragglers + if (dir === 'lr') dx = dx / 2; + if (dir === 'td') dy = dy / 2; return Math.sqrt(dx * dx + dy * dy); } + +gapp.overlay(base, { + gyroid: { slice } +}) + +}); diff --git a/src2/geo/line.js b/src.old/geo/line.js similarity index 80% rename from src2/geo/line.js rename to src.old/geo/line.js index 63221580..f391c7bd 100644 --- a/src2/geo/line.js +++ b/src.old/geo/line.js @@ -1,6 +1,13 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -export class Line { +"use strict"; + +// dep: geo.base +gapp.register("geo.line", [], (root, exports) => { + +const { base } = root; + +class Line { constructor(p1, p2, key) { if (!key) key = [p1.key, p2.key].join(';'); this.p1 = p1; @@ -47,10 +54,18 @@ export class Line { } } -export function newLine(p1, p2, key) { +function newLine(p1, p2, key) { return new Line(p1, p2, key); } -export function newOrderedLine(p1, p2, key) { +function newOrderedLine(p1, p2, key) { return p1.key < p2.key ? newLine(p1,p2,key) : newLine(p2,p1,key); } + +gapp.overlay(base, { + Line, + newLine, + newOrderedLine +}); + +}); diff --git a/src2/geo/paths.js b/src.old/geo/paths.js similarity index 85% rename from src2/geo/paths.js rename to src.old/geo/paths.js index 90cba421..134a63f3 100644 --- a/src2/geo/paths.js +++ b/src.old/geo/paths.js @@ -1,7 +1,19 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base } from './base.js'; -import { newPoint } from './point.js'; +"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; /** * emit each element in an array based on @@ -9,21 +21,21 @@ import { newPoint } from './point.js'; * elements with { first, last } points and * may be open polys, unlike poly2polyEmit */ -export function tip2tipEmit(array, startPoint, emitter) { +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; } }); @@ -44,7 +56,7 @@ export function tip2tipEmit(array, startPoint, emitter) { * to be more like outputOrderClosest() and have the option to account for * depth in determining distance */ -export function poly2polyEmit(array, startPoint, emitter, opt = {}) { +function poly2polyEmit(array, startPoint, emitter, opt = {}) { let marker = opt.mark || 'delete'; let mindist, dist, found, count = 0; for (;;) { @@ -62,21 +74,21 @@ export 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; } }); @@ -90,23 +102,23 @@ export 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; } -export function calc_normal(p1, p2) { +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 }); } -export function end_vertex(n1, n2, off, start) { +function end_vertex(n1, n2, off, start) { let dx, dy; if (start) { dx = n2.dx * off; @@ -118,7 +130,7 @@ export function end_vertex(n1, n2, off, start) { return { dx, dy, vp: n1.p2 }; } -export function calc_vertex(n1, n2, off, vp) { +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; @@ -136,7 +148,7 @@ export function calc_vertex(n1, n2, off, vp) { return { dx, dy, vp: vp || n1.p2, io, vl }; } -export function v2pl(rec) { +function v2pl(rec) { let p = rec.vp.clone(); p.x += rec.dx; p.y += rec.dy; @@ -144,7 +156,7 @@ export function v2pl(rec) { return p; } -export function v2pr(rec) { +function v2pr(rec) { let p = rec.vp.clone(); p.x -= rec.dx; p.y -= rec.dy; @@ -152,17 +164,17 @@ export function v2pr(rec) { return p; } -export function pointsToPath(points, offset, open, miter = 1.5) { +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 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} an array of points representing the arc. */ -export function arcToPath(start, end, arcdivs = 24, opts) { +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 (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); @@ -556,7 +572,7 @@ export 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 @@ -569,10 +585,10 @@ export function arcToPath(start, end, arcdivs = 24, opts) { let a2 = Math.atan2(center.y - end.y, center.x - end.x) + Math.PI; let ad = base.util.thetaDiff(a1, a2, clockwise); // angle difference in radians let samePoint = Math.abs(ad) < 0.001 - let ofFull = Math.abs(ad) / (2 * Math.PI); - let steps = samePoint ? arcdivs : Math.max(Math.floor(arcdivs * ofFull), 4); - let step = (samePoint ? (Math.PI * 2) : ad) / steps; - let numPoints = steps / step; + let ofFull = Math.abs(ad)/(2*Math.PI); + let steps = samePoint? arcdivs : Math.max(Math.floor( arcdivs * ofFull),4); + let step = (samePoint? (Math.PI*2) : ad) / steps; + let numPoints = steps/ step; let zStart = start.z; let zStep = dz / numPoints; let rot = a1 + step; @@ -591,9 +607,9 @@ export function arcToPath(start, end, arcdivs = 24, opts) { // } 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, @@ -607,7 +623,7 @@ export function arcToPath(start, end, arcdivs = 24, opts) { return arr } -export class FloatPacker { +class FloatPacker { constructor(size, factor) { this.size = size; this.factor = Math.min(factor || 1.2, 1.1); @@ -626,7 +642,7 @@ export class FloatPacker { this.array = nuarray; this.size = nusize; } - for (let i = 0; i < args; i++) { + for (let i=0; i -- All Rights Reserved */ -import { util, config, key } from './base.js'; -import { newLine } from './line.js'; -import { newSlope } from './slope.js'; +"use strict"; -const { Vector3 } = THREE; +// dep: geo.base +gapp.register("geo.point", [], (root, exports) => { + +const { base } = root; +const { util, config, key } = base; +const { round } = util; class Point { constructor(x = 0, y = 0, z = 0, key) { @@ -39,7 +42,7 @@ class Point { } toVector3() { - return new Vector3(this.x, this.y, this.z); + return new THREE.Vector3(this.x, this.y, this.z); } set(x, y, z) { @@ -123,11 +126,11 @@ class Point { } slopeTo(p) { - return newSlope(this, p); + return base.newSlope(this, p); } lineTo(p, k) { - return newLine(this, p, k); + return base.newLine(this, p, k); } isNear(p, dist) { @@ -293,7 +296,7 @@ class Point { np2 = newPoint(p2.x - oy, p2.y + ox, p2.z, key.NONE); np1.op = p1; np2.op = p2; - return newLine(np1, np2, key.NONE); + return base.newLine(np1, np2, key.NONE); } offset(x, y, z) { @@ -649,8 +652,10 @@ function pointFromClipper(cp, z) { return newPoint(cp.X / config.clipper, cp.Y / config.clipper, z); } - export { +gapp.overlay(base, { Point, newPoint, pointFromClipper -}; +}); + +}); diff --git a/src2/geo/points.js b/src.old/geo/points.js similarity index 89% rename from src2/geo/points.js rename to src.old/geo/points.js index 183306bd..d1118361 100644 --- a/src2/geo/points.js +++ b/src.old/geo/points.js @@ -2,14 +2,23 @@ "use strict"; -import { config } from './base.js'; -import { newPoint } from '../geo/point.js'; +// dep: geo.base +// dep: geo.point +gapp.register("geo.points", [], (root, exports) => { + +const { base } = root; +const { config } = base; + +gapp.overlay(base, { + verticesToPoints, + pointsToVertices +}); /** * converts a geometry point array into a kiri point array * with optional decimation */ -export function verticesToPoints(array, options) { +function verticesToPoints(array, options) { let parr = new Array(array.length / 3), i = 0, j = 0, @@ -32,7 +41,7 @@ export function verticesToPoints(array, options) { // replace point objects with their equivalents if (maxpass) { while (i < array.length) { - let p = newPoint(array[i++], array[i++], array[i++]), + let p = base.newPoint(array[i++], array[i++], array[i++]), k = p.key, m = hash[k]; if (!m) { @@ -44,7 +53,7 @@ export function verticesToPoints(array, options) { } } else { while (i < array.length) { - parr[j++] = newPoint(array[i++], array[i++], array[i++]); + parr[j++] = base.newPoint(array[i++], array[i++], array[i++]); } } @@ -115,7 +124,7 @@ export function verticesToPoints(array, options) { return parr; } -export function pointsToVertices(points) { +function pointsToVertices(points) { let vertices = new Float32Array(points.length * 3), i = 0, vi = 0; while (i < points.length) { @@ -125,3 +134,5 @@ export function pointsToVertices(points) { } return vertices; } + +}); diff --git a/src2/geo/polygon.js b/src.old/geo/polygon.js similarity index 94% rename from src2/geo/polygon.js rename to src.old/geo/polygon.js index 6e665522..4f9d0501 100644 --- a/src2/geo/polygon.js +++ b/src.old/geo/polygon.js @@ -2,17 +2,22 @@ "use strict"; -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'; +// 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) => { -const { Vector3 } = THREE; +const { base } = root; +const { config, util, polygons, newBounds, newPoint } = base; -let XAXIS = new Vector3(1,0,0), +const POLY = polygons, + XAXIS = new THREE.Vector3(1,0,0), DEG2RAD = Math.PI / 180, + ClipperLib = self.ClipperLib, Clipper = ClipperLib.Clipper, ClipType = ClipperLib.ClipType, PolyType = ClipperLib.PolyType, @@ -34,7 +39,7 @@ let XAXIS = new Vector3(1,0,0), let seqid = Math.round(Math.random() * 0xffffffff); -export class Polygon { +class Polygon { constructor(points) { this.id = seqid++; // polygon unique id this.open = false; @@ -71,11 +76,11 @@ export class Polygon { } toPath2D(offset) { - return paths.pointsToPath(this.points, offset, this.open); + return base.paths.pointsToPath(this.points, offset, this.open); } toPath3D(offset, height, z) { - return paths.pathTo3D(this.toPath2D(offset), height, z); + return base.paths.pathTo3D(this.toPath2D(offset), height, z); } toString(verbose) { @@ -230,7 +235,7 @@ export class Polygon { } // perform earcut() - let cut = earcut(out, holes, 3); + let cut = self.earcut(out, holes, 3); let ret = []; // preserve swaps in new polys @@ -462,7 +467,7 @@ export class Polygon { return polys .filter(poly => poly.length > 1) .map(poly => { - let np = newPolygon().setOpen(); + let np = base.newPolygon().setOpen(); for (let p of poly) { np.push(p); } @@ -1138,7 +1143,7 @@ export class Polygon { applyRotations() { for (let point of this.points) { if (point.a) { - let p2 = new Vector3(point.x, point.y, point.z) + let p2 = new THREE.Vector3(point.x, point.y, point.z) .applyAxisAngle(XAXIS, point.a * DEG2RAD); point.x = p2.x; point.y = p2.y; @@ -1200,76 +1205,15 @@ export class Polygon { return false; } + + /** - * 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 + * 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 */ - 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) { + forEachPoint(fn, close, start) { let index = start || 0, points = this.points, length = points.length, @@ -2059,7 +2003,7 @@ export class Polygon { return res.map(array => { let poly = newPolygon(); for (let pt of array) { - poly.push(pointFromClipper(pt, z)); + poly.push(base.pointFromClipper(pt, z)); } return poly; }); @@ -2374,7 +2318,8 @@ export class Polygon { } -export function slopeDiff(s1, s2) { +// use Slope.angleDiff() then re-test path mitering / rendering +function slopeDiff(s1, s2) { const n1 = s1.angle; const n2 = s2.angle; let diff = n2 - n1; @@ -2383,17 +2328,28 @@ export function slopeDiff(s1, s2) { return Math.abs(diff); } -export function fromClipperPath(path, z) { +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(pointFromClipper(path[i++], z)); + poly.push(base.pointFromClipper(path[i++], z)); } return poly; } -export function newPolygon(points) { +function newPolygon(points) { return new Polygon(points); } + +Polygon.fromArray = function(array) { + return newPolygon().fromArray(array); +}; + +gapp.overlay(base, { + Polygon, + newPolygon +}); + +}); diff --git a/src2/geo/polygons.js b/src.old/geo/polygons.js similarity index 87% rename from src2/geo/polygons.js rename to src.old/geo/polygons.js index 07cea123..86d866a6 100644 --- a/src2/geo/polygons.js +++ b/src.old/geo/polygons.js @@ -2,45 +2,45 @@ "use strict"; -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'; +// dep: geo.base +// dep: geo.point +// use: ext.clip2 +// use: geo.slope +// use: geo.polygon +gapp.register("geo.polygons", [], (root, exports) => { -const geo = base; -const { numOrDefault } = util; +const { base } = root; +const { util, paths, config, newPoint } = base; +const { sqr, numOrDefault } = util; const DEG2RAD = Math.PI / 180, SQRT = Math.sqrt, SQR = util.sqr, ABS = Math.abs; -const { - Clipper, - ClipType, - PolyType, - PolyFillType, - EndType, - JoinType, - PolyTree, - ClipperOffset -} = ClipperLib; - -const CleanPolygon = Clipper.CleanPolygon, +const ClipperLib = self.ClipperLib, + Clipper = ClipperLib.Clipper, + ClipType = ClipperLib.ClipType, + PolyType = ClipperLib.PolyType, + PolyFillType = ClipperLib.PolyFillType, + 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; + ClipIntersect = ClipType.ctIntersection, + ClipperOffset = ClipperLib.ClipperOffset + ; -const POLYS = { +const POLYS = base.polygons = { clearInner, rayIntersect, alignWindings, @@ -73,16 +73,14 @@ const POLYS = { fingerprint }; -export { POLYS }; - -export function outer(polys) { +function outer(polys) { for (let p of polys) { p.inner = undefined; } return polys; } -export function inner(polys) { +function inner(polys) { const ret = []; for (let p of polys) { if (p.inner) { @@ -92,7 +90,7 @@ export function inner(polys) { return ret; } -export function length(polys) { +function length(polys) { let length = 0; for (let p of polys) { length += p.deepLength; @@ -100,20 +98,20 @@ export function length(polys) { return length; } -export function setZ(polys, z) { +function setZ(polys, z) { for (let poly of polys) { poly.setZ(z); } return polys; } -export function clearInner(polys) { +function clearInner(polys) { for (let p of polys) { p.clearInner(); } } -export function toClipper(polys = []) { +function toClipper(polys = []) { let out = []; for (let poly of polys) { poly.toClipper(out); @@ -121,16 +119,16 @@ export function toClipper(polys = []) { return out; } -export function fromClipperNode(tnode, z) { - let poly = newPolygon(); +function fromClipperNode(tnode, z) { + let poly = base.newPolygon(); for (let point of tnode.m_polygon) { - poly.push(pointFromClipper(point, z)); + poly.push(base.pointFromClipper(point, z)); } poly.open = tnode.IsOpen; return poly; -} +}; -export function fromClipperTree(tnode, z, tops, parent, minarea) { +function fromClipperTree(tnode, z, tops, parent, minarea) { let poly, polys = tops || [], min = numOrDefault(minarea, 0.1); @@ -152,9 +150,9 @@ export function fromClipperTree(tnode, z, tops, parent, minarea) { } return polys; -} +}; -export function fromClipperTreeUnion(tnode, z, minarea, tops, parent) { +function fromClipperTreeUnion(tnode, z, minarea, tops, parent) { let polys = tops || [], poly; for (let child of tnode.m_Childs) { @@ -173,9 +171,9 @@ export function fromClipperTreeUnion(tnode, z, minarea, tops, parent) { } return polys; -} +}; -export function cleanClipperTree(tree) { +function cleanClipperTree(tree) { if (tree.m_Childs) for (let child of tree.m_Childs) { child.m_polygon = CleanPolygon(child.m_polygon, config.clipperClean); @@ -183,9 +181,9 @@ export function cleanClipperTree(tree) { } return tree; -} +}; -export function filter(array, output, fn) { +function filter(array, output, fn) { for (let poly of array) { poly = fn(poly); if (poly) { @@ -199,14 +197,14 @@ export function filter(array, output, fn) { return output; } -export function points(polys) { +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 */ -export function renest(polygons, deep) { +function renest(polygons, deep) { return nest(flatten(polygons, [], true), deep); } @@ -222,7 +220,7 @@ export function renest(polygons, deep) { * @param {boolean} opentop prevent open polygons from having inners * @returns {Polygon[]} top level parent polygons */ -export function nest(polygons, deep, opentop) { +function nest(polygons, deep, opentop) { if (!polygons) { return polygons; } @@ -286,7 +284,7 @@ export function nest(polygons, deep, opentop) { * @param {boolean} CW * @param {boolean} [recurse] */ -export function setWinding(array, CW, recurse) { +function setWinding(array, CW, recurse) { if (!array) return; let poly, i = 0; while (i < array.length) { @@ -304,7 +302,7 @@ export function setWinding(array, CW, recurse) { * @param {Polygon[]} polys * @return {boolean} true if aligned clockwise */ -export function alignWindings(polys) { +function alignWindings(polys) { let len = polys.length, fwd = 0, pts = 0, @@ -325,14 +323,21 @@ export function alignWindings(polys) { return setCW; } -export function setContains(setA, poly) { +function setContains(setA, poly) { for (let i=0; i 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 p.open) : []; if (open.length) { open = open.map(p => paths.pointsToPath(p.points, dist, true)); - open = open.map(p => newPolygon().setOpen().addPoints(p.right)); + open = open.map(p => base.newPolygon().setOpen().addPoints(p.right)); } // do not use clipper to offset open lines @@ -701,7 +706,7 @@ export function offset(polys, dist, opts = {}) { * as performing subtractive analysis between initial layer shell (ref) * and last offset (cmp) to produce gap candidates (for thinfill) */ -export function inset(polys, dist, count, z, wasm) { +function inset(polys, dist, count, z, wasm) { let total = count; let layers = []; let ref = polys; @@ -744,7 +749,7 @@ export function inset(polys, dist, count, z, wasm) { * @param {number} [maxLen] * @returns {Point[]} supplied output or new array */ -export function fillArea(polys, angle, spacing, output, minLen, maxLen) { +function fillArea(polys, angle, spacing, output, minLen, maxLen) { if (polys.length === 0) return; let i = 1, @@ -766,7 +771,7 @@ export function fillArea(polys, angle, spacing, output, minLen, maxLen) { while (angle > 90) angle -= 180; // X,Y ray slope derived from angle - raySlope = newSlope(0,0, + raySlope = base.newSlope(0,0, Math.cos(angle * DEG2RAD) * spacing, Math.sin(angle * DEG2RAD) * spacing ); @@ -838,8 +843,8 @@ export function fillArea(polys, angle, spacing, output, minLen, maxLen) { if (minlen && plen < minlen) continue; if (maxlen && plen > maxlen) continue; } - let p1 = pointFromClipper(poly.m_polygon[0], zpos); - let p2 = pointFromClipper(poly.m_polygon[1], zpos); + let p1 = base.pointFromClipper(poly.m_polygon[0], zpos); + let p2 = base.pointFromClipper(poly.m_polygon[1], zpos); let od = rayint.origin.distToLineNew(p1,p2) / spacing; lines.push([p1, p2, od]); } @@ -870,7 +875,7 @@ export function fillArea(polys, angle, spacing, output, minLen, maxLen) { * @param {boolean} [for_fill] * @returns {Point[]} */ -export function rayIntersect(start, slope, polygons, for_fill) { +function rayIntersect(start, slope, polygons, for_fill) { let i = 0, flat = [], points = [], @@ -999,11 +1004,11 @@ export function rayIntersect(start, slope, polygons, for_fill) { return points; } -export function pd(a,b) { +function pd(a,b) { return a > b ? Math.abs(1-b/a) : Math.abs(1-a/b); } -export function fingerprint(polys) { +function fingerprint(polys) { let recs = flatten(polys).map(p => { return { l: p.length, @@ -1032,7 +1037,7 @@ export function fingerprint(polys) { } // compare fingerprint arrays -export function fingerprintCompare(a, b) { +function fingerprintCompare(a, b) { // true if array is the same object if (a === b) { return true; @@ -1074,7 +1079,7 @@ export function fingerprintCompare(a, b) { // plan a route through an array of polygon center points // starting with the polygon center closest to "start" -export function route(polys, start) { +function route(polys, start) { let centers = []; let first, minDist = Infinity; for (let poly of polys) { @@ -1111,4 +1116,4 @@ export function route(polys, start) { return routed.map(r => r.poly); } -export const polygons = POLYS; +}); diff --git a/src2/geo/slicer.js b/src.old/geo/slicer.js similarity index 97% rename from src2/geo/slicer.js rename to src.old/geo/slicer.js index e9713328..dddf3c04 100644 --- a/src2/geo/slicer.js +++ b/src.old/geo/slicer.js @@ -1,16 +1,23 @@ /** Copyright Stewart Allen -- 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) => { -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'; +const { base } = root; +const { config, util, polygons } = base +const { newOrderedLine, newPolygon, newPoint } = base; + +const POLY = base.polygons; function dval(v, dv) { return v !== undefined ? v : dv; @@ -25,7 +32,7 @@ function dval(v, dv) { * @param {Point[]} points vertex array * @param {Object} options slicing parameters */ -export async function slice(points, options = {}) { +async function slice(points, options = {}) { let zMin = options.zMin || 0, zMax = options.zMax || 0, zInc = options.zInc || 0, @@ -65,6 +72,7 @@ export async function slice(points, options = {}) { for (i = 0; i < points.length; i++) { points[i] = points[i].round(3); } + } // gather z-index stats @@ -78,7 +86,7 @@ export 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(base.util.area2(p1,p2,p3)) / 2; + area = Math.abs(util.area2(p1,p2,p3)) / 2; if (!zFlat[zkey]) { zFlat[zkey] = area; } else { @@ -314,7 +322,7 @@ function makeZLine(phash, p1, p2, coplanar, edge) { * * @param {number} z */ -export async function sliceZ(z, points, options = {}) { +async function sliceZ(z, points, options = {}) { if (Array.isArray(z)) { return Promise.all(z.map(z => sliceZ(z, points, options))); } @@ -380,14 +388,14 @@ export async function sliceZ(z, points, options = {}) { if (groupFn) { let groups = groupFn(lines, z, options); if (options.xor) { - groups = polygons.xor(groups); + groups = POLY.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 = polygons.union(polygons.nest(groups), 0.1, true, opt); + let union = POLY.union(POLY.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 @@ -401,14 +409,14 @@ export async function sliceZ(z, points, options = {}) { } // track total poly length changes to determine if healed rval.changes = opt.changes; - groups = polygons.flatten(union, null, true); + groups = POLY.flatten(union, null, true); } rval.groups = groups; } // look for driver-specific slice post-processor if (options.post) { - let fn = slicer.slicePost[options.post]; + let fn = base.slicePost[options.post]; if (fn) fn(rval, options); } @@ -450,7 +458,7 @@ export async function sliceZ(z, points, options = {}) { * @param {number} [index] * @returns {Array} */ -export function sliceConnect(input, z, opt = {}) { +function sliceConnect(input, z, opt = {}) { let { debug, both } = opt; if (both) { @@ -774,7 +782,7 @@ export function sliceConnect(input, z, opt = {}) { * @param {Line[]} lines * @returns {Line[]} */ -export function removeDuplicateLines(lines) { +function removeDuplicateLines(lines) { let output = [], tmplines = [], points = [], @@ -865,11 +873,12 @@ export function removeDuplicateLines(lines) { return output; } -export const slicer = { +gapp.overlay(base, { slice, sliceZ, slicePost: {}, sliceDedup: removeDuplicateLines, sliceConnect -} +}); +}); diff --git a/src2/geo/slope.js b/src.old/geo/slope.js similarity index 88% rename from src2/geo/slope.js rename to src.old/geo/slope.js index 1777832d..9fc6e67d 100644 --- a/src2/geo/slope.js +++ b/src.old/geo/slope.js @@ -2,13 +2,19 @@ "use strict"; -import { config } from './base.js'; +// dep: geo.base +// use: geo.line +// use: geo.point +gapp.register("geo.slope", [], (root, exports) => { + +const { base } = root; +const { config } = base; const ABS = Math.abs, DEG2RAD = Math.PI / 180, RAD2DEG = 180 / Math.PI; -export class Slope { +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; @@ -88,13 +94,21 @@ function angleWithinDelta(a1, a2, delta) { return (ABS(a1-a2) <= delta || 360-ABS(a1-a2) <= delta); } -export function newSlope(p1, p2, dx, dy) { +function newSlope(p1, p2, dx, dy) { return new Slope(p1, p2, dx, dy); } -export function newSlopeFromAngle(angle) { +function newSlopeFromAngle(angle) { return newSlope(0,0, Math.cos(angle * DEG2RAD), Math.sin(angle * DEG2RAD) ); } + +gapp.overlay(base, { + Slope, + newSlope, + newSlopeFromAngle +}); + +}); diff --git a/src2/geo/wasm.js b/src.old/geo/wasm.js similarity index 92% rename from src2/geo/wasm.js rename to src.old/geo/wasm.js index 87ec5cdd..0e08be8d 100644 --- a/src2/geo/wasm.js +++ b/src.old/geo/wasm.js @@ -1,12 +1,16 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base } from '../geo/base.js'; +"use strict"; +// dep: geo.base +// dep: ext.clip2 +gapp.register("geo.wasm", [], (root, exports) => { + +const { base } = root; const { config } = base; const factor = config.clipper; - -export const wasm_ctrl = { +const wasm_ctrl = { enable, disable, count: { @@ -16,6 +20,8 @@ export const wasm_ctrl = { } }; +gapp.overlay(base, { wasm_ctrl }); + function log() { console.log(...arguments); } @@ -54,7 +60,7 @@ function writePoly(view, poly, inner) { function readPoly(view, z) { let points = view.readU16(true); if (points === 0) return; - let poly = base.newPolygon(); + let poly = self.base.newPolygon(); while (points-- > 0) { poly.add(view.readI32(true)/factor, view.readI32(true)/factor, z || 0); } @@ -73,7 +79,7 @@ function readPolys(view, z, out = []) { return out; } -export function polyOffset(polys, offset, z, clean, simple) { +function polyOffset(polys, offset, z, clean, simple) { wasm_ctrl.count.offset++; let wasm = base.wasm, buffer = wasm.shared, @@ -83,7 +89,7 @@ export function polyOffset(polys, offset, z, clean, simple) { return polyNest(out); } -export function polyUnion(polys, z) { +function polyUnion(polys, z) { wasm_ctrl.count.union++; let wasm = base.wasm, buffer = wasm.shared, @@ -93,7 +99,7 @@ export function polyUnion(polys, z) { return polyNest(out); } -export function polyDiff(polysA, polysB, z, AB, BA) { +function polyDiff(polysA, polysB, z, AB, BA) { wasm_ctrl.count.diff++; let wasm = base.wasm, buffer = wasm.shared, @@ -147,7 +153,7 @@ function readString(pos, len) { return out.join(''); } -export function enable() { +function enable() { if (base.wasm || base._wasm) { return; } @@ -194,9 +200,11 @@ export function enable() { }); } -export function disable() { +function disable() { if (base.wasm) { - base.wasm.free(base.wasm.shared); delete base.wasm; + // console.log({disabled: geo}); } } + +}); diff --git a/src2/kiri-dev/cam/Any.Generic.Grbl.json b/src.old/kiri-dev/cam/Any.Generic.Grbl.json similarity index 100% rename from src2/kiri-dev/cam/Any.Generic.Grbl.json rename to src.old/kiri-dev/cam/Any.Generic.Grbl.json diff --git a/src2/kiri-dev/cam/Any.Generic.LinuxCNC.json b/src.old/kiri-dev/cam/Any.Generic.LinuxCNC.json similarity index 100% rename from src2/kiri-dev/cam/Any.Generic.LinuxCNC.json rename to src.old/kiri-dev/cam/Any.Generic.LinuxCNC.json diff --git a/src2/kiri-dev/cam/Any.Generic.TinyG.json b/src.old/kiri-dev/cam/Any.Generic.TinyG.json similarity index 100% rename from src2/kiri-dev/cam/Any.Generic.TinyG.json rename to src.old/kiri-dev/cam/Any.Generic.TinyG.json diff --git a/src2/kiri-dev/cam/Carbide3D.Nomad.883Pro.json b/src.old/kiri-dev/cam/Carbide3D.Nomad.883Pro.json similarity index 100% rename from src2/kiri-dev/cam/Carbide3D.Nomad.883Pro.json rename to src.old/kiri-dev/cam/Carbide3D.Nomad.883Pro.json diff --git a/src2/kiri-dev/cam/Carbide3D.Shapeoko.3.json b/src.old/kiri-dev/cam/Carbide3D.Shapeoko.3.json similarity index 100% rename from src2/kiri-dev/cam/Carbide3D.Shapeoko.3.json rename to src.old/kiri-dev/cam/Carbide3D.Shapeoko.3.json diff --git a/src2/kiri-dev/cam/Carbide3D.Shapeoko.XL.json b/src.old/kiri-dev/cam/Carbide3D.Shapeoko.XL.json similarity index 100% rename from src2/kiri-dev/cam/Carbide3D.Shapeoko.XL.json rename to src.old/kiri-dev/cam/Carbide3D.Shapeoko.XL.json diff --git a/src2/kiri-dev/cam/Carbide3D.Shapeoko.XXL.json b/src.old/kiri-dev/cam/Carbide3D.Shapeoko.XXL.json similarity index 100% rename from src2/kiri-dev/cam/Carbide3D.Shapeoko.XXL.json rename to src.old/kiri-dev/cam/Carbide3D.Shapeoko.XXL.json diff --git a/src2/kiri-dev/cam/Genmitsu.3018.json b/src.old/kiri-dev/cam/Genmitsu.3018.json similarity index 100% rename from src2/kiri-dev/cam/Genmitsu.3018.json rename to src.old/kiri-dev/cam/Genmitsu.3018.json diff --git a/src2/kiri-dev/cam/Genmitsu.PROVerXL.4030.json b/src.old/kiri-dev/cam/Genmitsu.PROVerXL.4030.json similarity index 100% rename from src2/kiri-dev/cam/Genmitsu.PROVerXL.4030.json rename to src.old/kiri-dev/cam/Genmitsu.PROVerXL.4030.json diff --git a/src2/kiri-dev/cam/Inventables.Carvey.Any.json b/src.old/kiri-dev/cam/Inventables.Carvey.Any.json similarity index 100% rename from src2/kiri-dev/cam/Inventables.Carvey.Any.json rename to src.old/kiri-dev/cam/Inventables.Carvey.Any.json diff --git a/src2/kiri-dev/cam/Inventables.XCarve.1000mm.json b/src.old/kiri-dev/cam/Inventables.XCarve.1000mm.json similarity index 100% rename from src2/kiri-dev/cam/Inventables.XCarve.1000mm.json rename to src.old/kiri-dev/cam/Inventables.XCarve.1000mm.json diff --git a/src2/kiri-dev/cam/Inventables.XCarve.500mm.json b/src.old/kiri-dev/cam/Inventables.XCarve.500mm.json similarity index 100% rename from src2/kiri-dev/cam/Inventables.XCarve.500mm.json rename to src.old/kiri-dev/cam/Inventables.XCarve.500mm.json diff --git a/src2/kiri-dev/cam/Langmuir.MR-1.json b/src.old/kiri-dev/cam/Langmuir.MR-1.json similarity index 100% rename from src2/kiri-dev/cam/Langmuir.MR-1.json rename to src.old/kiri-dev/cam/Langmuir.MR-1.json diff --git a/src2/kiri-dev/cam/Makera.Carvera.json b/src.old/kiri-dev/cam/Makera.Carvera.json similarity index 100% rename from src2/kiri-dev/cam/Makera.Carvera.json rename to src.old/kiri-dev/cam/Makera.Carvera.json diff --git a/src2/kiri-dev/cam/PrintNC.GrblHAL.json b/src.old/kiri-dev/cam/PrintNC.GrblHAL.json similarity index 100% rename from src2/kiri-dev/cam/PrintNC.GrblHAL.json rename to src.old/kiri-dev/cam/PrintNC.GrblHAL.json diff --git a/src2/kiri-dev/cam/RatRig.StrongHold.ONE.750x750mm.json b/src.old/kiri-dev/cam/RatRig.StrongHold.ONE.750x750mm.json similarity index 100% rename from src2/kiri-dev/cam/RatRig.StrongHold.ONE.750x750mm.json rename to src.old/kiri-dev/cam/RatRig.StrongHold.ONE.750x750mm.json diff --git a/src2/kiri-dev/cam/Roland.MDX-50.json b/src.old/kiri-dev/cam/Roland.MDX-50.json similarity index 100% rename from src2/kiri-dev/cam/Roland.MDX-50.json rename to src.old/kiri-dev/cam/Roland.MDX-50.json diff --git a/src2/kiri-dev/cam/ShopBot.Basic.json b/src.old/kiri-dev/cam/ShopBot.Basic.json similarity index 100% rename from src2/kiri-dev/cam/ShopBot.Basic.json rename to src.old/kiri-dev/cam/ShopBot.Basic.json diff --git a/src2/kiri-dev/cam/Sienci.AltMill.json b/src.old/kiri-dev/cam/Sienci.AltMill.json similarity index 100% rename from src2/kiri-dev/cam/Sienci.AltMill.json rename to src.old/kiri-dev/cam/Sienci.AltMill.json diff --git a/src2/kiri-dev/cam/Sienci.LongMill.MK2.json b/src.old/kiri-dev/cam/Sienci.LongMill.MK2.json similarity index 100% rename from src2/kiri-dev/cam/Sienci.LongMill.MK2.json rename to src.old/kiri-dev/cam/Sienci.LongMill.MK2.json diff --git a/src2/kiri-dev/cam/Sienci.LongMill.json b/src.old/kiri-dev/cam/Sienci.LongMill.json similarity index 100% rename from src2/kiri-dev/cam/Sienci.LongMill.json rename to src.old/kiri-dev/cam/Sienci.LongMill.json diff --git a/src2/kiri-dev/cam/Sienci.Mill.One.json b/src.old/kiri-dev/cam/Sienci.Mill.One.json similarity index 100% rename from src2/kiri-dev/cam/Sienci.Mill.One.json rename to src.old/kiri-dev/cam/Sienci.Mill.One.json diff --git a/src2/kiri-dev/cam/Snapmaker.Original.json b/src.old/kiri-dev/cam/Snapmaker.Original.json similarity index 100% rename from src2/kiri-dev/cam/Snapmaker.Original.json rename to src.old/kiri-dev/cam/Snapmaker.Original.json diff --git a/src2/kiri-dev/cam/Techno.Davinci.CNC.json b/src.old/kiri-dev/cam/Techno.Davinci.CNC.json similarity index 100% rename from src2/kiri-dev/cam/Techno.Davinci.CNC.json rename to src.old/kiri-dev/cam/Techno.Davinci.CNC.json diff --git a/src2/kiri-dev/cam/Tormach.24R.json b/src.old/kiri-dev/cam/Tormach.24R.json similarity index 100% rename from src2/kiri-dev/cam/Tormach.24R.json rename to src.old/kiri-dev/cam/Tormach.24R.json diff --git a/src2/kiri-dev/cam/V1Engineering.Lowrider3.json b/src.old/kiri-dev/cam/V1Engineering.Lowrider3.json similarity index 100% rename from src2/kiri-dev/cam/V1Engineering.Lowrider3.json rename to src.old/kiri-dev/cam/V1Engineering.Lowrider3.json diff --git a/src2/kiri-dev/cam/V1Engineering.MPCNC.json b/src.old/kiri-dev/cam/V1Engineering.MPCNC.json similarity index 100% rename from src2/kiri-dev/cam/V1Engineering.MPCNC.json rename to src.old/kiri-dev/cam/V1Engineering.MPCNC.json diff --git a/src2/kiri-dev/drag/Any.Generic.DragKnife.json b/src.old/kiri-dev/drag/Any.Generic.DragKnife.json similarity index 100% rename from src2/kiri-dev/drag/Any.Generic.DragKnife.json rename to src.old/kiri-dev/drag/Any.Generic.DragKnife.json diff --git a/src2/kiri-dev/fdm/Any.Generic.Klipper.json b/src.old/kiri-dev/fdm/Any.Generic.Klipper.json similarity index 100% rename from src2/kiri-dev/fdm/Any.Generic.Klipper.json rename to src.old/kiri-dev/fdm/Any.Generic.Klipper.json diff --git a/src2/kiri-dev/fdm/Any.Generic.Marlin.json b/src.old/kiri-dev/fdm/Any.Generic.Marlin.json similarity index 100% rename from src2/kiri-dev/fdm/Any.Generic.Marlin.json rename to src.old/kiri-dev/fdm/Any.Generic.Marlin.json diff --git a/src2/kiri-dev/fdm/Anycubic.Chiron.json b/src.old/kiri-dev/fdm/Anycubic.Chiron.json similarity index 100% rename from src2/kiri-dev/fdm/Anycubic.Chiron.json rename to src.old/kiri-dev/fdm/Anycubic.Chiron.json diff --git a/src2/kiri-dev/fdm/Anycubic.Kobra.2.json b/src.old/kiri-dev/fdm/Anycubic.Kobra.2.json similarity index 100% rename from src2/kiri-dev/fdm/Anycubic.Kobra.2.json rename to src.old/kiri-dev/fdm/Anycubic.Kobra.2.json diff --git a/src2/kiri-dev/fdm/Anycubic.Kobra.json b/src.old/kiri-dev/fdm/Anycubic.Kobra.json similarity index 100% rename from src2/kiri-dev/fdm/Anycubic.Kobra.json rename to src.old/kiri-dev/fdm/Anycubic.Kobra.json diff --git a/src2/kiri-dev/fdm/Anycubic.Predator.json b/src.old/kiri-dev/fdm/Anycubic.Predator.json similarity index 100% rename from src2/kiri-dev/fdm/Anycubic.Predator.json rename to src.old/kiri-dev/fdm/Anycubic.Predator.json diff --git a/src2/kiri-dev/fdm/Anycubic.i3.Mega.json b/src.old/kiri-dev/fdm/Anycubic.i3.Mega.json similarity index 100% rename from src2/kiri-dev/fdm/Anycubic.i3.Mega.json rename to src.old/kiri-dev/fdm/Anycubic.i3.Mega.json diff --git a/src2/kiri-dev/fdm/Atom.v2.json b/src.old/kiri-dev/fdm/Atom.v2.json similarity index 100% rename from src2/kiri-dev/fdm/Atom.v2.json rename to src.old/kiri-dev/fdm/Atom.v2.json diff --git a/src2/kiri-dev/fdm/Bambu.A1.json b/src.old/kiri-dev/fdm/Bambu.A1.json similarity index 100% rename from src2/kiri-dev/fdm/Bambu.A1.json rename to src.old/kiri-dev/fdm/Bambu.A1.json diff --git a/src2/kiri-dev/fdm/Bambu.P1S.json b/src.old/kiri-dev/fdm/Bambu.P1S.json similarity index 100% rename from src2/kiri-dev/fdm/Bambu.P1S.json rename to src.old/kiri-dev/fdm/Bambu.P1S.json diff --git a/src2/kiri-dev/fdm/Creality.CR-10S-Pro.json b/src.old/kiri-dev/fdm/Creality.CR-10S-Pro.json similarity index 100% rename from src2/kiri-dev/fdm/Creality.CR-10S-Pro.json rename to src.old/kiri-dev/fdm/Creality.CR-10S-Pro.json diff --git a/src2/kiri-dev/fdm/Creality.CR-10S.json b/src.old/kiri-dev/fdm/Creality.CR-10S.json similarity index 100% rename from src2/kiri-dev/fdm/Creality.CR-10S.json rename to src.old/kiri-dev/fdm/Creality.CR-10S.json diff --git a/src2/kiri-dev/fdm/Creality.CR-30.json b/src.old/kiri-dev/fdm/Creality.CR-30.json similarity index 100% rename from src2/kiri-dev/fdm/Creality.CR-30.json rename to src.old/kiri-dev/fdm/Creality.CR-30.json diff --git a/src2/kiri-dev/fdm/Creality.Ender.3.json b/src.old/kiri-dev/fdm/Creality.Ender.3.json similarity index 100% rename from src2/kiri-dev/fdm/Creality.Ender.3.json rename to src.old/kiri-dev/fdm/Creality.Ender.3.json diff --git a/src2/kiri-dev/fdm/Creality.Ender.5.json b/src.old/kiri-dev/fdm/Creality.Ender.5.json similarity index 100% rename from src2/kiri-dev/fdm/Creality.Ender.5.json rename to src.old/kiri-dev/fdm/Creality.Ender.5.json diff --git a/src2/kiri-dev/fdm/Creality.Ender.6.json b/src.old/kiri-dev/fdm/Creality.Ender.6.json similarity index 100% rename from src2/kiri-dev/fdm/Creality.Ender.6.json rename to src.old/kiri-dev/fdm/Creality.Ender.6.json diff --git a/src2/kiri-dev/fdm/Creality.K1.json b/src.old/kiri-dev/fdm/Creality.K1.json similarity index 100% rename from src2/kiri-dev/fdm/Creality.K1.json rename to src.old/kiri-dev/fdm/Creality.K1.json diff --git a/src2/kiri-dev/fdm/Creality.Sermoon.D1.json b/src.old/kiri-dev/fdm/Creality.Sermoon.D1.json similarity index 100% rename from src2/kiri-dev/fdm/Creality.Sermoon.D1.json rename to src.old/kiri-dev/fdm/Creality.Sermoon.D1.json diff --git a/src2/kiri-dev/fdm/Cubicon.Single+.json b/src.old/kiri-dev/fdm/Cubicon.Single+.json similarity index 100% rename from src2/kiri-dev/fdm/Cubicon.Single+.json rename to src.old/kiri-dev/fdm/Cubicon.Single+.json diff --git a/src2/kiri-dev/fdm/Cubicon.Style.json b/src.old/kiri-dev/fdm/Cubicon.Style.json similarity index 100% rename from src2/kiri-dev/fdm/Cubicon.Style.json rename to src.old/kiri-dev/fdm/Cubicon.Style.json diff --git a/src2/kiri-dev/fdm/Dagoma.Disco.Ult.V2.json b/src.old/kiri-dev/fdm/Dagoma.Disco.Ult.V2.json similarity index 100% rename from src2/kiri-dev/fdm/Dagoma.Disco.Ult.V2.json rename to src.old/kiri-dev/fdm/Dagoma.Disco.Ult.V2.json diff --git a/src2/kiri-dev/fdm/DeltaMaker.2T.json b/src.old/kiri-dev/fdm/DeltaMaker.2T.json similarity index 100% rename from src2/kiri-dev/fdm/DeltaMaker.2T.json rename to src.old/kiri-dev/fdm/DeltaMaker.2T.json diff --git a/src2/kiri-dev/fdm/DeltaMaker.Original.json b/src.old/kiri-dev/fdm/DeltaMaker.Original.json similarity index 100% rename from src2/kiri-dev/fdm/DeltaMaker.Original.json rename to src.old/kiri-dev/fdm/DeltaMaker.Original.json diff --git a/src2/kiri-dev/fdm/FLSUN.QQ-S.json b/src.old/kiri-dev/fdm/FLSUN.QQ-S.json similarity index 100% rename from src2/kiri-dev/fdm/FLSUN.QQ-S.json rename to src.old/kiri-dev/fdm/FLSUN.QQ-S.json diff --git a/src2/kiri-dev/fdm/Flashforge.Finder.json b/src.old/kiri-dev/fdm/Flashforge.Finder.json similarity index 100% rename from src2/kiri-dev/fdm/Flashforge.Finder.json rename to src.old/kiri-dev/fdm/Flashforge.Finder.json diff --git a/src2/kiri-dev/fdm/Flashforge.Guider.II.json b/src.old/kiri-dev/fdm/Flashforge.Guider.II.json similarity index 100% rename from src2/kiri-dev/fdm/Flashforge.Guider.II.json rename to src.old/kiri-dev/fdm/Flashforge.Guider.II.json diff --git a/src2/kiri-dev/fdm/FolgerTech.FT5.json b/src.old/kiri-dev/fdm/FolgerTech.FT5.json similarity index 100% rename from src2/kiri-dev/fdm/FolgerTech.FT5.json rename to src.old/kiri-dev/fdm/FolgerTech.FT5.json diff --git a/src2/kiri-dev/fdm/GridBot.One.json b/src.old/kiri-dev/fdm/GridBot.One.json similarity index 100% rename from src2/kiri-dev/fdm/GridBot.One.json rename to src.old/kiri-dev/fdm/GridBot.One.json diff --git a/src2/kiri-dev/fdm/GridBot.Two.json b/src.old/kiri-dev/fdm/GridBot.Two.json similarity index 100% rename from src2/kiri-dev/fdm/GridBot.Two.json rename to src.old/kiri-dev/fdm/GridBot.Two.json diff --git a/src2/kiri-dev/fdm/IMADE3D.JellyBOX.1.N0.4.json b/src.old/kiri-dev/fdm/IMADE3D.JellyBOX.1.N0.4.json similarity index 100% rename from src2/kiri-dev/fdm/IMADE3D.JellyBOX.1.N0.4.json rename to src.old/kiri-dev/fdm/IMADE3D.JellyBOX.1.N0.4.json diff --git a/src2/kiri-dev/fdm/IMADE3D.JellyBOX.2.N0.4.json b/src.old/kiri-dev/fdm/IMADE3D.JellyBOX.2.N0.4.json similarity index 100% rename from src2/kiri-dev/fdm/IMADE3D.JellyBOX.2.N0.4.json rename to src.old/kiri-dev/fdm/IMADE3D.JellyBOX.2.N0.4.json diff --git a/src2/kiri-dev/fdm/IMADE3D.JellyBOX.3.N0.4.json b/src.old/kiri-dev/fdm/IMADE3D.JellyBOX.3.N0.4.json similarity index 100% rename from src2/kiri-dev/fdm/IMADE3D.JellyBOX.3.N0.4.json rename to src.old/kiri-dev/fdm/IMADE3D.JellyBOX.3.N0.4.json diff --git a/src2/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.4.json b/src.old/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.4.json similarity index 100% rename from src2/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.4.json rename to src.old/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.4.json diff --git a/src2/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.6.json b/src.old/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.6.json similarity index 100% rename from src2/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.6.json rename to src.old/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.6.json diff --git a/src2/kiri-dev/fdm/Leapfrog.CreatrHS.json b/src.old/kiri-dev/fdm/Leapfrog.CreatrHS.json similarity index 100% rename from src2/kiri-dev/fdm/Leapfrog.CreatrHS.json rename to src.old/kiri-dev/fdm/Leapfrog.CreatrHS.json diff --git a/src2/kiri-dev/fdm/MakerGear.M2.json b/src.old/kiri-dev/fdm/MakerGear.M2.json similarity index 100% rename from src2/kiri-dev/fdm/MakerGear.M2.json rename to src.old/kiri-dev/fdm/MakerGear.M2.json diff --git a/src2/kiri-dev/fdm/Makerbot.Replicator2.json b/src.old/kiri-dev/fdm/Makerbot.Replicator2.json similarity index 100% rename from src2/kiri-dev/fdm/Makerbot.Replicator2.json rename to src.old/kiri-dev/fdm/Makerbot.Replicator2.json diff --git a/src2/kiri-dev/fdm/Prusa.i3.MK2S.json b/src.old/kiri-dev/fdm/Prusa.i3.MK2S.json similarity index 100% rename from src2/kiri-dev/fdm/Prusa.i3.MK2S.json rename to src.old/kiri-dev/fdm/Prusa.i3.MK2S.json diff --git a/src2/kiri-dev/fdm/Prusa.i3.MK3S+.json b/src.old/kiri-dev/fdm/Prusa.i3.MK3S+.json similarity index 100% rename from src2/kiri-dev/fdm/Prusa.i3.MK3S+.json rename to src.old/kiri-dev/fdm/Prusa.i3.MK3S+.json diff --git a/src2/kiri-dev/fdm/Prusa.mini.json b/src.old/kiri-dev/fdm/Prusa.mini.json similarity index 100% rename from src2/kiri-dev/fdm/Prusa.mini.json rename to src.old/kiri-dev/fdm/Prusa.mini.json diff --git a/src2/kiri-dev/fdm/Raise3D.N2.json b/src.old/kiri-dev/fdm/Raise3D.N2.json similarity index 100% rename from src2/kiri-dev/fdm/Raise3D.N2.json rename to src.old/kiri-dev/fdm/Raise3D.N2.json diff --git a/src2/kiri-dev/fdm/Rolohaun.Rook.MK1.LDO.json b/src.old/kiri-dev/fdm/Rolohaun.Rook.MK1.LDO.json similarity index 100% rename from src2/kiri-dev/fdm/Rolohaun.Rook.MK1.LDO.json rename to src.old/kiri-dev/fdm/Rolohaun.Rook.MK1.LDO.json diff --git a/src2/kiri-dev/fdm/SainSmart.Infi-20.json b/src.old/kiri-dev/fdm/SainSmart.Infi-20.json similarity index 100% rename from src2/kiri-dev/fdm/SainSmart.Infi-20.json rename to src.old/kiri-dev/fdm/SainSmart.Infi-20.json diff --git a/src2/kiri-dev/fdm/Snapmaker.Original.json b/src.old/kiri-dev/fdm/Snapmaker.Original.json similarity index 100% rename from src2/kiri-dev/fdm/Snapmaker.Original.json rename to src.old/kiri-dev/fdm/Snapmaker.Original.json diff --git a/src2/kiri-dev/fdm/Tina2.Basic.json b/src.old/kiri-dev/fdm/Tina2.Basic.json similarity index 100% rename from src2/kiri-dev/fdm/Tina2.Basic.json rename to src.old/kiri-dev/fdm/Tina2.Basic.json diff --git a/src2/kiri-dev/fdm/TypeA.Series1.json b/src.old/kiri-dev/fdm/TypeA.Series1.json similarity index 100% rename from src2/kiri-dev/fdm/TypeA.Series1.json rename to src.old/kiri-dev/fdm/TypeA.Series1.json diff --git a/src2/kiri-dev/fdm/TypeA.Series1Pro.json b/src.old/kiri-dev/fdm/TypeA.Series1Pro.json similarity index 100% rename from src2/kiri-dev/fdm/TypeA.Series1Pro.json rename to src.old/kiri-dev/fdm/TypeA.Series1Pro.json diff --git a/src2/kiri-dev/fdm/ULIO.3D.BRIX.json b/src.old/kiri-dev/fdm/ULIO.3D.BRIX.json similarity index 100% rename from src2/kiri-dev/fdm/ULIO.3D.BRIX.json rename to src.old/kiri-dev/fdm/ULIO.3D.BRIX.json diff --git a/src2/kiri-dev/fdm/ULIO.3D.STEAM.json b/src.old/kiri-dev/fdm/ULIO.3D.STEAM.json similarity index 100% rename from src2/kiri-dev/fdm/ULIO.3D.STEAM.json rename to src.old/kiri-dev/fdm/ULIO.3D.STEAM.json diff --git a/src2/kiri-dev/fdm/Ultimaker.Ultimaker2.json b/src.old/kiri-dev/fdm/Ultimaker.Ultimaker2.json similarity index 100% rename from src2/kiri-dev/fdm/Ultimaker.Ultimaker2.json rename to src.old/kiri-dev/fdm/Ultimaker.Ultimaker2.json diff --git a/src2/kiri-dev/fdm/Wanhao.Duplicator_4s.json b/src.old/kiri-dev/fdm/Wanhao.Duplicator_4s.json similarity index 100% rename from src2/kiri-dev/fdm/Wanhao.Duplicator_4s.json rename to src.old/kiri-dev/fdm/Wanhao.Duplicator_4s.json diff --git a/src2/kiri-dev/fdm/Wanhao.Duplicator_5s.json b/src.old/kiri-dev/fdm/Wanhao.Duplicator_5s.json similarity index 100% rename from src2/kiri-dev/fdm/Wanhao.Duplicator_5s.json rename to src.old/kiri-dev/fdm/Wanhao.Duplicator_5s.json diff --git a/src2/kiri-dev/fdm/Wanhao.Duplicator_6.json b/src.old/kiri-dev/fdm/Wanhao.Duplicator_6.json similarity index 100% rename from src2/kiri-dev/fdm/Wanhao.Duplicator_6.json rename to src.old/kiri-dev/fdm/Wanhao.Duplicator_6.json diff --git a/src2/kiri-dev/fdm/Wanhao.Duplicator_i3.json b/src.old/kiri-dev/fdm/Wanhao.Duplicator_i3.json similarity index 100% rename from src2/kiri-dev/fdm/Wanhao.Duplicator_i3.json rename to src.old/kiri-dev/fdm/Wanhao.Duplicator_i3.json diff --git a/src2/kiri-dev/fdm/XYZ.daVinci.Jr.Pro.json b/src.old/kiri-dev/fdm/XYZ.daVinci.Jr.Pro.json similarity index 100% rename from src2/kiri-dev/fdm/XYZ.daVinci.Jr.Pro.json rename to src.old/kiri-dev/fdm/XYZ.daVinci.Jr.Pro.json diff --git a/src2/kiri-dev/fdm/XYZ.daVinci.Mini.w.json b/src.old/kiri-dev/fdm/XYZ.daVinci.Mini.w.json similarity index 100% rename from src2/kiri-dev/fdm/XYZ.daVinci.Mini.w.json rename to src.old/kiri-dev/fdm/XYZ.daVinci.Mini.w.json diff --git a/src2/kiri-dev/laser/Any.Generic.Laser.json b/src.old/kiri-dev/laser/Any.Generic.Laser.json similarity index 100% rename from src2/kiri-dev/laser/Any.Generic.Laser.json rename to src.old/kiri-dev/laser/Any.Generic.Laser.json diff --git a/src2/kiri-dev/laser/Glowforge.json b/src.old/kiri-dev/laser/Glowforge.json similarity index 100% rename from src2/kiri-dev/laser/Glowforge.json rename to src.old/kiri-dev/laser/Glowforge.json diff --git a/src2/kiri-dev/laser/Snapmaker.A250T.json b/src.old/kiri-dev/laser/Snapmaker.A250T.json similarity index 100% rename from src2/kiri-dev/laser/Snapmaker.A250T.json rename to src.old/kiri-dev/laser/Snapmaker.A250T.json diff --git a/src2/kiri-dev/laser/Snapmaker.A350T.json b/src.old/kiri-dev/laser/Snapmaker.A350T.json similarity index 100% rename from src2/kiri-dev/laser/Snapmaker.A350T.json rename to src.old/kiri-dev/laser/Snapmaker.A350T.json diff --git a/src2/kiri-dev/laser/Snapmaker.Original.json b/src.old/kiri-dev/laser/Snapmaker.Original.json similarity index 100% rename from src2/kiri-dev/laser/Snapmaker.Original.json rename to src.old/kiri-dev/laser/Snapmaker.Original.json diff --git a/src2/kiri-dev/laser/Xtool.P2.json b/src.old/kiri-dev/laser/Xtool.P2.json similarity index 100% rename from src2/kiri-dev/laser/Xtool.P2.json rename to src.old/kiri-dev/laser/Xtool.P2.json diff --git a/src2/kiri-dev/laser/xTool.D1.json b/src.old/kiri-dev/laser/xTool.D1.json similarity index 100% rename from src2/kiri-dev/laser/xTool.D1.json rename to src.old/kiri-dev/laser/xTool.D1.json diff --git a/src2/kiri-dev/laser/xTool.F1.Ultra.json b/src.old/kiri-dev/laser/xTool.F1.Ultra.json similarity index 100% rename from src2/kiri-dev/laser/xTool.F1.Ultra.json rename to src.old/kiri-dev/laser/xTool.F1.Ultra.json diff --git a/src2/kiri-dev/laser/xTool.F1.json b/src.old/kiri-dev/laser/xTool.F1.json similarity index 100% rename from src2/kiri-dev/laser/xTool.F1.json rename to src.old/kiri-dev/laser/xTool.F1.json diff --git a/src2/kiri-dev/laser/xTool.M1.json b/src.old/kiri-dev/laser/xTool.M1.json similarity index 100% rename from src2/kiri-dev/laser/xTool.M1.json rename to src.old/kiri-dev/laser/xTool.M1.json diff --git a/src2/kiri-dev/laser/xTool.P2S.json b/src.old/kiri-dev/laser/xTool.P2S.json similarity index 100% rename from src2/kiri-dev/laser/xTool.P2S.json rename to src.old/kiri-dev/laser/xTool.P2S.json diff --git a/src2/kiri-dev/laser/xTool.S1.json b/src.old/kiri-dev/laser/xTool.S1.json similarity index 100% rename from src2/kiri-dev/laser/xTool.S1.json rename to src.old/kiri-dev/laser/xTool.S1.json diff --git a/src2/kiri-dev/sla/Anycubic.Photon.S.json b/src.old/kiri-dev/sla/Anycubic.Photon.S.json similarity index 100% rename from src2/kiri-dev/sla/Anycubic.Photon.S.json rename to src.old/kiri-dev/sla/Anycubic.Photon.S.json diff --git a/src2/kiri-dev/sla/Anycubic.Photon.json b/src.old/kiri-dev/sla/Anycubic.Photon.json similarity index 100% rename from src2/kiri-dev/sla/Anycubic.Photon.json rename to src.old/kiri-dev/sla/Anycubic.Photon.json diff --git a/src2/kiri-dev/sla/Creality.Halot.Max.json b/src.old/kiri-dev/sla/Creality.Halot.Max.json similarity index 100% rename from src2/kiri-dev/sla/Creality.Halot.Max.json rename to src.old/kiri-dev/sla/Creality.Halot.Max.json diff --git a/src2/kiri-dev/sla/Creality.Halot.One.json b/src.old/kiri-dev/sla/Creality.Halot.One.json similarity index 100% rename from src2/kiri-dev/sla/Creality.Halot.One.json rename to src.old/kiri-dev/sla/Creality.Halot.One.json diff --git a/src2/kiri-dev/sla/Creality.Halot.Sky.json b/src.old/kiri-dev/sla/Creality.Halot.Sky.json similarity index 100% rename from src2/kiri-dev/sla/Creality.Halot.Sky.json rename to src.old/kiri-dev/sla/Creality.Halot.Sky.json diff --git a/src2/kiri-dev/wedm/Any.Generic.WireEDM.json b/src.old/kiri-dev/wedm/Any.Generic.WireEDM.json similarity index 100% rename from src2/kiri-dev/wedm/Any.Generic.WireEDM.json rename to src.old/kiri-dev/wedm/Any.Generic.WireEDM.json diff --git a/src2/kiri-dev/wedm/RackRobo.Betta.Wire.V1.json b/src.old/kiri-dev/wedm/RackRobo.Betta.Wire.V1.json similarity index 100% rename from src2/kiri-dev/wedm/RackRobo.Betta.Wire.V1.json rename to src.old/kiri-dev/wedm/RackRobo.Betta.Wire.V1.json diff --git a/src2/kiri-dev/wjet/Any.Generic.WaterJet.json b/src.old/kiri-dev/wjet/Any.Generic.WaterJet.json similarity index 100% rename from src2/kiri-dev/wjet/Any.Generic.WaterJet.json rename to src.old/kiri-dev/wjet/Any.Generic.WaterJet.json diff --git a/src2/kiri-dev/wjet/HydroBLADE.json b/src.old/kiri-dev/wjet/HydroBLADE.json similarity index 100% rename from src2/kiri-dev/wjet/HydroBLADE.json rename to src.old/kiri-dev/wjet/HydroBLADE.json diff --git a/src/kiri-mode/cam/animate.js b/src.old/kiri-mode/cam/animate.js similarity index 100% rename from src/kiri-mode/cam/animate.js rename to src.old/kiri-mode/cam/animate.js diff --git a/src/kiri-mode/cam/animate2.js b/src.old/kiri-mode/cam/animate2.js similarity index 100% rename from src/kiri-mode/cam/animate2.js rename to src.old/kiri-mode/cam/animate2.js diff --git a/src.old/kiri-mode/cam/client.js b/src.old/kiri-mode/cam/client.js new file mode 100644 index 00000000..8edaee87 --- /dev/null +++ b/src.old/kiri-mode/cam/client.js @@ -0,0 +1,2306 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +// dep: kiri-mode.cam.driver +// dep: kiri.api +// dep: kiri.alerts +// dep: geo.polygon +// use: kiri-mode.cam.animate +// use: kiri-mode.cam.animate2 +// use: kiri-mode.cam.tools +// use: load.gbr +gapp.register("kiri-mode.cam.client", [], (root, exports) => { + +const { base, kiri } = root; +const { Polygon } = base +const { driver, api } = kiri; +const {alerts, conf} = api; +const { CAM } = driver; +const DEG2RAD = Math.PI / 180; +const RAD2DEG = 180 / Math.PI; +const hasSharedArrays = self.SharedArrayBuffer ? true : false; + +let isAnimate, + isArrange, + isPreview, + isCamMode, + isIndexed, + isParsed, + camStock, + camZTop, + camZBottom, + current, + currentIndex, + flipping, + poppedRec, + hoveredOp, + lastMode, + API, FDM, SPACE, STACKS, MODES, VIEWS, UI, UC, LANG, MCAM, WIDGETS; + +let zaxis = { x: 0, y: 0, z: 1 }, + popOp = {}, + animVer = 0, + seed = Date.now(), + func = { + hover: noop, + hoverUp: noop + }; + +function animFn() { + return [{ + animate: CAM.animate, + animate_clear: CAM.animate_clear + },{ + animate: CAM.animate2, + animate_clear: CAM.animate_clear2 + }][animVer]; +} + +CAM.restoreTabs = restoreTabs; + +CAM.init = function(kiri, api) { + FDM = kiri.driver.FDM; + + // console.log({kiri,api}) + WIDGETS = api.widgets; + SPACE = kiri.space; + MODES = kiri.consts.MODES; + VIEWS = kiri.consts.VIEWS; + STACKS = api.const.STACKS; + LANG = api.const.LANG; + MCAM = [ MODES.CAM ]; + UI = api.ui; + UC = api.uc; + API = api; + + function updateAxisMode(refresh) { + const { camStockIndexGrid, camStockIndexed } = current.process; + let newIndexed = camStockIndexed; + let changed = refresh || isIndexed !== newIndexed; + isIndexed = newIndexed; + if (!isIndexed || !isCamMode) { + WIDGETS.setAxisIndex(0); + } + if (!isCamMode) { + return; + } + if (isIndexed) { + current.process.camZAnchor = "middle"; + } + animVer = isIndexed ? 1 : 0; + SPACE.platform.setVisible(!isIndexed); + SPACE.platform.showGrid2(!isIndexed || camStockIndexGrid); + const showIndexed = isIndexed ? '' : 'none'; + const showNonIndexed = isIndexed ? 'none' : ''; + $('cam-index').style.display = showIndexed; + $('cam-lathe').style.display = showIndexed; + $('cam-flip').style.display = showNonIndexed; + $('cam-reg').style.display = showNonIndexed; + if (!changed) { + return; + } + WIDGETS.setIndexed(isIndexed ? true : false); + api.platform.update_bounds(); + // add or remove clock op depending on indexing + const cp = current.process; + if (!cp.ops) { + return; + } + const clockOp = cp.ops.filter(op => op.type === '|')[0]; + if (!clockOp) { + func.opAdd(popOp['|'].new()); + } else { + func.opRender(); + } + updateStock(); + } + + api.event.on("cam.parse.gerber", opts => { + const { data, mesh } = opts; + const { open, closed, circs, rects } = load.GBR.parse(data); + const stack = new kiri.Stack(mesh || moto.space.world.newGroup()); + const layers = new kiri.Layers(); + for (let poly of open) { + layers.setLayer("open", {line: 0xff8800}, false).addPoly(poly); + let diam = poly.tool?.shape?.diameter; + if (diam) { + const exp = poly.offset_open(diam, 'round'); + layers.setLayer("open-exp", {line: 0xff5555}, false).addPolys(exp); + } + } + for (let poly of closed) { + layers.setLayer("close", {line: 0xff0000}, false).addPoly(poly); + } + for (let poly of circs) { + layers.setLayer("circs", {line: 0x008800}, false).addPoly(poly); + } + for (let poly of rects) { + layers.setLayer("rects", {line: 0x0000ff}, false).addPoly(poly); + } + stack.addLayers(layers); + }); + + api.event.on("widget.add", widget => { + if (isCamMode && !Array.isArray(widget)) { + updateAxisMode(true); + widget.setIndexed(isIndexed ? true : false); + api.platform.update_bounds(); + } + }); + + // wire up animate button in ui + api.event.on("function.animate", (mode) => { + if (isAnimate || !isCamMode) { + return; + } + api.function.prepare(() => { + if (isCamMode) { + animate(); + } + }); + }); + + api.event.on("function.export", (mode) => { + if (isAnimate) { + isAnimate = false; + animFn().animate_clear(api); + } + }); + + api.event.on("mode.set", (mode) => { + isIndexed = undefined; + isCamMode = mode === 'CAM'; + SPACE.platform.setColor(isCamMode ? 0xeeeeee : 0xcccccc); + api.uc.setVisible(UI.func.animate, isCamMode); + // hide/show cam mode elements + for (let el of [...document.getElementsByClassName('mode-cam')]) { + api.uc.setClass(el, 'hide', !isCamMode); + } + if (!isCamMode) { + func.clearPops(); + func.tabClear(); + } + // do not persist traces across page reloads + func.traceClear(); + func.opRender(); + updateStock(); + }); + + api.event.on("view.set", (mode) => { + lastMode = mode; + isArrange = (mode === VIEWS.ARRANGE); + isPreview = (mode === VIEWS.PREVIEW); + isAnimate = (mode === VIEWS.ANIMATE); + animFn().animate_clear(api); + func.clearPops(); + if (isCamMode && isPreview) { + WIDGETS.setAxisIndex(0); + } + updateStock(); + func.opRender(); + api.uc.setVisible($('layer-animate'), isAnimate && isCamMode); + }); + + api.event.on("settings.saved", (settings) => { + validateTools(settings.tools); + current = settings; + let proc = settings.process; + let hasTabs = false; + let hasTraces = false; + if (isCamMode && proc.ops) { + proc.ops = proc.ops.filter(v => v); + } + // for any tabs or traces to set markers + for (let widget of API.widgets.all()) { + let wannot = widget.anno; + if (wannot.tab && wannot.tab.length) hasTabs = true; + if (wannot.trace && wannot.trace.length) hasTraces = true; + } + api.platform.update_bounds(); + updateIndex(); + updateStock(); + updateAxisMode(); + if (!poppedRec) { + func.opRender(); + } + }); + + api.event.on("settings.load", (settings) => { + func.opRender(); + if (!isCamMode) return; + validateTools(settings.tools); + restoreTabs(api.widgets.all()); + updateAxisMode(); + }); + + api.event.on("cam.stock.toggle", (bool) => { + camStock && (camStock.visible = bool ?? !camStock.visible); + }); + + api.event.on("boolean.click", api.platform.update_bounds); + + api.event.on([ + "init-done", + "view.set", + // update stock when modes change? + "slice.end", + "preview.end", + // update stock when preferences change + // "boolean.click", + "boolean.update", + // update stock when objects move + "platform.layout", + "selection.drag", + "selection.move", + // force bounds update, too + "selection.scale", + "selection.rotate" + ], updateStock); + + // invalidate trace and drill ops on scale or rotate + api.event.on([ + "selection.scale", + "selection.rotate" + ], () => { + if (!isCamMode) return; + for (let op of current.process.ops) { + if (op.type === 'trace' && !flipping) { + op.areas = {}; + } + else if( op.type === 'drill' && !flipping){ + op.drills = {}; + } + } + }); + + // invalidate tabs when scaleds + api.event.on([ + "selection.scale", + ], () => { + func.tabClear(); + }); + + api.event.on([ + // update tab color/opacity on dark/light change + "boolean.update" + ], updateTabs); + + api.event.on("preview.end", () => { + isParsed = false; + if (isCamMode) { + let bounds = STACKS.getStack("bounds"); + if (bounds) bounds.button("animate", animate); + } + }); + + api.event.on("code.loaded", (info) => { + if (isCamMode) { + isParsed = true; + let parse = STACKS.getStack("parse", SPACE.world); + if (parse) parse.button("animate", animate); + } + }); + + $('op-add').onmouseenter = () => { + if (func.unpop) func.unpop(); + }; + + $('op-add-list').onclick = (ev) => { + let settings = API.conf.get(); + let { process, device } = settings; + switch (ev.target.innerText.toLowerCase()) { + case "index": return func.opAddIndex(); + case "laser on": return func.opAddLaserOn(); + case "laser off": return func.opAddLaserOff(); + case "gcode": return func.opAddGCode(); + case "level": return func.opAddLevel(); + case "rough": return func.opAddRough(); + case "outline": return func.opAddOutline(); + case "contour": + let caxis = "X"; + for (let op of current.process.ops) { + if (op.type === "contour" && op.axis === "X") { + caxis = "Y"; + } + } + return func.opAddContour(caxis); + case "lathe": + let laxis = "X"; + for (let op of current.process.ops) { + if (op.type === "lathe" && op.axis === "X") { + laxis = "Y"; + } + } + return func.opAddLathe(laxis); + case "register": return func.opAddRegister('X', 2); + case "drill": return func.opAddDrill(); + case "trace": return func.opAddTrace(); + case "pocket": return func.opAddPocket(); + case "flip": + // only one flip op permitted + for (let op of current.process.ops) { + if (op.type === 'flip') { + return; + } + } + return func.opAddFlip(); + } + }; + + func.opAddLaserOn = () => { + func.opAdd(popOp['laser on'].new()); + }; + + func.opAddLaserOff = () => { + func.opAdd(popOp['laser off'].new()); + }; + + func.opAddGCode = () => { + func.opAdd(popOp.gcode.new()); + }; + + func.opAddIndex = () => { + func.opAdd(popOp.index.new()); + }; + + func.opAddLevel = () => { + func.opAdd(popOp.level.new()); + }; + + func.opAddRough = () => { + func.opAdd(popOp.rough.new()); + }; + + func.opAddOutline = () => { + func.opAdd(popOp.outline.new()); + }; + + func.opAddPocket = () => { + func.traceDone(); + func.surfaceDone(); + let rec = popOp.pocket.new(); + rec.surfaces = { /* widget.id: [ faces... ] */ }; + func.opAdd(rec); + }; + + func.opAddContour = (axis) => { + let rec = popOp.contour.new(); + rec.axis = axis.toUpperCase(); + func.opAdd(rec); + }; + + func.opAddLathe = (axis) => { + let rec = popOp.lathe.new(); + rec.axis = axis.toUpperCase(); + func.opAdd(rec); + }; + + func.opAddTrace = () => { + let rec = popOp.trace.new(); + rec.areas = { /* widget.id: [ polygons... ] */ }; + func.opAdd(rec); + }; + + func.opAddDrill = () => { + let rec = popOp.drill.new(); + rec.drills = { }; + func.opAdd(rec); + }; + + func.opAddRegister = (axis, points) => { + let rec = popOp.register.new(); + rec.axis = axis.toUpperCase(); + rec.points = points; + func.opAdd(rec); + }; + + func.opAddFlip = () => { + func.opAdd(popOp.flip.new()); + }; + + // TAB/TRACE BUTTON HANDLERS + api.event.on("button.click", target => { + let process = API.conf.get().process; + switch (target) { + case api.ui.tabAdd: + return func.tabAdd(); + case api.ui.tabDun: + return func.tabDone(); + case api.ui.tabClr: + api.uc.confirm("clear tabs?").then(ok => { + if (ok) func.tabClear(); + }); + break; + } + }); + + // OPS FUNCS + api.event.on("cam.op.add", func.opAdd = (rec) => { + if (!isCamMode) return; + func.clearPops(); + let oplist = current.process.ops; + if (oplist.indexOf(rec) < 0) { + if (oplist.length && oplist[oplist.length-1].type === '|') { + oplist.splice(oplist.length-1,0,rec); + } else { + oplist.push(rec); + } + let fpos = oplist.findWith(rec => rec.type === 'flip'); + if (fpos >= 0 && oplist.length > 1) { + let oprec = oplist.splice(fpos,1); + oplist.push(oprec[0]); + } + API.conf.save(); + func.opRender(); + } + }); + + api.event.on("cam.op.del", func.opDel = (rec) => { + if (!isCamMode) return; + func.clearPops(); + let oplist = current.process.ops; + let pos = oplist.indexOf(rec); + if (pos >= 0) { + oplist.splice(pos,1); + API.conf.save(); + func.opRender(); + } + }); + + function updateIndex() { + let oplist = current.process.ops; + if (!(isCamMode && oplist) || lastMode === VIEWS.ANIMATE) { + return; + } + let index = 0; + for (let op of oplist) { + if (op.type === '|') { + break; + } + if (op.type === 'index' && !op.disabled) { + if (op.absolute) { + index = op.degrees + } else { + index += op.degrees; + } + } + } + WIDGETS.setAxisIndex(isPreview || !isIndexed ? 0 : -index); + currentIndex = isIndexed && !isPreview ? index * DEG2RAD : 0; + } + + // (re)render the re-orderable op list + api.event.on("cam.op.render", func.opRender = () => { + let oplist = current.process.ops; + if (!(isCamMode && oplist)) { + return; + } + oplist = oplist.filter(rec => !Array.isArray(rec)); + let mark = Date.now(); + let html = []; + let bind = {}; + let scale = API.view.unit_scale(); + let notime = false; + oplist.forEach((rec,i) => { + let title = ''; + let clock = rec.type === '|'; + let label = clock ? `` : rec.type; + let clazz = notime ? [ "draggable", "notime" ] : [ "draggable" ]; + let notable = rec.note ? rec.note.split(' ').filter(v => v.charAt(0) === '#') : undefined; + if (clock) { clazz.push('clock'); title = ` title="end of ops chain\ndrag/drop like an op\nops after this are disabled"` } + if (notable?.length) label += ` (${notable[0].slice(1)})`; + html.appendAll([ + `
`, + ``, + clock ? '' : + ``, + `
` + ]); + bind[mark+i] = rec; + notime = notime || clock; + }); + let listel = $('oplist'); + listel.innerHTML = html.join(''); + let bounds = []; + let unpop = null; + let index = 0; + let indexing = true; + // drag and drop re-ordering + for (let [id, rec] of Object.entries(bind)) { + let type = rec.type; + let clock = type === '|'; + if (!clock) { + $(`${id}-x`).onmousedown = (ev) => { + ev.stopPropagation(); + ev.preventDefault(); + func.surfaceDone(); + func.traceDone(); + func.tabDone(); + func.opDel(rec); + }; + } else { + indexing = false; + } + let el = $(id); + if (!isIndexed && type === 'lathe') { + rec.disabled = true; + } + if (!hasSharedArrays && (type === 'contour' || type === 'lathe')) { + rec.disabled = true; + } + if (rec.disabled) { + el.classList.add("disabled"); + } + bounds.push(el); + let timer = null; + let inside = true; + let popped = false; + let poprec = popOp[rec.type]; + if (type === 'index' && indexing && !rec.disabled) { + index = rec.absolute ? rec.degrees : index + rec.degrees; + } + el.rec = rec; + el.unpop = () => { + let pos = [...el.childNodes].indexOf(poprec.div); + if (pos >= 0) { + el.removeChild(poprec.div); + } + popped = false; + }; + function onEnter(ev) { + if ((surfaceOn || traceOn) && poppedRec != rec) { + return; + } + if (popped && poppedRec != rec) { + func.surfaceDone(); + func.traceDone(); + } + if (unpop) unpop(); + unpop = func.unpop = el.unpop; + inside = true; + // pointer to current rec for trace editing + poppedRec = rec; + popped = true; + poprec.use(rec); + hoveredOp = el; + if (!clock) { + // offset Y position of pop div by % of Y screen location of button + el.appendChild(poprec.div); + poprec.addNote(); + const { innerHeight } = window; + const brect = ev.target.getBoundingClientRect(); + const prect = poprec.div.getBoundingClientRect(); + const pcty = (brect.top / innerHeight) * 0.9; + const offpx = -pcty * prect.height; + poprec.div.style.transform = `translateY(${offpx}px)`; + } + // option click event appears latent + // and overides the sticky settings + setTimeout(() => { + UC.setSticky(false); + }, 0); + } + function onLeave(ev) { + inside = false; + clearTimeout(timer); + timer = setTimeout(() => { + if (!inside && poprec.using(rec) && !UC.isSticky()) { + el.unpop(); + } + }, 250); + } + function onDown(ev) { + if (!ev.target.rec) { + // only trigger on operation buttons bound to recs + return; + } + let mobile = ev.touches; + func.surfaceDone(); + func.traceDone(); + let target = ev.target, clist = target.classList; + if (!clist.contains("draggable")) { + return; + } + // toggle enable / disable + if (!clock && (ev.ctrlKey || ev.metaKey)) { + ev.preventDefault(); + ev.stopPropagation(); + rec.disabled = !rec.disabled; + for (let op of ev.shiftKey ? oplist : [ rec ]) { + if (op !== rec) { + op.disabled = op.type !== '|' ? !rec.disabled : false; + } + } + for (let el of bounds) { + if (el.rec.disabled) { + el.classList.add("disabled"); + } else { + el.classList.remove("disabled"); + } + } + if (isIndexed) { + updateIndex(); + } + return true; + } + // duplicate op + if (!clock && ev.shiftKey) { + ev.preventDefault(); + ev.stopPropagation(); + oplist = current.process.ops; + oplist.push(Object.clone(rec)); + API.conf.save(); + func.opRender(); + return true; + } + clist.add("drag"); + ev.stopPropagation(); + ev.preventDefault(); + let tracker = UI.tracker; + tracker.style.display = 'block'; + let cancel = tracker.onmouseup = (ev) => { + oplist = current.process.ops; + clist.remove("drag"); + tracker.style.display = 'none'; + if (ev) { + ev.stopPropagation(); + ev.preventDefault(); + } + oplist.length = 0; + for (let child of listel.childNodes) { + oplist.push(child.rec); + } + API.conf.save(); + func.opRender(); + if (mobile) { + el.ontouchmove = onDown; + el.ontouchend = undefined; + } + }; + function onMove(ev) { + ev.stopPropagation(); + ev.preventDefault(); + if (ev.buttons === 0) { + return cancel(); + } + for (let el of bounds) { + if (el === target) continue; + let rect = el.getBoundingClientRect(); + let top = rect.top; + let bottom = rect.bottom;// + rect.height; + let tar = mobile ? ev.touches[0] : ev; + if (tar.pageY >= top && tar.pageY <= bottom) { + let mid = (top + bottom) / 2; + try { listel.removeChild(target); } catch (e) { } + el.insertAdjacentElement(tar.pageY < mid ? "beforebegin" : "afterend", target); + } + } + } + tracker.onmousemove = onMove; + if (mobile) { + el.ontouchmove = onMove; + el.ontouchend = cancel; + } + } + if (moto.space.info.mob) { + let touched = false; + el.ontouchstart = (ev) => { + touched = true; + if (poppedRec === rec && popped) { + onLeave(ev); + } else { + onEnter(ev); + } + }; + el.ontouchmove = onDown; + el.onmouseenter = (ev) => { + if (touched) { + // touches block mouse events on touchscreen PCs + // which are often sent along with touch events + // but when not touched, allow the mouse to work + return; + } + el.onmousedown = onDown; + el.onmouseleave = onLeave; + onEnter(ev); + }; + } else { + el.onmousedown = onDown; + el.onmouseenter = onEnter; + el.onmouseleave = onLeave; + } + } + if (lastMode !== VIEWS.ANIMATE) { + // update widget rotations from timeline marker + WIDGETS.setAxisIndex(isPreview || !isIndexed ? 0 : -index); + } + currentIndex = isIndexed && !isPreview ? index * DEG2RAD : 0; + updateStock(); + }); + + func.opGCode = (label, field = 'gcode') => { + API.dialog.show('any'); + const { c_gcode } = h.bind( + $('mod-any'), h.div({ id: "camop_dialog" }, [ + h.label(label || 'custom gcode operation'), + h.textarea({ id: "c_gcode", rows: 15, cols: 50 }), + h.button({ _: 'done', onclick: () => { + API.dialog.hide(); + API.conf.save(); + } }) + ]) + ); + let av = poppedRec[field] || []; + c_gcode.value = typeof(av) === 'string' ? av : av.join('\n'); + c_gcode.onkeyup = (el) => { + poppedRec[field] = c_gcode.value.trim().split('\n'); + }; + c_gcode.focus(); + }; + + // create custom gcode editor function + function gcodeEditor(label, field) { + return function() { + func.opGCode(label, field); + } + } + + func.opFlip = () => { + API.view.set_arrange(); + let widgets = API.widgets.all(); + let { process } = current; + let { ops, op2 } = process; + // add flip singleton to b-side + let add2 = op2.length === 0; + let axis = poppedRec.axis; + flipping = true; + process.camZAnchor = { + top: "bottom", + bottom: "top", + middle: "middle" + }[process.camZAnchor]; + // flip tabs + for (let widget of widgets) { + let anno = API.widgets.annotate(widget.id).tab || []; + let wbm = widget.bounds.max.z; + for (let tab of anno) { + let box = widget.tabs[tab.id].box; + let bpo = box.position; + let xr = 0, yr = 0; + let flz = wbm - bpo.z; + if (axis === 'X') { + tab.pos.y = -tab.pos.y; + bpo.y = -bpo.y; + xr = Math.PI / 2; + } + if (axis === 'Y') { + tab.pos.x = -tab.pos.x; + bpo.x = -bpo.x; + yr = Math.PI / 2; + } + tab.pos.z = bpo.z = flz; + let [ rx, ry, rz, rw ] = tab.rot; + let qat = new THREE.Quaternion(rx, ry, rz, rw); + let eul = new THREE.Euler().setFromQuaternion(qat); + eul._z = -eul._z; + tab.rot = new THREE.Quaternion().setFromEuler(eul); + } + clearTabs(widget, true); + restoreTabs([widget]); + } + // flip widget + if (axis === 'X') { + API.selection.rotate(Math.PI, 0, 0); + } + if (axis === 'Y') { + API.selection.rotate(0, Math.PI, 0); + } + // clear traces cache + CAM.traces_clear(); + kiri.client.clear(); + flipping = false; + process.ops = op2; + process.op2 = ops; + // flip camZBottom + if (poppedRec.invert && process.camZBottom && camZBottom) { + const maxZ = camZBottom._max.z + process.camZBottom = maxZ - process.camZBottom; + API.util.rec2ui(process); + updateStock(); + } + // keep flip operations in sync + for (let op of op2) { + if (op.type === 'flip') { + op.axis = poppedRec.axis; + op.invert = poppedRec.invert; + } + } + if (add2) { + func.opAdd(poppedRec); + } else { + func.opRender(); + } + }; + + // TAB FUNCS + let showTab, lastTab, tab, iw, ic; + api.event.on("cam.tabs.add", func.tabAdd = () => { + func.traceDone(); + alert = api.show.alert("[esc] cancels tab editing"); + api.feature.hover = true; + func.hover = func.tabHover; + func.hoverUp = func.tabHoverUp; + }); + api.event.on("cam.tabs.done", func.tabDone = () => { + delbox('tabb'); + api.hide.alert(alert); + api.feature.hover = false; + if (lastTab) { + lastTab.box.material.color.r = 0; + lastTab = null; + } + }); + api.event.on("cam.tabs.clear", func.tabClear = () => { + func.tabDone(); + API.widgets.all().forEach(widget => { + clearTabs(widget); + widget.saveState(); + }); + API.conf.save(); + }); + func.tabHover = function(data) { + delbox('tabb'); + const { int, type, point } = data; + const object = int ? int.object : null; + const tab = int ? object.tab : null; + if (lastTab) { + lastTab.box.material.color.r = 0; + lastTab = null; + } + if (tab) { + tab.box.material.color.r = 0.5; + lastTab = tab; + return; + } + if (type !== 'widget') { + iw = null; + return; + } + let n = int.face.normal; + iw = int.object.widget; + ic = int.point; + // only near vertical faces + // if (Math.abs(n.z) > 0.3) { + // return; + // } + showTab = createTabBox(iw, ic, n); + }; + func.tabHoverUp = function(int) { + delbox('tabb'); + if (lastTab) { + const {widget, box, id} = lastTab; + widget.adds.remove(box); + widget.mesh.remove(box); + delete widget.tabs[id]; + let ta = API.widgets.annotate(widget.id).tab; + let ix = 0; + ta.forEach((rec,i) => { + if (rec.id === id) { + ix = i; + } + }); + ta.splice(ix,1); + API.conf.save(); + widget.saveState(); + return; + } + if (!iw) return; + let ip = iw.track.pos; + let wa = api.widgets.annotate(iw.id); + let wt = (wa.tab = wa.tab || []); + let pos = { + x: showTab.pos.x - ip.x, + y: -showTab.pos.z - ip.y, + z: showTab.stock.z ? + showTab.pos.y + ip.z + (isIndexed ? 0 : iw.track.tzoff) : + showTab.dim.z/2, + } + let id = Date.now(); + let { dim, rot } = showTab; + let rec = { pos, dim, rot, id }; + wt.push(Object.clone(rec)); + addWidgetTab(iw, rec); + API.conf.save(); + iw.saveState(); + }; + + // SURFACE FUNCS + let surfaceOn = false, lastWidget; + func.surfaceAdd = (ev) => { + if (surfaceOn) { + return func.surfaceDone(); + } + func.clearPops(); + alert = api.show.alert("analyzing surfaces...", 1000); + let surfaces = poppedRec.surfaces; + let radians = poppedRec.follow * DEG2RAD; + CAM.surface_prep(currentIndex * RAD2DEG, () => { + api.hide.alert(alert); + alert = api.show.alert("[esc] cancels surface selection"); + for (let [wid, arr] of Object.entries(surfaces)) { + let widget = api.widgets.forid(wid); + if (widget && arr.length) + for (let faceid of arr) { + CAM.surface_toggle(widget, faceid, radians, faceids => { + // surfaces[widget.id] = faceids; + }); + } + } + }); + surfaceOn = hoveredOp; + surfaceOn.classList.add("editing"); + api.feature.on_mouse_up = (obj, ev) => { + let { face } = obj; + let min = Math.min(face.a, face.b, face.c); + let faceid = min / 3; + let widget = lastWidget = obj.object.widget; + CAM.surface_toggle(widget, faceid, radians, faceids => { + surfaces[widget.id] = faceids; + }); + }; + }; + func.surfaceDone = () => { + if (!(surfaceOn && poppedRec && poppedRec.surfaces)) { + return; + } + let surfaces = poppedRec.surfaces; + for (let wid of Object.keys(surfaces)) { + let widget = api.widgets.forid(wid); + if (widget) { + CAM.surface_clear(widget); + } else { + delete surfaces[wid]; + } + } + api.hide.alert(alert); + api.feature.on_mouse_up = undefined; + surfaceOn.classList.remove("editing"); + surfaceOn = false; + }; + + // TRACE FUNCS + let traceOn = false, lastTrace; + func.traceAdd = (ev) => { + if (traceOn) { + return func.traceDone(); + } + func.clearPops(); + alert = api.show.alert("analyzing parts...", 1000); + traceOn = hoveredOp; + traceOn.classList.add("editing"); + api.feature.hover = true; + api.feature.hoverAdds = true; + func.hover = func.traceHover; + func.hoverUp = func.traceHoverUp; + CAM.traces((ids) => { + api.hide.alert(alert); + alert = api.show.alert("[esc] cancels trace editing"); + kiri.api.widgets.for(widget => { + if (ids.indexOf(widget.id) >= 0) { + unselectTraces(widget, true); + widget.trace_stack = null; + } + if (widget.trace_stack) { + widget.adds.appendAll(widget.trace_stack.meshes); + widget.trace_stack.show(); + return; + } + let areas = (poppedRec.areas[widget.id] || []); + let stack = new kiri.Stack(widget.mesh); + widget.trace_stack = stack; + widget.traces.forEach(poly => { + let match = areas.filter(arr => poly.matches(arr)); + let layers = new kiri.Layers(); + layers.setLayer("trace", {line: 0xaaaa55, fat:4, order:-10}, false).addPoly(poly); + stack.addLayers(layers); + stack.new_meshes.forEach(mesh => { + mesh.trace = {widget, poly}; + // ensure trace poly singleton from matches + if (match.length > 0) { + poly._trace = match[0]; + } else { + poly._trace = poly.toArray(); + } + }); + widget.adds.appendAll(stack.new_meshes); + // console.log(widget) + }); + }); + // ensure appropriate traces are toggled matching current record + kiri.api.widgets.for(widget => { + widget.setVisualState({ opacity: 0.25 }); + let areas = (poppedRec.areas[widget.id] || []); + let stack = widget.trace_stack; + stack.meshes.forEach(mesh => { + let { poly } = mesh.trace; + let match = areas.filter(arr => poly.matches(arr)); + if (match.length > 0) { + if (!mesh.selected) { + func.traceToggle(mesh, true); + } + } else if (mesh.selected) { + func.traceToggle(mesh, true); + } + }); + }); + }, poppedRec.select === 'lines'); + }; + func.traceDone = () => { + if (!traceOn) { + return; + } + func.unpop(); + traceOn.classList.remove("editing"); + traceOn = false; + kiri.api.widgets.opacity(1); + api.hide.alert(alert); + api.feature.hover = false; + api.feature.hoverAdds = false; + kiri.api.widgets.for(widget => { + widget.restoreVisualState(); + if (widget.trace_stack) { + widget.trace_stack.hide(); + widget.adds.removeAll(widget.trace_stack.meshes); + } + }); + }; + func.clearPops = () => { + if (func.unpop) func.unpop(); + func.tabDone(); + func.traceDone(); + func.surfaceDone(); + func.selectHolesDone(); + }; + api.event.on("cam.trace.clear", func.traceClear = () => { + func.traceDone(); + API.widgets.all().forEach(widget => { + unselectTraces(widget); + }); + API.conf.save(); + }); + func.traceHover = function(data) { + if (lastTrace) { + let { color, colorSave } = lastTrace.material[0] || lastTrace.material; + color.r = colorSave.r; + color.g = colorSave.g; + color.b = colorSave.b; + } + lastTrace = null; + if (data.type === 'platform') { + return; + } + if (!data.int.object.trace) { + return; + } + lastTrace = data.int.object; + if (lastTrace.selected) { + let event = data.event; + let target = event.target; + let { clientX, clientY } = event; + let { offsetWidth, offsetHeight } = target; + } + let material = lastTrace.material[0] || lastTrace.material; + let color = material.color; + material.colorSave = color.clone(); + color.setHex(isDark() ? 0x0066ff : 0x0000ff); + }; + func.traceHoverUp = function(int, ev) { + if (!int) return; + let { object } = int; + func.traceToggle(object); + if (ev.metaKey || ev.ctrlKey) { + let { selected } = object; + let { widget, poly } = object.trace; + let avgZ = poly.avgZ(); + for (let add of widget.adds) { + if (add.trace && add.selected !== selected && add.trace.poly.onZ(avgZ)) { + func.traceToggle(add); + } + } + } + }; + func.traceToggle = function(obj, skip) { + let material = obj.material[0] || obj.material; + if (!material) return; + let { color, colorSave } = material; + let { widget, poly } = obj.trace; + let areas = poppedRec.areas; + if (!areas) { + return; + } + let wlist = areas[widget.id] = areas[widget.id] || []; + obj.selected = !obj.selected; + if (!colorSave) { + colorSave = material.colorSave = color.clone(); + } + if (obj.selected) { + color.setHex(isDark() ? 0xdd0011 : 0xff0033); + colorSave.r = color.r; + colorSave.g = color.g; + colorSave.b = color.b; + if (!skip) wlist.push(poly._trace); + } else { + color.setHex(0xaaaa55); + colorSave.setHex(0xaaaa55); + if (!skip) wlist.remove(poly._trace); + } + API.conf.save(); + }; + + + let holeSelOn = false, lastSelHoles; + /** + * Client side function to select holes in widgets for CAM operations. + * If holes have already been analyzed, they are displayed immediately. + * Otherwise, the CAM server is queried and the results are cached. + * @param {boolean} individual - select all holes, false to only select + * holes of tool diameter. + */ + func.selectHoles = async function(individual){ + // console.log("client individual selected",individual) + if (holeSelOn) { + return func.selectHolesDone(); + } + func.clearPops(); + let alert = api.show.alert("analyzing parts..."); + holeSelOn = hoveredOp; + holeSelOn.classList.add("editing"); + api.feature.hover = true; + api.feature.hoverAdds = true; + func.hover = func.selectHolesHover; + func.hoverUp = func.selectHolesHoverUp; + + const widgets = kiri.api.widgets.all() + /** + * creates a mesh for a hole and adds it to a widget + * @param {Object3D} widget - widget to add the hole mesh to + * @param {Object} drill - {depth,selected} object of the hole + * @returns {Mesh} the created mesh + */ + function createHoleMesh(widget,drill){ + let {depth,selected, diam} = drill + let color = selected ? 0xFF0000:0x39e366 + let geo = new THREE.CylinderGeometry(diam/2,diam/2,depth,20); + const material = new THREE.MeshPhongMaterial( { color } ); + let mesh = new THREE.Mesh(geo, material); + mesh.position.copy(drill) + mesh.position.z -= depth/2 + mesh.rotation.x = Math.PI/2 + drill.widgetID = widget.id + drill.meshId = mesh.id; //add pointers to both objects + mesh.hole = drill; + mesh.parent = widget.mesh; + widget.mesh.add(mesh); + widget.adds.push(mesh); //for click detection + return mesh + } + let meshesCached = widgets.every(widget=>poppedRec.drills[widget.id] != undefined) + if( individual && meshesCached ){ // if any widget already has cached holes + // console.log("already has cached holes",poppedRec.drills) + api.hide.alert(alert) + kiri.api.widgets.for(widget => { + if(widget.adds){ + let drills = poppedRec.drills[widget.id] + let ids = drills + .map(hole => hole.meshId) + if(widget.adds.includes(mesh)){ + widget.adds.forEach(add=>{ + if(ids.includes(add.id)){ + add.visible = true + } + }) + }else{ + drills.forEach(drill => { + createHoleMesh(widget, drill) + }) + } + } + }) + + }else{ // if no widget has cached holes + let alert = api.show.alert(""); + await CAM.holes( + individual, + poppedRec, + (progress,msg)=>{ + alert[0]=msg + api.show.progress(progress,msg) + api.alerts.update() + }, + async centers => { + api.show.progress(0) + if(! Array.isArray(centers) ){ + console.log("worker returned a malformed drills response") + return + } + let shadow = centers.some(c=>c.shadowed) + api.hide.alert(alert) + if(shadow){ + alert = api.show.alert("Some holes are shadowed by part and are not shown."); + } + centers = centers ?? [] + // list of all hole centers and if they are selected + kiri.api.widgets.for(widget => { + const {holes} = centers.find(center=>center.id == widget.id) + // console.log(holes) + if (!holes.length) unselectHoles(holes); + holes.forEach(hole => { + createHoleMesh(widget, hole) + }) + //add hole data to record + poppedRec.drills = poppedRec.drills ?? {} + poppedRec.drills[widget.id] = holes + //give widget access to an array of drill records that refrence it + //so that it can be cleared when widget is rotated or mirrored etc. + if(!widget.drills){widget.drills = []} + widget.drills.push(holes) + }) + } + ); + } + //hide the alert once hole meshes are calculated on the worker, and then added to the scene + // api.hide.alert(alert); + let escAlert = api.show.alert("[esc] cancels drill editing",1000); + setTimeout(() => { + api.hide.alert(escAlert); + }, 5000); + kiri.api.widgets.opacity(0.8); + } + + func.selectHolesHover = function(data) { + //not used right now. may be useful in the future + if (lastTrace) { + let { color, colorSave } = lastTrace.material[0] || lastTrace.material; + color.r = colorSave.r; + color.g = colorSave.g; + color.b = colorSave.b; + lastTrace.position.z -= 0.01; + } + if (data.type === 'platform') { + lastTrace = null; + return; + } + } + + func.selectHolesHoverUp = function(int, ev) { + if (!int) return; //if not a hole mesh return + let { object } = int; + func.selectHoleToggle(object); + } + + /** + * Toggle the selection of a hole mesh and update its color + * @param {Object3D} mesh - the hole mesh to toggle + */ + func.selectHoleToggle = function(mesh) { + let {hole} = mesh + if(!hole) return + hole.selected = !hole.selected; + mesh.material.color.setHex( hole.selected ? 0xFF0000:0x39e366 ); + } + + /** + * Clears the recorded holes in the widget (widget.drills array) + * and also clears the widget.adds array. + * @param {Object} widget - the widget with the drills array to clear + */ + func.clearHolesRec = (widget)=>{ + if(widget.drills){ + widget.drills.forEach(rec=>{ + }) + } + if(widget.adds){ + widget.adds.length = 0 //clear adds array + } + } + + /** + * Cleanup function for selectHoles. + * Removes all adds from the scene, hides the alert, and resets the opacity of the widgets. + * Also resets the hover features and the editing class on the holeSelOn html element. + */ + func.selectHolesDone = () => { + if (!holeSelOn) { + return; + } + func.unpop(); + holeSelOn.classList.remove("editing"); + holeSelOn = false; + kiri.api.widgets.opacity(1); + api.hide.alert(alert); + api.feature.hover = false; + api.feature.hoverAdds = false; + + kiri.api.widgets.for(widget => { + for(let add of widget.adds){ + add.visible = false + widget.mesh.remove(add); + } + }); + }; + + // COMMON TAB/TRACE EVENT HANDLERS + api.event.on("slice.begin", () => { + if (isCamMode) { + func.clearPops(); + } + }); + api.event.on("key.esc", () => { + if (isCamMode) { + func.clearPops(); + } + }); + api.event.on("selection.scale", () => { + if (isCamMode) { + func.clearPops(); + } + }); + api.event.on("widget.duplicate", (widget, oldwidget) => { + if (!isCamMode) { + return; + } + if (traceOn) { + func.traceDone(); + } + unselectTraces(widget); + if (flipping) { + return; + } + let ann = API.widgets.annotate(widget.id); + if (ann.tab) { + ann.tab.forEach((tab,i) => { + tab.id = Date.now() + i; + }); + restoreTabs([widget]); + } + }); + api.event.on("widget.mirror", widget => { + if (!isCamMode) { + return; + } + if (traceOn) { + func.traceDone(); + } + if (holeSelOn) { + func.selectHolesDone(); + } + func.clearHolesRec(widget) + unselectTraces(widget); + if (flipping) { + return; + } + mirrorTabs(widget); + }); + api.event.on("widget.rotate", rot => { + if (!isCamMode) { + return; + } + let {widget, x, y, z} = rot; + if (traceOn) { + func.traceDone(); + } + unselectTraces(widget); + if (holeSelOn) { + func.selectHolesDone(); + } + if (flipping) { + return; + } + func.clearHolesRec(widget) + if (x || y) { + clearTabs(widget); + } else { + rotateTabs(widget, x, y, z); + } + }); + api.event.on("mouse.hover.up", rec => { + if (!isCamMode) { + return; + } + let { object, event } = rec; + func.hoverUp(object, event); + }); + api.event.on("mouse.hover", data => { + if (!isCamMode) { + return; + } + func.hover(data); + }); + + function mirrorTabs(widget) { + let tabs = API.widgets.annotate(widget.id).tab || []; + tabs.forEach(rec => { + let { id, pos, rot } = rec; + let tab = widget.tabs[id]; + let e = new THREE.Euler().setFromQuaternion(rot); + e._z = Math.PI - e._z; + let { _x, _y, _z, _w } = rec.rot; + let or = new THREE.Quaternion(_x, _y, _z, _w); + let nr = new THREE.Quaternion().setFromEuler(e); + let ra = or.angleTo(nr); + // console.log({or, nr, ra}); + rec.rot = nr; + // let m4 = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(0,0,e._z)); + // tab.box.geometry.applyMatrix4(m4); + tab.box.position.x = pos.x = -pos.x; + }); + SPACE.update(); + } + + function rotateTabs(widget, x, y, z) { + let tabs = API.widgets.annotate(widget.id).tab || []; + tabs.forEach(rec => { + let { id, pos, rot } = rec; + if (!Array.isArray(rot)) { + rot = rot.toArray(); + } + let coff = widget.track.center; + let tab = widget.tabs[id]; + let m4 = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(x || 0, y || 0, z || 0)); + // update position vector + let vc = new THREE.Vector3(pos.x, pos.y, pos.z).applyMatrix4(m4); + // update rotation quaternion + let [ rx, ry, rz, rw ] = rot; + rec.rot = new THREE.Quaternion().multiplyQuaternions( + new THREE.Quaternion(rx, ry, rz, rw), + new THREE.Quaternion().setFromRotationMatrix(m4) + ).toArray(); + tab.box.geometry.applyMatrix4(m4); + tab.box.position.x = pos.x = vc.x - coff.dx; + tab.box.position.y = pos.y = vc.y - coff.dy; + tab.box.position.z = pos.z = vc.z; + }); + SPACE.update(); + } + + function hasIndexing() { + return isIndexed; + } + + function hasSpindle() { + return current.device.spindleMax > 0; + } + + function zTop() { + return API.conf.get().process.camZTop > 0; + } + + function zBottom() { + return API.conf.get().process.camZBottom > 0; + } + + createPopOp('level', { + tool: 'camLevelTool', + spindle: 'camLevelSpindle', + step: 'camLevelOver', + rate: 'camLevelSpeed', + down: 'camLevelDown', + over: 'camLevelOver', + stock: 'camLevelStock' + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + down: UC.newInput(LANG.cc_loff_s, {title:LANG.cc_loff_l, convert:UC.toFloat, units:true}), + over: UC.newInput(LANG.cc_lxyo_s, {title:LANG.cc_lxyo_l, convert:UC.toFloat, units:true}), + sep: UC.newBlank({class:"pop-sep"}), + stock: UC.newBoolean(LANG.cc_lsto_s, undefined, {title:LANG.cc_lsto_l}), + }; + + createPopOp('rough', { + tool: 'camRoughTool', + spindle: 'camRoughSpindle', + down: 'camRoughDown', + step: 'camRoughOver', + rate: 'camRoughSpeed', + plunge: 'camRoughPlunge', + leave: 'camRoughStock', + leavez: 'camRoughStockZ', + all: 'camRoughAll', + voids: 'camRoughVoid', + flats: 'camRoughFlat', + inside: 'camRoughIn', + ov_topz: 0, + ov_botz: 0, + ov_conv: '~camConventional', + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + sep: UC.newBlank({class:"pop-sep"}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}), + leave: UC.newInput(LANG.cr_lsto_s, {title:LANG.cr_lsto_l, convert:UC.toFloat, units:true}), + leavez: UC.newInput(LANG.cr_lstz_s, {title:LANG.cr_lstz_l, convert:UC.toFloat, bound:UC.bound(0,10),units:true}), + sep: UC.newBlank({class:"pop-sep"}), + all: UC.newBoolean(LANG.cr_clst_s, undefined, {title:LANG.cr_clst_l, show:hasIndexing}), + voids: UC.newBoolean(LANG.cr_clrp_s, undefined, {title:LANG.cr_clrp_l}), + flats: UC.newBoolean(LANG.cr_clrf_s, undefined, {title:LANG.cr_clrf_l}), + inside: UC.newBoolean(LANG.cr_olin_s, undefined, {title:LANG.cr_olin_l}), + sep: UC.newBlank({class:"pop-sep"}), + exp: UC.newExpand("overrides"), + ov_topz: UC.newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:UC.toFloat, units:true}), + ov_botz: UC.newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:UC.toFloat, units:true}), + ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, {title:LANG.ou_conv_l}), + exp_end: UC.endExpand(), + }; + + createPopOp('outline', { + tool: 'camOutlineTool', + spindle: 'camOutlineSpindle', + step: 'camOutlineOver', + steps: 'camOutlineOverCount', + down: 'camOutlineDown', + rate: 'camOutlineSpeed', + plunge: 'camOutlinePlunge', + dogbones: 'camOutlineDogbone', + omitvoid: 'camOutlineOmitVoid', + omitthru: 'camOutlineOmitThru', + outside: 'camOutlineOut', + inside: 'camOutlineIn', + wide: 'camOutlineWide', + top: 'camOutlineTop', + ov_topz: 0, + ov_botz: 0, + ov_conv: '~camConventional', + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0), show:() => popOp.outline.rec.wide}), + steps: UC.newInput(LANG.cc_sovc_s, {title:LANG.cc_sovc_l, convert:UC.toInt, bound:UC.bound(1,5), show:() => popOp.outline.rec.wide}), + sep: UC.newBlank({class:"pop-sep"}), + top: UC.newBoolean(LANG.co_clrt_s, undefined, {title:LANG.co_clrt_l}), + inside: UC.newBoolean(LANG.co_olin_s, undefined, {title:LANG.co_olin_l, show:(op) => { return !op.inputs.outside.checked }}), + outside: UC.newBoolean(LANG.co_olot_s, undefined, {title:LANG.co_olot_l, show:(op) => { return !op.inputs.inside.checked }}), + sep: UC.newBlank({class:"pop-sep"}), + omitthru: UC.newBoolean(LANG.co_omit_s, undefined, {title:LANG.co_omit_l, xshow:(op) => { return op.inputs.outside.checked }}), + omitvoid: UC.newBoolean(LANG.co_omvd_s, undefined, {title:LANG.co_omvd_l, xshow:(op) => { return op.inputs.outside.checked }}), + wide: UC.newBoolean(LANG.co_wide_s, undefined, {title:LANG.co_wide_l, show:(op) => { return !op.inputs.inside.checked }}), + dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, {title:LANG.co_dogb_l, show:(op) => { return !op.inputs.wide.checked }}), + sep: UC.newBlank({class:"pop-sep"}), + exp: UC.newExpand("overrides"), + ov_topz: UC.newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:UC.toFloat, units:true}), + ov_botz: UC.newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:UC.toFloat, units:true}), + ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, {title:LANG.ou_conv_l}), + exp_end: UC.endExpand(), + }; + + const contourFilter = gcodeEditor('Layer Filter', 'filter'); + + createPopOp('contour', { + tool: 'camContourTool', + spindle: 'camContourSpindle', + step: 'camContourOver', + rate: 'camContourSpeed', + angle: 'camContourAngle', + leave: 'camContourLeave', + tolerance: 'camTolerance', + flatness: 'camFlatness', + reduction: 'camContourReduce', + bridging: 'camContourBridge', + bottom: 'camContourBottom', + curves: 'camContourCurves', + inside: 'camContourIn', + filter: 'camContourFilter', + axis: 'X' + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + axis: UC.newSelect(LANG.cd_axis, {}, "xyaxis"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + sep: UC.newBlank({class:"pop-sep"}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,10.0)}), + leave: UC.newInput(LANG.cf_leav_s, {title:LANG.cf_leav_l, convert:UC.toFloat, bound:UC.bound(0,100)}), + sep: UC.newBlank({class:"pop-sep"}), + angle: UC.newInput(LANG.cf_angl_s, {title:LANG.cf_angl_l, convert:UC.toFloat, bound:UC.bound(45,90), show:(op) => op.inputs.curves.checked}), + flatness: UC.newInput(LANG.ou_flat_s, {title:LANG.ou_flat_l, convert:UC.toFloat, bound:UC.bound(0,1.0), units:false, round:4}), + tolerance: UC.newInput(LANG.ou_toll_s, {title:LANG.ou_toll_l, convert:UC.toFloat, bound:UC.bound(0,10.0), units:true, round:4}), + reduction: UC.newInput(LANG.ou_redu_s, {title:LANG.ou_redu_l, convert:UC.toInt, bound:UC.bound(0,10), units:false}), + // bridging: UC.newInput(LANG.ou_brdg_s, {title:LANG.ou_brdg_l, convert:UC.toFloat, bound:UC.bound(0,1000.0), units:true, round:4, show:(op) => op.inputs.curves.checked}), + sep: UC.newBlank({class:"pop-sep"}), + curves: UC.newBoolean(LANG.cf_curv_s, undefined, {title:LANG.cf_curv_l}), + inside: UC.newBoolean(LANG.cf_olin_s, undefined, {title:LANG.cf_olin_l}), + bottom: UC.newBoolean(LANG.cf_botm_s, undefined, {title:LANG.cf_botm_l, show:(op,conf) => conf ? conf.process.camZBottom : 0}), + filter: UC.newRow([ UC.newButton(LANG.filter, contourFilter) ], {class:"ext-buttons f-row"}) + }; + + createPopOp('lathe', { + tool: 'camLatheTool', + spindle: 'camLatheSpindle', + step: 'camLatheOver', + angle: 'camLatheAngle', + rate: 'camLatheSpeed', + tolerance: 'camTolerance', + filter: 'camContourFilter', + leave: 'camContourLeave', + linear: 'camLatheLinear' + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + // axis: UC.newSelect(LANG.cd_axis, {}, "xyaxis"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + sep: UC.newBlank({class:"pop-sep"}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,100.0)}), + angle: UC.newInput(LANG.cc_sang_s, {title:LANG.cc_sang_l, convert:UC.toFloat, bound:UC.bound(0.01,180.0)}), + sep: UC.newBlank({class:"pop-sep"}), + tolerance: UC.newInput(LANG.ou_toll_s, {title:LANG.ou_toll_l, convert:UC.toFloat, bound:UC.bound(0,10.0), units:true, round:4}), + leave: UC.newInput(LANG.cf_leav_s, {title:LANG.cf_leav_l, convert:UC.toFloat, bound:UC.bound(0,100)}), + sep: UC.newBlank({class:"pop-sep"}), + linear: UC.newBoolean(LANG.ci_line_s, undefined, {title:LANG.ci_line_l}), + // filter: UC.newRow([ UC.newButton(LANG.filter, contourFilter) ], {class:"ext-buttons f-row"}) + }; + + function canDogBones() { + if (!poppedRec) return false; + return poppedRec.mode === 'follow';// && poppedRec.offset && poppedRec.offset !== 'none'; + } + + function canDogBonesRev() { + return canDogBones() && poppedRec.dogbone; + } + + function zDogSep() { + return canDogBones() || zBottom(); + } + + createPopOp('trace', { + mode: 'camTraceType', + offset: 'camTraceOffset', + spindle: 'camTraceSpindle', + tool: 'camTraceTool', + step: 'camTraceOver', + down: 'camTraceDown', + thru: 'camTraceThru', + rate: 'camTraceSpeed', + plunge: 'camTracePlunge', + offover: 'camTraceOffOver', + dogbone: 'camTraceDogbone', + revbone: 'camTraceDogbone', + merge: 'camTraceMerge', + ov_topz: 0, + ov_botz: 0, + ov_conv: '~camConventional', + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + mode: UC.newSelect(LANG.cu_type_s, {title:LANG.cu_type_l}, "trace"), + offset: UC.newSelect(LANG.cc_offs_s, {title: LANG.cc_offs_l, show:() => (poppedRec.mode === 'follow')}, "traceoff"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + sep: UC.newBlank({class:"pop-sep"}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0), show:(op) => popOp.trace.rec.mode === "clear"}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), + thru: UC.newInput(LANG.cc_thru_s, {title:LANG.cc_thru_l, convert:UC.toFloat, units:true}), + offover: UC.newInput(LANG.cc_offd_s, {title:LANG.cc_offd_l, convert:UC.toFloat, units:true, show:() => poppedRec.offset !== "none" || poppedRec.mode === "clear"}), + sep: UC.newBlank({class:"pop-sep", modes:MCAM, xshow:zDogSep}), + merge: UC.newBoolean(LANG.co_merg_s, undefined, {title:LANG.co_merg_l, show:() => !popOp.trace.rec.down}), + dogbone: UC.newBoolean(LANG.co_dogb_s, undefined, {title:LANG.co_dogb_l, show:canDogBones}), + revbone: UC.newBoolean(LANG.co_dogr_s, undefined, {title:LANG.co_dogr_l, show:canDogBonesRev}), + exp: UC.newExpand("overrides"), + sep: UC.newBlank({class:"pop-sep"}), + ov_topz: UC.newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:UC.toFloat, units:true}), + ov_botz: UC.newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:UC.toFloat, units:true}), + ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, {title:LANG.ou_conv_l}), + exp_end: UC.endExpand(), + sep: UC.newBlank({class:"pop-sep"}), + menu: UC.newRow([ UC.newButton("select", func.traceAdd) ], {class:"ext-buttons f-row"}), + }; + + createPopOp('pocket', { + spindle: 'camPocketSpindle', + tool: 'camPocketTool', + step: 'camPocketOver', + down: 'camPocketDown', + rate: 'camPocketSpeed', + plunge: 'camPocketPlunge', + expand: 'camPocketExpand', + smooth: 'camPocketSmooth', + refine: 'camPocketRefine', + follow: 'camPocketFollow', + contour: 'camPocketContour', + engrave: 'camPocketEngrave', + outline: 'camPocketOutline', + ov_topz: 0, + ov_botz: 0, + ov_conv: '~camConventional', + tolerance: 'camTolerance', + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), + plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + sep: UC.newBlank({class:"pop-sep"}), + step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true, show:() => !poppedRec.contour}), + sep: UC.newBlank({class:"pop-sep"}), + expand: UC.newInput(LANG.cp_xpnd_s, {title:LANG.cp_xpnd_l, convert:UC.toFloat, units:true, xshow:() => !poppedRec.contour}), + refine: UC.newInput(LANG.cp_refi_s, {title:LANG.cp_refi_l, convert:UC.toInt, show:() => poppedRec.contour}), + smooth: UC.newInput(LANG.cp_smoo_s, {title:LANG.cp_smoo_l, convert:UC.toInt}), + tolerance: UC.newInput(LANG.ou_toll_s, {title:LANG.ou_toll_l, convert:UC.toFloat, bound:UC.bound(0,10.0), units:true, round:4, show:() => poppedRec.contour}), + follow: UC.newInput(LANG.cp_foll_s, {title:LANG.cp_foll_l, convert:UC.toFloat}), + sep: UC.newBlank({class:"pop-sep"}), + contour: UC.newBoolean(LANG.cp_cont_s, undefined, {title:LANG.cp_cont_s}), + engrave: UC.newBoolean(LANG.cp_engr_s, undefined, {title:LANG.cp_engr_l, show:() => poppedRec.contour}), + outline: UC.newBoolean(LANG.cp_outl_s, undefined, {title:LANG.cp_outl_l}), + exp: UC.newExpand("overrides"), + sep: UC.newBlank({class:"pop-sep"}), + ov_topz: UC.newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:UC.toFloat, units:true}), + ov_botz: UC.newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:UC.toFloat, units:true}), + ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, {title:LANG.ou_conv_l}), + exp_end: UC.endExpand(), + sep: UC.newBlank({class:"pop-sep"}), + menu: UC.newRow([ UC.newButton("select", func.surfaceAdd) ], {class:"ext-buttons f-row"}), + }; + + createPopOp('drill', { + tool: 'camDrillTool', + spindle: 'camDrillSpindle', + down: 'camDrillDown', + rate: 'camDrillDownSpeed', + dwell: 'camDrillDwell', + lift: 'camDrillLift', + mark: 'camDrillMark', + precision:'camDrillPrecision', + thru: 'camDrillThru', + fromTop: 'camDrillFromStockTop', + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), + dwell: UC.newInput(LANG.cd_dwll_s, {title:LANG.cd_dwll_l, convert:UC.toFloat}), + lift: UC.newInput(LANG.cd_lift_s, {title:LANG.cd_lift_l, convert:UC.toFloat, units:true, show:() => !poppedRec.mark}), + mark: UC.newBoolean(LANG.cd_mark_s,undefined, {title:LANG.cd_mark_l, show:() => !poppedRec.fromTop}), + fromTop: UC.newBoolean(LANG.cd_ftop_s,undefined, {title:LANG.cd_ftop_l, show:() => !poppedRec.mark}), + sep: UC.newBlank({class:"pop-sep"}), + thru: UC.newInput(LANG.cd_dtru_s, {title:LANG.cd_dtru_l, convert:UC.toFloat, units:true,show:() => !poppedRec.mark}), + precision:UC.newInput(LANG.cd_prcn_s, {title:LANG.cd_prcn_l, convert:UC.toFloat, units:true,show:() => !poppedRec.mark}), + sep: UC.newBlank({class:"pop-sep", }), + actions: UC.newRow([ + UC.newButton(LANG.select, ()=>func.selectHoles(true), {title:LANG.cd_seli_l}), + UC.newButton(LANG.cd_sela_s, ()=>func.selectHoles(false), {title:LANG.cd_sela_l}) + ], {class:"ext-buttons f-col"}) + }; + + createPopOp('register', { + tool: 'camDrillTool', + spindle: 'camDrillSpindle', + down: 'camDrillDown', + rate: 'camDrillDownSpeed', + dwell: 'camDrillDwell', + lift: 'camDrillLift', + feed: 'camRegisterSpeed', + thru: 'camRegisterThru' + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + axis: UC.newSelect(LANG.cd_axis, {}, "regaxis"), + points: UC.newSelect(LANG.cd_points, {show:() => poppedRec.axis !== '-'}, "regpoints"), + sep: UC.newBlank({class:"pop-sep"}), + spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), + rate: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), + feed: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true, show:() => poppedRec.axis === '-'}), + sep: UC.newBlank({class:"pop-sep"}), + down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), + dwell: UC.newInput(LANG.cd_dwll_s, {title:LANG.cd_dwll_l, convert:UC.toFloat, show:() => poppedRec.axis !== '-'}), + lift: UC.newInput(LANG.cd_lift_s, {title:LANG.cd_lift_l, convert:UC.toFloat, units:true, show:() => poppedRec.axis !== '-'}), + sep: UC.newBlank({class:"pop-sep"}), + thru: UC.newInput(LANG.cd_thru_s, {title:LANG.cd_thru_l, convert:UC.toFloat, units:true}), + }; + + createPopOp('flip', { + axis: 'camFlipAxis', + invert: 'camFlipInvert' + }).inputs = { + axis: UC.newSelect(LANG.cd_axis, {}, "regaxis"), + sep: UC.newBlank({class:"pop-sep", modes:MCAM, show:zBottom}), + invert: UC.newBoolean(LANG.cf_nvrt_s, undefined, {title:LANG.cf_nvrt_l, show:zBottom}), + sep: UC.newBlank({class:"pop-sep"}), + action: UC.newRow([ UC.newButton(LANG.cf_menu, func.opFlip) ], {class:"ext-buttons f-row"}) + }; + + createPopOp('gcode', { + gcode: 'camCustomGcode', + }).inputs = { + action: UC.newRow([ UC.newButton(LANG.edit, gcodeEditor()) ], {class:"ext-buttons f-row"}) + }; + + function angleTowardZUp() { + api.event.emit('tool.mesh.face-up'); + } + + api.event.on('tool.mesh.face-normal', normal => { + // console.log({ poppedRec }); + poppedRec.degrees = (Math.atan2(normal.y, normal.z) * RAD2DEG).round(2); + poppedRec.absolute = true; + func.opRender(); + updateStock(); + }); + + createPopOp('index', { + degrees: 'camIndexAxis', + absolute: 'camIndexAbs' + }).inputs = { + degrees: UC.newInput(LANG.ci_degr_s, {title:LANG.ci_degr_l, convert:UC.toFloat, bound:UC.bound(-360,360.0) }), + absolute: UC.newBoolean(LANG.ci_abso_s, undefined, {title:LANG.ci_abso_l}), + select: UC.newRow([ UC.newButton(LANG.ci_face_s, angleTowardZUp) ], {class:"ext-buttons f-row"}) + }; + + const editEnable = gcodeEditor('Laser Enable Script', 'enable'); + const editOn = gcodeEditor('Laser On Script', 'on'); + const editOff = gcodeEditor('Laser Off Script', 'off'); + + createPopOp('laser on', { + enable: 'camLaserEnable', + on: 'camLaserOn', + off: 'camLaserOff', + power: 'camLaserPower', + adapt: 'camLaserAdaptive', + adaptrp: 'camLaserAdaptMod', + flat: 'camLaserFlatten', + flatz: 'camLaserFlatZ', + minp: 'camLaserPowerMin', + maxp: 'camLaserPowerMax', + minz: 'camLaserZMin', + maxz: 'camLaserZMax', + }).inputs = { + enable: UC.newRow([ UC.newButton(LANG.enable, editEnable) ], {class:"ext-buttons f-row"}), + on: UC.newRow([ UC.newButton(LANG.on, editOn) ], {class:"ext-buttons f-row"}), + off: UC.newRow([ UC.newButton(LANG.off, editOff) ], {class:"ext-buttons f-row"}), + sep: UC.newBlank({class:"pop-sep"}), + power: UC.newInput(LANG.cl_powr_s, {title:LANG.cl_powr_l, convert:UC.toFloat, bound:UC.bound(0,1.0), show:() => !poppedRec.adapt}), + maxp: UC.newInput(LANG.cl_maxp_s, {title:LANG.cl_maxp_l, convert:UC.toFloat, bound:UC.bound(0,1.0), show:() => poppedRec.adapt}), + minp: UC.newInput(LANG.cl_minp_s, {title:LANG.cl_minp_l, convert:UC.toFloat, bound:UC.bound(0,1.0), show:() => poppedRec.adapt}), + maxz: UC.newInput(LANG.cl_maxz_s, {title:LANG.cl_maxz_l, convert:UC.toFloat, show:() => poppedRec.adapt}), + minz: UC.newInput(LANG.cl_minz_s, {title:LANG.cl_minz_l, convert:UC.toFloat, show:() => poppedRec.adapt}), + flatz: UC.newInput(LANG.cl_flaz_s, {title:LANG.cl_flaz_l, convert:UC.toFloat, show:() => poppedRec.flat}), + sep: UC.newBlank({class:"pop-sep"}), + adapt: UC.newBoolean(LANG.cl_adap_s, undefined, {title:LANG.cl_adap_l}), + adaptrp: UC.newBoolean(LANG.cl_adrp_s, undefined, {title:LANG.cl_adrp_l, show:() => poppedRec.adapt}), + flat: UC.newBoolean(LANG.cl_flat_s, undefined, {title:LANG.cl_flat_l }), + }; + + const editDisable = gcodeEditor('Laser Disable Script', 'disable'); + + createPopOp('laser off', { + disable: 'camLaserDisable', + }).inputs = { + disable: UC.newRow([ UC.newButton(LANG.disable, editDisable) ], {class:"ext-buttons f-row"}), + }; + + createPopOp('|', {}).inputs = {}; +}; + +/** + * Create a new popup operation (popOp) for a given type. + * @param {string} type - the name of the popOp to create + * @param {object} map - an object mapping popOp keys to either a string + * (representing a key in current.process) or a value (to be used as the default). + * @returns {object} the newly created popOp. + */ +function createPopOp(type, map) { + let op = popOp[type] = { + div: UC.newElement('div', { id:`${type}-op`, class:"cam-pop-op" }), + use: (rec) => { + op.rec = rec; + for (let [key, val] of Object.entries(op.inputs)) { + let type = val.type; + let from = map[key]; + let rval = rec[key]; + // fill undef entries older defines + if (type && (rval === null || rval === undefined)) { + if (typeof(from) === 'string') { + rec[key] = current.process[from]; + } else if (from !== undefined) { + rec[key] = from; + } else { + console.log('error', { key, val, type, from }); + } + } + } + API.util.rec2ui(rec, op.inputs); + op.hideshow(); + }, + using: (rec) => { + return op.rec === rec; + }, + bind: (ev) => { + API.util.ui2rec(op.rec, op.inputs); + + const settings = conf.get(); + const {tool} = new CAM.Tool(settings,op.rec.tool); //get tool by id + const opType = op.rec.type + const drillOrRegister = opType == "drill" || opType == "register" + + if ( !drillOrRegister && tool.type == "drill"){ + alerts.show(`Warning: Drills should not be used for ${opType} operations.`) + } + else if ( drillOrRegister && tool.type != "drill"){ + alerts.show(`Warning: Only drills should be used for drilling operations.`) + } + + for (let [key, val] of Object.entries(op.rec)) { + let saveTo = map[key]; + if (saveTo && typeof(key) === 'string' && !key.startsWith("~")) { + current.process[saveTo] = val; + } + } + API.conf.save(); + op.hideshow(); + }, + new: () => { + let rec = { type }; + for (let [key, src] of Object.entries(map)) { + rec[key] = typeof(src) === 'string' + ? current.process[src.replace('~','')] + : src; + } + return rec; + }, + hideshow: () => { + for (let inp of Object.values(op.inputs)) { + let parent = inp.parentElement; + if (parent && parent.setVisible && parent.__opt.show) { + parent.setVisible(parent.__opt.show(op, API.conf.get())); + } + } + }, + addNote: () => { + if (!op.note && type !== 'flip') { + const divid = `div-${++seed}`; + const noteid = `note-${++seed}`; + const div = document.createElement('div'); + div.setAttribute('id', divid); + div.classList.add('pop-tics') + op.div.appendChild(div); + div.innerHTML = h.build( + h.div([ h.label({ id: noteid }) ]) + ); + op.note = { divid, noteid }; + } + if (op.note) { + const { divid, noteid } = op.note; + const div = $(divid); + if (div) div.onclick = () => { + API.uc.prompt('Edit Note for Operation', poppedRec.note || '').then(note => { + if (note !== undefined && note !== null) { + poppedRec.note = op.x = note; + API.conf.save(); + } + func.opRender(); + }); + }; + const note = $(noteid); + if (note) note.innerText = poppedRec.note || ''; + } + }, + group: [], + }; + + /** + * @function createRecordGetter + * @description Creates a getter function for the current record. + * The getter will bind the current record to the op before returning it. + * This is useful for passing a record to a pre-slice function + * @returns {function} A getter function for the current record. + */ + op.createRecordGetter = () => { + return () => { + op.bind(); + return op.rec; + } + } + + UC.restore({ + addTo: op.div, + bindTo: op.bind, + lastDiv: op.div, + lastGroup: op.group + }); + return op; +} + +function createTabBox(iw, ic, n) { + const { track } = iw; + const { stock, bounds, process } = API.conf.get(); + const { camTabsWidth, camTabsHeight, camTabsDepth, camTabsMidline } = process; + const { camZBottom, camStockIndexed } = process; + const isIndexed = camStockIndexed; + const sz = stock.z || bounds.max.z; + const zto = sz - iw.track.top; + const zp = (camZBottom || isIndexed ? camZBottom : sz - track.box.d - zto) + (camTabsMidline ? 0 : camTabsHeight / 2); + ic.x += n.x * camTabsDepth / 2; // offset from part + ic.z -= n.y * camTabsDepth / 2; // offset swap z,y + ic.y = zp; // offset swap in world space y,z + const rot = new THREE.Quaternion().setFromAxisAngle(zaxis, Math.atan2(n.y, n.x)); + const pos = { x:ic.x, y:ic.y, z:ic.z }; + const dim = { x:camTabsDepth, y:camTabsWidth, z:camTabsHeight }; + const tab = addbox(pos, boxColor(), 'tabb', dim, { rotate: rot, opacity: boxOpacity() }); + return { pos, dim, rot, tab, width: camTabsWidth, height: camTabsHeight, stock }; +} + +function addWidgetTab(widget, rec) { + const { pos, dim, rot, id } = rec; + const tabs = widget.tabs = (widget.tabs || {}); + // prevent duplicate restore from repeated settings load calls + if (!tabs[id]) { + pos.box = addbox( + pos, boxColor(), id, + dim, { group: widget.mesh, rotate: rot, opacity: boxOpacity() } + ); + pos.box.tab = Object.assign({widget, id}, pos); + widget.adds.push(pos.box); + tabs[id] = pos; + } +} + +function restoreTabs(widgets) { + widgets.forEach(widget => { + const tabs = API.widgets.annotate(widget.id).tab || []; + tabs.forEach(rec => { + let [ x, y, z, w ] = rec.rot; + rec = Object.clone(rec); + rec.rot = new THREE.Quaternion(x, y, z, w); + addWidgetTab(widget, rec); + }); + }); +} + +function clearTabs(widget, skiprec) { + Object.values(widget.tabs || {}).forEach(rec => { + widget.adds.remove(rec.box); + widget.mesh.remove(rec.box); + }); + widget.tabs = {}; + if (!skiprec) { + delete API.widgets.annotate(widget.id).tab; + } +} + +function updateTabs() { + // update tab color and opacity + API.widgets.all().forEach(widget => { + Object.values(widget.tabs || {}).forEach(rec => { + for (let rec of widget.adds || []) { + rec.material.color = new THREE.Color(boxColor()); + rec.material.opacity = boxOpacity(); + } + }); + }); +} + +function unselectTraces(widget, skip) { + if (widget.trace_stack) { + widget.trace_stack.meshes.forEach(mesh => { + if (mesh.selected) { + func.traceToggle(mesh, skip); + } + }); + } +} + +function unselectHoles(widget) { + if (!widget.holes) return + widget.holes.forEach(hole => { + hole.selected = false + }) +} + +function validateTools(tools) { + if (tools) { + let max = 0; + for (let t of tools) { + if (Number.isInteger(t.number)) { + max = Math.max(max, t.number); + } + } + for (let t of tools) { + if (!Number.isInteger(t.number)) { + t.number = ++max; + console.log('added tool #', t); + } + } + } +} + +function isDark() { return API.space.is_dark() }; + +function addbox() { return FDM.addbox(...arguments)}; + +function delbox() { return FDM.delbox(...arguments)}; + +function boxColor() { + return isDark() ? 0x00ddff : 0x0000dd; +} + +function boxOpacity() { + return isDark() ? 0.75 : 0.6; +} + +function animate() { + isAnimate = true; + API.widgets.opacity(isParsed ? 0 : 0.75); + API.hide.slider(); + STACKS.clear(); + animFn().animate(API); + API.view.set_animate(); +} + +function updateStock() { + if (isAnimate) { + if (isIndexed) { + SPACE.world.remove(camStock); + camStock = undefined; + } + return; + } + + if (!isCamMode) { + SPACE.world.remove(camZTop); + SPACE.world.remove(camZBottom); + SPACE.world.remove(camStock); + camStock = null; + camZTop = null; + camZBottom = null; + return; + } + + api.platform.update_bounds(); + + const settings = API.conf.get(); + const widgets = API.widgets.all(); + + const { stock, process } = settings; + const { x, y, z, center } = stock; + + UI.func.animate.classList.add('disabled'); + if (camStock) { + SPACE.world.remove(camStock); + camStock = null; + } + if (x && y && z) { + UI.func.animate.classList.remove('disabled'); + { + let geo = new THREE.BoxGeometry(1, 1, 1); + let mat = new THREE.MeshBasicMaterial({ + color: 0x777777, + opacity: 0.05, + transparent: true, + side:THREE.DoubleSide + }); + camStock = new THREE.Mesh(geo, mat); + camStock.renderOrder = 2; + + let lo = 0.5; + let lidat = [ + lo, lo, lo, lo, lo,-lo, + lo, lo, lo, lo,-lo, lo, + lo, lo, lo, -lo, lo, lo, + -lo,-lo,-lo, -lo,-lo, lo, + -lo,-lo,-lo, -lo, lo,-lo, + -lo,-lo,-lo, lo,-lo,-lo, + lo, lo,-lo, -lo, lo,-lo, + lo, lo,-lo, lo,-lo,-lo, + lo,-lo,-lo, lo,-lo, lo, + lo,-lo, lo, -lo,-lo, lo, + -lo,-lo, lo, -lo, lo, lo, + -lo, lo, lo, -lo, lo,-lo + ]; + let ligeo = new THREE.BufferGeometry(); + ligeo.setAttribute('position', new THREE.BufferAttribute(lidat.toFloat32(), 3)); + let limat = new THREE.LineBasicMaterial({ color: 0xaaaaaa }); + let lines = new THREE.LineSegments(ligeo, limat); + camStock.lines = lines; + camStock.add(lines); + SPACE.world.add(camStock); + } + // fight z fighting in threejs + camStock.scale.x = x + 0.005; + camStock.scale.y = y + 0.005; + camStock.scale.z = z + 0.005; + camStock.position.x = center.x; + camStock.position.y = center.y; + camStock.position.z = center.z; + camStock.lines.material.color = + new THREE.Color(isDark() ? 0x555555 : 0xaaaaaa); + } + + SPACE.world.remove(camZTop); + if (process.camZTop && widgets.length) { + let max = { x, y, z }; + for (let w of widgets) { + max.x = Math.max(max.x, w.track.box.w); + max.y = Math.max(max.y, w.track.box.h); + max.z = Math.max(max.z, w.track.box.d); + } + let geo = new THREE.PlaneGeometry(max.x, max.y); + let mat = new THREE.MeshBasicMaterial({ + color: 0x777777, + opacity: 0.55, + transparent: true, + side:THREE.DoubleSide + }); + camZTop = new THREE.Mesh(geo, mat); + camZTop._max = max; + camZTop.renderOrder = 1; + camZTop.position.x = center.x; + camZTop.position.y = center.y; + camZTop.position.z = process.camZTop; + SPACE.world.add(camZTop); + } else { + camZTop = undefined; + } + + SPACE.world.remove(camZBottom); + if (process.camZBottom && widgets.length) { + let max = { x, y, z }; + for (let w of widgets) { + max.x = Math.max(max.x, w.track.box.w); + max.y = Math.max(max.y, w.track.box.h); + max.z = Math.max(max.z, w.track.box.d); + } + let geo = new THREE.PlaneGeometry(max.x, max.y); + let mat = new THREE.MeshBasicMaterial({ + color: 0x777777, + opacity: 0.55, + transparent: true, + side:THREE.DoubleSide + }); + camZBottom = new THREE.Mesh(geo, mat); + camZBottom._max = max; + camZBottom.renderOrder = 1; + camZBottom.position.x = center.x; + camZBottom.position.y = center.y; + camZBottom.position.z = process.camZBottom; + SPACE.world.add(camZBottom); + } else { + camZBottom = undefined; + } + + SPACE.update(); +} + +}); diff --git a/src/kiri-mode/cam/driver.js b/src.old/kiri-mode/cam/driver.js similarity index 100% rename from src/kiri-mode/cam/driver.js rename to src.old/kiri-mode/cam/driver.js diff --git a/src2/kiri-mode/cam/export.js b/src.old/kiri-mode/cam/export.js similarity index 82% rename from src2/kiri-mode/cam/export.js rename to src.old/kiri-mode/cam/export.js index b2226302..d93fbe3d 100644 --- a/src2/kiri-mode/cam/export.js +++ b/src.old/kiri-mode/cam/export.js @@ -1,17 +1,28 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base } from '../../geo/base.js'; +"use strict"; -const { util } = base; +// dep: geo.base +// dep: geo.polygons +// dep: kiri-mode.cam.driver +gapp.register("kiri-mode.cam.export", (root, exports) => { + +const { base, kiri } = root; +const { polygons, util } = base; +const { driver } = kiri; +const { CAM } = driver; + +const POLY = polygons; +const PRO = CAM.process; /** * @returns {Array} gcode lines */ -export function cam_export(print, online) { +CAM.export = function(print, online) { const widget = print.widgets[0]; if (!widget) return; - console.log({ cam_export: print }); + console.log(print); const { settings } = print; const { device, tools } = settings; @@ -31,16 +42,16 @@ export function cam_export(print, online) { space = device.gcodeSpace ? ' ' : '', isRML = device.gcodeFExt?.toLowerCase() === 'rml', stripComments = device.gcodeStrip || false, - cmdToolChange = device.gcodeChange || ["M6 T{tool}"], - cmdSpindle = device.gcodeSpindle || ["M3 S{speed}"], - cmdDwell = device.gcodeDwell || ["G4 P{time}"], + cmdToolChange = device.gcodeChange || [ "M6 T{tool}" ], + cmdSpindle = device.gcodeSpindle || [ "M3 S{speed}" ], + cmdDwell = device.gcodeDwell || [ "G4 P{time}" ], axis = { X: 'X', Y: 'Y', Z: 'Z', A: 'A', F: 'F', R: 'R', I: 'I', J: 'J' }, dev = settings.device, spro = settings.process, maxZd = spro.camFastFeedZ, maxXYd = spro.camFastFeed, decimals = base.config.gcode_decimals || 3, - pos = { x: null, y: null, z: null, a: undefined, f: null, t: null, emit: null }, + pos = { x:null, y:null, z:null, a:undefined, f:null, t:null, emit:null }, line, cidx, mode, @@ -51,18 +62,18 @@ export function cam_export(print, online) { compact_output = false, lasering = false, laserOp, - stock = settings.stock || {}, + stock = settings.stock || { }, ztOff = stock.z - widget.track.top, bounds = widget.getBoundingBox(), zmax = stock.z, runbox = { - max: { x: -Infinity, y: -Infinity, z: -Infinity }, - min: { x: Infinity, y: Infinity, z: Infinity } + max: { x:-Infinity, y:-Infinity, z:-Infinity}, + min: { x:Infinity, y:Infinity, z:Infinity} }, offset = { x: -origin.x, - y: origin.y, - z: spro.camOriginTop ? origin.z - zmax : origin.z + y: origin.y, + z: spro.camOriginTop ? origin.z - zmax : origin.z }, scale = { x: 1, @@ -73,10 +84,10 @@ export function cam_export(print, online) { box: runbox, tool: 0, tool_name: "unknown", - top: (offset ? dev.bedDepth : dev.bedDepth / 2), - left: (offset ? 0 : -dev.bedWidth / 2), - right: (offset ? dev.bedWidth : dev.bedWidth / 2), - bottom: (offset ? 0 : -dev.bedDepth / 2), + top: (offset ? dev.bedDepth : dev.bedDepth/2), + left: (offset ? 0 : -dev.bedWidth/2), + right: (offset ? dev.bedWidth : dev.bedWidth/2), + bottom: (offset ? 0 : -dev.bedDepth/2), time_sec: 0, time_ms: 0, time: 0 @@ -96,9 +107,9 @@ export function cam_export(print, online) { if (spos >= 0) { const left = line.substring(0, spos); const right = '(' + line.substring(spos) - .replace(/; /g, '') - .replace(/;/g, '') + ')'; - line = [left, right].join(''); + .replace(/; /g,'') + .replace(/;/g,'') + ')'; + line = [ left, right ].join(''); } } output.append(line); @@ -123,21 +134,21 @@ export function cam_export(print, online) { if (!array) { return; } - if (typeof (array) === 'string') { + if (typeof(array) === 'string') { array = array.split('\n'); } - for (i = 0; i < array.length; i++) { + for (i=0; i= 0) { line = line.substring(0, cidx).trim(); if (line.length === 0) continue; } if (line.indexOf('G20') === 0) { - factor = 1 / 25.4; - consts.top = (offset ? dev.bedDepth : dev.bedDepth / 2) * factor; - consts.left = (offset ? 0 : -dev.bedWidth / 2) * factor; - consts.right = (offset ? dev.bedWidth : dev.bedWidth / 2) * factor; - consts.bottom = (offset ? 0 : -dev.bedDepth / 2) * factor; + factor = 1/25.4; + consts.top = (offset ? dev.bedDepth : dev.bedDepth/2) * factor; + consts.left = (offset ? 0 : -dev.bedWidth/2) * factor; + consts.right = (offset ? dev.bedWidth : dev.bedWidth/2) * factor; + consts.bottom = (offset ? 0 : -dev.bedDepth/2) * factor; } else if (line.indexOf('G21') === 0) { factor = 1; } @@ -155,18 +166,19 @@ export function cam_export(print, online) { } } - /** - * Move to a point, applies post-processing including for laser/RML, and appends to output - * @param {Point} out.point - the point to move to - * @param {Tool} [out.tool] - the tool to move with - * @param {number} [out.speed] - the speed to move at (in mm/min) - * @param {number} [out.emit] - the emission state (0=off, 1=on) - * @param {boolean} [opt.newOp] - whether to force a new operation - * @param {number} [opt.dx] - the delta X change (if not given, uses out.point.x - pos.x) - * @param {number} [opt.dy] - the delta Y change (if not given, uses out.point.y - pos.y) - * @param {number} [opt.dz] - the delta Z change (if not given, uses out.point.z - pos.z) - * @param {number} [opt.time] - the time to move (in ms) (if not given, calculate from speed) - */ + +/** + * Move to a point, applies post-processing including for laser/RML, and appends to output + * @param {Point} out.point - the point to move to + * @param {Tool} [out.tool] - the tool to move with + * @param {number} [out.speed] - the speed to move at (in mm/min) + * @param {number} [out.emit] - the emission state (0=off, 1=on) + * @param {boolean} [opt.newOp] - whether to force a new operation + * @param {number} [opt.dx] - the delta X change (if not given, uses out.point.x - pos.x) + * @param {number} [opt.dy] - the delta Y change (if not given, uses out.point.y - pos.y) + * @param {number} [opt.dz] - the delta Z change (if not given, uses out.point.z - pos.z) + * @param {number} [opt.time] - the time to move (in ms) (if not given, calculate from speed) + */ function moveTo(out, opt = {}) { let laser = out.type === 'laser'; let newpos = out.point; @@ -193,7 +205,7 @@ export function cam_export(print, online) { consts.tool = pos.t.getNumber(); consts.tool_name = pos.t.getName(); if (!laserOp && (spro.camToolInit || toolChanges > 0)) { - filterEmit(cmdToolChange, { ...consts, spindle: newSpindle }); + filterEmit(cmdToolChange, { ...consts, spindle: newSpindle } ); } toolChanges++; } @@ -259,20 +271,20 @@ export function cam_export(print, online) { return; } let gn; - if (out.emit >= 0 && out.emit <= 3) gn = `G${out.emit}`; + if ( out.emit >=0 && out.emit <= 3) gn = `G${out.emit}`; let speed = out.speed, - arc = out.emit == 2 || out.emit == 3, - center = out.center, - nl = (compact_output && lastGn === gn) ? [] : [gn], - dx = opt.dx || newpos.x - pos.x, - dy = opt.dy || newpos.y - pos.y, - dz = opt.dz || newpos.z - pos.z, - da = newpos.a != pos.a, - maxf = dz ? maxZd : maxXYd, - feed = Math.min(speed || maxf, maxf), - dist = Math.sqrt(dx * dx + dy * dy + dz * dz), - newFeed = feed && feed !== pos.f; - + arc = out.emit == 2 || out.emit == 3, + center = out.center, + nl = (compact_output && lastGn === gn) ? [] : [gn], + dx = opt.dx || newpos.x - pos.x, + dy = opt.dy || newpos.y - pos.y, + dz = opt.dz || newpos.z - pos.z, + da = newpos.a != pos.a, + maxf = dz ? maxZd : maxXYd, + feed = Math.min(speed || maxf, maxf), + dist = Math.sqrt(dx * dx + dy * dy + dz * dz), + newFeed = feed && feed !== pos.f; + // drop dup points (all deltas are 0) if (!arc && !(dx || dy || dz || da)) { @@ -285,7 +297,7 @@ export function cam_export(print, online) { pos.x = newpos.x; runbox.min.x = Math.min(runbox.min.x, pos.x); runbox.max.x = Math.max(runbox.max.x, pos.x); - nl.append(space).append(axis.X).append(add0(consts.pos_x = pos.x * factor)); + nl.append(space).append(axis.X).append(add0(consts.pos_x = pos.x * factor)); } if (dy || newpos.y !== pos.y) { pos.y = newpos.y; @@ -307,9 +319,9 @@ export function cam_export(print, online) { pos.f = feed; nl.append(space).append(axis.F).append(add0(consts.feed = feed * factor, true)); } - if (arc) { + if(arc){ nl.append(space).append(axis.I).append(add0(center.x * factor, true)) - .append(space).append(axis.J).append(add0(center.y * factor, true)); + .append(space).append(axis.J).append(add0(center.y * factor, true)); } // temp hack to support RML1 dialect from a file extensions trigger @@ -318,9 +330,9 @@ export function cam_export(print, online) { if (newFeed) { append(`VS${feed};`); } - nl = [axis.Z, add0(pos.x * factor), ",", add0(pos.y * factor), ",", add0(pos.z * factor), ";"]; + nl = [ axis.Z, add0(pos.x * factor), ",", add0(pos.y * factor), ",", add0(pos.z * factor), ";" ]; } else { - nl = ["PU", add0(pos.x * factor), ",", add0(pos.y * factor), ";"]; + nl = [ "PU", add0(pos.x * factor), ",", add0(pos.y * factor), ";" ]; } } @@ -516,3 +528,5 @@ export function cam_export(print, online) { print.bytes = bytes + lines - 1; print.bounds = runbox; }; + +}); diff --git a/src.old/kiri-mode/cam/ops.js b/src.old/kiri-mode/cam/ops.js new file mode 100644 index 00000000..28f6a546 --- /dev/null +++ b/src.old/kiri-mode/cam/ops.js @@ -0,0 +1,1968 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +// dep: geo.base +// dep: geo.paths +// dep: geo.point +// dep: geo.polygons +// dep: geo.slicer +// dep: kiri.slice +// use: kiri-mode.cam.topo +// use: kiri-mode.cam.topo4 +gapp.register("kiri-mode.cam.ops", (root, exports) => { + +const { base, kiri } = root; +const { paths, polygons, newPoint, newPolygon, sliceConnect } = base; +const { poly2polyEmit, tip2tipEmit, segmentNormal, vertexNormal } = paths; +const { driver, newSlice } = kiri; +const { CAM } = driver; + +const DEG2RAG = Math.PI / 180; +const POLY = polygons; + +class CamOp { + constructor(state, op) { + this.state = state; + this.op = op + } + + type() { + return this.op.type; + } + + weight() { + return 1; + } + + async slice() { } + + prepare() { } +} + +class OpIndex extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice() { + let { op, state } = this; + if (!state.isIndexed) { + throw 'index op requires indexed stock'; + } + let { widget, updateSlicer, computeShadows, setAxisIndex } = state; + this.degrees = setAxisIndex(op.degrees, op.absolute); + // force recompute of topo + widget.topo = undefined; + updateSlicer(); + await computeShadows(); + } + + prepare(ops, progress) { + const { lastPoint, zmax, zclear, camOut, stock } = ops; + let last = lastPoint(); + if (last) { + // max point of stock corner radius when rotating (safe z when indexing) + const rzmax = (Math.max(stock.y, stock.z) * Math.sqrt(2)) / 2 + zclear; + const zmove = Math.max(rzmax, zmax); + // move above rotating stock + camOut(last = last.clone().setZ(zmove), 0); + // issue rotation command + camOut(last = last.clone().setY(0).setA(this.degrees), 0); + } + } +} + +class OpGCode extends CamOp { + constructor(state, op) { + super(state, op); + } + + prepare(ops, progress) { + ops.addGCode(this.op.gcode); + } +} + +class OpLaserOn extends CamOp { + constructor(state, op) { + super(state, op); + } + + prepare(ops, progress) { + const { printPoint, setPrintPoint, setTool, zmax, camOut } = ops; + this.op.silent = true; + setTool(0); + ops.addGCode(this.op.enable); + ops.setLasering(true, this.op.power); + } +} + +class OpLaserOff extends CamOp { + constructor(state, op) { + super(state, op); + } + + prepare(ops, progress) { + const { printPoint, zmax, camOut } = ops; + this.op.silent = true; + ops.addGCode(this.op.disable); + ops.setLasering(false); + camOut(printPoint.clone().setZ(zmax), 0); + } +} + +class OpLevel extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, widget, addSlices } = state; + let { updateToolDiams, tabs, cutTabs } = state; + let { bounds, zMax, ztOff, color, tshadow } = state; + let { stock } = settings; + + let toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); + let stepOver = this.stepOver = toolDiam * op.step; + let z = zMax + ztOff - op.down; + + updateToolDiams(toolDiam); + + let points = []; + let clear = op.stock ? + [ newPolygon().centerRectangle({x:0,y:0,z:0}, stock.x, stock.y) ] : + POLY.outer(POLY.offset(tshadow, toolDiam * (op.over || 0))); + + POLY.fillArea(clear, 90, stepOver, points); + + let lines = this.lines = []; + for (let i=0; i { return { first: p.first(), last: p.last(), poly: p } }); + printPoint = tip2tipEmit(lines, printPoint, (el, point, count) => { + let poly = el.poly; + if (poly.last() === point) { + poly.reverse(); + } + poly.forEachPoint((point, pidx) => { + camOut(point.clone(), true, stepOver); + }, false); + }); + setPrintPoint(printPoint); + + newLayer(); + } +} + +class OpRough extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, slicer, addSlices, unsafe, color } = state; + let { updateToolDiams, thruHoles, tabs, cutTabs, cutPolys } = state; + let { ztOff, zMax, shadowAt, isIndexed} = state; + let { workarea } = state; + let { process, stock } = settings; + + if (op.down <= 0) { + throw `invalid step down "${op.down}"`; + } + + let roughIn = op.inside; + let roughDown = op.down; + let roughLeave = op.leave || 0; + let roughLeaveZ = op.leavez || 0; + let roughStock = op.all && isIndexed; + let toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); + let trueShadow = process.camTrueShadow === true; + + updateToolDiams(toolDiam); + + // clear the stock above the area to be roughed out + if (workarea.top_z > workarea.top_part) { + let shadow = state.shadow.base.clone(); + let step = toolDiam * op.step; + let inset = roughStock ? + POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], step) : + POLY.offset(shadow, roughIn ? step : step + roughLeave + toolDiam / 2); + let facing = POLY.offset(inset, -step, { count: 999, flat: true }); + let zdiv = ztOff / roughDown; + let zstep = (zdiv % 1 > 0) ? ztOff / (Math.floor(zdiv) + 1) : roughDown; + if (ztOff === 0) { + // compensate for lack of z top offset in this scenario + ztOff = zstep; + } + let zsteps = Math.round(ztOff / zstep); + let camFaces = this.camFaces = []; + let zstart = zMax + ztOff - zstep; + for (let z = zstart; zsteps > 0; zsteps--) { + let slice = newSlice(); + slice.z = z; + slice.camLines = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ); + slice.output() + .setLayer("face", {face: color, line: color}) + .addPolys(slice.camLines); + addSlices(slice); + camFaces.push(slice); + z -= zstep; + } + } + + // create roughing slices + let flats = []; + let shadow = []; + let slices = []; + let indices = slicer.interval(roughDown, { + down: true, min: 0, fit: true, off: 0.01 + }); + + // shift out first (top-most) slice + indices.shift(); + + // find flats and add to indices for slicing + if (op.flats) { + let flatArea = (Math.PI * (toolDiam/2) * (toolDiam/2)) / 2; + let flats = Object.entries(slicer.zFlat) + .filter(row => row[1] > flatArea) + .map(row => row[0]) + .map(v => parseFloat(v).round(5)) + .filter(v => v >= workarea.bottom_z); + flats.forEach(v => { + if (!indices.contains(v)) { + indices.push(v); + } + }); + indices = indices.sort((a,b) => { return b - a }); + // if layer is not on a flat and next one is, + // then move this layer up to mid-point to previous layer + // this is not perfect. the best method is to interpolate + // between flats so that each step is < step down. on todo list + for (let i=1; i (parseFloat(v) - 0.01).round(5)) + .filter(v => v > 0 && indices.indexOf(v) < 0); + indices = indices.appendAll(flats).sort((a,b) => b-a); + } + + indices = indices.filter(v => v >= workarea.bottom_z); + // console.log('indices', ...indices, {zBottom}); + + let cnt = 0; + let tot = 0; + await slicer.slice(indices, { each: data => { + shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true); + if (flats.indexOf(data.z) >= 0) { + // exclude flats injected to complete shadow + return; + } + if (data.z > workarea.top_z) { + return; + } + data.shadow = trueShadow ? shadowAt(data.z) : shadow.clone(true); + data.slice.shadow = data.shadow; + slices.push(data.slice); + progress(0.25 + 0.25 * (++cnt / tot)); + }, progress: (index, total) => { + tot = total; + progress((index / total) * 0.25); + } }); + + if (trueShadow) { + shadow = state.shadow.base.clone(true); + } else { + shadow = POLY.union(shadow.appendAll(state.shadow.base), 0.01, true); + } + + // inset or eliminate thru holes from shadow + shadow = POLY.flatten(shadow.clone(true), [], true); + thruHoles.forEach(hole => { + shadow = shadow.map(p => { + if (p.isEquivalent(hole)) { + let po = POLY.offset([p], -(toolDiam / 2 + roughLeave + 0.05)); + return po ? po[0] : undefined; + } else { + return p; + } + }).filter(p => p); + }); + shadow = POLY.nest(shadow); + if (op.voids) { + // eliminate voids from shadow when "clear voids" enables + for (let s of shadow) s.inner = undefined; + } + + // shell = shadow expanded by half tool diameter + leave stock + const sadd = roughIn ? toolDiam / 2 : toolDiam / 2; + const shell = roughStock ? + POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], sadd) : + POLY.offset(shadow, sadd + roughLeave); + + slices.forEach((slice, index) => { + let offset = [shell.clone(true),slice.shadow.clone(true)].flat(); + let flat = POLY.flatten(offset, [], true); + let nest = POLY.setZ(POLY.nest(flat), slice.z); + + // inset offset array by 1/2 diameter then by tool overlap % + offset = POLY.offset(nest, [-(toolDiam / 2 + roughLeave), -toolDiam * op.step], { + minArea: Math.min(0.01, toolDiam * op.step / 4), + z: slice.z, + count: 999, + flat: true, + call: (polys, count, depth) => { + // used in depth-first path creation + polys.forEach(p => { + p.depth = depth; + if (p.inner) { + p.inner.forEach(p => p.depth = depth); + } + }); + } + }) || []; + + // add outside pass if not inside only + if (!roughIn && !roughStock) { + const outside = POLY.offset(shadow.clone(), toolDiam / 2 + roughLeave, {z: slice.z}); + if (outside) { + outside.forEach(p => p.depth = -p.depth); + offset.appendAll(outside); + } + } + + if (tabs) { + tabs.forEach(tab => { + tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); + }); + offset = cutTabs(tabs, offset, slice.z); + } + + if (!offset) return; + + if (process.camStockClipTo && stock.x && stock.y && stock.center) { + let rect = newPolygon().centerRectangle(stock.center, stock.x, stock.y); + offset = cutPolys([rect], offset, slice.z, true); + } + + // elimate double inset on inners + offset.forEach(op => { + if (op.inner) { + let pv1 = op.perimeter(); + let newinner = []; + op.inner.forEach(oi => { + let pv2 = oi.perimeter(); + let pct = pv1 > pv2 ? pv2/pv1 : pv1/pv2; + if (pct < 0.98) { + newinner.push(oi); + } + }); + op.inner = newinner; + } + }); + + slice.camLines = offset; + if (roughLeaveZ) { + // offset roughing in Z as well to minimize + // tool marks on curved surfaces + // const roughLeaveZ = 1 * Math.min(roughDown, roughLeave / 2); + slice.camLines.forEach(p => { + p.setZ(p.getZ() + roughLeaveZ); + }); + } + if (false) slice.output() + .setLayer("slice", {line: 0xaaaa00}, true) + .addPolys(slice.topPolys()) + // .setLayer("top shadow", {line: 0x0000aa}) + // .addPolys(tshadow) + // .setLayer("rough shadow", {line: 0x00aa00}) + // .addPolys(shadow) + .setLayer("rough shell", {line: 0xaa0000}) + .addPolys(shell); + progress(0.5 + 0.5 * (index / slices.length)); + }); + + let last = slices[slices.length-1]; + + if (workarea.bottom_z < 0) + for (let zneg of base.util.lerp(0, -workarea.bottom_cut, op.down)) { + if (!last) continue; + let add = last.clone(true); + add.z -= zneg; + add.camLines = last.camLines.clone(true); + add.camLines.forEach(p => p.setZ(add.z + roughLeaveZ)); + // add.tops.forEach(top => top.poly.setZ(add.z)); + // add.shadow = last.shadow.clone(true); + slices.push(add); + } + + slices.forEach(slice => { + slice.output() + .setLayer("roughing", {face: color, line: color}) + .addPolys(slice.camLines); + }); + this.sliceOut = slices.filter(slice => slice.camLines); + + addSlices(this.sliceOut); + } + + prepare(ops, progress) { + let { op, state, sliceOut, camFaces } = this; + let { setTool, setSpindle, setPrintPoint, sliceOutput, polyEmit } = ops; + let { camOut, newLayer, printPoint } = ops; + let { settings } = state; + let { process } = settings; + + let easeDown = process.camEaseDown; + let cutdir = op.ov_conv; + let depthFirst = process.camDepthFirst && !state.isIndexed; + let depthData = []; + + setTool(op.tool, op.rate, op.plunge); + setSpindle(op.spindle); + + // output the clearing of stock above roughing + for (let slice of (camFaces || [])) { + const level = []; + for (let poly of slice.camLines) { + level.push(poly); + if (poly.inner) { + poly.inner.forEach(function(inner) { + level.push(inner); + }); + } + } + // set winding specified in output + POLY.setWinding(level, cutdir, false); + poly2polyEmit(level, printPoint, (poly, index, count) => { + printPoint = polyEmit(poly, index, count, printPoint); + }); + newLayer(); + } + + // output the roughing passes + setPrintPoint(printPoint); + sliceOutput(sliceOut, { + cutdir, + depthFirst, + easeDown: op.down && easeDown ? 0.001 : 0, + progress: (n,m) => progress(n/m, "routing") + }); + } +} + +class OpOutline extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, widget, slicer, addSlices, tshadow, thruHoles, unsafe, color } = state; + let { updateToolDiams, tabs, cutTabs, cutPolys, workarea, zMax } = state; + let { process, stock } = settings; + + if (op.down <= 0) { + throw `invalid step down "${op.down}"`; + } + let toolDiam = this.toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); + updateToolDiams(toolDiam); + + let shadow = []; + let slices = []; + let intopt = { + off: 0.01, + fit: true, + down: true, + min: Math.max(0, workarea.bottom_z), + max: workarea.top_z + }; + let indices = slicer.interval(op.down, intopt); + let trueShadow = process.camTrueShadow === true; + // shift out first (top-most) slice + indices.shift(); + // add flats to shadow + const flats = Object.keys(slicer.zFlat) + .map(v => (parseFloat(v) - 0.01).round(5)) + .filter(v => v > 0 && indices.indexOf(v) < 0); + indices = indices.appendAll(flats).sort((a,b) => b-a); + + let cnt = 0; + let tot = 0; + if (op.outside && !op.inside) { + // console.log({outline_bypass: indices, down: op.down}); + indices.forEach((ind,i) => { + if (flats.indexOf(ind) >= 0) { + // exclude flats + return; + } + let slice = newSlice(ind); + slice.shadow = shadow.clone(true); + slices.push(slice); + }); + } else + await slicer.slice(indices, { each: data => { + shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true); + if (flats.indexOf(data.z) >= 0) { + // exclude flats injected to complete shadow + return; + } + data.shadow = trueShadow ? CAM.shadowAt(widget, data.z) : shadow.clone(true); + data.slice.shadow = data.shadow; + // data.slice.tops[0].inner = data.shadow; + // data.slice.tops[0].inner = POLY.setZ(tshadow.clone(true), data.z); + slices.push(data.slice); + // data.slice.xray(); + // onupdate(0.2 + (index/total) * 0.1, "outlines"); + progress(0.5 + 0.5 * (++cnt / tot)); + }, progress: (index, total) => { + tot = total; + progress((index / total) * 0.5); + } }); + shadow = POLY.union(shadow.appendAll(state.shadow.base), 0.01, true); + + // start slices at top of stock when `clear top` enabled + if (op.top) { + let first = slices[0]; + let zlist = slices.map(s => s.z); + for (let z of indices.filter(v => v >= zMax)) { + if (zlist.contains(z)) { + continue; + } + let add = first.clone(true); + add.tops.forEach(top => top.poly.setZ(add.z)); + add.shadow = first.shadow.clone(true); + add.z = z; + slices.splice(0,0,add); + } + } + + // extend cut thru (only when z bottom is 0) + if (workarea.bottom_z < 0) { + let last = slices[slices.length-1]; + for (let zneg of base.util.lerp(0, -workarea.bottom_cut, op.down)) { + if (!last) continue; + let add = last.clone(true); + add.tops.forEach(top => top.poly.setZ(add.z)); + add.shadow = last.shadow.clone(true); + add.z -= zneg; + slices.push(add); + } + } + + slices.forEach(slice => { + let tops = slice.shadow; + + // outside only (use tshadow for entire cut) + if (op.outside) { + tops = tshadow; + } + + if (op.omitthru) { + // eliminate thru holes from shadow + for (let hole of thruHoles) { + for (let top of tops) { + if (!top.inner) continue; + top.inner = top.inner.filter(innr => { + return !innr.isEquivalent(hole, false, 0.1); + }); + } + } + } + + if (op.omitvoid) { + for (let top of tops) { + delete top.inner; + } + } + + let offset = POLY.expand(tops, toolDiam / 2, slice.z); + if (!(offset && offset.length)) { + return; + } + + // when pocket only, drop first outer poly + // if it matches the shell and promote inner polys + if (op.inside) { + let shell = POLY.expand(tops.clone(), toolDiam / 2); + offset = POLY.filter(offset, [], function(poly) { + if (poly.area() < 1) { + return null; + } + for (let sp=0; sp 0; c--){ + offset.slice().forEach(op => { + // clone removes inners but the real solution is + // to limit expanded shells to through holes + let wideCut = POLY.expand([op.clone(true)], stepover*c, slice.z, [], 1); + wideCut.forEach(cut =>{ //set order of cuts when wide + cut.order = c + if(cut.inner) cut.inner.forEach(inn =>{ inn.order = c }) + }); + wideCuts.push(...wideCut) + }); + } + offset.appendAll(wideCuts); + } + } + + if (op.dogbones && !op.wide) { + CAM.addDogbones(offset, toolDiam / 5); + } + + if (tabs) { + tabs.forEach(tab => { + tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); + }); + offset = cutTabs(tabs, offset, slice.z); + } + + if (process.camStockClipTo && stock.x && stock.y && stock.center) { + let rect = newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y); + offset = cutPolys([rect], offset, slice.z, true); + } + + // offset.xout(`slice ${slice.z}`); + slice.camLines = offset; + }); + + // when top expand fails above, it creates an empty slice + slices = slices.filter(s => s.camLines); + + // project empty up and render + for (let slice of slices) { + if (false) slice.output() + .setLayer("slice", {line: 0xaaaa00}, false) + .addPolys(slice.topPolys()) + slice.output() + .setLayer("outline", {face: color, line: color}) + .addPolys(slice.camLines); + } + + addSlices(slices); + this.sliceOut = slices; + } + + prepare(ops, progress) { + let { op, state, sliceOut } = this; + let { setTool, setSpindle, setPrintPoint } = ops; + let { polyEmit, depthOutlinePath } = ops; + let { camOut, newLayer, printPoint } = ops; + let { settings, widget } = state; + let { process, controller } = settings; + + let easeDown = process.camEaseDown; + let toolDiam = this.toolDiam; + let cutdir = op.ov_conv;//process.camConventional; + let depthFirst = process.camDepthFirst; + let depthData = []; + + setTool(op.tool, op.rate, op.plunge); + setSpindle(op.spindle); + + // printpoint becomes NaN in engine mode. not sure why but this fixes it + if(Object.values(printPoint).some(v=>Number.isNaN(v))){ + printPoint = newPoint(0,0,0); + } + + for (let slice of sliceOut) { + let polys = [], t = [], c = []; + let lines =POLY.flatten(slice.camLines) + // console.log(lines); + lines.forEach((poly)=> { + poly.order = poly.order ?? 0; + let child = poly.parent; + if (depthFirst) { poly = poly.clone(); poly.parent = child ? 1 : 0 } + if (child) c.push(poly); else t.push(poly); + poly.layer = depthData.layer; + polys.push(poly); + }); + + // set cut direction on outer polys + POLY.setWinding(t, cutdir); + // set cut direction on inner polys + POLY.setWinding(c, !cutdir); + + if (depthFirst) { + depthData.push(polys); + } else { + let orderSplit = {} + polys.forEach(poly => { + if(poly.order in orderSplit) orderSplit[poly.order].push(poly); + else orderSplit[poly.order] = [poly]; + }) + Object.entries(orderSplit) //split the polys by order + .sort((a,b) => -(a[0] - b[0] )) //sort by order (highest first) + .forEach(([order, orderPolys]) => { // emit based on closest for each order + let polyLast; + // console.log({order, orderPolys}); + printPoint = poly2polyEmit(orderPolys, printPoint, function(poly, index, count) { + + polyLast = polyEmit(poly, index, count, polyLast); + }, { swapdir: false }); + }) + newLayer(); + } + } + + if (depthFirst) { + let flatLevels = depthData.map(level => { + return POLY.flatten(level.clone(true), [], true).filter(p => !(p.depth = 0)); + }).filter(l => l.length > 0); + if (flatLevels.length && flatLevels[0].length) { + // start with the smallest polygon on the top + printPoint = flatLevels[0] + .sort((a,b) => { return a.area() - b.area() })[0] + .average(); + // experimental start of ease down + let ease = op.down && easeDown ? 0.001 : 0; + printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, false, ease); + printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, true, ease); + } + } + + setPrintPoint(printPoint); + } +} + +function createFilter(op) { + let filter = slices => slices; + if (op.filter) { + try { + const obj = eval(`( ${op.filter.join('\n')} )`); + let idx = 0; + if (obj && obj.slices) { + const nadd = []; + filter = function(slices) { + for (let slice of slices) { + if (obj.slices(slice, idx++)) { + nadd.push(slice); + } + } + return nadd; + }; + } + } catch (e) { + console.log('filter parse error', e, op.filter); + } + } + return filter; +} + +class OpContour extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { addSlices } = state; + let filter = createFilter(op); + // we need topo for safe travel moves when roughing and outlining + // not generated when drilling-only. then all z moves use bounds max. + // also generates x and y contouring when selected + let topo = await CAM.Topo({ + // onupdate: (update, msg) => { + onupdate: (index, total, msg) => { + progress(index / total, msg); + }, + ondone: (slices) => { + slices = filter(slices); + this.sliceOut = slices; + addSlices(slices); + }, + contour: op, + state: state + }); + // computed if set to 0 + this.tolerance = topo.tolerance; + } + + prepare(ops, progress) { + let { op, state, sliceOut } = this; + let { settings, widget } = state; + let { process } = settings; + + let { setTolerance, setTool, setSpindle, setPrintPoint } = ops; + let { camOut, polyEmit, newLayer, printPoint, lastPoint } = ops; + let { bounds, zmax } = ops; + + let toolDiam = this.toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); + let stepover = toolDiam * op.step * 2; + let depthFirst = process.camDepthFirst; + let depthData = []; + + setTool(op.tool, op.rate, process.camFastFeedZ); + setSpindle(op.spindle); + setTolerance(this.tolerance); + + printPoint = newPoint(bounds.min.x,bounds.min.y,zmax); + + for (let slice of sliceOut) { + // ignore debug slices + if (!slice.camLines) { + continue; + } + let polys = [], poly, emit; + slice.camLines.forEach(function (poly) { + if (depthFirst) poly = poly.clone(true); + polys.push({first:poly.first(), last:poly.last(), poly:poly}); + }); + if (depthFirst) { + depthData.appendAll(polys); + } else { + printPoint = tip2tipEmit(polys, printPoint, function(el, point, count) { + poly = el.poly; + if (poly.last() === point) { + poly.reverse(); + } + poly.forEachPoint(function(point, pidx) { + camOut(point.clone(), pidx > 0, stepover); + }, false); + }); + newLayer(); + } + } + + if (depthFirst) { + printPoint = tip2tipEmit(depthData, printPoint, function(el, point, count) { + let poly = el.poly; + if (poly.last() === point) { + poly.reverse(); + } + poly.forEachPoint(function(point, pidx) { + camOut(point.clone(), pidx > 0, stepover); + }, false); + newLayer(); + return lastPoint(); + }); + } + + setPrintPoint(printPoint); + } +} + +class OpLathe extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { addSlices } = state; + let filter = createFilter(op); + + this.topo = await CAM.Topo4({ + op, + state, + onupdate: (pct, msg) => { + progress(pct, msg); + }, + ondone: (slices) => { + slices = filter(slices); + this.slices = slices; + addSlices(slices, false); + } + }); + } + + prepare(ops, progress) { + let { op, state, slices, topo } = this; + let { settings } = state; + + let { setTool, setSpindle, setPrintPoint } = ops; + let { camOut, newLayer, printPoint } = ops; + let { zmax } = ops; + + let toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); + let stepover = toolDiam * op.step * 2; + let rez = topo.resolution; + + setTool(op.tool, op.rate, op.plunge); + setSpindle(op.spindle); + + // start top center, X = 0, Y = 0 closest to 4th axis chuck + printPoint = newPoint(0, 0, zmax); + + for (let slice of slices) { + // ignore debug slices + if (!slice.camLines) { + continue; + } + + let last; + for (let path of slice.camLines) { + let latent; + path.forEachPoint((point, pidx) => { + if (last) { + const dz = Math.abs(last.z - point.z); + if (dz < rez) { + // latent point should still be included in + // preview b/c arcs would look like straight lines + latent = point.clone(); + return; + } + if (latent) { + camOut(latent, true, stepover); + latent = undefined; + } + } + camOut(last = point.clone(), pidx > 0, stepover); + }, false); + if (latent) { + camOut(latent, true, stepover); + } + } + + newLayer(); + } + + // move to safe height and reset A axis + let last = ops.lastPoint(); + let amax = (Math.round(last.a / 360) * 360).round(2); + // camOut(last = last.clone().setZ(zmax), 0); + // camOut(last = last.clone().setA(amax), 0); + newLayer(); + ops.addGCode([`G0 Z${zmax.round(2)}`,`G0 A${amax}`,"G92 A0"]); + + setPrintPoint(last); + } +} + +class OpTrace extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + const debug = false; + let { op, state } = this; + let { tool, rate, down, plunge, offset, offover, thru } = op; + let { ov_conv } = op; + let { settings, widget, addSlices, zThru, tabs, workarea } = state; + let { updateToolDiams, cutTabs, cutPolys, healPolys, color, shadowAt } = state; + let { process, stock } = settings; + let { camStockClipTo } = process; + if (state.isIndexed) { + throw 'trace op not supported with indexed stock'; + } + // generate tracing offsets from chosen features + let zTop = workarea.top_z; + let zBottom = workarea.bottom_z; + let sliceOut = this.sliceOut = []; + let areas = op.areas[widget.id] || []; + let camTool = new CAM.Tool(settings, tool); + let toolDiam = camTool.fluteDiameter(); + let toolOver = toolDiam * op.step; + let traceOffset = camTool.traceOffset() + let cutdir = ov_conv; + let polys = []; + let reContour = false; + let canRecontour = offset !== 'none' && down === 0; + let stockRect = stock.center && stock.x && stock.y ? + newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y) : undefined; + updateToolDiams(toolDiam); + + if (tabs) { + tabs.forEach(tab => { + tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); + }); + } + for (let arr of areas) { + let poly = newPolygon().fromArray(arr); + POLY.setWinding([ poly ], cutdir, false); + polys.push(poly); + let zs = poly.points.map(p => p.z); + let min = Math.min(...zs); + let max = Math.max(...zs); + if (max - min > 0.0001 && canRecontour) { + reContour = true; + } + } + if (false) newSliceOut(0).output() + .setLayer("polys", {line: 0xaaaa00}, false) + .addPolys(polys); + function newSliceOut(z) { + let slice = newSlice(z); + addSlices(slice); + sliceOut.push(slice); + return slice; + } + function minZ(z) { + return zBottom ? Math.max(zBottom, z - thru) : z - thru; + } + function followZ(poly) { + if (op.dogbone) { + CAM.addDogbones(poly, toolDiam / 5, !op.revbone); + } + let z = poly.getZ(); + let slice = newSliceOut(z); + slice.camTrace = { tool, rate, plunge }; + if (tabs) { + slice.camLines = cutTabs(tabs, [poly], z); + } else { + slice.camLines = [ poly ]; + } + if (camStockClipTo && stockRect) { + slice.camLines = cutPolys([stockRect], slice.camLines, z, true); + } + if (reContour) { + state.contourPolys(widget, slice.camLines); + } + slice.output() + .setLayer("trace follow", {line: color}, false) + .addPolys(slice.camLines) + } + function clearZnew(polys, z, down) { + if (down) { + // adjust step down to a value <= down that + // ends on the lowest z specified + let diff = zTop - z; + down = diff / Math.ceil(diff / down); + } + let zs = down ? base.util.lerp(zTop, z, down) : [ z ]; + let zpro = 0, zinc = 1 / (polys.length * zs.length); + for (let poly of polys) { + // newPocket(); + for (let z of zs) { + let clip = [], shadow; + shadow = shadowAt(z); + POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0); + if (op.outline) { + POLY.clearInner(clip); + } + if (clip.length === 0) { + continue; + } + let slice = newSliceOut(z); + let count = 999; + slice.camTrace = { tool, rate, plunge }; + if (toolDiam) { + const offs = [ -toolDiam / 2, -toolOver ]; + POLY.offset(clip, offs, { + count, outs: slice.camLines = [], flat:true, z, minArea: 0 + }); + } else { + // when engraving with a 0 width tip + slice.camLines = clip; + } + if (tabs) { + slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z); + } else { + slice.camLines = POLY.flatten(slice.camLines, null, true); + } + POLY.setWinding(slice.camLines, cutdir, false); + slice.output() + .setLayer("trace", {line: color}, false) + .addPolys(slice.camLines) + if (debug && shadow) slice.output() + .setLayer("trace shadow", {line: 0xff8811}, false) + .addPolys(shadow) + progress(zpro, "trace"); + zpro += zinc; + addSlices(slice); + } + } + } + function similar(v1, v2, epsilon = 0.01) { + return Math.abs(v1-v2) <= epsilon; + } + function centerPoly(p1, p2) { + // follow poly with most points + if (p2.length > p1.length) { + let t = p1; + p1 = p2; + p2 = t; + } + let np = newPolygon().setOpen(true); + for (let p of p1.points) { + let q = p2.findClosestPointTo(p); + np.push(p.midPointTo3D(q.point)); + } + return np; + } + function centerPolys(polys) { + // select open polys and sort by length + let ptst = polys.filter(p => p.isOpen()).sort((a,b) => b.perimeter() - a.perimeter()); + if (ptst.length < 2) { + return polys; + } + let pt = newPoint(0,0,0); + // ensure polys are ordered with start point closest to 0,0 + ptst.forEach(p => { + if (p.last().distTo2D(pt) < p.first().distTo2D(pt)) { + p.reverse(); + } + }); + let pout = polys.filter(p => p.isClosed()); + outer: for (let i=0,l=ptst.length; i p)); + return pout; + } + // connect selected segments if open and touching + polys = healPolys(polys); + // find center line for open polys spaced by tool diameter + polys = centerPolys(polys); + switch (op.mode) { + case "follow": + let routed = []; + poly2polyEmit(polys, newPoint(0,0,0), (poly, index, count, spoint) => { + routed.push(poly); + }); + let output = []; + for (let poly of POLY.nest(routed)) { + let offdist = offset !== 'none' ? offover : 0; + if (!offdist) + switch (offset) { + case "outside": offdist = traceOffset; break; + case "inside": offdist = -traceOffset; break; + } else if (offset === "inside") { + offdist = -offdist; + } + if (offdist) { + let pnew = POLY.offset([poly], offdist, { minArea: 0, open: true }); + if (pnew) { + poly = POLY.setZ(pnew, poly.getZ()); + } else { + continue; + } + } else { + poly = [ poly ]; + } + for (let pi of POLY.flatten(poly, [], true)) + if (down) { + let zto = minZ(pi.getZ()); + if (zThru && similar(zto,0)) { + zto -= zThru; + } + for (let z of base.util.lerp(zTop, zto, down)) { + output.push(pi.clone().setZ(z)); + } + } else { + if (thru) { + pi.setZ(pi.getZ() - thru); + } + output.push(pi); + } + if (!down && op.merge) { + let nest = POLY.nest(output); + let union = POLY.union(nest, 0, true); + output = POLY.flatten(union, [], true); + } + } + for (let poly of output) { + followZ(poly); + } + break; + case "clear": + const zbo = widget.track.top - widget.track.box.d; + let zmap = {}; + for (let poly of polys) { + let z = minZ(poly.minZ()); + if (offover) { + let pnew = POLY.offset([poly], -offover, { minArea: 0, open: true }); + if (pnew) { + poly = POLY.setZ(pnew, poly.getZ()); + } else { + continue; + } + } else { + poly = [ poly ]; + } + (zmap[z] = zmap[z] || []).appendAll(poly); + } + for (let [zv, polys] of Object.entries(zmap)) { + clearZnew(polys, parseFloat(zv), down); + } + } + } + + prepare(ops, progress) { + let { op, state } = this; + let { settings } = state; + let { setTool, setSpindle } = ops; + + setTool(op.tool, op.rate); + setSpindle(op.spindle); + for (let slice of this.sliceOut) { + ops.emitTrace(slice); + } + } +} + +class OpPocket extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + const pocket = this; + const debug = false; + let { op, state } = this; + let { tool, rate, down, plunge, expand, contour, smooth, tolerance } = op; + let { ov_botz, ov_conv } = op; + let { settings, widget, addSlices, zBottom, zThru, tabs, color } = state; + let { updateToolDiams, cutTabs, healPolys, shadowAt, workarea } = state; + let { process } = settings; + zBottom = ov_botz ? workarea.bottom_stock + ov_botz : zBottom; + // generate tracing offsets from chosen features + let sliceOut; + let pockets = this.pockets = []; + let camTool = new CAM.Tool(settings, tool); + let toolDiam = camTool.fluteDiameter(); + let toolOver = toolDiam * op.step; + let cutdir = ov_conv; + let engrave = contour && op.engrave; + let zTop = workarea.top_z; + if (contour) { + down = 0; + this.topo = await CAM.Topo({ + // onupdate: (update, msg) => { + onupdate: (index, total, msg) => { + progress((index / total) * 0.9, msg); + }, + ondone: (slices) => { + // console.log({ contour: slices }); + }, + contour: { + tool, + tolerance, + inside: true, + axis: "-" + }, + state: state + }); + } + updateToolDiams(toolDiam); + if (tabs) { + tabs.forEach(tab => { + tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); + }); + } + function newPocket() { + pockets.push(sliceOut = []); + } + function newSliceOut(z) { + let slice = newSlice(z); + sliceOut.push(slice); + return slice; + } + function clearZ(polys, z, down) { + if (down) { + // adjust step down to a value <= down that + // ends on the lowest z specified + let diff = zTop - z; + down = diff / Math.ceil(diff / down); + } + let zs = down ? base.util.lerp(zTop, z, down) : [ z ]; + if (engrave) { + toolDiam = toolOver; + } + if (contour) { + expand = engrave ? 0 : expand; + } else if (expand) { + polys = POLY.offset(polys, expand); + } + let zpro = 0, zinc = 1 / (polys.length * zs.length); + for (let poly of polys) { + newPocket(); + for (let z of zs) { + let clip = [], shadow; + if (contour) { + if (smooth) { + clip = POLY.offset(POLY.offset([ poly ], smooth), -smooth); + } else { + clip = [ poly ]; + } + } else { + shadow = shadowAt(z); + if (smooth) { + shadow = POLY.setZ(POLY.offset(POLY.offset(shadow, smooth), -smooth), z); + } + POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0); + if (op.outline) { + POLY.clearInner(clip); + } + } + if (clip.length === 0) { + continue; + } + let slice = newSliceOut(z); + let count = engrave ? 1 : 999; + slice.camTrace = { tool, rate, plunge }; + if (toolDiam) { + const offs = contour ? + [ expand || (-0.02), -toolOver ] : + [ -toolDiam / 2, -toolOver ]; + POLY.offset(clip, offs, { + count, outs: slice.camLines = [], flat:true, z, minArea: 0 + }); + } else { + // when engraving with a 0 width tip + slice.camLines = clip; + } + if (tabs) { + slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z); + } else { + slice.camLines = POLY.flatten(slice.camLines, null, true); + } + POLY.setWinding(slice.camLines, cutdir, false); + if (contour) { + slice.camLines = pocket.conform(slice.camLines, op.refine, engrave, pct => { + progress(0.9 + (zpro + zinc * pct) * 0.1, "conform"); + }); + } + slice.output() + .setLayer("pocket", {line: color}, false) + .addPolys(slice.camLines) + if (debug && shadow) slice.output() + .setLayer("pocket shadow", {line: 0xff8811}, false) + .addPolys(shadow) + if (!contour) { + progress(zpro, "pocket"); + } + zpro += zinc; + addSlices(slice); + } + } + } + let surfaces = op.surfaces[widget.id] || []; + let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4)); + // let vert = widget.getVertices().array.map(v => v.round(4)); + let outline = []; + let faces = CAM.surface_find(widget, surfaces, (op.follow || 5) * DEG2RAG); + let zmin = Infinity; + let j=0, k=faces.length; + for (let face of faces) { + let i = face * 9; + outline.push(newPolygon() + .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) + .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) + .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) + ); + } + zmin = Math.max(zBottom, zmin); + outline = POLY.union(outline, 0.0001, true); + outline = POLY.setWinding(outline, cutdir, false); + outline = healPolys(outline); + if (smooth) { + outline = POLY.offset(POLY.offset(outline, smooth), -smooth); + } + if (outline.length) { + // option to skip interior features (holes, pillars) + if (op.outline) { + POLY.clearInner(outline); + } + if (debug) newSliceOut(zmin).output() + .setLayer("pocket area", {line: 0x1188ff}, false) + .addPolys(outline) + clearZ(outline, zmin + 0.0001, down); + progress(1, "pocket"); + } + } + + // mold cam output lines to the surface of the topo offset by tool geometry + conform(camLines, refine, engrave, progress) { + const topo = this.topo; + // re-segment polygon to a higher resolution + const hirez = camLines.map(p => p.segment(topo.tolerance * 2)); + // walk points and offset from surface taking into account tool geometry + let steps = hirez.length; + let iter = 0; + for (let poly of hirez) { + for (let point of poly.points) { + point.z = engrave ? topo.zAtXY(point.x, point.y) : topo.toolAtXY(point.x, point.y); + } + progress((iter++ / steps) * 0.8); + } + steps = steps * refine; + iter = 0; + // walk points noting z deltas and smoothing z sawtooth patterns + for (let j=0; j p.midpoints(topo.tolerance * 8)); + return hirez; + } + + prepare(ops, progress) { + let { op, state, pockets } = this; + let { setTool, setSpindle, setTolerance, sliceOutput, getPrintPoint } = ops; + let { process } = state.settings; + + setTool(op.tool, op.rate); + setSpindle(op.spindle); + + if (this.topo) { + setTolerance(this.topo.tolerance); + } + + // eliminate empty pockets + pockets = pockets.filter(p => p.length); + + // pockets is an [ array of an [ array of slices ] ] + // each top level array is a pocket containing a [ z layer array of slices ] + // follow each pocket to the next closest one from previous exit + for (;;) { + let printPoint = getPrintPoint(); + let min = { + dist: Infinity, + pocket: undefined + }; + for (let pocket of pockets.filter(p => !p.used)) { + let poly = pocket[0].camLines.slice().sort((a,b) => b.area() - a.area())[0]; + if (!poly) continue; + let find = poly.findClosestPointTo(printPoint); + if (find.distance < min.dist) { + min.pocket = pocket; + min.dist = find.distance; + } + } + if (min.pocket) { + min.pocket.used = true; + sliceOutput(min.pocket, { + cutdir: op.ov_conv, + depthFirst: process.camDepthFirst && !state.isIndexed, + easeDown: op.down && process.easeDown ? op.down : 0, + progress: (n,m) => progress(n/m, "pocket") + }); + } else { + break; + } + } + } +} + +class OpDrill extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, addSlices, widget, updateToolDiams } = state; + let { zBottom, zThru, thruHoles, color } = state; + let { drills, drillThrough} = op + + let drillTool = new CAM.Tool(settings, op.tool), + drillToolDiam = drillTool.fluteDiameter(), + sliceOut = this.sliceOut = []; + + updateToolDiams(drillToolDiam); + + const allDrills = drills[widget.id] ?? [] + // drill points to use center (average of all points) of the polygon + allDrills.forEach((drill)=> { + if(!drill.selected){ + return + } + let slice = newSlice(0); + if (op.mark) { + // replace depth with single down peck + drill.depth = op.down + } + drill.zBottom = drill.z - drill.depth; + // for thru holes, follow z thru when set + if ((op.thru>0) ) { + drill.zBottom -= op.thru; + } + const poly = newPolygon() + poly.points.push(newPoint(drill.x, drill.y, drill.z)) + poly.points.push(newPoint(drill.x, drill.y, drill.zBottom)) + // poly.points.pop(); + slice.camTrace = { tool: op.tool, rate: op.feed, plunge: op.rate }; + slice.camLines = [poly]; + + slice.output() + .setLayer("drill", {face: color, line: color}) + .addPolys(slice.camLines); + addSlices(slice); + sliceOut.push(slice); + }); + } + + prepare(ops, progress) { + let { op, state } = this; + let { settings, widget, addSlices, updateToolDiams } = state; + let { setTool, setSpindle, setDrill, emitDrills } = ops; + setTool(op.tool, undefined, op.rate); + setDrill(op.down, op.lift, op.dwell,op.thru); + setSpindle(op.spindle); + emitDrills(this.sliceOut.map(slice => slice.camLines).flat()); + } +} + +class OpRegister extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, widget, bounds, addSlices, zMax, zThru, color } = state; + let { updateToolDiams } = state; + + let tool = new CAM.Tool(settings, op.tool); + let sliceOut = this.sliceOut = []; + + updateToolDiams(tool.fluteDiameter()); + + let { stock } = settings, + tz = widget.track.pos.z, + lx = bounds.min.x, + hx = bounds.max.x, + ly = bounds.min.y, + hy = bounds.max.y, + o3 = tool.fluteDiameter() * 2, + mx = (lx + hx) / 2, + my = (ly + hy) / 2, + mz = op.thru || zThru || 0, + dx = (stock.x - (hx - lx)) / 4, + dy = (stock.y - (hy - ly)) / 4, + dz = stock.z, + points = [], + wo = stock.z - bounds.max.z, + z1 = bounds.max.z + wo + tz, + z2 = tz - mz; + + if (!(stock.x && stock.y && stock.z)) { + return; + } + + switch (op.axis) { + case "X": + case "x": + if (op.points == 3) { + points.push(newPoint(lx - dx, my, 0)); + points.push(newPoint(hx + dx, my - o3, 0)); + points.push(newPoint(hx + dx, my + o3, 0)); + } else { + points.push(newPoint(lx - dx, my, 0)); + points.push(newPoint(hx + dx, my, 0)); + } + break; + case "Y": + case "y": + if (op.points == 3) { + points.push(newPoint(mx, ly - dy, 0)); + points.push(newPoint(mx - o3, hy + dy, 0)); + points.push(newPoint(mx + o3, hy + dy, 0)); + } else { + points.push(newPoint(mx, ly - dy, 0)); + points.push(newPoint(mx, hy + dy, 0)); + } + break; + case "-": + let o2 = o3 / 2, + x0 = lx - dx, + x1 = hx + dx, + y0 = ly - dy - o2, + y1 = hy + dy + o2, + x4 = (x1 - x0 - o2) / 4, + y4 = (y1 - y0 - o2 * 3) / 4, + poly, cp, cz; + function start(z) { + cz = z; + cp = {x:x0 + o2 * 0.5, y:y0 + o2 * 1.5}; + poly = newPolygon().add(cp.x, cp.y, z); + } + function move(dx, dy) { + cp.x += dx; + cp.y += dy; + poly.add(cp.x, cp.y, cz); + } + function rept(count, tv, fn) { + while (count-- > 0) { + fn(tv, count === 0); + tv = -tv; + } + } + for (let z of base.util.lerp(z1, z2, op.down)) { + let slice = newSlice(z); + addSlices(slice); + sliceOut.push(slice); + start(z); + rept(4, o2, oy => { + move(0, -oy); + move(x4, 0); + }); + rept(4, o2, ox => { + move(ox, 0); + move(0, y4); + }); + rept(4, o2, oy => { + move(0, oy); + move(-x4, 0); + }); + rept(4, o2, ox => { + move(-ox, 0); + move(0, -y4); + }); + poly.points.pop(); + slice.camTrace = { tool: tool.getID(), rate: op.feed, plunge: op.rate }; + slice.camLines = [ poly ]; + slice.output() + .setLayer("register", {line: color}, false) + .addPolys(slice.camLines) + } + break; + } + + if (points.length) { + let slice = newSlice(0,null), polys = []; + points.forEach(point => { + polys.push(newPolygon() + .append(point.clone().setZ(z1)) + .append(point.clone().setZ(z2))); + }); + slice.camLines = polys; + slice.output() + .setLayer("register", {face: color, line: color}) + .addPolys(polys); + addSlices(slice); + sliceOut.push(slice); + } + } + + prepare(ops, progress) { + let { op, state } = this; + let { settings, widget, addSlices, updateToolDiams } = state; + let { setTool, setSpindle, setDrill, emitDrills } = ops; + + if (op.axis === '-') { + setTool(op.tool, op.feed, op.rate); + setSpindle(op.spindle); + for (let slice of this.sliceOut) { + ops.emitTrace(slice); + } + } else { + setTool(op.tool, undefined, op.rate); + setDrill(op.down, op.lift, op.dwell); + setSpindle(op.spindle); + emitDrills(this.sliceOut.map(slice => slice.camLines).flat()); + } + } +} + +class OpXRay extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { widget, addSlices } = this.state; + let slicer = new kiri.cam_slicer(widget); + let xrayind = Object.keys(slicer.zLine) + .map(v => parseFloat(v).round(5)) + .sort((a,b) => a-b); + let xrayopt = { each: data => { + let slice = newSlice(data.z); + slice.addTops(data.tops); + // data.tops.forEach(top => slice.addTop(top)); + slice.lines = data.lines; + slice.xray(); + addSlices(slice); + }, over: false, flatoff: 0, edges: true, openok: true }; + await slicer.slice(xrayind, xrayopt); + // xrayopt.over = true; + // slicer.slice(xrayind, xrayopt); + } +} + +/** + * Computes the Part "shadow" and attaches relevant data to the "state" object + * + * The part shadow consists of top-cown layers at which the polygon shadow changes + * shape. For curved or sloped surfaces, this is approximated and paths that clip + * to it should use the next lower layer from current Z to ensure no part collisions. + * + * The shadow at each layer is computed by top-down unioning the part outline with + * the shadow from the layer above. + * + * This operation is injected at the start of the operation chain before processing. + */ +class OpShadow extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let state = this.state; + let { ops, slicer, widget, unsafe, addSlices, shadowAt } = state; + + let realOps = ops.map(rec => rec.op).filter(op => op); + let trueShadow = state.settings.process.camTrueShadow === true; + + let minStepDown = realOps + .map(op => (op.down || 3) / (trueShadow ? 1 : 3)) + .reduce((a,v) => Math.min(a, v, 1)); + + let tslices = []; + let tshadow = []; + let tzindex = slicer.interval(minStepDown, { + fit: true, off: 0.01, down: true, flats: true + }); + let skipTerrain = unsafe; + + if (skipTerrain) { + console.log("skipping terrain generation"); + tzindex = [ tzindex.pop() ]; + } + + let lsz; // only shadow up to bottom of last shadow for progressive union + let cnt = 0; + let tot = 0; + + // terrain is the "shadow stack" where index 0 = top of part + // thus array.length -1 = bottom of part + let terrain = await slicer.slice(tzindex, { each: data => { + let shadow = trueShadow ? shadowAt(data.z, lsz) : []; + tshadow = POLY.union(tshadow.slice().appendAll(data.tops).appendAll(shadow), 0.01, true); + tslices.push(data.slice); + // capture current shadow for this slice + data.slice.shadow = tshadow; + if (false) { + const slice = data.slice; + addSlices(slice); + slice.output() + .setLayer("shadow", {line: 0x888800, thin: true }) + .addPolys(POLY.setZ(tshadow.clone(true), data.z), { thin: true }); + slice.output() + .setLayer("slice", {line: 0x886622, thin: true }) + .addPolys(POLY.setZ(data.tops.clone(true), data.z), { thin: true }); + // let p1 = [], p2 = [], cp = p1; + // for (let line of data.lines) { + // cp.push(line.p1); + // cp.push(line.p2); + // cp = (cp === p1 ? p2 : p1); + // } + // slice.output() + // .setLayer("lines1", {line: 0x884444, thin: true }) + // .addLines(p1, { thin: true }); + // slice.output() + // .setLayer("lines2", {line: 0x444488, thin: true }) + // .addLines(p2, { thin: true }); + } + lsz = data.z; + progress(0.5 + 0.5 * (++cnt / tot)); + }, progress: (index, total) => { + tot = total; + progress((index / total) * 0.5); + } }); + + if (terrain.length === 0) { + throw `invalid widget shadow`; + } + + // TODO: deprecate use of separate shadow vars in state + state.center = tshadow[0].bounds.center(); + state.tshadow = tshadow; // true shadow (base of part) + state.terrain = terrain; // stack of shadow slices stored in tops + state.tslices = tslices; // raw slicer 'data' layer outputs + state.skipTerrain = skipTerrain; + + // TODO: refactor ops to use a unified shadow object + state.shadow = { + base: tshadow, // computed shadow union at base of part + stack: terrain, // stack of shadow slices + slices: tslices, // raw slicer 'data' objects + skip: skipTerrain + }; + + // identify through holes which are inner/child polygons + // on the bottom-most layer of the shadow stack (tshadow, index == 0) + state.thruHoles = tshadow.map(p => p.inner || []).flat(); + } +} + +// union triangles > z (opt cap < ztop) into polygon(s) +CAM.shadowAt = function(widget, z, ztop) { + const geo = widget.cache.geo; + const length = geo.length; + // cache faces with normals up + if (!widget.cache.shadow) { + const faces = []; + for (let i=0, ip=0; i 0.001) { + faces.push(a,b,c); + // faces.push(newPoint(...a), newPoint(...b), newPoint(...c)); + } + } + widget.cache.shadow = faces; + } + const found = []; + const faces = widget.cache.shadow; + const { checkOverUnderOn, intersectPoints } = self.kiri.cam_slicer; + for (let i=0; i ztop && b.z > ztop && c.z > ztop) { + // skip faces over top threshold + continue; + } + if (a.z < z && b.z < z && c.z < z) { + // skip faces under threshold + continue; + } else if (a.z > z && b.z > z && c.z > z) { + found.push([a,b,c]); + } else { + // check faces straddling threshold + const where = { under: [], over: [], on: [] }; + checkOverUnderOn(newPoint(a.x, a.y, a.z), z, where); + checkOverUnderOn(newPoint(b.x, b.y, b.z), z, where); + checkOverUnderOn(newPoint(c.x, c.y, c.z), z, where); + if (where.on.length === 0 && (where.over.length === 2 || where.under.length === 2)) { + // compute two point intersections and construct line + let line = intersectPoints(where.over, where.under, z); + if (line.length === 2) { + if (where.over.length === 2) { + found.push([where.over[1], line[0], line[1]]); + found.push([where.over[0], where.over[1], line[0]]); + } else { + found.push([where.over[0], line[0], line[1]]); + } + } else { + console.log({msg: "invalid ips", line: line, where: where}); + } + } + } + } + + // const lines = {}; + // function addline(p1, p2) { + // let key = p1.key < p2.key ? p1.key + ',' + p2.key : p2.key + ',' + p1.key; + // let rec = lines[key]; + // if (rec) { + // rec.count++; + // } else { + // lines[key] = { p1, p2, count: 1 }; + // } + // } + // for (let face of found) { + // addline(face[0], face[1]); + // addline(face[1], face[2]); + // addline(face[2], face[0]); + // } + // const singles = Object.entries(lines).filter(a => a[1].count === 1).map(a => a[1]); + // const loops = POLY.nest(sliceConnect(singles, z)); + + let polys = found.map(a => { + return newPolygon() + .add(a[0].x,a[0].y,a[0].z) + .add(a[1].x,a[1].y,a[1].z) + .add(a[2].x,a[2].y,a[2].z); + }); + polys = POLY.union(polys, 0.001, true); + // console.log({z, loops, polys}); + // return loops; + return polys; +}; + +CAM.OPS = CamOp.MAP = { + "xray": OpXRay, + "shadow": OpShadow, + "level": OpLevel, + "rough": OpRough, + "outline": OpOutline, + "contour": OpContour, + "pocket": OpPocket, + "lathe": OpLathe, + "trace": OpTrace, + "drill": OpDrill, + "register": OpRegister, + "laser on": OpLaserOn, + "laser off": OpLaserOff, + "gcode": OpGCode, + "index": OpIndex, + "flip": CamOp +}; + +}); diff --git a/src2/kiri-mode/cam/prepare.js b/src.old/kiri-mode/cam/prepare.js similarity index 85% rename from src2/kiri-mode/cam/prepare.js rename to src.old/kiri-mode/cam/prepare.js index 166a58ed..507c2f61 100644 --- a/src2/kiri-mode/cam/prepare.js +++ b/src.old/kiri-mode/cam/prepare.js @@ -1,14 +1,25 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base, util } from '../../geo/base.js'; -import { tip2tipEmit, poly2polyEmit, arcToPath } from '../../geo/paths.js'; -import { newPoint } from '../../geo/point.js'; -import { polygons as POLY } from '../../geo/polygons.js'; -import { render } from '../../kiri/render.js'; -import { newPrint } from '../../kiri/print.js'; -import { Tool } from './tool.js'; +"use strict"; + +// dep: geo.paths +// dep: geo.point +// dep: geo.polygons +// dep: kiri.render +// dep: kiri-mode.cam.driver +// dep: kiri.print +// use: kiri-mode.cam.ops +gapp.register("kiri-mode.cam.prepare", (root, exports) => { + +const { base, kiri } = root; +const { paths, polygons, newPoint, util } = base; +const { toRadians} = util +const { tip2tipEmit, poly2polyEmit, arcToPath } = paths; +const { driver, render } = kiri; +const { CAM } = driver; + +const POLY = polygons; -const { toRadians } = util /** * DRIVER PRINT CONTRACT @@ -18,11 +29,11 @@ const { toRadians } = util * @param {Number} [index] into widget array * @param {Object} [firstPoint] starting point */ -export async function cam_prepare(widall, settings, update) { +CAM.prepare = async function(widall, settings, update) { const widgets = widall.filter(w => !w.track.ignore && !w.meta.disabled); const count = widgets.length; - const weight = 1 / count; - const print = self.kiri_worker.current.print = newPrint(settings, widgets); + const weight = 1/count; + const print = self.worker.print = kiri.newPrint(settings, widgets); print.output = []; let point; @@ -35,11 +46,11 @@ export async function cam_prepare(widall, settings, update) { const output = print.output.filter(level => Array.isArray(level)); if (render) // allows it to run from CLI - return render.path( - output, - (progress, layer) => { - update(0.75 + progress * 0.25, "render", layer); - }, { + return render.path( + output, + (progress, layer) => { + update(0.75 + progress * 0.25, "render", layer); + }, { thin: true, print: 0, move: 0x557799, @@ -49,10 +60,10 @@ export async function cam_prepare(widall, settings, update) { action: "milling", maxspeed: settings.process.camFastFeed || 6000 } - ); + ); }; -export function prepEach(widget, settings, print, firstPoint, update) { +function prepEach(widget, settings, print, firstPoint, update) { if (widget.camops.length === 0 || widget.meta.disabled) return; @@ -169,7 +180,7 @@ export function prepEach(widget, settings, print, firstPoint, update) { function setTool(toolID, feed, plunge) { if (toolID !== lastTool) { - tool = new Tool(settings, toolID); + tool = new CAM.Tool(settings, toolID); toolType = tool.getType(); toolDiam = tool.fluteDiameter(); toolDiamMove = toolType === 'endmill' ? toolDiam : tolerance * 2; @@ -193,13 +204,13 @@ export function prepEach(widget, settings, print, firstPoint, update) { function emitDrills(polys) { polys = polys.slice(); - for (; ;) { + for (;;) { let closestDist = Infinity, closestI, closest = null, dist; - for (let i = 0; i < polys.length; i++) { + for (let i=0; i down * 2) { points.push(point.clone()); point.z -= down; @@ -242,14 +253,14 @@ export function prepEach(widget, settings, print, firstPoint, update) { } } camOut(point.clone().setZ(zmax)); - points.forEach(function (point, index) { + points.forEach(function(point, index) { camOut(point, 1); if (index > 0 && index < points.length - 1) { if (dwell) camDwell(dwell); if (lift) camOut(point.clone().setZ(point.z + lift), 0); } }) - camOut(point.clone().setZ(zmax), 0); + camOut(point.clone().setZ(zmax),0); newLayer(); } @@ -290,9 +301,9 @@ export function prepEach(widget, settings, print, firstPoint, update) { if (lasering.flat) { point.z = (stock && stock.z ? stock.z : wztop) + lasering.flatz; } - print.addOutput(layerOut, point, power, speed, tool, { type: 'laser' }); + print.addOutput(layerOut, point, power, speed, tool, {type:'laser'}); } else { - print.addOutput(layerOut, point, emit, speed, tool, { type, center, arcPoints }); + print.addOutput(layerOut, point, emit, speed, tool, {type, center, arcPoints}); } lastPush = { point, emit, speed, tool }; return point; @@ -307,12 +318,13 @@ export function prepEach(widget, settings, print, firstPoint, update) { ); } + /** * Move a point by the widget's movement offset. * @param {Point} p - point to move * @return {Point} new point with offset applied */ - function applyWidgetMovement(p) { + function applyWidgetMovement(p){ return newPoint( p.x + wmx, p.y + wmy, @@ -329,7 +341,8 @@ export function prepEach(widget, settings, print, firstPoint, update) { * @param {number} opts.moveLen typically = tool diameter used to trigger terrain detection * @param {number} opts.factor speed scale factor */ - function camOut(point, emit, opts) { + function camOut(point, emit,opts) { + // console.log({point, emit, opts}) let { center = {}, @@ -373,7 +386,7 @@ export function prepEach(widget, settings, print, firstPoint, update) { emit, rate, tool, - { type: "lerp" }, + {type:"lerp"}, ); } } @@ -400,7 +413,7 @@ export function prepEach(widget, settings, print, firstPoint, update) { } // convert short planar moves to cuts in some cases - if (!isRough && !isArc && isMove && deltaXY <= moveLen && deltaZ <= 0 && !lasering) { + if (!isRough&& !isArc && isMove && deltaXY <= moveLen && deltaZ <= 0 && !lasering ) { let iscontour = tolerance > 0; let isflat = absDeltaZ < 0.001; // restrict this to contouring @@ -422,21 +435,21 @@ export function prepEach(widget, settings, print, firstPoint, update) { } else if (isMove) { // for longer moves, check the terrain to see if we need to go up and over const bigXY = (deltaXY > moveLen && !lasering); - const bigZ = (deltaZ > toolDiam / 2 && deltaXY > tolerance); + const bigZ = (deltaZ > toolDiam/2 && deltaXY > tolerance); const midZ = (tolerance && absDeltaZ >= tolerance) && !isContour; if (bigXY || bigZ || midZ) { let maxz = getZClearPath( - terrain, - lastPoint.x - wmx, - lastPoint.y - wmy, - point.x - wmx, - point.y - wmy, - Math.max(point.z, lastPoint.z), - zadd, - maxToolDiam / 2, - zclear - ), + terrain, + lastPoint.x - wmx, + lastPoint.y - wmy, + point.x - wmx, + point.y - wmy, + Math.max(point.z, lastPoint.z), + zadd, + maxToolDiam/2, + zclear + ), maxZdelta = Math.max(maxz - point.z, maxz - lastPoint.z), mustGoUp = maxZdelta >= tolerance, clearz = maxz; @@ -470,11 +483,12 @@ export function prepEach(widget, settings, print, firstPoint, update) { rate = Math.round(plungeRate + ((feedRate - plungeRate) * modifier)); cut = 1; } else { - rate = 1 / Math.hypot(deltaXY / feedRate, absDeltaZ / plungeRate); + rate = 1 / Math.hypot(deltaXY / feedRate , absDeltaZ / plungeRate ); } } - if (isArc) { + + if(isArc) { layerOut.mode = currentOp; layerOut.spindle = spindle; lastPoint = layerPush( @@ -487,8 +501,9 @@ export function prepEach(widget, settings, print, firstPoint, update) { arcPoints } ); - } else { + }else{ // for g1 moves + // TODO: synthesize move speed from feed / plunge accordingly layerOut.mode = currentOp; layerOut.spindle = spindle; @@ -533,8 +548,8 @@ export function prepEach(widget, settings, print, firstPoint, update) { depthData.push(polys); } else { // if not depth first, output the polys in slice order - printPoint = poly2polyEmit(polys, printPoint, function (poly, index, count) { - poly.forEachPoint(function (point, pidx, points, offset) { + printPoint = poly2polyEmit(polys, printPoint, function(poly, index, count) { + poly.forEachPoint(function(point, pidx, points, offset) { // scale speed of first cutting poly since it engages the full bit camOut(point.clone(), offset !== 0, undefined, count === 1 ? engageFactor : 1); }, poly.isClosed(), index); @@ -546,7 +561,7 @@ export function prepEach(widget, settings, print, firstPoint, update) { // crucially returns true for -0 as well as other negative #s function isNeg(v) { - return v < 0 || (v === 0 && 1 / v === -Infinity); + return v < 0 || (v === 0 && 1/v === -Infinity); } if (depthFirst) { @@ -570,7 +585,7 @@ export function prepEach(widget, settings, print, firstPoint, update) { * 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, */ - function generateEaseDown(fn, poly, fromPoint, degrees = 45) { + function generateEaseDown(fn,poly, fromPoint, degrees = 45){ let index = poly.findClosestPointTo(fromPoint).index, fromZ = fromPoint.z, offset = 0, @@ -626,7 +641,7 @@ export function prepEach(widget, settings, print, firstPoint, update) { index++; } - return touch; + return touch; } // coming from a previous widget, use previous last point @@ -676,7 +691,7 @@ export function prepEach(widget, settings, print, firstPoint, update) { }; let opSum = 0; - let opTot = widget.camops.map(op => op.weight()).reduce((a, v) => a + v); + let opTot = widget.camops.map(op => op.weight()).reduce((a,v) => a + v); for (let op of widget.camops) { setTolerance(0); @@ -705,12 +720,15 @@ export function prepEach(widget, settings, print, firstPoint, update) { function emitTrace(slice) { let { tool, rate, plunge } = slice.camTrace; setTool(tool, rate, plunge); - let traceTool = new Tool(settings, tool); + let traceTool = new CAM.Tool(settings, tool); let traceToolDiam = traceTool.fluteDiameter(); printPoint = poly2polyEmit(slice.camLines, printPoint, polyEmit, { swapdir: false }); newLayer(); } + + + /** * Output a single polygon as gcode. The polygon is walked in either the * clockwise or counter-clockwise direction depending on the winding of the @@ -728,90 +746,97 @@ export function prepEach(widget, settings, print, firstPoint, update) { * @returns {Point} - the last point of the polygon */ function polyEmit(poly, index, count, fromPoint) { - let arcQ = [], - arcMax = Infinity, // no max arc radius + + let arcMax = Infinity, // no max arc radius lineTolerance = 0.001; // do not consider points under 0.001mm for arcs fromPoint = fromPoint || printPoint; + let arcQ = []; arcQ.angle = [] - let lastPoint = fromPoint; - let startIndex = index; + let closest = poly.findClosestPointTo(fromPoint); + let lastPoint = closest.point; + let startIndex = closest.index; - // console.log({poly, index, count, fromPoint, startIndex}) + // console.log({poly, index, count, fromPoint,startIndex}) // scale speed of first cutting poly since it engages the full bit let scale = ((isRough || isPocket) && count === 1) ? engageFactor : 1; - // easeDown only allowed on closed polys (that we can continue around indefinitly) - if (easeDown && poly.isClosed()) { - let closest = poly.findClosestPointTo(fromPoint); - lastPoint = closest.point; - startIndex = closest.index; - let last = generateEaseDown((point, offset) => { // generate ease-down points - if (offset == 0) camOut(point.clone(), 0, { factor: engageFactor }); - camOut(point.clone(), 1, { factor: scale }); // and pass them to camOut + + if (easeDown && poly.isClosed()) { //if doing ease-down + + let last = generateEaseDown((point,offset )=>{ //generate ease-down points + if(offset == 0) camOut(point.clone(), 0, {factor:engageFactor}); + camOut(point.clone(), 1, {factor:scale}); // and pass them to camOut }, poly, fromPoint, easeAngle); lastPoint = poly.points[last]; startIndex = last; - } + } // console.log(poly,poly.isClosed(),startIndex) - // A is first point of segment, B is last - poly.forEachSegment((pointA, pointB, indexA, indexB) => { + + //A is first point of segment, B is last + poly.forEachSegment( ( pointA, pointB, indexA, indexB) => { // if(offset == 0) console.log("forEachPoint",point,pidx,points) // console.log({pointA, pointB, indexA, indexB,startIndex}) - if (indexA == startIndex) { - camOut(pointA.clone(), 0, { factor: engageFactor }); + if(indexA == startIndex){ + camOut(pointA.clone(), 0, {factor:engageFactor}); // if first point, move to and call export function - arcQ.push(pointA); + quePush(pointA); } lastPoint = arcExport(pointB, pointA); }, !poly.isClosed(), startIndex); // console.log("at end of arcExport",structuredClone(arcQ)); - if (arcQ.length > 3) { - // if few points left, emit as lines + if(arcQ.length > 3){ + //if few points left, emit as lines drainQ(); } - while (arcQ.length) { - camOut(arcQ.shift(), 1); + while (arcQ.length){ + camOut(arcQ.shift(),1); } - function arcExport(point, lastp) { - let dist = lastp ? point.distTo2D(lastp) : 0; - if (lastp) { - if (dist > lineTolerance && lastp) { - let rec = Object.assign(point, { dist }); + function quePush(point){ + arcQ.push(point); + } + + function arcExport(point,lastp){ + let dist = lastp? point.distTo2D(lastp) : 0; + if (lastp) { + if (dist >lineTolerance && lastp) { + let rec = Object.assign(point,{dist}); arcQ.push(rec); // ondebug({arcQ}); if (arcQ.length > 2) { let el = arcQ.length; let e1 = arcQ[0]; // first in arcQ - let e2 = arcQ[Math.floor(el / 2)]; // mid in arcQ - let e3 = arcQ[el - 1]; // last in arcQ - let e4 = arcQ[el - 2]; // second last in arcQ - let e5 = arcQ[el - 3]; // third last in arcQ + let e2 = arcQ[Math.floor(el/2)]; // mid in arcQ + let e3 = arcQ[el-1]; // last in arcQ + let e4 = arcQ[el-2]; // second last in arcQ + let e5 = arcQ[el-3]; // third last in arcQ let cc = util.center2d(e1, e2, e3, 1); // find center let lr = util.center2d(e3, e4, e5, 1); // find local radius let dc = 0; let radFault = false; if (lr) { - let angle = 2 * Math.asin(dist / (2 * lr.r)); + let angle = 2 * Math.asin(dist/(2*lr.r)); radFault = Math.abs(angle) > arcRes; // enforce arcRes(olution) + } else { - // console.log("too much angle") radFault = true; + // console.log("too much angle") } + if (cc) { - if ([cc.x, cc.y, cc.z, cc.r].hasNaN()) { + if ([cc.x,cc.y,cc.z,cc.r].hasNaN()) { // console.log({cc, e1, e2, e3}); } if (arcQ.length === 3) { - arcQ.center = [cc]; + arcQ.center = [ cc ]; arcQ.xSum = cc.x; arcQ.ySum = cc.y; arcQ.rSum = cc.r; @@ -819,39 +844,39 @@ export function prepEach(widget, settings, print, firstPoint, update) { // check if first angles should have caused radFault let a = toRadians(arcQ[0].slopeTo(cc).angle) let b = toRadians(arcQ[1].slopeTo(cc).angle) - let angle = Math.abs(b - a) + let angle = Math.abs(b-a) - if (Math.abs(angle) > arcRes) { + if( Math.abs(angle) > arcRes){ // if so, remove first point camOut(arcQ.shift(), 1) radFault = true } } else { // check center point delta - arcQ.xSum = arcQ.center.reduce(function (t, v) { return t + v.x }, 0); - arcQ.ySum = arcQ.center.reduce(function (t, v) { return t + v.y }, 0); - arcQ.rSum = arcQ.center.reduce(function (t, v) { return t + v.r }, 0); + arcQ.xSum = arcQ.center.reduce( function (t, v) { return t + v.x }, 0 ); + arcQ.ySum = arcQ.center.reduce( function (t, v) { return t + v.y }, 0 ); + arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 ); let dx = cc.x - arcQ.xSum / arcQ.center.length; let dy = cc.y - arcQ.ySum / arcQ.center.length; dc = Math.hypot(dx, dy); // delta center distance } // if new point is off the arc // if (deem || depm || desp || dc > arcTolerance || cc.r < arcMin || cc.r > arcMax || dist > cc.r) { - if (dc * arcQ.center.length / arcQ.rSum > arcTolerance || dist > cc.r || cc.r > arcMax || radFault) { + if ( dc * arcQ.center.length / arcQ.rSum > arcTolerance || dist > cc.r || cc.r > arcMax || radFault ) { // let debug = [deem, depm, desp, dc * arcQ.center.length / arcQ.rSum > arcTolerance, dist > cc.r, cc.r > arcMax, radFault]; // console.log("point off the arc,",structuredClone(arcQ),); if (arcQ.length === 4) { // not enough points for an arc, drop first point and recalc center - camOut(arcQ.shift(), 1); + camOut(arcQ.shift(),1); let tc = util.center2d(arcQ[0], arcQ[1], arcQ[2], 1); // the new center is invalid as well. drop the first point if (!tc) { - camOut(arcQ.shift(), 1); + camOut(arcQ.shift(),1); } else { - arcQ.center = [tc]; - let angle = 2 * Math.asin(arcQ[1].dist / (2 * tc.r)); + arcQ.center = [ tc ]; + let angle = 2 * Math.asin(arcQ[1].dist/(2*tc.r)); if (Math.abs(angle) > arcRes) { // enforce arcRes on initial angle - camOut(arcQ.shift(), 1); + camOut(arcQ.shift(),1); } } } else { @@ -877,6 +902,7 @@ export function prepEach(widget, settings, print, firstPoint, update) { } } else { // if first point, emit and set + camOut(point, 1); // TODO disabling out of plane z moves until a better mechanism // can be built that doesn't rely on computed zpos from layer heights... @@ -886,26 +912,26 @@ export function prepEach(widget, settings, print, firstPoint, update) { } return point; } - + function drainQ() { + let arcPreviewRes = 64 if (!arcTolerance) { return; } - if (arcQ.length > 4) { // ondebug({arcQ}); let vec1 = new THREE.Vector2(arcQ[1].x - arcQ[0].x, arcQ[1].y - arcQ[0].y); let vec2 = new THREE.Vector2(arcQ.center[0].x - arcQ[0].x, arcQ.center[0].y - arcQ[0].y); - let clockwise = vec1.cross(vec2) < 0 - let gc = clockwise ? 2 : 3 + let clockwise = vec1.cross(vec2) < 0 + let gc = clockwise ? 2 :3 let from = arcQ[0]; let to = arcQ.peek(); let delta = from.distTo2D(to) - arcQ.xSum = arcQ.center.reduce((t, v) => t + v.x, 0); - arcQ.ySum = arcQ.center.reduce((t, v) => t + v.y, 0); - arcQ.rSum = arcQ.center.reduce((t, v) => t + v.r, 0); + arcQ.xSum = arcQ.center.reduce( (t, v) => t + v.x , 0 ); + arcQ.ySum = arcQ.center.reduce( (t, v) => t + v.y , 0 ); + arcQ.rSum = arcQ.center.reduce( (t, v) => t + v.r , 0 ); let cl = arcQ.center.length; let center = newPoint( arcQ.xSum / cl, @@ -913,30 +939,32 @@ export function prepEach(widget, settings, print, firstPoint, update) { ) // console.log("draining") - if (arcQ.length == poly.points.length) { + + if(arcQ.length == poly.points.length ){ //if is a circle // generate circle // console.log("circle",{from, to,center}); - let arcPoints = arcToPath(from, from, arcPreviewRes, { clockwise, center }) - .map(applyWidgetMovement); - camOut(from, 1); - camOut(from, gc, { center: center.sub(from), clockwise, arcPoints }); + let arcPoints = arcToPath( from, from, arcPreviewRes,{ clockwise,center}) + .map(applyWidgetMovement); + camOut(from,1); + camOut(from,gc,{ center:center.sub(from), clockwise, arcPoints}); // lastPoint = to.clone(); - } else { + }else{ //if a non-circle arc - let arcPoints = arcToPath(from, to, arcPreviewRes, { clockwise, center }) - .map(applyWidgetMovement); + let arcPoints = arcToPath( from, to, arcPreviewRes,{ clockwise,center}) + .map(applyWidgetMovement); // console.log("arc") // first arc point - camOut(from, 1); + camOut(from,1); // rest of arc to final point - camOut(to, gc, { center: center.sub(from), clockwise, arcPoints }); + camOut(to,gc,{ center:center.sub(from), clockwise, arcPoints}); lastPoint = to.clone(); } + } else { //if q too short, emit as lines for (let rec of arcQ) { - camOut(rec, 1); + camOut(rec,1); } lastPoint = arcQ.peek().clone(); } @@ -944,10 +972,7 @@ export function prepEach(widget, settings, print, firstPoint, update) { arcQ.center = undefined; } - if (depthFirst) { - newLayer(); - } - + // newLayer(); return lastPoint; } @@ -989,7 +1014,7 @@ export function prepEach(widget, settings, print, firstPoint, update) { function depthOutlinePath(start, depth, levels, radius, emitter, dir, ease) { let bottm = depth < levels.length - 1 ? levels[levels.length - 1] : null; - let above = levels[depth - 1]; + let above = levels[depth-1]; let level = levels[depth]; if (!level) { return start; @@ -1055,7 +1080,7 @@ function getZClearPath(terrain, x1, y1, x2, y2, z, zadd, off, over) { } let maxz = z; let check = []; - for (let i = 0; i < terrain.length; i++) { + for (let i=0; i -- All Rights Reserved */ -import { base, util } from '../../geo/base.js'; -import { newLine } from '../../geo/line.js'; -import { newPoint } from '../../geo/point.js'; -import { newPolygon } from '../../geo/polygon.js'; -import { polygons as POLY } from '../../geo/polygons.js'; -import { setSliceTracker } from '../../kiri/slice.js'; -import { ops as OPS } from './ops.js'; -import { Tool } from './tool.js'; -import { Slicer as cam_slicer } from './slicer.js'; +"use strict"; + +// dep: geo.line +// dep: geo.point +// dep: geo.polygons +// dep: kiri.slice +// dep: kiri-mode.cam.driver +// use: kiri-mode.cam.slicer +// use: kiri-mode.cam.slicer2 +// use: kiri-mode.cam.ops +gapp.register("kiri-mode.cam.slice", (root, exports) => { + +const { base, kiri } = root; +const { util } = base; +const { driver, newSlice, setSliceTracker } = kiri; +const { polygons, newLine, newPoint, newPolygon } = base; +const { CAM } = driver; +const { OPS } = CAM; + +const PRO = CAM.process; +const POLY = polygons; /** * DRIVER SLICE CONTRACT @@ -17,7 +29,7 @@ import { Slicer as cam_slicer } from './slicer.js'; * @param {Widget} widget * @param {Function} output */ -export async function cam_slice(settings, widget, onupdate, ondone) { +CAM.slice = async function(settings, widget, onupdate, ondone) { let proc = settings.process, sliceAll = widget.slices = [], camOps = widget.camops = [], @@ -93,15 +105,15 @@ export async function cam_slice(settings, widget, onupdate, ondone) { if (tabs) { // make tab polygons tabs.forEach(tab => { - let zero = newPoint(0, 0, 0), + let zero = newPoint(0,0,0), point = newPoint(tab.pos.x, tab.pos.y, tab.pos.z), poly = newPolygon().centerRectangle(zero, tab.dim.x, tab.dim.y), - [rx, ry, rz, rw] = tab.rot, + [ rx, ry, rz, rw ] = tab.rot, m4 = new THREE.Matrix4().makeRotationFromQuaternion( new THREE.Quaternion(rx, ry, rz, rw) ); poly.points = poly.points - .map(p => new THREE.Vector3(p.x, p.y, p.z).applyMatrix4(m4)) + .map(p => new THREE.Vector3(p.x,p.y,p.z).applyMatrix4(m4)) .map(v => newPoint(v.x, v.y, v.z)); poly.move(point); tab.poly = poly; @@ -141,7 +153,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { } if (zMin >= bounds.max.z) { - return error(`invalid z bottom ${(zMin / units).round(3)} >= bounds z max ${(zMax / units).round(3)}`); + return error(`invalid z bottom ${(zMin/units).round(3)} >= bounds z max ${(zMax/units).round(3)}`); } let mark = Date.now(); @@ -180,12 +192,12 @@ export async function cam_slice(settings, widget, onupdate, ondone) { let tracker = setSliceTracker({ rotation: 0 }); function updateSlicer() { - slicer = state.slicer = new cam_slicer(widget); + slicer = state.slicer = new kiri.cam_slicer(widget); } async function computeShadows() { shadows = {}; - await new OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => { + await new CAM.OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => { // console.log('reshadow', progress.round(3)); }); } @@ -210,7 +222,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { minZabove = zkey; } } - let shadow = computeShadowAt(widget, z, minZabove); + let shadow = CAM.shadowAt(widget, z, minZabove); if (minZabove) { // const merge = shadow.length; // const plus = shadows[minZabove].length; @@ -246,7 +258,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { function addSlices(slices, addIndexing = isIndexed) { if (!Array.isArray(slices)) { - slices = [slices]; + slices = [ slices ]; } sliceAll.appendAll(slices); if (addIndexing && axisIndex !== undefined) { @@ -258,7 +270,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { } if (false) { - opList.push(new OPS.xray(state, { type: "xray" })); + opList.push(new CAM.OPS.xray(state, { type: "xray" })); } let activeOps = proc.ops.filter(op => !op.disabled); @@ -266,9 +278,9 @@ export async function cam_slice(settings, widget, onupdate, ondone) { // silently preface op list with OpShadow if (isIndexed) { if (activeOps.length === 0 || activeOps[0].type !== 'index') - opList.push(new OPS.index(state, { type: "index", index: 0 })); + opList.push(new CAM.OPS.index(state, { type: "index", index: 0 })); } else { - opList.push(new OPS.shadow(state, { type: "shadow", silent: true })); + opList.push(new CAM.OPS.shadow(state, { type: "shadow", silent: true })); } // determing # of steps and step weighting for progress bar @@ -333,7 +345,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { if (tabs) { state.terrain.forEach(slab => { tabs.forEach(tab => { - if (tab.pos.z + tab.dim.z / 2 >= slab.z) { + if (tab.pos.z + tab.dim.z/2 >= slab.z) { let all = [...slab.tops, tab.poly]; slab.tops = POLY.union(all, 0, true); // slab.slice.output() @@ -348,7 +360,7 @@ export async function cam_slice(settings, widget, onupdate, ondone) { let tabpoly = tabs ? tabs.map(tab => tab.poly) : []; let allpoly = POLY.union([...state.shadow.base, ...tabpoly], 0, true); let shadowOff = maxToolDiam < 0 ? allpoly : - POLY.offset(allpoly, [minToolDiam / 2, maxToolDiam / 2], { count: 2, flat: true, minArea: 0 }); + POLY.offset(allpoly, [minToolDiam/2,maxToolDiam/2], { count: 2, flat: true, minArea: 0 }); state.terrain.forEach(level => level.tops.appendAll(shadowOff)); widget.terrain = state.skipTerrain ? null : state.terrain; @@ -358,9 +370,9 @@ export async function cam_slice(settings, widget, onupdate, ondone) { ondone(); }; -export function addDogbones(poly, dist, reverse) { +CAM.addDogbones = function(poly, dist, reverse) { if (Array.isArray(poly)) { - return poly.forEach(p => addDogbones(p, dist)); + return poly.forEach(p => CAM.addDogbones(p, dist)); } let open = poly.open; let isCW = poly.isClockwise(); @@ -369,8 +381,8 @@ export function addDogbones(poly, dist, reverse) { let lastpt = oldpts[oldpts.length - 1]; let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); let length = oldpts.length + (open ? 0 : 1); - let newpts = []; - for (let i = 0; i < length; i++) { + let newpts = [ ]; + for (let i=0; i (v * 10000) | 0).join(','); let point = pcache[key]; if (!point) { @@ -420,7 +432,7 @@ export async function traces(settings, widget, single) { } points[j++ % 2] = point; if (j % 2 === 0) { - let [p0, p1] = points; + let [ p0, p1 ] = points; let line = lines[k++] = newLine(p0, p1); p0.lines.push(line); p1.lines.push(line); @@ -513,10 +525,10 @@ export async function traces(settings, widget, single) { * @param {Function} onProgress - callback function to report progress * @returns {Array} list of hole centers as objects with `x`, `y`, `z`, `depth`, and `selected` properties. */ -export async function holes(settings, widget, individual, rec, onProgress) { +CAM.holes = async function(settings, widget, individual, rec,onProgress) { - let { tool, mark, precision } = rec //TODO: display some visual difference if mark is selected - let toolDiam = new Tool(settings, tool).fluteDiameter() + let {tool,mark,precision} = rec //TODO: display some visual difference if mark is selected + let toolDiam = new CAM.Tool(settings,tool).fluteDiameter() let diam = individual ? 1 : toolDiam; // sets default diameter when select individual used let proc = settings.process, @@ -535,51 +547,51 @@ export async function holes(settings, widget, individual, rec, onProgress) { zBottom = isIndexed ? camZBottom : camZBottom - zbOff; - let slicerOpts = { flatoff: 0.001 } - let slicer = new cam_slicer(widget, slicerOpts); - let zFlats = Object.keys(slicer.zFlat).map(Number).map(z => [z, z - 0.002]).flat() + let slicerOpts = {flatoff: 0.001} + let slicer = new kiri.cam_slicer(widget,slicerOpts); + let zFlats = Object.keys(slicer.zFlat).map(Number).map(z=>[z,z-0.002]).flat() - precision = Math.max(0, precision) + precision = Math.max( 0, precision ) let intervals = (precision == 0) ? [] : slicer.interval( precision, - { + { fit: false, off: -0.01, flats: true } ) - let zees = [...zFlats, ...intervals] + let zees = [...zFlats,...intervals] let indices = [...new Set(zees .map(kv => parseFloat(kv).round(5)) .filter(z => z !== null) )] let centerDiff = diam * 0.1, - area = (diam / 2) * (diam / 2) * Math.PI, + area = (diam/2) * (diam/2) * Math.PI, circles = [], slices = []; function onEach(slice) { slices.push(slice); } - let opts = { each: onEach, progress: (num, total) => onProgress(num / total * 0.5, "slice") }; + let opts = { each: onEach, progress: (num,total)=> onProgress(num/total*0.5,"slice") }; await slicer.slice(indices, opts); let shadowedDrills = false; // console.log("slices",slices) - for (let [i, slice] of slices.entries()) { - for (let top of slice.tops) { + for (let [i,slice] of slices.entries()) { + for(let top of slice.tops){ // console.log("slicing",slice.z,top) - slice.shadow = computeShadowAt(widget, slice.z, 0) + slice.shadow = CAM.shadowAt(widget, slice.z, 0) let inner = top.inner; if (!inner) { //no holes continue; } for (let poly of inner) { - if (poly.points.length < 7) continue; + if ( poly.points.length < 7 ) continue; let center = poly.calcCircleCenter(); center.area = poly.area(); center.overlapping = [center] center.depth = 0; // console.log("center",center) - if (poly.circularity() < 0.985) { + if ( poly.circularity() < 0.985 ){ // if not circular, don't add to holes continue; } @@ -589,11 +601,11 @@ export async function holes(settings, widget, individual, rec, onProgress) { continue; } let overlap = false; - for (let [i, circle] of circles.entries()) { + for (let [i,circle] of circles.entries()) { let dist = circle.distTo2D(center) // //if on the same xy point, - if (dist <= centerDiff) { + if (dist <= centerDiff ) { // console.log("overlap",center,circle); circle.overlapping.push(center); // if overlapping, don't add and continue @@ -604,41 +616,41 @@ export async function holes(settings, widget, individual, rec, onProgress) { if (!overlap) circles.push(center); } } - onProgress(0.5 + (i / slices.length * 0.25), "recognize circles") + onProgress(0.5+(i/slices.length*0.25),"recognize circles") } let drills = [] - for (let [i, c] of circles.entries()) { + for (let [i,c] of circles.entries()) { let overlapping = c.overlapping let last = overlapping.shift(); while (overlapping.length) { let circ = overlapping.shift(); let aveArea = (circ.area + last.area) / 2; - let areaDelta = Math.abs(circ.area - last.area) - if (areaDelta < aveArea * 0.05) { // if area delta less than 5% of average area - //keep top circle selected - last.depth = last.z - circ.z; - } else { // if not the same area - //push and move on - drills.push(last); - last = circ; + let areaDelta = Math.abs(circ.area -last.area) + if (areaDelta < aveArea * 0.05){ // if area delta less than 5% of average area + //keep top circle selected + last.depth = last.z - circ.z; + }else{ // if not the same area + //push and move on + drills.push(last); + last = circ; } } if (last.depth != 0) drills.push(last) //add last circle - onProgress(0.75 + (i / circles.length * 0.25), "assemble holes") + onProgress(0.75+(i/circles.length*0.25),"assemble holes") } - drills.forEach(h => { - if (rec.fromTop) { + drills.forEach( h=>{ + if(rec.fromTop){ // set z top if selected h.depth += wztop - h.z h.z = wztop } delete h.overlapping //for encoding h.diam = toolDiam // for mesh generation - h.selected = (!individual && Math.abs(h.area - area) <= area * 0.05); //for same size selection - }) + h.selected = (!individual && Math.abs(h.area - area) <= area * 0.05 ); //for same size selection + }) // console.log("unfiltered circles",circles) // console.log("drills",drills) @@ -649,13 +661,13 @@ export async function holes(settings, widget, individual, rec, onProgress) { } function cutTabs(tabs, offset, z, inter) { - tabs = tabs.filter(tab => z < tab.pos.z + tab.dim.z / 2).map(tab => tab.off).flat(); + tabs = tabs.filter(tab => z < tab.pos.z + tab.dim.z/2).map(tab => tab.off).flat(); return cutPolys(tabs, offset, z, false); } function cutPolys(polys, offset, z, inter) { let noff = []; - offset.forEach(op => noff.appendAll(op.cut(POLY.union(polys, 0, true), inter))); + offset.forEach(op => noff.appendAll( op.cut(POLY.union(polys, 0, true), inter) )); return healPolys(noff); } @@ -667,7 +679,7 @@ function contourPolys(widget, polys) { raycaster.ray.origin.set(point.x, point.y, 10000); const intersects = raycaster.intersectObject(widget.mesh, false); const firstHit = intersects[0] || null; - if (firstHit) point.z = firstHit.point.z; + point.z = firstHit.point.z; } } } @@ -682,12 +694,12 @@ function healPolys(noff) { if (noff.length > 1) { let heal = 0; // heal/rejoin open segments that share endpoints - outer: for (; ; heal++) { + outer: for(;; heal++) { let ntmp = noff, tlen = ntmp.length; - for (let i = 0; i < tlen; i++) { + for (let i=0; i z (opt cap < ztop) into polygon(s) -export function computeShadowAt(widget, z, ztop) { - const geo = widget.cache.geo; - const length = geo.length; - // cache faces with normals up - if (!widget.cache.shadow) { - const faces = []; - for (let i = 0, ip = 0; i < length; i += 3) { - const a = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); - const b = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); - const c = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); - const n = THREE.computeFaceNormal(a, b, c); - if (n.z > 0.001) { - faces.push(a, b, c); - // faces.push(newPoint(...a), newPoint(...b), newPoint(...c)); - } - } - widget.cache.shadow = faces; - } - const found = []; - const faces = widget.cache.shadow; - const { checkOverUnderOn, intersectPoints } = cam_slicer; - for (let i = 0; i < faces.length;) { - const a = faces[i++]; - const b = faces[i++]; - const c = faces[i++]; - let where = undefined; - if (ztop && a.z > ztop && b.z > ztop && c.z > ztop) { - // skip faces over top threshold - continue; - } - if (a.z < z && b.z < z && c.z < z) { - // skip faces under threshold - continue; - } else if (a.z > z && b.z > z && c.z > z) { - found.push([a, b, c]); - } else { - // check faces straddling threshold - const where = { under: [], over: [], on: [] }; - checkOverUnderOn(newPoint(a.x, a.y, a.z), z, where); - checkOverUnderOn(newPoint(b.x, b.y, b.z), z, where); - checkOverUnderOn(newPoint(c.x, c.y, c.z), z, where); - if (where.on.length === 0 && (where.over.length === 2 || where.under.length === 2)) { - // compute two point intersections and construct line - let line = intersectPoints(where.over, where.under, z); - if (line.length === 2) { - if (where.over.length === 2) { - found.push([where.over[1], line[0], line[1]]); - found.push([where.over[0], where.over[1], line[0]]); - } else { - found.push([where.over[0], line[0], line[1]]); - } - } else { - console.log({ msg: "invalid ips", line: line, where: where }); - } - } - } - } - - let polys = found.map(a => { - return newPolygon() - .add(a[0].x, a[0].y, a[0].z) - .add(a[1].x, a[1].y, a[1].z) - .add(a[2].x, a[2].y, a[2].z); - }); - - polys = POLY.union(polys, 0.001, true); - - return polys; -} +}); diff --git a/src2/kiri-mode/cam/slicer.js b/src.old/kiri-mode/cam/slicer.js similarity index 88% rename from src2/kiri-mode/cam/slicer.js rename to src.old/kiri-mode/cam/slicer.js index 2a08bb42..ab4b4317 100644 --- a/src2/kiri-mode/cam/slicer.js +++ b/src.old/kiri-mode/cam/slicer.js @@ -1,32 +1,34 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base, util } from '../../geo/base.js'; -import { broker } from '../../moto/broker.js'; -import { decode, decodePointArray, encode, encodePointArray } from '../../kiri/codec.js'; -import { newLine, newOrderedLine } from '../../geo/line.js'; -import { newPoint } from '../../geo/point.js'; -import { newSlice } from '../../kiri/slice.js'; -import { polygons as POLY } from '../../geo/polygons.js'; -import { slicer } from '../../geo/slicer.js'; +"use strict"; -const { sliceConnect, sliceDedup } = slicer; -const { config } = base; +/** + * Slicing engine used by CAM + */ +// dep: kiri.slice +// dep: moto.broker +gapp.register("kiri-mode.cam.slicer", [], (root, exports) => { + +const { base, kiri, moto } = root; +const { config, util, polygons, newOrderedLine, newPoint, newLine } = base; +const { sliceConnect, sliceDedup } = base; +const { newSlice } = kiri; + +const POLY = polygons; const timing = false; -const begin = function () { +const begin = function() { if (timing) console.time(...arguments); }; -const end = function () { +const end = function() { if (timing) console.timeEnd(...arguments); }; -export class Slicer { +class Slicer { constructor(widget, options = { zlist: true, zline: true, lines: false }) { - this.minions = self.kiri_worker.minions; this.options = {}; this.setOptions(options); - if (widget) { this.setPoints(widget.getGeoVertices({ unroll: true, translate: true })); this.computeFeatures(); @@ -74,15 +76,15 @@ export class Slicer { zList[z] = (zList[z] || 0) + 1; } - let p1 = newPoint(0, 0, 0), - p2 = newPoint(0, 0, 0), - p3 = newPoint(0, 0, 0); + let p1 = newPoint(0,0,0), + p2 = newPoint(0,0,0), + p3 = newPoint(0,0,0); // for (let i = 0, il = points.length; i < il; i++) { // points[i] = points[i].round(5); // } - for (let i = 0, il = points.length; i < il;) { + for (let i = 0, il = points.length; i < il; ) { p1.set(points[i++], points[i++], points[i++]); p2.set(points[i++], points[i++], points[i++]); p3.set(points[i++], points[i++], points[i++]); @@ -100,7 +102,7 @@ export class Slicer { if (p1.z === p2.z && p2.z === p3.z) { // detect zFlat faces to avoid slicing directly on them let zkey = p1.z.toFixed(5), - area = Math.abs(util.area2(p1, p2, p3)) / 2; + area = Math.abs(util.area2(p1,p2,p3)) / 2; if (!zFlat[zkey]) { zFlat[zkey] = area; } else { @@ -155,7 +157,7 @@ export class Slicer { // sort Z and offset when on a flat const flatoff = util.numOrDefault(opt.flatoff, 0.01); - zs = zs.sort((a, b) => a - b).map(z => { + zs = zs.sort((a,b) => a-b).map(z => { if (!flatoff) { return z; } @@ -183,13 +185,13 @@ export class Slicer { b += step; } - let p1 = newPoint(0, 0, 0), - p2 = newPoint(0, 0, 0), - p3 = newPoint(0, 0, 0), + let p1 = newPoint(0,0,0), + p2 = newPoint(0,0,0), + p3 = newPoint(0,0,0), ep = 0.001; begin("create buckets"); - for (let i = 0, il = points.length; i < il;) { + for (let i = 0, il = points.length; i < il; ) { p1.set(points[i++], points[i++], points[i++]); p2.set(points[i++], points[i++], points[i++]); p3.set(points[i++], points[i++], points[i++]); @@ -217,7 +219,7 @@ export class Slicer { // console.log({ zs, zlen, count, step, buckets, flatoff }); begin("slicing"); - const { minions } = this; + const { minions } = kiri; const threaded = minions && minions.running; const sliceFn = (threaded ? this.sliceBucketMinion : this.sliceBucket).bind(this); const track = { count: 0, total: zs.length }; @@ -236,7 +238,7 @@ export class Slicer { } const data = (await Promise.all(promises)).flat(); - data.sort((a, b) => b.z - a.z).forEach((rec, i) => { + data.sort((a,b) => b.z - a.z).forEach((rec, i) => { rec.tops = rec.polys; rec.slice = newSlice(rec.z).addTops(rec.tops); if (opt.each) { @@ -251,8 +253,9 @@ export class Slicer { } async sliceBucketMinion(bucket, opt, oneach) { + const { decode, decodePointArray } = kiri.codec; const slices = (await new Promise(resolve => { - this.minions.queue({ + kiri.minions.queue({ cmd: "cam_slice", opt: Object.clone(opt), bucket, @@ -297,16 +300,16 @@ export class Slicer { // const { points } = this, const points = indices, - p1 = newPoint(0, 0, 0), - p2 = newPoint(0, 0, 0), - p3 = newPoint(0, 0, 0); + p1 = newPoint(0,0,0), + p2 = newPoint(0,0,0), + p3 = newPoint(0,0,0); - for (let index = 0; index < points.length;) { + for (let index = 0; index < points.length; ) { p1.set(points[index++], points[index++], points[index++]); p2.set(points[index++], points[index++], points[index++]); p3.set(points[index++], points[index++], points[index++]); - let where = { under: [], over: [], on: [] }; + let where = {under: [], over: [], on: []}; checkOverUnderOn(p1, z, where); checkOverUnderOn(p2, z, where); checkOverUnderOn(p3, z, where); @@ -334,7 +337,7 @@ export class Slicer { if (line.length === 2) { lines.push(makeZLine(phash, line[0], line[1])); } else { - console.log({ msg: "invalid ips", line: line, where: where }); + console.log({msg: "invalid ips", line: line, where: where}); } } } @@ -344,8 +347,7 @@ export class Slicer { lines = sliceDedup(lines, debug); } - return lines.length ? { - z, + return lines.length ? { z, lines: opt.lines !== false ? lines : undefined, polys: links ? POLY.nest(sliceConnect(lines, opt, debug)) : undefined } : null; @@ -368,12 +370,12 @@ export class Slicer { step = (zmax - zmin) / count; } if (opt.down) { - for (let i = 0; i < count; i++) { + for (let i=0; i { - return opt.down ? b - a : a - b; + array.appendAll(add).sort((a,b) => { + return opt.down ? b-a : a-b; }); } // filter duplicate values - array = array.map(v => v.round(5)).filter((e, i, a) => i < 1 || a[i - 1] !== a[i]); + array = array.map(v => v.round(5)).filter((e,i,a) => i < 1 || a[i-1] !== a[i]); // return array.map(v => Math.abs(parseFloat(v.toFixed(5)))); return array.map(v => parseFloat(v.toFixed(5))); @@ -472,7 +474,7 @@ function getCachedPoint(phash, p) { function makeZLine(phash, p1, p2, coplanar, edge) { p1 = getCachedPoint(phash, p1.clone()); p2 = getCachedPoint(phash, p2.clone()); - let line = newOrderedLine(p1, p2); + let line = newOrderedLine(p1,p2); line.coplanar = coplanar || false; line.edge = edge || false; return line; @@ -481,7 +483,9 @@ function makeZLine(phash, p1, p2, coplanar, edge) { Slicer.checkOverUnderOn = checkOverUnderOn; Slicer.intersectPoints = intersectPoints; -broker.subscribe("minion.started", msg => { +kiri.cam_slicer = Slicer; + +moto.broker.subscribe("minion.started", msg => { const { funcs, cache, reply, log } = msg; funcs.cam_slice_init = () => { @@ -494,6 +498,7 @@ broker.subscribe("minion.started", msg => { funcs.cam_slice = (data, seq) => { const { bucket, opt } = data; + const { encode, encodePointArray } = kiri.codec; // log({ slice: bucket, opt }); cache.slicer.sliceBucket(bucket, opt, slice => { // console.log({ slice }); @@ -509,3 +514,5 @@ broker.subscribe("minion.started", msg => { }); }; }); + +}); diff --git a/src2/kiri-mode/cam/slicer2.js b/src.old/kiri-mode/cam/slicer2.js similarity index 96% rename from src2/kiri-mode/cam/slicer2.js rename to src.old/kiri-mode/cam/slicer2.js index 5fb5c4a4..89452dac 100644 --- a/src2/kiri-mode/cam/slicer2.js +++ b/src.old/kiri-mode/cam/slicer2.js @@ -1,12 +1,16 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base } from '../../geo/base.js'; -import { newPoint } from '../../geo/point.js'; -import { newOrderedLine } from '../../geo/line.js'; +"use strict"; -const { config } = base; +/** + * Slicing engine used by CAM Topo + */ +gapp.register("kiri-mode.cam.slicer2", [], (root, exports) => { -export class Slicer { +const { base, kiri } = root; +const { config, newOrderedLine, newPoint } = base; + +class Slicer { static intersectPoints = intersectPoints; static checkOverUnderOn = checkOverUnderOn; @@ -279,7 +283,7 @@ function removeDuplicateLines(lines, debug) { // find new endpoints that are not shared point let p1 = l1.p1 != point ? l1.p1 : l1.p2, p2 = l2.p1 != point ? l2.p1 : l2.p2, - newline = newOrderedLine(p1,p2); + newline = base.newOrderedLine(p1,p2); // remove deleted lines from associated points p1.group.remove(l1); p1.group.remove(l2); @@ -316,3 +320,7 @@ function removeDuplicateLines(lines, debug) { return output; } + +kiri.topo_slicer = Slicer; + +}); diff --git a/src2/kiri-mode/cam/tool.js b/src.old/kiri-mode/cam/tool.js similarity index 92% rename from src2/kiri-mode/cam/tool.js rename to src.old/kiri-mode/cam/tool.js index b4aab164..a7a8d9fa 100644 --- a/src2/kiri-mode/cam/tool.js +++ b/src.old/kiri-mode/cam/tool.js @@ -1,5 +1,14 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +"use strict"; + +// dep: kiri-mode.cam.driver +gapp.register("kiri-mode.cam.tools", [], (root, exports) => { + +const { kiri } = root; +const { driver } = kiri; +const { CAM } = driver; + const HPI = Math.PI/2; const RAD2DEG = 180/Math.PI; @@ -68,11 +77,11 @@ class Tool { setTaperLengthFromAngle(angle) { const rad = (this.flute_diam - this.taper_tip) / 2; - return this.flute_len = calcTaperLength(rad, angle); + return this.flute_len = CAM.calcTaperLength(rad, angle); } getTaperAngle() { - return calcTaperAngle((this.flute_diam - this.taper_tip) / 2, this.flute_len); + return CAM.calcTaperAngle((this.flute_diam - this.taper_tip) / 2, this.flute_len); } shaftLength() { @@ -188,21 +197,18 @@ class Tool { } } -function calcTaperAngle(rad, len) { +CAM.Tool = Tool; + +CAM.calcTaperAngle = function(rad, len) { return (Math.atan(rad / len) * RAD2DEG); }; -function calcTaperLength(rad, angle) { +CAM.calcTaperLength = function(rad, angle) { return (rad / Math.tan(angle)); }; -function getToolDiameter(settings, id) { - return new Tool(settings, id).fluteDiameter(); +CAM.getToolDiameter = function(settings, id) { + return new CAM.Tool(settings, id).fluteDiameter(); }; -export { - Tool, - calcTaperAngle, - calcTaperLength, - getToolDiameter -}; +}); diff --git a/src.old/kiri-mode/cam/tools.js b/src.old/kiri-mode/cam/tools.js new file mode 100644 index 00000000..7a7ba8c9 --- /dev/null +++ b/src.old/kiri-mode/cam/tools.js @@ -0,0 +1,353 @@ +/** Copyright Stewart Allen -- 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(' ')}>`); + }); + 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 = ''; + 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(); + } + +}); \ No newline at end of file diff --git a/src/kiri-mode/cam/topo.js b/src.old/kiri-mode/cam/topo.js similarity index 100% rename from src/kiri-mode/cam/topo.js rename to src.old/kiri-mode/cam/topo.js diff --git a/src2/kiri-mode/cam/topo4.js b/src.old/kiri-mode/cam/topo4.js similarity index 95% rename from src2/kiri-mode/cam/topo4.js rename to src.old/kiri-mode/cam/topo4.js index fc4807b0..a0df0e50 100644 --- a/src2/kiri-mode/cam/topo4.js +++ b/src.old/kiri-mode/cam/topo4.js @@ -1,14 +1,17 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base } from '../geo/base.js'; -import { line } from '../geo/line.js'; -import { point } from '../geo/point.js'; -import { polygon } from '../geo/polygon.js'; -import { polygons } from '../geo/polygons.js'; -import { broker } from '../moto/broker.js'; -import { slice } from '../kiri/slice.js'; -import { driver } from '../kiri-mode/cam/driver.js'; -import { slicer2 } from '../kiri-mode/cam/slicer2.js'; +"use strict"; + +// dep: geo.base +// dep: geo.line +// dep: geo.point +// dep: geo.polygon +// dep: geo.polygons +// dep: moto.broker +// dep: kiri.slice +// dep: kiri-mode.cam.driver +// dep: kiri-mode.cam.slicer2 +gapp.register("kiri-mode.cam.topo", [], (root, exports) => { const { base, kiri, moto } = root; const { driver, newSlice } = kiri; @@ -530,5 +533,4 @@ moto.broker.subscribe("minion.started", msg => { }); - -export { scale, rotatePoints, PRO, POLY, RAD2DEG, DEG2RAD, axis, range, slices, lathe, box, i, x, z, shards, step, index, slice, output, complete, recs, points, shared, promises, tlen, slen, sinc, heights, oslices, lines, plen, rec, py0, pz0, py1, pz1, lz, si, rx, mz, ti, xo, yo, zo, ts, dz, dy, fr, rota, steps, mrot, paths, angle, count, poly, repeat, done, tangle, p, linear, vertices, stmp, topo4, Topo4 }; +}); diff --git a/src.old/kiri-mode/drag/driver.js b/src.old/kiri-mode/drag/driver.js new file mode 100644 index 00000000..50b6953e --- /dev/null +++ b/src.old/kiri-mode/drag/driver.js @@ -0,0 +1,14 @@ +/** Copyright Stewart Allen -- 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 }); + +}); + diff --git a/src2/kiri-mode/fdm/client.js b/src.old/kiri-mode/fdm/client.js similarity index 90% rename from src2/kiri-mode/fdm/client.js rename to src.old/kiri-mode/fdm/client.js index 1dccce42..6e7709db 100644 --- a/src2/kiri-mode/fdm/client.js +++ b/src.old/kiri-mode/fdm/client.js @@ -1,20 +1,25 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { $ } from '../../moto/webui.js'; -import { api } from '../../kiri/api.js'; -import { conf } from '../../kiri/conf.js'; -import { util } from '../../geo/base.js'; -import { client } from '../../kiri/client.js'; -import { addbox, delbox, getlastbox } from '../../kiri/boxes.js'; -import { Layers } from '../../kiri/layers.js'; -import { Stack } from '../../kiri/stack.js'; -import { Widget, newWidget } from '../../kiri/widget.js'; -import { space } from '../../moto/space.js'; +"use strict"; -const { Matrix4, Vector3, BufferAttribute, BufferGeometryUtils, Raycaster, Euler } = THREE; -const { VIEWS } = api.const; +// dep: add.three +// dep: geo.base +// dep: moto.space +// dep: kiri.api +// dep: kiri.lang +// dep: kiri.consts +// dep: kiri-mode.fdm.driver +gapp.register("kiri-mode.fdm.client", [], (root, exports) => { -const LANG = api.language.current; +const { Quaternion, Matrix4, Vector3, BufferAttribute, BufferGeometryUtils } = THREE; +const { Mesh, BoxGeometry, MeshPhongMaterial, Raycaster, Euler } = THREE; +const { base, kiri, moto } = root; +const { util } = base; +const { api, consts, lang } = kiri; +const { FDM } = kiri.driver; +const { VIEWS } = consts; +const { space } = moto; +const LANG = lang.current; let lastBox, lastMode, lastView, lastPillar, fromPillar, addingSupports = false, @@ -22,11 +27,12 @@ let lastBox, lastMode, lastView, lastPillar, fromPillar, nextID = Date.now(), p1, p2, iw, alert = [], + boxes = {}, func = {}; -export function init() { - const { ui } = api; - const proc_keys = Object.keys(conf.defaults.fdm.p); +FDM.init = function(kiri, api) { + const proc_keys = Object.keys(kiri.conf.defaults.fdm.p); + const { ui, uc } = api; const rangeVars = { // slice // "sliceHeight": LANG.sl_lahi_s, @@ -86,8 +92,8 @@ export function init() { function filterSynth() { api.widgets.filter((widget) => { if (widget.track.synth) { - space.world.remove(widget.mesh); - Widget.Groups.remove(widget); + kiri.space.world.remove(widget.mesh); + kiri.Widget.Groups.remove(widget); } return !widget.track.synth }); @@ -203,7 +209,7 @@ export function init() { } } alerts.push(api.show.alert("analyzing part(s)...", 1000)); - support_generate(array => { + FDM.support_generate(array => { func.sclear(); api.hide.alert(undefined,alerts); for (let rec of array) { @@ -260,7 +266,7 @@ export function init() { updateSupportVisiblity(); // synth support widget for each widget group // iow, create support meshes based on support widget annotation - for (let group of Widget.Groups.list()) { + for (let group of kiri.Widget.Groups.list()) { let merge = group.filter(w => w.sups).map(w => Object.values(w.sups)).flat(); if (!merge.length) { continue; @@ -275,14 +281,14 @@ export function init() { box.setAttribute('uv',new BufferAttribute(new Float32Array(0), 3)); } let bbg = BufferGeometryUtils.mergeGeometries(boxen); - let sw = newWidget(null, group); + let sw = kiri.newWidget(null, group); let fwp = group[0].track.pos; sw.loadGeometry(bbg); sw._move(fwp.x, fwp.y, fwp.z); api.widgets.add(sw); sw.track.synth = true; sw.track.support = true; - space.world.add(sw.mesh); + kiri.space.world.add(sw.mesh); } }); @@ -372,7 +378,6 @@ export function init() { removeWidgetSupport(lastPillar.widget, lastPillar); return; } - lastBox = getlastbox(); if (!(iw && lastBox)) return; let { point, dim } = lastBox; p1.y = Math.max(0, p1.y); @@ -513,7 +518,7 @@ export function init() { poly.addObj(rec); } - let layers = new Layers(); + let layers = new kiri.Layers(); let layer = layers.setLayer("arcq", { line: 0xff00ff, face: 0xff00ff, opacity: 1 }); layer.addPoly(poly, {thin:true}); @@ -525,7 +530,7 @@ export function init() { )); } - xpdebug = new Stack(space.world, false); + xpdebug = new kiri.Stack(space.world, false); xpdebug.addLayers(layers); xpdebug.setVisible(0,Infinity); xpdebug.show(); @@ -541,23 +546,6 @@ let xpdebug; let supsave = {}; let suptimer; -function support_generate(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); - }); -}; - function scheduleSupportSave(widget) { clearTimeout(suptimer); supsave[widget.id] = widget; @@ -568,7 +556,7 @@ function scheduleSupportSave(widget) { }, 50); } -export function restoreSupports(widgets) { +function restoreSupports(widgets) { widgets.forEach(widget => { const supports = api.widgets.annotate(widget.id).support || []; supports.forEach(pos => { @@ -645,3 +633,49 @@ function clearWidgetSupports(widget) { scheduleSupportSave(widget); return cleared; } + +function delbox(name) { + const old = boxes[name]; + if (old) { + old.groupTo.remove(old); + } +} + +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; +} + +FDM.delbox = delbox; +FDM.addbox = addbox; +FDM.restoreSupports = restoreSupports; + +}); diff --git a/src.old/kiri-mode/fdm/driver.js b/src.old/kiri-mode/fdm/driver.js new file mode 100644 index 00000000..7ab55ca9 --- /dev/null +++ b/src.old/kiri-mode/fdm/driver.js @@ -0,0 +1,85 @@ +/** Copyright Stewart Allen -- 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, + } } ))); + }; + } + +}); + +}); diff --git a/src2/kiri-mode/fdm/export.js b/src.old/kiri-mode/fdm/export.js similarity index 98% rename from src2/kiri-mode/fdm/export.js rename to src.old/kiri-mode/fdm/export.js index b82402fb..1570bb93 100644 --- a/src2/kiri-mode/fdm/export.js +++ b/src.old/kiri-mode/fdm/export.js @@ -1,14 +1,19 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base, util } from '../../geo/base.js'; -import { getRangeParameters } from './driver.js'; -import { FDM } from './driver-be.js'; -import { version } from '../../moto/license.js'; +"use strict"; -const { config } = base; +// dep: geo.base +// dep: kiri.utils +// dep: kiri-mode.fdm.driver +gapp.register("kiri-mode.fdm.export", [], (root, exports) => { + +const { base, kiri } = root; +const { util, config } = base; +const { FDM } = kiri.driver; +const { getRangeParameters } = FDM; const debug = false; -export function fdm_export(print, online, ondone, ondebug) { +FDM.export = function(print, online, ondone, ondebug) { const { widgets, settings, belty, tools, firstTool } = print; const { bounds, controller, device, process, filter, mode } = settings; const { extruders, fwRetract } = device; @@ -25,7 +30,6 @@ export function fdm_export(print, online, ondone, ondebug) { isBambu = extras.bbl, { extrudeAbs } = device, { exportThumb } = controller, - { extrudeMM, extrudePerMM } = FDM, extused = Object.keys(print.extruders).map(v => parseInt(v)), timeFactor = (device.gcodeTime || 1) * 1.5, decimals = config.gcode_decimals || 4, @@ -122,7 +126,9 @@ export function fdm_export(print, online, ondone, ondebug) { arcQ = [], minz = { x: Infinity, y: Infinity, z: Infinity }, // lenghts of each filament (by nozzle) consumed - segments = []; + segments = [], + extrudeMM = FDM.extrudeMM, + extrudePerMM = FDM.extrudePerMM; let tools_used = Object.keys(tools); for (let tool of tools_used) { @@ -286,7 +292,7 @@ export function fdm_export(print, online, ondone, ondebug) { } function preamble() { - append(`; Generated by Kiri:Moto ${version}`); + append(`; Generated by Kiri:Moto ${kiri.version}`); append(`; ${new Date().toString()}`); appendSub("; Bed left:{left} right:{right} top:{top} bottom:{bottom}"); append(`; Bed type: ${bedType}`); @@ -989,3 +995,5 @@ export function fdm_export(print, online, ondone, ondebug) { console.log('minz', minz); } }; + +}); diff --git a/src2/kiri-mode/fdm/fill.js b/src.old/kiri-mode/fdm/fill.js similarity index 92% rename from src2/kiri-mode/fdm/fill.js rename to src.old/kiri-mode/fdm/fill.js index 5e85b344..1ef29575 100644 --- a/src2/kiri-mode/fdm/fill.js +++ b/src.old/kiri-mode/fdm/fill.js @@ -1,24 +1,29 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base, util } from '../../geo/base.js'; -import { slice as gyroid_slice } from '../../geo/gyroid.js'; +"use strict"; -export const fill_fixed = { - hex: fillHexFull, - grid: fillGrid, - triangle: fillTriangle -}; +// dep: geo.base +// dep: geo.gyroid +// dep: kiri-mode.fdm.driver +gapp.register("kiri-mode.fdm.fill", [], (root, exports) => { -export const fill = { - hex: fillHexFull, - grid: fillGrid, - gyroid: fillGyroid, - triangle: fillTriangle, - linear: fillLinear, - cubic: fillCubic -}; +const { base, kiri } = root; +const { util } = base; -const DEG2RAD = Math.PI / 180; +const DEG2RAD = Math.PI / 180, + FILL = kiri.fill = { + hex: fillHexFull, + grid: fillGrid, + gyroid: fillGyroid, + triangle: fillTriangle, + linear: fillLinear, + cubic: fillCubic + }, + CACHE = kiri.fill_fixed = { + hex: fillHexFull, + grid: fillGrid, + triangle: fillTriangle + }; function fillHexFull(target) { fillHex(target, true); @@ -104,13 +109,13 @@ function fillGyroid(target) { let tile_x = span_x / tile; let tile_y = span_y / tile; let tile_z = 1 / tile; - let gen = gyroid_slice(target.zValue() * tile_z, (1 - density) * 500); + let gyroid = base.gyroid.slice(target.zValue() * tile_z, (1 - density) * 500); let polys = []; - if (gen.dir == 'lr') { + if (gyroid.dir == 'lr') { for (let ty=0; ty<=tile_y; ty++) { for (let tx=0; tx<=tile_x; tx++) { - for (let poly of gen.polys) { + for (let poly of gyroid.polys) { target.newline(); let points = poly.map(el => { return { @@ -127,7 +132,7 @@ function fillGyroid(target) { } else { for (let tx=0; tx<=tile_x; tx++) { for (let ty=0; ty<=tile_y; ty++) { - for (let poly of gen.polys) { + for (let poly of gyroid.polys) { target.newline(); let points = poly.map(el => { return { @@ -318,3 +323,5 @@ function fillTriangle(target) { target.emit(xp, bounds.max.y); } } + +}); diff --git a/src2/kiri-mode/fdm/post.js b/src.old/kiri-mode/fdm/post.js similarity index 94% rename from src2/kiri-mode/fdm/post.js rename to src.old/kiri-mode/fdm/post.js index 2fe7dd85..5385f041 100644 --- a/src2/kiri-mode/fdm/post.js +++ b/src.old/kiri-mode/fdm/post.js @@ -1,18 +1,26 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base } from '../../geo/base.js'; -import { polygons as POLY, fillArea } from '../../geo/polygons.js'; -import { getRangeParameters } from './driver.js'; -import { slicer } from '../../geo/slicer.js'; +"use strict"; -slicer.slicePost.FDM = slicePost; +// dep: geo.base +// dep: geo.polygons +// dep: kiri-mode.fdm.driver +gapp.register("kiri-mode.fdm.post", [], (root, exports) => { + +const { base, kiri } = root; +const { driver } = kiri; +const { polygons } = base; +const { fillArea } = polygons; +const { FDM } = driver; + +const POLY = polygons; /** * may run in minion or worker context. do not create objects * that will not quickly encode in threaded mode. add to existing * data object. return is ignored. */ -export function slicePost(data, options) { +base.slicePost.FDM = function(data, options) { const { z, lines, groups } = data; const { useAssembly, post_args, zIndexes } = options; const { process, isSynth, vaseMode } = post_args; @@ -32,7 +40,7 @@ export function slicePost(data, options) { return; } const index = zIndexes.indexOf(z); - const range = getRangeParameters(process, index); + const range = FDM.getRangeParameters(process, index); // calculate fractional shells let shellFrac = (range.sliceShells - (range.sliceShells | 0)); let sliceShells = range.sliceShells | 0; @@ -54,7 +62,7 @@ export function slicePost(data, options) { const nutops = []; // co-locate shell processing with top generation in slicer for (let top of data.tops) { - nutops.push(doTopShells(z, top, count, offset/2, offset, fillOff, { + nutops.push(FDM.doTopShells(z, top, count, offset/2, offset, fillOff, { thinType, vaseMode, useAssembly @@ -185,7 +193,7 @@ function thin_type_3(params) { * may run in minion or worker context. performs shell offsetting * including thin wall detection when enabled */ -export function doTopShells(z, top, count, offset1, offsetN, fillOffset, opt = {}) { +FDM.doTopShells = function(z, top, count, offset1, offsetN, fillOffset, opt = {}) { // pretend we're a top object in minions if (!top.poly) { top = { poly: top }; @@ -311,3 +319,5 @@ function cullIntersections() { } return good.length > 2 ? good : []; } + +}); diff --git a/src2/kiri-mode/fdm/prepare.js b/src.old/kiri-mode/fdm/prepare.js similarity index 98% rename from src2/kiri-mode/fdm/prepare.js rename to src.old/kiri-mode/fdm/prepare.js index f610688d..42ffa6cd 100644 --- a/src2/kiri-mode/fdm/prepare.js +++ b/src.old/kiri-mode/fdm/prepare.js @@ -1,18 +1,28 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base, util } from '../../geo/base.js'; -import { poly2polyEmit, tip2tipEmit } from '../../geo/paths.js'; -import { newBounds } from '../../geo/bounds.js'; -import { newPoint } from '../../geo/point.js'; -import { newPolygon, Polygon } from '../../geo/polygon.js'; -import { polygons as POLY, fillArea } from '../../geo/polygons.js'; -import { newPrint } from '../../kiri/print.js'; -import { newSlice } from '../../kiri/slice.js'; -import { newWidget } from '../../kiri/widget.js'; -import { render } from '../../kiri/render.js'; -import { getRangeParameters } from './driver.js'; +"use strict"; +// dep: geo.base +// dep: geo.paths +// dep: geo.point +// dep: geo.polygons +// dep: kiri.utils +// dep: kiri.slice +// dep: kiri.consts +// dep: kiri.render +// dep: kiri-mode.fdm.driver +gapp.register("kiri-mode.fdm.prepare", [], (root, exports) => { + +const { base, kiri } = root; +const { consts, driver, newSlice, render } = kiri; +const { polygons, paths, util, newPoint, newPolygon, Polygon } = base; +const { poly2polyEmit, tip2tipEmit } = paths; const { numOrDefault } = util; +const { fillArea } = polygons; +const { beltfact } = consts; +const { FDM } = driver; +const { getRangeParameters } = FDM; +const POLY = polygons; /** * DRIVER PRINT CONTRACT @@ -20,7 +30,7 @@ const { numOrDefault } = util; * @param {Function} update progress callback * @returns {Object[]} returns array of render objects */ -export async function fdm_prepare(widgets, settings, update) { +FDM.prepare = async function(widgets, settings, update) { // filter ignored widgets widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); @@ -30,7 +40,7 @@ export async function fdm_prepare(widgets, settings, update) { { bedWidth, bedDepth } = device, { lineType } = controller, printPoint = newPoint(0,0,0), - print = self.kiri_worker.current.print = newPrint(settings, widgets), + print = self.worker.print = kiri.newPrint(settings, widgets), nozzle = process.sliceLineWidth || device.extruders[0].extNozzle, isBelt = device.bedBelt, isThin = lineType === "line", @@ -203,7 +213,7 @@ export async function fdm_prepare(widgets, settings, update) { } } // recompute bounds for purge block offsets - let bbounds = newBounds(); + let bbounds = base.newBounds(); brims.forEach(brim => { bbounds.merge(brim.bounds); }); @@ -234,7 +244,7 @@ export async function fdm_prepare(widgets, settings, update) { sslices.push(sslice); } if (sslices.length) { - let swidget = newWidget(null,widget.group); + let swidget = kiri.newWidget(null,widget.group); swidget.slices = sslices; swidget.support = true; swidget.belt = widget.belt; @@ -1502,3 +1512,5 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { return startPoint.add(offset); } + +}); diff --git a/src2/kiri-mode/fdm/slice.js b/src.old/kiri-mode/fdm/slice.js similarity index 96% rename from src2/kiri-mode/fdm/slice.js rename to src.old/kiri-mode/fdm/slice.js index 66ec638e..45789e10 100644 --- a/src2/kiri-mode/fdm/slice.js +++ b/src.old/kiri-mode/fdm/slice.js @@ -1,19 +1,28 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base, util } from '../../geo/base.js'; -import { newBounds } from '../../geo/bounds.js'; -import { newPolygon } from '../../geo/polygon.js'; -import { polygons as POLY } from '../../geo/polygons.js'; -import { newPoint } from '../../geo/point.js'; -import { slice, sliceZ } from '../../geo/slicer.js'; -import { newSlice } from '../../kiri/slice.js'; -import { fill, fill_fixed } from './fill.js'; -import { getRangeParameters } from './driver.js'; -import { doTopShells } from './post.js'; +"use strict"; -const { config } = base; +// dep: geo.base +// dep: geo.slicer +// dep: geo.polygons +// dep: kiri.utils +// dep: kiri.consts +// dep: kiri-mode.fdm.driver +// dep: kiri-mode.fdm.post +// use: kiri-mode.fdm.fill +// use: ext.clip2 +// use: add.three +gapp.register("kiri-mode.fdm.slice", [], (root, exports) => { -let tracker = util.pwait, +const { base, kiri, noop } = root; +const { consts, driver, fill, fill_fixed, newSlice, utils } = kiri; +const { config, polygons, util, newPoint } = base; +const { fillArea } = polygons; +const { FDM } = driver; +const { doTopShells, getRangeParameters } = FDM; + +const POLY = polygons, + tracker = util.pwait, lopacity = 0.6, opacity = 1, fat = 1.5, @@ -35,7 +44,6 @@ let tracker = util.pwait, part: { line: 0x777777, check: 0x777777, face: 0 }, thin: { check: 0xbb8800, face: 0xbb8800, line: 0xbb8800, opacity, lopacity, fat }, }), - noop = () => {}, PROTO = Object.clone(COLOR), profile = false, profileStart = profile ? console.profile : noop, @@ -58,9 +66,9 @@ function vopt(opt) { return opt; } -export function sliceAll(settings, onupdate) { +FDM.sliceAll = function(settings, onupdate) { // future home of brim and anchor generation - let widgets = Object.values(self.kiri_worker.cache) + let widgets = Object.values(kiri.worker.cache) .filter(w => !w.meta.disabled) .sort((a,b) => { return a.slices[0].z - b.slices[0].z @@ -115,16 +123,15 @@ export function sliceAll(settings, onupdate) { * @param {Function} onupdate (called with % complete and optional message) * @param {Function} ondone (called when complete with an array of Slice objects) */ -export function fdm_slice(settings, widget, onupdate, ondone) { +FDM.slice = function(settings, widget, onupdate, ondone) { let render = settings.render !== false, - { minions } = self.kiri_worker, { process, device, controller } = settings, isBelt = device.bedBelt, isBrick = controller.devel && process.sliceZInterleave, isSynth = widget.track.synth, isSupport = widget.track.support, useAssembly = controller.assembly, - isConcurrent = controller.threaded && minions.concurrent, + isConcurrent = controller.threaded && kiri.minions.concurrent, topLayers = process.sliceTopLayers || 0, bottomLayers = process.sliceBottomLayers || 0, vaseMode = process.sliceFillType === 'vase' && !isSynth, @@ -219,7 +226,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { } } - slice(points, { + base.slice(points, { debug: process.xray, xray: process.xray, zMin: bounds.min.z, @@ -322,7 +329,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { // slicer function (worker local or minion distributed) slicer(z, points, opts) { // opts.debug = opts.debug || isSynth; - return (isConcurrent ? minions.sliceZ : sliceZ)(z, points, opts); + return (isConcurrent ? kiri.minions.sliceZ : base.sliceZ)(z, points, opts); }, onupdate(update) { return onupdate(0.0 + update * 0.5) @@ -373,7 +380,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { .map(w => w.slices).flat() .map(s => s.tops).flat().map(t => t.simple); let shadow = isConcurrent ? - await minions.union(alltops, 0.1) : + await kiri.minions.union(alltops, 0.1) : POLY.union(alltops, 0.1, true); // expand shadow when requested (support clipping) if (process.sliceSupportExtra) { @@ -577,7 +584,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { addto.belt.touch = false; let z = addto.z; let y = (slope * z) - smin - (lineWidth / 2); - let splat = newPolygon().add(minx, y, z).add(maxx, y, z).setOpen(); + let splat = base.newPolygon().add(minx, y, z).add(maxx, y, z).setOpen(); let snew = addto.addTop(splat).fill_sparse = [ splat ]; adds.push(snew); start = addto; @@ -610,7 +617,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { let fz = first.z; let n2 = lineWidth / 2; for (let x = mp - dx; x <= mp + dx ; x += 3) { - add.push(newPolygon().add(x, fy - n2, fz).add(x, fy + y + n2, fz).setOpen() ); + add.push( base.newPolygon().add(x, fy - n2, fz).add(x, fy + y + n2, fz).setOpen() ); } } } @@ -704,7 +711,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { let z = slice.z; let y = z - smin - (lineWidth / 2); for (let line of lines) { - supps.push(newPolygon() + supps.push(base.newPolygon() .add(minX + line.start / 2, y, z) .add(minX + line.end / 2, y, z) .setOpen() @@ -932,7 +939,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { } if (isBelt) { - let bounds = newBounds(); + let bounds = base.newBounds(); let slice = slices[0]; if (slice.tops) { for (let top of slice.tops) { @@ -1074,6 +1081,15 @@ function doRender(slice, isSynth, params, opt = {}) { // console.log(slice.index, slice.render.stats); } +// shared with SLA driver and minions +FDM.share = { + doShells, + doTopShells, + doDiff, + projectFlats, + projectBridges +}; + /** * Compute offset shell polygons. For FDM, the first offset is usually half * of the nozzle width. Each subsequent offset is a full nozzle width. User @@ -1088,7 +1104,7 @@ function doRender(slice, isSynth, params, opt = {}) { * @param {number} fillOffset * @param {Obejct} options */ -export function doShells(slice, count, offset1, offsetN, fillOffset, opt = {}) { +function doShells(slice, count, offset1, offsetN, fillOffset, opt = {}) { for (let top of slice.tops) { doTopShells(slice.z, top, count, offset1, offsetN, fillOffset, opt); } @@ -1114,7 +1130,7 @@ export function doShells(slice, count, offset1, offsetN, fillOffset, opt = {}) { slice.tops.forEach(function(top) { if (!top.fill_off) return; // missing for inner brick layers - let lines = POLY.fillArea(top.fill_off, angle, spacing, null); + let lines = fillArea(top.fill_off, angle, spacing, null); top.fill_lines.appendAll(lines); }); @@ -1274,7 +1290,7 @@ function doSparseLayerFill(slice, options = {}) { } if (options.promises) { - options.promises.push(self.kiri_worker.minions.clip(slice, polys, lines)); + options.promises.push(kiri.minions.clip(slice, polys, lines)); return; } @@ -1300,7 +1316,7 @@ function doSparseLayerFill(slice, options = {}) { * Used to calculate bridges, flats and then solid projections. * 'expand' is used for top offsets in SLA mode */ -export function doDiff(slice, options = {}) { +function doDiff(slice, options = {}) { const { sla, fakedown, grow, min } = options; if ((slice.index <= 0 && !fakedown) || slice.xray) { return; @@ -1356,7 +1372,7 @@ function addSolidFills(slice, polys) { /** * project bottom flats down */ -export function projectFlats(slice, count, expand) { +function projectFlats(slice, count, expand) { if (!slice.down || !slice.flats) return; // these flats are marked for finishing print speed if (slice.flats.length) slice.finishSolids = true; @@ -1369,7 +1385,7 @@ export function projectFlats(slice, count, expand) { /** * project top bridges up */ -export function projectBridges(slice, count) { +function projectBridges(slice, count) { if (!slice.up || !slice.bridges) return; // these flats are marked for finishing print speed if (slice.bridges.length) slice.finishSolids = true; @@ -1500,7 +1516,7 @@ function doSolidsFill(slice, spacing, angle, minArea, fillQ) { function doFillArea(fillQ, polys, angle, spacing, output, minLen, maxLen) { if (fillQ) { - fillQ.push(self.kiri_worker.minions.fill(polys, angle, spacing, output, minLen, maxLen)); + fillQ.push(kiri.minions.fill(polys, angle, spacing, output, minLen, maxLen)); } else { POLY.fillArea(polys, angle, spacing, output, minLen, maxLen); } @@ -1510,8 +1526,7 @@ function doFillArea(fillQ, polys, angle, spacing, output, minLen, maxLen) { * calculate external overhangs requiring support */ async function doSupport(slice, proc, shadow, opt = {}) { - let { minions } = self.kiri_worker, - maxBridge = proc.sliceSupportSpan || 5, + let maxBridge = proc.sliceSupportSpan || 5, minArea = proc.supportMinArea || 0.1, pillarSize = proc.sliceSupportSize, offset = proc.sliceSupportOffset || 0, @@ -1600,26 +1615,26 @@ async function doSupport(slice, proc, shadow, opt = {}) { // for each point, create a bounding rectangle points.forEach(function(point) { - pillars.push(newPolygon().centerRectangle(point, size/2, size/2)); + pillars.push(base.newPolygon().centerRectangle(point, size/2, size/2)); }); supports.appendAll(POLY.union(pillars, null, true, { wasm: false })); // merge pillars and replace with convex hull of outer points (aka smoothing) pillars = POLY.union(pillars, null, true, { wasm: false }).forEach(function(pillar) { - supports.push(newPolygon().createConvexHull(pillar.points)); + supports.push(base.newPolygon().createConvexHull(pillar.points)); }); })(); // DEBUG code if (SDBG && down_traces) slice.output() .setLayer('cks', { line: 0xee5533, check: 0xee5533 }) - .addPolys(cks.map(p => newPolygon().centerRectangle(p, 0.25, 0.25))) + .addPolys(cks.map(p => base.newPolygon().centerRectangle(p, 0.25, 0.25))) .setLayer('pip', { line: 0xdd4422, check: 0xdd4422 }) - .addPolys(pip.map(p => newPolygon().centerRectangle(p, 0.4, 0.4))) + .addPolys(pip.map(p => base.newPolygon().centerRectangle(p, 0.4, 0.4))) .setLayer('pcl', { line: 0xcc3311, check: 0xcc3311 }) - .addPolys(pcl.map(p => newPolygon().centerRectangle(p, 0.3, 0.3))) + .addPolys(pcl.map(p => base.newPolygon().centerRectangle(p, 0.3, 0.3))) .setLayer('pts', { line: 0xdd33dd, check: 0xdd33dd }) - .addPolys(points.map(p => newPolygon().centerRectangle(p, 0.8, 0.8))) + .addPolys(points.map(p => base.newPolygon().centerRectangle(p, 0.8, 0.8))) .setLayer('dtr', { line: 0x0, check: 0x0 }) .addPolys(POLY.setZ(down_traces.clone(true),slice.z)); ; @@ -1630,7 +1645,7 @@ async function doSupport(slice, proc, shadow, opt = {}) { // then union supports if (supports.length > 10) { - supports = await minions.union(supports); + supports = await kiri.minions.union(supports); } else { supports = POLY.union(supports, null, true, { wasm: false }); } @@ -1748,7 +1763,7 @@ function projectSolid(slice, polys, count, up, first) { } } -export function supports(settings, widget) { +FDM.supports = function(settings, widget) { let isBelt = settings.device.bedBelt; let process = settings.process; let size = process.sliceSupportSize; @@ -1909,7 +1924,7 @@ class Coplanars { // convert groups of faces to contiguous polygon groups groups = groups.map(group => { let parr = group.map(arr => { - return newPolygon() + return base.newPolygon() .add(arr[0].x, arr[0].y, arr[0].z) .add(arr[1].x, arr[1].y, arr[1].z) .add(arr[2].x, arr[2].y, arr[2].z); @@ -1927,3 +1942,5 @@ class Coplanars { return out; } } + +}); diff --git a/src2/kiri-mode/laser/driver.js b/src.old/kiri-mode/laser/driver.js similarity index 94% rename from src2/kiri-mode/laser/driver.js rename to src.old/kiri-mode/laser/driver.js index ace92697..ee4909d2 100644 --- a/src2/kiri-mode/laser/driver.js +++ b/src.old/kiri-mode/laser/driver.js @@ -1,25 +1,43 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base } from '../../geo/base.js'; -import { Packer } from '../../kiri/pack.js'; -import { newBounds } from '../../geo/bounds.js'; -import { newPoint } from '../../geo/point.js'; -import { newPrint } from '../../kiri/print.js'; -import { newSlopeFromAngle } from '../../geo/slope.js'; -import { polygons as POLY } from '../../geo/polygons.js'; -import { render } from '../../kiri/render.js'; -import { slicer } from '../../geo/slicer.js'; -import { newSlice } from '../../kiri/slice.js'; +"use strict"; -export const TYPE = { +// dep: geo.polygons +// dep: geo.point +// dep: kiri.slice +// use: kiri.pack +// use: kiri.render +gapp.register("kiri-mode.laser.driver", [], (root, exports) => { + +const { base, kiri } = root; +const { driver, newSlice } = kiri; +const { polygons, newPoint } = base; + +const POLY = polygons; +const TYPE = { LASER: 0, DRAG: 1, WJET: 2, WEDM: 3 }; -function init(worker) { - // console.log({ LASER_INIT: worker }); +driver.LASER = { + TYPE, + type: TYPE.LASER, + name: 'Laser', + init, + slice, + prepare, + export: exportLaser, + exportGCode, + exportSVG, + exportDXF, +}; + +function init(kiri, api, current) { + api.event.on("settings.saved", settings => { + let ui = kiri.api.ui; + }); } function polyLabel(poly, label) { @@ -110,7 +128,7 @@ function arc(center, rad, s1, s2, out) { out.push( center.projectOnSlope(s1, rad) ); } while (ticks-- > 0) { - out.push( center.projectOnSlope(newSlopeFromAngle(a1 + off), rad) ); + out.push( center.projectOnSlope(base.newSlopeFromAngle(a1 + off), rad) ); a1 += step * dir; } out.push( center.projectOnSlope(s2, rad) ); @@ -168,7 +186,7 @@ function addKnifeRadii(poly, tipoff) { * @param {Function} onupdate (called with % complete and optional message) * @param {Function} ondone (called when complete with an array of Slice objects) */ -function laser_slice(settings, widget, onupdate, ondone) { +function slice(settings, widget, onupdate, ondone) { let proc = settings.process; let offset = proc.ctSliceKerf || 0; let color = settings.controller.dark ? 0xbbbbbb : 0; @@ -182,7 +200,7 @@ function laser_slice(settings, widget, onupdate, ondone) { let points = widget.getPoints(); let indices = []; - slicer.slice(points, { + base.slice(points, { zMin: bounds.min.z, zMax: bounds.max.z, zGen(zopt) { @@ -244,14 +262,13 @@ function laser_slice(settings, widget, onupdate, ondone) { * @param {Object} print state object * @param {Function} update incremental callback */ -async function laser_prepare(widgets, settings, update) { - let worker = self.kiri_worker.current, - device = settings.device, +async function prepare(widgets, settings, update) { + let device = settings.device, process = settings.process, - print = worker.print = newPrint(settings, widgets), - isWire = worker.mode === 'WEDM', - isKnife = worker.mode === 'DRAG', - isLaser = worker.mode === 'LASER', + print = self.worker.print = kiri.newPrint(settings, widgets), + isWire = self.worker.mode === 'WEDM', + isKnife = self.worker.mode === 'DRAG', + isLaser = self.worker.mode === 'LASER', output = print.output = [], totalSlices = 0, slices = 0, @@ -374,7 +391,7 @@ async function laser_prepare(widgets, settings, update) { // compute tile bounds for (let tile of tiles) { - let bounds = newBounds(); + let bounds = base.newBounds(); tile.forEach(rec => bounds.merge(rec.poly.bounds)); tile.w = bounds.width(); tile.h = bounds.height(); @@ -382,7 +399,7 @@ async function laser_prepare(widgets, settings, update) { } // pack tiles - let tp = new Packer(dw, dh, spacing, { invy:isWire, simple: !sort }).pack(tiles, packer => { + let tp = new kiri.Pack(dw, dh, spacing, { invy:isWire, simple: !sort }).pack(tiles, packer => { return packer.rescale(1.1, 1.1); }); @@ -397,7 +414,7 @@ async function laser_prepare(widgets, settings, update) { y: fit.y - (tp.max.h / 2) - bounds.miny, z: 0}, true); } - tile.bounds = newBounds(); + tile.bounds = base.newBounds(); for (let { poly } of tile) { tile.bounds.merge(poly.bounds); } @@ -446,7 +463,7 @@ async function laser_prepare(widgets, settings, update) { } } - return render.path(output, (progress, layer) => { + return kiri.render.path(output, (progress, layer) => { if (ctOutShaper) { for (let layer of output) { for (let out of layer) { @@ -458,7 +475,7 @@ async function laser_prepare(widgets, settings, update) { }, { thin: true, z: ctOutStack ? undefined : 0, action: "cut", moves: process.isKnife }); }; -function laser_export(print, online, ondone) { +function exportLaser(print, online, ondone) { ondone(print.output); } @@ -495,7 +512,7 @@ function exportElements(settings, output, onpre, onpoly, onpost, onpoint, onlaye size.w = max.x - min.x; size.h = max.y - min.y; - let bounds = newBounds(); + let bounds = base.newBounds(); if (ctOriginCenter) { // place origin at geometric center of all points // regardless of workspace size @@ -846,15 +863,4 @@ function exportDXF(settings, data) { return lines.join('\n'); }; -export const LASER = { - TYPE, - type: TYPE.LASER, - name: 'Laser', - init, - slice: laser_slice, - prepare: laser_prepare, - export: laser_export, - exportGCode, - exportDXF, - exportSVG -}; +}); diff --git a/src.old/kiri-mode/sla/client.js b/src.old/kiri-mode/sla/client.js new file mode 100644 index 00000000..c65aa6a4 --- /dev/null +++ b/src.old/kiri-mode/sla/client.js @@ -0,0 +1,101 @@ +/** Copyright Stewart Allen -- 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 { 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(); +} + +}); diff --git a/src2/kiri-mode/sla/driver.js b/src.old/kiri-mode/sla/driver.js similarity index 71% rename from src2/kiri-mode/sla/driver.js rename to src.old/kiri-mode/sla/driver.js index c0b68880..e0bc8488 100644 --- a/src2/kiri-mode/sla/driver.js +++ b/src.old/kiri-mode/sla/driver.js @@ -1,29 +1,26 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { newPrint } from '../../kiri/print.js'; -import { sla_slice } from './slice.js'; -import { sla_export } from './export.js'; +"use strict"; -export const SLA = { - init, - legacy: false, - slice: sla_slice, - prepare: sla_prepare, - export: sla_export, +// use: kiri.print +gapp.register("kiri-mode.sla.driver", [], (root, exports) => { + +const { kiri } = root +const { driver } = kiri; + +const SLA = driver.SLA = { + prepare, + legacy: false }; if (SLA.legacy) { console.log("SLA Driver in Legacy Mode"); } -function init(worker) { - // console.log({ INIT_SLA: worker }); -} - // runs in worker. would usually be in src/mode/sla/prepare.js // but the SLA driver skips the prepare step because there is no path routing -async function sla_prepare(widgets, settings, update) { - self.kiri_worker.current.print = newPrint(settings, widgets); +async function prepare(widgets, settings, update) { + root.worker.print = kiri.newPrint(settings, widgets); if (!SLA.wasm) { fetch('/wasm/kiri-sla.wasm') .then(response => response.arrayBuffer()) @@ -47,3 +44,5 @@ async function sla_prepare(widgets, settings, update) { } update(1); } + +}); diff --git a/src2/kiri-mode/sla/export.js b/src.old/kiri-mode/sla/export.js similarity index 92% rename from src2/kiri-mode/sla/export.js rename to src.old/kiri-mode/sla/export.js index 82867ea4..da4fb277 100644 --- a/src2/kiri-mode/sla/export.js +++ b/src.old/kiri-mode/sla/export.js @@ -1,8 +1,17 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { CXDLP } from './x_cxdlp.js'; -import { photon } from './x_photon.js'; -import { SLA } from './driver.js'; +"use strict"; + +// dep: geo.base +// dep: kiri-mode.sla.driver +// dep: kiri-mode.sla.x_cxdlp +// dep: kiri-mode.sla.x_photon +gapp.register("kiri-mode.sla.export", [], (root, exports) => { + +const { base, kiri } = root +const { driver } = kiri; +const { util } = base; +const { SLA } = driver; /** * DRIVER CONTRACT - runs in worker @@ -10,7 +19,7 @@ import { SLA } from './driver.js'; * @param {Function} online streaming reply * @param {Function} ondone last reply */ -export function sla_export(print, online, ondone) { +SLA.export = function(print, online, ondone) { let widgets = print.widgets, settings = print.settings, device = settings.device, @@ -158,6 +167,8 @@ export function sla_export(print, online, ondone) { console.log('print.export', Date.now() - mark); }; -export function generateCXDLP(print, conf, progress) { +function generateCXDLP(print, conf, progress) { console.log({generateCXDLP: print, conf, progress}); } + +}); diff --git a/src2/kiri-mode/sla/slice.js b/src.old/kiri-mode/sla/slice.js similarity index 94% rename from src2/kiri-mode/sla/slice.js rename to src.old/kiri-mode/sla/slice.js index 34c34fd1..c55e0aea 100644 --- a/src2/kiri-mode/sla/slice.js +++ b/src.old/kiri-mode/sla/slice.js @@ -1,14 +1,23 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { util } from '../../geo/base.js'; -import { slicer } from '../../geo/slicer.js'; -import { newPolygon } from '../../geo/polygon.js'; -import { polygons as POLY } from '../../geo/polygons.js'; -import { newSlice, newTop } from '../../kiri/slice.js'; -import { doShells, doDiff, projectFlats, projectBridges } from '../fdm/slice.js'; -import { PNG } from '../../ext/pngjs.esm.js'; -import { SLA } from './driver.js'; +"use strict"; +// dep: geo.base +// dep: geo.point +// dep: geo.polygon +// dep: geo.polygons +// dep: kiri.slice +// dep: kiri-mode.sla.driver +// use: ext.pngjs +gapp.register("kiri-mode.sla.slice", [], (root, exports) => { + +const { base, kiri } = root; +const { driver, newSlice, newTop } = kiri; +const { util, newPoint, newPolygon, polygons } = base; +const { SLA } = driver; + +const POLY = polygons; +const FDM = driver.FDM.share; const tracker = util.pwait; let fill_cache; @@ -20,10 +29,9 @@ let fill_cache; * @param {Function} onupdate (called with % complete and optional message) * @param {Function} ondone (called when complete with an array of Slice objects) */ -export function sla_slice(settings, widget, onupdate, ondone) { - let { minions } = self.kiri_worker, - { process, device, controller } = settings, - isConcurrent = controller.threaded && minions.concurrent, +SLA.slice = function(settings, widget, onupdate, ondone) { + let { process, device, controller } = settings, + isConcurrent = controller.threaded && kiri.minions.concurrent, work_total, work_remain; @@ -61,10 +69,10 @@ export function sla_slice(settings, widget, onupdate, ondone) { break; } - let sws = self.kiri_worker.current.snap.url; + let sws = self.worker.snap.url; let b64 = atob(sws.substring(sws.indexOf(',') + 1)); let bin = Uint8Array.from(b64, c => c.charCodeAt(0)); - let img = new PNG(); + let img = new png.PNG(); img.parse(bin, (err, data) => { SLA.preview = img; SLA.previewSmall = samplePNG(img, smallDims.x, smallDims.y); @@ -149,14 +157,14 @@ export function sla_slice(settings, widget, onupdate, ondone) { work_remain = work_total; forSlices(slices, 5, (slice,index) => { if (process.slaShell) { - doShells(slice, 2, 0, process.slaShell); + FDM.doShells(slice, 2, 0, process.slaShell); } else { - doShells(slice, 1, 0); + FDM.doShells(slice, 1, 0); } }, "shells"); forSlices(slices, 10, (slice) => { if (slice.synth) return; - doDiff(slice, 0.000001, { + FDM.doDiff(slice, 0.000001, { sla: true, fakedown: !process.slaOpenBase }); @@ -164,8 +172,8 @@ export function sla_slice(settings, widget, onupdate, ondone) { if (solidLayers) { forSlices(slices, 10, (slice) => { if (slice.synth) return; - projectFlats(slice, solidLayers); - projectBridges(slice, solidLayers); + FDM.projectFlats(slice, solidLayers); + FDM.projectBridges(slice, solidLayers); }, "project"); async function doUnionSolid(slice) { if (slice.synth) return; @@ -174,7 +182,7 @@ export function sla_slice(settings, widget, onupdate, ondone) { let trims = slice.solids || []; traces.appendAll(trims); // slice.unioned = POLY.setZ(POLY.union(traces, undefined, true), slice.z); - slice.unioned = POLY.setZ(await minions.union(traces), slice.z); + slice.unioned = POLY.setZ(await kiri.minions.union(traces), slice.z); } else { slice.unioned = traces; } @@ -207,7 +215,7 @@ export function sla_slice(settings, widget, onupdate, ondone) { let bounds = widget.getBoundingBox(); let points = widget.getPoints(); - slicer.slice(points, { + base.slice(points, { indices: process.indices || process.xray, union: controller.healMesh, debug: process.xray, @@ -217,7 +225,7 @@ export function sla_slice(settings, widget, onupdate, ondone) { zInc: height, // slicer function (worker local or minion distributed) slicer(z, points, opts) { - return (isConcurrent ? minions.sliceZ : slicer.sliceZ)(z, points, opts); + return (isConcurrent ? kiri.minions.sliceZ : base.sliceZ)(z, points, opts); }, onupdate(v) { return onupdate(0.0 + v * 0.25); @@ -598,7 +606,7 @@ function fillPolys(slice, settings) { if (!cached && seq_c !== 1) for (let x=start_x; x -- All Rights Reserved */ +"use strict"; + +gapp.register("kiri-mode.sla.x_cxdlp", [], (root, exports) => { + const default_values = { magic1: 'CXSW3DV2', magic2: 'CXSW3DV2', @@ -34,7 +38,7 @@ const data_term = 0x0d0a; // CL-89 3840 x 2400 // CL-133 3840 x 2160 -export class CXDLP { +class CXDLP { constructor() { Object.assign(this, default_values); } @@ -429,3 +433,23 @@ CXDLP.render = function(params) { } return { lines, area }; } + +if (!self.navigator && self.process && self.process.env) { + let fs = require('fs'); + eval(fs.readFileSync("src/add/class.js").toString()); + let args = process.argv.slice(2); + let file = args.shift(); + // let fpos = 0; + let view = new DataView(fs.readFileSync(file).buffer); + let read = new DataReader(view, 0); + let cxdlp = new CXDLP().read(view); + console.log({ + cxdlp, + layers5: cxdlp.layers.slice(0,5), + lines1: cxdlp.get_layer_lines(1) + }); +} else if (self.navigator) { + self.CXDLP = CXDLP; +} + +}); diff --git a/src2/kiri-mode/sla/x_photon.js b/src.old/kiri-mode/sla/x_photon.js similarity index 98% rename from src2/kiri-mode/sla/x_photon.js rename to src.old/kiri-mode/sla/x_photon.js index 968bfa5c..961f54ee 100644 --- a/src2/kiri-mode/sla/x_photon.js +++ b/src.old/kiri-mode/sla/x_photon.js @@ -1,6 +1,13 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { SLA } from './driver.js'; +"use strict"; + +gapp.register("kiri-mode.sla.x_photons", [], (root, exports) => { + +const { base, kiri } = root; +const { driver } = kiri; +const { util } = base; +const { SLA } = driver; function generatePhoton(print, conf, progress) { let printset = print.settings, @@ -526,9 +533,11 @@ function polyout(poly, ctx, opt) { ctx.closePath(); } -export const photon = { +self.photon = { generatePhoton, generatePhotons, renderLayer, renderLayerWasm }; + +}); diff --git a/src.old/kiri-mode/wedm/driver.js b/src.old/kiri-mode/wedm/driver.js new file mode 100644 index 00000000..f94c7808 --- /dev/null +++ b/src.old/kiri-mode/wedm/driver.js @@ -0,0 +1,132 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +// dep: kiri-mode.laser.driver +gapp.register("kiri-mode.wedm.driver", [], (root, exports) => { + +const { kiri, moto } = root; +const DRIVERS = kiri.driver; +const { CAM, LASER } = DRIVERS; +const { TYPE } = LASER; +const WEDM = DRIVERS.WEDM = Object.assign({}, LASER, { + name: "WireEDM", + type: TYPE.WEDM, + init, + prepare +}); +const DEG2RAD = Math.PI / 180; + +const state = { + api: undefined, + alert: undefined, + radians: 0, + surfaces: [], + selecting: false +}; + +function faceAdd() { + if (state.selecting) return faceDone(); + + const { api, radians, surfaces } = state; + CAM.surface_prep(radians, () => { + api.hide.alert(alert); + state.alert = api.show.alert("[esc] cancels surface selection"); + for (let [wid, arr] of Object.entries(surfaces)) { + let widget = api.widgets.forid(wid); + if (widget && arr.length) + for (let faceid of arr) { + CAM.surface_toggle(widget, faceid, radians, faceids => { + // surfaces[widget.id] = faceids; + }); + } + } + api.feature.on_mouse_up = (obj, ev) => { + let { face } = obj; + let min = Math.min(face.a, face.b, face.c); + let faceid = min / 3; + let widget = obj.object.widget; + CAM.surface_toggle(widget, faceid, radians, faceids => { + surfaces[widget.id] = faceids; + }); + }; + state.selecting = true; + }); +} + +function faceDone() { + if (!state.selecting) return; + + const { api, alert, surfaces } = state; + for (let wid of Object.keys(surfaces)) { + let widget = api.widgets.forid(wid); + if (widget) { + CAM.surface_clear(widget); + } else { + delete surfaces[wid]; + } + } + api.hide.alert(alert); + api.feature.on_mouse_up = undefined; + api.ui.faceAdd.classList.remove('editing') + state.selecting = false; +} + +function init(kiri, api, driver) { + LASER.init(kiri, api, driver); + + state.api = api; + + api.event.on("key.esc", () => { + if (state.selecting) { + faceDone(); + } + }); + + // api.event.on("mode.set", (mode) => { }); + + // api.event.on("view.set", (mode) => { }); + + api.event.on("preview.view", view => { + const process = api.conf.get().process; + if (!process.ctOriginBounds) return; + const bounds = new THREE.Box3().expandByObject(view); + api.platform.set_bounds(new THREE.Box3( + new THREE.Vector3(bounds.min.x, bounds.min.z, bounds.min.y), + new THREE.Vector3(bounds.max.x, bounds.max.z, bounds.max.y), + )); + }); + + // Surface Selection Buttons + api.event.on("button.click", target => { + let process = api.conf.get().process; + switch (target) { + case api.ui.faceAdd: + target.classList.add('editing'); + return faceAdd(); + case api.ui.faceDun: + return faceDone(); + case api.ui.faceClr: + api.uc.confirm("clear surface selection?").then(ok => { + // todo + }); + break; + } + }); + +}; + +async function prepare(widgets, settings, update) { + // let device = settings.device, + // process = settings.process, + // print = self.worker.print = kiri.newPrint(settings, widgets), + // output = print.output = []; + + // filter ignored widgets + widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); + + LASER.prepare(widgets, settings, update); +} + +}); + diff --git a/src.old/kiri-mode/wjet/driver.js b/src.old/kiri-mode/wjet/driver.js new file mode 100644 index 00000000..8e4bf4b3 --- /dev/null +++ b/src.old/kiri-mode/wjet/driver.js @@ -0,0 +1,14 @@ +/** Copyright Stewart Allen -- 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 }); + +}); + diff --git a/src2/kiri-run/cli.js b/src.old/kiri-run/cli.js similarity index 100% rename from src2/kiri-run/cli.js rename to src.old/kiri-run/cli.js diff --git a/src2/kiri-run/engine.js b/src.old/kiri-run/engine.js similarity index 79% rename from src2/kiri-run/engine.js rename to src.old/kiri-run/engine.js index 4ee2b2a9..d0350a17 100644 --- a/src2/kiri-run/engine.js +++ b/src.old/kiri-run/engine.js @@ -2,31 +2,16 @@ "use strict"; -// Future imports for src/kiri-run/engine.js when unwrapped from gapp.register() +// dep: moto.license +// dep: load.stl +// dep: kiri.conf +// dep: kiri.client +// dep: kiri.widget +// use: add.three +// use: add.array +gapp.register("kiri-run.engine", [], (root, exports) => { -// Dependencies from dep: comments -// import { moto_license } from 'moto/license.js'; -// import { load_stl } from 'load/stl.js'; -// import { kiri_conf } from 'kiri/conf.js'; -// import { kiri_client } from 'kiri/client.js'; -// import { kiri_widget } from 'kiri/widget.js'; - -// Dependencies from use: comments -// import { add_three } from 'add/three.js'; -// import { add_array } from 'add/array.js'; - -// Core dependencies from root -import { kiri } from '../../main/gapp.js'; - -// Note: This module also depends on: -// - moto.license (for licensing functionality) -// - load.stl (for STL file loading) -// - kiri.conf (for configuration) -// - kiri.client (for client functionality) -// - kiri.widget (for widget functionality) -// - add.three (for Three.js utilities) -// - add.array (for array utilities) -// - Various kiri API functions injected via root +const { kiri } = root; class Engine { constructor() { @@ -207,16 +192,4 @@ gapp.overlay(kiri, { newEngine }); -// Future exports for src/kiri-run/engine.js when unwrapped from gapp.register() - -// Main Engine class -export { Engine }; - -// Factory function -export { newEngine }; - -// Default export - the main engine functionality -export default { - Engine, - newEngine -}; \ No newline at end of file +}); diff --git a/src2/kiri-run/frame.js b/src.old/kiri-run/frame.js similarity index 100% rename from src2/kiri-run/frame.js rename to src.old/kiri-run/frame.js diff --git a/src2/kiri-run/minion.js b/src.old/kiri-run/minion.js similarity index 82% rename from src2/kiri-run/minion.js rename to src.old/kiri-run/minion.js index f20135ad..3a7da0a1 100644 --- a/src2/kiri-run/minion.js +++ b/src.old/kiri-run/minion.js @@ -1,20 +1,26 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import "../ext/clip2.js"; -import "../ext/earcut.js"; -import '../add/array.js'; -import '../add/class.js'; -import '../add/three.js'; +"use strict"; -import { base } from '../geo/base.js'; -import { newPoint } from '../geo/point.js'; -import { polygons as POLY } from '../geo/polygons.js'; -import { sliceZ } from '../geo/slicer.js'; -import { codec } from '../kiri/codec.js'; -import { broker } from '../moto/broker.js'; -import { doTopShells } from '../kiri-mode/fdm/post.js'; -import { wasm_ctrl } from '../geo/wasm.js'; +// 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; @@ -69,7 +75,7 @@ const funcs = self.minion = { topShells: (data, seq) => { let top = codec.decode(data.top, {full: true}); let {z, count, offset1, offsetN, fillOffset, opt} = data; - doTopShells(z, top, count, offset1, offsetN, fillOffset, opt); + kiri.driver.FDM.doTopShells(z, top, count, offset1, offsetN, fillOffset, opt); let state = { zeros: [] }; reply({ seq, top: codec.encode(top, {full: true}) }, state.zeros); }, @@ -116,15 +122,14 @@ const funcs = self.minion = { }, sliceZ: (data, seq) => { - console.log('minion.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++] = newPoint(points[i++], points[i++], points[i++]).round(3); + realp[p++] = base.newPoint(points[i++], points[i++], points[i++]).round(3); } let state = { zero: [] }; let output = []; - sliceZ(z, realp, { + base.sliceZ(z, realp, { ...options, each(out) { output.push(out) } }).then(() => { @@ -154,9 +159,9 @@ const funcs = self.minion = { wasm: data => { if (data.enable) { - wasm_ctrl.enable(); + base.wasm_ctrl.enable(); } else { - wasm_ctrl.disable(); + base.wasm_ctrl.disable(); } }, @@ -165,4 +170,6 @@ const funcs = self.minion = { } }; -broker.publish("minion.started", { funcs, cache, reply, log }); +moto.broker.publish("minion.started", { funcs, cache, reply, log }); + +}); diff --git a/src2/kiri-run/worker.js b/src.old/kiri-run/worker.js similarity index 80% rename from src2/kiri-run/worker.js rename to src.old/kiri-run/worker.js index 7683588b..bcb463e4 100644 --- a/src2/kiri-run/worker.js +++ b/src.old/kiri-run/worker.js @@ -1,48 +1,51 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import '../add/array.js'; -import '../add/class.js'; -import '../add/three.js'; -import '../ext/jszip.js'; +"use strict"; -import { base } from '../geo/base.js'; -import { broker } from '../moto/broker.js'; -import { codec } from '../kiri/codec.js'; -import { util } from '../geo/base.js'; -import { newPoint } from '../geo/point.js'; -import { polygons as POLY } from '../geo/polygons.js'; -import { newPrint } from '../kiri/print.js'; -import { render } from '../kiri/render.js'; -import { wasm_ctrl } from '../geo/wasm.js'; -import { version } from '../moto/license.js'; -import { Widget, newWidget } from '../kiri/widget.js'; - -import { CAM } from '../kiri-mode/cam/driver-be.js'; -import { DRAG } from '../kiri-mode/drag/driver.js'; -import { FDM } from '../kiri-mode/fdm/driver-be.js'; -import { LASER } from '../kiri-mode/laser/driver.js'; -import { SLA } from '../kiri-mode/sla/driver.js'; -import { WEDM } from '../kiri-mode/wedm/driver.js'; -import { WJET } from '../kiri-mode/wjet/driver.js'; +// dep: geo.base +// dep: geo.polygons +// dep: geo.wasm +// dep: kiri.codec +// dep: kiri.slice +// dep: moto.license +// dep: moto.broker +// use: load.png +// use: load.gbr +// use: ext.jszip +// use: kiri.render +// use: kiri-mode.cam.animate +// use: kiri-mode.cam.animate2 +// use: kiri-mode.cam.slice +// use: kiri-mode.cam.prepare +// use: kiri-mode.cam.export +// use: kiri-mode.cam.tool +// use: kiri-mode.fdm.slice +// use: kiri-mode.fdm.prepare +// use: kiri-mode.fdm.export +// use: kiri-mode.sla.slice +// use: kiri-mode.sla.export +// use: kiri-mode.fdm.slice +// use: kiri-mode.fdm.prepare +// use: kiri-mode.fdm.export +// use: kiri-mode.laser.driver +// use: kiri-mode.drag.driver +// use: kiri-mode.wjet.driver +// use: kiri-mode.wedm.driver +gapp.register("kiri-run.worker", [], (root, exports) => { +const { base, kiri, moto } = root; +const { util, polygons, wasm_ctrl } = base; +const { codec } = kiri; const { time } = util; +const POLY = polygons; -let debug = (self.debug === true), - drivers = { - DRAG, - CAM, - FDM, - LASER, - SLA, - WEDM, - WJET - }, - ccvalue = self.navigator ? self.navigator.hardwareConcurrency || 0 : 0, +let debug = self.debug === true, + ccvalue = this.navigator ? navigator.hardwareConcurrency || 0 : 0, concurrent = Math.min(4, self.Worker && ccvalue > 3 ? ccvalue - 1 : 0), - current = { + current = self.worker = { print: null, snap: null, - mode: null, + mode: null }, wgroup = {}, wcache = {}, @@ -52,6 +55,8 @@ let debug = (self.debug === true), minifns = {}, miniseq = 0; +kiri.version = gapp.version; + // catch clipper alerts and convert to console messages self.alert = function(o) { console.log(o); @@ -71,7 +76,8 @@ function minhandler(msg) { } // for concurrent operations -const minwork = { +const minwork = +kiri.minions = { get concurrent() { return concurrent }, @@ -85,20 +91,13 @@ const minwork = { return; } for (let i=0; i < concurrent; i++) { - let minion = new Worker(`/v2/lib/kiri-run/minion.js`, { type: 'module' }); - minion.onerror = (error) => { - console.log({ MINION_ERROR: error }); - error.preventDefault(); - }; - minion.onmessageerror = (error) => { - console.log({ MINION_MESSAGE_ERROR: error }); - error.preventDefault(); - }; + let _ = debug ? '_' : ''; + let minion = new Worker(`/code/kiri_pool.js?${_}${self.kiri.version}`); minion.onmessage = minhandler; minion.postMessage({ cmd: "label", name: `#${i}` }); minions.push(minion); } - console.log(`kiri | init pool | ${version || "rogue"} | ${concurrent + 1}`); + console.log(`kiri | init pool | ${gapp.version || "rogue"} | ${concurrent + 1}`); }, stop() { @@ -151,7 +150,7 @@ const minwork = { let arr = data.fill; let fill = []; for (let i=0; i { if (concurrent < 2) { reject("concurrent slice unavaiable"); @@ -201,63 +199,64 @@ const minwork = { floatP[i++] = p.y; floatP[i++] = p.z; } - const state = { zeros: [] }; minwork.queue({ cmd: "sliceZ", z, points: floatP, - options + options: codec.toCodable(options) }, data => { - console.log({ data }); - for (let rec of data.output) { - each(rec); + let recs = codec.decode(data.output); + if (each) { + for (let rec of recs) { + each(rec); + } } - resolve(); - }, state.zeros); + resolve(recs); + }, [ floatP.buffer ]); }); }, queue(work, ondone, direct) { - if (direct) { - return ondone(work); - } - let seq = ++miniseq; - minifns[seq] = ondone; - minionq.push({ seq, work }); + minionq.push({work, ondone, direct}); minwork.kick(); }, queueAsync(work, direct) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { minwork.queue(work, resolve, direct); }); }, kick() { - if (minionq.length === 0 || minions.length === 0) { - return; + if (minions.length && minionq.length) { + let qrec = minionq.shift(); + let minion = minions.shift(); + let seq = miniseq++; + qrec.work.seq = seq; + minifns[seq] = (data) => { + qrec.ondone(data); + minions.push(minion); + minwork.kick(); + }; + minion.postMessage(qrec.work, qrec.direct); } - let minion = minions.shift(); - let { seq, work } = minionq.shift(); - work.seq = seq; - minion.postMessage(work); - minions.push(minion); }, broadcast(cmd, data, direct) { - if (direct) { - return; - } for (let minion of minions) { - minion.postMessage({ cmd, data }); + minion.postMessage({ + cmd, ...data + }, direct); } } }; -console.log(`kiri | init work | ${version || "rogue"}`); +console.log(`kiri | init work | ${gapp.version || "rogue"}`); // code is running in the worker / server context -const dispatch = { +const dispatch = +kiri.server = +kiri.worker = { pool_start(data, send) { minwork.start(); send.done({}); @@ -296,8 +295,8 @@ const dispatch = { // current.snap = null; current.print = null; dispatch.group = wgroup = {}; - dispatch.cache = worker.cache = wcache = {}; - Widget.Groups.clear(); + dispatch.cache = wcache = {}; + kiri.Widget.Groups.clear(); send.done({ clear: true }); }, @@ -312,7 +311,7 @@ const dispatch = { } let vertices = data.vertices, - widget = newWidget(data.id, group) + widget = kiri.newWidget(data.id, group) .setInWorker() .loadVertices(vertices); @@ -394,35 +393,19 @@ const dispatch = { }, slice(data, send) { - send.data({ update:0.001, updateStatus:"slicing" }); + send.data({update:0.001, updateStatus:"slicing"}); - const { settings } = data; - const { mode } = settings; - const driver = drivers[mode]; - const widget = wcache[data.id]; - - if (!(driver && driver.prepare)) { - return console.log({ invalid_print_driver: mode, driver }); - } - - let last = time(), now; + let settings = data.settings, + widget = wcache[data.id], + last = time(), + now; current.print = null; current.mode = settings.mode.toUpperCase(); widget.anno = data.anno || widget.anno; - widget.settings = settings; - widget.clearSlices(); - driver.slice(settings, widget, (update, msg, alert) => { - now = time(); - // on alert - if (alert) send.data({ alert }); - // on update - if (now - last < 10 && update < 0.99) return; - if (update || msg) send.data({update: (0.05 + update * 0.95), updateStatus: msg}); - last = now; - }, (error) => { + widget.slice(settings, function(error) { if (error) { send.data({error: error}); } else { @@ -439,14 +422,21 @@ const dispatch = { send.data({send_end: time()}); } send.done({done: true}); - widget.points = undefined; + }, function(update, msg, alert) { + now = time(); + // on alert + if (alert) send.data({ alert }); + // on update + if (now - last < 10 && update < 0.99) return; + if (update || msg) send.data({update: (0.05 + update * 0.95), updateStatus: msg}); + last = now; }); }, sliceAll(data, send) { const { settings } = data; const { mode } = settings; - const driver = drivers[mode]; + const driver = kiri.driver[mode]; if (driver.sliceAll) { driver.sliceAll(settings, send.data); @@ -464,10 +454,10 @@ const dispatch = { const { settings } = data; const { mode } = settings; - const driver = drivers[mode]; + const driver = kiri.driver[mode]; if (!(driver && driver.prepare)) { - return console.log({ invalid_print_driver: mode, driver }); + return console.log({invalid_print_driver: mode, driver}); } driver.prepare(widgets, settings, (progress, message, layer) => { @@ -487,7 +477,7 @@ const dispatch = { export(data, send) { const mode = data.settings.mode; - const driver = drivers[mode]; + const driver = kiri.driver[mode]; if (!(driver && driver.export)) { console.log({missing_export_driver: mode}); @@ -495,26 +485,26 @@ const dispatch = { } let output; - driver.export(current.print, (line, direct) => { + driver.export(current.print, function(line, direct) { send.data({line}, direct); - }, (done) => { + }, function(done) { // SLA workaround output = done; - }, (debug) => { + }, function(debug) { send.data({debug}); }); - // export in different modes, based on device type - if (mode.toUpperCase() === "LASER") { + //export in different modes, based on device type + if(mode.toUpperCase() === "LASER"){ let FExt = data.settings.device.gcodeFExt.toUpperCase(); - if (FExt === "DXF"){ + if(FExt === "DXF"){ let dxf = driver.exportDXF(data.settings, current.print.output); send.data({line:dxf}); - } else if(FExt === "SVG") { + }else if(FExt === "SVG"){ let svg = driver.exportSVG(data.settings, current.print.output); send.data({line:svg}); - } else { - // if not svg or dxf, default to gcode + }else{ + //if not svg or dxf, default to gcode let gcode = driver.exportGCode(data.settings, current.print.output); send.data({line:gcode}); } @@ -552,7 +542,7 @@ const dispatch = { const { colors, max } = data; const colorMap = {}; colors.forEach(color => { - colorMap[color] = render.rate_to_color(color, max); + colorMap[color] = kiri.render.rate_to_color(color, max); }); send.done(colorMap); }, @@ -566,7 +556,7 @@ const dispatch = { z: origin.z }; const device = settings.device; - const print = current.print = newPrint(settings, Object.values(wcache)); + const print = current.print = kiri.newPrint(settings, Object.values(wcache)); const tools = device.extruders; const mode = settings.mode; const thin = settings.controller.lineType === 'line' || mode !== 'FDM'; @@ -576,7 +566,7 @@ const dispatch = { }, done => { const minSpeed = print.minSpeed; const maxSpeed = print.maxSpeed; - render.path(done.output, progress => { + kiri.render.path(done.output, progress => { send.data({ progress: 0.25 + progress * 0.75 }); }, { thin: thin || print.belt, flat, tools }) .then(layers => { @@ -593,11 +583,11 @@ const dispatch = { parsed.forEach(layer => { layer.forEach(out => { const { x, y, z } = out.point; - out.point = newPoint(x,y,z || 0); + out.point = base.newPoint(x,y,z || 0); }); }); - const print = current.print = newPrint(null, Object.values(wcache)); - render.path(parsed, progress => { + const print = current.print = kiri.newPrint(null, Object.values(wcache)); + kiri.render.path(parsed, progress => { send.data({ progress }); }, { thin: true }) .then(layers => { @@ -704,7 +694,7 @@ dispatch.onmessage = self.onmessage = async function(e) { msg = e.data || {}, run = dispatch[msg.task], send = { - data : function(data, direct) { + data : function(data,direct) { // if (direct && direct.length) { // console.log( direct.map(z => z.byteLength).reduce((a,v) => a+v) ); // } @@ -753,19 +743,9 @@ dispatch.onmessage = self.onmessage = async function(e) { } }; -const worker = self.kiri_worker = { - cache: wcache, - current, - dispatch, - drivers, - version, - minions: minwork -}; +moto.broker.publish("worker.started", { dispatch, minions: minwork }); -// initilize driver mode handlers -CAM.init(worker); -FDM.init(worker); -LASER.init(worker); -SLA.init(worker); +}); -broker.publish("worker.started", { dispatch, minions: minwork }); +// load kiri modules +// kiri.load_exec(dispatch); diff --git a/src2/kiri/alerts.js b/src.old/kiri/alerts.js similarity index 89% rename from src2/kiri/alerts.js rename to src.old/kiri/alerts.js index 5cf3c46d..8ed32689 100644 --- a/src2/kiri/alerts.js +++ b/src.old/kiri/alerts.js @@ -1,6 +1,12 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from './api.js'; +"use strict"; + +// dep: kiri.api +gapp.register("kiri.alerts", [], (root, exports) => { + +const { kiri } = root; +const { api } = kiri; let alerts = []; @@ -62,14 +68,11 @@ function update(clear) { } } -export { +// extend API +Object.assign(api.alerts, { hide, show, update -}; +}); -export default { - hide, - show, - update -}; +}); diff --git a/src.old/kiri/api.js b/src.old/kiri/api.js new file mode 100644 index 00000000..9dae1981 --- /dev/null +++ b/src.old/kiri/api.js @@ -0,0 +1,149 @@ +/** Copyright Stewart Allen -- 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; +} + +}); diff --git a/src2/kiri/client.js b/src.old/kiri/client.js similarity index 91% rename from src2/kiri/client.js rename to src.old/kiri/client.js index c902a948..b9e6e2bb 100644 --- a/src2/kiri/client.js +++ b/src.old/kiri/client.js @@ -1,17 +1,27 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from './api.js'; -import { noop } from './utils.js'; +"use strict"; + +// use: kiri.api +gapp.register("kiri.client", [], (root, exports) => { + +const { kiri } = root; // this code runs in kiri's main loop -let debug = self.debug === true, +let loc = self.location, + host = loc.hostname, + port = loc.port, + proto = loc.protocol, + debug = self.debug === true, time = Date.now, seqid = 1, syncd = {}, running = {}, worker = null, minions = false, - restarting = false; + restarting = false + // occ = new Worker("/kiri/ext/occ-worker.js", {type:"module"}), + ; /** * @param {Function} fn name of function in kiri.worker @@ -42,7 +52,7 @@ function send(fn, data, onreply, zerocopy) { } // code is running in the browser / client context -export const client = { +const client = exports({ send: send, pool: { @@ -65,16 +75,8 @@ export const client = { const blob = new Blob([ work ], { type: 'application/javascript' }); return new Worker(URL.createObjectURL(blob)); } else { - let worker = new Worker(`/v2/lib/kiri-run/worker.js`, { 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; + let _ = debug ? '_' : ''; + return new Worker(`/code/kiri_work.js?${_}${gapp.version}`); } }, @@ -110,7 +112,7 @@ export const client = { running = {}; worker = client.newWorker(); - client.onmessage = worker.onmessage = (e) => { + client.onmessage = worker.onmessage = function(e) { let now = time(), reply = e.data, record = running[reply.seq], @@ -159,7 +161,7 @@ export const client = { // widget sync sync(widgets) { if (!widgets) { - widgets = api.widgets.all(); + widgets = kiri.api.widgets.all(); } // sync any widget that has changed for (let widget of widgets.filter(w => w.modified || !syncd[w.id])) { @@ -243,7 +245,7 @@ export const client = { ondone(null, reply.error); } if (reply.debug) { - api.event.emit("export.debug", reply.debug); + kiri.api.event.emit("export.debug", reply.debug); } }); }, @@ -332,11 +334,9 @@ export const client = { // console.log({ clearCache_reply: reply }); }); } -}; +}); // start worker client.restart(); -self.kiri_client = client; - -export default client; +}); diff --git a/src2/kiri/codec.js b/src.old/kiri/codec.js similarity index 89% rename from src2/kiri/codec.js rename to src.old/kiri/codec.js index 2f07bb54..db11d16c 100644 --- a/src2/kiri/codec.js +++ b/src.old/kiri/codec.js @@ -1,15 +1,32 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base } from '../geo/base.js'; -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'; +"use strict"; +// 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, @@ -193,7 +210,7 @@ function decodePointArray(array) { const points = new Array(length / 3); for (let vid=0, pid=0; vid < length; ) { - points[pid++] = newPoint(array[vid++], array[vid++], array[vid++]); + points[pid++] = base.newPoint(array[vid++], array[vid++], array[vid++]); } return points; @@ -208,7 +225,7 @@ function decodePointArray2D(array, z, fn) { for (let vid=0, pid=0; vid < length; ) { points[pid++] = fn ? fn(array[vid++], array[vid++]) : - newPoint(array[vid++], array[vid++], z); + base.newPoint(array[vid++], array[vid++], z); } return points; @@ -216,7 +233,7 @@ function decodePointArray2D(array, z, fn) { // ----- Widget Codec ----- -Widget.prototype.encode = function(state) { +kiri.Widget.prototype.encode = function(state) { const json = state._json_; const geo = this.getGeoVertices(); const coded = { @@ -235,7 +252,7 @@ registerDecoder(TYPE.WIDGET, function(v, state) { const id = v.id, group = v.group || id, track = v.track || undefined, - widget = newWidget(id, Widget.Groups.forid(group)); + widget = kiri.newWidget(id, kiri.Widget.Groups.forid(group)); widget.loadVertices(v.json ? v.geo.toFloat32() : v.geo); widget.saved = Date.now(); @@ -249,7 +266,7 @@ registerDecoder(TYPE.WIDGET, function(v, state) { // ----- Slice Codec ----- -Slice.prototype.encode = function(state) { +kiri.Slice.prototype.encode = function(state) { const rv = { type: TYPE.SLICE, z: this.z, @@ -262,7 +279,7 @@ Slice.prototype.encode = function(state) { }; registerDecoder(TYPE.SLICE, function(v, state) { - let slice = newSlice(v.z, state.mesh ? state.mesh.newGroup() : null); + let slice = kiri.newSlice(v.z, state.mesh ? state.mesh.newGroup() : null); slice.index = v.index; slice.layers = decode(v.layers, state) @@ -272,7 +289,7 @@ registerDecoder(TYPE.SLICE, function(v, state) { // ----- Slice.Top Codec ----- -Top.prototype.encode = function(state) { +kiri.Top.prototype.encode = function(state) { let obj = { type: TYPE.TOP, poly: encode(this.poly, state) @@ -290,7 +307,7 @@ Top.prototype.encode = function(state) { }; registerDecoder(TYPE.TOP, function(v, state) { - let top = newTop(decode(v.poly, state)); + let top = kiri.newTop(decode(v.poly, state)); if (state.full) { // top.gaps = decode(v.gaps, state); top.last = decode(v.last, state); @@ -303,7 +320,7 @@ registerDecoder(TYPE.TOP, function(v, state) { // ----- Polygon Codec ----- -Polygon.prototype.encode = function(state) { +base.Polygon.prototype.encode = function(state) { if (!state.poly) state.poly = {}; let cached = state.poly[this.id]; @@ -335,10 +352,10 @@ registerDecoder(TYPE.POLY, function(v, state) { const array = v.array; const length = array.length; - const poly = newPolygon(); + const poly = base.newPolygon(); for (let vid = 0; vid < length; ) { - poly.push(newPoint(array[vid++], array[vid++], array[vid++])); + poly.push(base.newPoint(array[vid++], array[vid++], array[vid++])); } state.poly[v.id] = poly; @@ -365,7 +382,7 @@ function encodeLayerPolys(polys, state) { }); } -Layers.prototype.encode = function(state) { +kiri.Layers.prototype.encode = function(state) { let zeros = state.zeros; let enc = { type: TYPE.LAYERS, @@ -402,7 +419,7 @@ Layers.prototype.encode = function(state) { }; registerDecoder(TYPE.LAYERS, function(v, state) { - const render = new Layers(); + const render = new kiri.Layers(); const { layers } = v; for (let i=0; i -- 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; @@ -211,7 +219,7 @@ const renamed = { outputClockwise: "camConventional" }; -export const conf = { +const conf = exports({ // --------------- helper functions normalize, device_from_code, @@ -963,7 +971,7 @@ export const conf = { id: genID(), ver: CVER } -}; +}); const settings = conf.template; @@ -982,3 +990,5 @@ settings.cdev.LASER = clone(settings.device); settings.cdev.DRAG = clone(settings.device); settings.cdev.WJET = clone(settings.device); settings.cdev.WEDM = clone(settings.device); + +}); diff --git a/src2/kiri/consts.js b/src.old/kiri/consts.js similarity index 97% rename from src2/kiri/consts.js rename to src.old/kiri/consts.js index 750f4296..5ba2d52f 100644 --- a/src2/kiri/consts.js +++ b/src.old/kiri/consts.js @@ -1,5 +1,9 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +"use strict"; + +gapp.register("kiri.consts", (root, exports) => { + const COLOR = { wireframe: 0x444444, wireframe_opacity: 0.25, @@ -143,7 +147,7 @@ const PMODES = { const SEED = 'kiri-seed'; -export let consts = { +exports({ PMODES, COLOR, LISTS, @@ -151,6 +155,6 @@ export let consts = { VIEWS, SEED, beltfact: Math.cos(Math.PI / 4) -}; +}); -export { COLOR, LISTS, MODES, VIEWS, PMODES, SEED }; +}); diff --git a/src2/kiri/conv.js b/src.old/kiri/conv.js similarity index 100% rename from src2/kiri/conv.js rename to src.old/kiri/conv.js diff --git a/src.old/kiri/devices.js b/src.old/kiri/devices.js new file mode 100644 index 00000000..e9055c51 --- /dev/null +++ b/src.old/kiri/devices.js @@ -0,0 +1,393 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +// dep: kiri.api +// dep: kiri.settings +gapp.register("kiri.devices", (root, exports) => { + + let { kiri } = root, + { api, conf } = kiri; + + // extend API + Object.assign(api.show, { + devices: showDevices + }); + + Object.assign(api.device, { + clone: cloneDevice, + code: currentDeviceCode, + get: currentDeviceName, + set: selectDevice, + isBelt + }); + + Object.assign(api.devices, { + show: showDevices, + select: selectDevice, + refresh: updateDeviceList, + update_laser_state: updateLaserState + }); + + function isBelt() { + return api.conf.get().device.bedBelt; + } + + function currentDeviceName() { + return api.conf.get().filter[api.mode.get()]; + } + + function currentDeviceCode() { + return api.conf.get().devices[currentDeviceName()]; + } + + function getModeDevices() { + // devices are injected into self scope by + // app.js generateDevices() + return Object.keys(devices[api.mode.get_lower()]).sort(); + } + + function showDevices() { + api.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 = api.conf.get().devices; + if (newname !== selected) { + devs[newname] = devs[selected]; + delete devs[selected]; + selectDevice(newname); + updateDeviceList(); + } + } + + function putLocalDevice(devicename, obj) { + api.conf.get().devices[devicename] = obj; + api.conf.save(); + } + + function removeLocalDevice(devicename) { + delete api.conf.get().devices[devicename]; + api.conf.save(); + api.settings.sync.put(); + } + + function isLocalDevice(devicename) { + return api.conf.get().devices[devicename] ? true : false; + } + + function getSelectedDevice() { + return api.device.get(); + } + + function selectDevice(devicename) { + if (isLocalDevice(devicename)) { + setDeviceCode(api.conf.get().devices[devicename], devicename); + } else { + let code = devices[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(api.conf.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); + api.settings.sync.put(); + } + + function updateLaserState() { + const dev = api.conf.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 = api.conf.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; + let { space } = kiri; + + 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 { + if (newname) { + updateDeviceName(newname); + api.conf.save(); + api.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 ? api.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); + } + +}); diff --git a/src2/kiri/do.js b/src.old/kiri/do.js similarity index 93% rename from src2/kiri/do.js rename to src.old/kiri/do.js index 2432eab3..6989c2be 100644 --- a/src2/kiri/do.js +++ b/src.old/kiri/do.js @@ -1,8 +1,13 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from './api.js'; -import { space } from '../moto/space.js'; +// dep: kiri.api +// use: kiri.selection +// use: moto.space +gapp.register("kiri.do", [], (root, events) => { +const { kiri, moto } = root; +const { api } = kiri; +const { space } = moto; const { event } = api; let stack = []; @@ -39,7 +44,7 @@ let clear = api.doit.clear = function() { }; function updateButtons() { - let isArrange = api.view.get() === api.consts.VIEWS.ARRANGE; + let isArrange = api.view.get() === kiri.consts.VIEWS.ARRANGE; $('doit').style.display = isArrange && stack.length ? 'flex' : 'none'; $('undo').disabled = stpos === 0; $('redo').disabled = stpos == stack.length; @@ -170,3 +175,5 @@ event.on('mouse.drag.done', () => { }); moved = {x:0, y:0}; }); + +}); diff --git a/src2/kiri/export.js b/src.old/kiri/export.js similarity index 96% rename from src2/kiri/export.js rename to src.old/kiri/export.js index af88ffc0..f217f5f7 100644 --- a/src2/kiri/export.js +++ b/src.old/kiri/export.js @@ -1,13 +1,23 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -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 '../kiri-mode/laser/driver.js'; -import { SLA as sla_client } from '../kiri-mode/sla/client.js'; +"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; let printSeq = parseInt(local['kiri-print-seq'] || local['print-seq'] || "0") + 1; @@ -19,7 +29,7 @@ function localSet(key, val) { return api.local.set(key, val); } -export function exportFile(options) { +function exportFile(options) { let mode = api.mode.get(); let names = api.widgets.all().map(w => w.meta ? w.meta.file : undefined) .filter(v => v) @@ -43,7 +53,7 @@ function callExport(callback, mode, names) { let gcode = []; let section = []; let sections = { }; - client.export(api.conf.get(), (line) => { + kiri.client.export(api.conf.get(), (line) => { if (typeof line !== 'string') { if (line.section) { sections[line.section] = section = []; @@ -65,7 +75,7 @@ function callExport(callback, mode, names) { } function callExportLaser(options, names) { - client.export(api.conf.get(), (line) => { + kiri.client.export(api.conf.get(), (line) => { // engine export uses lines // console.log({unexpected_line: line}); }, (output, error) => { @@ -79,14 +89,14 @@ function callExportLaser(options, names) { } function callExportSLA(options, names) { - client.export(api.conf.get(), (line) => { + kiri.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 { - sla_client.printDownload(output, api, names); + kiri.driver.SLA.printDownload(output, api, names); } }); } @@ -97,7 +107,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 = laser_driver; + const driver = kiri.driver.LASER; function download_svg() { api.util.download( @@ -197,7 +207,7 @@ function exportGCodeDialog(gcode, sections, info, names) { ajax.onreadystatechange = function() { if (ajax.readyState === 4) { let status = ajax.status; - api.stats.add(`ua_${api.mode.get_lower()}_print_octo_${status}`); + stats.add(`ua_${api.mode.get_lower()}_print_octo_${status}`); if (status >= 200 && status < 300) { api.modal.hide(); } else { @@ -268,11 +278,11 @@ function exportGCodeDialog(gcode, sections, info, names) { ) .then(t => t.text()) .then(t => { - api.stats.add(`ua_${api.mode.get_lower()}_print_local_ok`); + stats.add(`ua_${api.mode.get_lower()}_print_local_ok`); console.log({grid_spool_said: t}); }) .catch(e => { - api.stats.add(`ua_${api.mode.get_lower()}_print_local_err`); + stats.add(`ua_${api.mode.get_lower()}_print_local_err`); console.log({grid_local_spool_error: e}); }) .finally(() => { @@ -384,7 +394,7 @@ function exportGCodeDialog(gcode, sections, info, names) { xhtr.onreadystatechange = function() { if (xhtr.readyState === 4) { let status = xhtr.status; - api.stats.add(`ua_${api.mode.get_lower()}_print_grid_${status}`); + 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); @@ -501,7 +511,7 @@ function exportGCodeDialog(gcode, sections, info, names) { }) } } - client.zip(files, progress => { + kiri.client.zip(files, progress => { api.show.progress(progress.percent/100, "generating zip files"); }, output => { api.show.progress(0); @@ -671,7 +681,7 @@ function exportGCodeDialog(gcode, sections, info, names) { name: `Metadata/top_1.png`, data: api.view.bambu.s512.png }]; - client.zip(files, progress => { + kiri.client.zip(files, progress => { api.show.progress(progress.percent/100, "generating 3mf"); }, output => { api.show.progress(0); @@ -732,3 +742,5 @@ 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); } + +}); diff --git a/src2/kiri/files.js b/src.old/kiri/files.js similarity index 96% rename from src2/kiri/files.js rename to src.old/kiri/files.js index 0067915e..ab878c95 100644 --- a/src2/kiri/files.js +++ b/src.old/kiri/files.js @@ -1,5 +1,13 @@ /** Copyright Stewart Allen -- 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; @@ -168,8 +176,8 @@ function notifyFileListeners(store) { } } -export const openFiles = function(indexdb) { +kiri.openFiles = function(indexdb) { return new Files(indexdb); }; -export { saveFileList, notifyFileListeners }; +}); diff --git a/src2/kiri/frame.js b/src.old/kiri/frame.js similarity index 90% rename from src2/kiri/frame.js rename to src.old/kiri/frame.js index 8f5df085..396135ae 100644 --- a/src2/kiri/frame.js +++ b/src.old/kiri/frame.js @@ -1,12 +1,14 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { load } from '../load.js'; -import { kiri } from './kiri.js'; -import { moto } from '../moto.js'; -import { api } from './api.js'; -import { newWidget } from './widget.js'; -import { conf, event, feature, platform, settings, show } from './api.js'; -import { VIEWS } from './consts.js'; +"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) => { // add frame message api listener window.addEventListener('message', msg => { @@ -125,5 +127,4 @@ window.addEventListener('message', msg => { } }); - -// No direct exports; sets up message listener +}); diff --git a/src2/kiri/function.js b/src.old/kiri/function.js similarity index 84% rename from src2/kiri/function.js rename to src.old/kiri/function.js index a59ec751..8c68fd50 100644 --- a/src2/kiri/function.js +++ b/src.old/kiri/function.js @@ -1,16 +1,30 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -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'; +"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; let complete = {}; function prepareSlices(callback, scale = 1, offset = 0) { - const { conf, event, feature, hide, mode, view, platform, show, stacks } = api; + const { conf, event, feature, hide, mode, view, platform, show } = api; + const { stacks } = kiri; if (view.is_arrange()) { // in arrange mode, create a screenshot at the start slicing @@ -125,34 +139,10 @@ function prepareSlices(callback, scale = 1, offset = 0) { } function sliceWidget(widget) { + // weight each widget progress % by their # vertices + let factor = (widget.getVertices().count / defvert); - 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) { + widget.slice(settings, (sliced, error) => { let mark = Date.now(); // update UI info if (sliced) { @@ -179,55 +169,28 @@ function prepareSlices(callback, scale = 1, offset = 0) { // start next widget slice sliceNext(); } - } - - // 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); + }, (update, msg, alert) => { + if (alert) { + api.show.alert(alert); } - if (reply.update) { - onupdate(reply.update, reply.updateStatus); + if (msg && msg !== lastMsg) { + let mark = Date.now(); + if (lastMsg) { + segtimes[`${widget.id}_${segNumber++}_${lastMsg}`] = mark - startTime; + } + lastMsg = msg; + startTime = mark; } - 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); + // 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); } }); - - // discard point cache - widget.points = undefined; } function sliceDone() { @@ -284,7 +247,9 @@ function prepareSlices(callback, scale = 1, offset = 0) { } function preparePreview(callback, scale = 1, offset = 0) { - const { conf, event, feature, hide, mode, view, platform, show, stacks } = api; + const { conf, event, feature, hide, mode, view, platform, show } = api; + const { stacks } = kiri; + const widgets = api.widgets.all(); const settings = conf.get(); const { device, process, controller } = settings; @@ -340,7 +305,7 @@ function preparePreview(callback, scale = 1, offset = 0) { client.prepare(settings, (progress, message, layer) => { if (layer) { - output.push(codec.decode(layer)); + output.push(kiri.codec.decode(layer)); } if (message && message !== lastMsg) { const mark = Date.now(); @@ -442,7 +407,7 @@ function prepareExport() { } api.event.emit("function.export", {mode: settings.mode}); complete.export = true; - exportFile(...argsave); + kiri.export(...argsave); } function cancelWorker() { @@ -476,12 +441,8 @@ function parseCode(code, type) { }); } -function clear_progress() { - complete = {}; -} - // extend API (api.function) -export const functions = { +const functions = Object.assign(api.function, { slice: prepareSlices, print: preparePreview, prepare: preparePreview, @@ -490,16 +451,7 @@ export const functions = { cancel: cancelWorker, parse: parseCode, clear: client.clear, - clear_progress, -}; + clear_progress() { complete = {} } +}); -export { - prepareSlices as slice, - preparePreview as print, - preparePreview as prepare, - prepareAnimation as animate, - prepareExport as export, - cancelWorker as cancel, - parseCode as parse, - clear_progress, -}; +}); diff --git a/src.old/kiri/init.js b/src.old/kiri/init.js new file mode 100644 index 00000000..d489ed21 --- /dev/null +++ b/src.old/kiri/init.js @@ -0,0 +1,2169 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +// dep: geo.base +// dep: kiri.api +// dep: kiri.main +// dep: kiri.lang +// dep: kiri.stats +// dep: kiri.consts +// dep: kiri.devices +// dep: kiri.platform +// dep: kiri.selection +// use: kiri.tools +// use: kiri.pack +gapp.register("kiri.init", (root, exports) => { + + // builds and wires up the UI + let { base, kiri } = root, + { api, catalog, consts, space } = kiri, + { sdb, stats, platform, selection, ui, uc } = api, + { VIEWS, MODES, SEED } = consts, + { LANG, LOCAL, SETUP } = api.const, + { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, + WIN = self.window, + DOC = self.document, + STARTMODE = SETUP.sm && SETUP.sm.length === 1 ? SETUP.sm[0] : null, + TWOD = [LASER, DRAG, WJET, WEDM], + TWONED = [LASER, DRAG, WJET], + THREED = [FDM, CAM, SLA], + GCODE = [FDM, CAM, ...TWOD], + CAM_LZR = [CAM, ...TWOD], + FDM_LZN = [FDM, ...TWONED], + NO_WEDM = [FDM, CAM, SLA, LASER, DRAG, WJET], + FDM_CAM = [FDM, CAM], + proto = location.protocol, + platformColor, + separator = true, + hideable = true, + inline = true, + driven = true, + trigger = true, + units = true; + + // copy version from grid app + kiri.version = gapp.version; + + function settings() { + return api.conf.get(); + } + + function updateTool(ev) { + api.tool.update(ev); + } + + function checkSeed(then) { + // skip sample object load in onshape (or any script postload) + if (!sdb[SEED]) { + sdb[SEED] = new Date().getTime(); + if (!SETUP.s && api.feature.seed) { + if (SETUP.debug) { + return then(); + } + platform.load_stl("/obj/cube.stl", function(vert) { + catalog.putFile("sample cube.stl", vert); + platform.update_bounds(); + space.view.home(); + setTimeout(() => { api.space.save(true) },500); + then(); + api.help.show(); + }); + return true; + } + } + return false; + } + + function unitsSave() { + api.conf.update({ controller: true }); + platform.update_size(); + } + + function aniMeshSave() { + api.conf.update({ controller: true }); + api.conf.save(); + } + + function lineTypeSave() { + const sel = ui.lineType.options[ui.lineType.selectedIndex]; + if (sel) { + settings().controller.lineType = sel.value; + api.conf.save(); + } + } + + function thinWallSave() { + let opt = ui.sliceDetectThin; + let level = opt.options[opt.selectedIndex]; + if (level) { + settings().process.sliceDetectThin = level.value; + api.conf.save(); + } + } + + function detailSave() { + let level = ui.detail.options[ui.detail.selectedIndex]; + if (level) { + level = level.value; + let rez = base.config.clipperClean; + switch (level) { + case '100': rez = 50; break; + case '75': rez = base.config.clipperClean; break; + case '50': rez = 500; break; + case '25': rez = 1000; break; + } + kiri.client.config({ + base: { clipperClean: rez } + }); + settings().controller.detail = level; + api.conf.save(); + } + } + + function speedSave() { + settings().controller.showSpeeds = ui.showSpeeds.checked; + api.view.update_speeds(); + } + + function zAnchorSave() { + api.conf.update(); + api.platform.update_top_z(); + } + + function setThreaded(bool) { + if (bool) { + kiri.client.pool.start(); + } else { + kiri.client.pool.stop(); + } + return bool; + } + + api.event.on("set.threaded", bool => setThreaded(bool)); + + function booleanSave() { + let control = settings().controller; + let doAlert = ui.ortho.checked !== control.ortho; + if (control.assembly != ui.assembly.checked) { + kiri.client.wasm(ui.assembly.checked); + } + if (control.antiAlias != ui.antiAlias.checked) { + api.show.alert('Page Reload Required to Change Aliasing'); + } + control.shiny = ui.shiny.checked; + control.drawer = ui.drawer.checked; + control.scrolls = ui.scrolls.checked; + control.showOrigin = ui.showOrigin.checked; + control.showRulers = ui.showRulers.checked; + control.autoLayout = ui.autoLayout.checked; + control.freeLayout = ui.freeLayout.checked; + control.spaceRandoX = ui.spaceRandoX.checked; + control.autoSave = ui.autoSave.checked; + control.antiAlias = ui.antiAlias.checked; + control.reverseZoom = ui.reverseZoom.checked; + control.dark = ui.dark.checked; + control.exportOcto = ui.exportOcto.checked; + control.exportGhost = ui.exportGhost.checked; + control.exportLocal = ui.exportLocal.checked; + control.exportThumb = ui.exportThumb.checked; + control.exportPreview = ui.exportPreview.checked; + control.healMesh = ui.healMesh.checked; + control.threaded = setThreaded(ui.threaded.checked); + control.assembly = ui.assembly.checked; + control.ortho = ui.ortho.checked; + control.devel = ui.devel.checked; + space.view.setZoom(control.reverseZoom, control.zoomSpeed); + // platform.layout(); + api.conf.save(); + api.platform.update_size(); + updateStats(); + updateDrawer(); + api.event.emit('boolean.update'); + if (doAlert) { + api.show.alert("change requires page refresh"); + } + } + + function updateDrawer() { + const { drawer, scrolls } = settings().controller; + $c('app', drawer ? 'slideshow' : '', drawer ? '' : 'slideshow'); + $c('app', scrolls ? '' : 'hide-scroll', scrolls ? 'hide-scroll' : ''); + } + + function updateStats() { + if (self.debug !== true) { + return; + } + let { div, fps, rms, rnfo } = ui.stats; + div.style.display = 'flex'; + setInterval(() => { + const nrms = space.view.getRMS().toFixed(1); + const nfps = space.view.getFPS().toFixed(1); + const rend = space.renderInfo(); + const { memory, render } = rend; + if (nfps !== fps.innerText) { + fps.innerText = nfps; + } + if (nrms !== rms.innerText) { + rms.innerText = nrms; + } + if (rnfo.offsetParent !== null) { + rnfo.innerHTML = Object.entries({ ...memory, ...render, render_ms: nrms, frames_sec: nfps }).map(row => { + return `
${row[0]}
` + }).join(''); + } + }, 100); + } + + function onBooleanClick(el) { + // copy some ui elements to target settings + let settings = api.conf.get(); + settings.device.bedBelt = ui.deviceBelt.checked && api.mode.is_fdm(); + settings.device.bedRound = ui.deviceRound.checked && api.mode.is_fdm(); + settings.device.originCenter = ui.deviceOrigin.checked || ui.deviceRound.checked; + settings.device.fwRetract = ui.fwRetract.checked; + // refresh vars and other ui elements + uc.refresh(); + if (el === ui.camStockIndexed) { + api.view.set_arrange(); + } + api.conf.update(); + DOC.activeElement.blur(); + api.event.emit("boolean.click"); + api.devices.update_laser_state(); + } + + function onButtonClick(ev) { + let target = ev.target; + while (target && target.tagName !== 'BUTTON') { + target = target.parentNode; + } + api.event.emit("button.click", target); + } + + function inputHasFocus() { + let active = DOC.activeElement; + return active && (active.nodeName === "INPUT" || active.nodeName === "TEXTAREA"); + } + + function cca(c) { + return c.charCodeAt(0); + } + + function keyUpHandler(evt) { + if (api.feature.on_key) { + if (api.feature.on_key({up:evt})) return; + } + for (let handler of api.feature.on_key2) { + if (handler({up:evt})) return; + } + switch (evt.keyCode) { + // escape + case 27: + // blur text input focus + DOC.activeElement.blur(); + // dismiss modals + api.modal.hide(); + // deselect widgets + platform.deselect(); + // hide all dialogs + api.dialog.hide(); + // cancel slicing + api.function.cancel(); + // and send an event (used by FDM client) + api.event.emit("key.esc"); + break; + } + return false; + } + + function keyDownHandler(evt) { + if (api.modal.visible()) { + return false; + } + if (api.feature.on_key) { + if (api.feature.on_key({down:evt})) return; + } + for (let handler of api.feature.on_key2) { + if (handler({down:evt})) return; + } + let move = evt.altKey ? 5 : 0, + deg = move ? 0 : -Math.PI / (evt.shiftKey ? 36 : 2); + switch (evt.keyCode) { + case 8: // apple: delete/backspace + case 46: // others: delete + if (inputHasFocus()) return false; + platform.delete(selection.meshes()); + evt.preventDefault(); + break; + case 37: // left arrow + if (inputHasFocus()) return false; + if (deg) selection.rotate(0, 0, -deg); + if (move > 0) selection.move(-move, 0, 0); + evt.preventDefault(); + break; + case 39: // right arrow + if (inputHasFocus()) return false; + if (deg) selection.rotate(0, 0, deg); + if (move > 0) selection.move(move, 0, 0); + evt.preventDefault(); + break; + case 38: // up arrow + if (inputHasFocus()) return false; + if (evt.metaKey) return api.show.layer(api.var.layer_at+1); + if (deg) selection.rotate(deg, 0, 0); + if (move > 0) selection.move(0, move, 0); + evt.preventDefault(); + break; + case 40: // down arrow + if (inputHasFocus()) return false; + if (evt.metaKey) return api.show.layer(api.var.layer_at-1); + if (deg) selection.rotate(-deg, 0, 0); + if (move > 0) selection.move(0, -move, 0); + evt.preventDefault(); + break; + case 65: // 'a' for select all + if (evt.metaKey || evt.ctrlKey) { + if (inputHasFocus()) return false; + evt.preventDefault(); + platform.deselect(); + platform.select_all(); + } + break; + case 83: // 's' for save workspace + if (evt.ctrlKey) { + evt.preventDefault(); + api.conf.save(); + console.log("settings saved"); + } else + if (evt.metaKey) { + evt.preventDefault(); + api.space.save(); + api.settings.sync.put(); + } + break; + case 76: // 'l' for restore workspace + if (evt.metaKey) { + evt.preventDefault(); + api.space.restore(); + } + break; + } + } + + function keyHandler(evt) { + let handled = true; + if (api.modal.visible() || inputHasFocus()) { + return false; + } + if (api.feature.on_key) { + if (api.feature.on_key({key:evt})) return; + } + for (let handler of api.feature.on_key2) { + if (handler({key:evt})) return; + } + if (evt.ctrlKey) { + switch (evt.key) { + case 'g': return api.group.merge(); + case 'u': return api.group.split(); + } + } + switch (evt.charCode) { + case cca('`'): api.show.slices(0); break; + case cca('0'): api.show.slices(api.var.layer_max); break; + case cca('1'): api.show.slices(api.var.layer_max/10); break; + case cca('2'): api.show.slices(api.var.layer_max*2/10); break; + case cca('3'): api.show.slices(api.var.layer_max*3/10); break; + case cca('4'): api.show.slices(api.var.layer_max*4/10); break; + case cca('5'): api.show.slices(api.var.layer_max*5/10); break; + case cca('6'): api.show.slices(api.var.layer_max*6/10); break; + case cca('7'): api.show.slices(api.var.layer_max*7/10); break; + case cca('8'): api.show.slices(api.var.layer_max*8/10); break; + case cca('9'): api.show.slices(api.var.layer_max*9/10); break; + case cca('?'): + api.help.show(); + break; + case cca('Z'): // reset stored state + uc.confirm('clear all settings and preferences?').then(yes => { + if (yes) { + sdb.clear(); + WIN.location.reload(); + } + }); + break; + case cca('C'): // refresh catalog + catalog.refresh(); + break; + case cca('i'): // file import + api.event.import(); + break; + case cca('S'): // slice + case cca('s'): // slice + api.function.slice(); + break; + case cca('P'): // prepare + case cca('p'): // prepare + if (api.mode.get() !== 'SLA') { + // hidden in SLA mode + api.function.print(); + } + break; + case cca('X'): // export + case cca('x'): // export + api.function.export(); + break; + case cca('g'): // CAM animate + api.function.animate(); + break; + case cca('O'): // manual rotation + rotateInputSelection(); + break; + case cca('r'): // recent files + api.modal.show('files'); + break; + case cca('q'): // preferences + api.modal.show('prefs'); + break; + case cca('l'): // device + settingsLoad(); + break; + case cca('e'): // device + api.show.devices(); + break; + case cca('o'): // tools + api.show.tools(); + break; + case cca('c'): // local devices + api.show.local(); + break; + case cca('v'): // toggle single slice view mode + if (api.view.get() === VIEWS.ARRANGE) { + api.space.set_focus(selection.widgets()); + } + if (api.var.layer_hi == api.var.layer_lo) { + api.var.layer_lo = 0; + } else { + api.var.layer_lo = api.var.layer_hi; + } + api.show.slices(); + break; + case cca('d'): // duplicate object + duplicateSelection(); + break; + case cca('m'): // mirror object + mirrorSelection(); + break; + case cca('a'): + if (api.view.get() === VIEWS.ARRANGE) { + // auto arrange items on platform + platform.layout(); + if (!api.conf.get().controller.spaceRandoX) { + api.space.set_focus(selection.widgets()); + } + } else { + // go to arrange view + api.view.set(VIEWS.ARRANGE); + } + break; + default: + api.event.emit('keypress', evt); + handled = false; + break; + } + if (handled) { + evt.preventDefault(); + evt.stopPropagation(); + } + return false; + } + + function duplicateSelection() { + selection.duplicate(); + } + + function mirrorSelection() { + selection.mirror(); + } + + function keys(o) { + let key, list = []; + for (key in o) { if (o.hasOwnProperty(key)) list.push(key) } + return list.sort(); + } + + function clearSelected(children) { + for (let i=0; i { + coord = (coord || '').split(','); + let prod = Math.PI / 180, + x = parseFloat(coord[0] || 0.0) * prod, + y = parseFloat(coord[1] || 0.0) * prod, + z = parseFloat(coord[2] || 0.0) * prod; + selection.rotate(x, y, z); + }); + } + + function positionSelection() { + if (selection.meshes().length === 0) { + api.show.alert("select object to position"); + return; + } + let current = settings(), + { device, process} = current, + center = process.ctOriginCenter || process.camOriginCenter || device.bedRound || device.originCenter, + bounds = boundsSelection(); + + api.uc.prompt("Enter X,Y coordinates for selection","").then(coord => { + coord = (coord || '').split(','); + let x = parseFloat(coord[0] || 0.0), + y = parseFloat(coord[1] || 0.0), + z = parseFloat(coord[2] || 0.0); + + if (!center) { + x = x - device.bedWidth/2 + (bounds.max.x - bounds.min.x)/2; + y = y - device.bedDepth/2 + (bounds.max.y - bounds.min.y)/2 + } + + selection.move(x, y, z, true); + }); + } + + function deviceExport(exp, name) { + name = (name || "device") + .toLowerCase() + .replace(/ /g,'_') + .replace(/\./g,'_'); + uc.prompt("Export Device Filename", name).then(name => { + if (name) { + api.util.download(exp, `${name}.km`); + } + }); + } + + function objectsExport(format = "stl") { + // return selection.export(); + uc.confirm("Export Filename", {ok:true, cancel: false}, `selected.${format}`).then(name => { + if (!name) return; + if (name.toLowerCase().indexOf(`.${format}`) < 0) { + name = `${name}.${format}`; + } + api.util.download(selection.export(format), name); + }); + } + + function workspaceNew() { + uc.confirm("Clear Workspace?", {ok:true, cancel: false}).then(value => { + if (value === true) { + let proc = api.settings.proc(); + proc.ops && (proc.ops.length = 0); + proc.op2 && (proc.op2.length = 0); + api.platform.clear(); + } + }); + } + + function profileExport() { + const opt = {pre: [ + "
", + "

Workspace Export

", + " ", + " ", + "  Exclude meshes", + "
" + ]}; + let suggestion = "workspace"; + let file = api.widgets.all()[0]?.meta.file || ''; + if (file) { + suggestion = `${suggestion}_${file.split('.')[0]}`.replaceAll(' ','_'); + }; + uc.confirm("Filename", {ok:true, cancel: false}, suggestion, opt).then(name => { + if (!name) return; + + let work = !$('excwork').checked; + let json = api.conf.export({work, clear:true}); + + kiri.client.zip([ + {name:"workspace.json", data:JSON.stringify(json)} + ], progress => { + api.show.progress(progress.percent/100, "compressing workspace"); + }, output => { + api.show.progress(0); + api.util.download(output, `${name}.kmz`); + }); + }); + } + + function settingsSave(ev, name) { + if (ev) { + ev.stopPropagation(); + ev.preventDefault(); + } + + api.dialog.hide(); + let mode = api.mode.get(), + s = settings(), + def = "default", + cp = s.process, + pl = s.sproc[mode], + lp = s.cproc[mode], + saveAs = (name) => { + if (!name) { + return; + } + let np = pl[name] = {}; + cp.processName = name; + pl[name] = Object.clone(cp); + for (let k in cp) { + if (!cp.hasOwnProperty(k)) continue; + np[k] = cp[k]; + } + s.cproc[mode] = name; + s.devproc[s.device.deviceName] = name; + api.conf.save(); + api.conf.update(); + api.event.settings(); + sync_put(); + }; + + if (name) { + saveAs(name); + } else { + uc.prompt("Save Settings As", cp ? lp || def : def).then(saveAs); + } + } + + function settingsLoad() { + api.conf.show(); + } + + function updateDeviceSize() { + api.conf.update(); + platform.update_size(); + platform.update_origin(); + } + + async function sync_put() { + await api.settings.sync.put(); + } + + function dragOverHandler(evt) { + evt.stopPropagation(); + evt.preventDefault(); + + // prevent drop actions when a dialog is open + if (api.modal.visible()) { + return; + } + + evt.dataTransfer.dropEffect = 'copy'; + let oldcolor = space.platform.setColor(0x00ff00); + if (oldcolor !== 0x00ff00) platformColor = oldcolor; + } + + function dragLeave() { + space.platform.setColor(platformColor); + } + + function dropHandler(evt) { + evt.stopPropagation(); + evt.preventDefault(); + + // prevent drop actions when a dialog is open + if (api.modal.visible()) { + return; + } + + space.platform.setColor(platformColor); + + let files = evt.dataTransfer.files; + + switch (api.feature.drop_group) { + case true: + return api.platform.load_files(files, []); + case false: + return api.platform.load_files(files, undefined); + } + + if (files.length === 1) { + api.platform.load_files(files); + } else if (files.length > 1) { + uc.confirm(`group ${files.length} files?`).then(yes => { + api.platform.load_files(files, yes ? [] : undefined); + }); + } + } + + function loadCatalogFile(e) { + api.widgets.load(e.target.getAttribute('load'), function(widget) { + platform.add(widget); + api.dialog.hide(); + }); + } + + function updateCatalog(files) { + let table = ui.catalogList, + list = []; + table.innerHTML = ''; + for (let name in files) { + list.push({n:name, ln:name.toLowerCase(), v:files[name].vertices, t:files[name].updated}); + } + list.sort(function(a,b) { + return a.ln < b.ln ? -1 : 1; + }); + for (let i=0; i { + api.uc.prompt(`rename file`, short).then(newname => { + if (newname && newname !== short) { + catalog.rename(name, `${newname}${ext}`, then => { + api.modal.show('files'); + }); + } + }); + }; + + load.setAttribute('load', name); + load.setAttribute('title', `file: ${name}\nvertices: ${file.v}\ndate: ${date}`); + load.onclick = loadCatalogFile; + load.appendChild(DOC.createTextNode(short)); + + del.setAttribute('del', name); + del.setAttribute('title', "remove '"+name+"'"); + del.onclick = () => { catalog.deleteFile(name) }; + del.innerHTML = ''; + + size.setAttribute("disabled", true); + size.setAttribute("class", "label"); + size.appendChild(DOC.createTextNode(base.util.comma(file.v))); + + row.setAttribute("class", "f-row a-center"); + row.appendChild(renm); + row.appendChild(load); + row.appendChild(size); + row.appendChild(del); + table.appendChild(row); + } + } + + function isMultiHead() { + let dev = api.conf.get().device; + return isNotBelt() && dev.extruders && dev.extruders.length > 1; + } + + function isBelt() { + return api.device.isBelt(); + } + + function isNotBelt() { + return !isBelt(); + } + + // MAIN INITIALIZATION FUNCTION + function init_one() { + let { event, conf, view, show } = api, + { bound, toInt, toFloat } = uc, + { newBlank, newButton, newBoolean, newGroup, newInput } = uc, + { newSelect, newLabel, newValue, newRow, newGCode, newDiv } = uc; + + event.emit('init.one'); + + // ensure we have settings from last session + conf.restore(); + + let container = $('container'), + gcode = $('dev-gcode'), + tracker = $('tracker'), + controller = settings().controller; + + WIN.addEventListener("resize", () => { + event.emit('resize'); + }); + + event.on('resize', () => { + if (WIN.innerHeight < 800) { + ui.modalBox.classList.add('mh85'); + } else { + ui.modalBox.classList.remove('mh85'); + } + view.update_slider(); + }); + + space.sky.showGrid(false); + space.sky.setColor(controller.dark ? 0 : 0xffffff); + space.setAntiAlias(controller.antiAlias); + space.init(container, function (delta) { + let vars = api.var; + if (vars.layer_max === 0 || !delta) return; + if (controller.reverseZoom) delta = -delta; + let same = vars.layer_hi === vars.layer_lo; + let track = vars.layer_lo > 0; + if (delta > 0) { + vars.layer_hi = Math.max(same ? 0 : vars.layer_lo, vars.layer_hi - 1); + if (track) { + vars.layer_lo = Math.max(0, vars.layer_lo - 1); + } + } else if (delta < 0) { + vars.layer_hi = Math.min(vars.layer_max, vars.layer_hi + 1); + if (track) { + vars.layer_lo = Math.min(vars.layer_hi, vars.layer_lo + 1); + } + } + if (same) { + vars.layer_lo = vars.layer_hi; + } + view.update_slider(); + show.slices(); + }, controller.ortho); + space.platform.onMove(conf.save); + space.platform.setRound(true); + space.useDefaultKeys(api.feature.on_key === undefined || api.feature.on_key_defaults); + updateDrawer(); + + // api augmentation with local functions + api.device.export = deviceExport; + let anim = ui.anim = {}; + + Object.assign(ui, { + tracker: tracker, + container: container, + + alert: { + dialog: $('alert-area'), + text: $('alert-text') + }, + func: { + slice: $('act-slice'), + preview: $('act-preview'), + animate: $('act-animate'), + export: $('act-export') + }, + label: { + slice: $('label-slice'), + preview: $('label-preview'), + animate: $('label-animate'), + export: $('label-export'), + }, + acct: { + help: $('app-help'), + don8: $('app-don8'), + mesh: $('app-mesh'), + export: $('app-export') + }, + dev: { + header: $('dev-header'), + search: $('dev-search'), + filter: $('dev-filter') + }, + mesh: { + name: $('mesh-name'), + points: $('mesh-points'), + faces: $('mesh-faces'), + }, + + stats: { + fps: $('fps'), + rms: $('rms'), + div: $('stats'), + rnfo: $('rnfo'), + }, + + load: $('load-file'), + speeds: $('speeds'), + speedbar: $('speedbar'), + context: $('context-menu'), + + ltsetup: $('lt-setup'), + ltfile: $('lt-file'), + ltview: $('lt-view'), + ltact: $('lt-start'), + edit: $('lt-tools'), + nozzle: $('menu-nozzle'), + + modal: $('modal'), + modalBox: $('modal-box'), + modals: { + help: $('mod-help'), + setup: $('mod-setup'), + prefs: $('mod-prefs'), + files: $('mod-files'), + saves: $('mod-saves'), + tools: $('mod-tools'), + xany: $('mod-x-any'), + xsla: $('mod-x-sla'), + xlaser: $('mod-x-laser'), + local: $('mod-local'), + don8: $('mod-don8'), + any: $('mod-any'), + }, + + catalogBody: $('catalogBody'), + catalogList: $('catalogList'), + + devices: $('devices'), + deviceAdd: $('device-add'), + deviceDelete: $('device-del'), + deviceRename: $('device-ren'), + deviceExport: $('device-exp'), + deviceSave: $('device-save'), + + toolsSave: $('tools-save'), + toolsClose: $('tools-close'), + toolsImport: $('tools-import'), + toolsExport: $('tools-export'), + toolSelect: $('tool-select'), + toolAdd: $('tool-add'), + toolCopy: $('tool-dup'), + toolDelete: $('tool-del'), + toolType: $('tool-type'), + toolName: $('tool-name'), + toolNum: $('tool-num'), + toolFluteDiam: $('tool-fdiam'), + toolFluteLen: $('tool-flen'), + toolShaftDiam: $('tool-sdiam'), + toolShaftLen: $('tool-slen'), + toolTaperAngle: $('tool-tangle'), + toolTaperTip: $('tool-ttip'), + toolMetric: $('tool-metric'), + + setMenu: $('set-menu'), + settings: $('settings'), + settingsBody: $('settingsBody'), + settingsList: $('settingsList'), + + slider: $('slider'), + sliderMax: $('slider-max'), + sliderMin: $('slider-zero'), + sliderLo: $('slider-lo'), + sliderMid: $('slider-mid'), + sliderHi: $('slider-hi'), + sliderHold: $('slider-hold'), + sliderRange: $('slider-center'), + + loading: $('progress').style, + progress: $('progbar').style, + prostatus: $('progtxt'), + + selection: $('selection'), + sizeX: $('size_x'), + sizeY: $('size_y'), + sizeZ: $('size_z'), + scaleX: $('scale_x'), + scaleY: $('scale_y'), + scaleZ: $('scale_z'), + lockX: $('lock_x'), + lockY: $('lock_y'), + lockZ: $('lock_z'), + stock: $('stock'), + stockWidth: $('stock-width'), + stockDepth: $('stock-width'), + stockHeight: $('stock-width'), + + /** CAM Animation Bar */ + + _____: { + _____: newDiv({ addto: $('layer-animate'), set:true }), + row: newRow([ + anim.replay = newButton(null,"anim.replay",{icon:'',title:"restart"}), + anim.play = newButton(null,"anim.play",{icon:'',title:"play"}), + anim.pause = newButton(null,"anim.pause",{icon:'',title:"pause"}), + anim.step = newButton(null,"anim.step",{icon:'',title:"single step"}), + anim.speed = newButton(null,"anim.fast",{icon:'',title:"toggle speed"}), + anim.labspd = newValue(3, {class:"center padleft"}), + anim.labx = newLabel("X", {class:"padleft"}), + anim.valx = newValue(7, {class:"center"}), + anim.laby = newLabel("Y", {class:"padleft"}), + anim.valy = newValue(7, {class:"center"}), + anim.labz = newLabel("Z", {class:"padleft"}), + anim.valz = newValue(6, {class:"center"}), + anim.laba = newLabel("A", {class:"padleft hide"}), + anim.vala = newValue(6, {class:"center hide"}), + anim.labpro = newLabel("%", {class:"padleft"}), + anim.progress = newValue(5, {class:"center"}), + anim.trans = newButton(null,"anim.trans",{icon:'',title:"transparency",class:"padleft"}), + anim.model = newButton(null,"anim.model",{icon:'',title:"show model"}), + anim.shade = newButton(null,"anim.stock",{icon:'',title:"stock box"}), + ]) + }, + + /** Device Browser / Editor */ + + _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true }), + device: newGroup(LANG.dv_gr_dev, null, {group:"ddev", inline, class:"noshow"}), + + _____: newGroup("workspace", null, {group:"dext", inline}), + bedWidth: newInput('X (width)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), + bedDepth: newInput('Y (depth)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), + maxHeight: newInput('Z (height)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), + resolutionX: newInput(LANG.dv_rezx_s, {title:LANG.dv_rezx_l, convert:toInt, size:6, modes:SLA}), + resolutionY: newInput(LANG.dv_rezy_s, {title:LANG.dv_rezy_l, convert:toInt, size:6, modes:SLA}), + _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:NO_WEDM }), + _____: newGroup("firmware", null, {group:"dext", inline, modes:NO_WEDM}), + fwRetract: newBoolean(LANG.dv_retr_s, onBooleanClick, {title:LANG.dv_retr_l, modes:FDM}), + deviceOrigin: newBoolean(LANG.dv_orgc_s, onBooleanClick, {title:LANG.dv_orgc_l, modes:FDM_LZN, show:() => !ui.deviceRound.checked}), + deviceRound: newBoolean(LANG.dv_bedc_s, onBooleanClick, {title:LANG.dv_bedc_l, modes:FDM, trigger, show:isNotBelt}), + deviceBelt: newBoolean(LANG.dv_belt_s, onBooleanClick, {title:LANG.dv_belt_l, modes:FDM, trigger, show:() => !ui.deviceRound.checked}), + separator: newBlank({class:"pop-sep", modes:FDM, driven}), + spindleMax: newInput(LANG.dv_spmx_s, {title:LANG.dv_spmx_l, convert:toInt, size:5, modes:CAM, trigger}), + deviceZMax: newInput(LANG.dv_zmax_s, {title:LANG.dv_zmax_l, convert:toInt, size:5, modes:FDM}), + gcodeTime: newInput(LANG.dv_time_s, {title:LANG.dv_time_l, convert:toFloat, size:5, modes:FDM}), + _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:FDM }), + extruder: newGroup(LANG.dv_gr_ext, null, {group:"dext", inline}), + extFilament: newInput(LANG.dv_fila_s, {title:LANG.dv_fila_l, convert:toFloat, modes:FDM}), + extNozzle: newInput(LANG.dv_nozl_s, {title:LANG.dv_nozl_l, convert:toFloat, modes:FDM}), + extOffsetX: newInput(LANG.dv_exox_s, {title:LANG.dv_exox_l, convert:toFloat, modes:FDM}), + extOffsetY: newInput(LANG.dv_exoy_s, {title:LANG.dv_exoy_l, convert:toFloat, modes:FDM}), + extPad: newBlank({class:"grow", modes:FDM}), + separator: newBlank({class:"pop-sep", modes:FDM, driven}), + extActions: newRow([ + ui.extPrev = newButton(undefined, undefined, {icon:''}), + ui.extAdd = newButton(undefined, undefined, {icon:''}), + ui.extDel = newButton(undefined, undefined, {icon:''}), + ui.extNext = newButton(undefined, undefined, {icon:''}) + ], {class:"dev-buttons ext-buttons var-row", modes:FDM}), + _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:CAM_LZR }), + _____: newGroup(LANG.dv_gr_out, null, {group:"dgco", inline}), + gcodeStrip: newBoolean(LANG.dv_strc_s, onBooleanClick, {title:LANG.dv_strc_l, modes:CAM}), + gcodeSpace: newBoolean(LANG.dv_tksp_s, onBooleanClick, {title:LANG.dv_tksp_l, modes:CAM_LZR}), + laserMaxPower: newInput(LANG.ou_maxp_s, {title:LANG.ou_maxp_l, modes:LASER, size:7, text:true}), + useLaser: newBoolean(LANG.dv_lazr_s, onBooleanClick, {title:LANG.dv_lazr_l, modes:CAM}), + gcodeFExt: newInput(LANG.dv_fext_s, {title:LANG.dv_fext_l, modes:CAM_LZR, size:7, text:true}), + gcodeEd: newGroup(LANG.dv_gr_gco, $('dg'), {group:"dgcp", inline, modes:GCODE}), + gcodeMacros: newRow([ + (ui.gcodePre = newGCode(LANG.dv_head_s, {title:LANG.dv_head_l, modes:GCODE, area:gcode})).button, + (ui.gcodePost = newGCode(LANG.dv_foot_s, {title:LANG.dv_foot_l, modes:GCODE, area:gcode})).button, + (ui.gcodeLayer = newGCode(LANG.dv_layr_s, {title:LANG.dv_layr_l, modes:FDM, area:gcode})).button, + (ui.gcodeTrack = newGCode(LANG.dv_prog_s, {title:LANG.dv_prog_l, modes:FDM, area:gcode})).button, + (ui.gcodeFan = newGCode(LANG.dv_fanp_s, {title:LANG.dv_fanp_l, modes:FDM, area:gcode})).button, + (ui.gcodeFeature = newGCode(LANG.dv_feat_s, {title:LANG.dv_feat_l, modes:FDM, area:gcode})).button, + (ui.gcodeLaserOn = newGCode(LANG.dv_lzon_s, {title:LANG.dv_lzon_l, modes:LASER, area:gcode})).button, + (ui.gcodeLaserOff = newGCode(LANG.dv_lzof_s, {title:LANG.dv_lzof_l, modes:LASER, area:gcode})).button, + (ui.gcodeWaterOn = newGCode(LANG.dv_waon_s, {title:LANG.dv_waon_l, modes:WJET, area:gcode})).button, + (ui.gcodeWaterOff = newGCode(LANG.dv_waof_s, {title:LANG.dv_waof_l, modes:WJET, area:gcode})).button, + (ui.gcodeKnifeDn = newGCode(LANG.dv_dkon_s, {title:LANG.dv_dkon_l, modes:DRAG, area:gcode})).button, + (ui.gcodeKnifeUp = newGCode(LANG.dv_dkof_s, {title:LANG.dv_dkof_l, modes:DRAG, area:gcode})).button, + (ui.gcodeChange = newGCode(LANG.dv_tool_s, {title:LANG.dv_tool_l, modes:FDM_CAM, area:gcode})).button, + (ui.gcodeDwell = newGCode(LANG.dv_dwll_s, {title:LANG.dv_dwll_l, modes:CAM, area:gcode})).button, + (ui.gcodeSpindle = newGCode(LANG.dv_sspd_s, {title:LANG.dv_sspd_l, modes:CAM, area:gcode, show:() => ui.spindleMax.value > 0})).button + ], {class:"ext-buttons f-row gcode-macros"}), + + /** Preferences Menu */ + + _____: newGroup(LANG.op_menu, $('prefs-gen1'), {inline}), + antiAlias: newBoolean(LANG.op_anta_s, booleanSave, {title:LANG.op_anta_l}), + reverseZoom: newBoolean(LANG.op_invr_s, booleanSave, {title:LANG.op_invr_l}), + ortho: newBoolean(LANG.op_orth_s, booleanSave, {title:LANG.op_orth_l}), + dark: newBoolean(LANG.op_dark_s, booleanSave, {title:LANG.op_dark_l}), + drawer: newBoolean('slide out', booleanSave, {title:'slide out settings drawer'}), + scrolls: newBoolean('scrollbars', booleanSave, {title:'show scrollbars'}), + devel: newBoolean(LANG.op_devl_s, booleanSave, {title:LANG.op_devl_l}), + _____: newGroup(LANG.op_disp, $('prefs-gen2'), {inline}), + showOrigin: newBoolean(LANG.op_shor_s, booleanSave, {title:LANG.op_shor_l}), + showRulers: newBoolean(LANG.op_shru_s, booleanSave, {title:LANG.op_shru_l}), + showSpeeds: newBoolean(LANG.op_sped_s, speedSave, {title:LANG.op_sped_l}), + shiny: newBoolean(LANG.op_shny_s, booleanSave, {title:LANG.op_shny_l, modes:FDM}), + lineType: newSelect(LANG.op_line_s, {title: LANG.op_line_l, action: lineTypeSave, modes:FDM}, "linetype"), + animesh: newSelect(LANG.op_anim_s, {title: LANG.op_anim_l, action: aniMeshSave, modes:CAM}, "animesh"), + units: newSelect(LANG.op_unit_s, {title: LANG.op_unit_l, action: unitsSave, modes:CAM}, "units"), + edgeangle: newInput(LANG.op_spoa_s, {title:LANG.op_spoa_l, convert:toFloat, size:3}), + _____: newGroup(LANG.lo_menu, $('prefs-lay'), {inline}), + autoSave: newBoolean(LANG.op_save_s, booleanSave, {title:LANG.op_save_l}), + autoLayout: newBoolean(LANG.op_auto_s, booleanSave, {title:LANG.op_auto_l}), + freeLayout: newBoolean(LANG.op_free_s, booleanSave, {title:LANG.op_free_l}), + spaceRandoX: newBoolean(LANG.op_spcx_s, booleanSave, {title:LANG.op_spcx_l, show:isBelt}), + spaceLayout: newInput(LANG.op_spcr_s, {title:LANG.op_spcr_l, convert:toFloat, size:3, units}), + _____: newGroup(LANG.xp_menu, $('prefs-xpo'), {inline: true}), + exportLocal: newBoolean(`Grid:Local`, booleanSave, {title:LANG.op_exgl_l}), + exportGhost: newBoolean(`Grid:Host`, booleanSave, {title:LANG.op_exgh_l}), + exportOcto: newBoolean(`OctoPrint`, booleanSave, {title:LANG.op_exop_l}), + exportThumb: newBoolean(`Thumbnail`, booleanSave, {modes:FDM}), + exportPreview: newBoolean(`Code Preview`, booleanSave), + _____: newGroup(LANG.pt_menu, $('prefs-prt'), {inline}), + detail: newSelect(LANG.pt_qual_s, {title: LANG.pt_qual_l, action: detailSave}, "detail"), + healMesh: newBoolean(LANG.pt_heal_s, booleanSave, {title: LANG.pt_heal_l}), + threaded: newBoolean(LANG.pt_thrd_s, booleanSave, {title: LANG.pt_thrd_l, modes:THREED}), + assembly: newBoolean(LANG.pt_assy_s, booleanSave, {title: LANG.pt_assy_l, modes:THREED}), + + prefadd: uc.checkpoint($('prefs-add')), + + /** FDM Settings */ + + _____: newGroup(LANG.sl_menu, $('fdm-layers'), { modes:FDM, driven, hideable, separator, group:"fdm-layers" }), + sliceHeight: newInput(LANG.sl_lahi_s, { title:LANG.sl_lahi_l, convert:toFloat }), + sliceMinHeight: newInput(LANG.ad_minl_s, { title:LANG.ad_minl_l, convert:toFloat, bound:bound(0,3.0), show:() => ui.sliceAdaptive.checked }), + sliceTopLayers: newInput(LANG.sl_ltop_s, { title:LANG.sl_ltop_l, convert:toInt }), + sliceBottomLayers: newInput(LANG.sl_lbot_s, { title:LANG.sl_lbot_l, convert:toInt }), + separator: newBlank({ class:"set-sep", driven }), + sliceAdaptive: newBoolean(LANG.ad_adap_s, onBooleanClick, { title: LANG.ad_adap_l }), + _____: newGroup(LANG.sw_menu, $('fdm-walls'), { modes:FDM, driven, hideable, separator, group:"fdm-walls" }), + sliceShells: newInput(LANG.sl_shel_s, { title:LANG.sl_shel_l, convert:toFloat }), + sliceLineWidth: newInput(LANG.sl_line_s, { title:LANG.sl_line_l, convert:toFloat, bound:bound(0,5) }), + separator: newBlank({ class:"set-sep", driven }), + sliceShellOrder: newSelect(LANG.sl_ordr_s, { title:LANG.sl_ordr_l}, "shell"), + sliceDetectThin: newSelect(LANG.ad_thin_s, { title: LANG.ad_thin_l, action: thinWallSave }, "thin"), + outputAlternating: newBoolean(LANG.ad_altr_s, onBooleanClick, {title:LANG.ad_altr_l}), + sliceZInterleave: newBoolean(LANG.ad_zint_s, onBooleanClick, {title:LANG.ad_zint_l, show:zIntShow}), + _____: newGroup(LANG.fi_menu, $('fdm-fill'), { modes:FDM, driven, hideable, separator, group:"fdm-fill" }), + sliceFillType: newSelect(LANG.fi_type, {trigger}, "infill"), + sliceFillSparse: newInput(LANG.fi_pcnt_s, {title:LANG.fi_pcnt_l, convert:toFloat, bound:bound(0.0,1.0), show:hasInfill}), + sliceFillRepeat: newInput(LANG.fi_rept_s, {title:LANG.fi_rept_l, convert:toInt, bound:bound(1,10), show:fillIsLinear}), + sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,2.0), show:hasInfill}), + separator: newBlank({ class:"set-sep", driven }), + sliceFillRate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt, bound:bound(0,500)}), + sliceSolidRate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt, bound:bound(0,500)}), + separator: newBlank({ class:"set-sep", driven }), + sliceFillGrow: newInput(LANG.fi_grow_s, {title:LANG.fi_grow_l, convert:toFloat}), + sliceFillAngle: newInput(LANG.fi_angl_s, {title:LANG.fi_angl_l, convert:toFloat}), + _____: newGroup(LANG.fh_menu, $('fdm-heat'), { modes:FDM, driven, hideable, separator, group:"fdm-heat" }), + outputTemp: newInput(LANG.ou_nozl_s, {title:LANG.ou_nozl_l, convert:toInt}), + outputBedTemp: newInput(LANG.ou_bedd_s, {title:LANG.ou_bedd_l, convert:toInt}), + _____: newGroup(LANG.fc_menu, $('fdm-cool'), { modes:FDM, driven, hideable, separator, group:"fdm-cool" }), + outputFanLayer: newInput(LANG.ou_fanl_s, { title:LANG.ou_fanl_l, convert:toInt, bound:bound(0,255) }), + outputFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255)}), + _____: newGroup(LANG.sp_menu, $('fdm-support'), { modes:FDM, driven, hideable, separator, group:"fdm-supp" }), + sliceSupportNozzle: newSelect(LANG.sp_nozl_s, {title:LANG.sp_nozl_l, show:isMultiHead}, "extruders"), + sliceSupportDensity: newInput(LANG.sp_dens_s, {title:LANG.sp_dens_l, convert:toFloat, bound:bound(0.0,1.0)}), + sliceSupportSize: newInput(LANG.sp_size_s, {title:LANG.sp_size_l, convert:toFloat, bound:bound(1.0,200.0)}), + sliceSupportOffset: newInput(LANG.sp_offs_s, {title:LANG.sp_offs_l, convert:toFloat, bound:bound(0.0,200.0)}), + sliceSupportGap: newInput(LANG.sp_gaps_s, {title:LANG.sp_gaps_l, convert:toInt, bound:bound(0,5)}), + sliceSupportArea: newInput(LANG.sp_area_s, {title:LANG.sp_area_l, convert:toFloat, bound:bound(0.0,200.0)}), + sliceSupportExtra: newInput(LANG.sp_xpnd_s, {title:LANG.sp_xpnd_l, convert:toFloat, bound:bound(0.0,10.0)}), + sliceSupportGrow: newInput(LANG.sp_grow_s, {title:LANG.sp_grow_l, convert:toFloat, bound:bound(0.0,10.0)}), + sliceSupportAngle: newInput(LANG.sp_angl_s, {title:LANG.sp_angl_l, convert:toFloat, bound:bound(0.0,90.0)}), + sliceSupportSpan: newInput(LANG.sp_span_s, {title:LANG.sp_span_l, convert:toFloat, bound:bound(0.0,200.0), show:() => ui.sliceSupportEnable.checked }), + separator: newBlank({ class:"set-sep", driven }), + sliceSupportEnable: newBoolean(LANG.sp_auto_s, onBooleanClick, {title:LANG.sp_auto_l, show:isNotBelt}), + sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l}), + separator: newBlank({ class:"set-sep", driven }), + sliceSupportGen: newRow([ + ui.ssaGen = newButton(LANG.sp_detect, onButtonClick, {class: "f-col grow a-center"}) + ], { modes: FDM, class: "ext-buttons f-row grow" }), + separator: newBlank({ class:"set-sep", driven }), + sliceSupportManual: newRow([ + (ui.ssmAdd = newButton(undefined, onButtonClick, {icon:''})), + (ui.ssmDun = newButton(undefined, onButtonClick, {icon:''})), + (ui.ssmClr = newButton(undefined, onButtonClick, {icon:''})) + ], {class:"ext-buttons f-row"}), + _____: newGroup(LANG.fl_menu, $('fdm-base'), { modes:FDM, driven, hideable, separator, group:"fdm-base" }), + firstSliceHeight: newInput(LANG.fl_lahi_s, {title:LANG.fl_lahi_l, convert:toFloat, show:isNotBelt}), + firstLayerNozzleTemp:newInput(LANG.fl_nozl_s, {title:LANG.fl_nozl_l, convert:toInt, show:isNotBelt}), + firstLayerBedTemp: newInput(LANG.fl_bedd_s, {title:LANG.fl_bedd_l, convert:toInt, show:isNotBelt}), + separator: newBlank({ class:"set-sep", driven }), + firstLayerFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255), show:isBelt}), + firstLayerYOffset: newInput(LANG.fl_zoff_s, {title:LANG.fl_zoff_l, convert:toFloat, show:isBelt}), + firstLayerFlatten: newInput(LANG.fl_flat_s, {title:LANG.fl_flat_l, convert:toFloat, show:isBelt}), + firstLayerRate: newInput(LANG.fl_rate_s, {title:LANG.fl_rate_l, convert:toFloat}), + firstLayerFillRate: newInput(LANG.fl_frat_s, {title:LANG.fl_frat_l, convert:toFloat, show:isNotBelt}), + separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), + firstLayerLineMult: newInput(LANG.fl_sfac_s, {title:LANG.fl_sfac_l, convert:toFloat, bound:bound(0.5,2), show:isNotBelt}), + firstLayerPrintMult: newInput(LANG.fl_mult_s, {title:LANG.fl_mult_l, convert:toFloat}), + separator: newBlank({ class:"set-sep", driven, show:isBelt }), + firstLayerBrim: newInput(LANG.fl_brim_s, {title:LANG.fl_brim_l, convert:toInt, show:isBelt}), + firstLayerBrimIn: newInput(LANG.fl_brin_s, {title:LANG.fl_brin_l, convert:toInt, show:isBelt}), + firstLayerBrimTrig: newInput(LANG.fl_brmn_s, {title:LANG.fl_brmn_l, convert:toInt, show:isBelt}), + firstLayerBrimGap: newInput(LANG.fl_brgp_s, {title:LANG.fl_brgp_l, convert:toFloat, show:isBelt}), + separator: newBlank({ class:"set-sep", driven, show:isBelt }), + firstLayerBeltLead: newInput(LANG.fl_bled_s, {title:LANG.fl_bled_l, convert:toFloat, show:isBelt}), + firstLayerBeltBump: newInput(LANG.fl_blmp_s, {title:LANG.fl_blmp_l, convert:toFloat, bound:bound(0, 10), show:isBelt}), + separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), + outputBrimCount: newInput(LANG.fl_skrt_s, {title:LANG.fl_skrt_l, convert:toInt, show:isNotBelt}), + outputBrimOffset: newInput(LANG.fl_skro_s, {title:LANG.fl_skro_l, convert:toFloat, show:isNotBelt}), + outputRaftSpacing: newInput(LANG.fr_spac_s, {title:LANG.fr_spac_l, convert:toFloat, bound:bound(0.0,3.0), show: () => ui.outputRaft.checked && isNotBelt() }), + separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), + outputRaft: newBoolean(LANG.fr_nabl_s, onBooleanClick, {title:LANG.fr_nabl_l, trigger, show:() => isNotBelt()}), + outputDraftShield: newBoolean(LANG.fr_draf_s, onBooleanClick, {title:LANG.fr_draf_l, trigger, show:() => isNotBelt()}), + _____: newGroup(LANG.ou_menu, $('fdm-output'), { modes:FDM, driven, hideable, separator, group:"fdm-out" }), + outputFeedrate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt}), + outputFinishrate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt}), + outputSeekrate: newInput(LANG.ou_move_s, {title:LANG.ou_move_l, convert:toInt}), + separator: newBlank({ class:"set-sep", driven }), + outputShellMult: newInput(LANG.ou_shml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), + outputFillMult: newInput(LANG.ou_flml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), + outputSparseMult: newInput(LANG.ou_spml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), + separator: newBlank({ class:"set-sep", driven }), + outputRetractDist: newInput(LANG.ad_rdst_s, {title:LANG.ad_rdst_l, convert:toFloat}), + outputRetractSpeed: newInput(LANG.ad_rrat_s, {title:LANG.ad_rrat_l, convert:toInt}), + outputRetractWipe: newInput(LANG.ad_wpln_s, {title:LANG.ad_wpln_l, convert:toFloat, bound:bound(0.0,10)}), + separator: newBlank({ class:"set-sep", driven }), + sliceLayerStart: newSelect(LANG.sl_strt_s, {title:LANG.sl_strt_l}, "start"), + outputLayerRetract: newBoolean(LANG.ad_lret_s, onBooleanClick, {title:LANG.ad_lret_l}), + outputAvoidGaps: newBoolean(LANG.ad_agap_s, onBooleanClick, {title:LANG.ad_agap_l}), + separator: newBlank({ class:"set-sep", driven, show:isBelt }), + outputBeltFirst: newBoolean(LANG.ad_lbir_s, onBooleanClick, {title:LANG.ad_lbir_l, show:isBelt}), + _____: newGroup(LANG.ad_menu, $('fdm-expert'), { modes:FDM, driven, hideable, separator, group:"fdm-xprt" }), + sliceAngle: newInput(LANG.sl_angl_s, {title:LANG.sl_angl_l, convert:toFloat, show:isBelt}), + outputRetractDwell: newInput(LANG.ad_rdwl_s, {title:LANG.ad_rdwl_l, convert:toInt}), + sliceSolidMinArea: newInput(LANG.ad_msol_s, {title:LANG.ad_msol_l, convert:toFloat}), + outputMinSpeed: newInput(LANG.ad_mins_s, {title:LANG.ad_mins_l, convert:toFloat, bound:bound(1,200)}), + outputShortPoly: newInput(LANG.ad_spol_s, {title:LANG.ad_spol_l, convert:toFloat, bound:bound(0,10000)}), + outputCoastDist: newInput(LANG.ad_scst_s, {title:LANG.ad_scst_l, convert:toFloat, bound:bound(0.0,10)}), + zHopDistance: newInput(LANG.ad_zhop_s, {title:LANG.ad_zhop_l, convert:toFloat, bound:bound(0,3.0)}), + arcTolerance: newInput(LANG.ad_arct_s, {title:LANG.ad_arct_l, convert:toFloat, bound:bound(0,1.0), show:() => { return isNotBelt() }}), + antiBacklash: newInput(LANG.ad_abkl_s, {title:LANG.ad_abkl_l, convert:toInt, bound:bound(0,3)}), + outputLoops: newInput(LANG.ag_loop_s, {title:LANG.ag_loop_l, convert:toInt, bound:bound(-1,1000), show:isBelt}), + outputPurgeTower: newInput(LANG.ad_purg_s, {title:LANG.ad_purg_l, convert:toInt, bound:bound(0,1000)}), + + fdmRanges: $('fdm-ranges'), + + /** CAM Settings */ + + _____: newGroup(LANG.ct_menu, $('cam-tabs'), { modes:CAM, marker:true, driven, separator }), + camTabsWidth: newInput(LANG.ct_wdth_s, {title:LANG.ct_wdth_l, convert:toFloat, bound:bound(0.005,100), units}), + camTabsHeight: newInput(LANG.ct_hght_s, {title:LANG.ct_hght_l, convert:toFloat, bound:bound(0.005,100), units}), + camTabsDepth: newInput(LANG.ct_dpth_s, {title:LANG.ct_dpth_l, convert:toFloat, bound:bound(0.005,100), units}), + separator: newBlank({ class:"set-sep", driven }), + camTabsMidline: newBoolean(LANG.ct_midl_s, onBooleanClick, {title:LANG.ct_midl_l}), + separator: newBlank({ class:"set-sep", driven }), + camTabsManual: newRow([ + (ui.tabAdd = newButton(undefined, onButtonClick, {icon:''})), + (ui.tabDun = newButton(undefined, onButtonClick, {icon:''})), + (ui.tabClr = newButton(undefined, onButtonClick, {icon:''})) + ], {class:"ext-buttons f-row"}), + _____: newGroup(LANG.cs_menu, $('cam-stock'), { modes:CAM, driven, separator }), + camStockX: newInput(LANG.cs_wdth_s, {title:LANG.cs_wdth_l, convert:toFloat, bound:bound(0,9999), units}), + camStockY: newInput(LANG.cs_dpth_s, {title:LANG.cs_dpth_l, convert:toFloat, bound:bound(0,9999), units}), + camStockZ: newInput(LANG.cs_hght_s, {title:LANG.cs_hght_l, convert:toFloat, bound:bound(0,9999), units}), + separator: newBlank({ class:"set-sep", driven }), + camStockOffset: newBoolean(LANG.cs_offs_s, onBooleanClick, {title:LANG.cs_offs_l}), + camStockClipTo: newBoolean(LANG.cs_clip_s, onBooleanClick, {title:LANG.cs_clip_l}), + camStockIndexed: newBoolean(LANG.cs_indx_s, onBooleanClick, {title:LANG.cs_indx_l}), + camStockIndexGrid: newBoolean(LANG.cs_ishg_s, onBooleanClick, {title:LANG.cs_ishg_l, show:() => ui.camStockIndexed.checked}), + _____: newGroup(LANG.cc_menu, $('cam-limits'), { modes:CAM, driven, separator }), + camZAnchor: newSelect(LANG.ou_zanc_s, {title: LANG.ou_zanc_l, action:zAnchorSave, show:() => !ui.camStockIndexed.checked}, "zanchor"), + camZOffset: newInput(LANG.ou_ztof_s, {title:LANG.ou_ztof_l, convert:toFloat, units}), + camZTop: newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:toFloat, units, trigger}), + camZBottom: newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:toFloat, units, trigger}), + camZThru: newInput(LANG.ou_ztru_s, {title:LANG.ou_ztru_l, convert:toFloat, bound:bound(0.0,100), units }), + camZClearance: newInput(LANG.ou_zclr_s, {title:LANG.ou_zclr_l, convert:toFloat, bound:bound(0.01,100), units }), + camFastFeedZ: newInput(LANG.cc_rzpd_s, {title:LANG.cc_rzpd_l, convert:toFloat, units}), + camFastFeed: newInput(LANG.cc_rapd_s, {title:LANG.cc_rapd_l, convert:toFloat, units}), + _____: newGroup(LANG.ou_menu, $('cam-output'), { modes:CAM, driven, separator, group:"cam-output" }), + camConventional: newBoolean(LANG.ou_conv_s, onBooleanClick, {title:LANG.ou_conv_l}), + camEaseDown: newBoolean(LANG.cr_ease_s, onBooleanClick, {title:LANG.cr_ease_l}), + camDepthFirst: newBoolean(LANG.ou_depf_s, onBooleanClick, {title:LANG.ou_depf_l}), + camToolInit: newBoolean(LANG.ou_toin_s, onBooleanClick, {title:LANG.ou_toin_l}), + separator: newBlank({ class:"set-sep", driven }), + camFirstZMax: newBoolean(LANG.ou_z1st_s, onBooleanClick, {title:LANG.ou_z1st_l}), + camForceZMax: newBoolean(LANG.ou_forz_s, onBooleanClick, {title:LANG.ou_forz_l}), + separator: newBlank({ class:"set-sep", driven }), + camEaseAngle: newInput(LANG.ou_eang_s, {title:LANG.ou_eang_l, convert:toFloat, bound:bound(0.1,85), show:() => ui.camEaseDown.checked}), + camFullEngage: newInput(LANG.ou_feng_s, {title:LANG.ou_feng_l, convert:toFloat, bound:bound(0.1,1.0)}), + _____: newGroup(LANG.co_menu, $('cam-origin'), { modes:CAM, driven, separator }), + camOriginTop: newBoolean(LANG.or_topp_s, onBooleanClick, {title:LANG.or_topp_l}), + camOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, {title:LANG.or_cntr_l}), + separator: newBlank({ class:"set-sep", driven }), + camOriginOffX: newInput(LANG.co_offx_s, {title:LANG.co_offx_l, convert:toFloat, units}), + camOriginOffY: newInput(LANG.co_offy_s, {title:LANG.co_offy_l, convert:toFloat, units}), + camOriginOffZ: newInput(LANG.co_offz_s, {title:LANG.co_offz_l, convert:toFloat, units}), + _____: newGroup(LANG.op_xprt_s, $('cam-expert'), { group:"cam_expert", modes:CAM, marker: false, driven, separator }), + camExpertFast: newBoolean(LANG.cx_fast_s, onBooleanClick, {title:LANG.cx_fast_l, show: () => !ui.camTrueShadow.checked }), + camTrueShadow: newBoolean(LANG.cx_true_s, onBooleanClick, {title:LANG.cx_true_l, show: () => !ui.camExpertFast.checked }), + separator: newBlank({ class:"set-sep", driven }), + camArcTolerance: newInput(LANG.cx_arct_s, {title:LANG.cx_arct_l, convert:toFloat, units, bound:bound(0,100)}), + camArcResolution: newInput(LANG.cx_arcr_s, {title:LANG.cx_arcr_l, convert:toFloat, bound:bound(0,180), show:() => ui.camArcTolerance.value > 0}), + + /** LASER/DRAG/WJET/WEDM cut tool Settings */ + + _____: newGroup(LANG.sl_menu, $('lzr-slice'), { modes:TWOD, driven, separator }), + ctSliceKerf: newInput(LANG.ls_offs_s, {title:LANG.ls_offs_l, convert:toFloat}), + ctSliceHeight: newInput(LANG.ls_lahi_s, {title:LANG.ls_lahi_l, convert:toFloat, trigger}), + ctSliceHeightMin: newInput(LANG.ls_lahm_s, {title:LANG.ls_lahm_l, convert:toFloat, show:() => ui.ctSliceHeight.value == 0 && !ui.ctSliceSingle.checked }), + separator: newBlank({ class:"set-sep", driven }), + ctSliceSingle: newBoolean(LANG.ls_sngl_s, onBooleanClick, {title:LANG.ls_sngl_l}), + ctOmitInner: newBoolean(LANG.we_omit_s, onBooleanClick, {title:LANG.we_omit_l, modes:WEDM}), + _____: newGroup('surfaces', $('lzr-surface'), { modes:[-1], driven, separator }), + ctSurfaces: newRow([ + (ui.faceAdd = newButton(undefined, onButtonClick, {icon:''})), + (ui.faceDun = newButton(undefined, onButtonClick, {icon:''})), + (ui.faceClr = newButton(undefined, onButtonClick, {icon:''})) + ], {class:"ext-buttons f-row", modes:WEDM}), + _____: newGroup(LANG.dk_menu, $('lzr-knife'), { modes:DRAG, marker:true, driven, separator }), + ctOutKnifeDepth: newInput(LANG.dk_dpth_s, { title:LANG.dk_dpth_l, convert:toFloat, bound:bound(0.0,5.0) }), + ctOutKnifePasses: newInput(LANG.dk_pass_s, { title:LANG.dk_pass_l, convert:toInt, bound:bound(0,5) }), + ctOutKnifeTip: newInput(LANG.dk_offs_s, { title:LANG.dk_offs_l, convert:toFloat, bound:bound(0.0,10.0) }), + _____: newGroup(LANG.lo_menu, $('lzr-layout'), { modes:TWOD, driven, separator }), + ctOutTileSpacing: newInput(LANG.ou_spac_s, { title:LANG.ou_spac_l, convert:toInt }), + ctOutMerged: newBoolean(LANG.ou_mrgd_s, onBooleanClick, {title:LANG.ou_mrgd_l, modes:TWONED, show:() => !ui.ctOutStack.checked }), + ctOutGroup: newBoolean(LANG.ou_grpd_s, onBooleanClick, {title:LANG.ou_grpd_l, show:() => !(ui.ctOutMark.checked || ui.ctOutStack.checked) }), + _____: newGroup(LANG.ou_menu, $('lzr-output'), { modes:TWOD, driven, separator, group:"lzr-output" }), + ctOutPower: newInput(LANG.ou_powr_s, {title:LANG.ou_powr_l, convert:toInt, bound:bound(1,100), modes:TWONED }), + ctOutSpeed: newInput(LANG.ou_sped_s, {title:LANG.ou_sped_l, convert:toInt }), + ctAdaptive: newBoolean('adaptive speed', onBooleanClick, {modes:WEDM, title:'controller determines best cutting speed based on material feedback at runtime'}), + separator: newBlank({ class:"set-sep", driven }), + ctOriginBounds: newBoolean(LANG.or_bnds_s, onBooleanClick, { title:LANG.or_bnds_l, show:() => !ui.ctOriginCenter.checked }), + ctOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, { title:LANG.or_cntr_l, show:() => !ui.ctOriginBounds.checked }), + separator: newBlank({ class:"set-sep", driven, modes:WEDM, show:() => ui.ctOriginBounds.checked }), + ctOriginOffX: newInput(LANG.or_offx_s, { title:LANG.or_offx_l, convert:toFloat, modes:WEDM, show:() => ui.ctOriginBounds.checked }), + ctOriginOffY: newInput(LANG.or_offy_s, { title:LANG.or_offy_l, convert:toFloat, modes:WEDM, show:() => ui.ctOriginBounds.checked }), + separator: newBlank({ class:"set-sep", driven, modes:TWONED }), + ctOutZColor: newBoolean(LANG.ou_layo_s, onBooleanClick, { title:LANG.ou_layo_l, modes:TWONED, show:() => !ui.ctOutMerged.checked }), + ctOutLayer: newBoolean(LANG.ou_layr_s, onBooleanClick, { title:LANG.ou_layr_l, modes:TWONED, show:() => !ui.ctOutStack.checked }), + ctOutMark: newBoolean(LANG.ou_lays_s, onBooleanClick, { title:LANG.ou_lays_l, modes:TWONED, show:() => !ui.ctOutStack.checked }), + separator: newBlank({ class:"set-sep", driven, modes:LASER }), + ctOutInches: newBoolean(LANG.ou_inch_s, onBooleanClick, { title:LANG.ou_inch_l, modes:LASER }), + ctOutStack: newBoolean(LANG.ou_stak_s, onBooleanClick, { title:LANG.ou_stak_l, modes:LASER }), + ctOutShaper: newBoolean(LANG.ou_shap_s, onBooleanClick, { title:LANG.ou_shap_l, modes:LASER, show:() => ui.ctOutStack.checked }), + + /** SLA SETTINGS */ + + slaProc: newGroup(LANG.sa_menu, $('sla-slice'), { modes:SLA, group:"sla-slice", driven, separator }), + slaSlice: newInput(LANG.sa_lahe_s, {title:LANG.sa_lahe_l, convert:toFloat}), + slaShell: newInput(LANG.sa_shel_s, {title:LANG.sa_shel_l, convert:toFloat}), + slaOpenTop: newBoolean(LANG.sa_otop_s, onBooleanClick, {title:LANG.sa_otop_l}), + slaOpenBase: newBoolean(LANG.sa_obas_s, onBooleanClick, {title:LANG.sa_obas_l}), + slaLayers: newGroup(LANG.sa_layr_m, $('sla-layers'), { modes:SLA, group:"sla-layers", driven, separator }), + slaLayerOn: newInput(LANG.sa_lton_s, {title:LANG.sa_lton_l, convert:toFloat}), + slaLayerOff: newInput(LANG.sa_ltof_s, {title:LANG.sa_ltof_l, convert:toFloat}), + slaPeelDist: newInput(LANG.sa_pldi_s, {title:LANG.sa_pldi_l, convert:toFloat}), + slaPeelLiftRate: newInput(LANG.sa_pllr_s, {title:LANG.sa_pllr_l, convert:toFloat}), + slaPeelDropRate: newInput(LANG.sa_pldr_s, {title:LANG.sa_pldr_l, convert:toFloat}), + slaBase: newGroup(LANG.sa_base_m, $('sla-base'), { modes:SLA, group:"sla-base", driven, separator }), + slaBaseLayers: newInput(LANG.sa_balc_s, {title:LANG.sa_balc_l, convert:toInt}), + slaBaseOn: newInput(LANG.sa_lton_s, {title:LANG.sa_bltn_l, convert:toFloat}), + slaBaseOff: newInput(LANG.sa_ltof_s, {title:LANG.sa_bltf_l, convert:toFloat}), + slaBasePeelDist: newInput(LANG.sa_pldi_s, {title:LANG.sa_pldi_l, convert:toFloat}), + slaBasePeelLiftRate: newInput(LANG.sa_pllr_s, {title:LANG.sa_pllr_l, convert:toFloat}), + slaFill: newGroup(LANG.sa_infl_m, $('sla-fill'), { modes:SLA, group:"sla-infill", driven, separator }), + slaFillDensity: newInput(LANG.sa_ifdn_s, {title:LANG.sa_ifdn_l, convert:toFloat, bound:bound(0,1)}), + slaFillLine: newInput(LANG.sa_iflw_s, {title:LANG.sa_iflw_l, convert:toFloat, bound:bound(0,5)}), + slaSupport: newGroup(LANG.sa_supp_m, $('sla-support'), { modes:SLA, group:"sla-support", driven, separator }), + slaSupportLayers: newInput(LANG.sa_slyr_s, {title:LANG.sa_slyr_l, convert:toInt, bound:bound(5,100)}), + slaSupportGap: newInput(LANG.sa_slgp_s, {title:LANG.sa_slgp_l, convert:toInt, bound:bound(3,30)}), + slaSupportDensity: newInput(LANG.sa_sldn_s, {title:LANG.sa_sldn_l, convert:toFloat, bound:bound(0.01,0.9)}), + slaSupportSize: newInput(LANG.sa_slsz_s, {title:LANG.sa_slsz_l, convert:toFloat, bound:bound(0.1,1)}), + slaSupportPoints: newInput(LANG.sa_slpt_s, {title:LANG.sa_slpt_l, convert:toInt, bound:bound(3,10)}), + slaSupportEnable: newBoolean(LANG.enable, onBooleanClick, {title:LANG.sl_slen_l}), + slaOutput: newGroup(LANG.sa_outp_m, $('sla-output'), { modes:SLA, driven, separator, group:"sla-output" }), + slaFirstOffset: newInput(LANG.sa_opzo_s, {title:LANG.sa_opzo_l, convert:toFloat, bound:bound(0,1)}), + slaAntiAlias: newSelect(LANG.sa_opaa_s, {title:LANG.sa_opaa_l}, "antialias"), + + layers: uc.setGroup($("layers")), + + settingsName: $('settingsName'), + settingsSave: $('settingsSave'), + }); + + // override old style settings two-button menu + ui.settingsSave.onclick = () => { + settingsSave(undefined, ui.settingsName.value); + }; + + function optSelected(sel) { + let opt = sel.options[sel.selectedIndex]; + return opt ? opt.value : undefined; + } + + function hasInfill() { + return optSelected(ui.sliceFillType) !== 'none' + } + + function fillIsLinear() { + return hasInfill() && optSelected(ui.sliceFillType) === 'linear'; + } + + function spindleShow() { + return settings().device.spindleMax > 0; + } + + function zIntShow() { + return settings().controller.devel; + } + + // slider setup + const mobile = moto.space.info.mob; + const slbar = mobile ? 80 : 30; + const slbar2 = slbar * 2; + const slider = ui.sliderRange; + const drag = { }; + + if (mobile) { + ui.slider.classList.add('slider-mobile'); + ui.sliderLo.classList.add('slider-mobile'); + ui.sliderHi.classList.add('slider-mobile'); + // add css style for mobile devices + DOC.body.classList.add('mobile'); + } + + function pxToInt(txt) { + return txt ? parseInt(txt.substring(0,txt.length-2)) : 0; + } + + function sliderUpdate() { + let start = drag.low / drag.maxval; + let end = (drag.low + drag.mid - slbar) / drag.maxval; + api.event.emit('slider.pos', { start, end }); + api.var.layer_lo = Math.round(start * api.var.layer_max); + api.var.layer_hi = Math.round(end * api.var.layer_max); + api.show.layer(); + space.scene.active(); + } + + function dragit(el, delta) { + el.ontouchstart = el.onmousedown = (ev) => { + // el.classList.add('sli-drag-el'); + tracker.style.display = 'block'; + ev.stopPropagation(); + let obj = (ev.touches ? ev.touches[0] : ev); + drag.width = slider.clientWidth; + drag.maxval = drag.width - slbar2; + drag.start = obj.screenX; + drag.loat = drag.low = pxToInt(ui.sliderHold.style.marginLeft); + drag.mdat = drag.mid = ui.sliderMid.clientWidth; + drag.hiat = pxToInt(ui.sliderHold.style.marginRight); + drag.mdmax = drag.width - slbar - drag.loat; + drag.himax = drag.width - slbar - drag.mdat; + let cancel_drag = tracker.ontouchend = tracker.onmouseup = (ev) => { + // el.classList.remove('sli-drag-el'); + if (ev) { + ev.stopPropagation(); + ev.preventDefault(); + } + slider.onmousemove = undefined; + tracker.style.display = 'none'; + }; + el.ontouchend = cancel_drag; + el.ontouchmove = tracker.ontouchmove = tracker.onmousemove = (ev) => { + ev.stopPropagation(); + ev.preventDefault(); + if (ev.buttons === 0) { + return cancel_drag(); + } + if (delta) { + let obj = (ev.touches ? ev.touches[0] : ev); + delta(obj.screenX - drag.start); + } + }; + }; + } + + dragit(ui.sliderLo, (delta) => { + let midval = drag.mdat - delta; + let lowval = drag.loat + delta; + if (midval < slbar || lowval < 0) { + return; + } + ui.sliderHold.style.marginLeft = `${lowval}px`; + ui.sliderMid.style.width = `${midval}px`; + drag.low = lowval; + drag.mid = midval; + sliderUpdate(); + }); + dragit(ui.sliderMid, (delta) => { + let loval = drag.loat + delta; + let hival = drag.hiat - delta; + if (loval < 0 || hival < 0) return; + ui.sliderHold.style.marginLeft = `${loval}px`; + ui.sliderHold.style.marginRight = `${hival}px`; + drag.low = loval; + sliderUpdate(); + }); + dragit(ui.sliderHi, (delta) => { + let midval = drag.mdat + delta; + let hival = drag.hiat - delta; + if (midval < slbar || midval > drag.mdmax || hival < 0) return; + ui.sliderMid.style.width = `${midval}px`; + ui.sliderHold.style.marginRight = `${hival}px`; + drag.mid = midval; + sliderUpdate(); + }); + + ui.load.onchange = function(event) { + api.platform.load_files(event.target.files); + ui.load.value = ''; // reset so you can re-import the same filee + }; + + ui.sliderMin.onclick = () => { + api.show.layer(0,0); + } + + ui.sliderMax.onclick = () => { + api.show.layer(api.var.layer_max,0); + } + + ui.slider.onmouseover = (ev) => { + api.event.emit('slider.label'); + }; + + ui.slider.onmouseleave = (ev) => { + if (!ev.buttons) api.event.emit('slider.unlabel'); + }; + + api.event.on('slider.unlabel', (values) => { + }); + + api.event.on('slider.label', (values) => { + let digits = api.var.layer_max.toString().length; + $('slider-zero').style.width = `${digits}em`; + $('slider-max').style.width = `${digits}em`; + $('slider-zero').innerText = api.var.layer_lo; + $('slider-max').innerText = api.var.layer_hi; + }); + + api.event.on('slider.set', (values) => { + let width = slider.clientWidth; + let maxval = width - slbar2; + let start = Math.max(0, Math.min(1, values.start)); + let end = Math.max(start, Math.min(1, values.end)); + let lowval = start * maxval; + let midval = ((end - start) * maxval) + slbar; + let hival = maxval - end * maxval; + ui.sliderHold.style.marginLeft = `${lowval}px`; + ui.sliderMid.style.width = `${midval}px`; + ui.sliderHold.style.marginRight = `${hival}px`; + }); + + // store layer preferences + api.event.on('stack.show', label => { + let mode = api.mode.get(); + let view = api.view.get(); + api.conf.get().labels[`${mode}-${view}-${label}`] = true; + }); + + api.event.on('stack.hide', label => { + let mode = api.mode.get(); + let view = api.view.get(); + api.conf.get().labels[`${mode}-${view}-${label}`] = false; + }); + + // bind language choices + $('lset-en').onclick = function() { + sdb.setItem('kiri-lang', 'en-us'); + api.space.reload(); + }; + $('lset-da').onclick = function() { + sdb.setItem('kiri-lang', 'da-dk'); + api.space.reload(); + }; + $('lset-de').onclick = function() { + sdb.setItem('kiri-lang', 'de-de'); + api.space.reload(); + }; + $('lset-fr').onclick = function() { + sdb.setItem('kiri-lang', 'fr-fr'); + api.space.reload(); + }; + $('lset-pl').onclick = function() { + sdb.setItem('kiri-lang', 'pl-pl'); + api.space.reload(); + }; + $('lset-pt').onclick = function() { + sdb.setItem('kiri-lang', 'pt-pt'); + api.space.reload(); + }; + $('lset-es').onclick = function() { + sdb.setItem('kiri-lang', 'es-es'); + api.space.reload(); + }; + $('lset-zh').onclick = function() { + sdb.setItem('kiri-lang', 'zh'); + api.space.reload(); + }; + + space.event.addHandlers(self, [ + 'keyup', keyUpHandler, + 'keydown', keyDownHandler, + 'keypress', keyHandler, + 'dragover', dragOverHandler, + 'dragleave', dragLeave, + 'drop', dropHandler + ]); + + function selectionSize(e) { + let dv = parseFloat(e.target.value || 1), + pv = parseFloat(e.target.was || 1), + ra = dv / pv, + xv = parseFloat(ui.sizeX.was ?? ui.scaleX.value) || 1, + yv = parseFloat(ui.sizeY.was ?? ui.scaleY.value) || 1, + zv = parseFloat(ui.sizeZ.was ?? ui.scaleZ.value) || 1, + ta = e.target, + xc = ui.lockX.checked, + yc = ui.lockY.checked, + zc = ui.lockZ.checked, + xt = ta === ui.sizeX, + yt = ta === ui.sizeY, + zt = ta === ui.sizeZ, + tl = (xt && xc) || (yt && yc) || (zt && zc), + xr = ((tl && xc) || (!tl && xt) ? ra : 1), + yr = ((tl && yc) || (!tl && yt) ? ra : 1), + zr = ((tl && zc) || (!tl && zt) ? ra : 1); + // prevent null scale + if (xr * yr * zr === 0) { + return; + } + selection.scale(xr,yr,zr); + ui.sizeX.was = ui.sizeX.value = xv * xr; + ui.sizeY.was = ui.sizeY.value = yv * yr; + ui.sizeZ.was = ui.sizeZ.value = zv * zr; + } + + function selectionScale(e) { + let dv = parseFloat(e.target.value || 1), + pv = parseFloat(e.target.was || 1), + ra = dv / pv, + xv = parseFloat(ui.scaleX.was ?? ui.scaleX.value) || 1, + yv = parseFloat(ui.scaleY.was ?? ui.scaleY.value) || 1, + zv = parseFloat(ui.scaleZ.was ?? ui.scaleY.value) || 1, + ta = e.target, + xc = ui.lockX.checked, + yc = ui.lockY.checked, + zc = ui.lockZ.checked, + xt = ta === ui.scaleX, + yt = ta === ui.scaleY, + zt = ta === ui.scaleZ, + tl = (xt && xc) || (yt && yc) || (zt && zc), + xr = ((tl && xc) || (!tl && xt) ? ra : 1), + yr = ((tl && yc) || (!tl && yt) ? ra : 1), + zr = ((tl && zc) || (!tl && zt) ? ra : 1); + + selection.scale(xr,yr,zr); + ui.scaleX.was = ui.scaleX.value = xv * xr; + ui.scaleY.was = ui.scaleY.value = yv * yr; + ui.scaleZ.was = ui.scaleZ.value = zv * zr; + } + + function selectionRotate(e) { + let val = parseFloat(e.target.value) || 0; + e.target.value = val; + } + + space.event.onEnterKey([ + ui.scaleX, selectionScale, + ui.scaleY, selectionScale, + ui.scaleZ, selectionScale, + ui.sizeX, selectionSize, + ui.sizeY, selectionSize, + ui.sizeZ, selectionSize, + ui.toolName, updateTool, + ui.toolNum, updateTool, + ui.toolFluteDiam, updateTool, + ui.toolFluteLen, updateTool, + ui.toolShaftDiam, updateTool, + ui.toolShaftLen, updateTool, + ui.toolTaperTip, updateTool, + ui.toolTaperAngle, updateTool, + $('rot_x'), selectionRotate, + $('rot_y'), selectionRotate, + $('rot_z'), selectionRotate + ], true); + + $('lab-axis').onclick = () => { + ui.lockX.checked = + ui.lockY.checked = + ui.lockZ.checked = !( + ui.lockX.checked || + ui.lockY.checked || + ui.lockZ.checked + ); + }; + + $('scale-reset').onclick = $('lab-scale').onclick = () => { + selection.scale(1 / ui.scaleX.was, 1 / ui.scaleY.was, 1 / ui.scaleZ.was); + ui.scaleX.value = ui.scaleY.value = ui.scaleZ.value = + ui.scaleX.was = ui.scaleY.was = ui.scaleZ.was = 1; + }; + + $('app-xpnd').onclick = () => { + try { + DOC.body.requestFullscreen(); + } catch (e) { + event.emit('resize'); + moto.space.event.onResize(); + } + }; + + if (!DOC.body.requestFullscreen) { + $('app-xpnd').style.display = 'none'; + } + + uc.onBlur([ + ui.toolName, + ui.toolNum, + ui.toolFluteDiam, + ui.toolFluteLen, + ui.toolShaftDiam, + ui.toolShaftLen, + ui.toolTaperTip, + ], updateTool); + + ui.toolMetric.onclick = updateTool; + ui.toolType.onchange = updateTool; + // default show gcode pre + ui.gcodePre.button.click(); + + function mksvg(src) { + let svg = DOC.createElement('svg'); + svg.innerHTML = src; + return svg; + } + + function mklbl(src) { + let lbl = DOC.createElement('label'); + lbl.innerText = src; + return lbl; + } + + api.platform.update_size(); + + function mouseOnHover(int, event, ints) { + if (!api.feature.hover) return; + if (!int) return api.feature.hovers || api.widgets.meshes(); + api.event.emit('mouse.hover', {int, ints, event, point: int.point, type: 'widget'}); + } + + function platformOnHover(int, event) { + if (!api.feature.hover) return; + if (int) api.event.emit('mouse.hover', {point: int, event, type: 'platform'}); + } + + api.event.on("feature.hover", enable => { + space.mouse.onHover(enable ? mouseOnHover : undefined); + space.platform.onHover(enable ? platformOnHover : undefined); + }); + + // block standard browser context menu + DOC.oncontextmenu = (event) => { + let et = event.target; + if (et.tagName === 'CANVAS' || et.id === 'context-menu' || et.classList.contains('draggable')) { + event.preventDefault(); + event.stopPropagation(); + return false; + } + }; + + space.mouse.downSelect((int, event) => { + if (api.feature.on_mouse_down) { + if (int) { + api.feature.on_mouse_down(int, event); + return; + } + } + if (api.feature.hover) { + if (int) { + return api.event.emit('mouse.hover.down', {int, point: int.point}); + } else { + return selection.meshes(); + } + } + // lay flat with meta or ctrl clicking a selected face + if (int && (event.ctrlKey || event.metaKey || api.feature.on_face_select)) { + let q = new THREE.Quaternion(); + // find intersecting point, look "up" on Z and rotate to face that + q.setFromUnitVectors(int.face.normal, new THREE.Vector3(0,0,-1)); + selection.rotate(q); + } + if (api.view.get() !== VIEWS.ARRANGE) { + // return no selection in modes other than arrange + return null; + } else { + // return selected meshes for further mouse processing + return api.feature.hovers || selection.meshes(); + } + }); + + space.mouse.upSelect((object, event) => { + if (api.feature.on_mouse_up) { + if (event && object) { + return api.feature.on_mouse_up(object, event); + } else { + return api.widgets.meshes(); + } + } + if (event && api.feature.hover) { + api.event.emit('mouse.hover.up', { object, event }); + return; + } + if (event && event.target.nodeName === "CANVAS") { + if (object && object.object) { + if (object.object.widget) { + platform.select(object.object.widget, event.shiftKey, false); + } + } else { + platform.deselect(); + } + } else { + return api.feature.hovers || api.widgets.meshes(); + } + }); + + space.mouse.onDrag(function(delta, offset, up = false) { + if (api.feature.hover) { + return; + } + if (up) { + api.event.emit('mouse.drag.done', offset); + } + if (delta && ui.freeLayout.checked) { + let set = settings(); + let dev = set.device; + let bound = set.bounds_sel; + let width = dev.bedWidth/2; + let depth = dev.bedDepth/2; + let isout = ( + bound.min.x <= -width || + bound.min.y <= -depth || + bound.max.x >= width || + bound.max.y >= depth + ); + if (!isout) { + if (bound.min.x + delta.x <= -width) return; + if (bound.min.y + delta.y <= -depth) return; + if (bound.max.x + delta.x >= width) return; + if (bound.max.y + delta.y >= depth) return; + } + selection.move(delta.x, delta.y, 0); + api.event.emit('selection.drag', delta); + } else { + return selection.meshes().length > 0; + } + }); + + api.space.restore(init_two) || checkSeed(init_two) || init_two(); + }; + + // SECOND STAGE INIT AFTER UI RESTORED + function init_two() { + api.event.emit('init.two'); + + // call driver initializations, if present + Object.values(kiri.driver).forEach(driver => { + if (driver.init) try { + driver.init(kiri, api, driver); + } catch (error) { + console.log({driver_init_fail: driver, error}) + } + }); + + // load script extensions + if (SETUP.s) SETUP.s.forEach(function(lib) { + let scr = DOC.createElement('script'); + scr.setAttribute('defer',true); + scr.setAttribute('src',`/code/${lib}.js?${kiri.version}`); + DOC.body.appendChild(scr); + stats.add('load_'+lib); + api.event.emit('load.lib', lib); + }); + + // load CSS extensions + if (SETUP.ss) SETUP.ss.forEach(function(style) { + style = style.charAt(0) === '/' ? style : `/kiri/style-${style}`; + let ss = DOC.createElement('link'); + ss.setAttribute("type", "text/css"); + ss.setAttribute("rel", "stylesheet"); + ss.setAttribute("href", `${style}.css?${kiri.version}`); + DOC.body.appendChild(ss); + }); + + // override stored settings + if (SETUP.v) SETUP.v.forEach(function(kv) { + kv = kv.split('='); + sdb.setItem(kv[0],kv[1]); + }); + + // import octoprint settings + if (SETUP.ophost) { + let ohost = api.const.OCTO = { + host: SETUP.ophost[0], + apik: SETUP.opkey ? SETUP.opkey[0] : '' + }; + sdb['octo-host'] = ohost.host; + sdb['octo-apik'] = ohost.apik; + console.log({octoprint:ohost}); + } + + // load workspace from url + if (SETUP.wrk) { + api.settings.import_url(`${proto}//${SETUP.wrk[0]}`, false); + } + + // load an object from url + if (SETUP.load) { + console.log({load:SETUP}); + api.platform.load_url(`${proto}//${SETUP.load[0]}`); + } + + // bind this to UI so main can call it on settings import + ui.sync = function() { + const current = settings(); + const control = current.controller; + + if (!control.devel) { + // TODO: hide thin type 3 during development + kiri.consts.LISTS.thin.length = 3; + } + + platform.deselect(); + catalog.addFileListener(updateCatalog); + space.view.setZoom(control.reverseZoom, control.zoomSpeed); + space.platform.setGridZOff(undefined); + space.platform.setZOff(0.05); + + // restore UI state from settings + ui.showOrigin.checked = control.showOrigin; + ui.showRulers.checked = control.showRulers; + ui.showSpeeds.checked = control.showSpeeds; + ui.freeLayout.checked = control.freeLayout; + ui.autoLayout.checked = control.autoLayout; + ui.spaceRandoX.checked = control.spaceRandoX; + ui.antiAlias.checked = control.antiAlias; + ui.reverseZoom.checked = control.reverseZoom; + ui.autoSave.checked = control.autoSave; + ui.healMesh.checked = control.healMesh; + ui.threaded.checked = setThreaded(control.threaded); + ui.assembly.checked = control.assembly; + ui.ortho.checked = control.ortho; + ui.devel.checked = control.devel; + lineTypeSave(); + detailSave(); + updateStats(); + + // optional set-and-lock mode (hides mode menu) + let SETMODE = SETUP.mode ? SETUP.mode[0] : null; + + // optional set-and-lock device (hides device menu) + let DEVNAME = SETUP.dev ? SETUP.dev[0] : null; + + // setup default mode and enable mode locking, if set + api.mode.set(SETMODE || STARTMODE || current.mode, SETMODE); + + // fill device list + api.devices.refresh(); + + // update ui fields from settings + api.conf.update_fields(); + + // default to ARRANGE view mode + api.view.set(VIEWS.ARRANGE); + + // add ability to override + api.show.controls(api.feature.controls); + + // update everything dependent on the platform size + platform.update_size(); + + // load wasm if indicated + kiri.client.wasm(control.assembly === true); + }; + + ui.sync(); + + // clear alerts as they build up + setInterval(api.event.alerts, 1000); + + // add hide-alerts-on-alert-click + ui.alert.dialog.onclick = function() { + api.event.alerts(true); + }; + + // enable modal hiding + $('mod-x').onclick = api.modal.hide; + + if (!SETUP.s) console.log(`kiri | init main | ${kiri.version}`); + + // send init-done event + api.event.emit('init-done', stats); + + // show gdpr if it's never been seen and we're not iframed + const isLocal = LOCAL || WIN.location.host.split(':')[0] === 'localhost'; + if (!sdb.gdpr && WIN.self === WIN.top && !SETUP.debug && !isLocal) { + $('gdpr').style.display = 'flex'; + } + + // warn of degraded functionality when SharedArrayBuffers are missing + if (api.feature.work_alerts && !window.SharedArrayBuffer) { + api.alerts.show("The security context of this", 10); + api.alerts.show("Window blocks important functionality.", 10); + api.alerts.show("Try a Chromium-base Browser instead", 10); + } + + // add keyboard focus handler (must use for iframes) + WIN.addEventListener('load', function () { + WIN.focus(); + DOC.body.addEventListener('click', function() { + WIN.focus(); + },false); + }); + + // dismiss gdpr alert + $('gotit').onclick = () => { + $('gdpr').style.display = 'none'; + sdb.gdpr = Date.now(); + }; + + // lift curtain + $('curtain').style.display = 'none'; + + // bind interface action elements + $('mode-device').onclick = api.show.devices; + $('mode-profile').onclick = settingsLoad; + $('mode-fdm').onclick = () => api.mode.set('FDM'); + $('mode-cam').onclick = () => api.mode.set('CAM'); + $('mode-sla').onclick = () => api.mode.set('SLA'); + $('mode-laser').onclick = () => api.mode.set('LASER'); + $('mode-drag').onclick = () => api.mode.set('DRAG'); + $('mode-wjet').onclick = () => api.mode.set('WJET'); + $('mode-wedm').onclick = () => api.mode.set('WEDM'); + $('set-device').onclick = (ev) => { ev.stopPropagation(); api.show.devices() }; + $('set-profs').onclick = (ev) => { ev.stopPropagation(); api.conf.show() }; + $('set-tools').onclick = (ev) => { ev.stopPropagation(); api.show.tools() }; + $('set-prefs').onclick = (ev) => { ev.stopPropagation(); api.modal.show('prefs') }; + ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() }; + ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; + ui.acct.mesh.onclick = (ev) => { ev.stopPropagation(); WIN.location = "/mesh" }; + ui.acct.export.onclick = (ev) => { ev.stopPropagation(); profileExport() }; + ui.acct.export.title = LANG.acct_xpo; + $('file-new').onclick = (ev) => { ev.stopPropagation(); workspaceNew() }; + $('file-recent').onclick = () => { api.modal.show('files') }; + $('file-import').onclick = (ev) => { api.event.import(ev); }; + ui.func.slice.onclick = (ev) => { ev.stopPropagation(); api.function.slice() }; + ui.func.preview.onclick = (ev) => { ev.stopPropagation(); api.function.print() }; + ui.func.animate.onclick = (ev) => { ev.stopPropagation(); api.function.animate() }; + ui.func.export.onclick = (ev) => { ev.stopPropagation(); api.function.export() }; + $('view-arrange').onclick = api.platform.layout; + $('view-top').onclick = space.view.top; + $('view-home').onclick = space.view.home; + $('view-front').onclick = space.view.front; + $('view-back').onclick = space.view.back; + $('view-left').onclick = space.view.left; + $('view-right').onclick = space.view.right; + $('unrotate').onclick = () => { + api.widgets.for(w => w.unrotate()); + selection.update_info(); + }; + // attach button handlers to support targets + for (let btn of ["don8pt","don8gh","don8pp"]) { + $(btn).onclick = (ev) => { + window.open(ev.target.children[0].href); + } + } + // rotation buttons + let d = (Math.PI / 180); + $('rot_x_lt').onclick = () => { selection.rotate(-d * $('rot_x').value,0,0) }; + $('rot_x_gt').onclick = () => { selection.rotate( d * $('rot_x').value,0,0) }; + $('rot_y_lt').onclick = () => { selection.rotate(0,-d * $('rot_y').value,0) }; + $('rot_y_gt').onclick = () => { selection.rotate(0, d * $('rot_y').value,0) }; + $('rot_z_lt').onclick = () => { selection.rotate(0,0, d * $('rot_z').value) }; + $('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) }; + // rendering options + $('render-edges').onclick = () => { api.view.edges({ toggle: true }); api.conf.save() }; + $('render-ghost').onclick = () => { api.view.wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); }; + $('render-wire').onclick = () => { api.view.wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); }; + $('render-solid').onclick = () => { api.view.wireframe(false, 0, 1); }; + $('mesh-export-stl').onclick = () => { objectsExport('stl') }; + $('mesh-export-obj').onclick = () => { objectsExport('obj') }; + $('mesh-merge').onclick = selection.merge; + $('context-duplicate').onclick = duplicateSelection; + $('context-mirror').onclick = mirrorSelection; + $('context-layflat').onclick = () => { api.event.emit("tool.mesh.lay-flat") }; + $('context-setfocus').onclick = () => { + api.event.emit( + "tool.camera.focus", + ev => api.space.set_focus(undefined, ev.object.point) + ); + }; + + // ui.modal.onclick = api.modal.hide; + ui.modalBox.onclick = (ev) => { ev.stopPropagation() }; + + // add app name hover info + $('app-info').innerText = kiri.version; + + // show topline separator when iframed + try { if (WIN.self !== WIN.top) $('top-sep').style.display = 'flex' } catch (e) { console.log(e) } + + // warn users they are running a beta release + if (kiri.beta && kiri.beta > 0 && sdb.kiri_beta != kiri.beta) { + api.show.alert("CAUTION"); + api.show.alert("this is a development release"); + api.show.alert("and may not function properly"); + sdb.kiri_beta = kiri.beta; + } + } + + // update static html elements with language overrides + ui.lang = function() { + // lk attribute causes inner text to be replaced with lang value + for (let el of [...DOC.querySelectorAll("[lk]")]) { + let key = el.getAttribute('lk'); + let val = LANG[key]; + if (val) { + el.innerText = val; + } else { + console.log({missing_ln: key}); + } + } + // lkt attribute causes a title attribute to be set from lang value + for (let el of [...DOC.querySelectorAll("[lkt]")]) { + let key = el.getAttribute('lkt'); + let val = LANG[key]; + if (val) { + el.setAttribute("title", val); + } else { + console.log({missing_ln: key}); + } + } + } + + // init lang must happen before all other init functions + function init_lang() { + // if a language needs to load, the script is injected and loaded + // first. once this loads, or doesn't, the initialization begins + let lang = SETUP.ln ? SETUP.ln[0] : sdb.getItem('kiri-lang') || kiri.lang.get(); + // inject language script if not english + if (lang && lang !== 'en' && lang !== 'en-us') { + let map = kiri.lang.map(lang); + let scr = DOC.createElement('script'); + // scr.setAttribute('defer',true); + scr.setAttribute('src',`/kiri/lang/${map}.js?${kiri.version}`); + (DOC.body || DOC.head).appendChild(scr); + stats.set('ll',lang); + scr.onload = function() { + kiri.lang.set(map); + ui.lang(); + init_one(); + }; + scr.onerror = function(err) { + console.log({language_load_error: err, lang}) + kiri.lang.set(); + ui.lang(); + init_one(); + } + } else { + // set to browser default which will be overridden + // by any future script loads (above) + kiri.lang.set(); + ui.lang(); + init_one(); + } + } + + // setup init() trigger when dom + scripts complete + DOC.onreadystatechange = function() { + if (DOC.readyState === 'complete') { + init_lang(); + } + } + +}); diff --git a/src.old/kiri/lang-en.js b/src.old/kiri/lang-en.js new file mode 120000 index 00000000..92cc3a27 --- /dev/null +++ b/src.old/kiri/lang-en.js @@ -0,0 +1 @@ +../../web/kiri/lang/en.js \ No newline at end of file diff --git a/src2/kiri/lang.js b/src.old/kiri/lang.js similarity index 93% rename from src2/kiri/lang.js rename to src.old/kiri/lang.js index 187f590b..8b72c10e 100644 --- a/src2/kiri/lang.js +++ b/src.old/kiri/lang.js @@ -1,6 +1,11 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -const LANG = self.lang = { current: {} }; +"use strict"; + +gapp.register("kiri.lang", [], (root, exports) => { + +const { kiri } = root; +const LANG = kiri.lang = { current: {} }; const KDFL = 'en-us'; let lset = navigator.language.toLocaleLowerCase(); @@ -73,6 +78,4 @@ LANG.set = function() { return undefined; } -export default { LANG }; - -export { LANG }; +}); diff --git a/src2/kiri/layers.js b/src.old/kiri/layers.js similarity index 92% rename from src2/kiri/layers.js rename to src.old/kiri/layers.js index 7e9c3c7f..bbf05ac5 100644 --- a/src2/kiri/layers.js +++ b/src.old/kiri/layers.js @@ -1,9 +1,25 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { newPolygon, Polygon } from '../geo/polygon.js'; -import { polygons as POLY } from '../geo/polygons.js'; +"use strict"; -export class Layers { +/* + * provides the `output` abstraction for turning poly lines (open and closed) + * into primordial geometries (faces, lines, color ranges). used inside of + * workers as part of slicing and preview output. + */ + +// dep: geo.base +// dep: geo.paths +// dep: geo.polygon +// dep: geo.polygons +gapp.register("kiri.layers", [], (root, exports) => { + +const { base, kiri } = root; +const { polygons, newPolygon } = base; + +const POLY = base.polygons; + +class Layers { constructor() { this.init(); } @@ -78,7 +94,7 @@ export class Layers { options.open = true; const polys = []; for (let i=0; i -- All Rights Reserved */ + +"use strict"; + +// dep: main.kiri +// dep: moto.license +// dep: geo.base +// dep: kiri.ui +// dep: kiri.api +// dep: kiri.conf +// dep: kiri.lang +// dep: kiri.utils +// dep: kiri.files +// dep: kiri.consts +// dep: kiri.widget +// dep: kiri.stats +// dep: kiri.stacks +// dep: kiri.devices +// dep: kiri.function +// dep: kiri.platform +// dep: kiri.selection +// dep: kiri.settings +// use: kiri.alerts +// use: kiri.files +// use: kiri.frame +// use: moto.ajax +gapp.register("kiri.main", (root, exports) => { + + let { data, kiri, moto, noop } = root, + { api, consts, lang, Widget, newWidget, utils, stats } = kiri, + { parseOpt, encodeOpt, o2js, js2o, ls2o } = utils, + { platform, selection, settings } = api, + { 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 = kiri.broker = gapp.broker, + SDB = data.local, + SPACE = kiri.space = moto.space, + FILES = kiri.catalog = kiri.openFiles(new data.Index(SETUP.d ? SETUP.d[0] : 'kiri')), + clone = Object.clone, + UI = {}, + UC = kiri.ui.prefix('kiri').inputAction(api.conf.update), + MODE = MODES.FDM, + STACKS = kiri.stacks, + DRIVER = undefined, + viewMode = VIEWS.ARRANGE, + autoSaveTimer = null, + busy = 0, + { assign } = Object; + + // extend API + assign(api, { + ui: UI = assign(api.ui, UI), + uc: UC = assign(api.uc, UC), + stats, + catalog: FILES, + busy: assign(api.busy, { + val() { return busy }, + inc() { kiri.api.event.emit("busy", ++busy) }, + dec() { kiri.api.event.emit("busy", --busy) } + }), + color: COLOR, + const: assign(api.const, { + LANG, + LOCAL, + SETUP, + SECURE, + STACKS, + }), + dialog: assign(api.dialog, { + show: showModal, + hide: hideModal, + update_process_list: updateProcessList + }), + help: assign(api.help, { + show: showHelp, + file: showHelpFile + }), + event: assign(api.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 + }), + group: assign(api.group, { + merge: groupMerge, + split: groupSplit, + }), + hide: assign(api.hide, { + alert(rec, recs) { api.alerts.hide(...arguments) }, + import: noop, + slider: hideSlider + }), + image: assign(api.image, { + dialog: loadImageDialog, + convert: loadImageConvert + }), + language: kiri.lang, + modal: assign(api.modal, { + show: showModal, + hide: hideModal, + visible: modalShowing + }), + mode: assign(api.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() + } + }), + probe: assign(api.probe, { + live: "https://live.grid.space", + grid: noop, + local: noop + }), + process: assign(api.process, { + code: currentProcessCode, + get: currentProcessName + }), + show: assign(api.show, { + alert() { return api.alerts.show(...arguments) }, + progress: setProgress, + controls: setControlsVisible, + slices: showSlices, + layer: setVisibleLayer, + local: showLocal, + import: function() { UI.import.style.display = '' } + }), + space: assign(api.space, { + reload, + auto_save, + restore: restoreWorkspace, + clear: clearWorkspace, + save: saveWorkspace, + set_focus: setFocus, + update: SPACE.update, + is_dark() { return settings.ctrl().dark } + }), + util: assign(api.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))) } + }), + view: assign(api.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, + }), + work: kiri.client + }); + + // 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(); + const { stacks } = kiri; + // match label checkboxes to preference + for (let label of stacks.getLabels()) { + let check = `${settings.mode}-${api.view.get()}-${label}`; + 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 (busy === 0) { + LOC.reload(); + } else { + console.log(`reload deferred on busy=${busy}`); + do_reload(250); + } + }, time || 100); + } + + function auto_save() { + if (!settings.ctrl().autoSave) { + return; + } + clearTimeout(autoSaveTimer); + autoSaveTimer = setTimeout(() => { + api.space.save(true); + }, 1000); + } + + // update version and init count + let inits = parseInt(SDB.getItem('kiri-init') || stats.get('init') || 0) + 1; + SDB.setItem('kiri-init', inits); + stats.set('init', inits); + stats.set('kiri', kiri.version || gapp.version); + + // remove version from url, preserve other settings + WIN.history.replaceState({},'','/kiri/' + encodeOpt(SETUP) + LOC.hash); + + /** ****************************************************************** + * 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); + UI.progress.width = value+'%'; + if (self.debug) { + // console.log(msg, value.round(2)); + UI.prostatus.style.display = 'flex'; + if (msg) { + UI.prostatus.innerHTML = msg; + } else { + 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))); + } + kiri.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(``); + }); + 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 = 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() { + UI.layers.style.display = 'flex'; + UI.slider.style.display = 'flex'; + } + + function hideSlider() { + UI.layers.style.display = 'none'; + UI.slider.style.display = 'none'; + UI.speeds.style.display = 'none'; + } + + function loadImageDialog(image, name, force) { + if (!force && image.byteLength > 2500000) { + return UC.confirm("Large images may fail to import
Consider resizing under 1000 x 1000
Proceed with import?").then(ok => { + if (ok) { + loadImageDialog(image, name, true); + } + }); + } + const opt = {pre: [ + "
", + "

Image Conversion

", + "

", + " 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.", + "

", + "
", + " ", + " ", + " ", + " ", + " ", + " ", + "
blur value invert image
base size invert alpha
border size
", + "
" + ]}; + 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 = assign({settings: settings.get(), png:image}, opt); + kiri.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 = ``; + + 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 + kiri.client.clear(); + platform.select_all(); + platform.delete(selection.meshes()); + } + + function modalShowing() { + return UI.modal.style.display === 'flex'; + } + + function showModal(which) { + let mod = UI.modal, + style = mod.style, + visible = modalShowing(), + info = { pct: 0 }; + + // hide all modals befroe showing another + Object.keys(UI.modals).forEach(name => { + 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 = 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'); + 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 }); + 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: kiri.version, + moto: moto.id, + time: Date.now(), + mode, + name + }); + 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 = 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') + } + UI.settingsName.value = settings.proc().processName; + + del.setAttribute('del', sk); + del.setAttribute('title', "remove '"+sk+"'"); + del.innerHTML = ''; + del.onclick = deleteSettings; + + edit.setAttribute('name', sk); + edit.setAttribute('title', 'edit'); + edit.innerHTML = ''; + edit.onclick = editSettings; + + xprt.setAttribute('name', sk); + xprt.setAttribute('title', 'export'); + xprt.innerHTML = ''; + 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; + } + $('kiri-version').innerHTML = `${LANG.version} ${kiri.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 = ['']; + html.push(``); + html.push(``); + 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(``); + html.push(``); + html.push(``); + html.push(``); + html.push(``); + html.push(``); + devc++; + } + html.push(``); + html.push(`
devicetypestatus
${r.device.name}${r.device.mode}${r.state}
`); + if (devc) { + $('mod-local').innerHTML = html.join(''); + } else { + $('mod-local').innerHTML = `
no local devices`; + } + 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(); + kiri.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]; + DRIVER = kiri.driver[mode]; + kiri.driver.current = DRIVER; + // 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); + 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 = `x`; + $('_dexport_').click(); + } + + // prevent safari from exiting full screen mode + DOC.onkeydown = function (evt) { if (evt.keyCode == 27) evt.preventDefault() } + + // upon restore, seed presets + api.event.emit('preset', api.conf.dbo()); + +}); diff --git a/src2/kiri/pack.js b/src.old/kiri/pack.js similarity index 94% rename from src2/kiri/pack.js rename to src.old/kiri/pack.js index 2d35e626..9d1a1121 100644 --- a/src2/kiri/pack.js +++ b/src.old/kiri/pack.js @@ -1,6 +1,17 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -export class Packer { +"use strict"; + +/** + * adapted from + * http://codeincomplete.com/posts/2011/5/7/bin_packing/ + */ + +gapp.register("kiri.pack", [], (root, exports) => { + +const { kiri } = root; + +class Packer { constructor(w, h, spacing, opt = {}) { this.w = w; this.h = h; @@ -139,3 +150,7 @@ export class Packer { return node; } } + +kiri.Pack = Packer; + +}); diff --git a/src2/kiri/platform.js b/src.old/kiri/platform.js similarity index 97% rename from src2/kiri/platform.js rename to src.old/kiri/platform.js index 8570dc85..b5eb769c 100644 --- a/src2/kiri/platform.js +++ b/src.old/kiri/platform.js @@ -1,16 +1,26 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -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 } from './widget.js'; -import { Packer } from './pack.js'; +"use strict"; -import { COLOR, MODES } from './consts.js'; -import { THREE } from '../ext/three.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; const V0 = new THREE.Vector3(0,0,0); @@ -515,7 +525,7 @@ function platformDelete(widget, defer) { api.event.emit('widget.delete', widget); return; } - api.client.clear(widget); + kiri.client.clear(widget); api.widgets.remove(widget); api.selection.remove(widget); Widget.Groups.remove(widget); @@ -709,12 +719,12 @@ function platformLayout() { mp = [sz.x, sz.y], ms = [mp[0] / 2, mp[1] / 2], c = Widget.Groups.blocks().sort(), - p = new Packer(ms[0], ms[1], gap).fit(c); + p = new kiri.Pack(ms[0], ms[1], gap).fit(c); while (!p.packed) { ms[0] *= 1.1; ms[1] *= 1.1; - p = new Packer(ms[0], ms[1], gap).fit(c); + p = new kiri.Pack(ms[0], ms[1], gap).fit(c); } for (i = 0; i < c.length; i++) { @@ -799,7 +809,7 @@ function platformLoadFiles(files, group) { if (api.conf.get().controller.devel) { api.event.emit('cam.parse.gerber', { data: text }); } else { - api.client.gerber2mesh(text, progress => { + kiri.client.gerber2mesh(text, progress => { api.show.progress(progress, "converting"); }, vertices => { api.show.progress(0); @@ -911,7 +921,7 @@ function fitDeviceToWidgets() { } // extend API (api.platform) -export const platform = { +const platform = Object.assign(api.platform, { fit: fitDeviceToWidgets, add: platformAdd, changed: platformChanged, @@ -940,4 +950,6 @@ export const platform = { show_volume: space.platform.showVolume, top_z() { return topZ }, clear() { api.space.clear(); api.space.save(true) } -}; +}); + +}); diff --git a/src2/kiri/print.js b/src.old/kiri/print.js similarity index 98% rename from src2/kiri/print.js rename to src.old/kiri/print.js index 8a39bde5..a690011e 100644 --- a/src2/kiri/print.js +++ b/src.old/kiri/print.js @@ -1,13 +1,19 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { arcToPath } from '../geo/paths.js'; -import { consts } from './consts.js'; -import { newPoint } from '../geo/point.js'; -import { util } from '../geo/base.js'; +"use strict"; +// 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 } = consts; - +const { beltfact } = kiri.consts; const XAXIS = new THREE.Vector3(1,0,0); const DEG2RAD = Math.PI / 180; @@ -457,10 +463,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}); } @@ -674,7 +680,9 @@ function newPrint(settings, widgets, id) { return new Print(settings, widgets, id); }; -export { +gapp.overlay(kiri, { Print, newPrint -}; +}); + +}); diff --git a/src2/kiri/render.js b/src.old/kiri/render.js similarity index 90% rename from src2/kiri/render.js rename to src.old/kiri/render.js index 087d788e..0e0c1090 100644 --- a/src2/kiri/render.js +++ b/src.old/kiri/render.js @@ -1,24 +1,35 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { Layers } from './layers.js'; -import { newPoint } from '../geo/point.js'; -import { newPolygon } from '../geo/polygon.js'; -import { newSlopeFromAngle } from '../geo/slope.js'; +"use strict"; +/* + * 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; -function worker() { - return self.kiri_worker.current; -} +exports({ + path, + is_dark, + rate_to_color +}); function is_cam() { - return worker().print.settings.mode === 'CAM'; + return root.worker.print.settings.mode === 'CAM'; } function is_dark() { - return worker().print.settings.controller.dark ? true : false; + return root.worker.print.settings.controller.dark ? true : false; }; function rate_to_color(rate, max) { @@ -33,17 +44,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) { - worker().print.maxSpeed = 0; + self.worker.print.maxSpeed = 0; return []; } @@ -96,11 +107,10 @@ async function path(levels, update, opts = {}) { // }).reduce((a, v) => Math.max(a, v)) + 1; // for reporting - let print = worker().print; - print.minSpeed = minspd; - print.maxSpeed = maxspd; - print.thinColor = thin; - print.flatColor = flat; + self.worker.print.minSpeed = minspd; + self.worker.print.maxSpeed = maxspd; + self.worker.print.thinColor = thin; + self.worker.print.flatColor = flat; let lastTool = null; let lastEnd = null; @@ -128,8 +138,7 @@ async function path(levels, update, opts = {}) { const engages = []; const lasers = []; const sparks = []; - const output = new Layers(); - + const output = new kiri.Layers(); layers.push(output); const pushPrint = (toolid, poly) => { @@ -163,8 +172,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 = newPoint(p2.x, p2.y, p2.z); - // let sp = newPoint(outPoint.x * 1.1, outPoint.y * 1.1, outPoint.z * 1.1); + 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); // sparks.push(outPoint, sp); } if (out.tool !== lastTool) { @@ -312,8 +321,8 @@ async function path(levels, update, opts = {}) { .addAreas(heads.map(points => { const {p1, p2} = points; const slope = p2.slopeTo(p1); - const s1 = newSlopeFromAngle(slope.angle + 20); - const s2 = newSlopeFromAngle(slope.angle - 20); + const s1 = base.newSlopeFromAngle(slope.angle + 20); + const s2 = base.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); @@ -534,14 +543,4 @@ function color4(rgb, inc, seg) { } } -export const render = { - path, - is_dark, - rate_to_color -}; - -export { - path, - is_dark, - rate_to_color -}; +}); diff --git a/src2/kiri/selection.js b/src.old/kiri/selection.js similarity index 96% rename from src2/kiri/selection.js rename to src.old/kiri/selection.js index f19c350d..c889582b 100644 --- a/src2/kiri/selection.js +++ b/src.old/kiri/selection.js @@ -1,8 +1,16 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from './api.js'; -import { space } from '../moto/space.js'; -import { THREE } from '../ext/three.js'; +"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; const selectedMeshes = []; @@ -288,7 +296,7 @@ function setDisabled(bool) { } // extend API (api.selection) -export const selection = { +const selection = Object.assign(api.selection, { move, merge, scale, @@ -312,4 +320,6 @@ export const selection = { disable() { setDisabled(true) }, opacity() { api.widgets.opacity(...arguments) }, meshes() { return selectedMeshes.slice() }, -}; +}); + +}); diff --git a/src2/kiri/settings.js b/src.old/kiri/settings.js similarity index 97% rename from src2/kiri/settings.js rename to src.old/kiri/settings.js index b01d8570..768251d1 100644 --- a/src2/kiri/settings.js +++ b/src.old/kiri/settings.js @@ -1,19 +1,23 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -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'; +"use strict"; +// 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)) || []; @@ -502,7 +506,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 ? codec.encode(widgets,{_json_:true}) : undefined; + const work = opts.work ? kiri.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 @@ -511,7 +515,7 @@ function settingsExport(opts = {}) { } const xprt = { settings: setn, - version: version, + version: kiri.version, screen: shot, space: space.info, note: note, @@ -596,7 +600,7 @@ function settingsImport(data, ask) { work.type = 100; } } - codec.decode(data.work).forEach(widget => { + kiri.codec.decode(data.work).forEach(widget => { api.platform.add(widget, 0, true, true); }); if (data.view) { @@ -757,7 +761,7 @@ function setEnableWASM(bool) { } // extend API (api.conf) -const apiConf = { +Object.assign(api.conf, { dbo: () => { return ls2o('ws-settings') }, get: getSettings, put: putSettings, @@ -773,10 +777,10 @@ const apiConf = { restore: restoreSettings, export: settingsExport, import: settingsImport, -}; +}); // extend API (api.settings) -const apiSet = { +Object.assign(api.settings, { get: getSettings, import: settingsImport, import_zip: settingsImportZip, @@ -792,16 +796,6 @@ const apiSet = { async put() {}, status: false } -}; +}); -export default { - conf: apiConf, - set: apiSet, - settings: apiSet -}; - -export { - apiConf as conf, - apiSet as set, - apiSet as settings -}; +}); diff --git a/src2/kiri/slice.js b/src.old/kiri/slice.js similarity index 96% rename from src2/kiri/slice.js rename to src.old/kiri/slice.js index dce114bf..c858c082 100644 --- a/src2/kiri/slice.js +++ b/src.old/kiri/slice.js @@ -1,8 +1,14 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base } from '../geo/base.js'; -import { Layers } from './layers.js'; -import { polygons as POLY } from '../geo/polygons.js'; +"use strict"; + +// dep: geo.base +// dep: geo.polygons +gapp.register("kiri.slice", [], (root, exports) => { + +const { base, kiri } = root; + +const POLY = base.polygons; let tracker; @@ -35,7 +41,7 @@ class Slice { */ output() { if (this.layers) return this.layers; - let layers = this.layers = new Layers(); + let layers = this.layers = new kiri.Layers(); if (tracker) { layers.setRotation(-tracker.rotation || 0); } @@ -245,10 +251,12 @@ function newSlice(z, view) { return new Slice(z, view); } -export { +gapp.overlay(kiri, { Top, Slice, newTop, newSlice, setSliceTracker -}; +}); + +}); diff --git a/src2/kiri/stack.js b/src.old/kiri/stack.js similarity index 98% rename from src2/kiri/stack.js rename to src.old/kiri/stack.js index 74a47d5b..f4866822 100644 --- a/src2/kiri/stack.js +++ b/src.old/kiri/stack.js @@ -1,9 +1,15 @@ /** Copyright Stewart Allen -- 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 */ -export class Stack { +class Stack { constructor(view, freeMem, shiny) { this._view = view; this.view = view.newGroup(); @@ -239,6 +245,8 @@ export class Stack { } } +kiri.Stack = Stack; + let shininess = 15, specular = 0x444444, emissive = 0x101010, @@ -293,3 +301,5 @@ function createLambertMaterial(color, flat) { side: flat ? THREE.DoubleSide : THREE.FrontSide }); } + +}); diff --git a/src2/kiri/stacks.js b/src.old/kiri/stacks.js similarity index 87% rename from src2/kiri/stacks.js rename to src.old/kiri/stacks.js index 562a0e32..9e9adf22 100644 --- a/src2/kiri/stacks.js +++ b/src.old/kiri/stacks.js @@ -1,8 +1,13 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { $ } from '../moto/webui.js'; -import { api } from './api.js'; -import { Stack } from './stack.js'; +"use strict"; + +// dep: kiri.stack +// use: kiri.api +// use: kiri.ui +gapp.register("kiri.stacks", [], (root, exports) => { + +const { kiri } = root; let freeMem = true, stacks = {}, @@ -13,7 +18,7 @@ let freeMem = true, function init() { labels = $("layers"); - API = api, + API = kiri.api, UC = API.uc, UI = API.ui; } @@ -75,7 +80,7 @@ function create(name, view) { } const stack = stacks[name] = { layers: [ ], - obj: new Stack(view, freeMem, api.conf.get().controller.shiny), + obj: new kiri.Stack(view, freeMem, kiri.api.conf.get().controller.shiny), add: function(layers) { let lmap = layers.layers; let map = stack.obj.addLayers(layers); @@ -88,9 +93,9 @@ function create(name, view) { mat.visible = ctrl.toggle.checked; }); if (ctrl.toggle.checked) { - api.event.emit("stack.show", label); + kiri.api.event.emit("stack.show", label); } else { - api.event.emit("stack.hide", label) + kiri.api.event.emit("stack.hide", label) } }) }; @@ -150,7 +155,7 @@ function setFraction(frac) { }); } -export default { +kiri.stacks = { clear, create, rotate, @@ -165,17 +170,4 @@ export default { setFreeMem }; -export { - clear, - create, - rotate, - remove, - getStack, - getStacks, - getRange, - setRange, - getLabels, - setVisible, - setFraction, - setFreeMem -}; +}); diff --git a/src2/kiri/stats.js b/src.old/kiri/stats.js similarity index 75% rename from src2/kiri/stats.js rename to src.old/kiri/stats.js index cfe201d3..abec28ff 100644 --- a/src2/kiri/stats.js +++ b/src.old/kiri/stats.js @@ -1,12 +1,20 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { local } from '../data/local.js'; -import { broker } from '../moto/broker.js'; -import { js2o, o2js } from './utils.js'; +"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; class Stats { constructor(db) { - this.db = db || local; + this.db = db || data.local; this.obj = js2o(this.db['stats'] || '{}'); let o = this.obj, k; for (k in o) { @@ -48,6 +56,6 @@ class Stats { } } -export const stats = new Stats(); +kiri.stats = new Stats(); -export { stats as Stats }; +}); diff --git a/src2/kiri/tools.js b/src.old/kiri/tools.js similarity index 96% rename from src2/kiri/tools.js rename to src.old/kiri/tools.js index 25f28c41..ef19635d 100644 --- a/src2/kiri/tools.js +++ b/src.old/kiri/tools.js @@ -1,10 +1,12 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { api } from './api.js'; -import { THREE } from '../ext/three.js'; +// dep: add.three +// dep: kiri.api +gapp.register("kiri.tools", (root, exports) => { 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); @@ -162,5 +164,4 @@ api.event.on('mouse.hover.up', (ev) => { endIt(); }); - -export { onLayFlatSelect, onFaceUpSelect, startIt, endIt, cleanup, scale, onDone }; +}); diff --git a/src.old/kiri/ui.js b/src.old/kiri/ui.js new file mode 100644 index 00000000..bb18c9e6 --- /dev/null +++ b/src.old/kiri/ui.js @@ -0,0 +1,857 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +// use: kiri.init +// use: kiri.main +gapp.register("kiri.ui", [], (root, exports) => { + + const { kiri } = root; + + 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; + + self.$ = function (id) { return DOC.getElementById(id) }; + + kiri.ui = { + prefix: function(pre) { prefix = pre; return kiri.ui }, + inputAction: function(fn) { inputAction = fn; return kiri.ui }, + lastChange: function() { return lastChange }, + checkpoint, + restore, + refresh, + setHidden, + setMode, + bound, + toInt, + toFloat, + 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) { + kiri.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 { api } = kiri; + 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 = [ + `
` + ]; + if (message) { + html.push(``); + } + if (opt.pre) { + html = opt.pre.appendAll(html); + } + let iid; + if (Array.isArray(input)) { + iid = `confirm-input-${rnd}`; + html.append(`
`); + } else if (input !== undefined) { + iid = `confirm-input-${rnd}`; + html.append(`
`); + } + html.append(`
`); + Object.entries(btns).forEach((row,i) => { + html.append(``); + }); + html.append(`
`); + 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 = ''; + row.classList.add('hidden'); + } else { + arr.innerHTML = ''; + 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 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 = ``; + + 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'); + let { api } = kiri; + + 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; + } + +}); diff --git a/src2/kiri/utils.js b/src.old/kiri/utils.js similarity index 89% rename from src2/kiri/utils.js rename to src.old/kiri/utils.js index eb29046a..d75db786 100644 --- a/src2/kiri/utils.js +++ b/src.old/kiri/utils.js @@ -1,6 +1,8 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { local as dataLocal } from '../data/local.js'; +"use strict"; + +gapp.register("kiri.utils", [], (root, exports) => { function parseOpt(ov) { let opt = {}, kv, kva; @@ -44,7 +46,7 @@ function js2o(s,def) { function ls2o(key,def) { // defer ref b/c it may run in a worker - return js2o(dataLocal.getItem(key),def); + return js2o(root.data.local.getItem(key),def); } // split 24 bit color into [ r, g, b ] @@ -97,14 +99,11 @@ function trackFn(fn, name) { } } -function noop() {} - -export { +exports({ trackFn, areEqual, parseOpt, encodeOpt, - noop, ajax, o2js, js2o, @@ -112,19 +111,6 @@ export { avgc, rgb, a2c -}; +}); -export let utils = { - trackFn, - areEqual, - parseOpt, - encodeOpt, - noop, - ajax, - o2js, - js2o, - ls2o, - avgc, - rgb, - a2c -}; +}); diff --git a/src2/kiri/widget.js b/src.old/kiri/widget.js similarity index 85% rename from src2/kiri/widget.js rename to src.old/kiri/widget.js index 21b0cf71..c55df903 100644 --- a/src2/kiri/widget.js +++ b/src.old/kiri/widget.js @@ -1,11 +1,23 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -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'; +"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; -const { inRange, time } = base.util; const solid_opacity = 1.0; const groups = []; @@ -16,14 +28,12 @@ let nextId = 0; function newWidget(id,group) { return new Widget(id,group) } -function catalog() { return self.kiri_catalog }; +function catalog() { return kiri.catalog } -function index() { return self.kiri_catalog.index } +function index() { return 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 || []; @@ -337,7 +347,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) { @@ -493,8 +503,8 @@ class Widget { w._move(x, y, z, abs); }); // allow for use in engine / cli - if ((x || y || z) && this.api && this.api.event) { - this.api.event.emit('widget.move', {widget: this, pos: {x, y, z}, abs}); + if ((x || y || z) && api && api.event) { + api.event.emit('widget.move', {widget: this, pos: {x, y, z}, abs}); } } @@ -545,8 +555,8 @@ class Widget { w._scale(x, y, z); }); this.center(false); - if (this.api && this.api.event) { - this.api.event.emit('widget.scale', {widget: this, x, y, z}); + if (api && api.event) { + api.event.emit('widget.scale', {widget: this, x, y, z}); } } @@ -576,8 +586,8 @@ class Widget { if (this.outline) { this.setEdges(true); } - if ((x || y || z) && this.api && this.api.event) { - this.api.event.emit('widget.rotate', {widget: this, x, y, z}); + if ((x || y || z) && api && api.event) { + api.event.emit('widget.rotate', {widget: this, x, y, z}); } } @@ -620,8 +630,8 @@ class Widget { w._mirror(); }); this.center(); - if (this.api && this.api.event) { - this.api.event.emit('widget.mirror', {widget: this}); + if (api && api.event) { + api.event.emit('widget.mirror', {widget: this}); } } @@ -748,21 +758,113 @@ 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(); - for (let slice of this.slices || []) { + if (this.slices) + this.slices.forEach(slice => { if (slice.layers) { stack.add(slice.layers); } - } + }); return Date.now() - mark; } setEdges(set) { - if (!(this.api && this.api.conf)) { + if (!(api && api.conf)) { // missing api features in engine mode return; } @@ -776,8 +878,8 @@ class Widget { } if (set) { - let dark = this.api.space.is_dark(); - let angle = this.api.conf.get().controller.edgeangle || 20; + let dark = api.space.is_dark(); + let angle = 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); @@ -787,7 +889,7 @@ class Widget { } setWireframe(set, color, opacity) { - if (!(this.api && this.api.conf)) { + if (!(api && api.conf)) { // missing api features in engine mode return; } @@ -799,7 +901,7 @@ class Widget { this.wire = null; } if (set) { - let dark = this.api.space.is_dark(); + let dark = api.space.is_dark(); let mat = new THREE.MeshBasicMaterial({ wireframe: true, color: dark ? 0xaaaaaa : 0, @@ -809,7 +911,7 @@ class Widget { let wire = widget.wire = new THREE.Mesh(mesh.geometry.shallowClone(), mat); mesh.add(wire); } - if (this.api.view.is_arrange()) { + if (api.view.is_arrange()) { this.setColor(this.color); } else { this.setColor(0x888888,undefined,false); @@ -964,4 +1066,7 @@ Widget.deleteFromState = function(id,ondone) { index().remove('ws-save-'+id, ondone); }; -export { Widget, newWidget }; +kiri.Widget = Widget; +kiri.newWidget = newWidget; + +}); diff --git a/src2/kiri/widgets.js b/src.old/kiri/widgets.js similarity index 87% rename from src2/kiri/widgets.js rename to src.old/kiri/widgets.js index dce88a44..58d939cd 100644 --- a/src2/kiri/widgets.js +++ b/src.old/kiri/widgets.js @@ -1,9 +1,14 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { $ } from '../moto/webui.js'; -import { api } from './api.js'; -import { load } from '../load/file.js'; -import { Widget, newWidget } from './widget.js'; +"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; let WIDGETS = []; @@ -31,7 +36,7 @@ function rename(sel) { return; } let widget = widgets[0]; - api.uc.prompt("new widget name", widget.meta.file || "no name").then(newname => { + kiri.ui.prompt("new widget name", widget.meta.file || "no name").then(newname => { if (newname) { widget.meta.file = newname; api.platform.changed(); @@ -84,7 +89,7 @@ function meshes() { function opacity(value) { api.widgets.each(w => w.setOpacity(value)); - api.space.update(); + moto.space.update(); } function setIndexed(value) { @@ -96,7 +101,7 @@ function setAxisIndex(value) { } // extend API (api.widgets) -export const widgets = { +const widgets = Object.assign(api.widgets, { load: Widget.loadFromCatalog, new: newWidget, map, @@ -115,4 +120,6 @@ export const widgets = { each(fn) { WIDGETS.slice().forEach(widget => fn(widget)) }, for(fn) { widgets.each(fn) }, forid(id) { return WIDGETS.filter(w => w.id === id)[0] } -}; +}); + +}); diff --git a/src2/load/3mf.js b/src.old/load/3mf.js similarity index 95% rename from src2/load/3mf.js rename to src.old/load/3mf.js index a617c716..c1d59bc3 100644 --- a/src2/load/3mf.js +++ b/src.old/load/3mf.js @@ -2,8 +2,21 @@ 'use strict'; -import { THREE } from '../ext/three.js'; -import * as JSZip from '../ext/jszip.js'; +// dep: add.three +// dep: ext.jszip +gapp.register('load.3mf', [], (root, exports) => { + +let load = self.load = self.load || {}; + +if (load.TMF) return; + +load.TMF = { + parseAsync +}; + +load.XML = { + query +}; let { BufferAttribute, Matrix4 } = THREE; @@ -16,7 +29,8 @@ let scaleMap = { "centimeter": (1 / 10) }; -export function query(node, path, fn) { +// simple query api for xml structures +function query(node, path, fn) { let collect = {}; let match = path[0].split('|').map(key => { if (key[0] === '+') { @@ -216,9 +230,9 @@ function extractItems(records) { * @param {Object} data binary file * @returns {Array} vertex face array */ -export function parseAsync(data) { +function parseAsync(data) { return new Promise(async (resolve, reject) => { - let zip = await JSZip.default.loadAsync(data); + let zip = await JSZip.loadAsync(data); let models = {}; for (let [key, value] of Object.entries(zip.files)) { if (key.endsWith(".model")) { @@ -229,4 +243,6 @@ export function parseAsync(data) { } resolve(extractItems(models)); }); -} \ No newline at end of file +} + +}); \ No newline at end of file diff --git a/src2/load/file.js b/src.old/load/file.js similarity index 73% rename from src2/load/file.js rename to src.old/load/file.js index c70ca280..f92aa5ad 100644 --- a/src2/load/file.js +++ b/src.old/load/file.js @@ -2,29 +2,36 @@ 'use strict'; -import { STL } from './stl.js'; -import * as OBJ from './obj.js'; -import * as TMF from './3mf.js'; -import * as SVG from './svg.js'; -import * as GBR from './gbr.js'; -import { load as pngLoad } from './png.js'; +(function() { + +let load = self.load = self.load || {}; +if (load.File) return; + +// dep: load.3mf +// dep: load.obj +// dep: load.stl +// dep: load.svg +// dep: load.url +// dep: load.png +// dep: load.gbr +gapp.register('load.file', []); const types = { stl(data, file, resolve, reject, opt = {}) { resolve([{ - mesh: new STL().parse(data), file + mesh: new load.STL().parse(data), file }]); }, obj(data, file, resolve, reject, opt = {}) { - resolve(OBJ.parse(data).map(m => { + resolve(load.OBJ.parse(data).map(m => { return { mesh: m.toFloat32(), file: nameOf(file, m.name, 1) } })); }, "3mf"(data, file, resolve, reject, opt = {}) { let i = 1; - TMF.parseAsync(data).then((meshes) => { + load.TMF.parseAsync(data).then((meshes) => { resolve(meshes.map(m => { return { mesh: m.faces.toFloat32(), file: nameOf(file, m.name, i++) } })); @@ -32,29 +39,37 @@ const types = { }, svg(data, file, resolve, reject, opt = {}) { - let out = SVG.parse(data, opt); + let out = load.SVG.parse(data, opt); resolve(opt.flat ? out : out.map(m => { return { mesh: m.toFloat32(), file } })); }, png(data, file, resolve, reject, opt = {}) { - pngLoad.PNG.parse(data, { + load.PNG.parse(data, { ...opt, - done(vertices) { resolve({ mesh: vertices, file }) }, - error(err) { reject(err) } + done(data) { + resolve({ mesh: data, file }); + } }); }, gbr(data, file, resolve, reject, opt = {}) { if (opt.flat) { - resolve([ GBR.parse(data) ]) + resolve([ load.GBR.parse(data) ]) } else { - resolve([{ mesh: GBR.toMesh(data).toFloat32(), file }]) + resolve([{ mesh: load.GBR.toMesh(data).toFloat32(), file }]) } } }; const as_buffer = [ "stl", "png", "3mf" ]; +load.File = { + types, + as_buffer, + load_data, + load: load_file +}; + function nameOf(file, part, i) { let lid = file.lastIndexOf('.'); if (!part && lid > 0) { @@ -64,7 +79,7 @@ function nameOf(file, part, i) { } function load_data(data, file, ext, opt = {}) { - ext = ext || file.name.toLowerCase().split('.').pop(); + ext = ext || name.toLowerCase().split('.').pop(); return new Promise((resolve, reject) => { let fn = types[ext]; if (fn) { @@ -100,4 +115,4 @@ function load_file(file, opt) { }); } -export { types, as_buffer, load_data, load_file, load_file as load }; +})(); diff --git a/src2/load/gbr.js b/src.old/load/gbr.js similarity index 85% rename from src2/load/gbr.js rename to src.old/load/gbr.js index eac48dcd..45497112 100644 --- a/src2/load/gbr.js +++ b/src.old/load/gbr.js @@ -1,12 +1,22 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { polygons } from '../geo/polygons.js'; -import { CSG } from '../geo/csg.js'; -import * as TracespaceParser from '../ext/gerber.js'; -import { newPolygon } from '../geo/polygon.js'; -import { newBounds } from '../geo/bounds.js'; +// use: geo.bounds +// use: geo.polygon +// use: geo.polygons +// use: ext.gerber +// use: geo.csg +gapp.register("load.gbr", (root, exports) => { -export function toMesh(text, opt = {}) { +const { load } = root; +const POLYS = base.polygons; +const CSG = base.CSG; + +load.GBR = { + parse, + toMesh +}; + +function toMesh(text, opt = {}) { const progress = opt.progress || function(){}; const debug = opt.debug ? console.log : function(){}; @@ -19,7 +29,7 @@ export function toMesh(text, opt = {}) { // should produce one top level poly which is the board outline // next level down should be the remaining copper trace which we // subtract from the board to produce paths that can be milled - const nest = polygons.nest([...closed, ...circs, ...rects], true); + const nest = POLYS.nest([...closed, ...circs, ...rects], true); debug({ nest }); if (nest.length === 0) return [].toFloat32(); @@ -39,7 +49,7 @@ export function toMesh(text, opt = {}) { for (let p of nest) { bounds.merge(p.bounds); } - const rect = newPolygon().centerRectangle(bounds.center(0), bounds.width() + offset, bounds.height() + offset); + const rect = base.newPolygon().centerRectangle(bounds.center(0), bounds.width() + offset, bounds.height() + offset); for (let n of nest) { n.depth++; } @@ -82,7 +92,7 @@ export function toMesh(text, opt = {}) { // union open trace expansions const opn = []; - for (let poly of polygons.union(open_exp, 0, true)) { + for (let poly of POLYS.union(open_exp, 0, true)) { const tool = poly.tool; opn.push(poly.extrude(z0 + (opt.raise || 0))); } @@ -123,7 +133,7 @@ export function toMesh(text, opt = {}) { return CSG.toPositionArray(meshOut); } -export function parse(text) { +function parse(text) { let fact = 1000000; let tools = {}; let tool; @@ -132,7 +142,7 @@ export function parse(text) { let circs = []; let rects = []; let last = {}; - let bounds = newBounds(); + let bounds = base.newBounds(); let p = TracespaceParser.createParser(); p.feed(text); let r = p.results(); @@ -168,7 +178,7 @@ export function parse(text) { if (true && poly && last.x === x && last.y === y && last.tool === tool) { // console.log('continuation'); } else { - poly = newPolygon().setOpen().add(pos.x, pos.y, 0); + poly = base.newPolygon().setOpen().add(pos.x, pos.y, 0); poly.tool = tool; polys.push(poly); } @@ -182,16 +192,16 @@ export function parse(text) { switch (shape.type) { case 'obround': if (shape.xSize === shape.ySize) { - circs.push(newPolygon().centerCircle(pos, shape.xSize / 2, 20)); + circs.push(base.newPolygon().centerCircle(pos, shape.xSize / 2, 20)); } else { console.log('TODO', shape); } break; case 'circle': - circs.push(newPolygon().centerCircle(pos, shape.diameter / 2, 20)); + circs.push(base.newPolygon().centerCircle(pos, shape.diameter / 2, 20)); break; case 'rectangle': - rects.push(newPolygon().centerRectangle(pos, shape.xSize, shape.ySize)); + rects.push(base.newPolygon().centerRectangle(pos, shape.xSize, shape.ySize)); break; default: console.log('TODO', shape); @@ -223,5 +233,9 @@ export function parse(text) { open.push(poly); } } + // const nest = base.polygons.nest([...closed, ...circs, ...rects], true); + // console.log({ nest }); return { open, closed, circs, rects }; } + +}); diff --git a/src2/load/obj.js b/src.old/load/obj.js similarity index 78% rename from src2/load/obj.js rename to src.old/load/obj.js index 4a8b1f01..2986e3f6 100644 --- a/src2/load/obj.js +++ b/src.old/load/obj.js @@ -2,34 +2,36 @@ 'use strict'; -import { util } from '../geo/base.js'; +(function() { + +let load = self.load = self.load || {}; +if (load.OBJ) return; + +gapp.register('load.obj'); + +load.OBJ = { + parse, + parseAsync +}; /** * @param {String} text * @returns {Array} vertex face array */ -export function parseAsync(text) { + +function parseAsync(text) { return new Promise((resolve,reject) => { resolve(parse(text)); }); } -export function parse(text) { +function parse(text) { + let lines = text.replaceAll(/ +/g,' ').split('\n').map(l => l.trim()); let verts = []; let faces = []; let objs = [ faces ]; - // Polyfill for appendAll if not present - if (!Array.prototype.appendAll) { - Object.defineProperty(Array.prototype, 'appendAll', { - value: function(arr) { - for (let v of arr) this.push(v); - }, - enumerable: false - }); - } - for (let line of lines) { let toks = line.split(' '); switch (toks.shift()) { @@ -44,7 +46,7 @@ export function parse(text) { let tok = toks.map(f => parseInt(f.split('/')[0])); if (tok.length > 3) { const p = tok.map(t => verts[t-1]).flat(); - const t = util.triangulate(p, [], 3, true); + const t = base.util.triangulate(p, [], 3, true); const r = t.map(i => tok[i]); for (let v of r) { faces.appendAll(verts[v - 1]); @@ -68,3 +70,5 @@ export function parse(text) { } return objs; } + +})(); diff --git a/src2/load/png.js b/src.old/load/png.js similarity index 97% rename from src2/load/png.js rename to src.old/load/png.js index bd029144..c01b63cf 100644 --- a/src2/load/png.js +++ b/src.old/load/png.js @@ -1,16 +1,17 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { PNG } from '../ext/pngjs.esm.js'; +// use: ext.pngjs +gapp.register("load.png", (root, exports) => { -export const load = { - PNG: { - parseAsync, - parse - } +const { load } = root; + +load.PNG = { + parseAsync, + parse }; function parseAsync(bin, opt) { - return new Promise((resolve, reject) => { + return new Promise((resolve,reject) => { parse(bin, { ...opt, done(vertices) { resolve(vertices) } @@ -254,4 +255,4 @@ function parse(bin, opt = {}) { }); } -function noop() {} +}); diff --git a/src.old/load/stl.js b/src.old/load/stl.js new file mode 100644 index 00000000..866fd86a --- /dev/null +++ b/src.old/load/stl.js @@ -0,0 +1,268 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +'use strict'; + +(function() { + +let load = self.load = self.load || {}; +if (load.STL) return; + +gapp.register('load.stl'); + +const SP = STL.prototype; +const CDH = 'Content-Disposition'; + +load.STL = STL; + +/** + * @constructor + */ +function STL() { + this.vertices = null; + this.normals = null; + this.colors = null; +} + +SP.load = function(url, callback, formdata, scale, credentials, headers) { + const stl = this; + + fetch(url, { + method: formdata ? 'POST' : 'GET', + credentials: credentials ? credentials : 'same-origin', + body: formdata, + ...(headers ? {headers: headers} : {}) + }).then(response => { + if (response.status === 200 || response.status === 0) { + response.arrayBuffer().then(buffer => { + stl.parse(buffer, scale); + + let cdhVal = response.headers.get(CDH); + if (typeof cdhVal === "string") { + cdhVal = cdhVal.split(';').map(v => v.trim()).filter(v => { + return v.indexOf('filename=') === 0; + }).map(v => { + return v.substring(10,v.length-1); + })[0]; + } + if (callback) callback(stl.vertices, cdhVal); + }); + } else { + if (callback) callback(null, response.statusText); + } + }).catch(err => { + if (callback) callback(null, err) + }); +}; + +SP.encode = function(vertices, normals) { + if (!(vertices && vertices.length % 3 === 0)) throw "invalid vertices"; + + let vc = vertices.length / 3, + bs = (vc * 16) + (vc * (2/3)) + 84, + bin = new ArrayBuffer(bs), + writer = new DataView(bin), + i = 0, + j = 0, + pos = 80; + + function writeInt16(val) { + writer.setUint16(pos, val, true); + pos += 2; + } + + function writeInt32(val) { + writer.setUint32(pos, val, true); + pos += 4; + } + + function writeFloat(val) { + writer.setFloat32(pos, val, true); + pos += 4; + } + + function writeVertex() { + writeFloat(vertices[i++]); // x + writeFloat(vertices[i++]); // y + writeFloat(vertices[i++]); // z + } + + writeInt32(vc / 3); + while (i < vertices.length) { + writeFloat(normals ? normals[j++] : 0); // norm x + writeFloat(normals ? normals[j++] : 0); // norm y + writeFloat(normals ? normals[j++] : 0); // norm z + writeVertex(); // p1 + writeVertex(); // p2 + writeVertex(); // p3 + writeInt16(0); // attributes + } + + return bin; +}; + +/** + * @param {ArrayBuffer} data + * @param {number} scale + * @returns vertex array + */ +SP.parse = function(data, scale) { + let binData = this.convertToBinary(data); + + let isBinary = function () { + let expect, face_size, n_faces, reader; + reader = new DataView(binData); + face_size = (32 / 8 * 3) + ((32 / 8 * 3) * 3) + (16 / 8); + n_faces = reader.getUint32(80,true); + expect = 80 + (32 / 8) + (n_faces * face_size); + return expect === reader.byteLength; + }; + + return isBinary() + ? this.parseBinary(binData, scale) + : this.parseASCII(this.convertToString(data), scale); +}; + +SP.parseAsync = function(data, scale) { + return new Promise((resolve, reject) => { + resolve(this.parse(data, scale)); + }); +}; + +SP.parseBinary = function(data, scale = 1) { + let reader = new DataView(data), + faces = reader.getUint32 (80, true), + r, g, b, hasColors = false, colors, + defaultR, defaultG, defaultB, alpha; + + // check for default color in STL header ("COLOR=rgba" sequence). + for (let index = 0; index < 80 - 10; index++) { + if ((reader.getUint32(index, false) == 0x434F4C4F /*COLO*/) && + (reader.getUint8(index + 4) == 0x52 /*'R'*/) && + (reader.getUint8(index + 5) == 0x3D /*'='*/)) { + hasColors = true; + colors = new Float32Array(faces * 3 * 3); + defaultR = reader.getUint8(index + 6) / 255; + defaultG = reader.getUint8(index + 7) / 255; + defaultB = reader.getUint8(index + 8) / 255; + alpha = reader.getUint8(index + 9) / 255; + } + } + + let offset = 0, + dataOffset = 84, + faceLength = 12 * 4 + 2, + vertices = new Float32Array(faces * 3 * 3), + normals = new Float32Array(faces * 3 * 3); + + colors = hasColors ? new Uint16Array(faces * 3 * 3) : null; + + for (let face = 0; face < faces; face ++) { + + let start = dataOffset + face * faceLength, + normalX = reader.getFloat32(start, true), + normalY = reader.getFloat32(start + 4, true), + normalZ = reader.getFloat32(start + 8, true); + + if (hasColors) { + let packedColor = reader.getUint16(start + 48, true); + if ((packedColor & 0x8000) === 0) { // facet has its own unique color + r = (packedColor & 0x1F) / 31; + g = ((packedColor >> 5) & 0x1F) / 31; + b = ((packedColor >> 10) & 0x1F) / 31; + } else { + r = defaultR; + g = defaultG; + b = defaultB; + } + } + + let i = 1, vertexstart; + + while (i <= 3) { + vertexstart = start + (i++) * 12; + vertices[offset ] = reader.getFloat32 (vertexstart, true) * scale; + vertices[offset + 1] = reader.getFloat32 (vertexstart + 4, true) * scale; + vertices[offset + 2] = reader.getFloat32 (vertexstart + 8, true) * scale; + normals[offset ] = normalX; + normals[offset + 1] = normalY; + normals[offset + 2] = normalZ; + if (hasColors) { + colors[offset ] = r; + colors[offset + 1] = g; + colors[offset + 2] = b; + } + offset += 3; + } + } + + this.vertices = vertices; + this.normals = normals; + this.colors = colors; + + return vertices; +}; + +SP.parseASCII = function(data, scale = 1) { + let result, + resultText, + patternNormal, + patternVertex, + vertices = [], + normals = [], + patternFace = /facet([\s\S]*?)endfacet/g; + + while ((result = patternFace.exec(data)) !== null) { + resultText = result[0]; + patternNormal = /normal[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g; + patternVertex = /vertex[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g; + while ((result = patternNormal.exec(resultText)) !== null) { + normals.push(parseFloat(result[1])); + normals.push(parseFloat(result[3])); + normals.push(parseFloat(result[5])); + } + while ((result = patternVertex.exec(resultText)) !== null) { + vertices.push(parseFloat(result[1]) * scale); + vertices.push(parseFloat(result[3]) * scale); + vertices.push(parseFloat(result[5]) * scale); + } + } + + let vToFloat32 = new Float32Array(vertices.length), + nToFloat32 = new Float32Array(normals.length), + i; + + for (i=0; i -- All Rights Reserved */ -import { newPolygon } from '../geo/polygon.js'; -import { polygons } from '../geo/polygons.js'; -import { THREE } from '../ext/three.js'; +// use: add.three +gapp.register("load.svg", (root, exports) => { -export function parseAsync(text, opt) { +const { load } = root; + +load.SVG = { + parse, + parseAsync +}; + +function parseAsync(text, opt) { return new Promise((resolve,reject) => { resolve(parse(text, opt)); }); } -export function parse(text, opt = { }) { +function parse(text, opt = { }) { const justPoly = opt.flat || false; const fromSoup = opt.soup !== false || justPoly; const rez = (opt.resolution || 1); @@ -41,9 +47,10 @@ export function parse(text, opt = { }) { return curve.getPoints(segs); }).flat(); if (points.length < 3) { + // console.log({ sub, length, points }); continue; } - let poly = newPolygon().addPoints(points.map(p => newPolygon.newPoint(p.x, -p.y, 0))); + let poly = base.newPolygon().addPoints(points.map(p => base.newPoint(p.x, -p.y, 0))); if (poly.appearsClosed()) poly.points.pop(); if (type === 'polyline') poly.setOpen(true); poly._svg = { width, miter }; @@ -54,8 +61,9 @@ export function parse(text, opt = { }) { } } - const sub = fromSoup ? polygons.nest(polys) : polys; + const sub = fromSoup ? base.polygons.nest(polys) : polys; const nest = sub.filter(p => { + // filter duplicates for (let pc of polys) { if (pc === p) { return true; @@ -76,3 +84,5 @@ export function parse(text, opt = { }) { return objs; } + +}); diff --git a/src2/load/url.js b/src.old/load/url.js similarity index 83% rename from src2/load/url.js rename to src.old/load/url.js index 3cb8d711..d2021ea4 100644 --- a/src2/load/url.js +++ b/src.old/load/url.js @@ -2,14 +2,20 @@ 'use strict'; -import { STL } from './stl.js'; -import { OBJ } from './obj.js'; -import { TMF } from './3mf.js'; -import { SVG } from './svg.js'; +(function() { + +let load = self.load = self.load || {}; +if (load.URL) return; + +gapp.register('load.url'); + +load.URL = { + load: load_url +}; const CDH = 'Content-Disposition'; -export function load_url(url, options = {}) { +function load_url(url, options = {}) { return new Promise((resolve, reject) => { let xhr = new XMLHttpRequest(); let file = options.file || options.filename || (((url.split('?')[0]).split('#')[0]).split('/')).pop(); @@ -41,21 +47,21 @@ export function load_url(url, options = {}) { switch (ext) { case "stl": resolve([{ - mesh: new STL().parse(data), file + mesh: new load.STL().parse(data), file }]); break; case "obj": - resolve(OBJ.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); + resolve(load.OBJ.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); break; case "3mf": - TMF.parseAsync(data).then((meshes) => { + load.TMF.parseAsync(data).then((meshes) => { resolve(meshes.map(m => { return { mesh: m.faces.toFloat32(), file } })); }); break; case "svg": - resolve(SVG.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); + resolve(load.SVG.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); break; default: reject(`unknown file type: "${ext}" from ${url}`); @@ -78,4 +84,6 @@ export function load_url(url, options = {}) { xhr.responseType = datatype; xhr.send(formdata); }); -} +}; + +})(); diff --git a/src/main/gapp.js b/src.old/main/gapp.js similarity index 100% rename from src/main/gapp.js rename to src.old/main/gapp.js diff --git a/src.old/main/kiri.js b/src.old/main/kiri.js new file mode 100644 index 00000000..69e5ba14 --- /dev/null +++ b/src.old/main/kiri.js @@ -0,0 +1,41 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +gapp.main({ + app: "kiri", + + pre(root) { + let mods = root.kirimod = ( root.kirimod || [] ); + + let kiri = root.kiri = { + beta: 0, + driver: { + // attached driver modules + }, + load(fn) { + // modules register exec() functions + mods.push(fn); + }, + load_exec(api) { + // process all module exec() functions + const saferun = (fn) => { + try { + fn(api || kiri.api); + } catch (error) { + console.log({ module_error: error }); + } + }; + // complete module loading + mods.forEach(fn => saferun(fn)); + // rewrite load() to run immediately post-finalize + kiri.load = (fn) => saferun(fn); + } + }; + }, + + post(root) { + // complete module loading + root.kiri.load_exec(); + } +}); diff --git a/src2/main/mesh.js b/src.old/main/mesh.js similarity index 82% rename from src2/main/mesh.js rename to src.old/main/mesh.js index fb528139..128fc4f4 100644 --- a/src2/main/mesh.js +++ b/src.old/main/mesh.js @@ -1,50 +1,49 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import '../add/array.js'; -import '../add/class.js'; -import '../add/three.js'; -import '../mesh/build.js'; +"use strict"; -import { api } from '../mesh/api.js'; -import { space } from '../moto/space.js'; -import { broker } from '../moto/broker.js'; -import { $, $d, h, estop } from '../moto/webui.js'; -import { handles } from '../mesh/handles.js'; -import { client as motoClient } from '../moto/client.js'; -import { split as meshSplit } from '../mesh/split.js'; -import { model as meshModel, materials } from '../mesh/model.js'; -import { edges as meshEdges } from '../mesh/edges.js'; -import { open as dataOpen } from '../data/index.js'; -import { load as fileLoad } from '../load/file.js'; -import { THREE } from '../ext/three.js'; +// dep: moto.license +// dep: moto.webui +// dep: moto.client +// dep: moto.broker +// dep: moto.space +// dep: data.index +// dep: mesh.api +// dep: mesh.split +// dep: mesh.edges +// dep: mesh.model +// dep: mesh.build +// dep: load.file +// use: geo.polygons +gapp.main("main.mesh", [], (root) => { -const version = '1.5.7'; +const { Quaternion } = THREE; +const { mesh, moto } = root; +const { broker } = gapp; +const { space } = moto; + +const version = mesh.version = '1.5.6'; const call = broker.send; const dbindex = [ "admin", "space" ]; -const { Quaternion } = THREE; - function log() { - return api.log.emit(...arguments); + return mesh.api.log.emit(...arguments); } // set below. called once the DOM readyState = complete // this is the main() entrypoint called after all dependents load function init() { - let stores = dataOpen('mesh', { stores: dbindex, version: 4 }).init(), + let stores = data.open('mesh', { stores: dbindex, version: 4 }).init(), dark = false, ortho = false, zoomrev = true, zoomspd = 1, platform = space.platform, - db = api.db = { + db = mesh.db = { admin: stores.promise('admin'), space: stores.promise('space') }; - // initialize the API (to avoid circular dependencies) - api.init(); - // mark init time and use count db.admin.put("init", Date.now()); db.admin.get("uses").then(v => db.admin.put("uses", (v||0) + 1)); @@ -79,13 +78,13 @@ function init() { space.view.setZoom(zoomrev, zoomspd); // reload stored space when worker is ready - motoClient.on('ready', restore_space); + moto.client.on('ready', restore_space); // start worker - motoClient.start('../lib/mesh/work.js?' + version); + moto.client.start(`/code/mesh_work?${gapp.version}`); // trigger space event binding - call.space_init({ space: space, platform }); + call.space_init({ space, platform }); // trigger ui building call.ui_build(); @@ -93,8 +92,10 @@ function init() { // restore space layout and view from previous session async function restore_space() { - const db_admin = api.db.admin; - const db_space = api.db.space; + const { api } = mesh; + const space = moto.space; + const db_admin = mesh.db.admin; + const db_space = mesh.db.space; // let mcache = {}; await db_admin.get("camera") .then(saved => { @@ -125,17 +126,17 @@ async function restore_space() { return { id, md: cached[id] } }) .filter(r => r.md) // filter cache misses - .map(r => new meshModel(r.md, r.id).applyMeta(mcache[r.id])) + .map(r => new mesh.model(r.md, r.id).applyMeta(mcache[r.id])) if (models.length) { log(`restored ${models.length} model(s)`); - api.group.new(models, id).applyMeta(mcache[id]) + mesh.api.group.new(models, id).applyMeta(mcache[id]) } else { log(`removed empty group ${id}`); db_space.remove(id); } } else if (data.type === 'sketch') { claimed.push(id); - api.add.sketch({ id, ...data }); + mesh.api.add.sketch({ id, ...data }); } } for (let id of claimed) { @@ -176,12 +177,9 @@ async function restore_space() { }).finally(() => { // hide loading curtain $d('curtain','none'); - // restore handles visibility - handles.setEnabled(api.prefs.map.space.bounds); if (api.prefs.map.info.welcome !== false) { api.welcome(version); } - broker.publish("app_ready"); }); } @@ -189,7 +187,9 @@ async function restore_space() { function space_init(data) { let platcolor = 0x00ff00; let { space, platform } = data; + let { api } = mesh; let { selection } = api; + // add file drop handler space.event.addHandlers(self, [ 'drop', (evt) => { @@ -228,51 +228,54 @@ function space_init(data) { return api.mode.face(); case 'Digit6': return api.mode.edge(); - case 'KeyB': - return selection.boundsBox({toggle:true}); - case 'KeyC': - return selection.centerXY().focus(); + case 'KeyQ': + return api.settings(); + case 'KeyI': + return shiftKey ? api.tool.invert() : api.file.import(); + case 'KeyX': + return api.file.export(); case 'KeyD': return shiftKey && api.tool.duplicate(); + case 'KeyC': + return selection.centerXY().focus(); + case 'KeyF': + return shiftKey ? selection.focus() : selection.floor().focus(); + case 'KeyM': + return shiftKey ? api.tool.merge() : api.tool.mirror(); + case 'KeyU': + return shiftKey && api.tool.union(); + case 'KeyA': + if (api.mode.is([ api.modes.edge ])) return mesh.edges.add(); + return shiftKey && api.tool.analyze(); case 'KeyE': if (api.mode.is([ api.modes.sketch ])) { estop(evt); return api.sketch.extrude(); } return; - case 'KeyF': - return shiftKey ? selection.focus() : selection.floor().focus(); + case 'KeyV': + return shiftKey ? selection.show() : selection.focus(); + case 'KeyN': + return shiftKey ? estop(evt, api.tool.rename()) : api.normals(); + case 'KeyW': + return api.wireframe(); case 'KeyG': return shiftKey ? (api.mode.is([ api.modes.sketch ]) ? api.sketch.arrange.group() : api.tool.regroup()) : api.grid(); - case 'KeyH': - return shiftKey ? selection.hide() : space.view.home(); - case 'KeyI': - return api.file.import(); case 'KeyL': return api.log.toggle({ spinner: false }); - case 'KeyM': - return shiftKey ? api.tool.merge() : api.tool.mirror(); - case 'KeyN': - return shiftKey ? api.tool.invert() : api.normals(); - case 'KeyQ': - return api.settings(); - case 'KeyR': - return estop(evt, api.tool.rename()); case 'KeyS': if (!api.mode.is([ api.modes.object ])) return; - return shiftKey ? selection.visible({toggle:true}) : meshSplit.start(); + return shiftKey ? selection.visible({toggle:true}) : mesh.split.start(); + case 'KeyB': + return selection.boundsBox({toggle:true}); + case 'KeyH': + return shiftKey ? selection.hide() : space.view.home(); case 'KeyT': return shiftKey ? api.tool.triangulate() : space.view.top(); - case 'KeyU': - return shiftKey && api.tool.union(); - case 'KeyV': - return shiftKey ? selection.show() : selection.focus(); - case 'KeyW': - return shiftKey ? api.wireframe() : api.wireedges(); - case 'KeyX': - return api.file.export(); + case 'KeyZ': + return space.view.reset(); } }, 'keydown', evt => { @@ -292,35 +295,18 @@ function space_init(data) { let rot, floor = api.prefs.map.space.floor !== false; switch (code) { case 'KeyA': - estop(evt); - if (api.mode.is([ api.modes.edge ])) { - return meshEdges.add(); - } if (metaKey || ctrlKey) { if (!selection.sketch()?.selection.all()) { api.mode.object(); selection.set(api.group.list()); } - } else if (shiftKey) { - api.tool.analyze(); + estop(evt); } break; - case 'KeyY': - estop(evt); - if (metaKey || ctrlKey) { - return shiftKey ? api.history.undo() : api.history.redo(); - } - case 'KeyZ': - estop(evt); - if (metaKey || ctrlKey) { - return shiftKey ? api.history.redo() : api.history.undo(); - } else { - return space.view.reset(); - } case 'Escape': if (selection.clear()) { - meshEdges.clear(); - meshSplit.end(); + mesh.edges.clear(); + mesh.split.end(); } estop(evt); break; @@ -374,7 +360,11 @@ function space_init(data) { let obj = int?.object; if (obj) api.selection.drag({ start: int.object }); if (obj) obj.sketch_item?.sketch.drag({ start: int.object }); - return selection.count() && !event.altKey ? api.objects(true) : undefined; + if (event?.shiftKey) { + return api.objects(); + } else { + return undefined; + } }); // called two ways: @@ -390,8 +380,8 @@ function space_init(data) { // a selection was made const { model, sketch, sketch_item } = int?.object || {}; const { altKey, ctrlKey, metaKey, shiftKey } = event; - if (meshSplit.active()) { - return meshSplit.select(model); + if (mesh.split.active()) { + return mesh.split.select(model); } if (model) { const group = model.group; @@ -402,7 +392,7 @@ function space_init(data) { // rotate normal using group's matrix const normal = shiftKey ? int.face.normal.applyQuaternion(q) : undefined; // y,z swap due to world rotation for orbit controls - api.focus({ center: { x, y:-z, z:y }, normal }); + api.focus({center: { x, y:-z, z:y }, normal}); let one = api.sketch.selected.one; if (one && confirm('attach sketch to face?')) { one.center = { @@ -415,7 +405,7 @@ function space_init(data) { }; one.render(); } - } else if (ctrlKey && selection.contains(model)) { + } else if (ctrlKey) { // rotate selected face towawrd z "floor" group.rotateTowardZ(int.face.normal); selection.update(); @@ -441,7 +431,7 @@ function space_init(data) { opt); break; case modes.edge: - meshEdges.select(); + mesh.edges.select(); break; } } @@ -460,8 +450,6 @@ function space_init(data) { } } else if (sketch_item) { sketch_item.toggle(); - } else { - console.log('unknown selection'); } } else { // return objects upSelect can choose from @@ -478,7 +466,7 @@ function space_init(data) { space.mouse.onDrag((delta, offset, up = false) => { let { mode, modes } = api; if (delta) { - if (selection.count()) { + if (delta.event?.shiftKey && selection.count()) { selection.drag({ delta, offset }); } } else if (up) { @@ -494,6 +482,7 @@ function space_init(data) { function load_files(files) { log(`loading file...`); + let api = mesh.api; let has_image = false; let has_svg = false; let has_gbr = false; @@ -504,7 +493,7 @@ function load_files(files) { has_gbr = has_gbr || file.name.toLowerCase().endsWith(".gbr") > 0; } if (sketch && has_gbr) { - fileLoad([...files], { flat: true }).then(layers => { + load.File.load([...files], { flat: true }).then(layers => { for (let layer of layers.flat()) { let { circs, closed, open, rects } = layer; open = open.map(poly => { @@ -512,7 +501,7 @@ function load_files(files) { return diam ? poly.offset_open(diam / 2, 'round') : null; }).filter(p => p).flat(); for (let set of [ closed, open, circs, rects ]) { - let group = api.util.uuid(); + let group = mesh.util.uuid(); for (let poly of set) { sketch.add.polygon({ poly, group }); } @@ -521,13 +510,13 @@ function load_files(files) { }); } else if (sketch && has_svg) { - fileLoad([...files], { flat: true }) + load.File.load([...files], { flat: true }) .then(polys => polys.forEach(set => { - let group = api.util.uuid(); + let group = mesh.util.uuid(); set.forEach(poly => sketch.add.polygon({ poly, group })) })) .catch(error => dbug.error(error)) - .finally(() => api.log.hide()); + .finally(() => mesh.api.log.hide()); } else if (has_svg) { api.modal.dialog({ @@ -599,16 +588,16 @@ function load_files(files) { } function load_files_opt(files, opt) { - return fileLoad([...files], opt) + return load.File.load([...files], opt) .then(data => call.space_load(data)) .catch(error => log(error).pin({}) && dbug.error(error)) - // .finally(() => api.log.hide()); + // .finally(() => mesh.api.log.hide()); } // add object loader function space_load(data) { if (data && data.length && (data = data.flat()).length) - api.group.new(data.map(el => new meshModel(el))) + mesh.api.group.new(data.map(el => new mesh.model(el))) .promote() .focus(); } @@ -626,7 +615,7 @@ function key_once_cancel(code) { } function store_meta() { - api.db.admin.put("meta", metaCache); + mesh.db.admin.put("meta", metaCache); } function update_meta(id, data) { @@ -654,16 +643,16 @@ function object_destroy(id) { // listen for changes like dark mode toggle function set_darkmode(dark) { - let { prefs, model } = api; - let { sky, platform } = space; + let { prefs, model } = mesh.api; + let { sky, platform } = moto.space; prefs.map.space.dark = dark; if (dark) { - materials.wireframe.color.set(0xaaaaaa); - materials.wireline.color.set(0xaaaaaa); + mesh.material.wireframe.color.set(0xaaaaaa); + mesh.material.wireline.color.set(0xaaaaaa); $('app').classList.add('dark'); } else { - materials.wireframe.color.set(0,0,0); - materials.wireline.color.set(0,0,0); + mesh.material.wireframe.color.set(0,0,0); + mesh.material.wireline.color.set(0,0,0); $('app').classList.remove('dark'); } sky.set({ @@ -680,7 +669,7 @@ function set_darkmode(dark) { colorMinor: 0xeeeeee, }, }); - api.updateFog(); + mesh.api.updateFog(); platform.setSize(); for (let m of model.list()) { m.normals({ refresh: true }); @@ -689,19 +678,13 @@ function set_darkmode(dark) { } function set_normals_length(length) { - let { prefs, model } = api; + let { prefs } = mesh.api; prefs.map.normals.length = length || 1; prefs.save(); - // Update existing normals - for (let m of model.list()) { - if (m.normals()) { - m.normals({ refresh: true }); - } - } } function set_normals_color(color) { - let { prefs, model } = api; + let { prefs } = mesh.api; let { map } = prefs; if (map.space.dark) { map.normals.color_dark = color || 0; @@ -709,47 +692,34 @@ function set_normals_color(color) { map.normals.color_lite = color || 0; } prefs.save(); - // Update existing normals - for (let m of model.list()) { - if (m.normals()) { - m.normals({ refresh: true }); - } - } } function set_surface_radians(radians) { - let { prefs } = api; + let { prefs } = mesh.api; prefs.map.surface.radians = parseFloat(radians || 0.1); prefs.save(); } function set_surface_radius(radius) { - let { prefs } = api; + let { prefs } = mesh.api; prefs.map.surface.radius = parseFloat(radius || 0.2); prefs.save(); } function set_wireframe_opacity(opacity) { - let { prefs, model } = api; + let { prefs } = mesh.api; prefs.map.wireframe.opacity = parseFloat(opacity || 0.15); prefs.save(); - // Update existing wireframes - for (let m of model.list()) { - if (m.wireframe().enabled) { - m.opacity({temp: prefs.map.wireframe.opacity}); - } - } } function set_wireframe_fog(fogx) { - let { prefs } = api; + let { prefs } = mesh.api; prefs.map.wireframe.fog = parseFloat(fogx || 3); prefs.save(); - api.updateFog(); } function set_snap_value(snap) { - let { prefs } = api; + let { prefs } = mesh.api; prefs.map.space.snap = parseFloat(snap || 1); prefs.save(); } @@ -774,30 +744,14 @@ broker.listeners({ set_snap_value }); -init(); +// remove version cache bust from url +window.history.replaceState({},'','/mesh/'); -export { - version, - init, - log, - restore_space, - space_init, - load_files, - load_files_opt, - space_load, - key_once, - key_once_cancel, - store_meta, - update_meta, - object_visible, - object_meta, - object_destroy, - set_darkmode, - set_normals_length, - set_normals_color, - set_surface_radians, - set_surface_radius, - set_wireframe_opacity, - set_wireframe_fog, - set_snap_value -}; +// setup init() trigger when dom + scripts complete +document.onreadystatechange = function() { + if (document.readyState === 'complete') { + init(); + } +} + +}); diff --git a/src/main/meta.js b/src.old/main/meta.js similarity index 100% rename from src/main/meta.js rename to src.old/main/meta.js diff --git a/src2/main/service.js b/src.old/main/service.js similarity index 95% rename from src2/main/service.js rename to src.old/main/service.js index 6d8af693..a34d1595 100644 --- a/src2/main/service.js +++ b/src.old/main/service.js @@ -1,8 +1,7 @@ const service_worker = navigator.serviceWorker; -import { version } from '../moto/license.js'; - async function start_service_worker() { + const version = self.gapp.version; function debug() { console.log(`[${version}]`, ...arguments); } diff --git a/src2/mesh/api.js b/src.old/mesh/api.js similarity index 86% rename from src2/mesh/api.js rename to src.old/mesh/api.js index 75bc7381..bf67a9dc 100644 --- a/src2/mesh/api.js +++ b/src.old/mesh/api.js @@ -1,22 +1,27 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { broker } from '../moto/broker.js'; -import { history } from './history.js'; -import { client as motoClient } from '../moto/client.js'; -import { space as motoSpace } from '../moto/space.js'; -import { base } from '../geo/base.js'; -import { handles } from './handles.js'; -import { util as meshUtil } from './util.js'; -import { edges as meshEdges } from './edges.js'; -import { group as meshGroup } from './group.js'; -import { model as meshModel } from './model.js'; -import { sketch as meshSketch } from './sketch.js'; -import { newPolygon } from '../geo/polygon.js'; -import { THREE } from '../ext/three.js'; -import { $, $C, h } from '../moto/webui.js'; +"use strict"; + +// dep: moto.license +// dep: moto.client +// dep: moto.broker +// dep: moto.space +// dep: mesh.util +// dep: mesh.edges +// dep: mesh.group +// dep: mesh.model +// dep: mesh.sketch +// dep: add.three +// use: add.array +gapp.register("mesh.api", [], (root, exports) => { const { Matrix4, Vector3 } = THREE; -const worker = motoClient.fn; +const { base, mesh, moto } = root; +const { space } = moto; +const { util } = mesh; +const { newPolygon } = base; + +const worker = moto.client.fn const groups = []; const sketches = []; @@ -36,8 +41,8 @@ const selection = { // return selected groups + groups from selected models groups(strict = true) { let all = selection.list(strict); - let grp = all.filter(s => s instanceof meshGroup); - let mdl = all.filter(s => s instanceof meshModel); + let grp = all.filter(s => s instanceof mesh.group); + let mdl = all.filter(s => s instanceof mesh.model); for (let m of mdl) { grp.addOnce(m.group); } @@ -47,8 +52,8 @@ const selection = { // return selected models + models from selected groups models(strict = true) { let all = selection.list(strict); - let grp = all.filter(s => s instanceof meshGroup); - let mdl = all.filter(s => s instanceof meshModel); + let grp = all.filter(s => s instanceof mesh.group); + let mdl = all.filter(s => s instanceof mesh.model); for (let g of grp) { for (let m of g.models) { mdl.addOnce(m); @@ -78,7 +83,7 @@ const selection = { // flatten groups into model lists when present selected = objects.map(o => o.models ? o.models : o).flat(); tools = toolist || []; - meshUtil.defer(selection.update); + util.defer(selection.update); }, // @param group {MeshObject} @@ -89,7 +94,7 @@ const selection = { mode.sketch(); tools.length = 0; selected.length = 0; - meshEdges?.end(); + mesh.edges?.end(); break; case 'group': case 'model': @@ -110,7 +115,7 @@ const selection = { tools.addOnce(object); } } - meshUtil.defer(selection.update); + util.defer(selection.update); }, // @param group {MeshObject} @@ -124,7 +129,7 @@ const selection = { selected.remove(object); tools.remove(object); } - meshUtil.defer(selection.update); + util.defer(selection.update); }, // remove all @@ -135,6 +140,7 @@ const selection = { for (let s of selection.list()) { selection.remove(s); tools.remove(s); + s.showBounds(false); s.remove(); } }, @@ -174,7 +180,7 @@ const selection = { for (let m of selection.models()) { m.visible(...arguments); } - meshUtil.defer(selection.update); + util.defer(selection.update); }, // update selection and wireframe for all objects @@ -184,16 +190,15 @@ const selection = { group.select(false); group.normals(prefs.map.space.norm || false); group.wireframe(prefs.map.space.wire || false, { - opacity: prefs.map.wireframe.opacity, - edges: prefs.map.space.wire_edge - }); + opacity: prefs.map.wireframe.opacity} + ); } for (let sketch of sketches) { sketch.select(false); } api.updateFog(); // prevent selection of model and its group - let mgsel = selected.filter(s => s instanceof meshModel).map(m => m.group); + let mgsel = selected.filter(s => s instanceof mesh.model).map(m => m.group); selected = selected.filter(sel => !mgsel.contains(sel)).filter(v => v); // highlight selected for (let object of selected) { @@ -203,33 +208,24 @@ const selection = { prefs.save( prefs.map.space.select = selected.map(s => s.id) ); prefs.save( prefs.map.space.tools = tools.map(s => s.id) ); // force repaint in case of idle - motoSpace.update(); + space.update(); return selection; }, update_defer() { - meshUtil.defer(selection.update); + util.defer(selection.update); return selection; }, drag(opt = {}) { - if (opt.start) { - broker.publish('drag_start', selected); - } else if (opt.end) { - broker.publish('drag_end', selected); - } else { - broker.publish('drag', opt); - } selected.forEach(s => s.drag(opt)); return selection; }, move(dx = 0, dy = 0, dz = 0) { - let set = [...selection.groups(), ...selection.sketches()]; - for (let s of set) { + for (let s of [...selection.groups(), ...selection.sketches()]) { s.move(dx, dy, dz); } - broker.publish('move', { set, dx, dy, dz }); return selection; }, @@ -237,7 +233,6 @@ const selection = { for (let s of selection.models()) { s.move(dx, dy, dz); } - broker.publish('move', { set: selection.models(), dx, dy, dz }); return selection; }, @@ -245,7 +240,6 @@ const selection = { for (let s of selection.groups()) { s.rotate(dx, dy, dz); } - broker.publish('rotate', { set: selection.models(), dx, dy, dz }); return selection; }, @@ -253,15 +247,14 @@ const selection = { for (let s of selection.groups()) { s.qrotate(q); } - broker.publish('qrotate', { set: selection.groups(), q }); return selection; }, scale(dx = 0, dy = 0, dz = 0) { for (let s of selection.groups()) { - s.scale(dx, dy, dz); + let { x, y, z } = s.scale(); + s.scale(x * dx, y * dy, z * dz); } - broker.publish('scale', { set: selection.groups(), dx, dy, dz }); return selection; }, @@ -282,7 +275,9 @@ const selection = { }, boundsBox() { - handles.toggleBounds(); + for (let m of selection.groups()) { + m.showBounds(...arguments); + } return selection; }, @@ -291,11 +286,11 @@ const selection = { }, focus() { - selection.count() && api.focus(selection.list()); + api.focus(selection.list()); }, bounds() { - return meshUtil.bounds(selected.map(s => s.object)); + return util.bounds(selected.map(s => s.object)); }, show() { @@ -322,7 +317,7 @@ const pattern = { let models = selection.models(); let [ cx, cy ] = center; for (let model of models) { - let pos = model.bounds.mid; + let pos = model.world_bounds.mid; let stepr = (Math.PI * 2) / count; let modnu = [ model ]; for (let i=1; i parseFloat(v)).toFloat32(); - const nmdl = new meshModel({ file: "input", mesh: vert }); - const ngrp = api.group.new([ nmdl ]); + const nmdl = new mesh.model({ file: "input", mesh: vert }); + const ngrp = group.new([ nmdl ]); api.modal.hide(); } }) ]) ] @@ -773,8 +768,8 @@ let add = { cube() { const box = new THREE.BoxGeometry(1,1,1).toNonIndexed(); const vert = box.attributes.position.array; - const nmdl = new meshModel({ file: "box", mesh: vert }); - const ngrp = api.group.new([ nmdl ]); + const nmdl = new mesh.model({ file: "box", mesh: vert }); + const ngrp = group.new([ nmdl ]); ngrp.scale(10, 10, 10).floor(); selection.set([ nmdl ]); return ngrp; @@ -793,8 +788,8 @@ let add = { cyl.addInner(newPolygon().centerCircle(center, bore/2, facets)); } const vert = cyl.extrude(height, { chamfer }).toFloat32(); - const nmdl = new meshModel({ file: "cylinder", mesh: vert }); - const ngrp = api.group.new([ nmdl ]); + const nmdl = new mesh.model({ file: "cylinder", mesh: vert }); + const ngrp = group.new([ nmdl ]); selection.set([ nmdl ]); ngrp.floor(); } @@ -843,7 +838,7 @@ let add = { height: parseFloat(_height.value), chamfer: parseFloat(_chamfer.value), }).then(gear => { - const nmdl = new meshModel({ file: "gear", mesh: gear.toFloat32() }); + const nmdl = new mesh.model({ file: "gear", mesh: gear.toFloat32() }); const ngrp = group.new([ nmdl ]); selection.set([ nmdl ]); ngrp.floor(); @@ -894,7 +889,7 @@ let add = { steps: parseFloat(_steps.value), taper: _taper.checked ? true : false, }).then(verts => { - const nmdl = new meshModel({ file: "threads", mesh: verts.toFloat32() }); + const nmdl = new mesh.model({ file: "threads", mesh: verts.toFloat32() }); const ngrp = group.new([ nmdl ]); selection.set([ nmdl ]); ngrp.floor(); @@ -951,7 +946,7 @@ let file = { recs, format: ext }).then(data => { if (data.length) { - meshUtil.download(data, file); + util.download(data, file); } }).finally( api.modal.hide ); } @@ -988,7 +983,7 @@ const tool = { return { id: m.id, matrix: m.matrix } })) .then(data => { - let group = api.group.new([new meshModel({ + let group = api.group.new([new mesh.model({ file: `merged`, mesh: data })]).promote(); @@ -1007,7 +1002,7 @@ const tool = { worker .model_union(models.map(m => m.id)) .then(data => { - let group = api.group.new([new meshModel({ + let group = api.group.new([new mesh.model({ file: `union`, mesh: data })]).promote(); @@ -1030,7 +1025,7 @@ const tool = { log(`diff ${models.length} models`).pin(); worker.model_difference(models.map(m => m.id)) .then(data => { - let group = api.group.new([new meshModel({ + let group = api.group.new([new mesh.model({ file: `diff`, mesh: data })]).promote(); @@ -1053,7 +1048,7 @@ const tool = { log(`intersect ${models.length} models`).pin(); worker.model_intersect(models.map(m => m.id)) .then(data => { - let group = api.group.new([new meshModel({ + let group = api.group.new([new mesh.model({ file: `intersect`, mesh: data })]).promote(); @@ -1078,7 +1073,7 @@ const tool = { return { id: m.id, tool: m.tool() } })) .then(data => { - let group = api.group.new([new meshModel({ + let group = api.group.new([new mesh.model({ file: `subtract`, mesh: data })]).promote(); @@ -1128,7 +1123,7 @@ const tool = { }, toSketch() { - for (let m of api.model.list()) { + for (let m of selection.models()) { m.selectionToSketch(); } }, @@ -1148,7 +1143,7 @@ const tool = { return; } model.rename( el.value.trim() ); - meshUtil.defer(selection.update); + util.defer(selection.update); api.modal.hide(); }; let { tempedit } = api.modal.show(`rename ${type}`, h.div({ class: "rename"}, [ @@ -1163,7 +1158,7 @@ const tool = { models = fallback(models, true); if (models.length) { log(`regrouping ${models.length} model(s)`); - let group = api.group.new(models.map(m => m.ungroup())); + let group = mesh.api.group.new(models.map(m => m.ungroup())); let bounds = group.bounds; models.forEach(m => m.centerTo(bounds.mid)); return group; @@ -1178,12 +1173,12 @@ const tool = { for (let m of models) { let p = worker.model_analyze({ id: m.id, opt }).then(data => { let { areas, edges } = data.mapped; - let nm = areas.map(area => new meshModel({ + let nm = areas.map(area => new mesh.model({ file: (area.length/3).toString(), mesh: area.toFloat32() })).map( nm => nm.applyMatrix4(mcore.clone().multiply(m.mesh.matrixWorld)) ); if (nm.length) { - api.group.new(nm, undefined, "patch"); + mesh.api.group.new(nm, undefined, "patch"); } }); promises.push(p); @@ -1201,11 +1196,11 @@ const tool = { let mark = Date.now(); for (let m of models) { let p = worker.model_isolate({ id: m.id }).then(bodies => { - bodies = bodies.map(vert => new meshModel({ + bodies = bodies.map(vert => new mesh.model({ file: m.file, mesh: vert.toFloat32() })).map( nm => nm.applyMatrix4(mcore.clone().multiply(m.mesh.matrixWorld)) ); - api.group.new(bodies, undefined, "isolate"); + mesh.api.group.new(bodies, undefined, "isolate"); }); promises.push(p); } @@ -1254,7 +1249,7 @@ const tool = { log('repairing mesh(es)').pin(); tool.heal(models, { merge: true }).then(() => { log('repair commplete').unpin(); - meshUtil.defer(selection.update); + util.defer(selection.update); }); }, @@ -1273,7 +1268,7 @@ const tool = { log(`${vertices.length/3} vertices, ${zlist.length} unique Z`); } log('cleaning complete').unpin(); - meshUtil.defer(selection.update); + util.defer(selection.update); }); }, @@ -1301,7 +1296,7 @@ const mode = { $(`mode-${mode}`).classList.add('selected'); $('mode-label').innerText = mode; api.mode.check(); - meshEdges?.end(); + mesh.edges?.end(); if (mode === 'sketch') { $C('sketch-on', null, 'hide'); $C('sketch-off', 'hide', null); @@ -1361,7 +1356,7 @@ const mode = { selection.clear(); } mode.set(modes.edge); - meshEdges.start(); + mesh.edges.start(); }, sketch() { @@ -1430,65 +1425,22 @@ const prefs = { // persist to data store save() { if (prefs.changed()) { - api.db.admin.put("prefs", prefs.map); + mesh.db.admin.put("prefs", prefs.map); prefsig = prefsignew; } }, // reload from data store load() { - return api.db.admin.get("prefs").then(data => { + return mesh.db.admin.get("prefs").then(data => { // handle/ignore incompatible older prefs if (data) Object.assign(prefs.map, data); }); } }; -let mark = Date.now(); - -let since = () => { - return ((Date.now() - mark) / 1000).toFixed(2).padStart(9,0); -}; - -let dbug = self.dbug = self.dbug || { - level: 1, // 0=debug, 1=normal, 2=warn, 3=error - - set: (level) => { - if (level >= 0) dbug.level = Math.min(level, 3); - }, - - debug: function() { - if (dbug.level <= 0) return console.log(since(),'[DBG]', ...arguments); - }, - - log: function() { - if (dbug.level <= 1) return console.log(since(),'|',...arguments); - }, - - warn: function() { - if (dbug.level <= 2) return console.log(since(),'[WRN]', ...arguments); - }, - - error: function() { - if (dbug.level <= 3) return console.trace(since(),'[ERR]', ...arguments); - }, - - since -}; - - // api is augmented in mesh.build (log, modal, download) -const api = { - init() { - // publish messages when api functions are called - broker.wrapObject(selection, 'selection'); - // broker.wrapObject(model, 'model'); - // broker.wrapObject(group, 'group'); - - // optimize db writes by merging updates - prefs.save = meshUtil.deferWrap(prefs.save, 100); - }, - +const api = exports({ help() { window.open("https://docs.grid.space/projects/mesh-tool"); }, @@ -1519,18 +1471,9 @@ const api = { } }, - history: { - undo() { - history.undo(); - }, - redo() { - history.redo(); - } - }, - - // @param object {MeshObject | MeshObject[] | Object} + // @param object {THREE.Object3D | THREE.Object3D[] | Point} focus(object) { - let { center } = object.center ? object : meshUtil.bounds(object); + let { center } = object.center ? object : util.bounds(object); // when no valid objects supplied, set origin if (isNaN(center.x * center.y * center.z)) { center = { x: 0, y: 0, z: 0 }; @@ -1544,13 +1487,13 @@ const api = { if (x < 0) left = -left; } // sets "home" views (front, back, home, reset) - motoSpace.platform.setCenter(center.x, -center.y, center.z); + space.platform.setCenter(center.x, -center.y, center.z); // sets camera focus - motoSpace.view.panTo(center.x, center.z, -center.y, left, up); + space.view.panTo(center.x, center.z, -center.y, left, up); }, grid(state = { toggle:true }) { - let { platform } = motoSpace; + let { platform } = space; let { map, save } = prefs; if (state.toggle) { platform.showGrid(!platform.isGridVisible()); @@ -1561,19 +1504,9 @@ const api = { save(); }, - wireedges(state = { toggle:true }, opt = { - opacity: prefs.map.wireframe.opacity, - edges: base.util.toDegrees(api.prefs.map.surface.radians) - }) { - api.wireframe(state, opt); - }, - - wireframe(state = { toggle:true }, opt = { - opacity: prefs.map.wireframe.opacity, - edges: prefs.map.space.wire_edge - }) { + wireframe(state = { toggle:true }, opt = { opacity: prefs.map.wireframe.opacity }) { const mspace = prefs.map.space; - let { wire } = mspace; + let wire = mspace.wire; if (state.toggle) { wire = !wire; } else { @@ -1582,9 +1515,7 @@ const api = { for (let m of model.list()) { m.wireframe(wire, opt); } - mspace.wire = wire; - mspace.wire_edge = wire ? opt.edges : 0; - prefs.save(); + prefs.save( mspace.wire = wire ); api.updateFog(); }, @@ -1592,9 +1523,9 @@ const api = { const mspace = prefs.map.space; const mwire = prefs.map.wireframe; if (mspace.wire) { - motoSpace.scene.setFog(mwire.fog, mspace.dark ? 0 : 0xffffff); + space.scene.setFog(mwire.fog, mspace.dark ? 0 : 0xffffff); } else { - motoSpace.scene.setFog(false); + space.scene.setFog(false); } }, @@ -1611,59 +1542,52 @@ const api = { prefs.save( prefs.map.space.norm = norm ); }, - objects(selected = false) { - // return model objects suitable for finding ray intersections - if (selected) { - return [ - ...selection.models().map(o => o.mesh), - ...selection.sketches().map(s => s.meshes).flat() - ]; - } else { - return [ - ...group.list().map(o => o.models).flat().map(o => o.mesh), - ...sketches.map(s => s.meshes).flat() - ]; - } - }, - - add, - - dbug, - - file, - - group, - mode, modes, - model, + selection, pattern, - prefs, + group, - selection, + model, sketch, + add, + + file, + tool, + prefs, + + objects() { + // return model objects suitable for finding ray intersections + return [ + ...group.list().map(o => o.models).flat().map(o => o.mesh), + ...sketches.map(s => s.meshes).flat() + ]; + }, + isDebug: self.debug === true -}; +}); function log() { return api.log.emit(...arguments); } +const { broker } = gapp; const call = broker.send; -self.mesh_api = api; +// publish messages when api functions are called +broker.wrapObject(selection, 'selection'); +broker.wrapObject(model, 'model'); +broker.wrapObject(group, 'group'); -export { - sketches, - selection, - groups, - api -}; +// optimize db writes by merging updates +prefs.save = util.deferWrap(prefs.save, 100); + +}); diff --git a/src2/mesh/build.js b/src.old/mesh/build.js similarity index 90% rename from src2/mesh/build.js rename to src.old/mesh/build.js index d4ea9918..7be0ae46 100644 --- a/src2/mesh/build.js +++ b/src.old/mesh/build.js @@ -1,21 +1,22 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { broker } from '../moto/broker.js'; -import { $, h, estop } from '../moto/webui.js'; -import { api } from './api.js'; -import { util } from './util.js'; -import { space } from '../moto/space.js'; -import { split } from './split.js'; -import { newPolygon } from '../geo/polygon.js'; -import { version } from '../moto/license.js'; +"use strict"; -const { div, label, input, hr } = h; -const { dbug } = api; -const isDebug = api.isDebug; +// dep: moto.broker +// dep: mesh.util +// dep: mesh.api +gapp.register("mesh.build", [], (root, exports) => { + +const { broker } = gapp; +const { base, mesh, moto } = root; +const { api, util } = mesh; +const { space } = moto; +const { bind, div, label, input, hr } = h; + +const devel = api.isDebug; let call = broker.send; let rad = 180 / Math.PI; -let deg = Math.PI / 180; let und = undefined; broker.listeners({ @@ -37,7 +38,7 @@ const modal = api.modal = { if (this.info.showing) { throw `modal conflict showing "${title}"`; } - let bound = h.bind($('modal'), contents); + let bound = bind($('modal'), contents); $('modal_title_text').innerText = title; $('modal_page').style.display = 'flex'; $('modal_frame').style.display = 'flex'; @@ -100,7 +101,7 @@ const modal = api.modal = { let { title, body } = opt; let contents = []; if (typeof body === 'string') { - contents.push(h.div(body)) + contents.push(div(body)) } else if (Array.isArray(body)) { contents.appendAll(body); } else { @@ -136,7 +137,7 @@ const log = api.log = { text: `${dbug.since()} | ${[...arguments].join(' ')}`, time: now }); - if (isDebug) { + if (api.isDebug) { console.log(...data.peek().objs); } while (!log.pinned && data.length && (data.length > lines || now - data[0].time > age)) { @@ -183,14 +184,14 @@ const log = api.log = { // re-render and show current log messages render() { let area = $('logtext'); - h.bind(area, log.data.map(rec => h.div({ _: rec.text }))); + bind(area, log.data.map(rec => div({ _: rec.text }))); area.scrollTop = area.scrollHeight; return log.show(); }, }; // bind endpoint for worker to log in the ui -broker.subscribe("mesh.log", msg => { +gapp.broker.subscribe("mesh.log", msg => { log.emit(msg); }); @@ -198,14 +199,14 @@ broker.subscribe("mesh.log", msg => { api.welcome = function(version = "unknown") { const { prefs } = api; const { map } = prefs; - modal.show('About Mesh:Tool', h.div({ class:"welcome" }, [ + modal.show('About Mesh:Tool', div({ class:"welcome" }, [ h.a({ target: "site", _: "Grid.Space", href: "https://grid.space/" }), - h.div(`Version: ${version}`), - h.hr({ width: "100%" }), + div(`Version: ${version}`), + hr({ width: "100%" }), h.a({ target: "docs", _: "Guide & Documentation", @@ -221,9 +222,9 @@ api.welcome = function(version = "unknown") { _: "Discord Server", href: "https://discord.gg/suyCCgr" }), - h.hr({ width: "100%" }), - h.div({ class: "choice" }, [ - h.input({ + hr({ width: "100%" }), + div({ class: "choice" }, [ + input({ type: "checkbox", [ map.info.welcome !== false ? 'checked' : 'unchecked' ] : 1, onchange: (ev) => { @@ -231,13 +232,14 @@ api.welcome = function(version = "unknown") { prefs.save(); } }), - h.div("show at startup") + div("show at startup") ]), ])); }; // bind settings endpoint to api api.settings = function() { + const { util } = mesh; const { prefs } = api; const { surface, normals, space, sketch, wireframe } = prefs.map; const { dark } = space; @@ -350,7 +352,7 @@ api.settings = function() { modal.show('settings', div({ class: "settings" }, [ set1, set2, set3, set4, set5, set6, set7 ] )); -}; +} // bootstrap icons function bicon(name) { @@ -371,19 +373,18 @@ function editable() { function menu_item(text, fn, short, id) { if (short) { short = Array.isArray(short) ? short : [ short ]; - short = h.div({ class: "short" }, short.map(s => s.startsWith('bi-') ? - bicon(s) : h.div(s) + short = div({ class: "short" }, short.map(s => s.startsWith('bi-') ? + bicon(s) : div(s) )); } - return h.div({ id, onclick: fn }, [ - h.div({ _: text, class: "grow" }), + return div({ id, onclick: fn }, [ + div({ _: text, class: "grow" }), short ].filter(o => o)); } // create html elements function ui_build() { - let { bind, div, input, hr, label } = h; let { file, selection, mode, tool, sketch, prefs, add } = api; let trash = FontAwesome.icon({ prefix: "fas", iconName: "trash" }).html[0]; let eye_open = FontAwesome.icon({ prefix: "fas", iconName: "eye" }).html[0]; @@ -409,9 +410,6 @@ function ui_build() { div({ class: "menu sketch-on" }, [ div('Edit'), div({ class: "menu-items" }, [ - menu_item('Undo', api.history.undo, ['bi-command','Z']), - menu_item('Redo', api.history.redo, ['bi-command','Y']), - hr(), menu_item('Add Circle', api.add.circle), menu_item('Add Rectangle', api.add.rectangle), hr(), @@ -423,16 +421,13 @@ function ui_build() { div({ class: "menu sketch-off" }, [ div('Edit'), div({ class: "menu-items" }, [ - menu_item('Undo', api.history.undo, ['bi-command','Z']), - menu_item('Redo', api.history.redo, ['bi-command','Y']), - hr(), menu_item('Add Cylinder', add.cylinder), menu_item('Add Cube', add.cube), menu_item('Add Gear', add.gear), menu_item('Add Threads', add.threads), hr(), menu_item('Add Sketch', add.sketch), - isDebug ? menu_item('Add Vertices', add.input) : undefined, + devel ? menu_item('Add Vertices', add.input) : undefined, hr(), menu_item('Delete', api.selection.delete, 'bi-backspace'), ]) @@ -442,9 +437,7 @@ function ui_build() { div({ class: "menu-items" }, [ menu_item('Bounds', () => { selection.boundsBox({ toggle: true }) }, 'B'), menu_item('Normals', () => { api.normals() }, 'N'), - hr(), - menu_item('WireEdges', () => { api.wireedges() }, 'W'), - menu_item('WireFaces', () => { api.wireframe() }, [ 'bi-shift', 'W' ]), + menu_item('Wireframes', () => { api.wireframe() }, 'W'), hr(), menu_item('Gridlines', () => { api.grid() }, 'G'), menu_item('Messages', () => { api.log.toggle({ spinner: false }) }, 'L'), @@ -469,7 +462,7 @@ function ui_build() { div({ id: "mode-label" }) ]), div({ class: "menu sketch-on" }, [ - div('Items'), + div('Object'), div({ class: "menu-items" }, [ menu_item('Group', sketch.arrange.group), menu_item('Ungroup', sketch.arrange.ungroup), @@ -487,7 +480,7 @@ function ui_build() { ]) ]), div({ class: "menu sketch-off" }, [ - div('Objects'), + div('Object'), div({ class: "menu-items" }, [ menu_item('Regroup', tool.regroup), menu_item('Isolate', tool.isolate), @@ -495,7 +488,7 @@ function ui_build() { menu_item('Duplicate', tool.duplicate, ['bi-shift','D']), menu_item('Mirror', tool.mirror, 'M'), menu_item('Merge', tool.merge), - menu_item('Split', split.start, 'S'), + menu_item('Split', mesh.split.start, 'S'), hr(), menu_item('Union', tool.union), menu_item('Subtract', tool.subtract), @@ -512,14 +505,13 @@ function ui_build() { menu_item('Flip Normals', tool.invert, ['bi-shift','I']), menu_item('Triangulate', tool.triangulate, ['bi-shift','T']), menu_item('To Sketch', tool.toSketch), - hr(), menu_item('Analyze', tool.analyze), menu_item('Flatten', tool.flatten), menu_item('Clean', tool.clean), ]) ]), div({ class: "menu sketch-on" }, [ - div('Shapes'), + div('Shape'), div({ class: "menu-items" }, [ menu_item('Duplicate', tool.duplicate, ['bi-shift','D']), menu_item('Extrude', () => sketch.extrude(), ['bi-shift','E']), @@ -536,7 +528,7 @@ function ui_build() { div({ class: "menu" }, [ div('Help'), div({ class: "menu-items" }, [ - menu_item('About', () => { api.welcome(version) }), + menu_item('About', () => { api.welcome(mesh.version) }), hr(), menu_item('Documentation', api.help), menu_item('Report Bugs', api.bugs), @@ -769,14 +761,23 @@ function ui_build() { function field_edit(title, set, opt = {}) { return function(ev) { let floor = api.prefs.map.space.floor !== false; - let value = parseFloat(ev.target.innerText); + let value = ev.target.innerText; let onclick = (ev) => { let tempval = parseFloat($('tempval').value); api.modal.hide(modal.info.cancelled); if (modal.info.cancelled) { return; } - set(tempval, value); + if (opt.sketch) { + defer_selection(); + return set(tempval, value); + } + for (let g of api.selection.groups()) { + set(g, tempval, value); + if (floor && opt.floor !== false) { + g.floor(mesh.group); + } + } defer_selection(); }; let onkeydown = (ev) => { @@ -898,7 +899,7 @@ function ui_build() { el2.onclick = field_edit(`${axis} size`, (nval, oval) => { if (sel_item.type === 'polygon') { let scale = {x:1,y:1,z:1}; - let poly = newPolygon().fromObject(sel_item); + let poly = base.newPolygon().fromObject(sel_item); let bounds = poly.bounds; scale[axis] = nval/oval; poly.move(bounds.center().scale(-1,-1,-1)); @@ -918,7 +919,7 @@ function ui_build() { sel_item[lab] = nval; sketch.render(); }, { sketch: true }); - editable(el, el2, el3); + editable(el, el2, e3); } return; @@ -983,39 +984,39 @@ function ui_build() { // bind rotation editable fields let { group_val_X_rot, group_val_Y_rot, group_val_Z_rot } = bound; editable(group_val_X_rot, group_val_Y_rot, group_val_Z_rot); - group_val_X_rot.onclick = field_edit('x rotation', (val) => { - selection.rotate(val * deg,0,0); + group_val_X_rot.onclick = field_edit('x rotation', (group, val) => { + group.rotate(val,0,0); }); - group_val_Y_rot.onclick = field_edit('y rotation', (val) => { - selection.rotate(0,val * deg,0); + group_val_Y_rot.onclick = field_edit('y rotation', (group, val) => { + group.rotate(0,val,0); }); - group_val_Z_rot.onclick = field_edit('z rotation', (val) => { - selection.rotate(0,0,val * deg); + group_val_Z_rot.onclick = field_edit('z rotation', (group, val) => { + group.rotate(0,0,val); }); // bind position editable fields let { group_val_X_pos, group_val_Y_pos, group_val_Z_pos } = bound; editable(group_val_X_pos, group_val_Y_pos, group_val_Z_pos); - group_val_X_pos.onclick = field_edit('x position', (val) => { + group_val_X_pos.onclick = field_edit('x position', (group, val) => { selection.move_models(val,0,0); }, { floor: false }); - group_val_Y_pos.onclick = field_edit('y position', (val) => { + group_val_Y_pos.onclick = field_edit('y position', (group, val) => { selection.move_models(0,val,0); }, { floor: false }); - group_val_Z_pos.onclick = field_edit('z position', (val) => { + group_val_Z_pos.onclick = field_edit('z position', (group, val) => { selection.move_models(0,0,val); }, { floor: false }); // bind center editable fields let { span_val_X_center, span_val_Y_center, span_val_Z_center } = bound; editable(span_val_X_center, span_val_Y_center, span_val_Z_center); - span_val_X_center.onclick = field_edit('x center', (val, oval) => { + span_val_X_center.onclick = field_edit('x center', (group, val, oval) => { selection.move_models(val - parseFloat(oval),0,0); }); - span_val_Y_center.onclick = field_edit('y center', (val, oval) => { + span_val_Y_center.onclick = field_edit('y center', (group, val, oval) => { selection.move_models(0,val - parseFloat(oval),0); }); - span_val_Z_center.onclick = field_edit('z center', (val, oval) => { + span_val_Z_center.onclick = field_edit('z center', (group, val, oval) => { selection.move_models(0,0,val - parseFloat(oval)); }); @@ -1041,28 +1042,28 @@ function ui_build() { }; editable(lbl); }); - span_val_X_size.onclick = field_edit('x size', (val, oval) => { - let rel = val / oval; + span_val_X_size.onclick = field_edit('x size', (group, val, oval) => { + let rel = val / parseFloat(oval); if (axgrp.X) { - selection.scale(rel, axgrp.Y ? rel : 1, axgrp.Z ? rel : 1); + group.scale(rel, axgrp.Y ? rel : 1, axgrp.Z ? rel : 1); } else { - selection.scale(rel, 1, 1); + group.scale(rel, 1, 1); } }); - span_val_Y_size.onclick = field_edit('y size', (val, oval) => { - let rel = val / oval; + span_val_Y_size.onclick = field_edit('y size', (group, val, oval) => { + let rel = val / parseFloat(oval); if (axgrp.Y) { - selection.scale(axgrp.X ? rel : 1, rel, axgrp.Z ? rel : 1); + group.scale(axgrp.X ? rel : 1, rel, axgrp.Z ? rel : 1); } else { - selection.scale(1, rel, 1); + group.scale(1, rel, 1); } }); - span_val_Z_size.onclick = field_edit('z size', (val, oval) => { - let rel = val / oval; + span_val_Z_size.onclick = field_edit('z size', (group, val, oval) => { + let rel = val / parseFloat(oval); if (axgrp.Z) { - selection.scale(axgrp.X ? rel : 1, axgrp.Y ? rel : 1, rel); + group.scale(axgrp.X ? rel : 1, axgrp.Y ? rel : 1, rel); } else { - selection.scale(1, 1, rel); + group.scale(1, 1, rel); } }); @@ -1093,13 +1094,11 @@ function ui_build() { // listen for api calls // create a deferred wrapper to merge multiple rapid events - const defer_all = util.deferWrap(update_all); - const defer_selector = util.deferWrap(update_selector); - const defer_selection = util.deferWrap(update_selection); + let defer_all = util.deferWrap(update_all); + let defer_selector = util.deferWrap(update_selector); + let defer_selection = util.deferWrap(update_selection); broker.listeners({ - history_undo: defer_all, - history_redo: defer_all, model_add: defer_all, group_add: defer_all, model_remove: defer_all, @@ -1114,4 +1113,4 @@ function ui_build() { }); } -export { }; +}); diff --git a/src2/mesh/edges.js b/src.old/mesh/edges.js similarity index 88% rename from src2/mesh/edges.js rename to src.old/mesh/edges.js index 248ef16c..ec3d4966 100644 --- a/src2/mesh/edges.js +++ b/src.old/mesh/edges.js @@ -1,16 +1,21 @@ /** Copyright Stewart Allen -- All Rights Reserved */ /** - * facet edge selector. allows for creating new facets + * facet edge selecetor. allows for creating new facets * when two connected edges share a single point. */ -import { THREE, Line2, LineSegmentsGeometry, LineSegments2, LineGeometry, LineMaterial } from '../ext/three.js'; -import { space as motoSpace } from '../moto/space.js'; -import { newPoint } from '../geo/point.js'; -import { api } from './api.js'; +"use strict"; -const { Vector3, Group } = THREE; + +// dep: moto.space +gapp.register("mesh.edges", [], (root, exports) => { + +const { Line2, LineMaterial, LineGeometry, Vector3, Group } = THREE; +const { LineSegments2, LineSegmentsGeometry } = THREE; +const { base, mesh, moto } = root; +const { space } = moto; +const { newPoint } = base; let isActive; let hovered; @@ -44,14 +49,14 @@ function cmp(v1, v2) { // points equal if all axis values are within tolerance of each other function eq(p1, p2) { - return true && + return cmp(p1.x, p2.x) && cmp(p1.y, p2.y) && cmp(p1.z, p2.z); } // split functions -const edges = { +let edges = { active() { return isActive ? true : false; }, @@ -61,19 +66,21 @@ const edges = { return; } + let { api } = mesh; + isActive = true; selected.length = 0; obj.visible = false; - motoSpace.scene.add(obj); + space.scene.add(obj); // enable temp mode let state = edges.state = { obj }; - let models = state.models = api.model.list(); + let models = state.models = api.selection.models(); let meshes = models.map(m => m.mesh); // find closest facet edge to hover location - motoSpace.mouse.onHover((int, event, ints) => { + space.mouse.onHover((int, event, ints) => { if (!event) { obj.visible = false; return meshes; @@ -98,12 +105,12 @@ const edges = { if (!isActive) { return; } - let space = motoSpace; + let space = moto.space; space.scene.remove(obj); space.scene.remove(edges.selected); space.mouse.onHover(undefined); isActive = edges.state = undefined; - api.selection.update(); + mesh.api.selection.update(); }, async add() { @@ -156,11 +163,11 @@ const edges = { clear() { selected.length = 0; - motoSpace.scene.remove(edges.selected); + space.scene.remove(edges.selected); }, select() { - motoSpace.scene.remove(edges.selected); + space.scene.remove(edges.selected); if (!hovered) return; const { object, face, side } = hovered; @@ -192,7 +199,7 @@ const edges = { } const group = edges.selected = new Group(); - motoSpace.scene.add(edges.selected); + space.scene.add(edges.selected); let verts = selected.map(s => s.side.verts).flat(); group.add(line2(verts)); }, @@ -209,7 +216,8 @@ const edges = { let l2 = { p0:v2, p1:v0, d:point.distToLine3D(v2, v0), v:[face.c,face.a].sort() }; return [ l0, l1, l2 ].sort((a,b) => a.d - b.d)[0]; } -}; +} -export default edges; -export { edges }; +exports(edges); + +}); diff --git a/src2/mesh/geom.js b/src.old/mesh/geom.js similarity index 94% rename from src2/mesh/geom.js rename to src.old/mesh/geom.js index a3041072..99928024 100644 --- a/src2/mesh/geom.js +++ b/src.old/mesh/geom.js @@ -1,11 +1,17 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { THREE } from '../ext/three.js'; +"use strict"; + +// dep: moto.license +// dep: add.array +// dep: add.three +gapp.register("mesh.geom", [], (root, exports) => { + const { Matrix4, Matrix3, Vector3, Box3 } = THREE; -import { license as motoLicense } from '../moto/license.js'; // geometry helper functions -const geom = { +const geom = exports({ + // given a closed polyline as an expanded point array // return enclosed area with sign indicating winding order areaSigned(points, axes = 3) { @@ -87,6 +93,6 @@ const geom = { return recs; } -}; +}); -export { geom }; +}); diff --git a/src2/mesh/group.js b/src.old/mesh/group.js similarity index 79% rename from src2/mesh/group.js rename to src.old/mesh/group.js index 37ec9077..4b0449a1 100644 --- a/src2/mesh/group.js +++ b/src.old/mesh/group.js @@ -1,19 +1,27 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { THREE } from '../ext/three.js'; +"use strict"; + +// dep: add.array +// dep: add.three +// dep: moto.license +// dep: moto.broker +// dep: moto.client +// dep: mesh.object +// use: mesh.api +// use: mesh.model +gapp.register("mesh.group", [], (root, exports) => { + +const { mesh, moto } = root; const { Group, Box3 } = THREE; -import { license as motoLicense } from '../moto/license.js'; -import { broker as motoBroker } from '../moto/broker.js'; -import { client as motoClient } from '../moto/client.js'; -import { space as motoSpace } from '../moto/space.js'; -import { object as meshObject } from './object.js'; -import { api as meshApi } from './api.js'; -import { model as meshModel } from './model.js'; +const { broker } = gapp; +const { space } = moto; -const call = motoBroker.send; -const worker = motoClient.fn; +const call = broker.send; +const worker = moto.client.fn; + +mesh.group = class MeshGroup extends mesh.object { -class MeshGroup extends meshObject { // @param group {MeshModel[]} constructor(models = [], id, name) { super(id); @@ -36,7 +44,7 @@ class MeshGroup extends meshObject { get bounds() { let box3 = new Box3(); for (let model of this.models) { - box3.union(model.bounds); + box3.union(model.world_bounds); } return box3; } @@ -46,11 +54,11 @@ class MeshGroup extends meshObject { model.group = this; this.models.addOnce(model); this.group3.add(model.mesh); - motoSpace.update(); + space.update(); call.model_add({model, group:this}); worker.group_add({id: this.id, model:model.id}); // update data store - meshApi.db.space.put(this.id, this.models.map(m => m.id)); + mesh.db.space.put(this.id, this.models.map(m => m.id)); return this; } @@ -74,14 +82,14 @@ class MeshGroup extends meshObject { if (opt.free) model.remove(true); // auto-remove group when empty if (this.group3.children.length === 0) { - meshApi.group.remove(this); + mesh.api.group.remove(this); // manage lifecycle with worker, mesh app caches, etc this.destroy(); } else { // update data store - meshApi.db.space.put(this.id, this.models.map(m => m.id)); + mesh.db.space.put(this.id, this.models.map(m => m.id)); } - motoSpace.update(); + space.update(); return this; } @@ -90,10 +98,10 @@ class MeshGroup extends meshObject { let { x, y, z } = dim; let max = Math.max(x, y, z); if (max < 2) { - meshApi.log.emit(`auto-scaling import from ${max.round(5)}`); + mesh.api.log.emit(`auto-scaling import from ${max.round(5)}`); this.scale(1000, 1000, 1000); } - let { center, floor } = meshApi.prefs.map.space; + let { center, floor } = mesh.api.prefs.map.space; if (center !== false) this.centerXY(); if (floor !== false) this.floor(); return this; @@ -165,6 +173,6 @@ class MeshGroup extends meshObject { model.select(...arguments); } } -} +}; -export { MeshGroup as group }; +}); diff --git a/src2/mesh/model.js b/src.old/mesh/model.js similarity index 83% rename from src2/mesh/model.js rename to src.old/mesh/model.js index 70a5a92d..536a3253 100644 --- a/src2/mesh/model.js +++ b/src.old/mesh/model.js @@ -1,21 +1,33 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { THREE } from '../ext/three.js'; -import { client as motoClient } from '../moto/client.js'; -import { api as meshApi } from './api.js'; -import { object as meshObject } from './object.js'; -import { model as meshModel } from './model.js'; -import { newPolygon } from '../geo/polygon.js'; -import { polygons } from '../geo/polygons.js'; -import { space } from '../moto/space.js'; -import { util } from './util.js'; +"use strict"; -const { MeshPhongMaterial, MeshBasicMaterial, LineBasicMaterial, BufferGeometry, BufferAttribute, DoubleSide, Mesh, Box3, Vector3, Triangle } = THREE; -const worker = motoClient.fn; +// dep: add.array +// dep: add.three +// dep: geo.polygon +// dep: geo.polygons +// dep: moto.license +// dep: moto.client +// dep: mesh.object +// use: mesh.api +// use: mesh.util +// use: mesh.group +gapp.register("mesh.model", [], (root, exports) => { + +const { MeshPhongMaterial, MeshBasicMaterial, LineBasicMaterial } = THREE; +const { BufferGeometry, BufferAttribute, DoubleSide, Mesh } = THREE; +const { Box3, Vector3, Triangle } = THREE; +const { base, mesh, moto } = root; +const { space } = moto; +const { api } = mesh; +const { newBounds, newPolygon, polygons } = base; + +const mapp = mesh; +const worker = moto.client.fn; const zero = new Vector3(0,0,0); /** default materials **/ -const materials = { +let materials = mesh.material = { // model unselected normal: new MeshPhongMaterial({ flatShading: true, @@ -61,24 +73,24 @@ const materials = { wireframe: true, color: 0x0, transparent: true, - opacity: 0.4 + opacity: 0.5 }), wireline: new LineBasicMaterial({ side: DoubleSide, color: 0x0, transparent: true, - opacity: 0.4 + opacity: 0.5 }), }; /** 3D model rendered on plaform **/ -class MeshModel extends meshObject { +mesh.model = class MeshModel extends mesh.object { constructor(data, id) { super(id); let { file, mesh, vertices } = data; - if (!(mesh || vertices)) { - meshApi.dbug.error(`'${file}' missing mesh data`); + if (!mesh) { + dbug.error(`'${file}' missing mesh data`); return; } @@ -115,29 +127,37 @@ class MeshModel extends meshObject { return this.mesh; } - get geo_bounds() { + get bounds() { return this.geometry.boundingBox.clone(); } - get bounds() { - // console.log('bounds', - // this.geometry.boundingBox.clone(), - // this.position().clone() - // ); - return this.geometry.boundingBox.clone().translate(this.position()); + get world_bounds() { + return this.bounds.translate(this.position()); } + // get world_positions() { + // let pos = this.position(); + // return this.geometry + // .clone() + // .translate(pos.x,pos.y,pos.z) + // .attributes.position.array; + // } + get positions() { return this.geometry.attributes.position.array; } + // get matrix() { + // return this.matrixWorld.elements; + // } + get matrixWorld() { return this.mesh.matrixWorld; } drag(opt = {}) { let { start, delta, offset, end } = opt; - let { snap, snapon } = meshApi.prefs.map.space; + let { snap, snapon } = api.prefs.map.space; if (start) { let mid = this.bounds.mid; this._save = { @@ -174,6 +194,7 @@ class MeshModel extends meshObject { qrotate(quaternion) { this.log('model-rotate', quaternion.toArray()); this.geometry.applyQuaternion(quaternion); + this.geometry._model_invalid = true; this.updateBounds(); } @@ -181,6 +202,7 @@ class MeshModel extends meshObject { if (x === 1 && y === 1 && z === 1) return; this.log('model-scale', ...arguments, this.bounds); this.geometry.scale(x, y, z); + this.geometry._model_invalid = true; this.updateBounds(); } @@ -188,6 +210,7 @@ class MeshModel extends meshObject { if (!(x || y || z)) return; this.log('model-translate', ...arguments); this.geometry.translate(x, y, z); + this.geometry._model_invalid = true; this.updateBounds(); } @@ -225,15 +248,13 @@ class MeshModel extends meshObject { // moves mesh object center to target centerTo(to) { this.log('model-centerto', to); - let geo = this.geo_bounds; - let { mid } = this.bounds.translate({ - x: -to.x, - y: -to.y, - z: -to.z - }); - // this.translate(-geo.mid.x, -geo.mid.y, -geo.mid.z); - // this.translate(mid.x, mid.y, mid.z); - this.translate(-geo.mid.x+mid.x, -geo.mid.y+mid.y, -geo.mid.z+mid.z); + let { mid } = this.bounds; + let abs = this.world_bounds.mid; + this.translate( + -mid.x + (abs.x - to.x), + -mid.y + (abs.y - to.y), + -mid.z + (abs.z - to.z), + ); this.position(to.x, to.y, to.z); return this; } @@ -241,19 +262,22 @@ class MeshModel extends meshObject { // when geometry updates, recompute bounds for ray intersections // and sync data to worker and indexed db updateBounds() { - if (this._wire) { - this._wire.geometry.attributes.position.needsUpdate = true; - this.updateWireEdges(); - } + if (this._wire) + this._wire.geometry.attributes.position.needsUpdate = true; this.attributes.position.needsUpdate = true; this.geometry.computeBoundingBox(); this.geometry.computeBoundingSphere(); this.log('update-bounds', this.geometry.boundingBox); this.normals({ refresh: true }); - space.update(); + this.updateBoundsBox(); + moto.space.update(); this.sync(); } + updateBoundsBox() { + this.group?.updateBoundsBox(); + } + mirror() { return this.duplicate({ mirror: true }); } @@ -267,21 +291,14 @@ class MeshModel extends meshObject { let data = this.attributes.position.clone().array; if (opt.append) data = [...data, ...opt.append].toFloat32(); if (opt.x) return this.reload(data); - let model = new meshModel({ file: `${this.file}`, mesh: data }); - let group = opt.group || meshApi.group.new(); + let model = new mesh.model({ file: `${this.file}`, mesh: data }); + let group = opt.group || mesh.api.group.new(); let bounds = this.group.bounds; group.add(model); model.position(pos.x, pos.y, pos.z); - // preserve wireframe settings - let wireSettings = this.wireframe(); - if (wireSettings.enabled) { - model.wireframe(true, { - edges: wireSettings.edges, - opacity: wireSettings.opacity - }); - } + model.wireframe(this.wireframe()); if (opt.select) { - meshApi.selection.add(model); + api.selection.add(model); } if (opt.mirror) { group.move(0, 0, bounds.dim.z); @@ -315,12 +332,10 @@ class MeshModel extends meshObject { this.log('reload'); let was = this.wireframe(false); let geo = this.mesh.geometry; - const oldGeo = geo; - geo = new BufferGeometry(); geo.setAttribute('position', new BufferAttribute(vertices, 3)); + // signal util.box3expand that geometry changed + geo._model_invalid = true; geo.computeVertexNormals(); - this.mesh.geometry = geo; - oldGeo.dispose(); // restore wireframe state this.wireframe(was); // fixup normals @@ -328,7 +343,7 @@ class MeshModel extends meshObject { // sync worker, allows raycasting to work this.updateBounds(); // re-gen face index in surface mode - meshApi.mode.check(); + mesh.api.mode.check(); } rename(file) { @@ -341,7 +356,7 @@ class MeshModel extends meshObject { // sync to worker worker.model_load({ id: this.id, name: this.file, vertices: this.positions }); // persist in db so it can be restored on page load - meshApi.db.space.put(this.id, { + mapp.db.space.put(this.id, { file: this.file, mesh: this.attributes.position.array }); @@ -446,8 +461,6 @@ class MeshModel extends meshObject { enabled: true, opacity: this.opacity(), color: was ? this._wire.material.color : undefined, - edges: this._wire._edges, - isLineSegments: this._wire.isLineSegments } : { enabled: false }; @@ -455,42 +468,29 @@ class MeshModel extends meshObject { if (bool.toggle) { bool = !was; } + // no change if (was === bool) { return was; } if (was) { this.mesh.remove(this._wire); - if (this._wire) { - this._wire.geometry.dispose(); - this._wire.material.dispose(); - } this._wire = undefined; this.opacity({restore: true}); } - if (bool && opt.edges) { - let edges = new THREE.EdgesGeometry(this.mesh.geometry, opt.edges); - this._wire = new THREE.LineSegments(edges, materials.wireline.clone()); - this._wire._edges = opt.edges; + if (bool === 'edges') { + let edges = new THREE.EdgesGeometry(this.mesh.geometry, 5); + this._wire = new THREE.LineSegments(edges, materials.wireframe); this.mesh.add(this._wire); - this.opacity({temp: opt.opacity || meshApi.prefs.map.wireframe.opacity}); + this.opacity({temp: opt.opacity || 0.15}); } else if (bool) { - this._wire = new Mesh(this.mesh.geometry.shallowClone(), materials.wireframe.clone()); + this._wire = new Mesh(this.mesh.geometry.shallowClone(), materials.wireframe); this.mesh.add(this._wire); - this.opacity({temp: opt.opacity || meshApi.prefs.map.wireframe.opacity}); + this.opacity({temp: opt.opacity || 0.15}); } space.update(); return was; } - updateWireEdges() { - if (this._wire?.isLineSegments) { - const newEdges = new THREE.EdgesGeometry(this.mesh.geometry, this._wire._edges); - this._wire.geometry.dispose(); - this._wire.material.dispose(); - this._wire.geometry = newEdges; - } - } - normals(bool) { let was = this._norm ? true : false; if (bool === undefined) { @@ -511,7 +511,7 @@ class MeshModel extends meshObject { this._norm = undefined; } if (bool) { - this.mesh.add(this._norm = util.faceNormals(this.mesh)); + this.mesh.add(this._norm = mesh.util.faceNormals(this.mesh)); } } @@ -521,7 +521,7 @@ class MeshModel extends meshObject { // invert normals for entire mesh or selected faces depending on mode invert(mode) { - let { modes } = meshApi; + let { modes } = mesh.api; let geo = this.mesh.geometry; let pos = geo.attributes.position; let arr = pos.array; @@ -565,12 +565,12 @@ class MeshModel extends meshObject { return resolve(group); } if (o1?.length) - group.add(new meshModel({ + group.add(new mesh.model({ file: `${this.file}-bot`, mesh: o1 }).reCenter()); if (o2?.length) - group.add(new meshModel({ + group.add(new mesh.model({ file: `${this.file}-top`, mesh: o2 }).reCenter()); @@ -598,25 +598,6 @@ class MeshModel extends meshObject { this.group = undefined; this.removed = 'pending'; } else { - // Clean up geometries and materials - if (this._wire) { - this._wire.geometry.dispose(); - this._wire.material.dispose(); - this._wire = undefined; - } - if (this._norm) { - this._norm.geometry.dispose(); - this._norm.material.dispose(); - this._norm = undefined; - } - if (this.mesh) { - this.mesh.geometry.dispose(); - if (Array.isArray(this.mesh.material)) { - this.mesh.material.forEach(m => m.dispose()); - } else { - this.mesh.material.dispose(); - } - } // manage lifecycle with worker, mesh app caches, etc this.destroy(); // tag removed for debugging @@ -671,7 +652,7 @@ class MeshModel extends meshObject { let center = bounds.getCenter(new Vector3); polys.forEach(p => p.move({ x: -center.x, y: -center.y, z: -center.z })); let normal = new Vector3(0,0,1).applyQuaternion(quaternion); - let sketch = meshApi.add.sketch({ + let sketch = mesh.api.add.sketch({ normal, center: center.applyQuaternion(quaternion).add(this.position()) }); @@ -748,7 +729,7 @@ class MeshModel extends meshObject { } updateSelections() { - let groups = util.facesToGroups(this.sel.faces); + let groups = mesh.util.facesToGroups(this.sel.faces); let geo = this.mesh.geometry; geo.clearGroups(); for (let group of groups) { @@ -821,13 +802,13 @@ class MeshModel extends meshObject { let { a, b, c } = face; let timer = setTimeout(() => { timer = undefined; - meshApi.log.emit("matching surface").pin(); + mesh.api.log.emit("matching surface").pin(); }, 150); // let mark = Date.now(); worker.model_select({ id: this.id, x, y:-z, z:y, a, b, c, surface }).then(data => { - // meshApi.log.emit(`... data time = ${Date.now() - mark}`); mark = Date.now(); + // mesh.api.log.emit(`... data time = ${Date.now() - mark}`); mark = Date.now(); if (timer) { clearTimeout(timer); } @@ -835,16 +816,16 @@ class MeshModel extends meshObject { // console.log({data}); // this.toggleSelectedVertices(verts); this.selectFaces(faces, action); - // meshApi.log.emit(`... select time = ${Date.now() - mark}`); mark = Date.now(); + // mesh.api.log.emit(`... select time = ${Date.now() - mark}`); mark = Date.now(); this.updateSelections(); if (!timer) { - api.log.emit("surface match complete").unpin(); - space.refresh(); + mesh.api.log.emit("surface match complete").unpin(); + moto.space.refresh(); } - // meshApi.log.emit(`... paint time = ${Date.now() - mark}`); + // mesh.api.log.emit(`... paint time = ${Date.now() - mark}`); }); } -} +}; -export { MeshModel as model, materials }; +}); diff --git a/src2/mesh/object.js b/src.old/mesh/object.js similarity index 66% rename from src2/mesh/object.js rename to src.old/mesh/object.js index 349d6220..9c1bdea1 100644 --- a/src2/mesh/object.js +++ b/src.old/mesh/object.js @@ -1,26 +1,32 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { THREE } from '../ext/three.js'; -import { api } from './api.js'; -import { broker as motoBroker } from '../moto/broker.js'; -import { client as motoClient } from '../moto/client.js'; -import { space as motoSpace } from '../moto/space.js'; -import { util as meshUtil } from './util.js'; +"use strict"; + +// dep: add.three +// dep: moto.license +// dep: moto.broker +// dep: mesh.api +// use: mesh.util +gapp.register("mesh.object", [], (root, exports) => { const { Matrix4, Vector3, Box3, Box3Helper, Quaternion } = THREE; -const worker = motoClient.fn; +const { mesh, moto } = root; +const { space } = moto; + +const worker = moto.client.fn; const lookUp = new Vector3(0,0,-1); // broker updates generated by objects -const publish = { - meta: motoBroker.bind("object_meta"), - destroy: motoBroker.bind("object_destroy"), - visible: motoBroker.bind("object_visible"), +let publish = { + meta: gapp.broker.bind("object_meta"), + destroy: gapp.broker.bind("object_destroy"), + visible: gapp.broker.bind("object_visible"), }; -class MeshObject { +mesh.object = class MeshObject { + constructor(id) { - this.id = id || meshUtil.uuid(); + this.id = id || mesh.util.uuid(); this.meta = { pos: [0,0,0] }; worker.object_create({ id: this.id, type: this.type }); worker.object_meta({ id: this.id, meta: this.meta }); @@ -28,7 +34,7 @@ class MeshObject { } log() { - api.log.emit(this.id, this.type, ...arguments); + // mesh.api.log.emit(this.id, this.type, ...arguments); } get type() { @@ -49,7 +55,7 @@ class MeshObject { // update worker state worker.object_destroy({id: this.id}); // update object store - api.db.space.remove(this.id); + mesh.db.space.remove(this.id); // main app cache workspace updates publish.destroy(this.id); } @@ -86,7 +92,7 @@ class MeshObject { } focus() { - api.focus(this); + mesh.api.focus(this.object); return this; } @@ -104,15 +110,20 @@ class MeshObject { } floor() { - return this.move(0, 0, -this.bounds.min.z); + let b = this.bounds; + return this.move(0, 0, -b.min.z); } center() { throw "center() requires implementation"; + let b = this.bounds; + return this.move(-b.mid.x, -b.mid.y, -b.mid.z); } centerXY() { throw "centerXY() requires implementation"; + let b = this.bounds; + return this.move(-b.mid.x, -b.mid.y, 0); } move() { @@ -152,9 +163,41 @@ class MeshObject { throw "position() requires implementation"; } + showBounds(bool) { + let was = this._showBounds; + if (bool && bool.toggle) { + bool = !this._showBounds; + } + if (was === bool) { + return; + } + this._showBounds = bool; + this.updateBoundsBox(); + } + + updateBoundsBox() { + this.log('update-bounds-box', this._showBounds); + let helper = this._boundsBox; + let world = space.world; + if (helper) { + world.remove(helper); + } + if (this._showBounds) { + let { mid, dim } = this.bounds; + let b3 = new Box3().setFromCenterAndSize( + new Vector3(mid.x, mid.y, mid.z), + new Vector3(dim.x, dim.y, dim.z) + ); + let helper = this._boundsBox = new Box3Helper(b3, 0x555555); + world.add(helper); + return true; + } + } + metaChanged(values = {}) { this.object.updateMatrix(); - motoSpace.update(); + this.updateBoundsBox(); + space.update(); Object.assign(this.meta, values, { // matrix: this.object.matrix.elements }); @@ -162,6 +205,6 @@ class MeshObject { publish.meta({ id: this.id, meta: this.meta }); return this; } -} +}; -export { MeshObject as object }; +}); diff --git a/src.old/mesh/pool.js b/src.old/mesh/pool.js new file mode 100644 index 00000000..08863338 --- /dev/null +++ b/src.old/mesh/pool.js @@ -0,0 +1,9 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +// dep: moto.license +// dep: moto.worker +gapp.main("mesh.pool", [], (root) => { + // todo +}); diff --git a/src2/mesh/sketch.js b/src.old/mesh/sketch.js similarity index 85% rename from src2/mesh/sketch.js rename to src.old/mesh/sketch.js index be5504ba..bdf0fdf9 100644 --- a/src2/mesh/sketch.js +++ b/src.old/mesh/sketch.js @@ -1,18 +1,30 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { client as motoClient } from '../moto/client.js'; -import { space as motoSpace } from '../moto/space.js'; -import { broker } from '../moto/broker.js'; -import { object as meshObject } from './object.js'; -import { model as meshModel, materials } from './model.js'; -import { util as meshUtil } from './util.js'; -import { api as meshApi } from './api.js'; -import { newPolygon } from '../geo/polygon.js'; -import { polygons } from '../geo/polygons.js'; -import { THREE } from '../ext/three.js'; +"use strict"; -const { BufferGeometry, BufferAttribute, Quaternion, MeshBasicMaterial, LineBasicMaterial, LineSegments, DoubleSide, PlaneGeometry, EdgesGeometry, SphereGeometry, Vector3, Box3, Mesh, Group } = THREE; -const worker = motoClient.fn; +// dep: add.array +// dep: add.three +// dep: geo.polygon +// dep: geo.polygons +// dep: moto.license +// dep: moto.client +// dep: mesh.object +// dep: mesh.api +// use: mesh.util +// use: mesh.group +gapp.register("mesh.sketch", [], (root, exports) => { + +const { BufferGeometry, BufferAttribute, Quaternion } = THREE; +const { MeshBasicMaterial, LineBasicMaterial, LineSegments, DoubleSide } = THREE; +const { PlaneGeometry, EdgesGeometry, SphereGeometry, Vector3, Mesh, Group } = THREE; +const { base, mesh, moto } = root; +const { space, broker } = moto; +const { api, util } = mesh; +const { newPolygon } = base; + +const mapp = mesh; +const worker = moto.client.fn; +const POLYS = base.polygons; const drag = {}; const hpos = [ [-1, 1, 1], @@ -28,11 +40,11 @@ const material = { }; function log() { - meshApi.log.emit(...arguments); + mesh.api.log.emit(...arguments); } /** 2D plane containing open and closed polygons which can be extruded **/ -class MeshSketch extends meshObject { +mesh.sketch = class MeshSketch extends mesh.object { constructor(opt = {}) { super(opt.id); @@ -89,7 +101,7 @@ class MeshSketch extends meshObject { ); } - normal = new Vector3(normal.x, normal.y, normal.z).normalize(); + normal = new THREE.Vector3(normal.x, normal.y, normal.z).normalize(); group.quaternion.copy(new Quaternion().setFromUnitVectors(new Vector3(0, 0, 1), normal)); this.#db_save(); @@ -97,17 +109,17 @@ class MeshSketch extends meshObject { lookat(x,y,z) { this.group.lookAt(new Vector3(x,y,z)); - motoSpace.update(); + moto.space.update(); } #db_save() { let { center, normal, scale, type, file, items } = this; - meshApi.db.space.put(this.id, { center, normal, scale, type, file, items }); + mapp.db.space.put(this.id, { center, normal, scale, type, file, items }); } #db_remove() { - meshApi.sketch.remove(this); - meshApi.db.space.remove(this.id); + mapp.api.sketch.remove(this); + mapp.db.space.remove(this.id); } get type() { @@ -118,22 +130,6 @@ class MeshSketch extends meshObject { return this.group; } - get bounds() { - const { scale, center } = this; - return new Box3( - new Vector3( - center.x - scale.x/2, - center.y - scale.y/2, - center.z - ), - new Vector3( - center.x + scale.x/2, - center.y + scale.y/2, - center.z - ) - ); - } - get meshes() { return this.group.children.filter(c => { return c.sketch || (c.sketch_item && c.sketch_item.selected) ? c : undefined @@ -237,10 +233,10 @@ class MeshSketch extends meshObject { }, item(item) { - item.group = item.group || meshUtil.uuid(); + item.group = item.group || mesh.util.uuid(); sketch.items.push(item); sketch.render_defer(); - meshApi.selection.update_defer(); + api.selection.update_defer(); return item; } } @@ -253,7 +249,7 @@ class MeshSketch extends meshObject { let items = sketch.selection.mesh_items(); if (items.length < 2) return log('operation requires at least 2 items'); let polys = items.map(i => i.sketch_item.poly); - let union = polygons.union(polys, 0, true); + let union = POLYS.union(polys, 0, true); sketch.selection.delete(); sketch.arrange.group(union.map(poly => sketch.add.polygon({ poly }))); }, @@ -262,7 +258,7 @@ class MeshSketch extends meshObject { let items = sketch.selection.mesh_items(); if (items.length !== 2) return log('operation requires 2 items'); let polys = items.map(i => i.sketch_item.poly); - let trim = polygons.trimTo([polys[0]], [polys[1]]); + let trim = POLYS.trimTo([polys[0]], [polys[1]]); sketch.selection.delete(); for (let poly of trim) { sketch.add.polygon({ poly }); @@ -273,8 +269,8 @@ class MeshSketch extends meshObject { let items = sketch.selection.mesh_items(); if (items.length !== 2) return log('operation requires 2 items'); let polys = items.map(i => i.sketch_item.poly); - let diff1 = polygons.diff([polys[0]], [polys[1]]); - let diff2 = polygons.diff([polys[1]], [polys[0]]); + let diff1 = POLYS.diff([polys[0]], [polys[1]]); + let diff2 = POLYS.diff([polys[1]], [polys[0]]); sketch.selection.delete(); for (let poly of [...diff1, ...diff2]) { sketch.add.polygon({ poly }); @@ -285,7 +281,7 @@ class MeshSketch extends meshObject { let items = sketch.selection.items(); if (items.length < 2) return log('operation requires at least 2 items'); let polys = items.map(si => si.poly.annotate({group:si.group}).clone(true, ['group'])); - let union = polygons.nest(polygons.flatten(polys, [], true)); + let union = POLYS.nest(POLYS.flatten(polys, [], true)); sketch.selection.delete(); for (let poly of union) { sketch.add.polygon({ poly, group:poly.group }); @@ -295,8 +291,8 @@ class MeshSketch extends meshObject { evenodd() { let items = sketch.selection.items(); let polys = items.map(si => si.poly.annotate({group:si.group})); - let even = polygons.nest(polygons.flatten(polys.clone(true,['group']), [], true)); - let odd = polygons.nest(polygons.flatten(even.clone(true,['group']), [], true).filter(p => p.depth > 0)); + let even = POLYS.nest(POLYS.flatten(polys.clone(true,['group']), [], true)); + let odd = POLYS.nest(POLYS.flatten(even.clone(true,['group']), [], true).filter(p => p.depth > 0)); sketch.selection.delete(); for (let poly of [...even, ...odd]) { sketch.add.polygon({ poly, group:poly.group }); @@ -306,7 +302,7 @@ class MeshSketch extends meshObject { flatten() { let items = sketch.selection.items(); let polys = items.map(si => si.poly.annotate({group:si.group}).clone(true, ['group'])); - let flat = polygons.flatten(polys, [], true); + let flat = POLYS.flatten(polys, [], true); sketch.selection.delete(); for (let poly of flat) { sketch.add.polygon({ poly }); @@ -360,7 +356,7 @@ class MeshSketch extends meshObject { }, ungroup(items) { - items.forEach(i => i.group = meshUtil.uuid()); + items.forEach(i => i.group = mesh.util.uuid()); sketch.render(); } } @@ -411,20 +407,8 @@ class MeshSketch extends meshObject { return bool; } - position() { + move(x, y, z = 0) { const { target } = drag; - console.log({ sk_pos: [ ...arguments ], target }); - const [ nx, ny, nz ] = [ ...arguments ]; - if (nx || ny || nz) { - this.center = { x: nx, y: ny, z: nz }; - this.render(); - } else { - const { x, y, z } = this.center; - return new Vector3(x, y, z); - } - } - - move(x, y, z = 0, target = drag.target) { const { center, scale, plane, handles } = this; const handle = handles.indexOf(target); if (target === plane) { @@ -489,7 +473,7 @@ class MeshSketch extends meshObject { item?.center ?? center); } else if (offset) { let { start, item, handle } = drag; - let { snap, snapon } = meshApi.prefs.map.space; + let { snap, snapon } = api.prefs.map.space; let pos = handle >= 0 ? this.handle_pos(handle) : item?.center ?? center; @@ -534,12 +518,11 @@ class MeshSketch extends meshObject { group.add(...sketch_item.outs); } this.update(); - motoSpace.refresh(); - broker.publish('sketch_render', this); + space.refresh(); } render_defer() { - meshUtil.defer(() => this.render()); + util.defer(() => this.render()); } extrude(opt = {}) { @@ -554,13 +537,13 @@ class MeshSketch extends meshObject { chamfer_top, chamfer_bottom }); - let nmdl = new meshModel({ file: "item", mesh: vert.toFloat32() }); + let nmdl = new mesh.model({ file: "item", mesh: vert.toFloat32() }); models.push(nmdl); } if (models.length) { log('extrude', this.file || this.id, 'into', models.length, 'solid(s)'); let { center } = this; - let ngrp = meshApi.group.new(models); + let ngrp = api.group.new(models); ngrp.move(center.x, center.y, center.z); // align extrusion with sketch plane const euler = this.group.rotation; @@ -616,8 +599,9 @@ class SketchItem { update() { let { item, sketch, order } = this; + let { material } = mesh; let { type, center, width, height, miter, radius, points, spacing, poly, selected } = item; - let { open_close, open_width, open_type } = meshApi.prefs.map.sketch; + let { open_close, open_width, open_type } = api.prefs.map.sketch; let base = 0.025; let bump = sketch.scale.z || 0.0001; if (type === 'circle') { @@ -638,7 +622,7 @@ class SketchItem { this.poly = poly; let isSelected = selected && sketch.select(); // create solid filled area - let mat = (isSelected ? materials.select : materials.normal).clone(); + let mat = (isSelected ? material.select : material.normal).clone(); mat.transparent = true; mat.opacity = 0.5; let vrt = poly.extrude(0).toFloat32(); @@ -655,7 +639,7 @@ class SketchItem { let lpt = p.points.map(p => new Vector3(p.x, p.y, p.z)); lpt.push(lpt[0]); let lge = new BufferGeometry().setFromPoints(lpt); - let out = new THREE.Line(lge, materials.wireline); + let out = new THREE.Line(lge, material.wireline); out.renderOrder = -1; out.sketch_line = this; out.position.z += base + (order * bump); @@ -665,8 +649,4 @@ class SketchItem { } } -function target() { - return drag.target; -} - -export { MeshSketch as sketch, SketchItem, target }; \ No newline at end of file +}); \ No newline at end of file diff --git a/src2/mesh/split.js b/src.old/mesh/split.js similarity index 76% rename from src2/mesh/split.js rename to src.old/mesh/split.js index 229dcf60..065e27bc 100644 --- a/src2/mesh/split.js +++ b/src.old/mesh/split.js @@ -6,21 +6,24 @@ * click to split the selected mesh(es) */ -import { THREE } from '../ext/three.js'; -import { space as motoSpace } from '../moto/space.js'; -import { broker as motoBroker } from '../moto/broker.js'; -import { api as meshApi } from './api.js'; -import { util as meshUtil } from './util.js'; +"use strict"; +// dep: moto.space +// dep: moto.broker +gapp.register("mesh.split", [], (root, exports) => { + +const { broker } = gapp; const { Mesh, MeshPhongMaterial, PlaneGeometry, DoubleSide, Vector3 } = THREE; +const { mesh, moto } = root; +const { space } = moto; let isActive; -const key_once = motoBroker.bind('key_once'); -const key_cancel = motoBroker.bind('key_once_cancel'); +const key_once = broker.bind('key_once'); +const key_cancel = broker.bind('key_once_cancel'); // split functions -const split = { +let split = { active() { return isActive ? true : false; }, @@ -29,8 +32,9 @@ const split = { if (isActive) { return; } - let { log } = meshApi; - if (meshApi.selection.models().length === 0) { + let { api, util } = mesh; + let { log } = api; + if (api.selection.models().length === 0) { log.emit('no models selected for splitting'); return; } @@ -44,15 +48,15 @@ const split = { opacity: 0.5 }) ); - motoSpace.scene.add(obj); + space.scene.add(obj); // hide until first hover obj.visible = false; // enable temp mode let state = split.state = { obj }; - let models = state.models = meshApi.selection.models(); + let models = state.models = api.selection.models(); let meshes = models.map(m => m.mesh); // for split and lay flat modes - motoSpace.mouse.onHover((int, event, ints) => { + space.mouse.onHover((int, event, ints) => { if (!event) { return meshes; } @@ -61,11 +65,11 @@ const split = { if (buttons) { return; } - let { dim, mid } = meshUtil.bounds(models); + let { dim, mid } = util.bounds(meshes); let { point, face, object } = int; let { x, y, z } = point; // set appearance of split plane - mat.color.set(meshApi.prefs.map.space.dark ? 0x0059bb : 0x0079ff); + mat.color.set(api.prefs.map.space.dark ? 0x0059bb : 0x0079ff); obj.visible = true; if (event.shiftKey) { y = split.closestZ(y, object, face).y; @@ -88,31 +92,31 @@ const split = { state.plane = { z }; split.select(state); } - meshApi.modal.dialog({ + api.modal.dialog({ title: "split object Z", body: [ h.div({ class: "additem" }, [ h.label('Z value'), h.input({ value: 0, size: 5, id: "_value" }), h.button({ _: "split", onclick() { - const { _value } = meshApi.modal.bound; + const { _value } = api.modal.bound; doit(parseFloat(_value.value)); - meshApi.modal.hide(); + api.modal.hide(); } }) ]) ] }); - meshApi.modal.bound._value.focus(); + api.modal.bound._value.focus(); } }); }, select(state) { - let { log } = meshApi; + let { log } = mesh.api; let { models, plane } = split.state || state; if (!(models && plane)) { return split.end(); } log.emit(`splitting ${models.length} model(s) at ${plane.z.round(3)}`).pin(); Promise.all(models.map(m => m.split(plane))).then(models => { - meshApi.selection.set(models.filter(m => m)); + mesh.api.selection.set(models.filter(m => m)); log.emit('split complete').unpin(); split.end(); }); @@ -124,11 +128,12 @@ const split = { } key_cancel('KeyS'); key_cancel('KeyV'); + let space = moto.space; let { obj } = split.state; - motoSpace.scene.remove(obj); - motoSpace.mouse.onHover(undefined); + space.scene.remove(obj); + space.mouse.onHover(undefined); isActive = split.state = undefined; - meshApi.selection.update(); + mesh.api.selection.update(); }, closestZ(z, object, face) { @@ -142,6 +147,8 @@ const split = { v2._d = Math.abs(v2.y - z); return [ v0, v1, v2 ].sort((a,b) => a._d - b._d)[0]; } -}; +} -export { split }; +exports(split); + +}); diff --git a/src2/mesh/tool.js b/src.old/mesh/tool.js similarity index 95% rename from src2/mesh/tool.js rename to src.old/mesh/tool.js index 957b3d7d..851058bd 100644 --- a/src2/mesh/tool.js +++ b/src.old/mesh/tool.js @@ -1,9 +1,14 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { THREE } from '../ext/three.js'; -const { Vector3 } = THREE; -import { geom } from './geom.js'; +"use strict"; +// dep: ext.earcut +// dep: mesh.geom +gapp.register("mesh.tool", [], (root, exports) => { + +const { mesh } = root; +const { geom } = mesh; +const { Vector3 } = THREE; const empty = 0xffffffff; /** @@ -12,7 +17,7 @@ const empty = 0xffffffff; * two need to be merged. earcut needs to be migrated * to base.util.triagulate */ -class MeshTool { +mesh.tool = class MeshTool { constructor(params = {}) { this.precision = Math.pow(10, params.precision || 6); } @@ -244,17 +249,22 @@ class MeshTool { const s0 = faces[foff + 3]; const s1 = faces[foff + 4]; const s2 = faces[foff + 5]; - const adj = []; - for (let s of [s0, s1, s2]) { - const soff = s * 4; - const f1 = sides[soff + 2]; - const f2 = sides[soff + 3]; - if (f1 !== empty) adj.push(f1); - if (f2 !== empty) adj.push(f2); + const farr = [ + sides[s0 * 4 + 2], + sides[s0 * 4 + 3], + sides[s1 * 4 + 2], + sides[s1 * 4 + 3], + sides[s2 * 4 + 2], + sides[s2 * 4 + 3] + ].filter(f => f !== empty && f !== face); + if (!farr.length) { + console.log(`no adjacent faces to ${face}`); + return []; } - return adj; + return farr; } + // depends on index() being run first findConnectedSurface(faces, radians, filterZ, found = {}) { const norms = this.getIndex().faces; if (filterZ !== undefined) { @@ -294,6 +304,8 @@ class MeshTool { return faces; } + // given a list of faces (indices), return an array of closed + // polylines that represent the outlines of each discrete island generateOutlines(list) { const { faces, sides } = this.getIndex(); const prec = this.precision; @@ -368,6 +380,7 @@ class MeshTool { return outs; } + // depends on index() being run first isolateBodies() { const verts = this.checkVertices(this.vertices); const bodies = []; @@ -390,6 +403,12 @@ class MeshTool { return bodies; } + /** + * finds edge lines which are line segments on a single face. + * construct ordered line maps with array of connected edges. + * connect lines into polys. earcut polys into new faces. + * requires generateFaces() be run first + */ patch(opt = { merge: true }) { let vertices = this.checkVertices(this.uvert); let faces = this.faces; @@ -718,6 +737,7 @@ class MeshTool { return this; } + // add vertex data from given index into array appendVertex(index, array = []) { let vertx = this.uvert; array.push(vertx[index++]); @@ -726,6 +746,7 @@ class MeshTool { return array; } + // add vertices from a vertex index list to an array expandArray(indices, array = []) { for (let i of indices) { this.appendVertex(i, array); @@ -733,6 +754,7 @@ class MeshTool { return array; } + // turn loop into XYZ vertex array expandLoop(loop, array = []) { for (let line of loop) { this.appendVertex(line.v1, array); @@ -741,12 +763,14 @@ class MeshTool { return array; } + // merge generated poly areas into faces merge() { for (let area of this.areas || []) { this.faces.appendAll(area); } } + // return non-indexed vertex list (for compatibility) unrolled() { let out = []; for (let face of this.faces) { @@ -755,6 +779,8 @@ class MeshTool { return out; } + // returns points array for a polygon + // extrusion and twist is handled in work.js generateGear(numTeeth, module, pressureAngle, offset) { // Adapted from: Public Domain Parametric Involute Spur Gear by Leemon Baird, 2011, Leemon@Leemon.com http://www.thingiverse.com/thing:5505 // see also http://grabcad.com/questions/tutorial-how-to-model-involute-gears-in-solidworks-and-show-design-intent @@ -832,7 +858,7 @@ class MeshTool { } pitch = p.round(3); - geom.log(`gear pitch radius: ${pitch}`); + mesh.log(`gear pitch radius: ${pitch}`); } return { gear, pitch }; @@ -884,6 +910,7 @@ class MeshTool { } return verts; } -} -export { MeshTool as tool }; +}; + +}); diff --git a/src2/mesh/util.js b/src.old/mesh/util.js similarity index 64% rename from src2/mesh/util.js rename to src.old/mesh/util.js index 6f931ada..e5de2d01 100644 --- a/src2/mesh/util.js +++ b/src.old/mesh/util.js @@ -1,13 +1,20 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { THREE } from '../ext/three.js'; -import { api as meshApi } from './api.js'; +"use strict"; + +// dep: moto.license +// dep: add.array +// dep: add.three +gapp.register("mesh.util", [], (root, exports) => { + +const { Matrix4, Matrix3, Vector3, Box3 } = THREE; +const { mesh } = root; -const { Matrix3, Vector3, Box3, LineSegments, BufferGeometry, Float32BufferAttribute, LineBasicMaterial } = THREE; const deferFn = []; +const boundsCache = {}; // util functions augmented in build (download) -const util = { +const util = exports({ uuid(segs = 1) { let uid = []; @@ -64,20 +71,81 @@ const util = { } }, - // @param object {MeshObject | MeshObject[]} + // @param object {THREE.Object3D | THREE.Object3D[] | MeshObject | MeshObject[]} + // @returns bounds modified for moto.space bounds(object) { - // console.log({ bounds: object }); let box = new Box3(); if (Array.isArray(object)) { for (let o of object) { - box.union(o.bounds); + util.box3expand(box, o instanceof mesh.object ? o.object : o); } } else if (object) { - box.union(object.bounds); + util.box3expand(box, object instanceof mesh.object ? object.object : object); + } else { + return box; + } + let bnd = { + min: { + x: box.min.x, + y: box.min.z, + z: box.min.y + }, + max: { + x: box.max.x, + y: box.max.z, + z: box.max.y + } + }; + bnd.size = bnd.dim = { + x: bnd.max.x - bnd.min.x, + y: bnd.max.y - bnd.min.y, + z: bnd.max.z - bnd.min.z + }; + bnd.center = bnd.mid = { + x: (bnd.max.x + bnd.min.x) / 2, + y: -(bnd.max.y + bnd.min.y) / 2, + z: (bnd.max.z + bnd.min.z) / 2 + }; + return bnd; + }, + + // bounding box workaround adapted from: + // https://discourse.threejs.org/t/bounding-box-bigger-than-concave-object-extrudegeometry/26073/2 + // https://discourse.threejs.org/t/invalid-bounds-generated-for-some-orientations/33205 + box3expand(box3, object) { + if (object._no_bounds) { + return; + } + + let geometry = object.geometry; + object.updateWorldMatrix(geometry ? true : false, false); + + if (geometry) { + let matrix = object.matrixWorld; + let bkey = [matrix.elements.map(v => v.round(5))].join(',') + let cached = boundsCache[object.id]; + // geometry._model_invalid set on model.reload(), usually after a split + if (!cached || cached.bkey !== bkey || geometry._model_invalid) { + let position = geometry.attributes.position.clone(); + position.applyMatrix4(new Matrix4().extractRotation(matrix)); + let bounds = new Box3().setFromBufferAttribute(position); + // let scale = new Vector3().setFromMatrixScale(matrix); + // bounds.min.multiply(scale); + // bounds.max.multiply(scale); + cached = boundsCache[object.id] = { bkey, bounds }; + geometry._model_invalid = undefined; + } + let bt = cached.bounds.clone(); + let m4 = new Matrix4(); + m4.setPosition(new Vector3().setFromMatrixPosition(object.matrixWorld)); + bt.applyMatrix4(m4); + box3.union(bt); + } + + let children = object.children; + for (let i = 0, l = children.length; i < l; i++) { + util.box3expand(box3, children[i]); } - box.size = box.dim; - box.center = box.mid; - return box; }, // extract object fields into an array with optional rounding @@ -115,25 +183,25 @@ const util = { }, faceNormals(obj, opt = { }) { - const _va = new Vector3(); - const _vb = new Vector3(); - const _vc = new Vector3(); - const _v1 = new Vector3(); - const _v2 = new Vector3(); - const _normalMatrix = new Matrix3(); - const prefs = meshApi.prefs.map; + const _va = new THREE.Vector3(); + const _vb = new THREE.Vector3(); + const _vc = new THREE.Vector3(); + const _v1 = new THREE.Vector3(); + const _v2 = new THREE.Vector3(); + const _normalMatrix = new THREE.Matrix3(); + const prefs = mesh.api.prefs.map; const norms = prefs.normals; const defcolor = prefs.space.dark ? norms.color_dark : norms.color_lite; const normlen = norms.length || 1; - class FaceNormalsHelper extends LineSegments { + class FaceNormalsHelper extends THREE.LineSegments { constructor(object, size = opt.size || normlen, color = opt.color || defcolor) { const objGeometry = object.geometry; const nNormals = objGeometry.attributes.position.count / 3; - const geometry = new BufferGeometry(); - const positions = new Float32BufferAttribute(nNormals * 3 * 2, 3); + const geometry = new THREE.BufferGeometry(); + const positions = new THREE.Float32BufferAttribute(nNormals * 3 * 2, 3); geometry.setAttribute('position', positions); - super(geometry, new LineBasicMaterial({ + super(geometry, new THREE.LineBasicMaterial({ color, toneMapped: false })); this.object = object; @@ -172,21 +240,20 @@ const util = { const _v2 = new Vector3(); const _normalMatrix = new Matrix3(); - class VertexNormalsHelper extends LineSegments { + class VertexNormalsHelper extends THREE.LineSegments { constructor(object, size = 1, color = 0xff0000) { const objGeometry = object.geometry; const nNormals = objGeometry.attributes.normal.count; - const geometry = new BufferGeometry(); - const positions = new Float32BufferAttribute(nNormals * 2 * 3, 3); + const geometry = new THREE.BufferGeometry(); + const positions = new THREE.Float32BufferAttribute(nNormals * 2 * 3, 3); geometry.setAttribute('position', positions); - super(geometry, new LineBasicMaterial({ - color, toneMapped: false + super(geometry, new THREE.LineBasicMaterial({ + color,toneMapped: false })); this.object = object; this.size = size; this.type = 'VertexNormalsHelper'; this.matrixAutoUpdate = false; - this._no_bounds = true; this.update(); } @@ -194,13 +261,12 @@ const util = { this.object.updateMatrixWorld(true); const position = this.geometry.attributes.position; const objGeometry = this.object.geometry; - const objArr = objGeometry.attributes.position.array; - const normArr = objGeometry.attributes.normal.array; - let j=0; - for (let idx = 0, jl = objArr.length; j < jl; ) { - _v1.set(objArr[j++], objArr[j++], objArr[j++]); - _v2.set(normArr[j-3], normArr[j-2], normArr[j-1]); - _v2.multiplyScalar(this.size).add(_v1); + const objPos = objGeometry.attributes.position; + const objNorm = objGeometry.attributes.normal; + for (let idx = 0, j = 0, jl = objPos.count; j < jl; j++) { + _v1.set(objPos.getX(j), objPos.getY(j), objPos.getZ(j)); + _v2.set(objNorm.getX(j), objNorm.getY(j), objNorm.getZ(j)); + _v2.applyMatrix3(_normalMatrix).normalize().multiplyScalar(this.size).add(_v1); position.setXYZ(idx++, _v1.x, _v1.y, _v1.z); position.setXYZ(idx++, _v2.x, _v2.y, _v2.z); } @@ -266,6 +332,6 @@ const util = { } } -}; +}); -export { util }; +}); diff --git a/src2/mesh/work.js b/src.old/mesh/work.js similarity index 81% rename from src2/mesh/work.js rename to src.old/mesh/work.js index dd9f04de..ab242d52 100644 --- a/src2/mesh/work.js +++ b/src.old/mesh/work.js @@ -1,36 +1,43 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import "../ext/clip2.js"; -import "../ext/earcut.js"; -import '../add/array.js'; -import '../add/class.js'; -import '../add/three.js'; +"use strict"; -import { version } from '../moto/license.js'; -import { client as motoClient } from '../moto/client.js'; -import { worker } from '../moto/worker.js'; -import { sliceConnect } from '../geo/slicer.js'; -import { newPolygon } from '../geo/polygon.js'; -import { newPoint } from "../geo/point.js"; -import { polygons } from '../geo/polygons.js'; -import { CSG } from '../geo/csg.js'; -import { util } from './util.js'; -import { tool as meshTool } from './tool.js'; +// dep: moto.license +// dep: ext.clip2 +// dep: add.three +// dep: add.array +// dep: moto.client +// dep: moto.worker +// dep: mesh.util +// dep: geo.base +// dep: geo.line +// dep: geo.point +// dep: geo.polygon +// dep: geo.polygons +// dep: geo.bounds +// dep: geo.slicer +// dep: geo.csg +// dep: mesh.tool +gapp.main("mesh.work", [], (root) => { const { Triangle, Vector3, BufferGeometry, BufferAttribute, computeFaceNormal } = THREE; - +const { base, mesh, moto } = root; +const { client, worker } = moto; +const { CSG, newPoint, newPolygon, sliceConnect, polygons } = base; const cache = {}; // add scoped access to cache -const work = { cache }; +mesh.work = { cache }; // start worker pool (disabled for now with *0) -motoClient.start(`/v2/lib/mesh/pool.js?${version}`, motoClient.max() * 0); +client.start(`/code/mesh_pool?${gapp.version}`, client.max() * 0); function log(msg) { return worker.publish("mesh.log", msg); } +mesh.log = log; + function cacheUpdate(id, data) { return Object.assign(cache[id], data); } @@ -53,7 +60,7 @@ function analyze(id, opt = {}) { let rec = cache[id]; let { geo, tool } = rec; if (!tool) { - tool = rec.tool = new meshTool(opt); + tool = rec.tool = new mesh.tool(opt); } if (tool.faces) { log(`${id} | analysis cached`); @@ -76,7 +83,7 @@ function indexFaces(id) { let rec = cache[id]; let { geo, tool } = rec; if (!tool) { - tool = rec.tool = new meshTool(); + tool = rec.tool = new mesh.tool(); } if (!tool.indexed) { log(`${id} | indexing mesh`); @@ -152,7 +159,7 @@ function splitHeal(z, o1, edges, rev) { return edges; } -const model = { +let model = { load(data) { let { vertices, name, id } = data; let geo = new BufferGeometry(); @@ -200,8 +207,12 @@ const model = { let tools = recs.filter(rec => rec.tool) .map(rec => translate_encode(rec.id)) .map(a => CSG.fromPositionArray(a)); - let diff = CSG.difference(...bases, ...tools); - return CSG.toPositionArray(diff); + let subs = []; + for (let obj of bases) { + let sub = CSG.subtract(obj, ...tools); + subs.appendAll(CSG.toPositionArray(sub)); + } + return subs.toFloat32(); }, // split a model along an axis at a given point @@ -433,7 +444,7 @@ const model = { gen_gear(data, send) { const { teeth, module, angle, twist, shaft, offset, height, chamfer } = data; - const { gear, pitch } = new meshTool().generateGear(teeth, module, angle, offset); + const { gear, pitch } = new mesh.tool().generateGear(teeth, module, angle, offset); const points = gear.map(v => [...v, 0]).flat(); const poly = newPolygon().addVerts(points); if (shaft) { @@ -444,9 +455,9 @@ const model = { const zh = height || 15; const verts = poly.extrude(zh, { chamfer }); if (twist) { - const rad = util.toRadians(twist); + const rad = base.util.toRadians(twist); for (let i=0; i v.round(5)); - vtot += (vs.length / 3); - } - switch (format) { - case "obj": - let p = 1; - let obj = [header]; - for (let rec of recs) { - let { file, varr } = rec; - obj.push(`g ${file}`); - for (let i=0; i { - dat.setUint8(i, c.charCodeAt(0)); - }); - // todo put Mesh:Tool info in header - dat.setInt32(80, vtot/3, true); - for (let rec of recs) { - let { id, matrix, file, varr } = rec; - for (let i=0, l=varr.length; i -- All Rights Reserved */ -import { local } from '../data/local.js'; +"use strict"; + +// dep: data.local +gapp.register("moto.ajax", [], (root, exports) => { function rnd() { return Math.round(Math.random()*0xffffffff).toString(36); } +const { data } = root; +const { local } = data; + const KEY = "moto-ajax"; -const MOID = local.getItem(KEY) || (Date.now().toString(36)+rnd()+rnd()); +const MOID = moto.id = local.getItem(KEY) || (Date.now().toString(36)+rnd()+rnd()); local.setItem(KEY, MOID); @@ -63,13 +69,14 @@ function call(url, handler) { } function restore(id) { - MOID = id; + MOID = moto.id = id; local.setItem(KEY, MOID); } -export const ajax = { - new: (cb, rt) => new Ajax(cb, rt), +exports({ + new(cb, rt) { return new Ajax(cb, rt) }, call, restore, - id: MOID -}; +}); + +}); diff --git a/src2/moto/broker.js b/src.old/moto/broker.js similarity index 84% rename from src2/moto/broker.js rename to src.old/moto/broker.js index 55b4c4a6..6e73a75b 100644 --- a/src2/moto/broker.js +++ b/src.old/moto/broker.js @@ -1,12 +1,15 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +"use strict"; + +gapp.register("moto.broker", [], (root, exports) => { + class Broker { - #debug = false; - #topics = {}; - #used = {}; - #send = {}; constructor() { + this.topics = {}; + this.used = {}; + this.send = {}; // using the pattern "broker.send(msg)" induces runtime errors // when there is not at least one registered listener for a topic // while also allowing for a more natural function call interface @@ -14,11 +17,7 @@ class Broker { } topics() { - return Object.keys(this.#topics); - } - - get send() { - return this.#send; + return Object.keys(this.topics); } // create subscriptions for all functions in an object @@ -43,19 +42,16 @@ class Broker { return this; } if (typeof topic !== 'string') { - for (let [ key, fn ] of Object.entries(topic)) { - this.subscribe(key, fn.bind(listener)); - } - // console.trace({invalid_topic: topic}); + console.trace({invalid_topic: topic}); return; } - let topics = this.#topics; + let topics = this.topics; let channel = topics[topic]; if (!channel) { channel = topics[topic] = []; } if (channel.indexOf(listener) < 0) { - let send = this.#send; + let send = this.send; let name = topic.replace(/[\\ \.-]/g, '_'); send[name] = send[name] = this.bind(topic); channel.push(listener); @@ -65,7 +61,7 @@ class Broker { } unsubscribe(topic, listener) { - let channel = this.#topics[topic]; + let channel = this.topics[topic]; if (!channel) { return; } @@ -75,7 +71,7 @@ class Broker { } channel.splice(index,1); if (channel.length === 0) { - delete this.#topics[topic]; + delete this.topics[topic]; this.publish(".topic.remove", topic); } } @@ -90,13 +86,10 @@ class Broker { if (topic !== ".topic.publish") { this.publish(".topic.publish", {topic, message, options}); } - if (this.#debug && !topic.startsWith(".")) { - console.log({ topic, message }); - } // store last seen message on a topic // acts as a tracker for all used topics - this.#used[topic] = message; - let channel = this.#topics[topic]; + this.used[topic] = message; + let channel = this.topics[topic]; if (channel && channel.length) { for (let listener of channel) { try { @@ -143,6 +136,10 @@ class Broker { } } } + } -export const broker = new Broker(); +const { moto } = root; +gapp.broker = moto.broker = new Broker(); + +}); diff --git a/src2/moto/client.js b/src.old/moto/client.js similarity index 94% rename from src2/moto/client.js rename to src.old/moto/client.js index f9ea23c2..0392739e 100644 --- a/src2/moto/client.js +++ b/src.old/moto/client.js @@ -2,7 +2,9 @@ "use strict"; -import { broker } from './broker.js'; +// dep: add.array +// dep: moto.broker +gapp.register("moto.client", [], (root, exports) => { let ccvalue = self.navigator ? navigator.hardwareConcurrency || 1 : 1, ccmax = ccvalue > 3 ? ccvalue - 1 : 1, @@ -20,7 +22,8 @@ function dispatch(topic, message) { } // code is running in the browser / client context -const client = { +const client = exports({ + /** @returns [integer] total number of workers **/ live: () => { return workers.length; @@ -150,7 +153,7 @@ const client = { if (self.createWorker) { return self.createWorker(); } else { - return new Worker(workurl = url, { type: 'module' }); + return new Worker(workurl = url); } }, @@ -188,14 +191,9 @@ const client = { let worker = client.spawn(workurl); worker.run = null; worker.index = i; - worker.onerror = function(e) { - console.log({ worker_error: e }); - } - worker.onmessageerror = function(e) { - console.log({ worker_message_error: e }); - } worker.onmessage = function(e) { let reply = e.data; + // handle special case for task binding if (reply.bind) { client.bind(reply.bind); @@ -210,7 +208,7 @@ const client = { // handle special case for broker publish if (reply.publish) { - broker.publish(reply.publish, reply.message); + gapp.broker.publish(reply.publish, reply.message); return; } @@ -253,6 +251,7 @@ const client = { client.kick(); } -}; -export { client }; +}); + +}); diff --git a/src.old/moto/license.js b/src.old/moto/license.js new file mode 100644 index 00000000..768c94d4 --- /dev/null +++ b/src.old/moto/license.js @@ -0,0 +1,27 @@ +(function() { + + const is_self = typeof(self) !== 'undefined'; + + let terms = { + COPYRIGHT: "Copyright (C) Stewart Allen - All Rights Reserved", + LICENSE: "See the license.md file included with the source distribution", + VERSION: (is_self ? self : this).debug_version || "4.2.0" + }; + + if (typeof(module) === 'object') { + module.exports = terms; + } + + // allow license to be required() by app without gapp + if (is_self && self.gapp) { + let app = self.gapp; + app.license = terms.LICENSE; + app.version = terms.VERSION; + app.copyright = terms.COPYRIGHT; + // satisfy resolver + gapp.register('moto.license'); + // for earcut and other modules + (self.module = self.module || {}).exports = terms; + } + +})(); diff --git a/src.old/moto/orbit.js b/src.old/moto/orbit.js new file mode 100644 index 00000000..ccb2b8e3 --- /dev/null +++ b/src.old/moto/orbit.js @@ -0,0 +1,638 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +// dep: add.three +gapp.register("moto.orbit", [], (root, exports) => { + +const { moto } = root; + +/** + * Adapted from THREE.OrbitControls + */ +moto.Orbit = function (object, domElement, notify, slider) { + + this.object = object; + this.domElement = ( domElement !== undefined ) ? domElement : document; + + // Set to false to disable this control + this.enabled = true; + + // "target" sets the location of focus, where the control orbits around + // and where it pans with respect to. + this.target = new THREE.Vector3(); + + // center is old, deprecated; use "target" instead + this.center = this.target; + + // This option actually enables dollying in and out; left as "zoom" for + // backwards compatibility + this.noZoom = false; + this.zoomSpeed = 1.0; + this.reverseZoom = false; + + // Limits to how far you can dolly in and out + this.minDistance = 0; + this.maxDistance = Infinity; + + // Set to true to disable this control + this.noRotate = false; + this.rotateSpeed = 1.0; + + // Set to true to disable this control + this.noPan = false; + this.keyPanSpeed = 7.0; // pixels moved per arrow key push + + // How far you can orbit vertically, upper and lower limits. + // Range is 0 to Math.PI radians. + this.minPolarAngle = 0; // radians + this.maxPolarAngle = Math.PI; // radians + + // How far you can orbit horizontally, upper and lower limits. + // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. + this.minAzimuthAngle = -Infinity; // radians + this.maxAzimuthAngle = Infinity; // radians + + // Set to true to disable use of the keys + this.noKeys = false; + + // The four arrow keys + this.keys = { + UP: 38, + LEFT: 37, + RIGHT: 39, + BOTTOM: 40 + }; + + // default mouse buttons + this.mouseDefault = { + ORBIT: THREE.MOUSE.LEFT, + ZOOM: THREE.MOUSE.MIDDLE, + PAN: THREE.MOUSE.RIGHT + }; + + // Onshape + this.mouseOnshape = { + ORBIT: THREE.MOUSE.RIGHT, + ZOOM: THREE.MOUSE.LEFT, + PAN: THREE.MOUSE.MIDDLE + }; + + this.mouseButtons = this.mouseDefault; + + this.setMouse = function(bindings) { + this.mouseButtons = bindings; + }; + + let scope = this, + domEl = scope.domElement, + EPS = 0.000001, + rotateStart = new THREE.Vector2(), + rotateEnd = new THREE.Vector2(), + rotateDelta = new THREE.Vector2(), + panStart = new THREE.Vector2(), + panEnd = new THREE.Vector2(), + panDelta = new THREE.Vector2(), + panOffset = new THREE.Vector3(), + offset = new THREE.Vector3(), + dollyStart = new THREE.Vector2(), + dollyEnd = new THREE.Vector2(), + dollyDelta = new THREE.Vector2(), + theta, + thetaDelta = 0, + thetaSet = null, + phi, + phiDelta = 0, + phiSet = null, + scale = 1, + scaleSave = 1, + pan = new THREE.Vector3(), + lastPosition = new THREE.Vector3(), + lastQuaternion = new THREE.Quaternion(), + // so camera.up is the orbit axis + quat = new THREE.Quaternion().setFromUnitVectors(object.up, new THREE.Vector3(0, 1, 0)), + quatInverse = quat.clone().invert(), + // touch tracking + firstTouch = [], + touchSlide = false, + lastDY = 0, + // events + changeEvent = { type: 'change'}, + startEvent = { type: 'start'}, + endEvent = { type: 'end'}; + + let STATE = { NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5}, + state = STATE.NONE, + MODE = { PERSPECTIVE: 1, ORTHOGRAPHIC: 2, UNKNOWN: 3}, + mode = isValue(object.fov) ? MODE.PERSPECTIVE : isValue(object.top) ? MODE.ORTHOGRAPHIC : MODE.UNKNOWN; + + // for reset + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + + this.rotateLeft = function (angle) { + thetaDelta -= angle; + }; + + this.rotateUp = function (angle) { + phiDelta -= angle; + }; + + // pass in distance in world space to move left + this.panLeft = function (distance) { + let te = this.object.matrix.elements; + + // get X column of matrix + panOffset.set(te[ 0 ], te[ 1 ], te[ 2 ]); + panOffset.multiplyScalar(-distance); + pan.add(panOffset); + }; + + // pass in distance in world space to move up + this.panUp = function (distance) { + let te = this.object.matrix.elements; + + // get Y column of matrix + panOffset.set(te[ 4 ], te[ 5 ], te[ 6 ]); + panOffset.multiplyScalar(distance); + pan.add(panOffset); + }; + + // pass in distance in world space to move backward + this.panBack = function (distance) { + let te = this.object.matrix.elements; + + // get Y column of matrix + panOffset.set(te[ 8 ], te[ 9 ], te[ 10 ]); + panOffset.multiplyScalar(distance); + pan.add(panOffset); + }; + + // pass in x,y of change desired in pixel space, + // right and down are positive + this.pan = function (deltaX, deltaY) { + let element = scope.domElement === document ? scope.domElement.body : scope.domElement; + + switch (mode) { + case MODE.PERSPECTIVE: + let position = scope.object.position; + let offset = position.clone().sub(scope.target); + let targetDistance = offset.length(); + + // half of the fov is center to top of screen + targetDistance *= Math.tan(( scope.object.fov / 2 ) * Math.PI / 180.0); + + // we actually don't use screenWidth, since perspective camera is fixed to screen height + scope.panLeft(2 * deltaX * targetDistance / element.clientHeight); + scope.panUp(2 * deltaY * targetDistance / element.clientHeight); + break; + case MODE.ORTHOGRAPHIC: + scope.panLeft(deltaX * (scope.object.right - scope.object.left) / element.clientWidth); + scope.panUp(deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight); + break; + } + }; + + this.getTarget = function() { + return this.target; + }; + + this.setTarget = function(t) { + this.target = new THREE.Vector3().add(t); + this.target0 = this.target.clone(); + } + + function isValue(v) { + return v !== null && v !== undefined; + } + + function firstValue(choices) { + for (let i=0; i EPS + // using small-angle approximation cos(x/2) = 1 - x^2 / 8 + if (lastPosition.distanceToSquared(this.object.position) > EPS + || 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS) { + + this.dispatchEvent(changeEvent); + lastPosition.copy(this.object.position); + lastQuaternion.copy(this.object.quaternion); + if (notify) notify(position, true); + } else { + if (notify) notify(position, false); + } + }; + + this.reset = function () { + state = STATE.NONE; + scale = 1; + scaleSave = 1; + this.target.copy(this.target0); + this.object.position.copy(this.position0); + }; + + this.getPolarAngle = function () { + return phi; + }; + + this.getAzimuthalAngle = function () { + return theta + }; + + function getZoomScale(factor = 1) { + return Math.pow(0.95, factor * scope.zoomSpeed); + } + + function onMouseDown(event) { + if (scope.enabled === false) return; + event.preventDefault(); + + switch (event.button) { + case scope.mouseButtons.ORBIT: + state = event.metaKey ? STATE.PAN : STATE.ROTATE; + break; + case scope.mouseButtons.ZOOM: + state = STATE.DOLLY; + break; + case scope.mouseButtons.PAN: + state = STATE.PAN; + break; + } + + switch (state) { + case STATE.ROTATE: + if (scope.noRotate === true) return state = STATE.NONE; + rotateStart.set(event.clientX, event.clientY); + break; + case STATE.DOLLY: + if (scope.noZoom === true) return state = STATE.NONE; + dollyStart.set(event.clientX, event.clientY); + break; + case STATE.PAN: + if (scope.noPan === true) return state = STATE.NONE; + panStart.set(event.clientX, event.clientY); + break; + } + + document.addEventListener('mousemove', onMouseMove, false); + document.addEventListener('mouseup', onMouseUp, false); + scope.dispatchEvent(startEvent); + } + + function onMouseMove(event) { + if (scope.enabled === false) return; + event.preventDefault(); + + let element = scope.domElement === document ? scope.domElement.body : scope.domElement; + + if (state === STATE.ROTATE) { + if (scope.noRotate === true) return; + + rotateEnd.set(event.clientX, event.clientY); + rotateDelta.subVectors(rotateEnd, rotateStart); + + // rotating across whole screen goes 360 degrees around + scope.rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed); + // rotating up and down along whole screen attempts to go 360, but limited to 180 + scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed); + + rotateStart.copy(rotateEnd); + + } else if (state === STATE.DOLLY) { + if (scope.noZoom === true) return; + + dollyEnd.set(event.clientX, event.clientY); + dollyDelta.subVectors(dollyEnd, dollyStart); + + if (dollyDelta.y > 0) { + scope.dollyIn(); + } else { + scope.dollyOut(); + } + + dollyStart.copy(dollyEnd); + + } else if (state === STATE.PAN) { + if (scope.noPan === true) return; + + panEnd.set(event.clientX, event.clientY); + panDelta.subVectors(panEnd, panStart); + scope.pan(panDelta.x, panDelta.y); + panStart.copy(panEnd); + } + + scope.update(); + } + + function onMouseUp(event) { + if (scope.enabled === false) return; + + document.removeEventListener('mousemove', onMouseMove, false); + document.removeEventListener('mouseup', onMouseUp, false); + scope.dispatchEvent(endEvent); + state = STATE.NONE; + } + + function onMouseWheel(event) { + if (scope.enabled === false || scope.noZoom === true) return; + + event.preventDefault(); + event.stopPropagation(); + + if (event.shiftKey && slider) { + slider(event.deltaX || event.wheelDelta || event.detail); + return; + } + + slider(null); + + let delta = -(event.deltaY || event.wheelDelta || event.detail || 0); + + if (delta === 0) return; + + if (scope.reverseZoom) { + delta = -delta; + } + + if (delta > 0) { + scope.dollyOut(); + } else if (delta < 0) { + scope.dollyIn(); + } + + scope.update(); + scope.dispatchEvent(startEvent); + scope.dispatchEvent(endEvent); + } + + function onKeyDown(event) { + if (scope.enabled === false || scope.noKeys === true || scope.noPan === true) return; + + switch (event.keyCode) { + + case scope.keys.UP: + scope.pan(0, scope.keyPanSpeed); + scope.update(); + break; + + case scope.keys.BOTTOM: + scope.pan(0, -scope.keyPanSpeed); + scope.update(); + break; + + case scope.keys.LEFT: + scope.pan(scope.keyPanSpeed, 0); + scope.update(); + break; + + case scope.keys.RIGHT: + scope.pan(-scope.keyPanSpeed, 0); + scope.update(); + break; + } + } + + function touchstart(event) { + if (scope.enabled === false) return; + + const touches = [...event.touches]; + + switch (event.touches.length) { + case 1: // one-fingered touch: rotate + if (scope.noRotate === true) return; + + state = STATE.TOUCH_ROTATE; + + rotateStart.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); + break; + + case 2: // two-fingered touch: dolly + if (scope.noZoom === true) return; + + state = STATE.TOUCH_DOLLY; + + let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + let distance = Math.sqrt(dx * dx + dy * dy); + + firstTouch = [...event.touches].map(v => { return {x:v.pageX, y:v.pageY} }); + touchSlide = false; + lastDY = 0; + + dollyStart.set(0, distance); + break; + + case 3: // three-fingered touch: pan + case 4: // four-fingered touch: pan + if (scope.noPan === true) return; + + state = STATE.TOUCH_PAN; + const px = touches.map(t => t.pageX).reduce((a,b) => a+b) / touches.length; + const py = touches.map(t => t.pageY).reduce((a,b) => a+b) / touches.length; + + panStart.set(px, py); + break; + + default: + state = STATE.NONE; + } + scope.dispatchEvent(startEvent); + } + + function touchmove(event) { + if (scope.enabled === false) return; + + event.preventDefault(); + event.stopPropagation(); + + const element = scope.domElement === document ? scope.domElement.body : scope.domElement; + + switch (event.touches.length) { + case 1: // one-fingered touch: rotate + if (scope.noRotate === true) return; + if (state !== STATE.TOUCH_ROTATE) return; + + rotateEnd.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); + rotateDelta.subVectors(rotateEnd, rotateStart); + + // rotating across whole screen goes 360 degrees around + scope.rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed); + // rotating up and down along whole screen attempts to go 360, but limited to 180 + scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed); + + rotateStart.copy(rotateEnd); + scope.update(); + break; + + case 2: // two-fingered touch: dolly + if (scope.noZoom === true) return; + if (state !== STATE.TOUCH_DOLLY) return; + + let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + let distance = Math.sqrt(dx * dx + dy * dy); + + let touches = [...event.touches].map(v => { return {x:v.pageX, y:v.pageY} }); + let ddx = ( (touches[0].x - firstTouch[0].x) + (touches[1].x - firstTouch[1].x) ) / 2; + let ddy = ( (touches[0].y - firstTouch[0].y) + (touches[1].y - firstTouch[1].y) ) / 2; + + if (scope.reverseZoom) distance = -distance; + + dollyEnd.set(0, distance); + dollyDelta.subVectors(dollyEnd, dollyStart); + + if (touchSlide || (Math.abs(dy/dx) < 1 && slider && dx < 200)) { + touchSlide = true; + slider(lastDY ? (lastDY - ddy) : ddy); + lastDY = ddy; + } else { + if (dollyDelta.y > 0) { + scope.dollyOut(0.5); + } else if (dollyDelta.y < 0) { + scope.dollyIn(0.5); + } + } + + dollyStart.copy(dollyEnd); + scope.update(); + break; + + case 3: // three-fingered touch: pan + case 4: // four-fingered touch: pan + if (scope.noPan === true) return; + if (state !== STATE.TOUCH_PAN) return; + + const tlist = [...event.touches]; + const px = tlist.map(t => t.pageX).reduce((a,b) => a+b) / tlist.length; + const py = tlist.map(t => t.pageY).reduce((a,b) => a+b) / tlist.length; + + panEnd.set(px, py); + panDelta.subVectors(panEnd, panStart); + scope.pan(panDelta.x, panDelta.y); + panStart.copy(panEnd); + + scope.update(); + break; + + default: + state = STATE.NONE; + } + } + + function touchend(/* event */) { + if (scope.enabled === false) return; + scope.dispatchEvent(endEvent); + state = STATE.NONE; + } + + this.onMouseUp = onMouseUp; + + domEl.addEventListener('contextmenu', function (event) { event.preventDefault() }, false); + domEl.addEventListener('mousedown', onMouseDown, false); + domEl.addEventListener('mousewheel', onMouseWheel, false); + domEl.addEventListener('DOMMouseScroll', onMouseWheel, false); // firefox + domEl.addEventListener('touchstart', touchstart, false); + domEl.addEventListener('touchend', touchend, false); + domEl.addEventListener('touchmove', touchmove, false); + + window.addEventListener('keydown', onKeyDown, false); +}; + +moto.Orbit.prototype = Object.create(THREE.EventDispatcher.prototype); + +}); diff --git a/src2/moto/service.js b/src.old/moto/service.js similarity index 99% rename from src2/moto/service.js rename to src.old/moto/service.js index bc5b9669..7c8e1f3b 100644 --- a/src2/moto/service.js +++ b/src.old/moto/service.js @@ -1,7 +1,6 @@ // code for a ServiceWorker that provides support for running cached/offline -import { version } from './license.js'; - +const version = self.gapp.version; const origin = self.location.origin; const stats = { hit: 0, diff --git a/src.old/moto/space.js b/src.old/moto/space.js new file mode 100644 index 00000000..cb2e1a8e --- /dev/null +++ b/src.old/moto/space.js @@ -0,0 +1,1510 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; + +// dep: ext.tween +// dep: add.three +// dep: add.array +// dep: moto.orbit +gapp.register("moto.space", [], (root, exports) => { + + const { moto } = root; + const { WebGLRenderer } = THREE; + const nav = navigator; + + let WIN = window, + DOC = document, + SCENE = new THREE.Scene(), + WORLD = new THREE.Group(), + PI = Math.PI, + PI2 = PI / 2, + PI4 = PI / 4, + panX = 0, + panY = 0, + panZ = 0, + home = 0, + up = PI4, + gridZOff = 0, + tweenTime = 500, + tweenDelay = 20, + platformZOff = 0, + perspective = 35, + refreshTimeout = null, + refreshRequested = false, + selectRecurse = false, + defaultKeys = true, + initialized = false, + alignedTracking = false, + skyAmbient, + skyGridColor = 0xcccccc, + skyGridMaterial = undefined, + showSkyGrid = false, + showPlatform = true, + hidePlatformBelow = true, + hideGridBelow = false, + showGrid = true, + showGridLines = true, + showFocus = 0, + focalPoint = undefined, + lightInfo = { + mode: 2, + array: [], + intensity: 0.09, + debug: false + }, + cameraLight = addLight(0, 0, 0, lightInfo.intensity), + origin = {x:0, y:0, z: 0}, + mouse = {x: 0, y: 0}, + mouseStart = null, + mouseDragPoint = null, + mouseDragStart = null, + mouseDownSelect, + mouseUpSelect, + mouseUp, + mouseDown, + mouseHover, + mouseDrag, + grid = { + origin: origin, + unitMinor: 0, + unitMajor: 0, + colorMinor: 0xeeeeee, + colorMajor: 0xcccccc, + colorX: 0xff6666, + colorY: 0x6666ff, + zoff: 0, + opacity: 1, + view: undefined, + axes: undefined, + lines: undefined + }, + ruler = { + x1: 0, + x2: 0, + y1: 0, + y2: 0, + xlabel: 'X', + ylabel: 'Y', + xon: undefined, + yon: undefined, + factor: undefined, + view: undefined + }, + psize = {}, + timers = {}, + fontColor = '#333333', + fontScale = 1.4, // computed relative to grid size + rulerColor, + axisColor, + axesOn = true, + volumeOn = true, + viewControl, + trackPlane, + platform, + platformHover, + platformClick, + platformClickAt, + platformOnMove, + platformOnMoveTime = 500, + platformMoveTimer, + platformBelt = false, + volume, + camera, + renderer, + container, + raycaster, + freezeTo, + freeze, + isRound = false, + platformMaterial = new THREE.MeshPhongMaterial({ + color: 0xeeeeee, + specular: 0xeeeeee, + shininess: 0, + transparent: true, + opacity: 0.6, + side: THREE.DoubleSide + }), + hiddenKey, + vizChange, + docVisible = true, + antiAlias = window.devicePixelRatio <= 1, + lastAction = Date.now(), + renderTime = 0, + fps = 0; + + if (typeof DOC.hidden !== "undefined") { + hiddenKey = "hidden"; + vizChange = "visibilitychange"; + } else if (typeof DOC.msHidden !== "undefined") { + hiddenKey = "msHidden"; + vizChange = "msvisibilitychange"; + } else if (typeof DOC.webkitHidden !== "undefined") { + hiddenKey = "webkitHidden"; + vizChange = "webkitvisibilitychange"; + } + + DOC.addEventListener(vizChange, () => { + docVisible = DOC[hiddenKey] ? false : true; + }, false); + + function updateLastAction() { + lastAction = Date.now(); + } + + function delayed(key, time, fn) { + clearTimeout(timers[key]); + timers[key] = setTimeout(fn, time); + } + + function valueOr(val, def) { + return val !== undefined ? val : def; + } + + WORLD.contains = (obj) => { + return WORLD.children.contains(obj); + }; + + /** ****************************************************************** + * TWEENing Functions + ******************************************************************* */ + + function tweenit() { + TWEEN.update(); + setTimeout(tweenit, tweenDelay); + } + + tweenit(); + + function tweenCamPan(x,y,z,left,up) { + updateLastAction(); + let pos = viewControl.getPosition(); + pos.panX = x; + pos.panY = y; + pos.panZ = z; + if (left !== undefined) pos.left = left; + if (up !== undefined) pos.up = up; + tweenCam(pos); + } + + function tweenCam(pos) { + let tf = function () { + viewControl.setPosition(this); + updateLastAction(); + refresh(); + }; + let from = Object.clone(viewControl.getPosition()); + let to = Object.clone(pos); + let dist = Math.abs(from.left - to.left); + if (dist > Math.PI) { + if (from.left < to.left) { + from.left += Math.PI * 2; + } else { + from.left -= Math.PI * 2; + } + } + new TWEEN.Tween(from). + to(to, tweenTime). + onUpdate(tf). + onComplete(() => { + viewControl.setPosition(pos); + updateLastAction(); + refresh(); + let { then } = pos; + if (typeof then === 'function') { + then(); + } + }). + start(); + } + + /** ****************************************************************** + * Utility Functions + ******************************************************************* */ + + function width() { return WIN.innerWidth } + + function height() { return WIN.innerHeight } + + function aspect() { return width() / height() } + + function addEventListener(el, key, fn) { + el.addEventListener(key, fn); + } + + function addEventHandlers(el, pairs) { + for (let i=0; i { freeze = undefined }, 10); + } + + function setPlatformColor(color) { + let was = platform.material.color.getHex(); + platform.material.color.set(color); + requestRefresh(); + return was; + } + + function setFont(options) { + if (options.color) fontColor = options.color; + if (options.scale) fontScale = options.scale; + if (options.axisColor) axisColor = options.axisColor; + if (options.rulerColor) rulerColor = options.rulerColor; + updateRulers(); + } + + function showAxes(bool) { + axesOn = bool; + updateRulers(); + } + + function showVolume(bool) { + volumeOn = bool; + if (volume) volume.visible = bool; + requestRefresh(); + } + + function canvasInMesh(w, h, textAlign, textBaseline, color, size) { + let canvas = document.createElement('canvas'), + canvasTexture = new THREE.CanvasTexture(canvas), + plane = new THREE.PlaneGeometry(w, h), + context = canvas.getContext('2d'), + scale = 8; + + canvas.width = w * scale; + canvas.height = h * scale; + + canvas.addEventListener("webglcontextlost", event => { + console.log('WEB GL CONTEXT LOST'); + event.preventDefault(); + }, false); + + canvas.addEventListener("webglcontextrestored", event => { + console.log('WEB GL CONTEXT RESTORED'); + }, false); + + try { + context.scale(scale, scale); + } catch (e) { + console.log('unable to create label canvas', e ? e.name : 'unknown'); + return {}; + } + context.fillStyle = color || fontColor; + context.font = `${size}px sans-serif`; + context.textAlign = textAlign; + context.textBaseline = textBaseline; + canvasTexture.minFilter = THREE.LinearFilter; + + // set 'transparent' to false to debug mesh bounds + let material = new THREE.MeshBasicMaterial({transparent: true, map: canvasTexture}); + let mesh = new THREE.Mesh(plane, material); + + return { context, mesh }; + } + + + function setRulers( + xon = ruler.xon, + yon = ruler.yon, + factor = ruler.factor || 1, + xl = ruler.xlabel || 'X', + yl = ruler.ylabel || 'Y') + { + if (xon !== ruler.xon || yon !== ruler.yon || factor !== ruler.factor || xl !== ruler.xlabel || yl !== ruler.ylabel) { + ruler.factor = factor; + ruler.xon = xon; + ruler.yon = yon; + ruler.xlabel = xl; + ruler.ylabel = yl; + updateRulers(); + } + } + + function updateRulers() { + let { xon, yon, factor, xlabel, ylabel } = ruler; + let x = platform.scale.x, + y = isRound ? platform.scale.z : platform.scale.y, + z = isRound ? platform.scale.y : platform.scale.z, + w = x / 2, + h = y / 2, + d = z / 2, + zp = -d - platformZOff + numOrDef(gridZOff, (z/2-0.1)), + labelSize = grid.unitMinor * fontScale, + oldView = ruler.view, + view = ruler.view = new THREE.Group(); + + if (xon && axesOn) { + let xPadding = labelSize * 4, + canvas = canvasInMesh(x + xPadding, labelSize * 3, 'center', 'top', rulerColor, labelSize), + context = canvas.context, + mesh = canvas.mesh; + if (!(context && mesh)) return; + + if (platformBelt) { + for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { + context.fillText((i * factor).round(1).toString(), ruler.x2 - i + xPadding / 2, 0); + } + } else { + for (let i = 0; i >= ruler.x1; i -= grid.unitMajor) { + context.fillText((i * factor).round(1).toString(), ruler.xo + i + xPadding / 2, 0); + } + for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { + context.fillText((i * factor).round(1).toString(), ruler.xo + i + xPadding / 2, 0); + } + } + + context.font = (labelSize * 0.75) + 'px sans-serif'; + context.fillText(xlabel, (x + xPadding) / 2, labelSize * 1.5); + mesh.position.set(0, - h - labelSize * 2, zp); + view.add(mesh); + } + + if (yon && axesOn) { + let yPadding = labelSize, + canvas = canvasInMesh(labelSize * 4, y + yPadding, 'end', 'middle', rulerColor, labelSize), + context = canvas.context, + mesh = canvas.mesh; + if (!(context && mesh)) return; + + for (let i = 0; i >= ruler.y1; i -= grid.unitMajor) { + context.fillText((i * factor).round(1), labelSize * 4, y - (ruler.yo + i) + yPadding / 2); + } + for (let i = 0; i <= ruler.y2; i += grid.unitMajor) { + context.fillText((i * factor).round(1), labelSize * 4, y - (ruler.yo + i) + yPadding / 2); + } + + context.font = (labelSize * 0.75) + 'px sans-serif'; + context.fillText(ylabel, labelSize * 1.25, (y + yPadding) / 2); + mesh.position.set(-w - labelSize * 2 - 5, 0, zp); + view.add(mesh); + } + + Space.scene.remove(oldView); + Space.scene.add(view); + requestRefresh(); + } + + function setGrid( + unitMajor = grid.unitMajor, + unitMinor = grid.unitMinor, + colorMajor = grid.colorMajor, + colorMinor = grid.colorMinor) + { + if (!unitMajor) { + return; + } + if ( + unitMajor !== grid.unitMajor || unitMinor !== grid.unitMinor || + colorMajor !== grid.colorMajor || colorMinor !== grid.colorMinor + ) { + grid.unitMajor = unitMajor; + grid.unitMinor = unitMinor; + grid.colorMajor = colorMajor || grid.colorMajor; + grid.colorMinor = colorMinor || grid.colorMinor; + updateGrid(); + } + } + + function setGridColor(opt = {}) { + grid.colorMajor = valueOr(opt.major || opt.colorMajor, grid.colorMajor); + grid.colorMinor = valueOr(opt.minor || opt.colorMinor, grid.colorMinor); + grid.colorX = valueOr(opt.colorX, grid.colorX); + grid.colorY = valueOr(opt.colorY, grid.colorY); + updateGrid(); + } + + function numOrDef(v, dv) { + return v !== undefined ? v : dv; + } + + function modMatch(val, mod) { + let mv = Math.abs(val) % mod; + return (mv < 1) || ((mod - mv) < 1); + } + + function updateGrid() { + let { view, unitMinor, unitMajor, colorMajor, colorMinor, colorX, colorY } = grid; + let oldView = view; + let axes = grid.axes = new THREE.Group(); + let lines = grid.lines = new THREE.Group(); + + view = grid.view = new THREE.Group(); + view.visible = oldView ? oldView.visible : true; + view.add(lines); + view.add(axes); + + let majors = [], + minors = [], + x = platform.scale.x, + y = isRound ? platform.scale.z : platform.scale.y, + z = isRound ? platform.scale.y : platform.scale.z, + zp = -(z / 2) - platformZOff + numOrDef(gridZOff, (z/2-0.1)), + xh = x / 2, + yh = y / 2, + x1 = -xh - origin.x, + x2 = xh - origin.x, + y1 = -yh + origin.y, + y2 = yh + origin.y, + xo = x1 + xh, + yo = y1 + yh; + + ruler.x1 = x1; + ruler.x2 = x2; + ruler.y1 = y1; + ruler.y2 = y2; + ruler.xo = xh - xo; + ruler.yo = yh - yo; + + for (let x=-unitMinor; x>x1; x -= unitMinor) { + let oh = isRound ? Math.sqrt(1-(x/xh)*(x/xh)) * yh : yh; + let arr = modMatch(x, unitMajor) ? majors : minors; + arr.append({x:x-xo, y:-oh, z:zp}).append({x:x-xo, y:oh, z:zp}); + } + for (let x=unitMinor; xy1; y -= unitMinor) { + let ow = isRound ? Math.sqrt(1-(y/yh)*(y/yh)) * xh : xh; + let arr = modMatch(y, unitMajor) ? majors : minors; + arr.append({x:-ow, y:y-yo, z:zp}).append({x:ow, y:y-yo, z:zp}); + } + for (let y=unitMinor; y { + m.renderOrder = 3; + }); + Space.scene.add(group); + const scale = grid.origin.scale = () => { + const dist = camera.position.distanceTo(group.position); + const scale = Math.min(dist / 100, 0.5); + group.scale.set(scale, scale, scale); + }; + scale(); + updateDraws(); + } + + function setRound(bool) { + let current = platform; + isRound = bool; + if (bool) { + platform = new THREE.Mesh( + new THREE.CylinderGeometry(.5, .5, 1, 60), + platformMaterial + ); + platform.rotation.x = 0; + } else { + platform = new THREE.Mesh( + new THREE.BoxGeometry(1, 1, 1), + platformMaterial + ); + platform.rotation.x = -PI2; + } + + platform.position.y = current.position.y; + platform.visible = current.visible; + + SCENE.remove(current); + SCENE.add(platform); + THREE.dispose(current); + } + + function refresh() { + refreshRequested = false; + clearTimeout(refreshTimeout); + updateLastAction(); + viewControl.update(); + } + + /** deferred refresh that collapses multiple requests */ + function requestRefresh(timeout) { + if (refreshRequested === false) { + refreshRequested = true; + clearTimeout(refreshTimeout); + refreshTimeout = setTimeout(refresh, timeout || 10); + } + } + + function onResize() { + updateLastAction(); + camera.aspect = aspect(); + camera.updateProjectionMatrix(); + renderer.setSize(width(), height()); + container.style.width = width(); + container.style.height = height(); + requestRefresh(); + } + + function cca(c) { + return c.charCodeAt(0); + } + + function inputHasFocus() { + return DOC.activeElement && (DOC.activeElement != DOC.body); + } + + function keyHandler(evt) { + updateLastAction(); + if (!defaultKeys || inputHasFocus()) return false; + if (evt.metaKey) return false; + let handled = true; + switch (evt.charCode) { + case cca('z'): + Space.view.reset(); + break; + case cca('h'): + Space.view.home(); + break; + case cca('t'): + Space.view.top(); + break; + case cca('f'): + Space.view.front(); + cycleInd = 0; + break; + case cca('b'): + Space.view.back(); + cycleInd = 2; + break; + case cca('>'): + cycleInd = (++cycleInd % cycle.length); + cycle[cycleInd](); + break; + case cca('<'): + cycleInd--; + if (cycleInd < 0) { + cycleInd += cycle.length; + } + cycle[cycleInd](); + break; + default: + handled = false; + break; + } + if (handled) evt.preventDefault(); + return false; + } + + /** ****************************************************************** + * ThreeJS Helper Functions + ******************************************************************* */ + + function makeLinesFromPoints(points, color) { + if (points.length % 2 != 0) { + throw "invalid line : "+points.length; + } + const geo = new THREE.BufferGeometry(); + const vrt = new Float32Array(points.length * 3); + let vi = 0; + for (let p of points) { + vrt[vi++] = p.x; + vrt[vi++] = p.y; + vrt[vi++] = p.z; + } + geo.setAttribute('position', new THREE.BufferAttribute(vrt, 3)); + return new THREE.LineSegments(geo, new THREE.LineBasicMaterial({ color })); + } + + function intersect(objects, recurse) { + // console.log(({int: objects})); + let ints = raycaster.intersectObjects(objects, recurse); + // console.trace({ints}); + return ints; + } + + /** ****************************************************************** + * Mouse Functions + ******************************************************************* */ + + function onMouseDown(event) { + updateLastAction(); + if (event.target === renderer.domElement) { + DOC.activeElement.blur(); + event.preventDefault(); + let selection = null, + trackTo = alignedTracking ? trackPlane : platform, + isVis = trackTo.visible; + if (mouseDownSelect) selection = mouseDownSelect(undefined, event); + if (selection && selection.length > 0) { + // selection = selection.map(o => o.isGroup ? o.children : o).flat(); + // console.log({ selection }); + trackTo.visible = true; + let int = intersect(selection.slice().append(trackTo), false); + trackTo.visible = isVis; + if (int.length > 0) { + let trackInt, selectInt; + for (let i=0; i 0 ? int[0].point : null; + } + } else { + viewControl.enabled = false; + } + mouseStart = { + x: (event.clientX / width()) * 2 - 1, + y: -(event.clientY / height()) * 2 + 1}; + } + + function onMouseUp(event) { + updateLastAction(); + if (!viewControl.enabled) { + viewControl.enabled = true; + viewControl.onMouseUp(event); + } + let mouseEnd = { + x: (event.clientX / width()) * 2 - 1, + y: -(event.clientY / height()) * 2 + 1}; + // only fire on mouse move between mouseStart (down) and up + if (mouseStart && mouseEnd.x - mouseStart.x + mouseEnd.y - mouseStart.y === 0) { + event.preventDefault(); + let refresh = false, + selection = null; + if (mouseUpSelect) { + selection = mouseUpSelect(); + } + if (selection && selection.length > 0) { + let int = intersect(selection, selectRecurse); + if (mouseUp) { + if (int.length) { + mouseUp(event, int); + } else { + mouseUp(event, intersect([platform], selectRecurse)); + } + if (event.button === 2) { + return; + } + } + if (int.length > 0) { + mouseUpSelect(int[0], event, int); + refresh = true; + } else { + mouseUpSelect(null, event); + } + } + if (!refresh && platformClickAt) { + platformClick(platformClickAt); + } + if (refresh) { + requestRefresh(); + } + mouseStart = null; + } else if (mouseDrag && mouseDragStart) { + // fired on mouse drag end + mouseDrag(null,null,true); + } + mouseDragPoint = null; + mouseDragStart = null; + } + + function onMouseMove(event) { + updateLastAction(); + let int, vis; + + const mv = new THREE.Vector2(); + mv.x = ( event.clientX / window.innerWidth ) * 2 - 1; + mv.y = - ( event.clientY / window.innerHeight ) * 2 + 1; + raycaster.setFromCamera( mv, camera ); + + if (viewControl.enabled) { + event.preventDefault(); + let selection = mouseHover ? mouseHover() : null; + if (selection && selection.length > 0) { + int = intersect(selection, selectRecurse); + if (int.length > 0) mouseHover(int[0], event, int); + } + if ((!int || int.length == 0) && platformHover) { + vis = platform.visible; + platform.visible = true; + int = intersect([platform], false); + platform.visible = vis; + if (int && int.length > 0) platformHover(int[0].point); + } + } else if (mouseDragPoint && mouseDrag && mouseDrag()) { + event.preventDefault(); + let trackTo = alignedTracking ? trackPlane : platform; + let vis = trackTo.visible; + trackTo.visible = true; + int = intersect([trackTo], false); + trackTo.visible = vis; + if (int.length > 0 && int[0].object === trackTo) { + let delta = mouseDragPoint.clone().sub(int[0].point); + let offset = mouseDragStart.clone().sub(int[0].point); + mouseDragPoint = int[0].point; + mouseDrag({ + x: -delta.x, + y: delta.z, + z: 0, + event + }, { + x: -offset.x, + y: offset.z, + z: 0 + }); + requestRefresh(); + } + } + mouse = { + x: (event.clientX / width()) * 2 - 1, + y: -(event.clientY / height()) * 2 + 1}; + } + + /** ****************************************************************** + * Space Object + ******************************************************************* */ + + function updateFocus() { + if (focalPoint) { + Space.scene.remove(focalPoint); + } + if (showFocus) { + let mesh = focalPoint = new THREE.Mesh( + new THREE.SphereGeometry(1, 16, 16), + new THREE.MeshPhongMaterial({ + side: THREE.DoubleSide, + specular: 0x202020, + color: 0xff0000, + shininess: 125 + }) + ); + Space.scene.add(mesh); + mesh.position.copy(viewControl.target); + } + } + + function setSky(opt = {}) { + let { grid, color, gridColor } = opt; + if (grid) Space.sky.showGrid(grid); + if (color !== undefined) Space.sky.setColor(color); + if (gridColor !== undefined) Space.sky.setGridColor(gridColor); + if (skyAmbient && opt.ambient) { + let { color, intensity } = opt.ambient; + if (color) skyAmbient.color.set(color); + if (intensity) skyAmbient.intensity = intensity; + } + } + + function setPlatform(opt = {}) { + let platform = Space.platform; + let { color, round, size, grid, opacity } = opt; + let { visible, volume, zOffset, origin, light } = opt; + if (light) { + lightInfo.intensity = light; + } + if (color) { + platform.setColor(color); + } + if (round !== undefined) { + platform.setRound(round); + } + if (size) { + let { width = 300, depth = 300, height = 2.5, maxz = 300 } = size; + platform.setSize(width, depth, height, maxz); + } + if (grid) { + let { zOffset } = grid; + let { major = 25, minor = 5 } = grid; + let { colorX, colorY, colorMajor, colorMinor } = grid; + platform.setGrid(major, minor); + platform.setGridColor({ colorX, colorY, colorMajor, colorMinor }); + if (zOffset !== undefined) platform.setGridZOff(zOffset); + } + if (origin) { + let { x, y, z, show } = origin; + platform.setOrigin(x || 0, y || 0, z || 0, show); + } + if (opacity !== undefined) { + platform.opacity(opacity); + } + if (volume !== undefined) { + platform.showVolume(volume); + } + if (zOffset !== undefined) { + platform.setZOff(zOffset); + } + if (visible !== undefined) { + platform.setVisible(visible); + } + } + + let Space = exports({ + refresh: refresh, + update: requestRefresh, + + setAntiAlias(b) { antiAlias = b ? true : false }, + raycast: intersect, + + event: { + addHandlers: addEventHandlers, + onEnterKey: onEnterKey, + onResize: onResize + }, + + sky: { + set: setSky, + + showGrid: (b) => { + showSkyGrid = b; + }, + + setColor: (c) => { + SCENE.background = new THREE.Color(c); + }, + + setGridColor: (c) => { + skyGridColor = c; + if (skyGridMaterial) skyGridMaterial.color = new THREE.Color(c); + } + }, + + scene: { + add: function (o) { + o.rotation.x = WORLD.rotation.x; + return SCENE.add(o); + }, + + remove: function (o) { + THREE.dispose(o); + return SCENE.remove(o); + }, + + active: updateLastAction, + + setFog: function(mult, color) { + if (mult) { + SCENE.fog = new THREE.Fog(color, 100, 1000); + SCENE.fog.mult = mult > 0 ? mult : 3; + } else { + SCENE.fog = undefined; + } + Space.scene.updateFog(); + }, + + updateFog: function() { + const { fog } = SCENE; + if (fog) { + const dist = camera.position.distanceTo(viewControl.target); + fog.near = dist; + fog.far = dist * fog.mult; + } + // todo: option to clip for close views + // camera.near = dist / 2; + // camera.updateProjectionMatrix(); + }, + + lightInfo: () => { + setTimeout(updateLights, 0); + return lightInfo; + } + }, + + world: { + add: function(o) { + return WORLD.add(o); + }, + + remove: function(o) { + THREE.dispose(o); + return WORLD.remove(o); + }, + + newGroup: function() { + return WORLD.newGroup(); + } + }, + + platform: { + set: setPlatform, + update: updateDraws, + setSize: setPlatformSizeUpdateGrid, + setColor: setPlatformColor, + setOrigin, + setRulers, + setGrid, + setGridColor, + setFont, + setRound, + showAxes, + showVolume, + showGridBelow: (b) => { hideGridBelow = !b }, + showGrid: (b) => { showGrid = grid.view.visible = b }, + showGrid2: (b) => { showGridLines = grid.lines.visible = b }, + setMaxZ: (z) => { panY = z / 2 }, + setCenter: (x,y,z) => { panX = x; panY = z, panZ = y }, + setHidden: (b) => { showPlatform = !b; platform.visible = !b }, + setVisible: (b) => { showPlatform = b; platform.visible = b }, + setHiding: (b) => { hidePlatformBelow = b }, + setZOff: (z) => { platformZOff = z; updatePlatformPosition() }, + setGridZOff:(z) => { gridZOff = z; updatePlatformPosition() }, + setBelt: (b) => { platformBelt = b }, + isHidden: () => { return !showPlatform }, + isVisible: () => { return platform.visible }, + isGridVisible() { return grid.view.visible }, + opacity: (o) => { platform.material.opacity = o; Space.platform.setVisible(o > 0) }, + onMove: (f,t) => { platformOnMove = f, platformOnMoveTime = t || platformOnMoveTime }, + onHover: (f) => { platformHover = f }, + onClick: (f) => { platformClick = f}, + size: () => { return platform.scale }, + get world() { throw "platform.world deprecated" } + }, + + preset: { + top: {left: home, up: 0, panX, panY, panZ}, + back: {left: PI, up: PI2, panX, panY, panZ}, + home: {left: home, up, panX, panY, panZ}, + front: {left: 0, up: PI2, panX, panY, panZ}, + right: {left: PI2, up: PI2, panX, panY, panZ}, + left: {left: -PI2, up: PI2, panX, panY, panZ}, + }, + + view: { + top: (then) => { tweenCam({left: home, up: 0, panX, panY, panZ, then}) }, + back: (then) => { tweenCam({left: PI, up: PI2, panX, panY, panZ, then}) }, + home: (then) => { tweenCam({left: home, up, panX, panY, panZ, then}) }, + front: (then) => { tweenCam({left: 0, up: PI2, panX, panY, panZ, then}) }, + right: (then) => { tweenCam({left: PI2, up: PI2, panX, panY, panZ, then}) }, + left: (then) => { tweenCam({left: -PI2, up: PI2, panX, panY, panZ, then}) }, + reset: () => { viewControl.reset(); requestRefresh() }, + load: (cam) => { viewControl.setPosition(cam); requestRefresh() }, + save: () => { return viewControl.getPosition(true) }, + panTo: (x,y,z,l,u) => { tweenCamPan(x,y,z,l,u) }, + setZoom: (r,v) => { viewControl.setZoom(r,v) }, + setCtrl: (name) => { + if (name === 'onshape') { + viewControl.setMouse(viewControl.mouseOnshape); + } else { + viewControl.setMouse(viewControl.mouseDefault); + } + }, + getFPS () { return fps }, + getRMS() { return renderTime }, + getFocus() { return viewControl.getTarget() }, + setFocus(v) { + viewControl.setTarget(v); + updateFocus(); + refresh() + }, + showFocus(ms) { + showFocus = ms; + updateFocus(); + }, + setHome(r,u) { + home = r || 0; + up = u || PI4; + }, + spin(then, count) { + Space.view.front(() => { + Space.view.right(() => { + Space.view.back(() => { + Space.view.left(() => { + if (--count > 0) { + Space.view.spin(then, count); + } else { + Space.view.front(then); + } + }); + }); + }); + }); + }, + get ctrl() { + return viewControl; + } + }, + + mouse: { + up: (f) => { mouseUp = f }, + down: (f) => { mouseDown = f }, + downSelect: (f) => { mouseDownSelect = f }, + upSelect: (f) => { mouseUpSelect = f }, + onDrag: (f) => { mouseDrag = f }, + onHover: (f) => { mouseHover = f } + }, + + tween: { + setTime: (t) => { tweenTime = t || 500 }, + setDelay: (d) => { tweenDelay = d || 20 } + }, + + useDefaultKeys (b) { defaultKeys = b }, + selectRecurse (b) { selectRecurse = b }, + renderInfo () { return renderer.info }, + objects () { return WORLD.children }, + + screenshot(format, options) { + return renderer.domElement.toDataURL(format || "image/png", options); + }, + + screenshot2(param = {}) { + let oco = renderer.domElement; + let oWidth = oco.offsetWidth; + let oHeight = oco.offsetHeight; + let oRatio = oHeight / oWidth; + let width = param.width || 240; + let ncv = document.createElement('canvas'); + ncv.width = width; + ncv.height = width * oRatio; + let nco = ncv.getContext('2d'); + nco.drawImage(oco, 0, 0, ncv.width, ncv.height); + return { + url: ncv.toDataURL(param.format || "image/png", param.options), + width: ncv.width, + height: ncv.height, + }; + }, + + screenshot3(param = {}) { + let oco = renderer.domElement; + let oWidth = oco.offsetWidth; + let oHeight = oco.offsetHeight; + let oRatio = oWidth / oHeight; + let width = param.width || 512; + let height = param.height || width; + let nRatio = width / height; + let ncv = document.createElement('canvas'); + ncv.width = width; + ncv.height = height; + let nco = ncv.getContext('2d'); + let ox = 0, oy = 0; + if (oRatio > nRatio) { + let tmp = oWidth; + oWidth = oHeight * nRatio; + ox = (tmp - oWidth) / 2; + } else { + let tmp = oHeight; + oHeight = oWidth * nRatio; + oy = (tmp - oHeight) / 2; + } + nco.drawImage(oco, ox, oy, oWidth, oHeight, 0, 0, width, height); + if (param.out) { + ncv.toBlob(blob => blob.arrayBuffer().then(png => param.out({ png, width, height }))); + } + return ncv; + }, + + internals: () => { + return { renderer, camera, platform }; + }, + + init: (domelement, slider, ortho) => { + container = domelement; + + WORLD.rotation.x = -PI2; + SCENE.add(WORLD); + + domelement.style.width = width(); + domelement.style.height = height(); + + renderer = new WebGLRenderer({ + antialias: antiAlias, + preserveDrawingBuffer: true, + logarithmicDepthBuffer: true + }); + + // THREE.ColorManagement.enabled = false; + // renderer.outputColorSpace = THREE.LinearSRGBColorSpace; + + renderer.localClippingEnabled = true; + camera = ortho ? + new THREE.OrthographicCamera(-100 * aspect(), 100 * aspect(), 100, -100, 0.1, 100000) : + new THREE.PerspectiveCamera(perspective, aspect(), 0.1, 100000); + + camera.position.set(0, 200, 340); + renderer.setSize(width(), height()); + domelement.appendChild(renderer.domElement); + + raycaster = new THREE.Raycaster(); + + viewControl = new moto.Orbit(camera, domelement, (position, moved) => { + if (platform) { + platform.visible = hidePlatformBelow ? + initialized && position.y >= 0 && showPlatform : showPlatform; + volume.visible = showVolume && platform.visible; + } + if (grid.view) { + grid.view.visible = hideGridBelow ? platform.visible : showGrid; + } + if (cameraLight) { + cameraLight.position.copy(camera.position); + } + if (moved && platformOnMove) { + clearTimeout(platformMoveTimer); + platformMoveTimer = setTimeout(platformOnMove, 500); + Space.scene.updateFog(); + } + updateLastAction(); + updateFocus(); + }, (val) => { + if (camera && grid?.origin?.scale) { + grid.origin.scale(); + } + if (camera && viewControl) { + // increase intersect line precision on zoom + const dist = camera.position.distanceTo(viewControl.target); + raycaster.params.Line.threshold = Math.min(1, dist / 100); + } + moto.broker.publish("space.view.zoom", camera); + updateLastAction(); + if (slider) slider(val); + }); + + viewControl.noKeys = true; + viewControl.maxDistance = 1000; + + SCENE.add(skyAmbient = new THREE.AmbientLight(0x707070)); + + platform = new THREE.Mesh( + new THREE.BoxGeometry(1, 1, 1), + platformMaterial + ); + + platform.position.y = platformZOff; + platform.rotation.x = -PI2; + platform.visible = showPlatform; + + trackPlane = new THREE.Mesh( + new THREE.PlaneGeometry(100000, 100000, 1, 1), + new THREE.MeshBasicMaterial( { color: 0x777777, opacity: 0, transparent: true } ) + ); + trackPlane.visible = false; + trackPlane.rotation.x = PI2; + + let skygrid = new THREE.Mesh( + new THREE.BoxGeometry(10000, 10000, 10000, 10, 10, 10), + skyGridMaterial = + new THREE.MeshBasicMaterial({ color: skyGridColor, side: THREE.DoubleSide }) + ); + + SCENE.add(platform); + SCENE.add(trackPlane); + + if (showSkyGrid) { + skygrid.material.wireframe = true; + SCENE.add(skygrid); + } + + addEventHandlers(WIN, [ + 'resize', onResize, + 'mousemove', onMouseMove, + 'mousedown', onMouseDown, + 'mouseup', onMouseUp, + 'keypress', keyHandler, + 'touchstart', updateLastAction, + 'touchmove', updateLastAction, + 'touchend', updateLastAction + ]); + + let animates = 0; + let rateStart = Date.now(); + let renderStart; + let renders = []; + + function animate() { + animates++; + const now = Date.now(); + if (now - rateStart > 1000) { + // compute stats roughly every second + const delta = now - rateStart; + fps = 1000 * animates / delta; + animates = 0; + rateStart = now; + // look for slowest rendered frame + renderTime = Math.max(0, ...renders); + renders.length = 0; + } + requestAnimationFrame(animate); + if (docVisible && !freeze && Date.now() - lastAction < 1500) { + renderStart = Date.now(); + renderer.render(SCENE, camera); + // track frame render times + renders.push(Date.now() - renderStart); + } else { + fps = 0; + } + } + + animate(); + + const ctx = renderer.getContext(); + + Space.info = { + ver: ctx.getParameter(ctx.VERSION), + ven: ctx.getParameter(ctx.VENDOR), + glr: ctx.getParameter(ctx.RENDERER), + pla: nav.platform, + mob: nav.maxTouchPoints > 1 || (/android/i.test(nav.userAgent)) + }; + + initialized = true; + } + }); + + let cycle = [ + Space.view.front, + Space.view.right, + Space.view.back, + Space.view.left, + ]; + + let cycleInd = 0; + +}); diff --git a/src2/moto/webui.js b/src.old/moto/webui.js similarity index 77% rename from src2/moto/webui.js rename to src.old/moto/webui.js index 6bb0704c..bc77f910 100644 --- a/src2/moto/webui.js +++ b/src.old/moto/webui.js @@ -1,5 +1,10 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +"use strict"; + +// dep: moto.license +// dep: add.array +gapp.register("moto.webui", [], (root, exports) => { let nextid = 1; @@ -42,11 +47,8 @@ function build(data, context) { func[key] = val; elid = elid || `_${nextid++}`; } else if (tov === 'object') { - if (Array.isArray(val)) { - html.push(` ${key}="${val.filter(v => v !== undefined).join(' ')}"`); - } else { - html.push(` ${key}="${val}"`); - } + if (val.length) + html.push(` ${key}="${val.filter(v => v !== undefined).join(' ')}"`); } else if (tov !== 'undefined') { html.push(` ${key}="${val}"`); } @@ -75,7 +77,7 @@ function build(data, context) { } // core html builder funtions -const h = { +let h = exports({ bind: (el, data, opt = {}) => { let ctx = []; let html = build(data, ctx).join(''); @@ -116,39 +118,43 @@ const h = { build: (data) => { return build(data, []).join(''); } -}; +}); // window ui helpers -const $ = (id) => { - return document.getElementById(id); -}; +gapp.overlay(root, { + $: (id) => { + return document.getElementById(id); + }, -const $d = (id, v) => { - $(id).style.display = v; -}; + $d: (id, v) => { + $(id).style.display = v; + }, -const $h = (id, h) => { - $(id).innerHTML = h; -}; + $h: (id, h) => { + $(id).innerHTML = h; + }, -const $c = (id, add, del) => { - if (add) $(id).classList.add(add); - if (del) $(id).classList.remove(del); -}; + $c: (id, add, del) => { + if (add) $(id).classList.add(add); + if (del) $(id).classList.remove(del); + }, -const $C = (clazz, add, del) => { - let el = [...document.getElementsByClassName(clazz)]; - for (let e of el) { - if (add) e.classList.add(add); - if (del) e.classList.remove(del); + $C: (clazz, add, del) => { + let el = [...document.getElementsByClassName(clazz)]; + for (let e of el) { + if (add) e.classList.add(add); + if (del) e.classList.remove(del); + } + return el; + }, + + h, + + estop: (evt) => { + evt.stopPropagation(); + evt.preventDefault(); } - return el; -}; - -const estop = (evt) => { - evt.stopPropagation(); - evt.preventDefault(); -}; +}); // add common element types [ @@ -160,12 +166,4 @@ const estop = (evt) => { } }); -export { - $, - $d, - $h, - $c, - $C, - h, - estop -}; +}); diff --git a/src2/moto/worker.js b/src.old/moto/worker.js similarity index 96% rename from src2/moto/worker.js rename to src.old/moto/worker.js index b421f2e7..245815e5 100644 --- a/src2/moto/worker.js +++ b/src.old/moto/worker.js @@ -1,9 +1,14 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +"use strict"; + +gapp.register("moto.worker", [], (root, exports) => { + let endpoints = {}; // code is running in the worker / server context -const dispatch = { +const dispatch = exports({ + // bind an endpoint name to a function bind(name, fn) { endpoints[name] = fn; @@ -90,9 +95,10 @@ const dispatch = { send.error(`no registered endpoint: ${msg.task}`); } } -}; + +}); // attach onmessage hanler self.onmessage = dispatch.onmessage; -export const worker = dispatch; +}); diff --git a/src2/wasm/clipper.cpp b/src.old/wasm/clipper.cpp similarity index 100% rename from src2/wasm/clipper.cpp rename to src.old/wasm/clipper.cpp diff --git a/src2/wasm/clipper.hpp b/src.old/wasm/clipper.hpp similarity index 100% rename from src2/wasm/clipper.hpp rename to src.old/wasm/clipper.hpp diff --git a/src2/wasm/kiri-ani.c b/src.old/wasm/kiri-ani.c similarity index 100% rename from src2/wasm/kiri-ani.c rename to src.old/wasm/kiri-ani.c diff --git a/src2/wasm/kiri-ani.wasm b/src.old/wasm/kiri-ani.wasm similarity index 100% rename from src2/wasm/kiri-ani.wasm rename to src.old/wasm/kiri-ani.wasm diff --git a/src2/wasm/kiri-geo.cpp b/src.old/wasm/kiri-geo.cpp similarity index 100% rename from src2/wasm/kiri-geo.cpp rename to src.old/wasm/kiri-geo.cpp diff --git a/src2/wasm/kiri-geo.wasm b/src.old/wasm/kiri-geo.wasm similarity index 100% rename from src2/wasm/kiri-geo.wasm rename to src.old/wasm/kiri-geo.wasm diff --git a/src2/wasm/kiri-sla.c b/src.old/wasm/kiri-sla.c similarity index 100% rename from src2/wasm/kiri-sla.c rename to src.old/wasm/kiri-sla.c diff --git a/src2/wasm/kiri-sla.wasm b/src.old/wasm/kiri-sla.wasm similarity index 100% rename from src2/wasm/kiri-sla.wasm rename to src.old/wasm/kiri-sla.wasm diff --git a/src2/wasm/makefile b/src.old/wasm/makefile similarity index 100% rename from src2/wasm/makefile rename to src.old/wasm/makefile diff --git a/src2/wasm/manifold.wasm b/src.old/wasm/manifold.wasm similarity index 100% rename from src2/wasm/manifold.wasm rename to src.old/wasm/manifold.wasm diff --git a/src/add/array.js b/src/add/array.js index 93c9b8d3..cd4d6c42 100644 --- a/src/add/array.js +++ b/src/add/array.js @@ -1,11 +1,7 @@ /** 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() { @@ -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; }; - -}); diff --git a/src/add/class.js b/src/add/class.js index 2dd7d885..7d3b0ad6 100644 --- a/src/add/class.js +++ b/src/add/class.js @@ -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 }; diff --git a/src/add/three.js b/src/add/three.js index 9f48f60a..889c119c 100644 --- a/src/add/three.js +++ b/src/add/three.js @@ -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 }; diff --git a/src/data/index.js b/src/data/index.js index e102306b..e28217be 100644 --- a/src/data/index.js +++ b/src/data/index.js @@ -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; diff --git a/src/data/local.js b/src/data/local.js index e2c849dd..1c6b05f2 100644 --- a/src/data/local.js +++ b/src/data/local.js @@ -1,47 +1,44 @@ /** Copyright Stewart Allen -- 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; diff --git a/src2/ext/clip2.esm.js b/src/ext/clip2.esm.js similarity index 100% rename from src2/ext/clip2.esm.js rename to src/ext/clip2.esm.js diff --git a/src/ext/md5.js b/src/ext/md5.js index 88b36d68..e01a6b8e 100644 --- a/src/ext/md5.js +++ b/src/ext/md5.js @@ -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() -}; - -}); \ No newline at end of file +} \ No newline at end of file diff --git a/src2/ext/pngjs.esm.js b/src/ext/pngjs.esm.js similarity index 100% rename from src2/ext/pngjs.esm.js rename to src/ext/pngjs.esm.js diff --git a/src/geo/base.js b/src/geo/base.js index e851e803..d7ea18bf 100644 --- a/src/geo/base.js +++ b/src/geo/base.js @@ -1,12 +1,8 @@ /** Copyright Stewart Allen -- 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 }; diff --git a/src/geo/bounds.js b/src/geo/bounds.js index 3af6ef10..05950d3e 100644 --- a/src/geo/bounds.js +++ b/src/geo/bounds.js @@ -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(); +} diff --git a/src/geo/csg.js b/src/geo/csg.js index e93fa19d..0aec7c09 100644 --- a/src/geo/csg.js +++ b/src/geo/csg.js @@ -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 -}); - }); diff --git a/src/geo/gyroid.js b/src/geo/gyroid.js index b56ca25c..e333a37c 100644 --- a/src/geo/gyroid.js +++ b/src/geo/gyroid.js @@ -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,21 +194,16 @@ 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 - if (dir === 'lr') dx = dx / 2; - if (dir === 'td') dy = dy / 2; + if (dir === 'lr') { + return Math.abs(dx); + } else if (dir === 'td') { + return Math.abs(dy); + } return Math.sqrt(dx * dx + dy * dy); } - -gapp.overlay(base, { - gyroid: { slice } -}) - -}); diff --git a/src/geo/line.js b/src/geo/line.js index f391c7bd..63221580 100644 --- a/src/geo/line.js +++ b/src/geo/line.js @@ -1,13 +1,6 @@ /** Copyright Stewart Allen -- 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 -}); - -}); diff --git a/src/geo/paths.js b/src/geo/paths.js index 134a63f3..90cba421 100644 --- a/src/geo/paths.js +++ b/src/geo/paths.js @@ -1,19 +1,7 @@ /** Copyright Stewart Allen -- 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; i0; 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} 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 (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); @@ -572,7 +556,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 @@ -585,10 +569,10 @@ function arcToPath( start, end,arcdivs=24,opts) { let a2 = Math.atan2(center.y - end.y, center.x - end.x) + Math.PI; let ad = base.util.thetaDiff(a1, a2, clockwise); // angle difference in radians let samePoint = Math.abs(ad) < 0.001 - let ofFull = Math.abs(ad)/(2*Math.PI); - let steps = samePoint? arcdivs : Math.max(Math.floor( arcdivs * ofFull),4); - let step = (samePoint? (Math.PI*2) : ad) / steps; - let numPoints = steps/ step; + let ofFull = Math.abs(ad) / (2 * Math.PI); + let steps = samePoint ? arcdivs : Math.max(Math.floor(arcdivs * ofFull), 4); + let step = (samePoint ? (Math.PI * 2) : ad) / steps; + let numPoints = steps / step; let zStart = start.z; let zStep = dz / numPoints; let rot = a1 + step; @@ -607,9 +591,9 @@ function arcToPath( start, end,arcdivs=24,opts) { // } 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, @@ -623,7 +607,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); @@ -642,7 +626,7 @@ class FloatPacker { this.array = nuarray; this.size = nusize; } - for (let i=0; i -- 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 -}); - -}); +}; diff --git a/src/geo/points.js b/src/geo/points.js index d1118361..183306bd 100644 --- a/src/geo/points.js +++ b/src/geo/points.js @@ -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; } - -}); diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 4f9d0501..6e665522 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -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 -}); - -}); diff --git a/src/geo/polygons.js b/src/geo/polygons.js index 86d866a6..07cea123 100644 --- a/src/geo/polygons.js +++ b/src/geo/polygons.js @@ -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 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 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; diff --git a/src/geo/slicer.js b/src/geo/slicer.js index dddf3c04..e9713328 100644 --- a/src/geo/slicer.js +++ b/src/geo/slicer.js @@ -1,23 +1,16 @@ /** Copyright Stewart Allen -- 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 -}); +} -}); diff --git a/src/geo/slope.js b/src/geo/slope.js index 9fc6e67d..1777832d 100644 --- a/src/geo/slope.js +++ b/src/geo/slope.js @@ -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 -}); - -}); diff --git a/src/geo/wasm.js b/src/geo/wasm.js index 0e08be8d..87ec5cdd 100644 --- a/src/geo/wasm.js +++ b/src/geo/wasm.js @@ -1,16 +1,12 @@ /** Copyright Stewart Allen -- 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}); } } - -}); diff --git a/src2/kiri-dev/fdm/My.Bambu.P1S b/src/kiri-dev/fdm/My.Bambu.P1S similarity index 100% rename from src2/kiri-dev/fdm/My.Bambu.P1S rename to src/kiri-dev/fdm/My.Bambu.P1S diff --git a/src2/kiri-mode/cam/anim-2d-be.js b/src/kiri-mode/cam/anim-2d-be.js similarity index 100% rename from src2/kiri-mode/cam/anim-2d-be.js rename to src/kiri-mode/cam/anim-2d-be.js diff --git a/src2/kiri-mode/cam/anim-2d-fe.js b/src/kiri-mode/cam/anim-2d-fe.js similarity index 100% rename from src2/kiri-mode/cam/anim-2d-fe.js rename to src/kiri-mode/cam/anim-2d-fe.js diff --git a/src2/kiri-mode/cam/anim-3d-be.js b/src/kiri-mode/cam/anim-3d-be.js similarity index 100% rename from src2/kiri-mode/cam/anim-3d-be.js rename to src/kiri-mode/cam/anim-3d-be.js diff --git a/src2/kiri-mode/cam/anim-3d-fe.js b/src/kiri-mode/cam/anim-3d-fe.js similarity index 100% rename from src2/kiri-mode/cam/anim-3d-fe.js rename to src/kiri-mode/cam/anim-3d-fe.js diff --git a/src2/kiri-mode/cam/cl-flip.js b/src/kiri-mode/cam/cl-flip.js similarity index 100% rename from src2/kiri-mode/cam/cl-flip.js rename to src/kiri-mode/cam/cl-flip.js diff --git a/src2/kiri-mode/cam/cl-hole.js b/src/kiri-mode/cam/cl-hole.js similarity index 100% rename from src2/kiri-mode/cam/cl-hole.js rename to src/kiri-mode/cam/cl-hole.js diff --git a/src2/kiri-mode/cam/cl-ops.js b/src/kiri-mode/cam/cl-ops.js similarity index 100% rename from src2/kiri-mode/cam/cl-ops.js rename to src/kiri-mode/cam/cl-ops.js diff --git a/src2/kiri-mode/cam/cl-stock.js b/src/kiri-mode/cam/cl-stock.js similarity index 100% rename from src2/kiri-mode/cam/cl-stock.js rename to src/kiri-mode/cam/cl-stock.js diff --git a/src2/kiri-mode/cam/cl-surface.js b/src/kiri-mode/cam/cl-surface.js similarity index 100% rename from src2/kiri-mode/cam/cl-surface.js rename to src/kiri-mode/cam/cl-surface.js diff --git a/src2/kiri-mode/cam/cl-tab.js b/src/kiri-mode/cam/cl-tab.js similarity index 100% rename from src2/kiri-mode/cam/cl-tab.js rename to src/kiri-mode/cam/cl-tab.js diff --git a/src2/kiri-mode/cam/cl-trace.js b/src/kiri-mode/cam/cl-trace.js similarity index 100% rename from src2/kiri-mode/cam/cl-trace.js rename to src/kiri-mode/cam/cl-trace.js diff --git a/src/kiri-mode/cam/client.js b/src/kiri-mode/cam/client.js index 8edaee87..efa1e8f1 100644 --- a/src/kiri-mode/cam/client.js +++ b/src/kiri-mode/cam/client.js @@ -1,218 +1,607 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { $, h } from '../../moto/webui.js'; +import { api } from '../../kiri/api.js'; +import { load } from '../../load/file.js'; +import { animate as anim_2d, animate_clear as anim_2d_clear } from './anim-2d-fe.js'; +import { animate2 as anim_3d, animate_clear2 as anim_3d_clear } from './anim-3d-fe.js'; +import { space as SPACE } from '../../moto/space.js'; +import { Layers } from '../../kiri/layers.js'; +import { Stack } from '../../kiri/stack.js'; +import { updateStock } from './cl-stock.js'; +import { createPopOps } from './cl-ops.js'; +import { tabAdd, tabDone, tabClear, restoreTabs, updateTabs } from './cl-tab.js'; +import { traceOn, traceDone, unselectTraces } from './cl-trace.js'; +import { holeSelOn, selectHolesDone } from './cl-hole.js'; +import { surfaceOn, surfaceDone } from './cl-surface.js'; -// dep: kiri-mode.cam.driver -// dep: kiri.api -// dep: kiri.alerts -// dep: geo.polygon -// use: kiri-mode.cam.animate -// use: kiri-mode.cam.animate2 -// use: kiri-mode.cam.tools -// use: load.gbr -gapp.register("kiri-mode.cam.client", [], (root, exports) => { - -const { base, kiri } = root; -const { Polygon } = base -const { driver, api } = kiri; -const {alerts, conf} = api; -const { CAM } = driver; const DEG2RAD = Math.PI / 180; const RAD2DEG = 180 / Math.PI; const hasSharedArrays = self.SharedArrayBuffer ? true : false; -let isAnimate, - isArrange, - isPreview, - isCamMode, - isIndexed, - isParsed, - camStock, - camZTop, - camZBottom, - current, - currentIndex, - flipping, - poppedRec, - hoveredOp, - lastMode, - API, FDM, SPACE, STACKS, MODES, VIEWS, UI, UC, LANG, MCAM, WIDGETS; +const { VIEWS, STACKS } = api.const; +const { noop, space } = api; +const { ui: UI, uc: UC } = api; +const { widgets: WIDGETS } = api; -let zaxis = { x: 0, y: 0, z: 1 }, - popOp = {}, - animVer = 0, - seed = Date.now(), - func = { - hover: noop, - hoverUp: noop - }; +class Client { + animVer = 0; + camStock; + current; + currentIndex; + flipping; + func = {}; + hoveredOp; + hover = noop; + hoverUp = noop; + isAnimate; + isArrange; + isPreview; + isCamMode; + isIndexed; + isParsed; + lastMode; + popOp = {}; + poppedRec; + zaxis = { x: 0, y: 0, z: 1 }; +} + +export const env = new Client(); + +export function isDark() { + return api.space.is_dark() +} + +export function clearPops() { + if (env.func.unpop) env.func.unpop(); + tabDone(); + traceDone(); + surfaceDone(); + selectHolesDone(); +} function animFn() { return [{ - animate: CAM.animate, - animate_clear: CAM.animate_clear - },{ - animate: CAM.animate2, - animate_clear: CAM.animate_clear2 - }][animVer]; + animate: anim_2d, + animate_clear: anim_2d_clear + }, { + animate: anim_3d, + animate_clear: anim_3d_clear + }][env.animVer]; } -CAM.restoreTabs = restoreTabs; - -CAM.init = function(kiri, api) { - FDM = kiri.driver.FDM; - - // console.log({kiri,api}) - WIDGETS = api.widgets; - SPACE = kiri.space; - MODES = kiri.consts.MODES; - VIEWS = kiri.consts.VIEWS; - STACKS = api.const.STACKS; - LANG = api.const.LANG; - MCAM = [ MODES.CAM ]; - UI = api.ui; - UC = api.uc; - API = api; - - function updateAxisMode(refresh) { - const { camStockIndexGrid, camStockIndexed } = current.process; - let newIndexed = camStockIndexed; - let changed = refresh || isIndexed !== newIndexed; - isIndexed = newIndexed; - if (!isIndexed || !isCamMode) { - WIDGETS.setAxisIndex(0); - } - if (!isCamMode) { - return; - } - if (isIndexed) { - current.process.camZAnchor = "middle"; - } - animVer = isIndexed ? 1 : 0; - SPACE.platform.setVisible(!isIndexed); - SPACE.platform.showGrid2(!isIndexed || camStockIndexGrid); - const showIndexed = isIndexed ? '' : 'none'; - const showNonIndexed = isIndexed ? 'none' : ''; - $('cam-index').style.display = showIndexed; - $('cam-lathe').style.display = showIndexed; - $('cam-flip').style.display = showNonIndexed; - $('cam-reg').style.display = showNonIndexed; - if (!changed) { - return; - } - WIDGETS.setIndexed(isIndexed ? true : false); - api.platform.update_bounds(); - // add or remove clock op depending on indexing - const cp = current.process; - if (!cp.ops) { - return; - } - const clockOp = cp.ops.filter(op => op.type === '|')[0]; - if (!clockOp) { - func.opAdd(popOp['|'].new()); - } else { - func.opRender(); - } - updateStock(); +function updateIndex() { + let oplist = env.current.process.ops; + if (!(env.isCamMode && oplist) || env.lastMode === VIEWS.ANIMATE) { + return; } + let index = 0; + for (let op of oplist) { + if (op.type === '|') { + break; + } + if (op.type === 'index' && !op.disabled) { + if (op.absolute) { + index = op.degrees + } else { + index += op.degrees; + } + } + } + WIDGETS.setAxisIndex(env.isPreview || !env.isIndexed ? 0 : -index); + env.currentIndex = env.isIndexed && !env.isPreview ? index * DEG2RAD : 0; +} + +function updateAxisMode(refresh) { + const { camStockIndexGrid, camStockIndexed } = env.current.process; + let newIndexed = camStockIndexed; + let changed = refresh || env.isIndexed !== newIndexed; + env.isIndexed = newIndexed; + if (!env.isIndexed || !env.isCamMode) { + WIDGETS.setAxisIndex(0); + } + if (!env.isCamMode) { + return; + } + if (env.isIndexed) { + env.current.process.camZAnchor = "middle"; + } + env.animVer = env.isIndexed ? 1 : 0; + SPACE.platform.setVisible(!env.isIndexed); + SPACE.platform.showGrid2(!env.isIndexed || camStockIndexGrid); + const showIndexed = env.isIndexed ? '' : 'none'; + const showNonIndexed = env.isIndexed ? 'none' : ''; + $('cam-index').style.display = showIndexed; + $('cam-lathe').style.display = showIndexed; + $('cam-flip').style.display = showNonIndexed; + $('cam-reg').style.display = showNonIndexed; + if (!changed) { + return; + } + WIDGETS.setIndexed(env.isIndexed ? true : false); + api.platform.update_bounds(); + // add or remove clock op depending on indexing + const cp = env.current.process; + if (!cp.ops) { + return; + } + const clockOp = cp.ops.filter(op => op.type === '|')[0]; + if (!clockOp) { + opAdd(env.popOp['|'].new()); + } else { + opRender(); + } + updateStock(); +} + +const opAddLaserOn = () => { + opAdd(env.popOp['laser on'].new()); +}; + +const opAddLaserOff = () => { + opAdd(env.popOp['laser off'].new()); +}; + +const opAddGCode = () => { + opAdd(env.popOp.gcode.new()); +}; + +const opAddIndex = () => { + opAdd(env.popOp.index.new()); +}; + +const opAddLevel = () => { + opAdd(env.popOp.level.new()); +}; + +const opAddRough = () => { + opAdd(env.popOp.rough.new()); +}; + +const opAddOutline = () => { + opAdd(env.popOp.outline.new()); +}; + +const opAddPocket = () => { + traceDone(); + surfaceDone(); + let rec = env.popOp.pocket.new(); + rec.surfaces = { /* widget.id: [ faces... ] */ }; + opAdd(rec); +}; + +const opAddContour = (axis) => { + let rec = env.popOp.contour.new(); + rec.axis = axis.toUpperCase(); + opAdd(rec); +}; + +const opAddLathe = (axis) => { + let rec = env.popOp.lathe.new(); + rec.axis = axis.toUpperCase(); + opAdd(rec); +}; + +const opAddTrace = () => { + let rec = env.popOp.trace.new(); + rec.areas = { /* widget.id: [ polygons... ] */ }; + opAdd(rec); +}; + +const opAddDrill = () => { + let rec = env.popOp.drill.new(); + rec.drills = {}; + opAdd(rec); +}; + +const opAddRegister = (axis, points) => { + let rec = env.popOp.register.new(); + rec.axis = axis.toUpperCase(); + rec.points = points; + opAdd(rec); +}; + +const opAddFlip = () => { + opAdd(env.popOp.flip.new()); +}; + +const traceClear = () => { + traceDone(); + api.widgets.all().forEach(widget => { + unselectTraces(widget); + }); + api.conf.save(); +}; + +const opAdd = (rec) => { + if (!env.isCamMode) return; + clearPops(); + let oplist = env.current.process.ops; + if (oplist.indexOf(rec) < 0) { + if (oplist.length && oplist[oplist.length - 1].type === '|') { + oplist.splice(oplist.length - 1, 0, rec); + } else { + oplist.push(rec); + } + let fpos = oplist.findWith(rec => rec.type === 'flip'); + if (fpos >= 0 && oplist.length > 1) { + let oprec = oplist.splice(fpos, 1); + oplist.push(oprec[0]); + } + api.conf.save(); + opRender(); + } +}; + +const opDel = (rec) => { + if (!env.isCamMode) return; + clearPops(); + let oplist = env.current.process.ops; + let pos = oplist.indexOf(rec); + if (pos >= 0) { + oplist.splice(pos, 1); + api.conf.save(); + opRender(); + } +}; + +export function opRender() { + let oplist = env.current.process.ops; + if (!(env.isCamMode && oplist)) { + return; + } + oplist = oplist.filter(rec => !Array.isArray(rec)); + let mark = Date.now(); + let html = []; + let bind = {}; + let scale = api.view.unit_scale(); + let notime = false; + oplist.forEach((rec, i) => { + let title = ''; + let clock = rec.type === '|'; + let label = clock ? `` : rec.type; + let clazz = notime ? ["draggable", "notime"] : ["draggable"]; + let notable = rec.note ? rec.note.split(' ').filter(v => v.charAt(0) === '#') : undefined; + if (clock) { clazz.push('clock'); title = ` title="end of ops chain\ndrag/drop like an op\nops after this are disabled"` } + if (notable?.length) label += ` (${notable[0].slice(1)})`; + html.appendAll([ + `
`, + ``, + clock ? '' : + ``, + `
` + ]); + bind[mark + i] = rec; + notime = notime || clock; + }); + let listel = $('oplist'); + listel.innerHTML = html.join(''); + let bounds = []; + let unpop = null; + let index = 0; + let indexing = true; + // drag and drop re-ordering + for (let [id, rec] of Object.entries(bind)) { + let type = rec.type; + let clock = type === '|'; + if (!clock) { + $(`${id}-x`).onmousedown = (ev) => { + ev.stopPropagation(); + ev.preventDefault(); + surfaceDone(); + traceDone(); + tabDone(); + opDel(rec); + }; + } else { + indexing = false; + } + let el = $(id); + if (!env.isIndexed && type === 'lathe') { + rec.disabled = true; + } + if (!hasSharedArrays && (type === 'contour' || type === 'lathe')) { + rec.disabled = true; + } + if (rec.disabled) { + el.classList.add("disabled"); + } + bounds.push(el); + let timer = null; + let inside = true; + let popped = false; + let poprec = env.popOp[rec.type]; + if (type === 'index' && indexing && !rec.disabled) { + index = rec.absolute ? rec.degrees : index + rec.degrees; + } + el.rec = rec; + el.unpop = () => { + let pos = [...el.childNodes].indexOf(poprec.div); + if (pos >= 0) { + el.removeChild(poprec.div); + } + popped = false; + }; + function onEnter(ev) { + if ((surfaceOn || traceOn) && env.poppedRec != rec) { + return; + } + if (popped && env.poppedRec != rec) { + surfaceDone(); + traceDone(); + } + if (unpop) unpop(); + env.func.unpop = unpop = el.unpop; + inside = true; + // pointer to current rec for trace editing + env.poppedRec = rec; + popped = true; + poprec.use(rec); + env.hoveredOp = el; + if (!clock) { + // offset Y position of pop div by % of Y screen location of button + el.appendChild(poprec.div); + poprec.addNote(); + const { innerHeight } = window; + const brect = ev.target.getBoundingClientRect(); + const prect = poprec.div.getBoundingClientRect(); + const pcty = (brect.top / innerHeight) * 0.9; + const offpx = -pcty * prect.height; + poprec.div.style.transform = `translateY(${offpx}px)`; + } + // option click event appears latent + // and overides the sticky settings + setTimeout(() => { + UC.setSticky(false); + }, 0); + } + function onLeave(ev) { + inside = false; + clearTimeout(timer); + timer = setTimeout(() => { + if (!inside && poprec.using(rec) && !UC.isSticky()) { + el.unpop(); + } + }, 250); + } + function onDown(ev) { + if (!ev.target.rec) { + // only trigger on operation buttons bound to recs + return; + } + let mobile = ev.touches; + surfaceDone(); + traceDone(); + let target = ev.target, clist = target.classList; + if (!clist.contains("draggable")) { + return; + } + // toggle enable / disable + if (!clock && (ev.ctrlKey || ev.metaKey)) { + ev.preventDefault(); + ev.stopPropagation(); + rec.disabled = !rec.disabled; + for (let op of ev.shiftKey ? oplist : [rec]) { + if (op !== rec) { + op.disabled = op.type !== '|' ? !rec.disabled : false; + } + } + for (let el of bounds) { + if (el.rec.disabled) { + el.classList.add("disabled"); + } else { + el.classList.remove("disabled"); + } + } + if (env.isIndexed) { + updateIndex(); + } + return true; + } + // duplicate op + if (!clock && ev.shiftKey) { + ev.preventDefault(); + ev.stopPropagation(); + oplist = env.current.process.ops; + oplist.push(Object.clone(rec)); + api.conf.save(); + opRender(); + return true; + } + clist.add("drag"); + ev.stopPropagation(); + ev.preventDefault(); + let tracker = UI.tracker; + tracker.style.display = 'block'; + let cancel = tracker.onmouseup = (ev) => { + oplist = env.current.process.ops; + clist.remove("drag"); + tracker.style.display = 'none'; + if (ev) { + ev.stopPropagation(); + ev.preventDefault(); + } + oplist.length = 0; + for (let child of listel.childNodes) { + oplist.push(child.rec); + } + api.conf.save(); + opRender(); + if (mobile) { + el.ontouchmove = onDown; + el.ontouchend = undefined; + } + }; + function onMove(ev) { + ev.stopPropagation(); + ev.preventDefault(); + if (ev.buttons === 0) { + return cancel(); + } + for (let el of bounds) { + if (el === target) continue; + let rect = el.getBoundingClientRect(); + let top = rect.top; + let bottom = rect.bottom;// + rect.height; + let tar = mobile ? ev.touches[0] : ev; + if (tar.pageY >= top && tar.pageY <= bottom) { + let mid = (top + bottom) / 2; + try { listel.removeChild(target); } catch (e) { } + el.insertAdjacentElement(tar.pageY < mid ? "beforebegin" : "afterend", target); + } + } + } + tracker.onmousemove = onMove; + if (mobile) { + el.ontouchmove = onMove; + el.ontouchend = cancel; + } + } + if (SPACE.info.mob) { + let touched = false; + el.ontouchstart = (ev) => { + touched = true; + if (env.poppedRec === rec && popped) { + onLeave(ev); + } else { + onEnter(ev); + } + }; + el.ontouchmove = onDown; + el.onmouseenter = (ev) => { + if (touched) { + // touches block mouse events on touchscreen PCs + // which are often sent along with touch events + // but when not touched, allow the mouse to work + return; + } + el.onmousedown = onDown; + el.onmouseleave = onLeave; + onEnter(ev); + }; + } else { + el.onmousedown = onDown; + el.onmouseenter = onEnter; + el.onmouseleave = onLeave; + } + } + if (env.lastMode !== VIEWS.ANIMATE) { + // update widget rotations from timeline marker + WIDGETS.setAxisIndex(env.isPreview || !env.isIndexed ? 0 : -index); + } + env.currentIndex = env.isIndexed && !env.isPreview ? index * DEG2RAD : 0; + updateStock(); +} + +export function init() { + + api.event.on('tool.mesh.face-normal', normal => { + // console.log({ env.poppedRec }); + env.poppedRec.degrees = (Math.atan2(normal.y, normal.z) * RAD2DEG).round(2); + env.poppedRec.absolute = true; + opRender(); + updateStock(); + }); + + api.event.on("cam.trace.clear", traceClear); api.event.on("cam.parse.gerber", opts => { const { data, mesh } = opts; const { open, closed, circs, rects } = load.GBR.parse(data); - const stack = new kiri.Stack(mesh || moto.space.world.newGroup()); - const layers = new kiri.Layers(); + const stack = new Stack(mesh || space.world.newGroup()); + const layers = new Layers(); for (let poly of open) { - layers.setLayer("open", {line: 0xff8800}, false).addPoly(poly); + layers.setLayer("open", { line: 0xff8800 }, false).addPoly(poly); let diam = poly.tool?.shape?.diameter; if (diam) { const exp = poly.offset_open(diam, 'round'); - layers.setLayer("open-exp", {line: 0xff5555}, false).addPolys(exp); + layers.setLayer("open-exp", { line: 0xff5555 }, false).addPolys(exp); } } for (let poly of closed) { - layers.setLayer("close", {line: 0xff0000}, false).addPoly(poly); + layers.setLayer("close", { line: 0xff0000 }, false).addPoly(poly); } for (let poly of circs) { - layers.setLayer("circs", {line: 0x008800}, false).addPoly(poly); + layers.setLayer("circs", { line: 0x008800 }, false).addPoly(poly); } for (let poly of rects) { - layers.setLayer("rects", {line: 0x0000ff}, false).addPoly(poly); + layers.setLayer("rects", { line: 0x0000ff }, false).addPoly(poly); } stack.addLayers(layers); }); api.event.on("widget.add", widget => { - if (isCamMode && !Array.isArray(widget)) { + if (env.isCamMode && !Array.isArray(widget)) { updateAxisMode(true); - widget.setIndexed(isIndexed ? true : false); + widget.setIndexed(env.isIndexed ? true : false); api.platform.update_bounds(); } }); // wire up animate button in ui api.event.on("function.animate", (mode) => { - if (isAnimate || !isCamMode) { + if (env.isAnimate || !env.isCamMode) { return; } api.function.prepare(() => { - if (isCamMode) { + if (env.isCamMode) { animate(); } }); }); api.event.on("function.export", (mode) => { - if (isAnimate) { - isAnimate = false; + if (env.isAnimate) { + env.isAnimate = false; animFn().animate_clear(api); } }); api.event.on("mode.set", (mode) => { - isIndexed = undefined; - isCamMode = mode === 'CAM'; - SPACE.platform.setColor(isCamMode ? 0xeeeeee : 0xcccccc); - api.uc.setVisible(UI.func.animate, isCamMode); + env.isIndexed = undefined; + env.isCamMode = mode === 'CAM'; + SPACE.platform.setColor(env.isCamMode ? 0xeeeeee : 0xcccccc); + api.uc.setVisible(UI.func.animate, env.isCamMode); // hide/show cam mode elements for (let el of [...document.getElementsByClassName('mode-cam')]) { - api.uc.setClass(el, 'hide', !isCamMode); + api.uc.setClass(el, 'hide', !env.isCamMode); } - if (!isCamMode) { - func.clearPops(); - func.tabClear(); + if (!env.isCamMode) { + clearPops(); + tabClear(); } // do not persist traces across page reloads - func.traceClear(); - func.opRender(); + traceClear(); + opRender(); updateStock(); }); api.event.on("view.set", (mode) => { - lastMode = mode; - isArrange = (mode === VIEWS.ARRANGE); - isPreview = (mode === VIEWS.PREVIEW); - isAnimate = (mode === VIEWS.ANIMATE); + env.lastMode = mode; + env.isArrange = (mode === VIEWS.ARRANGE); + env.isPreview = (mode === VIEWS.PREVIEW); + env.isAnimate = (mode === VIEWS.ANIMATE); animFn().animate_clear(api); - func.clearPops(); - if (isCamMode && isPreview) { + clearPops(); + if (env.isCamMode && env.isPreview) { WIDGETS.setAxisIndex(0); } updateStock(); - func.opRender(); - api.uc.setVisible($('layer-animate'), isAnimate && isCamMode); + opRender(); + api.uc.setVisible($('layer-animate'), env.isAnimate && env.isCamMode); }); api.event.on("settings.saved", (settings) => { validateTools(settings.tools); - current = settings; + env.current = settings; let proc = settings.process; let hasTabs = false; let hasTraces = false; - if (isCamMode && proc.ops) { + if (env.isCamMode && proc.ops) { proc.ops = proc.ops.filter(v => v); } // for any tabs or traces to set markers - for (let widget of API.widgets.all()) { + for (let widget of api.widgets.all()) { let wannot = widget.anno; if (wannot.tab && wannot.tab.length) hasTabs = true; if (wannot.trace && wannot.trace.length) hasTraces = true; @@ -221,21 +610,21 @@ CAM.init = function(kiri, api) { updateIndex(); updateStock(); updateAxisMode(); - if (!poppedRec) { - func.opRender(); + if (!env.poppedRec) { + opRender(); } }); api.event.on("settings.load", (settings) => { - func.opRender(); - if (!isCamMode) return; + opRender(); + if (!env.isCamMode) return; validateTools(settings.tools); restoreTabs(api.widgets.all()); updateAxisMode(); }); api.event.on("cam.stock.toggle", (bool) => { - camStock && (camStock.visible = bool ?? !camStock.visible); + env.camStock && (env.camStock.visible = bool ?? !env.camStock.visible); }); api.event.on("boolean.click", api.platform.update_bounds); @@ -263,12 +652,12 @@ CAM.init = function(kiri, api) { "selection.scale", "selection.rotate" ], () => { - if (!isCamMode) return; - for (let op of current.process.ops) { - if (op.type === 'trace' && !flipping) { + if (!env.isCamMode) return; + for (let op of env.current.process.ops) { + if (op.type === 'trace' && !env.flipping) { op.areas = {}; } - else if( op.type === 'drill' && !flipping){ + else if (op.type === 'drill' && !env.flipping) { op.drills = {}; } } @@ -278,7 +667,7 @@ CAM.init = function(kiri, api) { api.event.on([ "selection.scale", ], () => { - func.tabClear(); + tabClear(); }); api.event.on([ @@ -287,1846 +676,191 @@ CAM.init = function(kiri, api) { ], updateTabs); api.event.on("preview.end", () => { - isParsed = false; - if (isCamMode) { + env.isParsed = false; + if (env.isCamMode) { let bounds = STACKS.getStack("bounds"); if (bounds) bounds.button("animate", animate); } }); api.event.on("code.loaded", (info) => { - if (isCamMode) { - isParsed = true; + if (env.isCamMode) { + env.isParsed = true; let parse = STACKS.getStack("parse", SPACE.world); if (parse) parse.button("animate", animate); } }); $('op-add').onmouseenter = () => { - if (func.unpop) func.unpop(); + if (env.func.unpop) env.func.unpop(); }; $('op-add-list').onclick = (ev) => { - let settings = API.conf.get(); + let settings = api.conf.get(); let { process, device } = settings; switch (ev.target.innerText.toLowerCase()) { - case "index": return func.opAddIndex(); - case "laser on": return func.opAddLaserOn(); - case "laser off": return func.opAddLaserOff(); - case "gcode": return func.opAddGCode(); - case "level": return func.opAddLevel(); - case "rough": return func.opAddRough(); - case "outline": return func.opAddOutline(); + case "index": return opAddIndex(); + case "laser on": return opAddLaserOn(); + case "laser off": return opAddLaserOff(); + case "gcode": return opAddGCode(); + case "level": return opAddLevel(); + case "rough": return opAddRough(); + case "outline": return opAddOutline(); case "contour": let caxis = "X"; - for (let op of current.process.ops) { + for (let op of env.current.process.ops) { if (op.type === "contour" && op.axis === "X") { caxis = "Y"; } } - return func.opAddContour(caxis); + return opAddContour(caxis); case "lathe": let laxis = "X"; - for (let op of current.process.ops) { + for (let op of env.current.process.ops) { if (op.type === "lathe" && op.axis === "X") { laxis = "Y"; } } - return func.opAddLathe(laxis); - case "register": return func.opAddRegister('X', 2); - case "drill": return func.opAddDrill(); - case "trace": return func.opAddTrace(); - case "pocket": return func.opAddPocket(); + return opAddLathe(laxis); + case "register": return opAddRegister('X', 2); + case "drill": return opAddDrill(); + case "trace": return opAddTrace(); + case "pocket": return opAddPocket(); case "flip": // only one flip op permitted - for (let op of current.process.ops) { + for (let op of env.current.process.ops) { if (op.type === 'flip') { return; } } - return func.opAddFlip(); + return opAddFlip(); } }; - func.opAddLaserOn = () => { - func.opAdd(popOp['laser on'].new()); - }; - - func.opAddLaserOff = () => { - func.opAdd(popOp['laser off'].new()); - }; - - func.opAddGCode = () => { - func.opAdd(popOp.gcode.new()); - }; - - func.opAddIndex = () => { - func.opAdd(popOp.index.new()); - }; - - func.opAddLevel = () => { - func.opAdd(popOp.level.new()); - }; - - func.opAddRough = () => { - func.opAdd(popOp.rough.new()); - }; - - func.opAddOutline = () => { - func.opAdd(popOp.outline.new()); - }; - - func.opAddPocket = () => { - func.traceDone(); - func.surfaceDone(); - let rec = popOp.pocket.new(); - rec.surfaces = { /* widget.id: [ faces... ] */ }; - func.opAdd(rec); - }; - - func.opAddContour = (axis) => { - let rec = popOp.contour.new(); - rec.axis = axis.toUpperCase(); - func.opAdd(rec); - }; - - func.opAddLathe = (axis) => { - let rec = popOp.lathe.new(); - rec.axis = axis.toUpperCase(); - func.opAdd(rec); - }; - - func.opAddTrace = () => { - let rec = popOp.trace.new(); - rec.areas = { /* widget.id: [ polygons... ] */ }; - func.opAdd(rec); - }; - - func.opAddDrill = () => { - let rec = popOp.drill.new(); - rec.drills = { }; - func.opAdd(rec); - }; - - func.opAddRegister = (axis, points) => { - let rec = popOp.register.new(); - rec.axis = axis.toUpperCase(); - rec.points = points; - func.opAdd(rec); - }; - - func.opAddFlip = () => { - func.opAdd(popOp.flip.new()); - }; - // TAB/TRACE BUTTON HANDLERS api.event.on("button.click", target => { - let process = API.conf.get().process; + let process = api.conf.get().process; switch (target) { case api.ui.tabAdd: - return func.tabAdd(); + return tabAdd(); case api.ui.tabDun: - return func.tabDone(); + return tabDone(); case api.ui.tabClr: api.uc.confirm("clear tabs?").then(ok => { - if (ok) func.tabClear(); + if (ok) tabClear(); }); break; } }); - // OPS FUNCS - api.event.on("cam.op.add", func.opAdd = (rec) => { - if (!isCamMode) return; - func.clearPops(); - let oplist = current.process.ops; - if (oplist.indexOf(rec) < 0) { - if (oplist.length && oplist[oplist.length-1].type === '|') { - oplist.splice(oplist.length-1,0,rec); - } else { - oplist.push(rec); - } - let fpos = oplist.findWith(rec => rec.type === 'flip'); - if (fpos >= 0 && oplist.length > 1) { - let oprec = oplist.splice(fpos,1); - oplist.push(oprec[0]); - } - API.conf.save(); - func.opRender(); - } - }); + api.event.on("cam.op.add", opAdd); - api.event.on("cam.op.del", func.opDel = (rec) => { - if (!isCamMode) return; - func.clearPops(); - let oplist = current.process.ops; - let pos = oplist.indexOf(rec); - if (pos >= 0) { - oplist.splice(pos,1); - API.conf.save(); - func.opRender(); - } - }); + api.event.on("cam.op.del", opDel); - function updateIndex() { - let oplist = current.process.ops; - if (!(isCamMode && oplist) || lastMode === VIEWS.ANIMATE) { - return; - } - let index = 0; - for (let op of oplist) { - if (op.type === '|') { - break; - } - if (op.type === 'index' && !op.disabled) { - if (op.absolute) { - index = op.degrees - } else { - index += op.degrees; - } - } - } - WIDGETS.setAxisIndex(isPreview || !isIndexed ? 0 : -index); - currentIndex = isIndexed && !isPreview ? index * DEG2RAD : 0; - } + api.event.on("cam.op.render", opRender); - // (re)render the re-orderable op list - api.event.on("cam.op.render", func.opRender = () => { - let oplist = current.process.ops; - if (!(isCamMode && oplist)) { - return; - } - oplist = oplist.filter(rec => !Array.isArray(rec)); - let mark = Date.now(); - let html = []; - let bind = {}; - let scale = API.view.unit_scale(); - let notime = false; - oplist.forEach((rec,i) => { - let title = ''; - let clock = rec.type === '|'; - let label = clock ? `` : rec.type; - let clazz = notime ? [ "draggable", "notime" ] : [ "draggable" ]; - let notable = rec.note ? rec.note.split(' ').filter(v => v.charAt(0) === '#') : undefined; - if (clock) { clazz.push('clock'); title = ` title="end of ops chain\ndrag/drop like an op\nops after this are disabled"` } - if (notable?.length) label += ` (${notable[0].slice(1)})`; - html.appendAll([ - `
`, - ``, - clock ? '' : - ``, - `
` - ]); - bind[mark+i] = rec; - notime = notime || clock; - }); - let listel = $('oplist'); - listel.innerHTML = html.join(''); - let bounds = []; - let unpop = null; - let index = 0; - let indexing = true; - // drag and drop re-ordering - for (let [id, rec] of Object.entries(bind)) { - let type = rec.type; - let clock = type === '|'; - if (!clock) { - $(`${id}-x`).onmousedown = (ev) => { - ev.stopPropagation(); - ev.preventDefault(); - func.surfaceDone(); - func.traceDone(); - func.tabDone(); - func.opDel(rec); - }; - } else { - indexing = false; - } - let el = $(id); - if (!isIndexed && type === 'lathe') { - rec.disabled = true; - } - if (!hasSharedArrays && (type === 'contour' || type === 'lathe')) { - rec.disabled = true; - } - if (rec.disabled) { - el.classList.add("disabled"); - } - bounds.push(el); - let timer = null; - let inside = true; - let popped = false; - let poprec = popOp[rec.type]; - if (type === 'index' && indexing && !rec.disabled) { - index = rec.absolute ? rec.degrees : index + rec.degrees; - } - el.rec = rec; - el.unpop = () => { - let pos = [...el.childNodes].indexOf(poprec.div); - if (pos >= 0) { - el.removeChild(poprec.div); - } - popped = false; - }; - function onEnter(ev) { - if ((surfaceOn || traceOn) && poppedRec != rec) { - return; - } - if (popped && poppedRec != rec) { - func.surfaceDone(); - func.traceDone(); - } - if (unpop) unpop(); - unpop = func.unpop = el.unpop; - inside = true; - // pointer to current rec for trace editing - poppedRec = rec; - popped = true; - poprec.use(rec); - hoveredOp = el; - if (!clock) { - // offset Y position of pop div by % of Y screen location of button - el.appendChild(poprec.div); - poprec.addNote(); - const { innerHeight } = window; - const brect = ev.target.getBoundingClientRect(); - const prect = poprec.div.getBoundingClientRect(); - const pcty = (brect.top / innerHeight) * 0.9; - const offpx = -pcty * prect.height; - poprec.div.style.transform = `translateY(${offpx}px)`; - } - // option click event appears latent - // and overides the sticky settings - setTimeout(() => { - UC.setSticky(false); - }, 0); - } - function onLeave(ev) { - inside = false; - clearTimeout(timer); - timer = setTimeout(() => { - if (!inside && poprec.using(rec) && !UC.isSticky()) { - el.unpop(); - } - }, 250); - } - function onDown(ev) { - if (!ev.target.rec) { - // only trigger on operation buttons bound to recs - return; - } - let mobile = ev.touches; - func.surfaceDone(); - func.traceDone(); - let target = ev.target, clist = target.classList; - if (!clist.contains("draggable")) { - return; - } - // toggle enable / disable - if (!clock && (ev.ctrlKey || ev.metaKey)) { - ev.preventDefault(); - ev.stopPropagation(); - rec.disabled = !rec.disabled; - for (let op of ev.shiftKey ? oplist : [ rec ]) { - if (op !== rec) { - op.disabled = op.type !== '|' ? !rec.disabled : false; - } - } - for (let el of bounds) { - if (el.rec.disabled) { - el.classList.add("disabled"); - } else { - el.classList.remove("disabled"); - } - } - if (isIndexed) { - updateIndex(); - } - return true; - } - // duplicate op - if (!clock && ev.shiftKey) { - ev.preventDefault(); - ev.stopPropagation(); - oplist = current.process.ops; - oplist.push(Object.clone(rec)); - API.conf.save(); - func.opRender(); - return true; - } - clist.add("drag"); - ev.stopPropagation(); - ev.preventDefault(); - let tracker = UI.tracker; - tracker.style.display = 'block'; - let cancel = tracker.onmouseup = (ev) => { - oplist = current.process.ops; - clist.remove("drag"); - tracker.style.display = 'none'; - if (ev) { - ev.stopPropagation(); - ev.preventDefault(); - } - oplist.length = 0; - for (let child of listel.childNodes) { - oplist.push(child.rec); - } - API.conf.save(); - func.opRender(); - if (mobile) { - el.ontouchmove = onDown; - el.ontouchend = undefined; - } - }; - function onMove(ev) { - ev.stopPropagation(); - ev.preventDefault(); - if (ev.buttons === 0) { - return cancel(); - } - for (let el of bounds) { - if (el === target) continue; - let rect = el.getBoundingClientRect(); - let top = rect.top; - let bottom = rect.bottom;// + rect.height; - let tar = mobile ? ev.touches[0] : ev; - if (tar.pageY >= top && tar.pageY <= bottom) { - let mid = (top + bottom) / 2; - try { listel.removeChild(target); } catch (e) { } - el.insertAdjacentElement(tar.pageY < mid ? "beforebegin" : "afterend", target); - } - } - } - tracker.onmousemove = onMove; - if (mobile) { - el.ontouchmove = onMove; - el.ontouchend = cancel; - } - } - if (moto.space.info.mob) { - let touched = false; - el.ontouchstart = (ev) => { - touched = true; - if (poppedRec === rec && popped) { - onLeave(ev); - } else { - onEnter(ev); - } - }; - el.ontouchmove = onDown; - el.onmouseenter = (ev) => { - if (touched) { - // touches block mouse events on touchscreen PCs - // which are often sent along with touch events - // but when not touched, allow the mouse to work - return; - } - el.onmousedown = onDown; - el.onmouseleave = onLeave; - onEnter(ev); - }; - } else { - el.onmousedown = onDown; - el.onmouseenter = onEnter; - el.onmouseleave = onLeave; - } - } - if (lastMode !== VIEWS.ANIMATE) { - // update widget rotations from timeline marker - WIDGETS.setAxisIndex(isPreview || !isIndexed ? 0 : -index); - } - currentIndex = isIndexed && !isPreview ? index * DEG2RAD : 0; - updateStock(); - }); + api.event.on("cam.tabs.add", tabAdd); - func.opGCode = (label, field = 'gcode') => { - API.dialog.show('any'); - const { c_gcode } = h.bind( - $('mod-any'), h.div({ id: "camop_dialog" }, [ - h.label(label || 'custom gcode operation'), - h.textarea({ id: "c_gcode", rows: 15, cols: 50 }), - h.button({ _: 'done', onclick: () => { - API.dialog.hide(); - API.conf.save(); - } }) - ]) - ); - let av = poppedRec[field] || []; - c_gcode.value = typeof(av) === 'string' ? av : av.join('\n'); - c_gcode.onkeyup = (el) => { - poppedRec[field] = c_gcode.value.trim().split('\n'); - }; - c_gcode.focus(); - }; + api.event.on("cam.tabs.done", tabDone); - // create custom gcode editor function - function gcodeEditor(label, field) { - return function() { - func.opGCode(label, field); - } - } - - func.opFlip = () => { - API.view.set_arrange(); - let widgets = API.widgets.all(); - let { process } = current; - let { ops, op2 } = process; - // add flip singleton to b-side - let add2 = op2.length === 0; - let axis = poppedRec.axis; - flipping = true; - process.camZAnchor = { - top: "bottom", - bottom: "top", - middle: "middle" - }[process.camZAnchor]; - // flip tabs - for (let widget of widgets) { - let anno = API.widgets.annotate(widget.id).tab || []; - let wbm = widget.bounds.max.z; - for (let tab of anno) { - let box = widget.tabs[tab.id].box; - let bpo = box.position; - let xr = 0, yr = 0; - let flz = wbm - bpo.z; - if (axis === 'X') { - tab.pos.y = -tab.pos.y; - bpo.y = -bpo.y; - xr = Math.PI / 2; - } - if (axis === 'Y') { - tab.pos.x = -tab.pos.x; - bpo.x = -bpo.x; - yr = Math.PI / 2; - } - tab.pos.z = bpo.z = flz; - let [ rx, ry, rz, rw ] = tab.rot; - let qat = new THREE.Quaternion(rx, ry, rz, rw); - let eul = new THREE.Euler().setFromQuaternion(qat); - eul._z = -eul._z; - tab.rot = new THREE.Quaternion().setFromEuler(eul); - } - clearTabs(widget, true); - restoreTabs([widget]); - } - // flip widget - if (axis === 'X') { - API.selection.rotate(Math.PI, 0, 0); - } - if (axis === 'Y') { - API.selection.rotate(0, Math.PI, 0); - } - // clear traces cache - CAM.traces_clear(); - kiri.client.clear(); - flipping = false; - process.ops = op2; - process.op2 = ops; - // flip camZBottom - if (poppedRec.invert && process.camZBottom && camZBottom) { - const maxZ = camZBottom._max.z - process.camZBottom = maxZ - process.camZBottom; - API.util.rec2ui(process); - updateStock(); - } - // keep flip operations in sync - for (let op of op2) { - if (op.type === 'flip') { - op.axis = poppedRec.axis; - op.invert = poppedRec.invert; - } - } - if (add2) { - func.opAdd(poppedRec); - } else { - func.opRender(); - } - }; - - // TAB FUNCS - let showTab, lastTab, tab, iw, ic; - api.event.on("cam.tabs.add", func.tabAdd = () => { - func.traceDone(); - alert = api.show.alert("[esc] cancels tab editing"); - api.feature.hover = true; - func.hover = func.tabHover; - func.hoverUp = func.tabHoverUp; - }); - api.event.on("cam.tabs.done", func.tabDone = () => { - delbox('tabb'); - api.hide.alert(alert); - api.feature.hover = false; - if (lastTab) { - lastTab.box.material.color.r = 0; - lastTab = null; - } - }); - api.event.on("cam.tabs.clear", func.tabClear = () => { - func.tabDone(); - API.widgets.all().forEach(widget => { - clearTabs(widget); - widget.saveState(); - }); - API.conf.save(); - }); - func.tabHover = function(data) { - delbox('tabb'); - const { int, type, point } = data; - const object = int ? int.object : null; - const tab = int ? object.tab : null; - if (lastTab) { - lastTab.box.material.color.r = 0; - lastTab = null; - } - if (tab) { - tab.box.material.color.r = 0.5; - lastTab = tab; - return; - } - if (type !== 'widget') { - iw = null; - return; - } - let n = int.face.normal; - iw = int.object.widget; - ic = int.point; - // only near vertical faces - // if (Math.abs(n.z) > 0.3) { - // return; - // } - showTab = createTabBox(iw, ic, n); - }; - func.tabHoverUp = function(int) { - delbox('tabb'); - if (lastTab) { - const {widget, box, id} = lastTab; - widget.adds.remove(box); - widget.mesh.remove(box); - delete widget.tabs[id]; - let ta = API.widgets.annotate(widget.id).tab; - let ix = 0; - ta.forEach((rec,i) => { - if (rec.id === id) { - ix = i; - } - }); - ta.splice(ix,1); - API.conf.save(); - widget.saveState(); - return; - } - if (!iw) return; - let ip = iw.track.pos; - let wa = api.widgets.annotate(iw.id); - let wt = (wa.tab = wa.tab || []); - let pos = { - x: showTab.pos.x - ip.x, - y: -showTab.pos.z - ip.y, - z: showTab.stock.z ? - showTab.pos.y + ip.z + (isIndexed ? 0 : iw.track.tzoff) : - showTab.dim.z/2, - } - let id = Date.now(); - let { dim, rot } = showTab; - let rec = { pos, dim, rot, id }; - wt.push(Object.clone(rec)); - addWidgetTab(iw, rec); - API.conf.save(); - iw.saveState(); - }; - - // SURFACE FUNCS - let surfaceOn = false, lastWidget; - func.surfaceAdd = (ev) => { - if (surfaceOn) { - return func.surfaceDone(); - } - func.clearPops(); - alert = api.show.alert("analyzing surfaces...", 1000); - let surfaces = poppedRec.surfaces; - let radians = poppedRec.follow * DEG2RAD; - CAM.surface_prep(currentIndex * RAD2DEG, () => { - api.hide.alert(alert); - alert = api.show.alert("[esc] cancels surface selection"); - for (let [wid, arr] of Object.entries(surfaces)) { - let widget = api.widgets.forid(wid); - if (widget && arr.length) - for (let faceid of arr) { - CAM.surface_toggle(widget, faceid, radians, faceids => { - // surfaces[widget.id] = faceids; - }); - } - } - }); - surfaceOn = hoveredOp; - surfaceOn.classList.add("editing"); - api.feature.on_mouse_up = (obj, ev) => { - let { face } = obj; - let min = Math.min(face.a, face.b, face.c); - let faceid = min / 3; - let widget = lastWidget = obj.object.widget; - CAM.surface_toggle(widget, faceid, radians, faceids => { - surfaces[widget.id] = faceids; - }); - }; - }; - func.surfaceDone = () => { - if (!(surfaceOn && poppedRec && poppedRec.surfaces)) { - return; - } - let surfaces = poppedRec.surfaces; - for (let wid of Object.keys(surfaces)) { - let widget = api.widgets.forid(wid); - if (widget) { - CAM.surface_clear(widget); - } else { - delete surfaces[wid]; - } - } - api.hide.alert(alert); - api.feature.on_mouse_up = undefined; - surfaceOn.classList.remove("editing"); - surfaceOn = false; - }; - - // TRACE FUNCS - let traceOn = false, lastTrace; - func.traceAdd = (ev) => { - if (traceOn) { - return func.traceDone(); - } - func.clearPops(); - alert = api.show.alert("analyzing parts...", 1000); - traceOn = hoveredOp; - traceOn.classList.add("editing"); - api.feature.hover = true; - api.feature.hoverAdds = true; - func.hover = func.traceHover; - func.hoverUp = func.traceHoverUp; - CAM.traces((ids) => { - api.hide.alert(alert); - alert = api.show.alert("[esc] cancels trace editing"); - kiri.api.widgets.for(widget => { - if (ids.indexOf(widget.id) >= 0) { - unselectTraces(widget, true); - widget.trace_stack = null; - } - if (widget.trace_stack) { - widget.adds.appendAll(widget.trace_stack.meshes); - widget.trace_stack.show(); - return; - } - let areas = (poppedRec.areas[widget.id] || []); - let stack = new kiri.Stack(widget.mesh); - widget.trace_stack = stack; - widget.traces.forEach(poly => { - let match = areas.filter(arr => poly.matches(arr)); - let layers = new kiri.Layers(); - layers.setLayer("trace", {line: 0xaaaa55, fat:4, order:-10}, false).addPoly(poly); - stack.addLayers(layers); - stack.new_meshes.forEach(mesh => { - mesh.trace = {widget, poly}; - // ensure trace poly singleton from matches - if (match.length > 0) { - poly._trace = match[0]; - } else { - poly._trace = poly.toArray(); - } - }); - widget.adds.appendAll(stack.new_meshes); - // console.log(widget) - }); - }); - // ensure appropriate traces are toggled matching current record - kiri.api.widgets.for(widget => { - widget.setVisualState({ opacity: 0.25 }); - let areas = (poppedRec.areas[widget.id] || []); - let stack = widget.trace_stack; - stack.meshes.forEach(mesh => { - let { poly } = mesh.trace; - let match = areas.filter(arr => poly.matches(arr)); - if (match.length > 0) { - if (!mesh.selected) { - func.traceToggle(mesh, true); - } - } else if (mesh.selected) { - func.traceToggle(mesh, true); - } - }); - }); - }, poppedRec.select === 'lines'); - }; - func.traceDone = () => { - if (!traceOn) { - return; - } - func.unpop(); - traceOn.classList.remove("editing"); - traceOn = false; - kiri.api.widgets.opacity(1); - api.hide.alert(alert); - api.feature.hover = false; - api.feature.hoverAdds = false; - kiri.api.widgets.for(widget => { - widget.restoreVisualState(); - if (widget.trace_stack) { - widget.trace_stack.hide(); - widget.adds.removeAll(widget.trace_stack.meshes); - } - }); - }; - func.clearPops = () => { - if (func.unpop) func.unpop(); - func.tabDone(); - func.traceDone(); - func.surfaceDone(); - func.selectHolesDone(); - }; - api.event.on("cam.trace.clear", func.traceClear = () => { - func.traceDone(); - API.widgets.all().forEach(widget => { - unselectTraces(widget); - }); - API.conf.save(); - }); - func.traceHover = function(data) { - if (lastTrace) { - let { color, colorSave } = lastTrace.material[0] || lastTrace.material; - color.r = colorSave.r; - color.g = colorSave.g; - color.b = colorSave.b; - } - lastTrace = null; - if (data.type === 'platform') { - return; - } - if (!data.int.object.trace) { - return; - } - lastTrace = data.int.object; - if (lastTrace.selected) { - let event = data.event; - let target = event.target; - let { clientX, clientY } = event; - let { offsetWidth, offsetHeight } = target; - } - let material = lastTrace.material[0] || lastTrace.material; - let color = material.color; - material.colorSave = color.clone(); - color.setHex(isDark() ? 0x0066ff : 0x0000ff); - }; - func.traceHoverUp = function(int, ev) { - if (!int) return; - let { object } = int; - func.traceToggle(object); - if (ev.metaKey || ev.ctrlKey) { - let { selected } = object; - let { widget, poly } = object.trace; - let avgZ = poly.avgZ(); - for (let add of widget.adds) { - if (add.trace && add.selected !== selected && add.trace.poly.onZ(avgZ)) { - func.traceToggle(add); - } - } - } - }; - func.traceToggle = function(obj, skip) { - let material = obj.material[0] || obj.material; - if (!material) return; - let { color, colorSave } = material; - let { widget, poly } = obj.trace; - let areas = poppedRec.areas; - if (!areas) { - return; - } - let wlist = areas[widget.id] = areas[widget.id] || []; - obj.selected = !obj.selected; - if (!colorSave) { - colorSave = material.colorSave = color.clone(); - } - if (obj.selected) { - color.setHex(isDark() ? 0xdd0011 : 0xff0033); - colorSave.r = color.r; - colorSave.g = color.g; - colorSave.b = color.b; - if (!skip) wlist.push(poly._trace); - } else { - color.setHex(0xaaaa55); - colorSave.setHex(0xaaaa55); - if (!skip) wlist.remove(poly._trace); - } - API.conf.save(); - }; - - - let holeSelOn = false, lastSelHoles; - /** - * Client side function to select holes in widgets for CAM operations. - * If holes have already been analyzed, they are displayed immediately. - * Otherwise, the CAM server is queried and the results are cached. - * @param {boolean} individual - select all holes, false to only select - * holes of tool diameter. - */ - func.selectHoles = async function(individual){ - // console.log("client individual selected",individual) - if (holeSelOn) { - return func.selectHolesDone(); - } - func.clearPops(); - let alert = api.show.alert("analyzing parts..."); - holeSelOn = hoveredOp; - holeSelOn.classList.add("editing"); - api.feature.hover = true; - api.feature.hoverAdds = true; - func.hover = func.selectHolesHover; - func.hoverUp = func.selectHolesHoverUp; - - const widgets = kiri.api.widgets.all() - /** - * creates a mesh for a hole and adds it to a widget - * @param {Object3D} widget - widget to add the hole mesh to - * @param {Object} drill - {depth,selected} object of the hole - * @returns {Mesh} the created mesh - */ - function createHoleMesh(widget,drill){ - let {depth,selected, diam} = drill - let color = selected ? 0xFF0000:0x39e366 - let geo = new THREE.CylinderGeometry(diam/2,diam/2,depth,20); - const material = new THREE.MeshPhongMaterial( { color } ); - let mesh = new THREE.Mesh(geo, material); - mesh.position.copy(drill) - mesh.position.z -= depth/2 - mesh.rotation.x = Math.PI/2 - drill.widgetID = widget.id - drill.meshId = mesh.id; //add pointers to both objects - mesh.hole = drill; - mesh.parent = widget.mesh; - widget.mesh.add(mesh); - widget.adds.push(mesh); //for click detection - return mesh - } - let meshesCached = widgets.every(widget=>poppedRec.drills[widget.id] != undefined) - if( individual && meshesCached ){ // if any widget already has cached holes - // console.log("already has cached holes",poppedRec.drills) - api.hide.alert(alert) - kiri.api.widgets.for(widget => { - if(widget.adds){ - let drills = poppedRec.drills[widget.id] - let ids = drills - .map(hole => hole.meshId) - if(widget.adds.includes(mesh)){ - widget.adds.forEach(add=>{ - if(ids.includes(add.id)){ - add.visible = true - } - }) - }else{ - drills.forEach(drill => { - createHoleMesh(widget, drill) - }) - } - } - }) - - }else{ // if no widget has cached holes - let alert = api.show.alert(""); - await CAM.holes( - individual, - poppedRec, - (progress,msg)=>{ - alert[0]=msg - api.show.progress(progress,msg) - api.alerts.update() - }, - async centers => { - api.show.progress(0) - if(! Array.isArray(centers) ){ - console.log("worker returned a malformed drills response") - return - } - let shadow = centers.some(c=>c.shadowed) - api.hide.alert(alert) - if(shadow){ - alert = api.show.alert("Some holes are shadowed by part and are not shown."); - } - centers = centers ?? [] - // list of all hole centers and if they are selected - kiri.api.widgets.for(widget => { - const {holes} = centers.find(center=>center.id == widget.id) - // console.log(holes) - if (!holes.length) unselectHoles(holes); - holes.forEach(hole => { - createHoleMesh(widget, hole) - }) - //add hole data to record - poppedRec.drills = poppedRec.drills ?? {} - poppedRec.drills[widget.id] = holes - //give widget access to an array of drill records that refrence it - //so that it can be cleared when widget is rotated or mirrored etc. - if(!widget.drills){widget.drills = []} - widget.drills.push(holes) - }) - } - ); - } - //hide the alert once hole meshes are calculated on the worker, and then added to the scene - // api.hide.alert(alert); - let escAlert = api.show.alert("[esc] cancels drill editing",1000); - setTimeout(() => { - api.hide.alert(escAlert); - }, 5000); - kiri.api.widgets.opacity(0.8); - } - - func.selectHolesHover = function(data) { - //not used right now. may be useful in the future - if (lastTrace) { - let { color, colorSave } = lastTrace.material[0] || lastTrace.material; - color.r = colorSave.r; - color.g = colorSave.g; - color.b = colorSave.b; - lastTrace.position.z -= 0.01; - } - if (data.type === 'platform') { - lastTrace = null; - return; - } - } - - func.selectHolesHoverUp = function(int, ev) { - if (!int) return; //if not a hole mesh return - let { object } = int; - func.selectHoleToggle(object); - } - - /** - * Toggle the selection of a hole mesh and update its color - * @param {Object3D} mesh - the hole mesh to toggle - */ - func.selectHoleToggle = function(mesh) { - let {hole} = mesh - if(!hole) return - hole.selected = !hole.selected; - mesh.material.color.setHex( hole.selected ? 0xFF0000:0x39e366 ); - } - - /** - * Clears the recorded holes in the widget (widget.drills array) - * and also clears the widget.adds array. - * @param {Object} widget - the widget with the drills array to clear - */ - func.clearHolesRec = (widget)=>{ - if(widget.drills){ - widget.drills.forEach(rec=>{ - }) - } - if(widget.adds){ - widget.adds.length = 0 //clear adds array - } - } - - /** - * Cleanup function for selectHoles. - * Removes all adds from the scene, hides the alert, and resets the opacity of the widgets. - * Also resets the hover features and the editing class on the holeSelOn html element. - */ - func.selectHolesDone = () => { - if (!holeSelOn) { - return; - } - func.unpop(); - holeSelOn.classList.remove("editing"); - holeSelOn = false; - kiri.api.widgets.opacity(1); - api.hide.alert(alert); - api.feature.hover = false; - api.feature.hoverAdds = false; - - kiri.api.widgets.for(widget => { - for(let add of widget.adds){ - add.visible = false - widget.mesh.remove(add); - } - }); - }; + api.event.on("cam.tabs.clear", tabClear); // COMMON TAB/TRACE EVENT HANDLERS api.event.on("slice.begin", () => { - if (isCamMode) { - func.clearPops(); + if (env.isCamMode) { + clearPops(); } }); + api.event.on("key.esc", () => { - if (isCamMode) { - func.clearPops(); + if (env.isCamMode) { + clearPops(); } }); + api.event.on("selection.scale", () => { - if (isCamMode) { - func.clearPops(); + if (env.isCamMode) { + clearPops(); } }); + api.event.on("widget.duplicate", (widget, oldwidget) => { - if (!isCamMode) { + if (!env.isCamMode) { return; } if (traceOn) { - func.traceDone(); + traceDone(); } unselectTraces(widget); - if (flipping) { + if (env.flipping) { return; } - let ann = API.widgets.annotate(widget.id); + let ann = api.widgets.annotate(widget.id); if (ann.tab) { - ann.tab.forEach((tab,i) => { + ann.tab.forEach((tab, i) => { tab.id = Date.now() + i; }); restoreTabs([widget]); } }); + api.event.on("widget.mirror", widget => { - if (!isCamMode) { + if (!env.isCamMode) { return; } if (traceOn) { - func.traceDone(); + traceDone(); } if (holeSelOn) { - func.selectHolesDone(); + selectHolesDone(); } - func.clearHolesRec(widget) + clearHolesRec(widget) unselectTraces(widget); - if (flipping) { + if (env.flipping) { return; } mirrorTabs(widget); }); + api.event.on("widget.rotate", rot => { - if (!isCamMode) { + if (!env.isCamMode) { return; } - let {widget, x, y, z} = rot; + let { widget, x, y, z } = rot; if (traceOn) { - func.traceDone(); + traceDone(); } unselectTraces(widget); if (holeSelOn) { - func.selectHolesDone(); + selectHolesDone(); } - if (flipping) { + if (env.flipping) { return; } - func.clearHolesRec(widget) + clearHolesRec(widget) if (x || y) { clearTabs(widget); } else { rotateTabs(widget, x, y, z); } }); + api.event.on("mouse.hover.up", rec => { - if (!isCamMode) { + if (!env.isCamMode) { return; } let { object, event } = rec; - func.hoverUp(object, event); + env.hoverUp(object, event); }); + api.event.on("mouse.hover", data => { - if (!isCamMode) { + if (!env.isCamMode) { return; } - func.hover(data); + env.hover(data); }); - function mirrorTabs(widget) { - let tabs = API.widgets.annotate(widget.id).tab || []; - tabs.forEach(rec => { - let { id, pos, rot } = rec; - let tab = widget.tabs[id]; - let e = new THREE.Euler().setFromQuaternion(rot); - e._z = Math.PI - e._z; - let { _x, _y, _z, _w } = rec.rot; - let or = new THREE.Quaternion(_x, _y, _z, _w); - let nr = new THREE.Quaternion().setFromEuler(e); - let ra = or.angleTo(nr); - // console.log({or, nr, ra}); - rec.rot = nr; - // let m4 = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(0,0,e._z)); - // tab.box.geometry.applyMatrix4(m4); - tab.box.position.x = pos.x = -pos.x; - }); - SPACE.update(); - } - - function rotateTabs(widget, x, y, z) { - let tabs = API.widgets.annotate(widget.id).tab || []; - tabs.forEach(rec => { - let { id, pos, rot } = rec; - if (!Array.isArray(rot)) { - rot = rot.toArray(); - } - let coff = widget.track.center; - let tab = widget.tabs[id]; - let m4 = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(x || 0, y || 0, z || 0)); - // update position vector - let vc = new THREE.Vector3(pos.x, pos.y, pos.z).applyMatrix4(m4); - // update rotation quaternion - let [ rx, ry, rz, rw ] = rot; - rec.rot = new THREE.Quaternion().multiplyQuaternions( - new THREE.Quaternion(rx, ry, rz, rw), - new THREE.Quaternion().setFromRotationMatrix(m4) - ).toArray(); - tab.box.geometry.applyMatrix4(m4); - tab.box.position.x = pos.x = vc.x - coff.dx; - tab.box.position.y = pos.y = vc.y - coff.dy; - tab.box.position.z = pos.z = vc.z; - }); - SPACE.update(); - } - - function hasIndexing() { - return isIndexed; - } - - function hasSpindle() { - return current.device.spindleMax > 0; - } - - function zTop() { - return API.conf.get().process.camZTop > 0; - } - - function zBottom() { - return API.conf.get().process.camZBottom > 0; - } - - createPopOp('level', { - tool: 'camLevelTool', - spindle: 'camLevelSpindle', - step: 'camLevelOver', - rate: 'camLevelSpeed', - down: 'camLevelDown', - over: 'camLevelOver', - stock: 'camLevelStock' - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - sep: UC.newBlank({class:"pop-sep"}), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}), - rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), - down: UC.newInput(LANG.cc_loff_s, {title:LANG.cc_loff_l, convert:UC.toFloat, units:true}), - over: UC.newInput(LANG.cc_lxyo_s, {title:LANG.cc_lxyo_l, convert:UC.toFloat, units:true}), - sep: UC.newBlank({class:"pop-sep"}), - stock: UC.newBoolean(LANG.cc_lsto_s, undefined, {title:LANG.cc_lsto_l}), - }; - - createPopOp('rough', { - tool: 'camRoughTool', - spindle: 'camRoughSpindle', - down: 'camRoughDown', - step: 'camRoughOver', - rate: 'camRoughSpeed', - plunge: 'camRoughPlunge', - leave: 'camRoughStock', - leavez: 'camRoughStockZ', - all: 'camRoughAll', - voids: 'camRoughVoid', - flats: 'camRoughFlat', - inside: 'camRoughIn', - ov_topz: 0, - ov_botz: 0, - ov_conv: '~camConventional', - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - sep: UC.newBlank({class:"pop-sep"}), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), - plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), - sep: UC.newBlank({class:"pop-sep"}), - down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), - step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}), - leave: UC.newInput(LANG.cr_lsto_s, {title:LANG.cr_lsto_l, convert:UC.toFloat, units:true}), - leavez: UC.newInput(LANG.cr_lstz_s, {title:LANG.cr_lstz_l, convert:UC.toFloat, bound:UC.bound(0,10),units:true}), - sep: UC.newBlank({class:"pop-sep"}), - all: UC.newBoolean(LANG.cr_clst_s, undefined, {title:LANG.cr_clst_l, show:hasIndexing}), - voids: UC.newBoolean(LANG.cr_clrp_s, undefined, {title:LANG.cr_clrp_l}), - flats: UC.newBoolean(LANG.cr_clrf_s, undefined, {title:LANG.cr_clrf_l}), - inside: UC.newBoolean(LANG.cr_olin_s, undefined, {title:LANG.cr_olin_l}), - sep: UC.newBlank({class:"pop-sep"}), - exp: UC.newExpand("overrides"), - ov_topz: UC.newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:UC.toFloat, units:true}), - ov_botz: UC.newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:UC.toFloat, units:true}), - ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, {title:LANG.ou_conv_l}), - exp_end: UC.endExpand(), - }; - - createPopOp('outline', { - tool: 'camOutlineTool', - spindle: 'camOutlineSpindle', - step: 'camOutlineOver', - steps: 'camOutlineOverCount', - down: 'camOutlineDown', - rate: 'camOutlineSpeed', - plunge: 'camOutlinePlunge', - dogbones: 'camOutlineDogbone', - omitvoid: 'camOutlineOmitVoid', - omitthru: 'camOutlineOmitThru', - outside: 'camOutlineOut', - inside: 'camOutlineIn', - wide: 'camOutlineWide', - top: 'camOutlineTop', - ov_topz: 0, - ov_botz: 0, - ov_conv: '~camConventional', - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - sep: UC.newBlank({class:"pop-sep"}), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), - plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), - down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), - step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0), show:() => popOp.outline.rec.wide}), - steps: UC.newInput(LANG.cc_sovc_s, {title:LANG.cc_sovc_l, convert:UC.toInt, bound:UC.bound(1,5), show:() => popOp.outline.rec.wide}), - sep: UC.newBlank({class:"pop-sep"}), - top: UC.newBoolean(LANG.co_clrt_s, undefined, {title:LANG.co_clrt_l}), - inside: UC.newBoolean(LANG.co_olin_s, undefined, {title:LANG.co_olin_l, show:(op) => { return !op.inputs.outside.checked }}), - outside: UC.newBoolean(LANG.co_olot_s, undefined, {title:LANG.co_olot_l, show:(op) => { return !op.inputs.inside.checked }}), - sep: UC.newBlank({class:"pop-sep"}), - omitthru: UC.newBoolean(LANG.co_omit_s, undefined, {title:LANG.co_omit_l, xshow:(op) => { return op.inputs.outside.checked }}), - omitvoid: UC.newBoolean(LANG.co_omvd_s, undefined, {title:LANG.co_omvd_l, xshow:(op) => { return op.inputs.outside.checked }}), - wide: UC.newBoolean(LANG.co_wide_s, undefined, {title:LANG.co_wide_l, show:(op) => { return !op.inputs.inside.checked }}), - dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, {title:LANG.co_dogb_l, show:(op) => { return !op.inputs.wide.checked }}), - sep: UC.newBlank({class:"pop-sep"}), - exp: UC.newExpand("overrides"), - ov_topz: UC.newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:UC.toFloat, units:true}), - ov_botz: UC.newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:UC.toFloat, units:true}), - ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, {title:LANG.ou_conv_l}), - exp_end: UC.endExpand(), - }; - - const contourFilter = gcodeEditor('Layer Filter', 'filter'); - - createPopOp('contour', { - tool: 'camContourTool', - spindle: 'camContourSpindle', - step: 'camContourOver', - rate: 'camContourSpeed', - angle: 'camContourAngle', - leave: 'camContourLeave', - tolerance: 'camTolerance', - flatness: 'camFlatness', - reduction: 'camContourReduce', - bridging: 'camContourBridge', - bottom: 'camContourBottom', - curves: 'camContourCurves', - inside: 'camContourIn', - filter: 'camContourFilter', - axis: 'X' - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - axis: UC.newSelect(LANG.cd_axis, {}, "xyaxis"), - sep: UC.newBlank({class:"pop-sep"}), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), - sep: UC.newBlank({class:"pop-sep"}), - step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,10.0)}), - leave: UC.newInput(LANG.cf_leav_s, {title:LANG.cf_leav_l, convert:UC.toFloat, bound:UC.bound(0,100)}), - sep: UC.newBlank({class:"pop-sep"}), - angle: UC.newInput(LANG.cf_angl_s, {title:LANG.cf_angl_l, convert:UC.toFloat, bound:UC.bound(45,90), show:(op) => op.inputs.curves.checked}), - flatness: UC.newInput(LANG.ou_flat_s, {title:LANG.ou_flat_l, convert:UC.toFloat, bound:UC.bound(0,1.0), units:false, round:4}), - tolerance: UC.newInput(LANG.ou_toll_s, {title:LANG.ou_toll_l, convert:UC.toFloat, bound:UC.bound(0,10.0), units:true, round:4}), - reduction: UC.newInput(LANG.ou_redu_s, {title:LANG.ou_redu_l, convert:UC.toInt, bound:UC.bound(0,10), units:false}), - // bridging: UC.newInput(LANG.ou_brdg_s, {title:LANG.ou_brdg_l, convert:UC.toFloat, bound:UC.bound(0,1000.0), units:true, round:4, show:(op) => op.inputs.curves.checked}), - sep: UC.newBlank({class:"pop-sep"}), - curves: UC.newBoolean(LANG.cf_curv_s, undefined, {title:LANG.cf_curv_l}), - inside: UC.newBoolean(LANG.cf_olin_s, undefined, {title:LANG.cf_olin_l}), - bottom: UC.newBoolean(LANG.cf_botm_s, undefined, {title:LANG.cf_botm_l, show:(op,conf) => conf ? conf.process.camZBottom : 0}), - filter: UC.newRow([ UC.newButton(LANG.filter, contourFilter) ], {class:"ext-buttons f-row"}) - }; - - createPopOp('lathe', { - tool: 'camLatheTool', - spindle: 'camLatheSpindle', - step: 'camLatheOver', - angle: 'camLatheAngle', - rate: 'camLatheSpeed', - tolerance: 'camTolerance', - filter: 'camContourFilter', - leave: 'camContourLeave', - linear: 'camLatheLinear' - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - // axis: UC.newSelect(LANG.cd_axis, {}, "xyaxis"), - sep: UC.newBlank({class:"pop-sep"}), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), - sep: UC.newBlank({class:"pop-sep"}), - step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,100.0)}), - angle: UC.newInput(LANG.cc_sang_s, {title:LANG.cc_sang_l, convert:UC.toFloat, bound:UC.bound(0.01,180.0)}), - sep: UC.newBlank({class:"pop-sep"}), - tolerance: UC.newInput(LANG.ou_toll_s, {title:LANG.ou_toll_l, convert:UC.toFloat, bound:UC.bound(0,10.0), units:true, round:4}), - leave: UC.newInput(LANG.cf_leav_s, {title:LANG.cf_leav_l, convert:UC.toFloat, bound:UC.bound(0,100)}), - sep: UC.newBlank({class:"pop-sep"}), - linear: UC.newBoolean(LANG.ci_line_s, undefined, {title:LANG.ci_line_l}), - // filter: UC.newRow([ UC.newButton(LANG.filter, contourFilter) ], {class:"ext-buttons f-row"}) - }; - - function canDogBones() { - if (!poppedRec) return false; - return poppedRec.mode === 'follow';// && poppedRec.offset && poppedRec.offset !== 'none'; - } - - function canDogBonesRev() { - return canDogBones() && poppedRec.dogbone; - } - - function zDogSep() { - return canDogBones() || zBottom(); - } - - createPopOp('trace', { - mode: 'camTraceType', - offset: 'camTraceOffset', - spindle: 'camTraceSpindle', - tool: 'camTraceTool', - step: 'camTraceOver', - down: 'camTraceDown', - thru: 'camTraceThru', - rate: 'camTraceSpeed', - plunge: 'camTracePlunge', - offover: 'camTraceOffOver', - dogbone: 'camTraceDogbone', - revbone: 'camTraceDogbone', - merge: 'camTraceMerge', - ov_topz: 0, - ov_botz: 0, - ov_conv: '~camConventional', - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - mode: UC.newSelect(LANG.cu_type_s, {title:LANG.cu_type_l}, "trace"), - offset: UC.newSelect(LANG.cc_offs_s, {title: LANG.cc_offs_l, show:() => (poppedRec.mode === 'follow')}, "traceoff"), - sep: UC.newBlank({class:"pop-sep"}), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), - plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), - sep: UC.newBlank({class:"pop-sep"}), - step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0), show:(op) => popOp.trace.rec.mode === "clear"}), - down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), - thru: UC.newInput(LANG.cc_thru_s, {title:LANG.cc_thru_l, convert:UC.toFloat, units:true}), - offover: UC.newInput(LANG.cc_offd_s, {title:LANG.cc_offd_l, convert:UC.toFloat, units:true, show:() => poppedRec.offset !== "none" || poppedRec.mode === "clear"}), - sep: UC.newBlank({class:"pop-sep", modes:MCAM, xshow:zDogSep}), - merge: UC.newBoolean(LANG.co_merg_s, undefined, {title:LANG.co_merg_l, show:() => !popOp.trace.rec.down}), - dogbone: UC.newBoolean(LANG.co_dogb_s, undefined, {title:LANG.co_dogb_l, show:canDogBones}), - revbone: UC.newBoolean(LANG.co_dogr_s, undefined, {title:LANG.co_dogr_l, show:canDogBonesRev}), - exp: UC.newExpand("overrides"), - sep: UC.newBlank({class:"pop-sep"}), - ov_topz: UC.newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:UC.toFloat, units:true}), - ov_botz: UC.newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:UC.toFloat, units:true}), - ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, {title:LANG.ou_conv_l}), - exp_end: UC.endExpand(), - sep: UC.newBlank({class:"pop-sep"}), - menu: UC.newRow([ UC.newButton("select", func.traceAdd) ], {class:"ext-buttons f-row"}), - }; - - createPopOp('pocket', { - spindle: 'camPocketSpindle', - tool: 'camPocketTool', - step: 'camPocketOver', - down: 'camPocketDown', - rate: 'camPocketSpeed', - plunge: 'camPocketPlunge', - expand: 'camPocketExpand', - smooth: 'camPocketSmooth', - refine: 'camPocketRefine', - follow: 'camPocketFollow', - contour: 'camPocketContour', - engrave: 'camPocketEngrave', - outline: 'camPocketOutline', - ov_topz: 0, - ov_botz: 0, - ov_conv: '~camConventional', - tolerance: 'camTolerance', - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - sep: UC.newBlank({class:"pop-sep"}), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}), - plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), - sep: UC.newBlank({class:"pop-sep"}), - step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}), - down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true, show:() => !poppedRec.contour}), - sep: UC.newBlank({class:"pop-sep"}), - expand: UC.newInput(LANG.cp_xpnd_s, {title:LANG.cp_xpnd_l, convert:UC.toFloat, units:true, xshow:() => !poppedRec.contour}), - refine: UC.newInput(LANG.cp_refi_s, {title:LANG.cp_refi_l, convert:UC.toInt, show:() => poppedRec.contour}), - smooth: UC.newInput(LANG.cp_smoo_s, {title:LANG.cp_smoo_l, convert:UC.toInt}), - tolerance: UC.newInput(LANG.ou_toll_s, {title:LANG.ou_toll_l, convert:UC.toFloat, bound:UC.bound(0,10.0), units:true, round:4, show:() => poppedRec.contour}), - follow: UC.newInput(LANG.cp_foll_s, {title:LANG.cp_foll_l, convert:UC.toFloat}), - sep: UC.newBlank({class:"pop-sep"}), - contour: UC.newBoolean(LANG.cp_cont_s, undefined, {title:LANG.cp_cont_s}), - engrave: UC.newBoolean(LANG.cp_engr_s, undefined, {title:LANG.cp_engr_l, show:() => poppedRec.contour}), - outline: UC.newBoolean(LANG.cp_outl_s, undefined, {title:LANG.cp_outl_l}), - exp: UC.newExpand("overrides"), - sep: UC.newBlank({class:"pop-sep"}), - ov_topz: UC.newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:UC.toFloat, units:true}), - ov_botz: UC.newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:UC.toFloat, units:true}), - ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, {title:LANG.ou_conv_l}), - exp_end: UC.endExpand(), - sep: UC.newBlank({class:"pop-sep"}), - menu: UC.newRow([ UC.newButton("select", func.surfaceAdd) ], {class:"ext-buttons f-row"}), - }; - - createPopOp('drill', { - tool: 'camDrillTool', - spindle: 'camDrillSpindle', - down: 'camDrillDown', - rate: 'camDrillDownSpeed', - dwell: 'camDrillDwell', - lift: 'camDrillLift', - mark: 'camDrillMark', - precision:'camDrillPrecision', - thru: 'camDrillThru', - fromTop: 'camDrillFromStockTop', - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - sep: UC.newBlank({class:"pop-sep"}), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - rate: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), - down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), - dwell: UC.newInput(LANG.cd_dwll_s, {title:LANG.cd_dwll_l, convert:UC.toFloat}), - lift: UC.newInput(LANG.cd_lift_s, {title:LANG.cd_lift_l, convert:UC.toFloat, units:true, show:() => !poppedRec.mark}), - mark: UC.newBoolean(LANG.cd_mark_s,undefined, {title:LANG.cd_mark_l, show:() => !poppedRec.fromTop}), - fromTop: UC.newBoolean(LANG.cd_ftop_s,undefined, {title:LANG.cd_ftop_l, show:() => !poppedRec.mark}), - sep: UC.newBlank({class:"pop-sep"}), - thru: UC.newInput(LANG.cd_dtru_s, {title:LANG.cd_dtru_l, convert:UC.toFloat, units:true,show:() => !poppedRec.mark}), - precision:UC.newInput(LANG.cd_prcn_s, {title:LANG.cd_prcn_l, convert:UC.toFloat, units:true,show:() => !poppedRec.mark}), - sep: UC.newBlank({class:"pop-sep", }), - actions: UC.newRow([ - UC.newButton(LANG.select, ()=>func.selectHoles(true), {title:LANG.cd_seli_l}), - UC.newButton(LANG.cd_sela_s, ()=>func.selectHoles(false), {title:LANG.cd_sela_l}) - ], {class:"ext-buttons f-col"}) - }; - - createPopOp('register', { - tool: 'camDrillTool', - spindle: 'camDrillSpindle', - down: 'camDrillDown', - rate: 'camDrillDownSpeed', - dwell: 'camDrillDwell', - lift: 'camDrillLift', - feed: 'camRegisterSpeed', - thru: 'camRegisterThru' - }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), - axis: UC.newSelect(LANG.cd_axis, {}, "regaxis"), - points: UC.newSelect(LANG.cd_points, {show:() => poppedRec.axis !== '-'}, "regpoints"), - sep: UC.newBlank({class:"pop-sep"}), - spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}), - rate: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}), - feed: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true, show:() => poppedRec.axis === '-'}), - sep: UC.newBlank({class:"pop-sep"}), - down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}), - dwell: UC.newInput(LANG.cd_dwll_s, {title:LANG.cd_dwll_l, convert:UC.toFloat, show:() => poppedRec.axis !== '-'}), - lift: UC.newInput(LANG.cd_lift_s, {title:LANG.cd_lift_l, convert:UC.toFloat, units:true, show:() => poppedRec.axis !== '-'}), - sep: UC.newBlank({class:"pop-sep"}), - thru: UC.newInput(LANG.cd_thru_s, {title:LANG.cd_thru_l, convert:UC.toFloat, units:true}), - }; - - createPopOp('flip', { - axis: 'camFlipAxis', - invert: 'camFlipInvert' - }).inputs = { - axis: UC.newSelect(LANG.cd_axis, {}, "regaxis"), - sep: UC.newBlank({class:"pop-sep", modes:MCAM, show:zBottom}), - invert: UC.newBoolean(LANG.cf_nvrt_s, undefined, {title:LANG.cf_nvrt_l, show:zBottom}), - sep: UC.newBlank({class:"pop-sep"}), - action: UC.newRow([ UC.newButton(LANG.cf_menu, func.opFlip) ], {class:"ext-buttons f-row"}) - }; - - createPopOp('gcode', { - gcode: 'camCustomGcode', - }).inputs = { - action: UC.newRow([ UC.newButton(LANG.edit, gcodeEditor()) ], {class:"ext-buttons f-row"}) - }; - - function angleTowardZUp() { - api.event.emit('tool.mesh.face-up'); - } - - api.event.on('tool.mesh.face-normal', normal => { - // console.log({ poppedRec }); - poppedRec.degrees = (Math.atan2(normal.y, normal.z) * RAD2DEG).round(2); - poppedRec.absolute = true; - func.opRender(); - updateStock(); - }); - - createPopOp('index', { - degrees: 'camIndexAxis', - absolute: 'camIndexAbs' - }).inputs = { - degrees: UC.newInput(LANG.ci_degr_s, {title:LANG.ci_degr_l, convert:UC.toFloat, bound:UC.bound(-360,360.0) }), - absolute: UC.newBoolean(LANG.ci_abso_s, undefined, {title:LANG.ci_abso_l}), - select: UC.newRow([ UC.newButton(LANG.ci_face_s, angleTowardZUp) ], {class:"ext-buttons f-row"}) - }; - - const editEnable = gcodeEditor('Laser Enable Script', 'enable'); - const editOn = gcodeEditor('Laser On Script', 'on'); - const editOff = gcodeEditor('Laser Off Script', 'off'); - - createPopOp('laser on', { - enable: 'camLaserEnable', - on: 'camLaserOn', - off: 'camLaserOff', - power: 'camLaserPower', - adapt: 'camLaserAdaptive', - adaptrp: 'camLaserAdaptMod', - flat: 'camLaserFlatten', - flatz: 'camLaserFlatZ', - minp: 'camLaserPowerMin', - maxp: 'camLaserPowerMax', - minz: 'camLaserZMin', - maxz: 'camLaserZMax', - }).inputs = { - enable: UC.newRow([ UC.newButton(LANG.enable, editEnable) ], {class:"ext-buttons f-row"}), - on: UC.newRow([ UC.newButton(LANG.on, editOn) ], {class:"ext-buttons f-row"}), - off: UC.newRow([ UC.newButton(LANG.off, editOff) ], {class:"ext-buttons f-row"}), - sep: UC.newBlank({class:"pop-sep"}), - power: UC.newInput(LANG.cl_powr_s, {title:LANG.cl_powr_l, convert:UC.toFloat, bound:UC.bound(0,1.0), show:() => !poppedRec.adapt}), - maxp: UC.newInput(LANG.cl_maxp_s, {title:LANG.cl_maxp_l, convert:UC.toFloat, bound:UC.bound(0,1.0), show:() => poppedRec.adapt}), - minp: UC.newInput(LANG.cl_minp_s, {title:LANG.cl_minp_l, convert:UC.toFloat, bound:UC.bound(0,1.0), show:() => poppedRec.adapt}), - maxz: UC.newInput(LANG.cl_maxz_s, {title:LANG.cl_maxz_l, convert:UC.toFloat, show:() => poppedRec.adapt}), - minz: UC.newInput(LANG.cl_minz_s, {title:LANG.cl_minz_l, convert:UC.toFloat, show:() => poppedRec.adapt}), - flatz: UC.newInput(LANG.cl_flaz_s, {title:LANG.cl_flaz_l, convert:UC.toFloat, show:() => poppedRec.flat}), - sep: UC.newBlank({class:"pop-sep"}), - adapt: UC.newBoolean(LANG.cl_adap_s, undefined, {title:LANG.cl_adap_l}), - adaptrp: UC.newBoolean(LANG.cl_adrp_s, undefined, {title:LANG.cl_adrp_l, show:() => poppedRec.adapt}), - flat: UC.newBoolean(LANG.cl_flat_s, undefined, {title:LANG.cl_flat_l }), - }; - - const editDisable = gcodeEditor('Laser Disable Script', 'disable'); - - createPopOp('laser off', { - disable: 'camLaserDisable', - }).inputs = { - disable: UC.newRow([ UC.newButton(LANG.disable, editDisable) ], {class:"ext-buttons f-row"}), - }; - - createPopOp('|', {}).inputs = {}; -}; - -/** - * Create a new popup operation (popOp) for a given type. - * @param {string} type - the name of the popOp to create - * @param {object} map - an object mapping popOp keys to either a string - * (representing a key in current.process) or a value (to be used as the default). - * @returns {object} the newly created popOp. - */ -function createPopOp(type, map) { - let op = popOp[type] = { - div: UC.newElement('div', { id:`${type}-op`, class:"cam-pop-op" }), - use: (rec) => { - op.rec = rec; - for (let [key, val] of Object.entries(op.inputs)) { - let type = val.type; - let from = map[key]; - let rval = rec[key]; - // fill undef entries older defines - if (type && (rval === null || rval === undefined)) { - if (typeof(from) === 'string') { - rec[key] = current.process[from]; - } else if (from !== undefined) { - rec[key] = from; - } else { - console.log('error', { key, val, type, from }); - } - } - } - API.util.rec2ui(rec, op.inputs); - op.hideshow(); - }, - using: (rec) => { - return op.rec === rec; - }, - bind: (ev) => { - API.util.ui2rec(op.rec, op.inputs); - - const settings = conf.get(); - const {tool} = new CAM.Tool(settings,op.rec.tool); //get tool by id - const opType = op.rec.type - const drillOrRegister = opType == "drill" || opType == "register" - - if ( !drillOrRegister && tool.type == "drill"){ - alerts.show(`Warning: Drills should not be used for ${opType} operations.`) - } - else if ( drillOrRegister && tool.type != "drill"){ - alerts.show(`Warning: Only drills should be used for drilling operations.`) - } - - for (let [key, val] of Object.entries(op.rec)) { - let saveTo = map[key]; - if (saveTo && typeof(key) === 'string' && !key.startsWith("~")) { - current.process[saveTo] = val; - } - } - API.conf.save(); - op.hideshow(); - }, - new: () => { - let rec = { type }; - for (let [key, src] of Object.entries(map)) { - rec[key] = typeof(src) === 'string' - ? current.process[src.replace('~','')] - : src; - } - return rec; - }, - hideshow: () => { - for (let inp of Object.values(op.inputs)) { - let parent = inp.parentElement; - if (parent && parent.setVisible && parent.__opt.show) { - parent.setVisible(parent.__opt.show(op, API.conf.get())); - } - } - }, - addNote: () => { - if (!op.note && type !== 'flip') { - const divid = `div-${++seed}`; - const noteid = `note-${++seed}`; - const div = document.createElement('div'); - div.setAttribute('id', divid); - div.classList.add('pop-tics') - op.div.appendChild(div); - div.innerHTML = h.build( - h.div([ h.label({ id: noteid }) ]) - ); - op.note = { divid, noteid }; - } - if (op.note) { - const { divid, noteid } = op.note; - const div = $(divid); - if (div) div.onclick = () => { - API.uc.prompt('Edit Note for Operation', poppedRec.note || '').then(note => { - if (note !== undefined && note !== null) { - poppedRec.note = op.x = note; - API.conf.save(); - } - func.opRender(); - }); - }; - const note = $(noteid); - if (note) note.innerText = poppedRec.note || ''; - } - }, - group: [], - }; - - /** - * @function createRecordGetter - * @description Creates a getter function for the current record. - * The getter will bind the current record to the op before returning it. - * This is useful for passing a record to a pre-slice function - * @returns {function} A getter function for the current record. - */ - op.createRecordGetter = () => { - return () => { - op.bind(); - return op.rec; - } - } - - UC.restore({ - addTo: op.div, - bindTo: op.bind, - lastDiv: op.div, - lastGroup: op.group - }); - return op; -} - -function createTabBox(iw, ic, n) { - const { track } = iw; - const { stock, bounds, process } = API.conf.get(); - const { camTabsWidth, camTabsHeight, camTabsDepth, camTabsMidline } = process; - const { camZBottom, camStockIndexed } = process; - const isIndexed = camStockIndexed; - const sz = stock.z || bounds.max.z; - const zto = sz - iw.track.top; - const zp = (camZBottom || isIndexed ? camZBottom : sz - track.box.d - zto) + (camTabsMidline ? 0 : camTabsHeight / 2); - ic.x += n.x * camTabsDepth / 2; // offset from part - ic.z -= n.y * camTabsDepth / 2; // offset swap z,y - ic.y = zp; // offset swap in world space y,z - const rot = new THREE.Quaternion().setFromAxisAngle(zaxis, Math.atan2(n.y, n.x)); - const pos = { x:ic.x, y:ic.y, z:ic.z }; - const dim = { x:camTabsDepth, y:camTabsWidth, z:camTabsHeight }; - const tab = addbox(pos, boxColor(), 'tabb', dim, { rotate: rot, opacity: boxOpacity() }); - return { pos, dim, rot, tab, width: camTabsWidth, height: camTabsHeight, stock }; -} - -function addWidgetTab(widget, rec) { - const { pos, dim, rot, id } = rec; - const tabs = widget.tabs = (widget.tabs || {}); - // prevent duplicate restore from repeated settings load calls - if (!tabs[id]) { - pos.box = addbox( - pos, boxColor(), id, - dim, { group: widget.mesh, rotate: rot, opacity: boxOpacity() } - ); - pos.box.tab = Object.assign({widget, id}, pos); - widget.adds.push(pos.box); - tabs[id] = pos; - } -} - -function restoreTabs(widgets) { - widgets.forEach(widget => { - const tabs = API.widgets.annotate(widget.id).tab || []; - tabs.forEach(rec => { - let [ x, y, z, w ] = rec.rot; - rec = Object.clone(rec); - rec.rot = new THREE.Quaternion(x, y, z, w); - addWidgetTab(widget, rec); - }); - }); -} - -function clearTabs(widget, skiprec) { - Object.values(widget.tabs || {}).forEach(rec => { - widget.adds.remove(rec.box); - widget.mesh.remove(rec.box); - }); - widget.tabs = {}; - if (!skiprec) { - delete API.widgets.annotate(widget.id).tab; - } -} - -function updateTabs() { - // update tab color and opacity - API.widgets.all().forEach(widget => { - Object.values(widget.tabs || {}).forEach(rec => { - for (let rec of widget.adds || []) { - rec.material.color = new THREE.Color(boxColor()); - rec.material.opacity = boxOpacity(); - } - }); - }); -} - -function unselectTraces(widget, skip) { - if (widget.trace_stack) { - widget.trace_stack.meshes.forEach(mesh => { - if (mesh.selected) { - func.traceToggle(mesh, skip); - } - }); - } -} - -function unselectHoles(widget) { - if (!widget.holes) return - widget.holes.forEach(hole => { - hole.selected = false - }) + createPopOps(); } function validateTools(tools) { @@ -2146,161 +880,11 @@ function validateTools(tools) { } } -function isDark() { return API.space.is_dark() }; - -function addbox() { return FDM.addbox(...arguments)}; - -function delbox() { return FDM.delbox(...arguments)}; - -function boxColor() { - return isDark() ? 0x00ddff : 0x0000dd; -} - -function boxOpacity() { - return isDark() ? 0.75 : 0.6; -} - function animate() { - isAnimate = true; - API.widgets.opacity(isParsed ? 0 : 0.75); - API.hide.slider(); + env.isAnimate = true; + api.widgets.opacity(env.isParsed ? 0 : 0.75); + api.hide.slider(); STACKS.clear(); - animFn().animate(API); - API.view.set_animate(); + animFn().animate(api); + api.view.set_animate(); } - -function updateStock() { - if (isAnimate) { - if (isIndexed) { - SPACE.world.remove(camStock); - camStock = undefined; - } - return; - } - - if (!isCamMode) { - SPACE.world.remove(camZTop); - SPACE.world.remove(camZBottom); - SPACE.world.remove(camStock); - camStock = null; - camZTop = null; - camZBottom = null; - return; - } - - api.platform.update_bounds(); - - const settings = API.conf.get(); - const widgets = API.widgets.all(); - - const { stock, process } = settings; - const { x, y, z, center } = stock; - - UI.func.animate.classList.add('disabled'); - if (camStock) { - SPACE.world.remove(camStock); - camStock = null; - } - if (x && y && z) { - UI.func.animate.classList.remove('disabled'); - { - let geo = new THREE.BoxGeometry(1, 1, 1); - let mat = new THREE.MeshBasicMaterial({ - color: 0x777777, - opacity: 0.05, - transparent: true, - side:THREE.DoubleSide - }); - camStock = new THREE.Mesh(geo, mat); - camStock.renderOrder = 2; - - let lo = 0.5; - let lidat = [ - lo, lo, lo, lo, lo,-lo, - lo, lo, lo, lo,-lo, lo, - lo, lo, lo, -lo, lo, lo, - -lo,-lo,-lo, -lo,-lo, lo, - -lo,-lo,-lo, -lo, lo,-lo, - -lo,-lo,-lo, lo,-lo,-lo, - lo, lo,-lo, -lo, lo,-lo, - lo, lo,-lo, lo,-lo,-lo, - lo,-lo,-lo, lo,-lo, lo, - lo,-lo, lo, -lo,-lo, lo, - -lo,-lo, lo, -lo, lo, lo, - -lo, lo, lo, -lo, lo,-lo - ]; - let ligeo = new THREE.BufferGeometry(); - ligeo.setAttribute('position', new THREE.BufferAttribute(lidat.toFloat32(), 3)); - let limat = new THREE.LineBasicMaterial({ color: 0xaaaaaa }); - let lines = new THREE.LineSegments(ligeo, limat); - camStock.lines = lines; - camStock.add(lines); - SPACE.world.add(camStock); - } - // fight z fighting in threejs - camStock.scale.x = x + 0.005; - camStock.scale.y = y + 0.005; - camStock.scale.z = z + 0.005; - camStock.position.x = center.x; - camStock.position.y = center.y; - camStock.position.z = center.z; - camStock.lines.material.color = - new THREE.Color(isDark() ? 0x555555 : 0xaaaaaa); - } - - SPACE.world.remove(camZTop); - if (process.camZTop && widgets.length) { - let max = { x, y, z }; - for (let w of widgets) { - max.x = Math.max(max.x, w.track.box.w); - max.y = Math.max(max.y, w.track.box.h); - max.z = Math.max(max.z, w.track.box.d); - } - let geo = new THREE.PlaneGeometry(max.x, max.y); - let mat = new THREE.MeshBasicMaterial({ - color: 0x777777, - opacity: 0.55, - transparent: true, - side:THREE.DoubleSide - }); - camZTop = new THREE.Mesh(geo, mat); - camZTop._max = max; - camZTop.renderOrder = 1; - camZTop.position.x = center.x; - camZTop.position.y = center.y; - camZTop.position.z = process.camZTop; - SPACE.world.add(camZTop); - } else { - camZTop = undefined; - } - - SPACE.world.remove(camZBottom); - if (process.camZBottom && widgets.length) { - let max = { x, y, z }; - for (let w of widgets) { - max.x = Math.max(max.x, w.track.box.w); - max.y = Math.max(max.y, w.track.box.h); - max.z = Math.max(max.z, w.track.box.d); - } - let geo = new THREE.PlaneGeometry(max.x, max.y); - let mat = new THREE.MeshBasicMaterial({ - color: 0x777777, - opacity: 0.55, - transparent: true, - side:THREE.DoubleSide - }); - camZBottom = new THREE.Mesh(geo, mat); - camZBottom._max = max; - camZBottom.renderOrder = 1; - camZBottom.position.x = center.x; - camZBottom.position.y = center.y; - camZBottom.position.z = process.camZBottom; - SPACE.world.add(camZBottom); - } else { - camZBottom = undefined; - } - - SPACE.update(); -} - -}); diff --git a/src2/kiri-mode/cam/driver-be.js b/src/kiri-mode/cam/driver-be.js similarity index 100% rename from src2/kiri-mode/cam/driver-be.js rename to src/kiri-mode/cam/driver-be.js diff --git a/src2/kiri-mode/cam/driver-fe.js b/src/kiri-mode/cam/driver-fe.js similarity index 100% rename from src2/kiri-mode/cam/driver-fe.js rename to src/kiri-mode/cam/driver-fe.js diff --git a/src/kiri-mode/cam/export.js b/src/kiri-mode/cam/export.js index d93fbe3d..b2226302 100644 --- a/src/kiri-mode/cam/export.js +++ b/src/kiri-mode/cam/export.js @@ -1,28 +1,17 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { base } from '../../geo/base.js'; -// dep: geo.base -// dep: geo.polygons -// dep: kiri-mode.cam.driver -gapp.register("kiri-mode.cam.export", (root, exports) => { - -const { base, kiri } = root; -const { polygons, util } = base; -const { driver } = kiri; -const { CAM } = driver; - -const POLY = polygons; -const PRO = CAM.process; +const { util } = base; /** * @returns {Array} gcode lines */ -CAM.export = function(print, online) { +export function cam_export(print, online) { const widget = print.widgets[0]; if (!widget) return; - console.log(print); + console.log({ cam_export: print }); const { settings } = print; const { device, tools } = settings; @@ -42,16 +31,16 @@ CAM.export = function(print, online) { space = device.gcodeSpace ? ' ' : '', isRML = device.gcodeFExt?.toLowerCase() === 'rml', stripComments = device.gcodeStrip || false, - cmdToolChange = device.gcodeChange || [ "M6 T{tool}" ], - cmdSpindle = device.gcodeSpindle || [ "M3 S{speed}" ], - cmdDwell = device.gcodeDwell || [ "G4 P{time}" ], + cmdToolChange = device.gcodeChange || ["M6 T{tool}"], + cmdSpindle = device.gcodeSpindle || ["M3 S{speed}"], + cmdDwell = device.gcodeDwell || ["G4 P{time}"], axis = { X: 'X', Y: 'Y', Z: 'Z', A: 'A', F: 'F', R: 'R', I: 'I', J: 'J' }, dev = settings.device, spro = settings.process, maxZd = spro.camFastFeedZ, maxXYd = spro.camFastFeed, decimals = base.config.gcode_decimals || 3, - pos = { x:null, y:null, z:null, a:undefined, f:null, t:null, emit:null }, + pos = { x: null, y: null, z: null, a: undefined, f: null, t: null, emit: null }, line, cidx, mode, @@ -62,18 +51,18 @@ CAM.export = function(print, online) { compact_output = false, lasering = false, laserOp, - stock = settings.stock || { }, + stock = settings.stock || {}, ztOff = stock.z - widget.track.top, bounds = widget.getBoundingBox(), zmax = stock.z, runbox = { - max: { x:-Infinity, y:-Infinity, z:-Infinity}, - min: { x:Infinity, y:Infinity, z:Infinity} + max: { x: -Infinity, y: -Infinity, z: -Infinity }, + min: { x: Infinity, y: Infinity, z: Infinity } }, offset = { x: -origin.x, - y: origin.y, - z: spro.camOriginTop ? origin.z - zmax : origin.z + y: origin.y, + z: spro.camOriginTop ? origin.z - zmax : origin.z }, scale = { x: 1, @@ -84,10 +73,10 @@ CAM.export = function(print, online) { box: runbox, tool: 0, tool_name: "unknown", - top: (offset ? dev.bedDepth : dev.bedDepth/2), - left: (offset ? 0 : -dev.bedWidth/2), - right: (offset ? dev.bedWidth : dev.bedWidth/2), - bottom: (offset ? 0 : -dev.bedDepth/2), + top: (offset ? dev.bedDepth : dev.bedDepth / 2), + left: (offset ? 0 : -dev.bedWidth / 2), + right: (offset ? dev.bedWidth : dev.bedWidth / 2), + bottom: (offset ? 0 : -dev.bedDepth / 2), time_sec: 0, time_ms: 0, time: 0 @@ -107,9 +96,9 @@ CAM.export = function(print, online) { if (spos >= 0) { const left = line.substring(0, spos); const right = '(' + line.substring(spos) - .replace(/; /g,'') - .replace(/;/g,'') + ')'; - line = [ left, right ].join(''); + .replace(/; /g, '') + .replace(/;/g, '') + ')'; + line = [left, right].join(''); } } output.append(line); @@ -134,21 +123,21 @@ CAM.export = function(print, online) { if (!array) { return; } - if (typeof(array) === 'string') { + if (typeof (array) === 'string') { array = array.split('\n'); } - for (i=0; i= 0) { line = line.substring(0, cidx).trim(); if (line.length === 0) continue; } if (line.indexOf('G20') === 0) { - factor = 1/25.4; - consts.top = (offset ? dev.bedDepth : dev.bedDepth/2) * factor; - consts.left = (offset ? 0 : -dev.bedWidth/2) * factor; - consts.right = (offset ? dev.bedWidth : dev.bedWidth/2) * factor; - consts.bottom = (offset ? 0 : -dev.bedDepth/2) * factor; + factor = 1 / 25.4; + consts.top = (offset ? dev.bedDepth : dev.bedDepth / 2) * factor; + consts.left = (offset ? 0 : -dev.bedWidth / 2) * factor; + consts.right = (offset ? dev.bedWidth : dev.bedWidth / 2) * factor; + consts.bottom = (offset ? 0 : -dev.bedDepth / 2) * factor; } else if (line.indexOf('G21') === 0) { factor = 1; } @@ -166,19 +155,18 @@ CAM.export = function(print, online) { } } - -/** - * Move to a point, applies post-processing including for laser/RML, and appends to output - * @param {Point} out.point - the point to move to - * @param {Tool} [out.tool] - the tool to move with - * @param {number} [out.speed] - the speed to move at (in mm/min) - * @param {number} [out.emit] - the emission state (0=off, 1=on) - * @param {boolean} [opt.newOp] - whether to force a new operation - * @param {number} [opt.dx] - the delta X change (if not given, uses out.point.x - pos.x) - * @param {number} [opt.dy] - the delta Y change (if not given, uses out.point.y - pos.y) - * @param {number} [opt.dz] - the delta Z change (if not given, uses out.point.z - pos.z) - * @param {number} [opt.time] - the time to move (in ms) (if not given, calculate from speed) - */ + /** + * Move to a point, applies post-processing including for laser/RML, and appends to output + * @param {Point} out.point - the point to move to + * @param {Tool} [out.tool] - the tool to move with + * @param {number} [out.speed] - the speed to move at (in mm/min) + * @param {number} [out.emit] - the emission state (0=off, 1=on) + * @param {boolean} [opt.newOp] - whether to force a new operation + * @param {number} [opt.dx] - the delta X change (if not given, uses out.point.x - pos.x) + * @param {number} [opt.dy] - the delta Y change (if not given, uses out.point.y - pos.y) + * @param {number} [opt.dz] - the delta Z change (if not given, uses out.point.z - pos.z) + * @param {number} [opt.time] - the time to move (in ms) (if not given, calculate from speed) + */ function moveTo(out, opt = {}) { let laser = out.type === 'laser'; let newpos = out.point; @@ -205,7 +193,7 @@ CAM.export = function(print, online) { consts.tool = pos.t.getNumber(); consts.tool_name = pos.t.getName(); if (!laserOp && (spro.camToolInit || toolChanges > 0)) { - filterEmit(cmdToolChange, { ...consts, spindle: newSpindle } ); + filterEmit(cmdToolChange, { ...consts, spindle: newSpindle }); } toolChanges++; } @@ -271,20 +259,20 @@ CAM.export = function(print, online) { return; } let gn; - if ( out.emit >=0 && out.emit <= 3) gn = `G${out.emit}`; + if (out.emit >= 0 && out.emit <= 3) gn = `G${out.emit}`; let speed = out.speed, - arc = out.emit == 2 || out.emit == 3, - center = out.center, - nl = (compact_output && lastGn === gn) ? [] : [gn], - dx = opt.dx || newpos.x - pos.x, - dy = opt.dy || newpos.y - pos.y, - dz = opt.dz || newpos.z - pos.z, - da = newpos.a != pos.a, - maxf = dz ? maxZd : maxXYd, - feed = Math.min(speed || maxf, maxf), - dist = Math.sqrt(dx * dx + dy * dy + dz * dz), - newFeed = feed && feed !== pos.f; - + arc = out.emit == 2 || out.emit == 3, + center = out.center, + nl = (compact_output && lastGn === gn) ? [] : [gn], + dx = opt.dx || newpos.x - pos.x, + dy = opt.dy || newpos.y - pos.y, + dz = opt.dz || newpos.z - pos.z, + da = newpos.a != pos.a, + maxf = dz ? maxZd : maxXYd, + feed = Math.min(speed || maxf, maxf), + dist = Math.sqrt(dx * dx + dy * dy + dz * dz), + newFeed = feed && feed !== pos.f; + // drop dup points (all deltas are 0) if (!arc && !(dx || dy || dz || da)) { @@ -297,7 +285,7 @@ CAM.export = function(print, online) { pos.x = newpos.x; runbox.min.x = Math.min(runbox.min.x, pos.x); runbox.max.x = Math.max(runbox.max.x, pos.x); - nl.append(space).append(axis.X).append(add0(consts.pos_x = pos.x * factor)); + nl.append(space).append(axis.X).append(add0(consts.pos_x = pos.x * factor)); } if (dy || newpos.y !== pos.y) { pos.y = newpos.y; @@ -319,9 +307,9 @@ CAM.export = function(print, online) { pos.f = feed; nl.append(space).append(axis.F).append(add0(consts.feed = feed * factor, true)); } - if(arc){ + if (arc) { nl.append(space).append(axis.I).append(add0(center.x * factor, true)) - .append(space).append(axis.J).append(add0(center.y * factor, true)); + .append(space).append(axis.J).append(add0(center.y * factor, true)); } // temp hack to support RML1 dialect from a file extensions trigger @@ -330,9 +318,9 @@ CAM.export = function(print, online) { if (newFeed) { append(`VS${feed};`); } - nl = [ axis.Z, add0(pos.x * factor), ",", add0(pos.y * factor), ",", add0(pos.z * factor), ";" ]; + nl = [axis.Z, add0(pos.x * factor), ",", add0(pos.y * factor), ",", add0(pos.z * factor), ";"]; } else { - nl = [ "PU", add0(pos.x * factor), ",", add0(pos.y * factor), ";" ]; + nl = ["PU", add0(pos.x * factor), ",", add0(pos.y * factor), ";"]; } } @@ -528,5 +516,3 @@ CAM.export = function(print, online) { print.bytes = bytes + lines - 1; print.bounds = runbox; }; - -}); diff --git a/src2/kiri-mode/cam/op-contour.js b/src/kiri-mode/cam/op-contour.js similarity index 100% rename from src2/kiri-mode/cam/op-contour.js rename to src/kiri-mode/cam/op-contour.js diff --git a/src2/kiri-mode/cam/op-drill.js b/src/kiri-mode/cam/op-drill.js similarity index 100% rename from src2/kiri-mode/cam/op-drill.js rename to src/kiri-mode/cam/op-drill.js diff --git a/src2/kiri-mode/cam/op-gcode.js b/src/kiri-mode/cam/op-gcode.js similarity index 100% rename from src2/kiri-mode/cam/op-gcode.js rename to src/kiri-mode/cam/op-gcode.js diff --git a/src2/kiri-mode/cam/op-index.js b/src/kiri-mode/cam/op-index.js similarity index 100% rename from src2/kiri-mode/cam/op-index.js rename to src/kiri-mode/cam/op-index.js diff --git a/src2/kiri-mode/cam/op-laser-off.js b/src/kiri-mode/cam/op-laser-off.js similarity index 100% rename from src2/kiri-mode/cam/op-laser-off.js rename to src/kiri-mode/cam/op-laser-off.js diff --git a/src2/kiri-mode/cam/op-laser-on.js b/src/kiri-mode/cam/op-laser-on.js similarity index 100% rename from src2/kiri-mode/cam/op-laser-on.js rename to src/kiri-mode/cam/op-laser-on.js diff --git a/src2/kiri-mode/cam/op-lathe.js b/src/kiri-mode/cam/op-lathe.js similarity index 100% rename from src2/kiri-mode/cam/op-lathe.js rename to src/kiri-mode/cam/op-lathe.js diff --git a/src2/kiri-mode/cam/op-level.js b/src/kiri-mode/cam/op-level.js similarity index 100% rename from src2/kiri-mode/cam/op-level.js rename to src/kiri-mode/cam/op-level.js diff --git a/src2/kiri-mode/cam/op-outline.js b/src/kiri-mode/cam/op-outline.js similarity index 100% rename from src2/kiri-mode/cam/op-outline.js rename to src/kiri-mode/cam/op-outline.js diff --git a/src2/kiri-mode/cam/op-pocket.js b/src/kiri-mode/cam/op-pocket.js similarity index 100% rename from src2/kiri-mode/cam/op-pocket.js rename to src/kiri-mode/cam/op-pocket.js diff --git a/src2/kiri-mode/cam/op-register.js b/src/kiri-mode/cam/op-register.js similarity index 100% rename from src2/kiri-mode/cam/op-register.js rename to src/kiri-mode/cam/op-register.js diff --git a/src2/kiri-mode/cam/op-rough.js b/src/kiri-mode/cam/op-rough.js similarity index 100% rename from src2/kiri-mode/cam/op-rough.js rename to src/kiri-mode/cam/op-rough.js diff --git a/src2/kiri-mode/cam/op-shadow.js b/src/kiri-mode/cam/op-shadow.js similarity index 100% rename from src2/kiri-mode/cam/op-shadow.js rename to src/kiri-mode/cam/op-shadow.js diff --git a/src2/kiri-mode/cam/op-trace.js b/src/kiri-mode/cam/op-trace.js similarity index 100% rename from src2/kiri-mode/cam/op-trace.js rename to src/kiri-mode/cam/op-trace.js diff --git a/src2/kiri-mode/cam/op-xray.js b/src/kiri-mode/cam/op-xray.js similarity index 100% rename from src2/kiri-mode/cam/op-xray.js rename to src/kiri-mode/cam/op-xray.js diff --git a/src2/kiri-mode/cam/op.js b/src/kiri-mode/cam/op.js similarity index 100% rename from src2/kiri-mode/cam/op.js rename to src/kiri-mode/cam/op.js diff --git a/src/kiri-mode/cam/ops.js b/src/kiri-mode/cam/ops.js index 28f6a546..2eb1dbd8 100644 --- a/src/kiri-mode/cam/ops.js +++ b/src/kiri-mode/cam/ops.js @@ -1,1952 +1,23 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -// dep: geo.base -// dep: geo.paths -// dep: geo.point -// dep: geo.polygons -// dep: geo.slicer -// dep: kiri.slice -// use: kiri-mode.cam.topo -// use: kiri-mode.cam.topo4 -gapp.register("kiri-mode.cam.ops", (root, exports) => { - -const { base, kiri } = root; -const { paths, polygons, newPoint, newPolygon, sliceConnect } = base; -const { poly2polyEmit, tip2tipEmit, segmentNormal, vertexNormal } = paths; -const { driver, newSlice } = kiri; -const { CAM } = driver; - -const DEG2RAG = Math.PI / 180; -const POLY = polygons; - -class CamOp { - constructor(state, op) { - this.state = state; - this.op = op - } - - type() { - return this.op.type; - } - - weight() { - return 1; - } - - async slice() { } - - prepare() { } -} - -class OpIndex extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice() { - let { op, state } = this; - if (!state.isIndexed) { - throw 'index op requires indexed stock'; - } - let { widget, updateSlicer, computeShadows, setAxisIndex } = state; - this.degrees = setAxisIndex(op.degrees, op.absolute); - // force recompute of topo - widget.topo = undefined; - updateSlicer(); - await computeShadows(); - } - - prepare(ops, progress) { - const { lastPoint, zmax, zclear, camOut, stock } = ops; - let last = lastPoint(); - if (last) { - // max point of stock corner radius when rotating (safe z when indexing) - const rzmax = (Math.max(stock.y, stock.z) * Math.sqrt(2)) / 2 + zclear; - const zmove = Math.max(rzmax, zmax); - // move above rotating stock - camOut(last = last.clone().setZ(zmove), 0); - // issue rotation command - camOut(last = last.clone().setY(0).setA(this.degrees), 0); - } - } -} - -class OpGCode extends CamOp { - constructor(state, op) { - super(state, op); - } - - prepare(ops, progress) { - ops.addGCode(this.op.gcode); - } -} - -class OpLaserOn extends CamOp { - constructor(state, op) { - super(state, op); - } - - prepare(ops, progress) { - const { printPoint, setPrintPoint, setTool, zmax, camOut } = ops; - this.op.silent = true; - setTool(0); - ops.addGCode(this.op.enable); - ops.setLasering(true, this.op.power); - } -} - -class OpLaserOff extends CamOp { - constructor(state, op) { - super(state, op); - } - - prepare(ops, progress) { - const { printPoint, zmax, camOut } = ops; - this.op.silent = true; - ops.addGCode(this.op.disable); - ops.setLasering(false); - camOut(printPoint.clone().setZ(zmax), 0); - } -} - -class OpLevel extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { op, state } = this; - let { settings, widget, addSlices } = state; - let { updateToolDiams, tabs, cutTabs } = state; - let { bounds, zMax, ztOff, color, tshadow } = state; - let { stock } = settings; - - let toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); - let stepOver = this.stepOver = toolDiam * op.step; - let z = zMax + ztOff - op.down; - - updateToolDiams(toolDiam); - - let points = []; - let clear = op.stock ? - [ newPolygon().centerRectangle({x:0,y:0,z:0}, stock.x, stock.y) ] : - POLY.outer(POLY.offset(tshadow, toolDiam * (op.over || 0))); - - POLY.fillArea(clear, 90, stepOver, points); - - let lines = this.lines = []; - for (let i=0; i { return { first: p.first(), last: p.last(), poly: p } }); - printPoint = tip2tipEmit(lines, printPoint, (el, point, count) => { - let poly = el.poly; - if (poly.last() === point) { - poly.reverse(); - } - poly.forEachPoint((point, pidx) => { - camOut(point.clone(), true, stepOver); - }, false); - }); - setPrintPoint(printPoint); - - newLayer(); - } -} - -class OpRough extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { op, state } = this; - let { settings, slicer, addSlices, unsafe, color } = state; - let { updateToolDiams, thruHoles, tabs, cutTabs, cutPolys } = state; - let { ztOff, zMax, shadowAt, isIndexed} = state; - let { workarea } = state; - let { process, stock } = settings; - - if (op.down <= 0) { - throw `invalid step down "${op.down}"`; - } - - let roughIn = op.inside; - let roughDown = op.down; - let roughLeave = op.leave || 0; - let roughLeaveZ = op.leavez || 0; - let roughStock = op.all && isIndexed; - let toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); - let trueShadow = process.camTrueShadow === true; - - updateToolDiams(toolDiam); - - // clear the stock above the area to be roughed out - if (workarea.top_z > workarea.top_part) { - let shadow = state.shadow.base.clone(); - let step = toolDiam * op.step; - let inset = roughStock ? - POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], step) : - POLY.offset(shadow, roughIn ? step : step + roughLeave + toolDiam / 2); - let facing = POLY.offset(inset, -step, { count: 999, flat: true }); - let zdiv = ztOff / roughDown; - let zstep = (zdiv % 1 > 0) ? ztOff / (Math.floor(zdiv) + 1) : roughDown; - if (ztOff === 0) { - // compensate for lack of z top offset in this scenario - ztOff = zstep; - } - let zsteps = Math.round(ztOff / zstep); - let camFaces = this.camFaces = []; - let zstart = zMax + ztOff - zstep; - for (let z = zstart; zsteps > 0; zsteps--) { - let slice = newSlice(); - slice.z = z; - slice.camLines = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ); - slice.output() - .setLayer("face", {face: color, line: color}) - .addPolys(slice.camLines); - addSlices(slice); - camFaces.push(slice); - z -= zstep; - } - } - - // create roughing slices - let flats = []; - let shadow = []; - let slices = []; - let indices = slicer.interval(roughDown, { - down: true, min: 0, fit: true, off: 0.01 - }); - - // shift out first (top-most) slice - indices.shift(); - - // find flats and add to indices for slicing - if (op.flats) { - let flatArea = (Math.PI * (toolDiam/2) * (toolDiam/2)) / 2; - let flats = Object.entries(slicer.zFlat) - .filter(row => row[1] > flatArea) - .map(row => row[0]) - .map(v => parseFloat(v).round(5)) - .filter(v => v >= workarea.bottom_z); - flats.forEach(v => { - if (!indices.contains(v)) { - indices.push(v); - } - }); - indices = indices.sort((a,b) => { return b - a }); - // if layer is not on a flat and next one is, - // then move this layer up to mid-point to previous layer - // this is not perfect. the best method is to interpolate - // between flats so that each step is < step down. on todo list - for (let i=1; i (parseFloat(v) - 0.01).round(5)) - .filter(v => v > 0 && indices.indexOf(v) < 0); - indices = indices.appendAll(flats).sort((a,b) => b-a); - } - - indices = indices.filter(v => v >= workarea.bottom_z); - // console.log('indices', ...indices, {zBottom}); - - let cnt = 0; - let tot = 0; - await slicer.slice(indices, { each: data => { - shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true); - if (flats.indexOf(data.z) >= 0) { - // exclude flats injected to complete shadow - return; - } - if (data.z > workarea.top_z) { - return; - } - data.shadow = trueShadow ? shadowAt(data.z) : shadow.clone(true); - data.slice.shadow = data.shadow; - slices.push(data.slice); - progress(0.25 + 0.25 * (++cnt / tot)); - }, progress: (index, total) => { - tot = total; - progress((index / total) * 0.25); - } }); - - if (trueShadow) { - shadow = state.shadow.base.clone(true); - } else { - shadow = POLY.union(shadow.appendAll(state.shadow.base), 0.01, true); - } - - // inset or eliminate thru holes from shadow - shadow = POLY.flatten(shadow.clone(true), [], true); - thruHoles.forEach(hole => { - shadow = shadow.map(p => { - if (p.isEquivalent(hole)) { - let po = POLY.offset([p], -(toolDiam / 2 + roughLeave + 0.05)); - return po ? po[0] : undefined; - } else { - return p; - } - }).filter(p => p); - }); - shadow = POLY.nest(shadow); - if (op.voids) { - // eliminate voids from shadow when "clear voids" enables - for (let s of shadow) s.inner = undefined; - } - - // shell = shadow expanded by half tool diameter + leave stock - const sadd = roughIn ? toolDiam / 2 : toolDiam / 2; - const shell = roughStock ? - POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], sadd) : - POLY.offset(shadow, sadd + roughLeave); - - slices.forEach((slice, index) => { - let offset = [shell.clone(true),slice.shadow.clone(true)].flat(); - let flat = POLY.flatten(offset, [], true); - let nest = POLY.setZ(POLY.nest(flat), slice.z); - - // inset offset array by 1/2 diameter then by tool overlap % - offset = POLY.offset(nest, [-(toolDiam / 2 + roughLeave), -toolDiam * op.step], { - minArea: Math.min(0.01, toolDiam * op.step / 4), - z: slice.z, - count: 999, - flat: true, - call: (polys, count, depth) => { - // used in depth-first path creation - polys.forEach(p => { - p.depth = depth; - if (p.inner) { - p.inner.forEach(p => p.depth = depth); - } - }); - } - }) || []; - - // add outside pass if not inside only - if (!roughIn && !roughStock) { - const outside = POLY.offset(shadow.clone(), toolDiam / 2 + roughLeave, {z: slice.z}); - if (outside) { - outside.forEach(p => p.depth = -p.depth); - offset.appendAll(outside); - } - } - - if (tabs) { - tabs.forEach(tab => { - tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); - }); - offset = cutTabs(tabs, offset, slice.z); - } - - if (!offset) return; - - if (process.camStockClipTo && stock.x && stock.y && stock.center) { - let rect = newPolygon().centerRectangle(stock.center, stock.x, stock.y); - offset = cutPolys([rect], offset, slice.z, true); - } - - // elimate double inset on inners - offset.forEach(op => { - if (op.inner) { - let pv1 = op.perimeter(); - let newinner = []; - op.inner.forEach(oi => { - let pv2 = oi.perimeter(); - let pct = pv1 > pv2 ? pv2/pv1 : pv1/pv2; - if (pct < 0.98) { - newinner.push(oi); - } - }); - op.inner = newinner; - } - }); - - slice.camLines = offset; - if (roughLeaveZ) { - // offset roughing in Z as well to minimize - // tool marks on curved surfaces - // const roughLeaveZ = 1 * Math.min(roughDown, roughLeave / 2); - slice.camLines.forEach(p => { - p.setZ(p.getZ() + roughLeaveZ); - }); - } - if (false) slice.output() - .setLayer("slice", {line: 0xaaaa00}, true) - .addPolys(slice.topPolys()) - // .setLayer("top shadow", {line: 0x0000aa}) - // .addPolys(tshadow) - // .setLayer("rough shadow", {line: 0x00aa00}) - // .addPolys(shadow) - .setLayer("rough shell", {line: 0xaa0000}) - .addPolys(shell); - progress(0.5 + 0.5 * (index / slices.length)); - }); - - let last = slices[slices.length-1]; - - if (workarea.bottom_z < 0) - for (let zneg of base.util.lerp(0, -workarea.bottom_cut, op.down)) { - if (!last) continue; - let add = last.clone(true); - add.z -= zneg; - add.camLines = last.camLines.clone(true); - add.camLines.forEach(p => p.setZ(add.z + roughLeaveZ)); - // add.tops.forEach(top => top.poly.setZ(add.z)); - // add.shadow = last.shadow.clone(true); - slices.push(add); - } - - slices.forEach(slice => { - slice.output() - .setLayer("roughing", {face: color, line: color}) - .addPolys(slice.camLines); - }); - this.sliceOut = slices.filter(slice => slice.camLines); - - addSlices(this.sliceOut); - } - - prepare(ops, progress) { - let { op, state, sliceOut, camFaces } = this; - let { setTool, setSpindle, setPrintPoint, sliceOutput, polyEmit } = ops; - let { camOut, newLayer, printPoint } = ops; - let { settings } = state; - let { process } = settings; - - let easeDown = process.camEaseDown; - let cutdir = op.ov_conv; - let depthFirst = process.camDepthFirst && !state.isIndexed; - let depthData = []; - - setTool(op.tool, op.rate, op.plunge); - setSpindle(op.spindle); - - // output the clearing of stock above roughing - for (let slice of (camFaces || [])) { - const level = []; - for (let poly of slice.camLines) { - level.push(poly); - if (poly.inner) { - poly.inner.forEach(function(inner) { - level.push(inner); - }); - } - } - // set winding specified in output - POLY.setWinding(level, cutdir, false); - poly2polyEmit(level, printPoint, (poly, index, count) => { - printPoint = polyEmit(poly, index, count, printPoint); - }); - newLayer(); - } - - // output the roughing passes - setPrintPoint(printPoint); - sliceOutput(sliceOut, { - cutdir, - depthFirst, - easeDown: op.down && easeDown ? 0.001 : 0, - progress: (n,m) => progress(n/m, "routing") - }); - } -} - -class OpOutline extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { op, state } = this; - let { settings, widget, slicer, addSlices, tshadow, thruHoles, unsafe, color } = state; - let { updateToolDiams, tabs, cutTabs, cutPolys, workarea, zMax } = state; - let { process, stock } = settings; - - if (op.down <= 0) { - throw `invalid step down "${op.down}"`; - } - let toolDiam = this.toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); - updateToolDiams(toolDiam); - - let shadow = []; - let slices = []; - let intopt = { - off: 0.01, - fit: true, - down: true, - min: Math.max(0, workarea.bottom_z), - max: workarea.top_z - }; - let indices = slicer.interval(op.down, intopt); - let trueShadow = process.camTrueShadow === true; - // shift out first (top-most) slice - indices.shift(); - // add flats to shadow - const flats = Object.keys(slicer.zFlat) - .map(v => (parseFloat(v) - 0.01).round(5)) - .filter(v => v > 0 && indices.indexOf(v) < 0); - indices = indices.appendAll(flats).sort((a,b) => b-a); - - let cnt = 0; - let tot = 0; - if (op.outside && !op.inside) { - // console.log({outline_bypass: indices, down: op.down}); - indices.forEach((ind,i) => { - if (flats.indexOf(ind) >= 0) { - // exclude flats - return; - } - let slice = newSlice(ind); - slice.shadow = shadow.clone(true); - slices.push(slice); - }); - } else - await slicer.slice(indices, { each: data => { - shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true); - if (flats.indexOf(data.z) >= 0) { - // exclude flats injected to complete shadow - return; - } - data.shadow = trueShadow ? CAM.shadowAt(widget, data.z) : shadow.clone(true); - data.slice.shadow = data.shadow; - // data.slice.tops[0].inner = data.shadow; - // data.slice.tops[0].inner = POLY.setZ(tshadow.clone(true), data.z); - slices.push(data.slice); - // data.slice.xray(); - // onupdate(0.2 + (index/total) * 0.1, "outlines"); - progress(0.5 + 0.5 * (++cnt / tot)); - }, progress: (index, total) => { - tot = total; - progress((index / total) * 0.5); - } }); - shadow = POLY.union(shadow.appendAll(state.shadow.base), 0.01, true); - - // start slices at top of stock when `clear top` enabled - if (op.top) { - let first = slices[0]; - let zlist = slices.map(s => s.z); - for (let z of indices.filter(v => v >= zMax)) { - if (zlist.contains(z)) { - continue; - } - let add = first.clone(true); - add.tops.forEach(top => top.poly.setZ(add.z)); - add.shadow = first.shadow.clone(true); - add.z = z; - slices.splice(0,0,add); - } - } - - // extend cut thru (only when z bottom is 0) - if (workarea.bottom_z < 0) { - let last = slices[slices.length-1]; - for (let zneg of base.util.lerp(0, -workarea.bottom_cut, op.down)) { - if (!last) continue; - let add = last.clone(true); - add.tops.forEach(top => top.poly.setZ(add.z)); - add.shadow = last.shadow.clone(true); - add.z -= zneg; - slices.push(add); - } - } - - slices.forEach(slice => { - let tops = slice.shadow; - - // outside only (use tshadow for entire cut) - if (op.outside) { - tops = tshadow; - } - - if (op.omitthru) { - // eliminate thru holes from shadow - for (let hole of thruHoles) { - for (let top of tops) { - if (!top.inner) continue; - top.inner = top.inner.filter(innr => { - return !innr.isEquivalent(hole, false, 0.1); - }); - } - } - } - - if (op.omitvoid) { - for (let top of tops) { - delete top.inner; - } - } - - let offset = POLY.expand(tops, toolDiam / 2, slice.z); - if (!(offset && offset.length)) { - return; - } - - // when pocket only, drop first outer poly - // if it matches the shell and promote inner polys - if (op.inside) { - let shell = POLY.expand(tops.clone(), toolDiam / 2); - offset = POLY.filter(offset, [], function(poly) { - if (poly.area() < 1) { - return null; - } - for (let sp=0; sp 0; c--){ - offset.slice().forEach(op => { - // clone removes inners but the real solution is - // to limit expanded shells to through holes - let wideCut = POLY.expand([op.clone(true)], stepover*c, slice.z, [], 1); - wideCut.forEach(cut =>{ //set order of cuts when wide - cut.order = c - if(cut.inner) cut.inner.forEach(inn =>{ inn.order = c }) - }); - wideCuts.push(...wideCut) - }); - } - offset.appendAll(wideCuts); - } - } - - if (op.dogbones && !op.wide) { - CAM.addDogbones(offset, toolDiam / 5); - } - - if (tabs) { - tabs.forEach(tab => { - tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); - }); - offset = cutTabs(tabs, offset, slice.z); - } - - if (process.camStockClipTo && stock.x && stock.y && stock.center) { - let rect = newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y); - offset = cutPolys([rect], offset, slice.z, true); - } - - // offset.xout(`slice ${slice.z}`); - slice.camLines = offset; - }); - - // when top expand fails above, it creates an empty slice - slices = slices.filter(s => s.camLines); - - // project empty up and render - for (let slice of slices) { - if (false) slice.output() - .setLayer("slice", {line: 0xaaaa00}, false) - .addPolys(slice.topPolys()) - slice.output() - .setLayer("outline", {face: color, line: color}) - .addPolys(slice.camLines); - } - - addSlices(slices); - this.sliceOut = slices; - } - - prepare(ops, progress) { - let { op, state, sliceOut } = this; - let { setTool, setSpindle, setPrintPoint } = ops; - let { polyEmit, depthOutlinePath } = ops; - let { camOut, newLayer, printPoint } = ops; - let { settings, widget } = state; - let { process, controller } = settings; - - let easeDown = process.camEaseDown; - let toolDiam = this.toolDiam; - let cutdir = op.ov_conv;//process.camConventional; - let depthFirst = process.camDepthFirst; - let depthData = []; - - setTool(op.tool, op.rate, op.plunge); - setSpindle(op.spindle); - - // printpoint becomes NaN in engine mode. not sure why but this fixes it - if(Object.values(printPoint).some(v=>Number.isNaN(v))){ - printPoint = newPoint(0,0,0); - } - - for (let slice of sliceOut) { - let polys = [], t = [], c = []; - let lines =POLY.flatten(slice.camLines) - // console.log(lines); - lines.forEach((poly)=> { - poly.order = poly.order ?? 0; - let child = poly.parent; - if (depthFirst) { poly = poly.clone(); poly.parent = child ? 1 : 0 } - if (child) c.push(poly); else t.push(poly); - poly.layer = depthData.layer; - polys.push(poly); - }); - - // set cut direction on outer polys - POLY.setWinding(t, cutdir); - // set cut direction on inner polys - POLY.setWinding(c, !cutdir); - - if (depthFirst) { - depthData.push(polys); - } else { - let orderSplit = {} - polys.forEach(poly => { - if(poly.order in orderSplit) orderSplit[poly.order].push(poly); - else orderSplit[poly.order] = [poly]; - }) - Object.entries(orderSplit) //split the polys by order - .sort((a,b) => -(a[0] - b[0] )) //sort by order (highest first) - .forEach(([order, orderPolys]) => { // emit based on closest for each order - let polyLast; - // console.log({order, orderPolys}); - printPoint = poly2polyEmit(orderPolys, printPoint, function(poly, index, count) { - - polyLast = polyEmit(poly, index, count, polyLast); - }, { swapdir: false }); - }) - newLayer(); - } - } - - if (depthFirst) { - let flatLevels = depthData.map(level => { - return POLY.flatten(level.clone(true), [], true).filter(p => !(p.depth = 0)); - }).filter(l => l.length > 0); - if (flatLevels.length && flatLevels[0].length) { - // start with the smallest polygon on the top - printPoint = flatLevels[0] - .sort((a,b) => { return a.area() - b.area() })[0] - .average(); - // experimental start of ease down - let ease = op.down && easeDown ? 0.001 : 0; - printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, false, ease); - printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, true, ease); - } - } - - setPrintPoint(printPoint); - } -} - -function createFilter(op) { - let filter = slices => slices; - if (op.filter) { - try { - const obj = eval(`( ${op.filter.join('\n')} )`); - let idx = 0; - if (obj && obj.slices) { - const nadd = []; - filter = function(slices) { - for (let slice of slices) { - if (obj.slices(slice, idx++)) { - nadd.push(slice); - } - } - return nadd; - }; - } - } catch (e) { - console.log('filter parse error', e, op.filter); - } - } - return filter; -} - -class OpContour extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { op, state } = this; - let { addSlices } = state; - let filter = createFilter(op); - // we need topo for safe travel moves when roughing and outlining - // not generated when drilling-only. then all z moves use bounds max. - // also generates x and y contouring when selected - let topo = await CAM.Topo({ - // onupdate: (update, msg) => { - onupdate: (index, total, msg) => { - progress(index / total, msg); - }, - ondone: (slices) => { - slices = filter(slices); - this.sliceOut = slices; - addSlices(slices); - }, - contour: op, - state: state - }); - // computed if set to 0 - this.tolerance = topo.tolerance; - } - - prepare(ops, progress) { - let { op, state, sliceOut } = this; - let { settings, widget } = state; - let { process } = settings; - - let { setTolerance, setTool, setSpindle, setPrintPoint } = ops; - let { camOut, polyEmit, newLayer, printPoint, lastPoint } = ops; - let { bounds, zmax } = ops; - - let toolDiam = this.toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); - let stepover = toolDiam * op.step * 2; - let depthFirst = process.camDepthFirst; - let depthData = []; - - setTool(op.tool, op.rate, process.camFastFeedZ); - setSpindle(op.spindle); - setTolerance(this.tolerance); - - printPoint = newPoint(bounds.min.x,bounds.min.y,zmax); - - for (let slice of sliceOut) { - // ignore debug slices - if (!slice.camLines) { - continue; - } - let polys = [], poly, emit; - slice.camLines.forEach(function (poly) { - if (depthFirst) poly = poly.clone(true); - polys.push({first:poly.first(), last:poly.last(), poly:poly}); - }); - if (depthFirst) { - depthData.appendAll(polys); - } else { - printPoint = tip2tipEmit(polys, printPoint, function(el, point, count) { - poly = el.poly; - if (poly.last() === point) { - poly.reverse(); - } - poly.forEachPoint(function(point, pidx) { - camOut(point.clone(), pidx > 0, stepover); - }, false); - }); - newLayer(); - } - } - - if (depthFirst) { - printPoint = tip2tipEmit(depthData, printPoint, function(el, point, count) { - let poly = el.poly; - if (poly.last() === point) { - poly.reverse(); - } - poly.forEachPoint(function(point, pidx) { - camOut(point.clone(), pidx > 0, stepover); - }, false); - newLayer(); - return lastPoint(); - }); - } - - setPrintPoint(printPoint); - } -} - -class OpLathe extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { op, state } = this; - let { addSlices } = state; - let filter = createFilter(op); - - this.topo = await CAM.Topo4({ - op, - state, - onupdate: (pct, msg) => { - progress(pct, msg); - }, - ondone: (slices) => { - slices = filter(slices); - this.slices = slices; - addSlices(slices, false); - } - }); - } - - prepare(ops, progress) { - let { op, state, slices, topo } = this; - let { settings } = state; - - let { setTool, setSpindle, setPrintPoint } = ops; - let { camOut, newLayer, printPoint } = ops; - let { zmax } = ops; - - let toolDiam = new CAM.Tool(settings, op.tool).fluteDiameter(); - let stepover = toolDiam * op.step * 2; - let rez = topo.resolution; - - setTool(op.tool, op.rate, op.plunge); - setSpindle(op.spindle); - - // start top center, X = 0, Y = 0 closest to 4th axis chuck - printPoint = newPoint(0, 0, zmax); - - for (let slice of slices) { - // ignore debug slices - if (!slice.camLines) { - continue; - } - - let last; - for (let path of slice.camLines) { - let latent; - path.forEachPoint((point, pidx) => { - if (last) { - const dz = Math.abs(last.z - point.z); - if (dz < rez) { - // latent point should still be included in - // preview b/c arcs would look like straight lines - latent = point.clone(); - return; - } - if (latent) { - camOut(latent, true, stepover); - latent = undefined; - } - } - camOut(last = point.clone(), pidx > 0, stepover); - }, false); - if (latent) { - camOut(latent, true, stepover); - } - } - - newLayer(); - } - - // move to safe height and reset A axis - let last = ops.lastPoint(); - let amax = (Math.round(last.a / 360) * 360).round(2); - // camOut(last = last.clone().setZ(zmax), 0); - // camOut(last = last.clone().setA(amax), 0); - newLayer(); - ops.addGCode([`G0 Z${zmax.round(2)}`,`G0 A${amax}`,"G92 A0"]); - - setPrintPoint(last); - } -} - -class OpTrace extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - const debug = false; - let { op, state } = this; - let { tool, rate, down, plunge, offset, offover, thru } = op; - let { ov_conv } = op; - let { settings, widget, addSlices, zThru, tabs, workarea } = state; - let { updateToolDiams, cutTabs, cutPolys, healPolys, color, shadowAt } = state; - let { process, stock } = settings; - let { camStockClipTo } = process; - if (state.isIndexed) { - throw 'trace op not supported with indexed stock'; - } - // generate tracing offsets from chosen features - let zTop = workarea.top_z; - let zBottom = workarea.bottom_z; - let sliceOut = this.sliceOut = []; - let areas = op.areas[widget.id] || []; - let camTool = new CAM.Tool(settings, tool); - let toolDiam = camTool.fluteDiameter(); - let toolOver = toolDiam * op.step; - let traceOffset = camTool.traceOffset() - let cutdir = ov_conv; - let polys = []; - let reContour = false; - let canRecontour = offset !== 'none' && down === 0; - let stockRect = stock.center && stock.x && stock.y ? - newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y) : undefined; - updateToolDiams(toolDiam); - - if (tabs) { - tabs.forEach(tab => { - tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); - }); - } - for (let arr of areas) { - let poly = newPolygon().fromArray(arr); - POLY.setWinding([ poly ], cutdir, false); - polys.push(poly); - let zs = poly.points.map(p => p.z); - let min = Math.min(...zs); - let max = Math.max(...zs); - if (max - min > 0.0001 && canRecontour) { - reContour = true; - } - } - if (false) newSliceOut(0).output() - .setLayer("polys", {line: 0xaaaa00}, false) - .addPolys(polys); - function newSliceOut(z) { - let slice = newSlice(z); - addSlices(slice); - sliceOut.push(slice); - return slice; - } - function minZ(z) { - return zBottom ? Math.max(zBottom, z - thru) : z - thru; - } - function followZ(poly) { - if (op.dogbone) { - CAM.addDogbones(poly, toolDiam / 5, !op.revbone); - } - let z = poly.getZ(); - let slice = newSliceOut(z); - slice.camTrace = { tool, rate, plunge }; - if (tabs) { - slice.camLines = cutTabs(tabs, [poly], z); - } else { - slice.camLines = [ poly ]; - } - if (camStockClipTo && stockRect) { - slice.camLines = cutPolys([stockRect], slice.camLines, z, true); - } - if (reContour) { - state.contourPolys(widget, slice.camLines); - } - slice.output() - .setLayer("trace follow", {line: color}, false) - .addPolys(slice.camLines) - } - function clearZnew(polys, z, down) { - if (down) { - // adjust step down to a value <= down that - // ends on the lowest z specified - let diff = zTop - z; - down = diff / Math.ceil(diff / down); - } - let zs = down ? base.util.lerp(zTop, z, down) : [ z ]; - let zpro = 0, zinc = 1 / (polys.length * zs.length); - for (let poly of polys) { - // newPocket(); - for (let z of zs) { - let clip = [], shadow; - shadow = shadowAt(z); - POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0); - if (op.outline) { - POLY.clearInner(clip); - } - if (clip.length === 0) { - continue; - } - let slice = newSliceOut(z); - let count = 999; - slice.camTrace = { tool, rate, plunge }; - if (toolDiam) { - const offs = [ -toolDiam / 2, -toolOver ]; - POLY.offset(clip, offs, { - count, outs: slice.camLines = [], flat:true, z, minArea: 0 - }); - } else { - // when engraving with a 0 width tip - slice.camLines = clip; - } - if (tabs) { - slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z); - } else { - slice.camLines = POLY.flatten(slice.camLines, null, true); - } - POLY.setWinding(slice.camLines, cutdir, false); - slice.output() - .setLayer("trace", {line: color}, false) - .addPolys(slice.camLines) - if (debug && shadow) slice.output() - .setLayer("trace shadow", {line: 0xff8811}, false) - .addPolys(shadow) - progress(zpro, "trace"); - zpro += zinc; - addSlices(slice); - } - } - } - function similar(v1, v2, epsilon = 0.01) { - return Math.abs(v1-v2) <= epsilon; - } - function centerPoly(p1, p2) { - // follow poly with most points - if (p2.length > p1.length) { - let t = p1; - p1 = p2; - p2 = t; - } - let np = newPolygon().setOpen(true); - for (let p of p1.points) { - let q = p2.findClosestPointTo(p); - np.push(p.midPointTo3D(q.point)); - } - return np; - } - function centerPolys(polys) { - // select open polys and sort by length - let ptst = polys.filter(p => p.isOpen()).sort((a,b) => b.perimeter() - a.perimeter()); - if (ptst.length < 2) { - return polys; - } - let pt = newPoint(0,0,0); - // ensure polys are ordered with start point closest to 0,0 - ptst.forEach(p => { - if (p.last().distTo2D(pt) < p.first().distTo2D(pt)) { - p.reverse(); - } - }); - let pout = polys.filter(p => p.isClosed()); - outer: for (let i=0,l=ptst.length; i p)); - return pout; - } - // connect selected segments if open and touching - polys = healPolys(polys); - // find center line for open polys spaced by tool diameter - polys = centerPolys(polys); - switch (op.mode) { - case "follow": - let routed = []; - poly2polyEmit(polys, newPoint(0,0,0), (poly, index, count, spoint) => { - routed.push(poly); - }); - let output = []; - for (let poly of POLY.nest(routed)) { - let offdist = offset !== 'none' ? offover : 0; - if (!offdist) - switch (offset) { - case "outside": offdist = traceOffset; break; - case "inside": offdist = -traceOffset; break; - } else if (offset === "inside") { - offdist = -offdist; - } - if (offdist) { - let pnew = POLY.offset([poly], offdist, { minArea: 0, open: true }); - if (pnew) { - poly = POLY.setZ(pnew, poly.getZ()); - } else { - continue; - } - } else { - poly = [ poly ]; - } - for (let pi of POLY.flatten(poly, [], true)) - if (down) { - let zto = minZ(pi.getZ()); - if (zThru && similar(zto,0)) { - zto -= zThru; - } - for (let z of base.util.lerp(zTop, zto, down)) { - output.push(pi.clone().setZ(z)); - } - } else { - if (thru) { - pi.setZ(pi.getZ() - thru); - } - output.push(pi); - } - if (!down && op.merge) { - let nest = POLY.nest(output); - let union = POLY.union(nest, 0, true); - output = POLY.flatten(union, [], true); - } - } - for (let poly of output) { - followZ(poly); - } - break; - case "clear": - const zbo = widget.track.top - widget.track.box.d; - let zmap = {}; - for (let poly of polys) { - let z = minZ(poly.minZ()); - if (offover) { - let pnew = POLY.offset([poly], -offover, { minArea: 0, open: true }); - if (pnew) { - poly = POLY.setZ(pnew, poly.getZ()); - } else { - continue; - } - } else { - poly = [ poly ]; - } - (zmap[z] = zmap[z] || []).appendAll(poly); - } - for (let [zv, polys] of Object.entries(zmap)) { - clearZnew(polys, parseFloat(zv), down); - } - } - } - - prepare(ops, progress) { - let { op, state } = this; - let { settings } = state; - let { setTool, setSpindle } = ops; - - setTool(op.tool, op.rate); - setSpindle(op.spindle); - for (let slice of this.sliceOut) { - ops.emitTrace(slice); - } - } -} - -class OpPocket extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - const pocket = this; - const debug = false; - let { op, state } = this; - let { tool, rate, down, plunge, expand, contour, smooth, tolerance } = op; - let { ov_botz, ov_conv } = op; - let { settings, widget, addSlices, zBottom, zThru, tabs, color } = state; - let { updateToolDiams, cutTabs, healPolys, shadowAt, workarea } = state; - let { process } = settings; - zBottom = ov_botz ? workarea.bottom_stock + ov_botz : zBottom; - // generate tracing offsets from chosen features - let sliceOut; - let pockets = this.pockets = []; - let camTool = new CAM.Tool(settings, tool); - let toolDiam = camTool.fluteDiameter(); - let toolOver = toolDiam * op.step; - let cutdir = ov_conv; - let engrave = contour && op.engrave; - let zTop = workarea.top_z; - if (contour) { - down = 0; - this.topo = await CAM.Topo({ - // onupdate: (update, msg) => { - onupdate: (index, total, msg) => { - progress((index / total) * 0.9, msg); - }, - ondone: (slices) => { - // console.log({ contour: slices }); - }, - contour: { - tool, - tolerance, - inside: true, - axis: "-" - }, - state: state - }); - } - updateToolDiams(toolDiam); - if (tabs) { - tabs.forEach(tab => { - tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); - }); - } - function newPocket() { - pockets.push(sliceOut = []); - } - function newSliceOut(z) { - let slice = newSlice(z); - sliceOut.push(slice); - return slice; - } - function clearZ(polys, z, down) { - if (down) { - // adjust step down to a value <= down that - // ends on the lowest z specified - let diff = zTop - z; - down = diff / Math.ceil(diff / down); - } - let zs = down ? base.util.lerp(zTop, z, down) : [ z ]; - if (engrave) { - toolDiam = toolOver; - } - if (contour) { - expand = engrave ? 0 : expand; - } else if (expand) { - polys = POLY.offset(polys, expand); - } - let zpro = 0, zinc = 1 / (polys.length * zs.length); - for (let poly of polys) { - newPocket(); - for (let z of zs) { - let clip = [], shadow; - if (contour) { - if (smooth) { - clip = POLY.offset(POLY.offset([ poly ], smooth), -smooth); - } else { - clip = [ poly ]; - } - } else { - shadow = shadowAt(z); - if (smooth) { - shadow = POLY.setZ(POLY.offset(POLY.offset(shadow, smooth), -smooth), z); - } - POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0); - if (op.outline) { - POLY.clearInner(clip); - } - } - if (clip.length === 0) { - continue; - } - let slice = newSliceOut(z); - let count = engrave ? 1 : 999; - slice.camTrace = { tool, rate, plunge }; - if (toolDiam) { - const offs = contour ? - [ expand || (-0.02), -toolOver ] : - [ -toolDiam / 2, -toolOver ]; - POLY.offset(clip, offs, { - count, outs: slice.camLines = [], flat:true, z, minArea: 0 - }); - } else { - // when engraving with a 0 width tip - slice.camLines = clip; - } - if (tabs) { - slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z); - } else { - slice.camLines = POLY.flatten(slice.camLines, null, true); - } - POLY.setWinding(slice.camLines, cutdir, false); - if (contour) { - slice.camLines = pocket.conform(slice.camLines, op.refine, engrave, pct => { - progress(0.9 + (zpro + zinc * pct) * 0.1, "conform"); - }); - } - slice.output() - .setLayer("pocket", {line: color}, false) - .addPolys(slice.camLines) - if (debug && shadow) slice.output() - .setLayer("pocket shadow", {line: 0xff8811}, false) - .addPolys(shadow) - if (!contour) { - progress(zpro, "pocket"); - } - zpro += zinc; - addSlices(slice); - } - } - } - let surfaces = op.surfaces[widget.id] || []; - let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4)); - // let vert = widget.getVertices().array.map(v => v.round(4)); - let outline = []; - let faces = CAM.surface_find(widget, surfaces, (op.follow || 5) * DEG2RAG); - let zmin = Infinity; - let j=0, k=faces.length; - for (let face of faces) { - let i = face * 9; - outline.push(newPolygon() - .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) - .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) - .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) - ); - } - zmin = Math.max(zBottom, zmin); - outline = POLY.union(outline, 0.0001, true); - outline = POLY.setWinding(outline, cutdir, false); - outline = healPolys(outline); - if (smooth) { - outline = POLY.offset(POLY.offset(outline, smooth), -smooth); - } - if (outline.length) { - // option to skip interior features (holes, pillars) - if (op.outline) { - POLY.clearInner(outline); - } - if (debug) newSliceOut(zmin).output() - .setLayer("pocket area", {line: 0x1188ff}, false) - .addPolys(outline) - clearZ(outline, zmin + 0.0001, down); - progress(1, "pocket"); - } - } - - // mold cam output lines to the surface of the topo offset by tool geometry - conform(camLines, refine, engrave, progress) { - const topo = this.topo; - // re-segment polygon to a higher resolution - const hirez = camLines.map(p => p.segment(topo.tolerance * 2)); - // walk points and offset from surface taking into account tool geometry - let steps = hirez.length; - let iter = 0; - for (let poly of hirez) { - for (let point of poly.points) { - point.z = engrave ? topo.zAtXY(point.x, point.y) : topo.toolAtXY(point.x, point.y); - } - progress((iter++ / steps) * 0.8); - } - steps = steps * refine; - iter = 0; - // walk points noting z deltas and smoothing z sawtooth patterns - for (let j=0; j p.midpoints(topo.tolerance * 8)); - return hirez; - } - - prepare(ops, progress) { - let { op, state, pockets } = this; - let { setTool, setSpindle, setTolerance, sliceOutput, getPrintPoint } = ops; - let { process } = state.settings; - - setTool(op.tool, op.rate); - setSpindle(op.spindle); - - if (this.topo) { - setTolerance(this.topo.tolerance); - } - - // eliminate empty pockets - pockets = pockets.filter(p => p.length); - - // pockets is an [ array of an [ array of slices ] ] - // each top level array is a pocket containing a [ z layer array of slices ] - // follow each pocket to the next closest one from previous exit - for (;;) { - let printPoint = getPrintPoint(); - let min = { - dist: Infinity, - pocket: undefined - }; - for (let pocket of pockets.filter(p => !p.used)) { - let poly = pocket[0].camLines.slice().sort((a,b) => b.area() - a.area())[0]; - if (!poly) continue; - let find = poly.findClosestPointTo(printPoint); - if (find.distance < min.dist) { - min.pocket = pocket; - min.dist = find.distance; - } - } - if (min.pocket) { - min.pocket.used = true; - sliceOutput(min.pocket, { - cutdir: op.ov_conv, - depthFirst: process.camDepthFirst && !state.isIndexed, - easeDown: op.down && process.easeDown ? op.down : 0, - progress: (n,m) => progress(n/m, "pocket") - }); - } else { - break; - } - } - } -} - -class OpDrill extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { op, state } = this; - let { settings, addSlices, widget, updateToolDiams } = state; - let { zBottom, zThru, thruHoles, color } = state; - let { drills, drillThrough} = op - - let drillTool = new CAM.Tool(settings, op.tool), - drillToolDiam = drillTool.fluteDiameter(), - sliceOut = this.sliceOut = []; - - updateToolDiams(drillToolDiam); - - const allDrills = drills[widget.id] ?? [] - // drill points to use center (average of all points) of the polygon - allDrills.forEach((drill)=> { - if(!drill.selected){ - return - } - let slice = newSlice(0); - if (op.mark) { - // replace depth with single down peck - drill.depth = op.down - } - drill.zBottom = drill.z - drill.depth; - // for thru holes, follow z thru when set - if ((op.thru>0) ) { - drill.zBottom -= op.thru; - } - const poly = newPolygon() - poly.points.push(newPoint(drill.x, drill.y, drill.z)) - poly.points.push(newPoint(drill.x, drill.y, drill.zBottom)) - // poly.points.pop(); - slice.camTrace = { tool: op.tool, rate: op.feed, plunge: op.rate }; - slice.camLines = [poly]; - - slice.output() - .setLayer("drill", {face: color, line: color}) - .addPolys(slice.camLines); - addSlices(slice); - sliceOut.push(slice); - }); - } - - prepare(ops, progress) { - let { op, state } = this; - let { settings, widget, addSlices, updateToolDiams } = state; - let { setTool, setSpindle, setDrill, emitDrills } = ops; - setTool(op.tool, undefined, op.rate); - setDrill(op.down, op.lift, op.dwell,op.thru); - setSpindle(op.spindle); - emitDrills(this.sliceOut.map(slice => slice.camLines).flat()); - } -} - -class OpRegister extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { op, state } = this; - let { settings, widget, bounds, addSlices, zMax, zThru, color } = state; - let { updateToolDiams } = state; - - let tool = new CAM.Tool(settings, op.tool); - let sliceOut = this.sliceOut = []; - - updateToolDiams(tool.fluteDiameter()); - - let { stock } = settings, - tz = widget.track.pos.z, - lx = bounds.min.x, - hx = bounds.max.x, - ly = bounds.min.y, - hy = bounds.max.y, - o3 = tool.fluteDiameter() * 2, - mx = (lx + hx) / 2, - my = (ly + hy) / 2, - mz = op.thru || zThru || 0, - dx = (stock.x - (hx - lx)) / 4, - dy = (stock.y - (hy - ly)) / 4, - dz = stock.z, - points = [], - wo = stock.z - bounds.max.z, - z1 = bounds.max.z + wo + tz, - z2 = tz - mz; - - if (!(stock.x && stock.y && stock.z)) { - return; - } - - switch (op.axis) { - case "X": - case "x": - if (op.points == 3) { - points.push(newPoint(lx - dx, my, 0)); - points.push(newPoint(hx + dx, my - o3, 0)); - points.push(newPoint(hx + dx, my + o3, 0)); - } else { - points.push(newPoint(lx - dx, my, 0)); - points.push(newPoint(hx + dx, my, 0)); - } - break; - case "Y": - case "y": - if (op.points == 3) { - points.push(newPoint(mx, ly - dy, 0)); - points.push(newPoint(mx - o3, hy + dy, 0)); - points.push(newPoint(mx + o3, hy + dy, 0)); - } else { - points.push(newPoint(mx, ly - dy, 0)); - points.push(newPoint(mx, hy + dy, 0)); - } - break; - case "-": - let o2 = o3 / 2, - x0 = lx - dx, - x1 = hx + dx, - y0 = ly - dy - o2, - y1 = hy + dy + o2, - x4 = (x1 - x0 - o2) / 4, - y4 = (y1 - y0 - o2 * 3) / 4, - poly, cp, cz; - function start(z) { - cz = z; - cp = {x:x0 + o2 * 0.5, y:y0 + o2 * 1.5}; - poly = newPolygon().add(cp.x, cp.y, z); - } - function move(dx, dy) { - cp.x += dx; - cp.y += dy; - poly.add(cp.x, cp.y, cz); - } - function rept(count, tv, fn) { - while (count-- > 0) { - fn(tv, count === 0); - tv = -tv; - } - } - for (let z of base.util.lerp(z1, z2, op.down)) { - let slice = newSlice(z); - addSlices(slice); - sliceOut.push(slice); - start(z); - rept(4, o2, oy => { - move(0, -oy); - move(x4, 0); - }); - rept(4, o2, ox => { - move(ox, 0); - move(0, y4); - }); - rept(4, o2, oy => { - move(0, oy); - move(-x4, 0); - }); - rept(4, o2, ox => { - move(-ox, 0); - move(0, -y4); - }); - poly.points.pop(); - slice.camTrace = { tool: tool.getID(), rate: op.feed, plunge: op.rate }; - slice.camLines = [ poly ]; - slice.output() - .setLayer("register", {line: color}, false) - .addPolys(slice.camLines) - } - break; - } - - if (points.length) { - let slice = newSlice(0,null), polys = []; - points.forEach(point => { - polys.push(newPolygon() - .append(point.clone().setZ(z1)) - .append(point.clone().setZ(z2))); - }); - slice.camLines = polys; - slice.output() - .setLayer("register", {face: color, line: color}) - .addPolys(polys); - addSlices(slice); - sliceOut.push(slice); - } - } - - prepare(ops, progress) { - let { op, state } = this; - let { settings, widget, addSlices, updateToolDiams } = state; - let { setTool, setSpindle, setDrill, emitDrills } = ops; - - if (op.axis === '-') { - setTool(op.tool, op.feed, op.rate); - setSpindle(op.spindle); - for (let slice of this.sliceOut) { - ops.emitTrace(slice); - } - } else { - setTool(op.tool, undefined, op.rate); - setDrill(op.down, op.lift, op.dwell); - setSpindle(op.spindle); - emitDrills(this.sliceOut.map(slice => slice.camLines).flat()); - } - } -} - -class OpXRay extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let { widget, addSlices } = this.state; - let slicer = new kiri.cam_slicer(widget); - let xrayind = Object.keys(slicer.zLine) - .map(v => parseFloat(v).round(5)) - .sort((a,b) => a-b); - let xrayopt = { each: data => { - let slice = newSlice(data.z); - slice.addTops(data.tops); - // data.tops.forEach(top => slice.addTop(top)); - slice.lines = data.lines; - slice.xray(); - addSlices(slice); - }, over: false, flatoff: 0, edges: true, openok: true }; - await slicer.slice(xrayind, xrayopt); - // xrayopt.over = true; - // slicer.slice(xrayind, xrayopt); - } -} - -/** - * Computes the Part "shadow" and attaches relevant data to the "state" object - * - * The part shadow consists of top-cown layers at which the polygon shadow changes - * shape. For curved or sloped surfaces, this is approximated and paths that clip - * to it should use the next lower layer from current Z to ensure no part collisions. - * - * The shadow at each layer is computed by top-down unioning the part outline with - * the shadow from the layer above. - * - * This operation is injected at the start of the operation chain before processing. - */ -class OpShadow extends CamOp { - constructor(state, op) { - super(state, op); - } - - async slice(progress) { - let state = this.state; - let { ops, slicer, widget, unsafe, addSlices, shadowAt } = state; - - let realOps = ops.map(rec => rec.op).filter(op => op); - let trueShadow = state.settings.process.camTrueShadow === true; - - let minStepDown = realOps - .map(op => (op.down || 3) / (trueShadow ? 1 : 3)) - .reduce((a,v) => Math.min(a, v, 1)); - - let tslices = []; - let tshadow = []; - let tzindex = slicer.interval(minStepDown, { - fit: true, off: 0.01, down: true, flats: true - }); - let skipTerrain = unsafe; - - if (skipTerrain) { - console.log("skipping terrain generation"); - tzindex = [ tzindex.pop() ]; - } - - let lsz; // only shadow up to bottom of last shadow for progressive union - let cnt = 0; - let tot = 0; - - // terrain is the "shadow stack" where index 0 = top of part - // thus array.length -1 = bottom of part - let terrain = await slicer.slice(tzindex, { each: data => { - let shadow = trueShadow ? shadowAt(data.z, lsz) : []; - tshadow = POLY.union(tshadow.slice().appendAll(data.tops).appendAll(shadow), 0.01, true); - tslices.push(data.slice); - // capture current shadow for this slice - data.slice.shadow = tshadow; - if (false) { - const slice = data.slice; - addSlices(slice); - slice.output() - .setLayer("shadow", {line: 0x888800, thin: true }) - .addPolys(POLY.setZ(tshadow.clone(true), data.z), { thin: true }); - slice.output() - .setLayer("slice", {line: 0x886622, thin: true }) - .addPolys(POLY.setZ(data.tops.clone(true), data.z), { thin: true }); - // let p1 = [], p2 = [], cp = p1; - // for (let line of data.lines) { - // cp.push(line.p1); - // cp.push(line.p2); - // cp = (cp === p1 ? p2 : p1); - // } - // slice.output() - // .setLayer("lines1", {line: 0x884444, thin: true }) - // .addLines(p1, { thin: true }); - // slice.output() - // .setLayer("lines2", {line: 0x444488, thin: true }) - // .addLines(p2, { thin: true }); - } - lsz = data.z; - progress(0.5 + 0.5 * (++cnt / tot)); - }, progress: (index, total) => { - tot = total; - progress((index / total) * 0.5); - } }); - - if (terrain.length === 0) { - throw `invalid widget shadow`; - } - - // TODO: deprecate use of separate shadow vars in state - state.center = tshadow[0].bounds.center(); - state.tshadow = tshadow; // true shadow (base of part) - state.terrain = terrain; // stack of shadow slices stored in tops - state.tslices = tslices; // raw slicer 'data' layer outputs - state.skipTerrain = skipTerrain; - - // TODO: refactor ops to use a unified shadow object - state.shadow = { - base: tshadow, // computed shadow union at base of part - stack: terrain, // stack of shadow slices - slices: tslices, // raw slicer 'data' objects - skip: skipTerrain - }; - - // identify through holes which are inner/child polygons - // on the bottom-most layer of the shadow stack (tshadow, index == 0) - state.thruHoles = tshadow.map(p => p.inner || []).flat(); - } -} - -// union triangles > z (opt cap < ztop) into polygon(s) -CAM.shadowAt = function(widget, z, ztop) { - const geo = widget.cache.geo; - const length = geo.length; - // cache faces with normals up - if (!widget.cache.shadow) { - const faces = []; - for (let i=0, ip=0; i 0.001) { - faces.push(a,b,c); - // faces.push(newPoint(...a), newPoint(...b), newPoint(...c)); - } - } - widget.cache.shadow = faces; - } - const found = []; - const faces = widget.cache.shadow; - const { checkOverUnderOn, intersectPoints } = self.kiri.cam_slicer; - for (let i=0; i ztop && b.z > ztop && c.z > ztop) { - // skip faces over top threshold - continue; - } - if (a.z < z && b.z < z && c.z < z) { - // skip faces under threshold - continue; - } else if (a.z > z && b.z > z && c.z > z) { - found.push([a,b,c]); - } else { - // check faces straddling threshold - const where = { under: [], over: [], on: [] }; - checkOverUnderOn(newPoint(a.x, a.y, a.z), z, where); - checkOverUnderOn(newPoint(b.x, b.y, b.z), z, where); - checkOverUnderOn(newPoint(c.x, c.y, c.z), z, where); - if (where.on.length === 0 && (where.over.length === 2 || where.under.length === 2)) { - // compute two point intersections and construct line - let line = intersectPoints(where.over, where.under, z); - if (line.length === 2) { - if (where.over.length === 2) { - found.push([where.over[1], line[0], line[1]]); - found.push([where.over[0], where.over[1], line[0]]); - } else { - found.push([where.over[0], line[0], line[1]]); - } - } else { - console.log({msg: "invalid ips", line: line, where: where}); - } - } - } - } - - // const lines = {}; - // function addline(p1, p2) { - // let key = p1.key < p2.key ? p1.key + ',' + p2.key : p2.key + ',' + p1.key; - // let rec = lines[key]; - // if (rec) { - // rec.count++; - // } else { - // lines[key] = { p1, p2, count: 1 }; - // } - // } - // for (let face of found) { - // addline(face[0], face[1]); - // addline(face[1], face[2]); - // addline(face[2], face[0]); - // } - // const singles = Object.entries(lines).filter(a => a[1].count === 1).map(a => a[1]); - // const loops = POLY.nest(sliceConnect(singles, z)); - - let polys = found.map(a => { - return newPolygon() - .add(a[0].x,a[0].y,a[0].z) - .add(a[1].x,a[1].y,a[1].z) - .add(a[2].x,a[2].y,a[2].z); - }); - polys = POLY.union(polys, 0.001, true); - // console.log({z, loops, polys}); - // return loops; - return polys; -}; - -CAM.OPS = CamOp.MAP = { +import { OpXRay } from './op-xray.js'; +import { OpShadow } from './op-shadow.js'; +import { OpLevel } from './op-level.js'; +import { OpRough } from './op-rough.js'; +import { OpOutline } from './op-outline.js'; +import { OpContour } from './op-contour.js'; +import { OpPocket } from './op-pocket.js'; +import { OpLathe } from './op-lathe.js'; +import { OpTrace } from './op-trace.js'; +import { OpDrill } from './op-drill.js'; +import { OpRegister } from './op-register.js'; +import { OpLaserOn } from './op-laser-on.js'; +import { OpLaserOff } from './op-laser-off.js'; +import { OpGCode } from './op-gcode.js'; +import { OpIndex } from './op-index.js'; +import { CamOp } from './op.js'; + +export const ops = { "xray": OpXRay, "shadow": OpShadow, "level": OpLevel, @@ -1964,5 +35,3 @@ CAM.OPS = CamOp.MAP = { "index": OpIndex, "flip": CamOp }; - -}); diff --git a/src/kiri-mode/cam/prepare.js b/src/kiri-mode/cam/prepare.js index 507c2f61..166a58ed 100644 --- a/src/kiri-mode/cam/prepare.js +++ b/src/kiri-mode/cam/prepare.js @@ -1,25 +1,14 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -// dep: geo.paths -// dep: geo.point -// dep: geo.polygons -// dep: kiri.render -// dep: kiri-mode.cam.driver -// dep: kiri.print -// use: kiri-mode.cam.ops -gapp.register("kiri-mode.cam.prepare", (root, exports) => { - -const { base, kiri } = root; -const { paths, polygons, newPoint, util } = base; -const { toRadians} = util -const { tip2tipEmit, poly2polyEmit, arcToPath } = paths; -const { driver, render } = kiri; -const { CAM } = driver; - -const POLY = polygons; +import { base, util } from '../../geo/base.js'; +import { tip2tipEmit, poly2polyEmit, arcToPath } from '../../geo/paths.js'; +import { newPoint } from '../../geo/point.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { render } from '../../kiri/render.js'; +import { newPrint } from '../../kiri/print.js'; +import { Tool } from './tool.js'; +const { toRadians } = util /** * DRIVER PRINT CONTRACT @@ -29,11 +18,11 @@ const POLY = polygons; * @param {Number} [index] into widget array * @param {Object} [firstPoint] starting point */ -CAM.prepare = async function(widall, settings, update) { +export async function cam_prepare(widall, settings, update) { const widgets = widall.filter(w => !w.track.ignore && !w.meta.disabled); const count = widgets.length; - const weight = 1/count; - const print = self.worker.print = kiri.newPrint(settings, widgets); + const weight = 1 / count; + const print = self.kiri_worker.current.print = newPrint(settings, widgets); print.output = []; let point; @@ -46,11 +35,11 @@ CAM.prepare = async function(widall, settings, update) { const output = print.output.filter(level => Array.isArray(level)); if (render) // allows it to run from CLI - return render.path( - output, - (progress, layer) => { - update(0.75 + progress * 0.25, "render", layer); - }, { + return render.path( + output, + (progress, layer) => { + update(0.75 + progress * 0.25, "render", layer); + }, { thin: true, print: 0, move: 0x557799, @@ -60,10 +49,10 @@ CAM.prepare = async function(widall, settings, update) { action: "milling", maxspeed: settings.process.camFastFeed || 6000 } - ); + ); }; -function prepEach(widget, settings, print, firstPoint, update) { +export function prepEach(widget, settings, print, firstPoint, update) { if (widget.camops.length === 0 || widget.meta.disabled) return; @@ -180,7 +169,7 @@ function prepEach(widget, settings, print, firstPoint, update) { function setTool(toolID, feed, plunge) { if (toolID !== lastTool) { - tool = new CAM.Tool(settings, toolID); + tool = new Tool(settings, toolID); toolType = tool.getType(); toolDiam = tool.fluteDiameter(); toolDiamMove = toolType === 'endmill' ? toolDiam : tolerance * 2; @@ -204,13 +193,13 @@ function prepEach(widget, settings, print, firstPoint, update) { function emitDrills(polys) { polys = polys.slice(); - for (;;) { + for (; ;) { let closestDist = Infinity, closestI, closest = null, dist; - for (let i=0; i down * 2) { points.push(point.clone()); point.z -= down; @@ -253,14 +242,14 @@ function prepEach(widget, settings, print, firstPoint, update) { } } camOut(point.clone().setZ(zmax)); - points.forEach(function(point, index) { + points.forEach(function (point, index) { camOut(point, 1); if (index > 0 && index < points.length - 1) { if (dwell) camDwell(dwell); if (lift) camOut(point.clone().setZ(point.z + lift), 0); } }) - camOut(point.clone().setZ(zmax),0); + camOut(point.clone().setZ(zmax), 0); newLayer(); } @@ -301,9 +290,9 @@ function prepEach(widget, settings, print, firstPoint, update) { if (lasering.flat) { point.z = (stock && stock.z ? stock.z : wztop) + lasering.flatz; } - print.addOutput(layerOut, point, power, speed, tool, {type:'laser'}); + print.addOutput(layerOut, point, power, speed, tool, { type: 'laser' }); } else { - print.addOutput(layerOut, point, emit, speed, tool, {type, center, arcPoints}); + print.addOutput(layerOut, point, emit, speed, tool, { type, center, arcPoints }); } lastPush = { point, emit, speed, tool }; return point; @@ -318,13 +307,12 @@ function prepEach(widget, settings, print, firstPoint, update) { ); } - /** * Move a point by the widget's movement offset. * @param {Point} p - point to move * @return {Point} new point with offset applied */ - function applyWidgetMovement(p){ + function applyWidgetMovement(p) { return newPoint( p.x + wmx, p.y + wmy, @@ -341,8 +329,7 @@ function prepEach(widget, settings, print, firstPoint, update) { * @param {number} opts.moveLen typically = tool diameter used to trigger terrain detection * @param {number} opts.factor speed scale factor */ - function camOut(point, emit,opts) { - + function camOut(point, emit, opts) { // console.log({point, emit, opts}) let { center = {}, @@ -386,7 +373,7 @@ function prepEach(widget, settings, print, firstPoint, update) { emit, rate, tool, - {type:"lerp"}, + { type: "lerp" }, ); } } @@ -413,7 +400,7 @@ function prepEach(widget, settings, print, firstPoint, update) { } // convert short planar moves to cuts in some cases - if (!isRough&& !isArc && isMove && deltaXY <= moveLen && deltaZ <= 0 && !lasering ) { + if (!isRough && !isArc && isMove && deltaXY <= moveLen && deltaZ <= 0 && !lasering) { let iscontour = tolerance > 0; let isflat = absDeltaZ < 0.001; // restrict this to contouring @@ -435,21 +422,21 @@ function prepEach(widget, settings, print, firstPoint, update) { } else if (isMove) { // for longer moves, check the terrain to see if we need to go up and over const bigXY = (deltaXY > moveLen && !lasering); - const bigZ = (deltaZ > toolDiam/2 && deltaXY > tolerance); + const bigZ = (deltaZ > toolDiam / 2 && deltaXY > tolerance); const midZ = (tolerance && absDeltaZ >= tolerance) && !isContour; if (bigXY || bigZ || midZ) { let maxz = getZClearPath( - terrain, - lastPoint.x - wmx, - lastPoint.y - wmy, - point.x - wmx, - point.y - wmy, - Math.max(point.z, lastPoint.z), - zadd, - maxToolDiam/2, - zclear - ), + terrain, + lastPoint.x - wmx, + lastPoint.y - wmy, + point.x - wmx, + point.y - wmy, + Math.max(point.z, lastPoint.z), + zadd, + maxToolDiam / 2, + zclear + ), maxZdelta = Math.max(maxz - point.z, maxz - lastPoint.z), mustGoUp = maxZdelta >= tolerance, clearz = maxz; @@ -483,12 +470,11 @@ function prepEach(widget, settings, print, firstPoint, update) { rate = Math.round(plungeRate + ((feedRate - plungeRate) * modifier)); cut = 1; } else { - rate = 1 / Math.hypot(deltaXY / feedRate , absDeltaZ / plungeRate ); + rate = 1 / Math.hypot(deltaXY / feedRate, absDeltaZ / plungeRate); } } - - if(isArc) { + if (isArc) { layerOut.mode = currentOp; layerOut.spindle = spindle; lastPoint = layerPush( @@ -501,9 +487,8 @@ function prepEach(widget, settings, print, firstPoint, update) { arcPoints } ); - }else{ + } else { // for g1 moves - // TODO: synthesize move speed from feed / plunge accordingly layerOut.mode = currentOp; layerOut.spindle = spindle; @@ -548,8 +533,8 @@ function prepEach(widget, settings, print, firstPoint, update) { depthData.push(polys); } else { // if not depth first, output the polys in slice order - printPoint = poly2polyEmit(polys, printPoint, function(poly, index, count) { - poly.forEachPoint(function(point, pidx, points, offset) { + printPoint = poly2polyEmit(polys, printPoint, function (poly, index, count) { + poly.forEachPoint(function (point, pidx, points, offset) { // scale speed of first cutting poly since it engages the full bit camOut(point.clone(), offset !== 0, undefined, count === 1 ? engageFactor : 1); }, poly.isClosed(), index); @@ -561,7 +546,7 @@ function prepEach(widget, settings, print, firstPoint, update) { // crucially returns true for -0 as well as other negative #s function isNeg(v) { - return v < 0 || (v === 0 && 1/v === -Infinity); + return v < 0 || (v === 0 && 1 / v === -Infinity); } if (depthFirst) { @@ -585,7 +570,7 @@ function prepEach(widget, settings, print, firstPoint, update) { * 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, */ - function generateEaseDown(fn,poly, fromPoint, degrees = 45){ + function generateEaseDown(fn, poly, fromPoint, degrees = 45) { let index = poly.findClosestPointTo(fromPoint).index, fromZ = fromPoint.z, offset = 0, @@ -641,7 +626,7 @@ function prepEach(widget, settings, print, firstPoint, update) { index++; } - return touch; + return touch; } // coming from a previous widget, use previous last point @@ -691,7 +676,7 @@ function prepEach(widget, settings, print, firstPoint, update) { }; let opSum = 0; - let opTot = widget.camops.map(op => op.weight()).reduce((a,v) => a + v); + let opTot = widget.camops.map(op => op.weight()).reduce((a, v) => a + v); for (let op of widget.camops) { setTolerance(0); @@ -720,15 +705,12 @@ function prepEach(widget, settings, print, firstPoint, update) { function emitTrace(slice) { let { tool, rate, plunge } = slice.camTrace; setTool(tool, rate, plunge); - let traceTool = new CAM.Tool(settings, tool); + let traceTool = new Tool(settings, tool); let traceToolDiam = traceTool.fluteDiameter(); printPoint = poly2polyEmit(slice.camLines, printPoint, polyEmit, { swapdir: false }); newLayer(); } - - - /** * Output a single polygon as gcode. The polygon is walked in either the * clockwise or counter-clockwise direction depending on the winding of the @@ -746,97 +728,90 @@ function prepEach(widget, settings, print, firstPoint, update) { * @returns {Point} - the last point of the polygon */ function polyEmit(poly, index, count, fromPoint) { - - let arcMax = Infinity, // no max arc radius + let arcQ = [], + arcMax = Infinity, // no max arc radius lineTolerance = 0.001; // do not consider points under 0.001mm for arcs fromPoint = fromPoint || printPoint; - let arcQ = []; arcQ.angle = [] - let closest = poly.findClosestPointTo(fromPoint); - let lastPoint = closest.point; - let startIndex = closest.index; + let lastPoint = fromPoint; + let startIndex = index; - // console.log({poly, index, count, fromPoint,startIndex}) + // console.log({poly, index, count, fromPoint, startIndex}) // scale speed of first cutting poly since it engages the full bit let scale = ((isRough || isPocket) && count === 1) ? engageFactor : 1; - - if (easeDown && poly.isClosed()) { //if doing ease-down - - let last = generateEaseDown((point,offset )=>{ //generate ease-down points - if(offset == 0) camOut(point.clone(), 0, {factor:engageFactor}); - camOut(point.clone(), 1, {factor:scale}); // and pass them to camOut + // easeDown only allowed on closed polys (that we can continue around indefinitly) + if (easeDown && poly.isClosed()) { + let closest = poly.findClosestPointTo(fromPoint); + lastPoint = closest.point; + startIndex = closest.index; + let last = generateEaseDown((point, offset) => { // generate ease-down points + if (offset == 0) camOut(point.clone(), 0, { factor: engageFactor }); + camOut(point.clone(), 1, { factor: scale }); // and pass them to camOut }, poly, fromPoint, easeAngle); lastPoint = poly.points[last]; startIndex = last; - } + } // console.log(poly,poly.isClosed(),startIndex) - - //A is first point of segment, B is last - poly.forEachSegment( ( pointA, pointB, indexA, indexB) => { + // A is first point of segment, B is last + poly.forEachSegment((pointA, pointB, indexA, indexB) => { // if(offset == 0) console.log("forEachPoint",point,pidx,points) // console.log({pointA, pointB, indexA, indexB,startIndex}) - if(indexA == startIndex){ - camOut(pointA.clone(), 0, {factor:engageFactor}); + if (indexA == startIndex) { + camOut(pointA.clone(), 0, { factor: engageFactor }); // if first point, move to and call export function - quePush(pointA); + arcQ.push(pointA); } lastPoint = arcExport(pointB, pointA); }, !poly.isClosed(), startIndex); // console.log("at end of arcExport",structuredClone(arcQ)); - if(arcQ.length > 3){ - //if few points left, emit as lines + if (arcQ.length > 3) { + // if few points left, emit as lines drainQ(); } - while (arcQ.length){ - camOut(arcQ.shift(),1); + while (arcQ.length) { + camOut(arcQ.shift(), 1); } - function quePush(point){ - arcQ.push(point); - } - - function arcExport(point,lastp){ - let dist = lastp? point.distTo2D(lastp) : 0; - if (lastp) { - if (dist >lineTolerance && lastp) { - let rec = Object.assign(point,{dist}); + function arcExport(point, lastp) { + let dist = lastp ? point.distTo2D(lastp) : 0; + if (lastp) { + if (dist > lineTolerance && lastp) { + let rec = Object.assign(point, { dist }); arcQ.push(rec); // ondebug({arcQ}); if (arcQ.length > 2) { let el = arcQ.length; let e1 = arcQ[0]; // first in arcQ - let e2 = arcQ[Math.floor(el/2)]; // mid in arcQ - let e3 = arcQ[el-1]; // last in arcQ - let e4 = arcQ[el-2]; // second last in arcQ - let e5 = arcQ[el-3]; // third last in arcQ + let e2 = arcQ[Math.floor(el / 2)]; // mid in arcQ + let e3 = arcQ[el - 1]; // last in arcQ + let e4 = arcQ[el - 2]; // second last in arcQ + let e5 = arcQ[el - 3]; // third last in arcQ let cc = util.center2d(e1, e2, e3, 1); // find center let lr = util.center2d(e3, e4, e5, 1); // find local radius let dc = 0; let radFault = false; if (lr) { - let angle = 2 * Math.asin(dist/(2*lr.r)); + let angle = 2 * Math.asin(dist / (2 * lr.r)); radFault = Math.abs(angle) > arcRes; // enforce arcRes(olution) - } else { - radFault = true; // console.log("too much angle") + radFault = true; } - if (cc) { - if ([cc.x,cc.y,cc.z,cc.r].hasNaN()) { + if ([cc.x, cc.y, cc.z, cc.r].hasNaN()) { // console.log({cc, e1, e2, e3}); } if (arcQ.length === 3) { - arcQ.center = [ cc ]; + arcQ.center = [cc]; arcQ.xSum = cc.x; arcQ.ySum = cc.y; arcQ.rSum = cc.r; @@ -844,39 +819,39 @@ function prepEach(widget, settings, print, firstPoint, update) { // check if first angles should have caused radFault let a = toRadians(arcQ[0].slopeTo(cc).angle) let b = toRadians(arcQ[1].slopeTo(cc).angle) - let angle = Math.abs(b-a) + let angle = Math.abs(b - a) - if( Math.abs(angle) > arcRes){ + if (Math.abs(angle) > arcRes) { // if so, remove first point camOut(arcQ.shift(), 1) radFault = true } } else { // check center point delta - arcQ.xSum = arcQ.center.reduce( function (t, v) { return t + v.x }, 0 ); - arcQ.ySum = arcQ.center.reduce( function (t, v) { return t + v.y }, 0 ); - arcQ.rSum = arcQ.center.reduce( function (t, v) { return t + v.r }, 0 ); + arcQ.xSum = arcQ.center.reduce(function (t, v) { return t + v.x }, 0); + arcQ.ySum = arcQ.center.reduce(function (t, v) { return t + v.y }, 0); + arcQ.rSum = arcQ.center.reduce(function (t, v) { return t + v.r }, 0); let dx = cc.x - arcQ.xSum / arcQ.center.length; let dy = cc.y - arcQ.ySum / arcQ.center.length; dc = Math.hypot(dx, dy); // delta center distance } // if new point is off the arc // if (deem || depm || desp || dc > arcTolerance || cc.r < arcMin || cc.r > arcMax || dist > cc.r) { - if ( dc * arcQ.center.length / arcQ.rSum > arcTolerance || dist > cc.r || cc.r > arcMax || radFault ) { + if (dc * arcQ.center.length / arcQ.rSum > arcTolerance || dist > cc.r || cc.r > arcMax || radFault) { // let debug = [deem, depm, desp, dc * arcQ.center.length / arcQ.rSum > arcTolerance, dist > cc.r, cc.r > arcMax, radFault]; // console.log("point off the arc,",structuredClone(arcQ),); if (arcQ.length === 4) { // not enough points for an arc, drop first point and recalc center - camOut(arcQ.shift(),1); + camOut(arcQ.shift(), 1); let tc = util.center2d(arcQ[0], arcQ[1], arcQ[2], 1); // the new center is invalid as well. drop the first point if (!tc) { - camOut(arcQ.shift(),1); + camOut(arcQ.shift(), 1); } else { - arcQ.center = [ tc ]; - let angle = 2 * Math.asin(arcQ[1].dist/(2*tc.r)); + arcQ.center = [tc]; + let angle = 2 * Math.asin(arcQ[1].dist / (2 * tc.r)); if (Math.abs(angle) > arcRes) { // enforce arcRes on initial angle - camOut(arcQ.shift(),1); + camOut(arcQ.shift(), 1); } } } else { @@ -902,7 +877,6 @@ function prepEach(widget, settings, print, firstPoint, update) { } } else { // if first point, emit and set - camOut(point, 1); // TODO disabling out of plane z moves until a better mechanism // can be built that doesn't rely on computed zpos from layer heights... @@ -912,26 +886,26 @@ function prepEach(widget, settings, print, firstPoint, update) { } return point; } - - function drainQ() { + function drainQ() { let arcPreviewRes = 64 if (!arcTolerance) { return; } + if (arcQ.length > 4) { // ondebug({arcQ}); let vec1 = new THREE.Vector2(arcQ[1].x - arcQ[0].x, arcQ[1].y - arcQ[0].y); let vec2 = new THREE.Vector2(arcQ.center[0].x - arcQ[0].x, arcQ.center[0].y - arcQ[0].y); - let clockwise = vec1.cross(vec2) < 0 - let gc = clockwise ? 2 :3 + let clockwise = vec1.cross(vec2) < 0 + let gc = clockwise ? 2 : 3 let from = arcQ[0]; let to = arcQ.peek(); let delta = from.distTo2D(to) - arcQ.xSum = arcQ.center.reduce( (t, v) => t + v.x , 0 ); - arcQ.ySum = arcQ.center.reduce( (t, v) => t + v.y , 0 ); - arcQ.rSum = arcQ.center.reduce( (t, v) => t + v.r , 0 ); + arcQ.xSum = arcQ.center.reduce((t, v) => t + v.x, 0); + arcQ.ySum = arcQ.center.reduce((t, v) => t + v.y, 0); + arcQ.rSum = arcQ.center.reduce((t, v) => t + v.r, 0); let cl = arcQ.center.length; let center = newPoint( arcQ.xSum / cl, @@ -939,32 +913,30 @@ function prepEach(widget, settings, print, firstPoint, update) { ) // console.log("draining") - - if(arcQ.length == poly.points.length ){ + if (arcQ.length == poly.points.length) { //if is a circle // generate circle // console.log("circle",{from, to,center}); - let arcPoints = arcToPath( from, from, arcPreviewRes,{ clockwise,center}) - .map(applyWidgetMovement); - camOut(from,1); - camOut(from,gc,{ center:center.sub(from), clockwise, arcPoints}); + let arcPoints = arcToPath(from, from, arcPreviewRes, { clockwise, center }) + .map(applyWidgetMovement); + camOut(from, 1); + camOut(from, gc, { center: center.sub(from), clockwise, arcPoints }); // lastPoint = to.clone(); - }else{ + } else { //if a non-circle arc - let arcPoints = arcToPath( from, to, arcPreviewRes,{ clockwise,center}) - .map(applyWidgetMovement); + let arcPoints = arcToPath(from, to, arcPreviewRes, { clockwise, center }) + .map(applyWidgetMovement); // console.log("arc") // first arc point - camOut(from,1); + camOut(from, 1); // rest of arc to final point - camOut(to,gc,{ center:center.sub(from), clockwise, arcPoints}); + camOut(to, gc, { center: center.sub(from), clockwise, arcPoints }); lastPoint = to.clone(); } - } else { //if q too short, emit as lines for (let rec of arcQ) { - camOut(rec,1); + camOut(rec, 1); } lastPoint = arcQ.peek().clone(); } @@ -972,7 +944,10 @@ function prepEach(widget, settings, print, firstPoint, update) { arcQ.center = undefined; } - // newLayer(); + if (depthFirst) { + newLayer(); + } + return lastPoint; } @@ -1014,7 +989,7 @@ function prepEach(widget, settings, print, firstPoint, update) { function depthOutlinePath(start, depth, levels, radius, emitter, dir, ease) { let bottm = depth < levels.length - 1 ? levels[levels.length - 1] : null; - let above = levels[depth-1]; + let above = levels[depth - 1]; let level = levels[depth]; if (!level) { return start; @@ -1080,7 +1055,7 @@ function getZClearPath(terrain, x1, y1, x2, y2, z, zadd, off, over) { } let maxz = z; let check = []; - for (let i=0; i -- All Rights Reserved */ -"use strict"; - -// dep: geo.line -// dep: geo.point -// dep: geo.polygons -// dep: kiri.slice -// dep: kiri-mode.cam.driver -// use: kiri-mode.cam.slicer -// use: kiri-mode.cam.slicer2 -// use: kiri-mode.cam.ops -gapp.register("kiri-mode.cam.slice", (root, exports) => { - -const { base, kiri } = root; -const { util } = base; -const { driver, newSlice, setSliceTracker } = kiri; -const { polygons, newLine, newPoint, newPolygon } = base; -const { CAM } = driver; -const { OPS } = CAM; - -const PRO = CAM.process; -const POLY = polygons; +import { base, util } from '../../geo/base.js'; +import { newLine } from '../../geo/line.js'; +import { newPoint } from '../../geo/point.js'; +import { newPolygon } from '../../geo/polygon.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { setSliceTracker } from '../../kiri/slice.js'; +import { ops as OPS } from './ops.js'; +import { Tool } from './tool.js'; +import { Slicer as cam_slicer } from './slicer.js'; /** * DRIVER SLICE CONTRACT @@ -29,7 +17,7 @@ const POLY = polygons; * @param {Widget} widget * @param {Function} output */ -CAM.slice = async function(settings, widget, onupdate, ondone) { +export async function cam_slice(settings, widget, onupdate, ondone) { let proc = settings.process, sliceAll = widget.slices = [], camOps = widget.camops = [], @@ -105,15 +93,15 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { if (tabs) { // make tab polygons tabs.forEach(tab => { - let zero = newPoint(0,0,0), + let zero = newPoint(0, 0, 0), point = newPoint(tab.pos.x, tab.pos.y, tab.pos.z), poly = newPolygon().centerRectangle(zero, tab.dim.x, tab.dim.y), - [ rx, ry, rz, rw ] = tab.rot, + [rx, ry, rz, rw] = tab.rot, m4 = new THREE.Matrix4().makeRotationFromQuaternion( new THREE.Quaternion(rx, ry, rz, rw) ); poly.points = poly.points - .map(p => new THREE.Vector3(p.x,p.y,p.z).applyMatrix4(m4)) + .map(p => new THREE.Vector3(p.x, p.y, p.z).applyMatrix4(m4)) .map(v => newPoint(v.x, v.y, v.z)); poly.move(point); tab.poly = poly; @@ -153,7 +141,7 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { } if (zMin >= bounds.max.z) { - return error(`invalid z bottom ${(zMin/units).round(3)} >= bounds z max ${(zMax/units).round(3)}`); + return error(`invalid z bottom ${(zMin / units).round(3)} >= bounds z max ${(zMax / units).round(3)}`); } let mark = Date.now(); @@ -192,12 +180,12 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { let tracker = setSliceTracker({ rotation: 0 }); function updateSlicer() { - slicer = state.slicer = new kiri.cam_slicer(widget); + slicer = state.slicer = new cam_slicer(widget); } async function computeShadows() { shadows = {}; - await new CAM.OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => { + await new OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => { // console.log('reshadow', progress.round(3)); }); } @@ -222,7 +210,7 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { minZabove = zkey; } } - let shadow = CAM.shadowAt(widget, z, minZabove); + let shadow = computeShadowAt(widget, z, minZabove); if (minZabove) { // const merge = shadow.length; // const plus = shadows[minZabove].length; @@ -258,7 +246,7 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { function addSlices(slices, addIndexing = isIndexed) { if (!Array.isArray(slices)) { - slices = [ slices ]; + slices = [slices]; } sliceAll.appendAll(slices); if (addIndexing && axisIndex !== undefined) { @@ -270,7 +258,7 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { } if (false) { - opList.push(new CAM.OPS.xray(state, { type: "xray" })); + opList.push(new OPS.xray(state, { type: "xray" })); } let activeOps = proc.ops.filter(op => !op.disabled); @@ -278,9 +266,9 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { // silently preface op list with OpShadow if (isIndexed) { if (activeOps.length === 0 || activeOps[0].type !== 'index') - opList.push(new CAM.OPS.index(state, { type: "index", index: 0 })); + opList.push(new OPS.index(state, { type: "index", index: 0 })); } else { - opList.push(new CAM.OPS.shadow(state, { type: "shadow", silent: true })); + opList.push(new OPS.shadow(state, { type: "shadow", silent: true })); } // determing # of steps and step weighting for progress bar @@ -345,7 +333,7 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { if (tabs) { state.terrain.forEach(slab => { tabs.forEach(tab => { - if (tab.pos.z + tab.dim.z/2 >= slab.z) { + if (tab.pos.z + tab.dim.z / 2 >= slab.z) { let all = [...slab.tops, tab.poly]; slab.tops = POLY.union(all, 0, true); // slab.slice.output() @@ -360,7 +348,7 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { let tabpoly = tabs ? tabs.map(tab => tab.poly) : []; let allpoly = POLY.union([...state.shadow.base, ...tabpoly], 0, true); let shadowOff = maxToolDiam < 0 ? allpoly : - POLY.offset(allpoly, [minToolDiam/2,maxToolDiam/2], { count: 2, flat: true, minArea: 0 }); + POLY.offset(allpoly, [minToolDiam / 2, maxToolDiam / 2], { count: 2, flat: true, minArea: 0 }); state.terrain.forEach(level => level.tops.appendAll(shadowOff)); widget.terrain = state.skipTerrain ? null : state.terrain; @@ -370,9 +358,9 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { ondone(); }; -CAM.addDogbones = function(poly, dist, reverse) { +export function addDogbones(poly, dist, reverse) { if (Array.isArray(poly)) { - return poly.forEach(p => CAM.addDogbones(p, dist)); + return poly.forEach(p => addDogbones(p, dist)); } let open = poly.open; let isCW = poly.isClockwise(); @@ -381,8 +369,8 @@ CAM.addDogbones = function(poly, dist, reverse) { let lastpt = oldpts[oldpts.length - 1]; let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); let length = oldpts.length + (open ? 0 : 1); - let newpts = [ ]; - for (let i=0; i (v * 10000) | 0).join(','); let point = pcache[key]; if (!point) { @@ -432,7 +420,7 @@ CAM.traces = async function(settings, widget, single) { } points[j++ % 2] = point; if (j % 2 === 0) { - let [ p0, p1 ] = points; + let [p0, p1] = points; let line = lines[k++] = newLine(p0, p1); p0.lines.push(line); p1.lines.push(line); @@ -525,10 +513,10 @@ CAM.traces = async function(settings, widget, single) { * @param {Function} onProgress - callback function to report progress * @returns {Array} list of hole centers as objects with `x`, `y`, `z`, `depth`, and `selected` properties. */ -CAM.holes = async function(settings, widget, individual, rec,onProgress) { +export async function holes(settings, widget, individual, rec, onProgress) { - let {tool,mark,precision} = rec //TODO: display some visual difference if mark is selected - let toolDiam = new CAM.Tool(settings,tool).fluteDiameter() + let { tool, mark, precision } = rec //TODO: display some visual difference if mark is selected + let toolDiam = new Tool(settings, tool).fluteDiameter() let diam = individual ? 1 : toolDiam; // sets default diameter when select individual used let proc = settings.process, @@ -547,51 +535,51 @@ CAM.holes = async function(settings, widget, individual, rec,onProgress) { zBottom = isIndexed ? camZBottom : camZBottom - zbOff; - let slicerOpts = {flatoff: 0.001} - let slicer = new kiri.cam_slicer(widget,slicerOpts); - let zFlats = Object.keys(slicer.zFlat).map(Number).map(z=>[z,z-0.002]).flat() + let slicerOpts = { flatoff: 0.001 } + let slicer = new cam_slicer(widget, slicerOpts); + let zFlats = Object.keys(slicer.zFlat).map(Number).map(z => [z, z - 0.002]).flat() - precision = Math.max( 0, precision ) + precision = Math.max(0, precision) let intervals = (precision == 0) ? [] : slicer.interval( precision, - { + { fit: false, off: -0.01, flats: true } ) - let zees = [...zFlats,...intervals] + let zees = [...zFlats, ...intervals] let indices = [...new Set(zees .map(kv => parseFloat(kv).round(5)) .filter(z => z !== null) )] let centerDiff = diam * 0.1, - area = (diam/2) * (diam/2) * Math.PI, + area = (diam / 2) * (diam / 2) * Math.PI, circles = [], slices = []; function onEach(slice) { slices.push(slice); } - let opts = { each: onEach, progress: (num,total)=> onProgress(num/total*0.5,"slice") }; + let opts = { each: onEach, progress: (num, total) => onProgress(num / total * 0.5, "slice") }; await slicer.slice(indices, opts); let shadowedDrills = false; // console.log("slices",slices) - for (let [i,slice] of slices.entries()) { - for(let top of slice.tops){ + for (let [i, slice] of slices.entries()) { + for (let top of slice.tops) { // console.log("slicing",slice.z,top) - slice.shadow = CAM.shadowAt(widget, slice.z, 0) + slice.shadow = computeShadowAt(widget, slice.z, 0) let inner = top.inner; if (!inner) { //no holes continue; } for (let poly of inner) { - if ( poly.points.length < 7 ) continue; + if (poly.points.length < 7) continue; let center = poly.calcCircleCenter(); center.area = poly.area(); center.overlapping = [center] center.depth = 0; // console.log("center",center) - if ( poly.circularity() < 0.985 ){ + if (poly.circularity() < 0.985) { // if not circular, don't add to holes continue; } @@ -601,11 +589,11 @@ CAM.holes = async function(settings, widget, individual, rec,onProgress) { continue; } let overlap = false; - for (let [i,circle] of circles.entries()) { + for (let [i, circle] of circles.entries()) { let dist = circle.distTo2D(center) // //if on the same xy point, - if (dist <= centerDiff ) { + if (dist <= centerDiff) { // console.log("overlap",center,circle); circle.overlapping.push(center); // if overlapping, don't add and continue @@ -616,41 +604,41 @@ CAM.holes = async function(settings, widget, individual, rec,onProgress) { if (!overlap) circles.push(center); } } - onProgress(0.5+(i/slices.length*0.25),"recognize circles") + onProgress(0.5 + (i / slices.length * 0.25), "recognize circles") } let drills = [] - for (let [i,c] of circles.entries()) { + for (let [i, c] of circles.entries()) { let overlapping = c.overlapping let last = overlapping.shift(); while (overlapping.length) { let circ = overlapping.shift(); let aveArea = (circ.area + last.area) / 2; - let areaDelta = Math.abs(circ.area -last.area) - if (areaDelta < aveArea * 0.05){ // if area delta less than 5% of average area - //keep top circle selected - last.depth = last.z - circ.z; - }else{ // if not the same area - //push and move on - drills.push(last); - last = circ; + let areaDelta = Math.abs(circ.area - last.area) + if (areaDelta < aveArea * 0.05) { // if area delta less than 5% of average area + //keep top circle selected + last.depth = last.z - circ.z; + } else { // if not the same area + //push and move on + drills.push(last); + last = circ; } } if (last.depth != 0) drills.push(last) //add last circle - onProgress(0.75+(i/circles.length*0.25),"assemble holes") + onProgress(0.75 + (i / circles.length * 0.25), "assemble holes") } - drills.forEach( h=>{ - if(rec.fromTop){ + drills.forEach(h => { + if (rec.fromTop) { // set z top if selected h.depth += wztop - h.z h.z = wztop } delete h.overlapping //for encoding h.diam = toolDiam // for mesh generation - h.selected = (!individual && Math.abs(h.area - area) <= area * 0.05 ); //for same size selection - }) + h.selected = (!individual && Math.abs(h.area - area) <= area * 0.05); //for same size selection + }) // console.log("unfiltered circles",circles) // console.log("drills",drills) @@ -661,13 +649,13 @@ CAM.holes = async function(settings, widget, individual, rec,onProgress) { } function cutTabs(tabs, offset, z, inter) { - tabs = tabs.filter(tab => z < tab.pos.z + tab.dim.z/2).map(tab => tab.off).flat(); + tabs = tabs.filter(tab => z < tab.pos.z + tab.dim.z / 2).map(tab => tab.off).flat(); return cutPolys(tabs, offset, z, false); } function cutPolys(polys, offset, z, inter) { let noff = []; - offset.forEach(op => noff.appendAll( op.cut(POLY.union(polys, 0, true), inter) )); + offset.forEach(op => noff.appendAll(op.cut(POLY.union(polys, 0, true), inter))); return healPolys(noff); } @@ -679,7 +667,7 @@ function contourPolys(widget, polys) { raycaster.ray.origin.set(point.x, point.y, 10000); const intersects = raycaster.intersectObject(widget.mesh, false); const firstHit = intersects[0] || null; - point.z = firstHit.point.z; + if (firstHit) point.z = firstHit.point.z; } } } @@ -694,12 +682,12 @@ function healPolys(noff) { if (noff.length > 1) { let heal = 0; // heal/rejoin open segments that share endpoints - outer: for(;; heal++) { + outer: for (; ; heal++) { let ntmp = noff, tlen = ntmp.length; - for (let i=0; i z (opt cap < ztop) into polygon(s) +export function computeShadowAt(widget, z, ztop) { + const geo = widget.cache.geo; + const length = geo.length; + // cache faces with normals up + if (!widget.cache.shadow) { + const faces = []; + for (let i = 0, ip = 0; i < length; i += 3) { + const a = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); + const b = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); + const c = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); + const n = THREE.computeFaceNormal(a, b, c); + if (n.z > 0.001) { + faces.push(a, b, c); + // faces.push(newPoint(...a), newPoint(...b), newPoint(...c)); + } + } + widget.cache.shadow = faces; + } + const found = []; + const faces = widget.cache.shadow; + const { checkOverUnderOn, intersectPoints } = cam_slicer; + for (let i = 0; i < faces.length;) { + const a = faces[i++]; + const b = faces[i++]; + const c = faces[i++]; + let where = undefined; + if (ztop && a.z > ztop && b.z > ztop && c.z > ztop) { + // skip faces over top threshold + continue; + } + if (a.z < z && b.z < z && c.z < z) { + // skip faces under threshold + continue; + } else if (a.z > z && b.z > z && c.z > z) { + found.push([a, b, c]); + } else { + // check faces straddling threshold + const where = { under: [], over: [], on: [] }; + checkOverUnderOn(newPoint(a.x, a.y, a.z), z, where); + checkOverUnderOn(newPoint(b.x, b.y, b.z), z, where); + checkOverUnderOn(newPoint(c.x, c.y, c.z), z, where); + if (where.on.length === 0 && (where.over.length === 2 || where.under.length === 2)) { + // compute two point intersections and construct line + let line = intersectPoints(where.over, where.under, z); + if (line.length === 2) { + if (where.over.length === 2) { + found.push([where.over[1], line[0], line[1]]); + found.push([where.over[0], where.over[1], line[0]]); + } else { + found.push([where.over[0], line[0], line[1]]); + } + } else { + console.log({ msg: "invalid ips", line: line, where: where }); + } + } + } + } + + let polys = found.map(a => { + return newPolygon() + .add(a[0].x, a[0].y, a[0].z) + .add(a[1].x, a[1].y, a[1].z) + .add(a[2].x, a[2].y, a[2].z); + }); + + polys = POLY.union(polys, 0.001, true); + + return polys; +} diff --git a/src/kiri-mode/cam/slicer.js b/src/kiri-mode/cam/slicer.js index ab4b4317..2a08bb42 100644 --- a/src/kiri-mode/cam/slicer.js +++ b/src/kiri-mode/cam/slicer.js @@ -1,34 +1,32 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { base, util } from '../../geo/base.js'; +import { broker } from '../../moto/broker.js'; +import { decode, decodePointArray, encode, encodePointArray } from '../../kiri/codec.js'; +import { newLine, newOrderedLine } from '../../geo/line.js'; +import { newPoint } from '../../geo/point.js'; +import { newSlice } from '../../kiri/slice.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { slicer } from '../../geo/slicer.js'; -/** - * Slicing engine used by CAM - */ -// dep: kiri.slice -// dep: moto.broker -gapp.register("kiri-mode.cam.slicer", [], (root, exports) => { - -const { base, kiri, moto } = root; -const { config, util, polygons, newOrderedLine, newPoint, newLine } = base; -const { sliceConnect, sliceDedup } = base; -const { newSlice } = kiri; - -const POLY = polygons; +const { sliceConnect, sliceDedup } = slicer; +const { config } = base; const timing = false; -const begin = function() { +const begin = function () { if (timing) console.time(...arguments); }; -const end = function() { +const end = function () { if (timing) console.timeEnd(...arguments); }; -class Slicer { +export class Slicer { constructor(widget, options = { zlist: true, zline: true, lines: false }) { + this.minions = self.kiri_worker.minions; this.options = {}; this.setOptions(options); + if (widget) { this.setPoints(widget.getGeoVertices({ unroll: true, translate: true })); this.computeFeatures(); @@ -76,15 +74,15 @@ class Slicer { zList[z] = (zList[z] || 0) + 1; } - let p1 = newPoint(0,0,0), - p2 = newPoint(0,0,0), - p3 = newPoint(0,0,0); + let p1 = newPoint(0, 0, 0), + p2 = newPoint(0, 0, 0), + p3 = newPoint(0, 0, 0); // for (let i = 0, il = points.length; i < il; i++) { // points[i] = points[i].round(5); // } - for (let i = 0, il = points.length; i < il; ) { + for (let i = 0, il = points.length; i < il;) { p1.set(points[i++], points[i++], points[i++]); p2.set(points[i++], points[i++], points[i++]); p3.set(points[i++], points[i++], points[i++]); @@ -102,7 +100,7 @@ class Slicer { if (p1.z === p2.z && p2.z === p3.z) { // detect zFlat faces to avoid slicing directly on them let zkey = p1.z.toFixed(5), - area = Math.abs(util.area2(p1,p2,p3)) / 2; + area = Math.abs(util.area2(p1, p2, p3)) / 2; if (!zFlat[zkey]) { zFlat[zkey] = area; } else { @@ -157,7 +155,7 @@ class Slicer { // sort Z and offset when on a flat const flatoff = util.numOrDefault(opt.flatoff, 0.01); - zs = zs.sort((a,b) => a-b).map(z => { + zs = zs.sort((a, b) => a - b).map(z => { if (!flatoff) { return z; } @@ -185,13 +183,13 @@ class Slicer { b += step; } - let p1 = newPoint(0,0,0), - p2 = newPoint(0,0,0), - p3 = newPoint(0,0,0), + let p1 = newPoint(0, 0, 0), + p2 = newPoint(0, 0, 0), + p3 = newPoint(0, 0, 0), ep = 0.001; begin("create buckets"); - for (let i = 0, il = points.length; i < il; ) { + for (let i = 0, il = points.length; i < il;) { p1.set(points[i++], points[i++], points[i++]); p2.set(points[i++], points[i++], points[i++]); p3.set(points[i++], points[i++], points[i++]); @@ -219,7 +217,7 @@ class Slicer { // console.log({ zs, zlen, count, step, buckets, flatoff }); begin("slicing"); - const { minions } = kiri; + const { minions } = this; const threaded = minions && minions.running; const sliceFn = (threaded ? this.sliceBucketMinion : this.sliceBucket).bind(this); const track = { count: 0, total: zs.length }; @@ -238,7 +236,7 @@ class Slicer { } const data = (await Promise.all(promises)).flat(); - data.sort((a,b) => b.z - a.z).forEach((rec, i) => { + data.sort((a, b) => b.z - a.z).forEach((rec, i) => { rec.tops = rec.polys; rec.slice = newSlice(rec.z).addTops(rec.tops); if (opt.each) { @@ -253,9 +251,8 @@ class Slicer { } async sliceBucketMinion(bucket, opt, oneach) { - const { decode, decodePointArray } = kiri.codec; const slices = (await new Promise(resolve => { - kiri.minions.queue({ + this.minions.queue({ cmd: "cam_slice", opt: Object.clone(opt), bucket, @@ -300,16 +297,16 @@ class Slicer { // const { points } = this, const points = indices, - p1 = newPoint(0,0,0), - p2 = newPoint(0,0,0), - p3 = newPoint(0,0,0); + p1 = newPoint(0, 0, 0), + p2 = newPoint(0, 0, 0), + p3 = newPoint(0, 0, 0); - for (let index = 0; index < points.length; ) { + for (let index = 0; index < points.length;) { p1.set(points[index++], points[index++], points[index++]); p2.set(points[index++], points[index++], points[index++]); p3.set(points[index++], points[index++], points[index++]); - let where = {under: [], over: [], on: []}; + let where = { under: [], over: [], on: [] }; checkOverUnderOn(p1, z, where); checkOverUnderOn(p2, z, where); checkOverUnderOn(p3, z, where); @@ -337,7 +334,7 @@ class Slicer { if (line.length === 2) { lines.push(makeZLine(phash, line[0], line[1])); } else { - console.log({msg: "invalid ips", line: line, where: where}); + console.log({ msg: "invalid ips", line: line, where: where }); } } } @@ -347,7 +344,8 @@ class Slicer { lines = sliceDedup(lines, debug); } - return lines.length ? { z, + return lines.length ? { + z, lines: opt.lines !== false ? lines : undefined, polys: links ? POLY.nest(sliceConnect(lines, opt, debug)) : undefined } : null; @@ -370,12 +368,12 @@ class Slicer { step = (zmax - zmin) / count; } if (opt.down) { - for (let i=0; i { - return opt.down ? b-a : a-b; + array.appendAll(add).sort((a, b) => { + return opt.down ? b - a : a - b; }); } // filter duplicate values - array = array.map(v => v.round(5)).filter((e,i,a) => i < 1 || a[i-1] !== a[i]); + array = array.map(v => v.round(5)).filter((e, i, a) => i < 1 || a[i - 1] !== a[i]); // return array.map(v => Math.abs(parseFloat(v.toFixed(5)))); return array.map(v => parseFloat(v.toFixed(5))); @@ -474,7 +472,7 @@ function getCachedPoint(phash, p) { function makeZLine(phash, p1, p2, coplanar, edge) { p1 = getCachedPoint(phash, p1.clone()); p2 = getCachedPoint(phash, p2.clone()); - let line = newOrderedLine(p1,p2); + let line = newOrderedLine(p1, p2); line.coplanar = coplanar || false; line.edge = edge || false; return line; @@ -483,9 +481,7 @@ function makeZLine(phash, p1, p2, coplanar, edge) { Slicer.checkOverUnderOn = checkOverUnderOn; Slicer.intersectPoints = intersectPoints; -kiri.cam_slicer = Slicer; - -moto.broker.subscribe("minion.started", msg => { +broker.subscribe("minion.started", msg => { const { funcs, cache, reply, log } = msg; funcs.cam_slice_init = () => { @@ -498,7 +494,6 @@ moto.broker.subscribe("minion.started", msg => { funcs.cam_slice = (data, seq) => { const { bucket, opt } = data; - const { encode, encodePointArray } = kiri.codec; // log({ slice: bucket, opt }); cache.slicer.sliceBucket(bucket, opt, slice => { // console.log({ slice }); @@ -514,5 +509,3 @@ moto.broker.subscribe("minion.started", msg => { }); }; }); - -}); diff --git a/src/kiri-mode/cam/slicer2.js b/src/kiri-mode/cam/slicer2.js index 89452dac..5fb5c4a4 100644 --- a/src/kiri-mode/cam/slicer2.js +++ b/src/kiri-mode/cam/slicer2.js @@ -1,16 +1,12 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { base } from '../../geo/base.js'; +import { newPoint } from '../../geo/point.js'; +import { newOrderedLine } from '../../geo/line.js'; -/** - * Slicing engine used by CAM Topo - */ -gapp.register("kiri-mode.cam.slicer2", [], (root, exports) => { +const { config } = base; -const { base, kiri } = root; -const { config, newOrderedLine, newPoint } = base; - -class Slicer { +export class Slicer { static intersectPoints = intersectPoints; static checkOverUnderOn = checkOverUnderOn; @@ -283,7 +279,7 @@ function removeDuplicateLines(lines, debug) { // find new endpoints that are not shared point let p1 = l1.p1 != point ? l1.p1 : l1.p2, p2 = l2.p1 != point ? l2.p1 : l2.p2, - newline = base.newOrderedLine(p1,p2); + newline = newOrderedLine(p1,p2); // remove deleted lines from associated points p1.group.remove(l1); p1.group.remove(l2); @@ -320,7 +316,3 @@ function removeDuplicateLines(lines, debug) { return output; } - -kiri.topo_slicer = Slicer; - -}); diff --git a/src/kiri-mode/cam/tool.js b/src/kiri-mode/cam/tool.js index a7a8d9fa..b4aab164 100644 --- a/src/kiri-mode/cam/tool.js +++ b/src/kiri-mode/cam/tool.js @@ -1,14 +1,5 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -// dep: kiri-mode.cam.driver -gapp.register("kiri-mode.cam.tools", [], (root, exports) => { - -const { kiri } = root; -const { driver } = kiri; -const { CAM } = driver; - const HPI = Math.PI/2; const RAD2DEG = 180/Math.PI; @@ -77,11 +68,11 @@ class Tool { setTaperLengthFromAngle(angle) { const rad = (this.flute_diam - this.taper_tip) / 2; - return this.flute_len = CAM.calcTaperLength(rad, angle); + return this.flute_len = calcTaperLength(rad, angle); } getTaperAngle() { - return CAM.calcTaperAngle((this.flute_diam - this.taper_tip) / 2, this.flute_len); + return calcTaperAngle((this.flute_diam - this.taper_tip) / 2, this.flute_len); } shaftLength() { @@ -197,18 +188,21 @@ class Tool { } } -CAM.Tool = Tool; - -CAM.calcTaperAngle = function(rad, len) { +function calcTaperAngle(rad, len) { return (Math.atan(rad / len) * RAD2DEG); }; -CAM.calcTaperLength = function(rad, angle) { +function calcTaperLength(rad, angle) { return (rad / Math.tan(angle)); }; -CAM.getToolDiameter = function(settings, id) { - return new CAM.Tool(settings, id).fluteDiameter(); +function getToolDiameter(settings, id) { + return new Tool(settings, id).fluteDiameter(); }; -}); +export { + Tool, + calcTaperAngle, + calcTaperLength, + getToolDiameter +}; diff --git a/src/kiri-mode/cam/tools.js b/src/kiri-mode/cam/tools.js index 7a7ba8c9..33fc3aab 100644 --- a/src/kiri-mode/cam/tools.js +++ b/src/kiri-mode/cam/tools.js @@ -1,353 +1,344 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { $ } from '../../moto/webui.js'; +import { api } from '../../kiri/api.js'; +import { consts } from '../../kiri/consts.js'; +import { settings as setconf } from '../../kiri/settings.js'; -// dep: kiri.api -// dep: kiri.consts -// dep: kiri.settings -gapp.register("kiri-mode.cam.tools", (root, exports) => { +const DEG2RAD = Math.PI / 180; - 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']; +let { MODES } = consts, + DOC = document, + selectedTool = null, + editTools = null, + maxTool = 0, + toolNames = ['endmill','ballmill','tapermill','drill']; - api.show.tools = showTools; +function settings() { + return api.conf.get(); +} - // extend API - Object.assign(api.tool, { - update: updateTool +function renderTools() { + api.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) }; + api.ui.toolSelect.appendChild(opt); + }); +} + +function selectTool(tool) { + const { ui } = api; + 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(' ')}>`); + }); + return tags.join(''); +} + +function renderTool(tool) { + const { ui } = api; + 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 = ''; + 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); +} + +export function updateTool(ev) { + const { ui } = api; + 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; + api.ui.toolsSave.disabled = !changed; +} + +export function showTools() { + if (api.mode.get_id() !== MODES.CAM) return; + setconf.sync.get().then(_showTools); +} + +function _showTools() { + const { ui } = api; + + editTools = settings().tools.slice().sort((a,b) => { + return a.name > b.name ? 1 : -1; }); - function settings() { - return api.conf.get(); - } + setToolChanged(false); - 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(' ')}>`); - }); - 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 = ''; - 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; + 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); - 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; + 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 { - ui.toolAdd.onclick(); + 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(); + setconf.sync.put(); + }; + ui.toolsImport.onclick = (ev) => api.event.import(ev); + ui.toolsExport.onclick = () => { + api.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`); + }); + }; - api.dialog.show('tools'); - ui.toolSelect.focus(); + renderTools(); + if (editTools.length > 0) { + selectTool(editTools[0]); + ui.toolSelect.selectedIndex = 0; + } else { + ui.toolAdd.onclick(); } -}); \ No newline at end of file + api.dialog.show('tools'); + ui.toolSelect.focus(); +} + +export const tools = { + updateTool, + showTools +}; diff --git a/src2/kiri-mode/cam/topo3.js b/src/kiri-mode/cam/topo3.js similarity index 100% rename from src2/kiri-mode/cam/topo3.js rename to src/kiri-mode/cam/topo3.js diff --git a/src/kiri-mode/cam/topo4.js b/src/kiri-mode/cam/topo4.js index a0df0e50..fc4807b0 100644 --- a/src/kiri-mode/cam/topo4.js +++ b/src/kiri-mode/cam/topo4.js @@ -1,17 +1,14 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -// dep: geo.base -// dep: geo.line -// dep: geo.point -// dep: geo.polygon -// dep: geo.polygons -// dep: moto.broker -// dep: kiri.slice -// dep: kiri-mode.cam.driver -// dep: kiri-mode.cam.slicer2 -gapp.register("kiri-mode.cam.topo", [], (root, exports) => { +import { base } from '../geo/base.js'; +import { line } from '../geo/line.js'; +import { point } from '../geo/point.js'; +import { polygon } from '../geo/polygon.js'; +import { polygons } from '../geo/polygons.js'; +import { broker } from '../moto/broker.js'; +import { slice } from '../kiri/slice.js'; +import { driver } from '../kiri-mode/cam/driver.js'; +import { slicer2 } from '../kiri-mode/cam/slicer2.js'; const { base, kiri, moto } = root; const { driver, newSlice } = kiri; @@ -533,4 +530,5 @@ moto.broker.subscribe("minion.started", msg => { }); -}); + +export { scale, rotatePoints, PRO, POLY, RAD2DEG, DEG2RAD, axis, range, slices, lathe, box, i, x, z, shards, step, index, slice, output, complete, recs, points, shared, promises, tlen, slen, sinc, heights, oslices, lines, plen, rec, py0, pz0, py1, pz1, lz, si, rx, mz, ti, xo, yo, zo, ts, dz, dy, fr, rota, steps, mrot, paths, angle, count, poly, repeat, done, tangle, p, linear, vertices, stmp, topo4, Topo4 }; diff --git a/src2/kiri-mode/drag/client.js b/src/kiri-mode/drag/client.js similarity index 100% rename from src2/kiri-mode/drag/client.js rename to src/kiri-mode/drag/client.js diff --git a/src/kiri-mode/drag/driver.js b/src/kiri-mode/drag/driver.js index 50b6953e..a6b5c853 100644 --- a/src/kiri-mode/drag/driver.js +++ b/src/kiri-mode/drag/driver.js @@ -1,14 +1,13 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { LASER, TYPE } from '../laser/driver.js'; -// 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 }); +function init(worker) { + // console.log({ DRAGKNIFE_INIT: worker }); +} +export const DRAG = Object.assign({}, LASER, { + type: TYPE.DRAG, + name: 'DragKnife', + init }); - diff --git a/src/kiri-mode/fdm/client.js b/src/kiri-mode/fdm/client.js index 6e7709db..1dccce42 100644 --- a/src/kiri-mode/fdm/client.js +++ b/src/kiri-mode/fdm/client.js @@ -1,25 +1,20 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { $ } from '../../moto/webui.js'; +import { api } from '../../kiri/api.js'; +import { conf } from '../../kiri/conf.js'; +import { util } from '../../geo/base.js'; +import { client } from '../../kiri/client.js'; +import { addbox, delbox, getlastbox } from '../../kiri/boxes.js'; +import { Layers } from '../../kiri/layers.js'; +import { Stack } from '../../kiri/stack.js'; +import { Widget, newWidget } from '../../kiri/widget.js'; +import { space } from '../../moto/space.js'; -// dep: add.three -// dep: geo.base -// dep: moto.space -// dep: kiri.api -// dep: kiri.lang -// dep: kiri.consts -// dep: kiri-mode.fdm.driver -gapp.register("kiri-mode.fdm.client", [], (root, exports) => { +const { Matrix4, Vector3, BufferAttribute, BufferGeometryUtils, Raycaster, Euler } = THREE; +const { VIEWS } = api.const; -const { Quaternion, Matrix4, Vector3, BufferAttribute, BufferGeometryUtils } = THREE; -const { Mesh, BoxGeometry, MeshPhongMaterial, Raycaster, Euler } = THREE; -const { base, kiri, moto } = root; -const { util } = base; -const { api, consts, lang } = kiri; -const { FDM } = kiri.driver; -const { VIEWS } = consts; -const { space } = moto; -const LANG = lang.current; +const LANG = api.language.current; let lastBox, lastMode, lastView, lastPillar, fromPillar, addingSupports = false, @@ -27,12 +22,11 @@ let lastBox, lastMode, lastView, lastPillar, fromPillar, nextID = Date.now(), p1, p2, iw, alert = [], - boxes = {}, func = {}; -FDM.init = function(kiri, api) { - const proc_keys = Object.keys(kiri.conf.defaults.fdm.p); - const { ui, uc } = api; +export function init() { + const { ui } = api; + const proc_keys = Object.keys(conf.defaults.fdm.p); const rangeVars = { // slice // "sliceHeight": LANG.sl_lahi_s, @@ -92,8 +86,8 @@ FDM.init = function(kiri, api) { function filterSynth() { api.widgets.filter((widget) => { if (widget.track.synth) { - kiri.space.world.remove(widget.mesh); - kiri.Widget.Groups.remove(widget); + space.world.remove(widget.mesh); + Widget.Groups.remove(widget); } return !widget.track.synth }); @@ -209,7 +203,7 @@ FDM.init = function(kiri, api) { } } alerts.push(api.show.alert("analyzing part(s)...", 1000)); - FDM.support_generate(array => { + support_generate(array => { func.sclear(); api.hide.alert(undefined,alerts); for (let rec of array) { @@ -266,7 +260,7 @@ FDM.init = function(kiri, api) { updateSupportVisiblity(); // synth support widget for each widget group // iow, create support meshes based on support widget annotation - for (let group of kiri.Widget.Groups.list()) { + for (let group of Widget.Groups.list()) { let merge = group.filter(w => w.sups).map(w => Object.values(w.sups)).flat(); if (!merge.length) { continue; @@ -281,14 +275,14 @@ FDM.init = function(kiri, api) { box.setAttribute('uv',new BufferAttribute(new Float32Array(0), 3)); } let bbg = BufferGeometryUtils.mergeGeometries(boxen); - let sw = kiri.newWidget(null, group); + let sw = newWidget(null, group); let fwp = group[0].track.pos; sw.loadGeometry(bbg); sw._move(fwp.x, fwp.y, fwp.z); api.widgets.add(sw); sw.track.synth = true; sw.track.support = true; - kiri.space.world.add(sw.mesh); + space.world.add(sw.mesh); } }); @@ -378,6 +372,7 @@ FDM.init = function(kiri, api) { removeWidgetSupport(lastPillar.widget, lastPillar); return; } + lastBox = getlastbox(); if (!(iw && lastBox)) return; let { point, dim } = lastBox; p1.y = Math.max(0, p1.y); @@ -518,7 +513,7 @@ FDM.init = function(kiri, api) { poly.addObj(rec); } - let layers = new kiri.Layers(); + let layers = new Layers(); let layer = layers.setLayer("arcq", { line: 0xff00ff, face: 0xff00ff, opacity: 1 }); layer.addPoly(poly, {thin:true}); @@ -530,7 +525,7 @@ FDM.init = function(kiri, api) { )); } - xpdebug = new kiri.Stack(space.world, false); + xpdebug = new Stack(space.world, false); xpdebug.addLayers(layers); xpdebug.setVisible(0,Infinity); xpdebug.show(); @@ -546,6 +541,23 @@ let xpdebug; let supsave = {}; let suptimer; +function support_generate(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); + }); +}; + function scheduleSupportSave(widget) { clearTimeout(suptimer); supsave[widget.id] = widget; @@ -556,7 +568,7 @@ function scheduleSupportSave(widget) { }, 50); } -function restoreSupports(widgets) { +export function restoreSupports(widgets) { widgets.forEach(widget => { const supports = api.widgets.annotate(widget.id).support || []; supports.forEach(pos => { @@ -633,49 +645,3 @@ function clearWidgetSupports(widget) { scheduleSupportSave(widget); return cleared; } - -function delbox(name) { - const old = boxes[name]; - if (old) { - old.groupTo.remove(old); - } -} - -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; -} - -FDM.delbox = delbox; -FDM.addbox = addbox; -FDM.restoreSupports = restoreSupports; - -}); diff --git a/src2/kiri-mode/fdm/driver-be.js b/src/kiri-mode/fdm/driver-be.js similarity index 100% rename from src2/kiri-mode/fdm/driver-be.js rename to src/kiri-mode/fdm/driver-be.js diff --git a/src/kiri-mode/fdm/driver.js b/src/kiri-mode/fdm/driver.js index 7ab55ca9..ea0deabd 100644 --- a/src/kiri-mode/fdm/driver.js +++ b/src/kiri-mode/fdm/driver.js @@ -1,16 +1,7 @@ /** Copyright Stewart Allen -- 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) { +export function getRangeParameters(process, index) { if (index === undefined || index === null || index < 0) { return process; } @@ -29,57 +20,3 @@ function getRangeParameters(process, index) { } 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, - } } ))); - }; - } - -}); - -}); diff --git a/src/kiri-mode/fdm/export.js b/src/kiri-mode/fdm/export.js index 1570bb93..b82402fb 100644 --- a/src/kiri-mode/fdm/export.js +++ b/src/kiri-mode/fdm/export.js @@ -1,19 +1,14 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { base, util } from '../../geo/base.js'; +import { getRangeParameters } from './driver.js'; +import { FDM } from './driver-be.js'; +import { version } from '../../moto/license.js'; -// dep: geo.base -// dep: kiri.utils -// dep: kiri-mode.fdm.driver -gapp.register("kiri-mode.fdm.export", [], (root, exports) => { - -const { base, kiri } = root; -const { util, config } = base; -const { FDM } = kiri.driver; -const { getRangeParameters } = FDM; +const { config } = base; const debug = false; -FDM.export = function(print, online, ondone, ondebug) { +export function fdm_export(print, online, ondone, ondebug) { const { widgets, settings, belty, tools, firstTool } = print; const { bounds, controller, device, process, filter, mode } = settings; const { extruders, fwRetract } = device; @@ -30,6 +25,7 @@ FDM.export = function(print, online, ondone, ondebug) { isBambu = extras.bbl, { extrudeAbs } = device, { exportThumb } = controller, + { extrudeMM, extrudePerMM } = FDM, extused = Object.keys(print.extruders).map(v => parseInt(v)), timeFactor = (device.gcodeTime || 1) * 1.5, decimals = config.gcode_decimals || 4, @@ -126,9 +122,7 @@ FDM.export = function(print, online, ondone, ondebug) { arcQ = [], minz = { x: Infinity, y: Infinity, z: Infinity }, // lenghts of each filament (by nozzle) consumed - segments = [], - extrudeMM = FDM.extrudeMM, - extrudePerMM = FDM.extrudePerMM; + segments = []; let tools_used = Object.keys(tools); for (let tool of tools_used) { @@ -292,7 +286,7 @@ FDM.export = function(print, online, ondone, ondebug) { } function preamble() { - append(`; Generated by Kiri:Moto ${kiri.version}`); + append(`; Generated by Kiri:Moto ${version}`); append(`; ${new Date().toString()}`); appendSub("; Bed left:{left} right:{right} top:{top} bottom:{bottom}"); append(`; Bed type: ${bedType}`); @@ -995,5 +989,3 @@ FDM.export = function(print, online, ondone, ondebug) { console.log('minz', minz); } }; - -}); diff --git a/src/kiri-mode/fdm/fill.js b/src/kiri-mode/fdm/fill.js index 1ef29575..5e85b344 100644 --- a/src/kiri-mode/fdm/fill.js +++ b/src/kiri-mode/fdm/fill.js @@ -1,29 +1,24 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { base, util } from '../../geo/base.js'; +import { slice as gyroid_slice } from '../../geo/gyroid.js'; -// dep: geo.base -// dep: geo.gyroid -// dep: kiri-mode.fdm.driver -gapp.register("kiri-mode.fdm.fill", [], (root, exports) => { +export const fill_fixed = { + hex: fillHexFull, + grid: fillGrid, + triangle: fillTriangle +}; -const { base, kiri } = root; -const { util } = base; +export const fill = { + hex: fillHexFull, + grid: fillGrid, + gyroid: fillGyroid, + triangle: fillTriangle, + linear: fillLinear, + cubic: fillCubic +}; -const DEG2RAD = Math.PI / 180, - FILL = kiri.fill = { - hex: fillHexFull, - grid: fillGrid, - gyroid: fillGyroid, - triangle: fillTriangle, - linear: fillLinear, - cubic: fillCubic - }, - CACHE = kiri.fill_fixed = { - hex: fillHexFull, - grid: fillGrid, - triangle: fillTriangle - }; +const DEG2RAD = Math.PI / 180; function fillHexFull(target) { fillHex(target, true); @@ -109,13 +104,13 @@ function fillGyroid(target) { let tile_x = span_x / tile; let tile_y = span_y / tile; let tile_z = 1 / tile; - let gyroid = base.gyroid.slice(target.zValue() * tile_z, (1 - density) * 500); + let gen = gyroid_slice(target.zValue() * tile_z, (1 - density) * 500); let polys = []; - if (gyroid.dir == 'lr') { + if (gen.dir == 'lr') { for (let ty=0; ty<=tile_y; ty++) { for (let tx=0; tx<=tile_x; tx++) { - for (let poly of gyroid.polys) { + for (let poly of gen.polys) { target.newline(); let points = poly.map(el => { return { @@ -132,7 +127,7 @@ function fillGyroid(target) { } else { for (let tx=0; tx<=tile_x; tx++) { for (let ty=0; ty<=tile_y; ty++) { - for (let poly of gyroid.polys) { + for (let poly of gen.polys) { target.newline(); let points = poly.map(el => { return { @@ -323,5 +318,3 @@ function fillTriangle(target) { target.emit(xp, bounds.max.y); } } - -}); diff --git a/src/kiri-mode/fdm/post.js b/src/kiri-mode/fdm/post.js index 5385f041..2fe7dd85 100644 --- a/src/kiri-mode/fdm/post.js +++ b/src/kiri-mode/fdm/post.js @@ -1,26 +1,18 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { base } from '../../geo/base.js'; +import { polygons as POLY, fillArea } from '../../geo/polygons.js'; +import { getRangeParameters } from './driver.js'; +import { slicer } from '../../geo/slicer.js'; -// dep: geo.base -// dep: geo.polygons -// dep: kiri-mode.fdm.driver -gapp.register("kiri-mode.fdm.post", [], (root, exports) => { - -const { base, kiri } = root; -const { driver } = kiri; -const { polygons } = base; -const { fillArea } = polygons; -const { FDM } = driver; - -const POLY = polygons; +slicer.slicePost.FDM = slicePost; /** * may run in minion or worker context. do not create objects * that will not quickly encode in threaded mode. add to existing * data object. return is ignored. */ -base.slicePost.FDM = function(data, options) { +export function slicePost(data, options) { const { z, lines, groups } = data; const { useAssembly, post_args, zIndexes } = options; const { process, isSynth, vaseMode } = post_args; @@ -40,7 +32,7 @@ base.slicePost.FDM = function(data, options) { return; } const index = zIndexes.indexOf(z); - const range = FDM.getRangeParameters(process, index); + const range = getRangeParameters(process, index); // calculate fractional shells let shellFrac = (range.sliceShells - (range.sliceShells | 0)); let sliceShells = range.sliceShells | 0; @@ -62,7 +54,7 @@ base.slicePost.FDM = function(data, options) { const nutops = []; // co-locate shell processing with top generation in slicer for (let top of data.tops) { - nutops.push(FDM.doTopShells(z, top, count, offset/2, offset, fillOff, { + nutops.push(doTopShells(z, top, count, offset/2, offset, fillOff, { thinType, vaseMode, useAssembly @@ -193,7 +185,7 @@ function thin_type_3(params) { * may run in minion or worker context. performs shell offsetting * including thin wall detection when enabled */ -FDM.doTopShells = function(z, top, count, offset1, offsetN, fillOffset, opt = {}) { +export function doTopShells(z, top, count, offset1, offsetN, fillOffset, opt = {}) { // pretend we're a top object in minions if (!top.poly) { top = { poly: top }; @@ -319,5 +311,3 @@ function cullIntersections() { } return good.length > 2 ? good : []; } - -}); diff --git a/src/kiri-mode/fdm/prepare.js b/src/kiri-mode/fdm/prepare.js index 42ffa6cd..f610688d 100644 --- a/src/kiri-mode/fdm/prepare.js +++ b/src/kiri-mode/fdm/prepare.js @@ -1,28 +1,18 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { base, util } from '../../geo/base.js'; +import { poly2polyEmit, tip2tipEmit } from '../../geo/paths.js'; +import { newBounds } from '../../geo/bounds.js'; +import { newPoint } from '../../geo/point.js'; +import { newPolygon, Polygon } from '../../geo/polygon.js'; +import { polygons as POLY, fillArea } from '../../geo/polygons.js'; +import { newPrint } from '../../kiri/print.js'; +import { newSlice } from '../../kiri/slice.js'; +import { newWidget } from '../../kiri/widget.js'; +import { render } from '../../kiri/render.js'; +import { getRangeParameters } from './driver.js'; -// dep: geo.base -// dep: geo.paths -// dep: geo.point -// dep: geo.polygons -// dep: kiri.utils -// dep: kiri.slice -// dep: kiri.consts -// dep: kiri.render -// dep: kiri-mode.fdm.driver -gapp.register("kiri-mode.fdm.prepare", [], (root, exports) => { - -const { base, kiri } = root; -const { consts, driver, newSlice, render } = kiri; -const { polygons, paths, util, newPoint, newPolygon, Polygon } = base; -const { poly2polyEmit, tip2tipEmit } = paths; const { numOrDefault } = util; -const { fillArea } = polygons; -const { beltfact } = consts; -const { FDM } = driver; -const { getRangeParameters } = FDM; -const POLY = polygons; /** * DRIVER PRINT CONTRACT @@ -30,7 +20,7 @@ const POLY = polygons; * @param {Function} update progress callback * @returns {Object[]} returns array of render objects */ -FDM.prepare = async function(widgets, settings, update) { +export async function fdm_prepare(widgets, settings, update) { // filter ignored widgets widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); @@ -40,7 +30,7 @@ FDM.prepare = async function(widgets, settings, update) { { bedWidth, bedDepth } = device, { lineType } = controller, printPoint = newPoint(0,0,0), - print = self.worker.print = kiri.newPrint(settings, widgets), + print = self.kiri_worker.current.print = newPrint(settings, widgets), nozzle = process.sliceLineWidth || device.extruders[0].extNozzle, isBelt = device.bedBelt, isThin = lineType === "line", @@ -213,7 +203,7 @@ FDM.prepare = async function(widgets, settings, update) { } } // recompute bounds for purge block offsets - let bbounds = base.newBounds(); + let bbounds = newBounds(); brims.forEach(brim => { bbounds.merge(brim.bounds); }); @@ -244,7 +234,7 @@ FDM.prepare = async function(widgets, settings, update) { sslices.push(sslice); } if (sslices.length) { - let swidget = kiri.newWidget(null,widget.group); + let swidget = newWidget(null,widget.group); swidget.slices = sslices; swidget.support = true; swidget.belt = widget.belt; @@ -1512,5 +1502,3 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { return startPoint.add(offset); } - -}); diff --git a/src/kiri-mode/fdm/slice.js b/src/kiri-mode/fdm/slice.js index 45789e10..66ec638e 100644 --- a/src/kiri-mode/fdm/slice.js +++ b/src/kiri-mode/fdm/slice.js @@ -1,28 +1,19 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { base, util } from '../../geo/base.js'; +import { newBounds } from '../../geo/bounds.js'; +import { newPolygon } from '../../geo/polygon.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { newPoint } from '../../geo/point.js'; +import { slice, sliceZ } from '../../geo/slicer.js'; +import { newSlice } from '../../kiri/slice.js'; +import { fill, fill_fixed } from './fill.js'; +import { getRangeParameters } from './driver.js'; +import { doTopShells } from './post.js'; -// dep: geo.base -// dep: geo.slicer -// dep: geo.polygons -// dep: kiri.utils -// dep: kiri.consts -// dep: kiri-mode.fdm.driver -// dep: kiri-mode.fdm.post -// use: kiri-mode.fdm.fill -// use: ext.clip2 -// use: add.three -gapp.register("kiri-mode.fdm.slice", [], (root, exports) => { +const { config } = base; -const { base, kiri, noop } = root; -const { consts, driver, fill, fill_fixed, newSlice, utils } = kiri; -const { config, polygons, util, newPoint } = base; -const { fillArea } = polygons; -const { FDM } = driver; -const { doTopShells, getRangeParameters } = FDM; - -const POLY = polygons, - tracker = util.pwait, +let tracker = util.pwait, lopacity = 0.6, opacity = 1, fat = 1.5, @@ -44,6 +35,7 @@ const POLY = polygons, part: { line: 0x777777, check: 0x777777, face: 0 }, thin: { check: 0xbb8800, face: 0xbb8800, line: 0xbb8800, opacity, lopacity, fat }, }), + noop = () => {}, PROTO = Object.clone(COLOR), profile = false, profileStart = profile ? console.profile : noop, @@ -66,9 +58,9 @@ function vopt(opt) { return opt; } -FDM.sliceAll = function(settings, onupdate) { +export function sliceAll(settings, onupdate) { // future home of brim and anchor generation - let widgets = Object.values(kiri.worker.cache) + let widgets = Object.values(self.kiri_worker.cache) .filter(w => !w.meta.disabled) .sort((a,b) => { return a.slices[0].z - b.slices[0].z @@ -123,15 +115,16 @@ FDM.sliceAll = function(settings, onupdate) { * @param {Function} onupdate (called with % complete and optional message) * @param {Function} ondone (called when complete with an array of Slice objects) */ -FDM.slice = function(settings, widget, onupdate, ondone) { +export function fdm_slice(settings, widget, onupdate, ondone) { let render = settings.render !== false, + { minions } = self.kiri_worker, { process, device, controller } = settings, isBelt = device.bedBelt, isBrick = controller.devel && process.sliceZInterleave, isSynth = widget.track.synth, isSupport = widget.track.support, useAssembly = controller.assembly, - isConcurrent = controller.threaded && kiri.minions.concurrent, + isConcurrent = controller.threaded && minions.concurrent, topLayers = process.sliceTopLayers || 0, bottomLayers = process.sliceBottomLayers || 0, vaseMode = process.sliceFillType === 'vase' && !isSynth, @@ -226,7 +219,7 @@ FDM.slice = function(settings, widget, onupdate, ondone) { } } - base.slice(points, { + slice(points, { debug: process.xray, xray: process.xray, zMin: bounds.min.z, @@ -329,7 +322,7 @@ FDM.slice = function(settings, widget, onupdate, ondone) { // slicer function (worker local or minion distributed) slicer(z, points, opts) { // opts.debug = opts.debug || isSynth; - return (isConcurrent ? kiri.minions.sliceZ : base.sliceZ)(z, points, opts); + return (isConcurrent ? minions.sliceZ : sliceZ)(z, points, opts); }, onupdate(update) { return onupdate(0.0 + update * 0.5) @@ -380,7 +373,7 @@ FDM.slice = function(settings, widget, onupdate, ondone) { .map(w => w.slices).flat() .map(s => s.tops).flat().map(t => t.simple); let shadow = isConcurrent ? - await kiri.minions.union(alltops, 0.1) : + await minions.union(alltops, 0.1) : POLY.union(alltops, 0.1, true); // expand shadow when requested (support clipping) if (process.sliceSupportExtra) { @@ -584,7 +577,7 @@ FDM.slice = function(settings, widget, onupdate, ondone) { addto.belt.touch = false; let z = addto.z; let y = (slope * z) - smin - (lineWidth / 2); - let splat = base.newPolygon().add(minx, y, z).add(maxx, y, z).setOpen(); + let splat = newPolygon().add(minx, y, z).add(maxx, y, z).setOpen(); let snew = addto.addTop(splat).fill_sparse = [ splat ]; adds.push(snew); start = addto; @@ -617,7 +610,7 @@ FDM.slice = function(settings, widget, onupdate, ondone) { let fz = first.z; let n2 = lineWidth / 2; for (let x = mp - dx; x <= mp + dx ; x += 3) { - add.push( base.newPolygon().add(x, fy - n2, fz).add(x, fy + y + n2, fz).setOpen() ); + add.push(newPolygon().add(x, fy - n2, fz).add(x, fy + y + n2, fz).setOpen() ); } } } @@ -711,7 +704,7 @@ FDM.slice = function(settings, widget, onupdate, ondone) { let z = slice.z; let y = z - smin - (lineWidth / 2); for (let line of lines) { - supps.push(base.newPolygon() + supps.push(newPolygon() .add(minX + line.start / 2, y, z) .add(minX + line.end / 2, y, z) .setOpen() @@ -939,7 +932,7 @@ FDM.slice = function(settings, widget, onupdate, ondone) { } if (isBelt) { - let bounds = base.newBounds(); + let bounds = newBounds(); let slice = slices[0]; if (slice.tops) { for (let top of slice.tops) { @@ -1081,15 +1074,6 @@ function doRender(slice, isSynth, params, opt = {}) { // console.log(slice.index, slice.render.stats); } -// shared with SLA driver and minions -FDM.share = { - doShells, - doTopShells, - doDiff, - projectFlats, - projectBridges -}; - /** * Compute offset shell polygons. For FDM, the first offset is usually half * of the nozzle width. Each subsequent offset is a full nozzle width. User @@ -1104,7 +1088,7 @@ FDM.share = { * @param {number} fillOffset * @param {Obejct} options */ -function doShells(slice, count, offset1, offsetN, fillOffset, opt = {}) { +export function doShells(slice, count, offset1, offsetN, fillOffset, opt = {}) { for (let top of slice.tops) { doTopShells(slice.z, top, count, offset1, offsetN, fillOffset, opt); } @@ -1130,7 +1114,7 @@ function doShells(slice, count, offset1, offsetN, fillOffset, opt = {}) { slice.tops.forEach(function(top) { if (!top.fill_off) return; // missing for inner brick layers - let lines = fillArea(top.fill_off, angle, spacing, null); + let lines = POLY.fillArea(top.fill_off, angle, spacing, null); top.fill_lines.appendAll(lines); }); @@ -1290,7 +1274,7 @@ function doSparseLayerFill(slice, options = {}) { } if (options.promises) { - options.promises.push(kiri.minions.clip(slice, polys, lines)); + options.promises.push(self.kiri_worker.minions.clip(slice, polys, lines)); return; } @@ -1316,7 +1300,7 @@ function doSparseLayerFill(slice, options = {}) { * Used to calculate bridges, flats and then solid projections. * 'expand' is used for top offsets in SLA mode */ -function doDiff(slice, options = {}) { +export function doDiff(slice, options = {}) { const { sla, fakedown, grow, min } = options; if ((slice.index <= 0 && !fakedown) || slice.xray) { return; @@ -1372,7 +1356,7 @@ function addSolidFills(slice, polys) { /** * project bottom flats down */ -function projectFlats(slice, count, expand) { +export function projectFlats(slice, count, expand) { if (!slice.down || !slice.flats) return; // these flats are marked for finishing print speed if (slice.flats.length) slice.finishSolids = true; @@ -1385,7 +1369,7 @@ function projectFlats(slice, count, expand) { /** * project top bridges up */ -function projectBridges(slice, count) { +export function projectBridges(slice, count) { if (!slice.up || !slice.bridges) return; // these flats are marked for finishing print speed if (slice.bridges.length) slice.finishSolids = true; @@ -1516,7 +1500,7 @@ function doSolidsFill(slice, spacing, angle, minArea, fillQ) { function doFillArea(fillQ, polys, angle, spacing, output, minLen, maxLen) { if (fillQ) { - fillQ.push(kiri.minions.fill(polys, angle, spacing, output, minLen, maxLen)); + fillQ.push(self.kiri_worker.minions.fill(polys, angle, spacing, output, minLen, maxLen)); } else { POLY.fillArea(polys, angle, spacing, output, minLen, maxLen); } @@ -1526,7 +1510,8 @@ function doFillArea(fillQ, polys, angle, spacing, output, minLen, maxLen) { * calculate external overhangs requiring support */ async function doSupport(slice, proc, shadow, opt = {}) { - let maxBridge = proc.sliceSupportSpan || 5, + let { minions } = self.kiri_worker, + maxBridge = proc.sliceSupportSpan || 5, minArea = proc.supportMinArea || 0.1, pillarSize = proc.sliceSupportSize, offset = proc.sliceSupportOffset || 0, @@ -1615,26 +1600,26 @@ async function doSupport(slice, proc, shadow, opt = {}) { // for each point, create a bounding rectangle points.forEach(function(point) { - pillars.push(base.newPolygon().centerRectangle(point, size/2, size/2)); + pillars.push(newPolygon().centerRectangle(point, size/2, size/2)); }); supports.appendAll(POLY.union(pillars, null, true, { wasm: false })); // merge pillars and replace with convex hull of outer points (aka smoothing) pillars = POLY.union(pillars, null, true, { wasm: false }).forEach(function(pillar) { - supports.push(base.newPolygon().createConvexHull(pillar.points)); + supports.push(newPolygon().createConvexHull(pillar.points)); }); })(); // DEBUG code if (SDBG && down_traces) slice.output() .setLayer('cks', { line: 0xee5533, check: 0xee5533 }) - .addPolys(cks.map(p => base.newPolygon().centerRectangle(p, 0.25, 0.25))) + .addPolys(cks.map(p => newPolygon().centerRectangle(p, 0.25, 0.25))) .setLayer('pip', { line: 0xdd4422, check: 0xdd4422 }) - .addPolys(pip.map(p => base.newPolygon().centerRectangle(p, 0.4, 0.4))) + .addPolys(pip.map(p => newPolygon().centerRectangle(p, 0.4, 0.4))) .setLayer('pcl', { line: 0xcc3311, check: 0xcc3311 }) - .addPolys(pcl.map(p => base.newPolygon().centerRectangle(p, 0.3, 0.3))) + .addPolys(pcl.map(p => newPolygon().centerRectangle(p, 0.3, 0.3))) .setLayer('pts', { line: 0xdd33dd, check: 0xdd33dd }) - .addPolys(points.map(p => base.newPolygon().centerRectangle(p, 0.8, 0.8))) + .addPolys(points.map(p => newPolygon().centerRectangle(p, 0.8, 0.8))) .setLayer('dtr', { line: 0x0, check: 0x0 }) .addPolys(POLY.setZ(down_traces.clone(true),slice.z)); ; @@ -1645,7 +1630,7 @@ async function doSupport(slice, proc, shadow, opt = {}) { // then union supports if (supports.length > 10) { - supports = await kiri.minions.union(supports); + supports = await minions.union(supports); } else { supports = POLY.union(supports, null, true, { wasm: false }); } @@ -1763,7 +1748,7 @@ function projectSolid(slice, polys, count, up, first) { } } -FDM.supports = function(settings, widget) { +export function supports(settings, widget) { let isBelt = settings.device.bedBelt; let process = settings.process; let size = process.sliceSupportSize; @@ -1924,7 +1909,7 @@ class Coplanars { // convert groups of faces to contiguous polygon groups groups = groups.map(group => { let parr = group.map(arr => { - return base.newPolygon() + return newPolygon() .add(arr[0].x, arr[0].y, arr[0].z) .add(arr[1].x, arr[1].y, arr[1].z) .add(arr[2].x, arr[2].y, arr[2].z); @@ -1942,5 +1927,3 @@ class Coplanars { return out; } } - -}); diff --git a/src2/kiri-mode/laser/client.js b/src/kiri-mode/laser/client.js similarity index 100% rename from src2/kiri-mode/laser/client.js rename to src/kiri-mode/laser/client.js diff --git a/src/kiri-mode/laser/driver.js b/src/kiri-mode/laser/driver.js index ee4909d2..ace92697 100644 --- a/src/kiri-mode/laser/driver.js +++ b/src/kiri-mode/laser/driver.js @@ -1,43 +1,25 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { base } from '../../geo/base.js'; +import { Packer } from '../../kiri/pack.js'; +import { newBounds } from '../../geo/bounds.js'; +import { newPoint } from '../../geo/point.js'; +import { newPrint } from '../../kiri/print.js'; +import { newSlopeFromAngle } from '../../geo/slope.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { render } from '../../kiri/render.js'; +import { slicer } from '../../geo/slicer.js'; +import { newSlice } from '../../kiri/slice.js'; -// dep: geo.polygons -// dep: geo.point -// dep: kiri.slice -// use: kiri.pack -// use: kiri.render -gapp.register("kiri-mode.laser.driver", [], (root, exports) => { - -const { base, kiri } = root; -const { driver, newSlice } = kiri; -const { polygons, newPoint } = base; - -const POLY = polygons; -const TYPE = { +export const TYPE = { LASER: 0, DRAG: 1, WJET: 2, WEDM: 3 }; -driver.LASER = { - TYPE, - type: TYPE.LASER, - name: 'Laser', - init, - slice, - prepare, - export: exportLaser, - exportGCode, - exportSVG, - exportDXF, -}; - -function init(kiri, api, current) { - api.event.on("settings.saved", settings => { - let ui = kiri.api.ui; - }); +function init(worker) { + // console.log({ LASER_INIT: worker }); } function polyLabel(poly, label) { @@ -128,7 +110,7 @@ function arc(center, rad, s1, s2, out) { out.push( center.projectOnSlope(s1, rad) ); } while (ticks-- > 0) { - out.push( center.projectOnSlope(base.newSlopeFromAngle(a1 + off), rad) ); + out.push( center.projectOnSlope(newSlopeFromAngle(a1 + off), rad) ); a1 += step * dir; } out.push( center.projectOnSlope(s2, rad) ); @@ -186,7 +168,7 @@ function addKnifeRadii(poly, tipoff) { * @param {Function} onupdate (called with % complete and optional message) * @param {Function} ondone (called when complete with an array of Slice objects) */ -function slice(settings, widget, onupdate, ondone) { +function laser_slice(settings, widget, onupdate, ondone) { let proc = settings.process; let offset = proc.ctSliceKerf || 0; let color = settings.controller.dark ? 0xbbbbbb : 0; @@ -200,7 +182,7 @@ function slice(settings, widget, onupdate, ondone) { let points = widget.getPoints(); let indices = []; - base.slice(points, { + slicer.slice(points, { zMin: bounds.min.z, zMax: bounds.max.z, zGen(zopt) { @@ -262,13 +244,14 @@ function slice(settings, widget, onupdate, ondone) { * @param {Object} print state object * @param {Function} update incremental callback */ -async function prepare(widgets, settings, update) { - let device = settings.device, +async function laser_prepare(widgets, settings, update) { + let worker = self.kiri_worker.current, + device = settings.device, process = settings.process, - print = self.worker.print = kiri.newPrint(settings, widgets), - isWire = self.worker.mode === 'WEDM', - isKnife = self.worker.mode === 'DRAG', - isLaser = self.worker.mode === 'LASER', + print = worker.print = newPrint(settings, widgets), + isWire = worker.mode === 'WEDM', + isKnife = worker.mode === 'DRAG', + isLaser = worker.mode === 'LASER', output = print.output = [], totalSlices = 0, slices = 0, @@ -391,7 +374,7 @@ async function prepare(widgets, settings, update) { // compute tile bounds for (let tile of tiles) { - let bounds = base.newBounds(); + let bounds = newBounds(); tile.forEach(rec => bounds.merge(rec.poly.bounds)); tile.w = bounds.width(); tile.h = bounds.height(); @@ -399,7 +382,7 @@ async function prepare(widgets, settings, update) { } // pack tiles - let tp = new kiri.Pack(dw, dh, spacing, { invy:isWire, simple: !sort }).pack(tiles, packer => { + let tp = new Packer(dw, dh, spacing, { invy:isWire, simple: !sort }).pack(tiles, packer => { return packer.rescale(1.1, 1.1); }); @@ -414,7 +397,7 @@ async function prepare(widgets, settings, update) { y: fit.y - (tp.max.h / 2) - bounds.miny, z: 0}, true); } - tile.bounds = base.newBounds(); + tile.bounds = newBounds(); for (let { poly } of tile) { tile.bounds.merge(poly.bounds); } @@ -463,7 +446,7 @@ async function prepare(widgets, settings, update) { } } - return kiri.render.path(output, (progress, layer) => { + return render.path(output, (progress, layer) => { if (ctOutShaper) { for (let layer of output) { for (let out of layer) { @@ -475,7 +458,7 @@ async function prepare(widgets, settings, update) { }, { thin: true, z: ctOutStack ? undefined : 0, action: "cut", moves: process.isKnife }); }; -function exportLaser(print, online, ondone) { +function laser_export(print, online, ondone) { ondone(print.output); } @@ -512,7 +495,7 @@ function exportElements(settings, output, onpre, onpoly, onpost, onpoint, onlaye size.w = max.x - min.x; size.h = max.y - min.y; - let bounds = base.newBounds(); + let bounds = newBounds(); if (ctOriginCenter) { // place origin at geometric center of all points // regardless of workspace size @@ -863,4 +846,15 @@ function exportDXF(settings, data) { return lines.join('\n'); }; -}); +export const LASER = { + TYPE, + type: TYPE.LASER, + name: 'Laser', + init, + slice: laser_slice, + prepare: laser_prepare, + export: laser_export, + exportGCode, + exportDXF, + exportSVG +}; diff --git a/src/kiri-mode/sla/client.js b/src/kiri-mode/sla/client.js index c65aa6a4..c613c88c 100644 --- a/src/kiri-mode/sla/client.js +++ b/src/kiri-mode/sla/client.js @@ -1,22 +1,14 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { $ } from '../../moto/webui.js'; +import { api } from '../../kiri/api.js'; -// 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, { +export const SLA = { init, printDownload -}); +} -function init(kiri, api) { +export function init() { api.event.on("mode.set", (mode) => { if (mode === 'SLA') { api.ui.func.preview.classList.add('hide'); @@ -26,76 +18,69 @@ function init(kiri, api) { }); } - function printDownload(output, api, names) { +export 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; + 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 { 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'); - // }); + 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; + } } function saveFile(api, file, ext) { api.util.download(file, $('print-filename-sla').value + ext); api.modal.hide(); } - -}); diff --git a/src/kiri-mode/sla/driver.js b/src/kiri-mode/sla/driver.js index e0bc8488..c0b68880 100644 --- a/src/kiri-mode/sla/driver.js +++ b/src/kiri-mode/sla/driver.js @@ -1,26 +1,29 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { newPrint } from '../../kiri/print.js'; +import { sla_slice } from './slice.js'; +import { sla_export } from './export.js'; -// use: kiri.print -gapp.register("kiri-mode.sla.driver", [], (root, exports) => { - -const { kiri } = root -const { driver } = kiri; - -const SLA = driver.SLA = { - prepare, - legacy: false +export const SLA = { + init, + legacy: false, + slice: sla_slice, + prepare: sla_prepare, + export: sla_export, }; if (SLA.legacy) { console.log("SLA Driver in Legacy Mode"); } +function init(worker) { + // console.log({ INIT_SLA: worker }); +} + // runs in worker. would usually be in src/mode/sla/prepare.js // but the SLA driver skips the prepare step because there is no path routing -async function prepare(widgets, settings, update) { - root.worker.print = kiri.newPrint(settings, widgets); +async function sla_prepare(widgets, settings, update) { + self.kiri_worker.current.print = newPrint(settings, widgets); if (!SLA.wasm) { fetch('/wasm/kiri-sla.wasm') .then(response => response.arrayBuffer()) @@ -44,5 +47,3 @@ async function prepare(widgets, settings, update) { } update(1); } - -}); diff --git a/src/kiri-mode/sla/export.js b/src/kiri-mode/sla/export.js index da4fb277..82867ea4 100644 --- a/src/kiri-mode/sla/export.js +++ b/src/kiri-mode/sla/export.js @@ -1,17 +1,8 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -// dep: geo.base -// dep: kiri-mode.sla.driver -// dep: kiri-mode.sla.x_cxdlp -// dep: kiri-mode.sla.x_photon -gapp.register("kiri-mode.sla.export", [], (root, exports) => { - -const { base, kiri } = root -const { driver } = kiri; -const { util } = base; -const { SLA } = driver; +import { CXDLP } from './x_cxdlp.js'; +import { photon } from './x_photon.js'; +import { SLA } from './driver.js'; /** * DRIVER CONTRACT - runs in worker @@ -19,7 +10,7 @@ const { SLA } = driver; * @param {Function} online streaming reply * @param {Function} ondone last reply */ -SLA.export = function(print, online, ondone) { +export function sla_export(print, online, ondone) { let widgets = print.widgets, settings = print.settings, device = settings.device, @@ -167,8 +158,6 @@ SLA.export = function(print, online, ondone) { console.log('print.export', Date.now() - mark); }; -function generateCXDLP(print, conf, progress) { +export function generateCXDLP(print, conf, progress) { console.log({generateCXDLP: print, conf, progress}); } - -}); diff --git a/src/kiri-mode/sla/slice.js b/src/kiri-mode/sla/slice.js index c55e0aea..34c34fd1 100644 --- a/src/kiri-mode/sla/slice.js +++ b/src/kiri-mode/sla/slice.js @@ -1,23 +1,14 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { util } from '../../geo/base.js'; +import { slicer } from '../../geo/slicer.js'; +import { newPolygon } from '../../geo/polygon.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { newSlice, newTop } from '../../kiri/slice.js'; +import { doShells, doDiff, projectFlats, projectBridges } from '../fdm/slice.js'; +import { PNG } from '../../ext/pngjs.esm.js'; +import { SLA } from './driver.js'; -// dep: geo.base -// dep: geo.point -// dep: geo.polygon -// dep: geo.polygons -// dep: kiri.slice -// dep: kiri-mode.sla.driver -// use: ext.pngjs -gapp.register("kiri-mode.sla.slice", [], (root, exports) => { - -const { base, kiri } = root; -const { driver, newSlice, newTop } = kiri; -const { util, newPoint, newPolygon, polygons } = base; -const { SLA } = driver; - -const POLY = polygons; -const FDM = driver.FDM.share; const tracker = util.pwait; let fill_cache; @@ -29,9 +20,10 @@ let fill_cache; * @param {Function} onupdate (called with % complete and optional message) * @param {Function} ondone (called when complete with an array of Slice objects) */ -SLA.slice = function(settings, widget, onupdate, ondone) { - let { process, device, controller } = settings, - isConcurrent = controller.threaded && kiri.minions.concurrent, +export function sla_slice(settings, widget, onupdate, ondone) { + let { minions } = self.kiri_worker, + { process, device, controller } = settings, + isConcurrent = controller.threaded && minions.concurrent, work_total, work_remain; @@ -69,10 +61,10 @@ SLA.slice = function(settings, widget, onupdate, ondone) { break; } - let sws = self.worker.snap.url; + let sws = self.kiri_worker.current.snap.url; let b64 = atob(sws.substring(sws.indexOf(',') + 1)); let bin = Uint8Array.from(b64, c => c.charCodeAt(0)); - let img = new png.PNG(); + let img = new PNG(); img.parse(bin, (err, data) => { SLA.preview = img; SLA.previewSmall = samplePNG(img, smallDims.x, smallDims.y); @@ -157,14 +149,14 @@ SLA.slice = function(settings, widget, onupdate, ondone) { work_remain = work_total; forSlices(slices, 5, (slice,index) => { if (process.slaShell) { - FDM.doShells(slice, 2, 0, process.slaShell); + doShells(slice, 2, 0, process.slaShell); } else { - FDM.doShells(slice, 1, 0); + doShells(slice, 1, 0); } }, "shells"); forSlices(slices, 10, (slice) => { if (slice.synth) return; - FDM.doDiff(slice, 0.000001, { + doDiff(slice, 0.000001, { sla: true, fakedown: !process.slaOpenBase }); @@ -172,8 +164,8 @@ SLA.slice = function(settings, widget, onupdate, ondone) { if (solidLayers) { forSlices(slices, 10, (slice) => { if (slice.synth) return; - FDM.projectFlats(slice, solidLayers); - FDM.projectBridges(slice, solidLayers); + projectFlats(slice, solidLayers); + projectBridges(slice, solidLayers); }, "project"); async function doUnionSolid(slice) { if (slice.synth) return; @@ -182,7 +174,7 @@ SLA.slice = function(settings, widget, onupdate, ondone) { let trims = slice.solids || []; traces.appendAll(trims); // slice.unioned = POLY.setZ(POLY.union(traces, undefined, true), slice.z); - slice.unioned = POLY.setZ(await kiri.minions.union(traces), slice.z); + slice.unioned = POLY.setZ(await minions.union(traces), slice.z); } else { slice.unioned = traces; } @@ -215,7 +207,7 @@ SLA.slice = function(settings, widget, onupdate, ondone) { let bounds = widget.getBoundingBox(); let points = widget.getPoints(); - base.slice(points, { + slicer.slice(points, { indices: process.indices || process.xray, union: controller.healMesh, debug: process.xray, @@ -225,7 +217,7 @@ SLA.slice = function(settings, widget, onupdate, ondone) { zInc: height, // slicer function (worker local or minion distributed) slicer(z, points, opts) { - return (isConcurrent ? kiri.minions.sliceZ : base.sliceZ)(z, points, opts); + return (isConcurrent ? minions.sliceZ : slicer.sliceZ)(z, points, opts); }, onupdate(v) { return onupdate(0.0 + v * 0.25); @@ -606,7 +598,7 @@ function fillPolys(slice, settings) { if (!cached && seq_c !== 1) for (let x=start_x; x -- All Rights Reserved */ -"use strict"; - -gapp.register("kiri-mode.sla.x_cxdlp", [], (root, exports) => { - const default_values = { magic1: 'CXSW3DV2', magic2: 'CXSW3DV2', @@ -38,7 +34,7 @@ const data_term = 0x0d0a; // CL-89 3840 x 2400 // CL-133 3840 x 2160 -class CXDLP { +export class CXDLP { constructor() { Object.assign(this, default_values); } @@ -433,23 +429,3 @@ CXDLP.render = function(params) { } return { lines, area }; } - -if (!self.navigator && self.process && self.process.env) { - let fs = require('fs'); - eval(fs.readFileSync("src/add/class.js").toString()); - let args = process.argv.slice(2); - let file = args.shift(); - // let fpos = 0; - let view = new DataView(fs.readFileSync(file).buffer); - let read = new DataReader(view, 0); - let cxdlp = new CXDLP().read(view); - console.log({ - cxdlp, - layers5: cxdlp.layers.slice(0,5), - lines1: cxdlp.get_layer_lines(1) - }); -} else if (self.navigator) { - self.CXDLP = CXDLP; -} - -}); diff --git a/src/kiri-mode/sla/x_photon.js b/src/kiri-mode/sla/x_photon.js index 961f54ee..968bfa5c 100644 --- a/src/kiri-mode/sla/x_photon.js +++ b/src/kiri-mode/sla/x_photon.js @@ -1,13 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -gapp.register("kiri-mode.sla.x_photons", [], (root, exports) => { - -const { base, kiri } = root; -const { driver } = kiri; -const { util } = base; -const { SLA } = driver; +import { SLA } from './driver.js'; function generatePhoton(print, conf, progress) { let printset = print.settings, @@ -533,11 +526,9 @@ function polyout(poly, ctx, opt) { ctx.closePath(); } -self.photon = { +export const photon = { generatePhoton, generatePhotons, renderLayer, renderLayerWasm }; - -}); diff --git a/src2/kiri-mode/wedm/client.js b/src/kiri-mode/wedm/client.js similarity index 100% rename from src2/kiri-mode/wedm/client.js rename to src/kiri-mode/wedm/client.js diff --git a/src/kiri-mode/wedm/driver.js b/src/kiri-mode/wedm/driver.js index f94c7808..3342abfd 100644 --- a/src/kiri-mode/wedm/driver.js +++ b/src/kiri-mode/wedm/driver.js @@ -1,132 +1,21 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { LASER, TYPE } from '../laser/driver.js'; -// dep: kiri-mode.laser.driver -gapp.register("kiri-mode.wedm.driver", [], (root, exports) => { - -const { kiri, moto } = root; -const DRIVERS = kiri.driver; -const { CAM, LASER } = DRIVERS; -const { TYPE } = LASER; -const WEDM = DRIVERS.WEDM = Object.assign({}, LASER, { - name: "WireEDM", - type: TYPE.WEDM, - init, - prepare -}); const DEG2RAD = Math.PI / 180; -const state = { - api: undefined, - alert: undefined, - radians: 0, - surfaces: [], - selecting: false -}; - -function faceAdd() { - if (state.selecting) return faceDone(); - - const { api, radians, surfaces } = state; - CAM.surface_prep(radians, () => { - api.hide.alert(alert); - state.alert = api.show.alert("[esc] cancels surface selection"); - for (let [wid, arr] of Object.entries(surfaces)) { - let widget = api.widgets.forid(wid); - if (widget && arr.length) - for (let faceid of arr) { - CAM.surface_toggle(widget, faceid, radians, faceids => { - // surfaces[widget.id] = faceids; - }); - } - } - api.feature.on_mouse_up = (obj, ev) => { - let { face } = obj; - let min = Math.min(face.a, face.b, face.c); - let faceid = min / 3; - let widget = obj.object.widget; - CAM.surface_toggle(widget, faceid, radians, faceids => { - surfaces[widget.id] = faceids; - }); - }; - state.selecting = true; - }); +function init(worker) { + // console.log({ WIREEDM_INIT: worker }); } -function faceDone() { - if (!state.selecting) return; - - const { api, alert, surfaces } = state; - for (let wid of Object.keys(surfaces)) { - let widget = api.widgets.forid(wid); - if (widget) { - CAM.surface_clear(widget); - } else { - delete surfaces[wid]; - } - } - api.hide.alert(alert); - api.feature.on_mouse_up = undefined; - api.ui.faceAdd.classList.remove('editing') - state.selecting = false; -} - -function init(kiri, api, driver) { - LASER.init(kiri, api, driver); - - state.api = api; - - api.event.on("key.esc", () => { - if (state.selecting) { - faceDone(); - } - }); - - // api.event.on("mode.set", (mode) => { }); - - // api.event.on("view.set", (mode) => { }); - - api.event.on("preview.view", view => { - const process = api.conf.get().process; - if (!process.ctOriginBounds) return; - const bounds = new THREE.Box3().expandByObject(view); - api.platform.set_bounds(new THREE.Box3( - new THREE.Vector3(bounds.min.x, bounds.min.z, bounds.min.y), - new THREE.Vector3(bounds.max.x, bounds.max.z, bounds.max.y), - )); - }); - - // Surface Selection Buttons - api.event.on("button.click", target => { - let process = api.conf.get().process; - switch (target) { - case api.ui.faceAdd: - target.classList.add('editing'); - return faceAdd(); - case api.ui.faceDun: - return faceDone(); - case api.ui.faceClr: - api.uc.confirm("clear surface selection?").then(ok => { - // todo - }); - break; - } - }); - -}; - async function prepare(widgets, settings, update) { - // let device = settings.device, - // process = settings.process, - // print = self.worker.print = kiri.newPrint(settings, widgets), - // output = print.output = []; - - // filter ignored widgets widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); - LASER.prepare(widgets, settings, update); } +export const WEDM = Object.assign({}, LASER, { + type: TYPE.WEDM, + name: 'WireEDM', + init, + prepare }); - diff --git a/src2/kiri-mode/wjet/client.js b/src/kiri-mode/wjet/client.js similarity index 100% rename from src2/kiri-mode/wjet/client.js rename to src/kiri-mode/wjet/client.js diff --git a/src/kiri-mode/wjet/driver.js b/src/kiri-mode/wjet/driver.js index 8e4bf4b3..b11c54fd 100644 --- a/src/kiri-mode/wjet/driver.js +++ b/src/kiri-mode/wjet/driver.js @@ -1,14 +1,13 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { LASER, TYPE } from '../laser/driver.js'; -// 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 }); +function init(worker) { + // console.log({ WATERJET_INIT: worker }); +} +export const WJET = Object.assign({}, LASER, { + type: TYPE.WJET, + name: 'WaterJet', + init }); - diff --git a/src/kiri-run/engine.js b/src/kiri-run/engine.js index d0350a17..4ee2b2a9 100644 --- a/src/kiri-run/engine.js +++ b/src/kiri-run/engine.js @@ -2,16 +2,31 @@ "use strict"; -// dep: moto.license -// dep: load.stl -// dep: kiri.conf -// dep: kiri.client -// dep: kiri.widget -// use: add.three -// use: add.array -gapp.register("kiri-run.engine", [], (root, exports) => { +// Future imports for src/kiri-run/engine.js when unwrapped from gapp.register() -const { kiri } = root; +// Dependencies from dep: comments +// import { moto_license } from 'moto/license.js'; +// import { load_stl } from 'load/stl.js'; +// import { kiri_conf } from 'kiri/conf.js'; +// import { kiri_client } from 'kiri/client.js'; +// import { kiri_widget } from 'kiri/widget.js'; + +// Dependencies from use: comments +// import { add_three } from 'add/three.js'; +// import { add_array } from 'add/array.js'; + +// Core dependencies from root +import { kiri } from '../../main/gapp.js'; + +// Note: This module also depends on: +// - moto.license (for licensing functionality) +// - load.stl (for STL file loading) +// - kiri.conf (for configuration) +// - kiri.client (for client functionality) +// - kiri.widget (for widget functionality) +// - add.three (for Three.js utilities) +// - add.array (for array utilities) +// - Various kiri API functions injected via root class Engine { constructor() { @@ -192,4 +207,16 @@ gapp.overlay(kiri, { newEngine }); -}); +// Future exports for src/kiri-run/engine.js when unwrapped from gapp.register() + +// Main Engine class +export { Engine }; + +// Factory function +export { newEngine }; + +// Default export - the main engine functionality +export default { + Engine, + newEngine +}; \ No newline at end of file diff --git a/src/kiri-run/minion.js b/src/kiri-run/minion.js index 3a7da0a1..f20135ad 100644 --- a/src/kiri-run/minion.js +++ b/src/kiri-run/minion.js @@ -1,26 +1,20 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import "../ext/clip2.js"; +import "../ext/earcut.js"; +import '../add/array.js'; +import '../add/class.js'; +import '../add/three.js'; -// 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) => { +import { base } from '../geo/base.js'; +import { newPoint } from '../geo/point.js'; +import { polygons as POLY } from '../geo/polygons.js'; +import { sliceZ } from '../geo/slicer.js'; +import { codec } from '../kiri/codec.js'; +import { broker } from '../moto/broker.js'; +import { doTopShells } from '../kiri-mode/fdm/post.js'; +import { wasm_ctrl } from '../geo/wasm.js'; -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; @@ -75,7 +69,7 @@ const funcs = self.minion = { 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); + doTopShells(z, top, count, offset1, offsetN, fillOffset, opt); let state = { zeros: [] }; reply({ seq, top: codec.encode(top, {full: true}) }, state.zeros); }, @@ -122,14 +116,15 @@ const funcs = self.minion = { }, sliceZ: (data, seq) => { + console.log('minion.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); + realp[p++] = newPoint(points[i++], points[i++], points[i++]).round(3); } let state = { zero: [] }; let output = []; - base.sliceZ(z, realp, { + sliceZ(z, realp, { ...options, each(out) { output.push(out) } }).then(() => { @@ -159,9 +154,9 @@ const funcs = self.minion = { wasm: data => { if (data.enable) { - base.wasm_ctrl.enable(); + wasm_ctrl.enable(); } else { - base.wasm_ctrl.disable(); + wasm_ctrl.disable(); } }, @@ -170,6 +165,4 @@ const funcs = self.minion = { } }; -moto.broker.publish("minion.started", { funcs, cache, reply, log }); - -}); +broker.publish("minion.started", { funcs, cache, reply, log }); diff --git a/src/kiri-run/worker.js b/src/kiri-run/worker.js index bcb463e4..a92b230d 100644 --- a/src/kiri-run/worker.js +++ b/src/kiri-run/worker.js @@ -1,51 +1,48 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import '../add/array.js'; +import '../add/class.js'; +import '../add/three.js'; +import '../ext/jszip.js'; -// dep: geo.base -// dep: geo.polygons -// dep: geo.wasm -// dep: kiri.codec -// dep: kiri.slice -// dep: moto.license -// dep: moto.broker -// use: load.png -// use: load.gbr -// use: ext.jszip -// use: kiri.render -// use: kiri-mode.cam.animate -// use: kiri-mode.cam.animate2 -// use: kiri-mode.cam.slice -// use: kiri-mode.cam.prepare -// use: kiri-mode.cam.export -// use: kiri-mode.cam.tool -// use: kiri-mode.fdm.slice -// use: kiri-mode.fdm.prepare -// use: kiri-mode.fdm.export -// use: kiri-mode.sla.slice -// use: kiri-mode.sla.export -// use: kiri-mode.fdm.slice -// use: kiri-mode.fdm.prepare -// use: kiri-mode.fdm.export -// use: kiri-mode.laser.driver -// use: kiri-mode.drag.driver -// use: kiri-mode.wjet.driver -// use: kiri-mode.wedm.driver -gapp.register("kiri-run.worker", [], (root, exports) => { +import { base } from '../geo/base.js'; +import { broker } from '../moto/broker.js'; +import { codec } from '../kiri/codec.js'; +import { util } from '../geo/base.js'; +import { newPoint } from '../geo/point.js'; +import { polygons as POLY } from '../geo/polygons.js'; +import { newPrint } from '../kiri/print.js'; +import { render } from '../kiri/render.js'; +import { wasm_ctrl } from '../geo/wasm.js'; +import { version } from '../moto/license.js'; +import { Widget, newWidget } from '../kiri/widget.js'; + +import { CAM } from '../kiri-mode/cam/driver-be.js'; +import { DRAG } from '../kiri-mode/drag/driver.js'; +import { FDM } from '../kiri-mode/fdm/driver-be.js'; +import { LASER } from '../kiri-mode/laser/driver.js'; +import { SLA } from '../kiri-mode/sla/driver.js'; +import { WEDM } from '../kiri-mode/wedm/driver.js'; +import { WJET } from '../kiri-mode/wjet/driver.js'; -const { base, kiri, moto } = root; -const { util, polygons, wasm_ctrl } = base; -const { codec } = kiri; const { time } = util; -const POLY = polygons; -let debug = self.debug === true, - ccvalue = this.navigator ? navigator.hardwareConcurrency || 0 : 0, +let debug = (self.debug === true), + drivers = { + DRAG, + CAM, + FDM, + LASER, + SLA, + WEDM, + WJET + }, + ccvalue = self.navigator ? self.navigator.hardwareConcurrency || 0 : 0, concurrent = Math.min(4, self.Worker && ccvalue > 3 ? ccvalue - 1 : 0), - current = self.worker = { + current = { print: null, snap: null, - mode: null + mode: null, }, wgroup = {}, wcache = {}, @@ -55,8 +52,6 @@ let debug = self.debug === true, minifns = {}, miniseq = 0; -kiri.version = gapp.version; - // catch clipper alerts and convert to console messages self.alert = function(o) { console.log(o); @@ -76,8 +71,7 @@ function minhandler(msg) { } // for concurrent operations -const minwork = -kiri.minions = { +const minwork = { get concurrent() { return concurrent }, @@ -91,13 +85,20 @@ kiri.minions = { return; } for (let i=0; i < concurrent; i++) { - let _ = debug ? '_' : ''; - let minion = new Worker(`/code/kiri_pool.js?${_}${self.kiri.version}`); + let minion = new Worker(`/lib/kiri-run/minion.js`, { type: 'module' }); + minion.onerror = (error) => { + console.log({ MINION_ERROR: error }); + error.preventDefault(); + }; + minion.onmessageerror = (error) => { + console.log({ MINION_MESSAGE_ERROR: error }); + error.preventDefault(); + }; minion.onmessage = minhandler; minion.postMessage({ cmd: "label", name: `#${i}` }); minions.push(minion); } - console.log(`kiri | init pool | ${gapp.version || "rogue"} | ${concurrent + 1}`); + console.log(`kiri | init pool | ${version || "rogue"} | ${concurrent + 1}`); }, stop() { @@ -150,7 +151,7 @@ kiri.minions = { let arr = data.fill; let fill = []; for (let i=0; i { if (concurrent < 2) { reject("concurrent slice unavaiable"); @@ -199,64 +201,63 @@ kiri.minions = { floatP[i++] = p.y; floatP[i++] = p.z; } + const state = { zeros: [] }; minwork.queue({ cmd: "sliceZ", z, points: floatP, - options: codec.toCodable(options) + options }, data => { - let recs = codec.decode(data.output); - if (each) { - for (let rec of recs) { - each(rec); - } + console.log({ data }); + for (let rec of data.output) { + each(rec); } - resolve(recs); - }, [ floatP.buffer ]); + resolve(); + }, state.zeros); }); }, queue(work, ondone, direct) { - minionq.push({work, ondone, direct}); + if (direct) { + return ondone(work); + } + let seq = ++miniseq; + minifns[seq] = ondone; + minionq.push({ seq, work }); minwork.kick(); }, queueAsync(work, direct) { - return new Promise(resolve => { + return new Promise((resolve, reject) => { minwork.queue(work, resolve, direct); }); }, kick() { - if (minions.length && minionq.length) { - let qrec = minionq.shift(); - let minion = minions.shift(); - let seq = miniseq++; - qrec.work.seq = seq; - minifns[seq] = (data) => { - qrec.ondone(data); - minions.push(minion); - minwork.kick(); - }; - minion.postMessage(qrec.work, qrec.direct); + if (minionq.length === 0 || minions.length === 0) { + return; } + let minion = minions.shift(); + let { seq, work } = minionq.shift(); + work.seq = seq; + minion.postMessage(work); + minions.push(minion); }, broadcast(cmd, data, direct) { + if (direct) { + return; + } for (let minion of minions) { - minion.postMessage({ - cmd, ...data - }, direct); + minion.postMessage({ cmd, data }); } } }; -console.log(`kiri | init work | ${gapp.version || "rogue"}`); +console.log(`kiri | init work | ${version || "rogue"}`); // code is running in the worker / server context -const dispatch = -kiri.server = -kiri.worker = { +const dispatch = { pool_start(data, send) { minwork.start(); send.done({}); @@ -295,8 +296,8 @@ kiri.worker = { // current.snap = null; current.print = null; dispatch.group = wgroup = {}; - dispatch.cache = wcache = {}; - kiri.Widget.Groups.clear(); + dispatch.cache = worker.cache = wcache = {}; + Widget.Groups.clear(); send.done({ clear: true }); }, @@ -311,7 +312,7 @@ kiri.worker = { } let vertices = data.vertices, - widget = kiri.newWidget(data.id, group) + widget = newWidget(data.id, group) .setInWorker() .loadVertices(vertices); @@ -393,19 +394,35 @@ kiri.worker = { }, slice(data, send) { - send.data({update:0.001, updateStatus:"slicing"}); + send.data({ update:0.001, updateStatus:"slicing" }); - let settings = data.settings, - widget = wcache[data.id], - last = time(), - now; + const { settings } = data; + const { mode } = settings; + const driver = drivers[mode]; + const widget = wcache[data.id]; + + if (!(driver && driver.prepare)) { + return console.log({ invalid_print_driver: mode, driver }); + } + + let last = time(), now; current.print = null; current.mode = settings.mode.toUpperCase(); widget.anno = data.anno || widget.anno; + widget.settings = settings; + widget.clearSlices(); - widget.slice(settings, function(error) { + driver.slice(settings, widget, (update, msg, alert) => { + now = time(); + // on alert + if (alert) send.data({ alert }); + // on update + if (now - last < 10 && update < 0.99) return; + if (update || msg) send.data({update: (0.05 + update * 0.95), updateStatus: msg}); + last = now; + }, (error) => { if (error) { send.data({error: error}); } else { @@ -422,21 +439,14 @@ kiri.worker = { send.data({send_end: time()}); } send.done({done: true}); - }, function(update, msg, alert) { - now = time(); - // on alert - if (alert) send.data({ alert }); - // on update - if (now - last < 10 && update < 0.99) return; - if (update || msg) send.data({update: (0.05 + update * 0.95), updateStatus: msg}); - last = now; + widget.points = undefined; }); }, sliceAll(data, send) { const { settings } = data; const { mode } = settings; - const driver = kiri.driver[mode]; + const driver = drivers[mode]; if (driver.sliceAll) { driver.sliceAll(settings, send.data); @@ -454,10 +464,10 @@ kiri.worker = { const { settings } = data; const { mode } = settings; - const driver = kiri.driver[mode]; + const driver = drivers[mode]; if (!(driver && driver.prepare)) { - return console.log({invalid_print_driver: mode, driver}); + return console.log({ invalid_print_driver: mode, driver }); } driver.prepare(widgets, settings, (progress, message, layer) => { @@ -477,7 +487,7 @@ kiri.worker = { export(data, send) { const mode = data.settings.mode; - const driver = kiri.driver[mode]; + const driver = drivers[mode]; if (!(driver && driver.export)) { console.log({missing_export_driver: mode}); @@ -485,26 +495,26 @@ kiri.worker = { } let output; - driver.export(current.print, function(line, direct) { + driver.export(current.print, (line, direct) => { send.data({line}, direct); - }, function(done) { + }, (done) => { // SLA workaround output = done; - }, function(debug) { + }, (debug) => { send.data({debug}); }); - //export in different modes, based on device type - if(mode.toUpperCase() === "LASER"){ + // export in different modes, based on device type + if (mode.toUpperCase() === "LASER") { let FExt = data.settings.device.gcodeFExt.toUpperCase(); - if(FExt === "DXF"){ + if (FExt === "DXF"){ let dxf = driver.exportDXF(data.settings, current.print.output); send.data({line:dxf}); - }else if(FExt === "SVG"){ + } else if(FExt === "SVG") { let svg = driver.exportSVG(data.settings, current.print.output); send.data({line:svg}); - }else{ - //if not svg or dxf, default to gcode + } else { + // if not svg or dxf, default to gcode let gcode = driver.exportGCode(data.settings, current.print.output); send.data({line:gcode}); } @@ -542,7 +552,7 @@ kiri.worker = { const { colors, max } = data; const colorMap = {}; colors.forEach(color => { - colorMap[color] = kiri.render.rate_to_color(color, max); + colorMap[color] = render.rate_to_color(color, max); }); send.done(colorMap); }, @@ -556,7 +566,7 @@ kiri.worker = { z: origin.z }; const device = settings.device; - const print = current.print = kiri.newPrint(settings, Object.values(wcache)); + const print = current.print = newPrint(settings, Object.values(wcache)); const tools = device.extruders; const mode = settings.mode; const thin = settings.controller.lineType === 'line' || mode !== 'FDM'; @@ -566,7 +576,7 @@ kiri.worker = { }, done => { const minSpeed = print.minSpeed; const maxSpeed = print.maxSpeed; - kiri.render.path(done.output, progress => { + render.path(done.output, progress => { send.data({ progress: 0.25 + progress * 0.75 }); }, { thin: thin || print.belt, flat, tools }) .then(layers => { @@ -583,11 +593,11 @@ kiri.worker = { parsed.forEach(layer => { layer.forEach(out => { const { x, y, z } = out.point; - out.point = base.newPoint(x,y,z || 0); + out.point = newPoint(x,y,z || 0); }); }); - const print = current.print = kiri.newPrint(null, Object.values(wcache)); - kiri.render.path(parsed, progress => { + const print = current.print = newPrint(null, Object.values(wcache)); + render.path(parsed, progress => { send.data({ progress }); }, { thin: true }) .then(layers => { @@ -694,7 +704,7 @@ dispatch.onmessage = self.onmessage = async function(e) { msg = e.data || {}, run = dispatch[msg.task], send = { - data : function(data,direct) { + data : function(data, direct) { // if (direct && direct.length) { // console.log( direct.map(z => z.byteLength).reduce((a,v) => a+v) ); // } @@ -743,9 +753,19 @@ dispatch.onmessage = self.onmessage = async function(e) { } }; -moto.broker.publish("worker.started", { dispatch, minions: minwork }); +const worker = self.kiri_worker = { + cache: wcache, + current, + dispatch, + drivers, + version, + minions: minwork +}; -}); +// initilize driver mode handlers +CAM.init(worker); +FDM.init(worker); +LASER.init(worker); +SLA.init(worker); -// load kiri modules -// kiri.load_exec(dispatch); +broker.publish("worker.started", { dispatch, minions: minwork }); diff --git a/src/kiri/alerts.js b/src/kiri/alerts.js index 8ed32689..5cf3c46d 100644 --- a/src/kiri/alerts.js +++ b/src/kiri/alerts.js @@ -1,12 +1,6 @@ /** Copyright Stewart Allen -- 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 +}; diff --git a/src/kiri/api.js b/src/kiri/api.js index 9dae1981..f3fed59b 100644 --- a/src/kiri/api.js +++ b/src/kiri/api.js @@ -1,17 +1,30 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import alerts from './alerts.js'; +import settings from './settings.js' +import STACKS from './stacks.js'; -// dep: moto.broker -// dep: data.local -// dep: kiri.utils -// dep: kiri.consts -gapp.register("kiri.api", (root, exports) => { +import { broker } from '../moto/broker.js'; +import { client as work } from './client.js'; +import { consts, COLOR as color, LISTS as lists, MODES, VIEWS } 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 '../kiri/stats.js'; +import { widgets } from './widgets.js'; +import { updateTool } from '../kiri-mode/cam/tools.js'; -let { data, kiri, moto, noop } = root, - { consts, utils } = kiri, - { ajax, o2js, js2o } = utils, - lists = consts.LISTS, +import { LANG } from './lang.js'; +import { LOCAL, SETUP, SECURE } from './main.js'; +import { UI } from './ui.js'; + +let UC = UI.prefix('kiri').inputAction(settings.conf.update), + und = undefined, clone = Object.clone, isHover = false, feature = { @@ -20,27 +33,33 @@ let { data, kiri, moto, noop } = root, 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 + 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: undefined, // function override default key handlers + on_key: und, // 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 + 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 + 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); + broker.publish("feature.hover", b); } }, + busyVal = 0, + busy = { + val() { return busy }, + inc() { api.event.emit("busy", ++busyVal) }, + dec() { api.event.emit("busy", --busyVal) } + }, onkey = (fn) => { api.feature.on_key2.push(fn); }, @@ -53,7 +72,7 @@ let { data, kiri, moto, noop } = root, let proc = api.conf.get().process, size = proc.sliceHeight || proc.slaSlice || 1, base = (proc.firstSliceHeight || size); - layers = Array.isArray(layers) ? layers : [ layers ]; + 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(); @@ -72,62 +91,9 @@ let { data, kiri, moto, noop } = root, 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 - }); + line_precision(v) { api.work.config({ base: { clipperClean: v } }) }, + gcode_decimals(v) { api.work.config({ base: { gcode_decimals: v } }) } + }; function clip(text) { navigator.clipboard @@ -146,4 +112,64 @@ function localSet(key, val) { return val; } -}); +export const api = { + ajax, + alerts, + busy, + catalog, + client: work, + clip, + clone, + color, + conf: settings.conf, + const: { LANG, LISTS: lists, LOCAL, MODES, SETUP, SECURE, STACKS, VIEWS }, + devel, + device, + devices, + dialog, + doit, + event, + feature, + function: functions, + group, + help, + hide, + image, + js2o, + language: LANG, + lists, + local, + modal, + mode, + noop, + o2js, + onkey, + platform, + probe, + process, + sdb: dataLocal, + selection, + settings, + show, + space, + stacks: STACKS, + stats, + tool: { + update: updateTool + }, + tweak, + uc: UC, + ui: {}, + util, + var: { + layer_lo: 0, + layer_hi: 0, + layer_max: 0 + }, + view, + widgets, + work, +}; + +// allow widget to straddle client / worker FOR NOW +self.kiri_api = api; diff --git a/src2/kiri/boxes.js b/src/kiri/boxes.js similarity index 100% rename from src2/kiri/boxes.js rename to src/kiri/boxes.js diff --git a/src/kiri/client.js b/src/kiri/client.js index b9e6e2bb..959eb1ae 100644 --- a/src/kiri/client.js +++ b/src/kiri/client.js @@ -1,27 +1,17 @@ /** Copyright Stewart Allen -- 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; /** * @param {Function} fn name of function in kiri.worker @@ -52,7 +42,7 @@ function send(fn, data, onreply, zerocopy) { } // code is running in the browser / client context -const client = exports({ +export const client = { send: send, pool: { @@ -75,8 +65,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(`/lib/kiri-run/worker.js`, { 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; } }, @@ -112,7 +110,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 +159,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 +243,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 +332,11 @@ const client = exports({ // console.log({ clearCache_reply: reply }); }); } -}); +}; // start worker client.restart(); -}); +self.kiri_client = client; + +export default client; diff --git a/src/kiri/codec.js b/src/kiri/codec.js index db11d16c..2f07bb54 100644 --- a/src/kiri/codec.js +++ b/src/kiri/codec.js @@ -1,32 +1,15 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { base } from '../geo/base.js'; +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 +193,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 +208,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 +216,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 +235,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 +249,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 +262,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 +272,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 +290,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 +303,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 +335,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 +365,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 +402,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 -- 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, @@ -971,7 +963,7 @@ const conf = exports({ id: genID(), ver: CVER } -}); +}; const settings = conf.template; @@ -990,5 +982,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); - -}); diff --git a/src/kiri/consts.js b/src/kiri/consts.js index 5ba2d52f..750f4296 100644 --- a/src/kiri/consts.js +++ b/src/kiri/consts.js @@ -1,9 +1,5 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -gapp.register("kiri.consts", (root, exports) => { - const COLOR = { wireframe: 0x444444, wireframe_opacity: 0.25, @@ -147,7 +143,7 @@ const PMODES = { const SEED = 'kiri-seed'; -exports({ +export let consts = { PMODES, COLOR, LISTS, @@ -155,6 +151,6 @@ exports({ VIEWS, SEED, beltfact: Math.cos(Math.PI / 4) -}); +}; -}); +export { COLOR, LISTS, MODES, VIEWS, PMODES, SEED }; diff --git a/src/kiri/devices.js b/src/kiri/devices.js index e9055c51..419386cb 100644 --- a/src/kiri/devices.js +++ b/src/kiri/devices.js @@ -1,393 +1,382 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +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/devices.js'; +import { settings, conf as setconf } from './settings.js'; -// dep: kiri.api -// dep: kiri.settings -gapp.register("kiri.devices", (root, exports) => { +export const device = { + clone: cloneDevice, + code: currentDeviceCode, + get: currentDeviceName, + set: selectDevice, + isBelt +}; - let { kiri } = root, - { api, conf } = kiri; +export const devices = { + show: showDevices, + select: selectDevice, + refresh: updateDeviceList, + update_laser_state: updateLaserState +}; - // extend API - Object.assign(api.show, { - devices: showDevices - }); +function isBelt() { + return setconf.get().device.bedBelt; +} - Object.assign(api.device, { - clone: cloneDevice, - code: currentDeviceCode, - get: currentDeviceName, - set: selectDevice, - isBelt - }); +function currentDeviceName() { + return setconf.get().filter[api.mode.get()]; +} - Object.assign(api.devices, { - show: showDevices, - select: selectDevice, - refresh: updateDeviceList, - update_laser_state: updateLaserState - }); +function currentDeviceCode() { + return setconf.get().devices[currentDeviceName()]; +} - function isBelt() { - return api.conf.get().device.bedBelt; - } +function getModeDevices() { + return Object.keys(devlist[ api.mode.get_lower() ]).sort(); +} - function currentDeviceName() { - return api.conf.get().filter[api.mode.get()]; - } +export function showDevices() { + settings.sync.get().then(_showDevices); +} - function currentDeviceCode() { - return api.conf.get().devices[currentDeviceName()]; - } +function _showDevices() { + updateDeviceList(); + api.modal.show('setup'); +} - function getModeDevices() { - // devices are injected into self scope by - // app.js generateDevices() - return Object.keys(devices[api.mode.get_lower()]).sort(); - } +function updateDeviceList() { + renderDevices(getModeDevices()); +} - function showDevices() { - api.settings.sync.get().then(_showDevices); - } - - function _showDevices() { +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(); - api.modal.show('setup'); } +} - function updateDeviceList() { - renderDevices(getModeDevices()); - } +function putLocalDevice(devicename, obj) { + setconf.get().devices[devicename] = obj; + setconf.save(); +} - function updateDeviceName(newname) { - let selected = api.device.get(), - devs = api.conf.get().devices; - if (newname !== selected) { - devs[newname] = devs[selected]; - delete devs[selected]; - selectDevice(newname); - updateDeviceList(); +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); } } +} - function putLocalDevice(devicename, obj) { - api.conf.get().devices[devicename] = obj; - api.conf.save(); +// 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 removeLocalDevice(devicename) { - delete api.conf.get().devices[devicename]; - api.conf.save(); - api.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 isLocalDevice(devicename) { - return api.conf.get().devices[devicename] ? true : false; - } +function setDeviceCode(code, devicename) { + api.event.emit('device.select', devicename); + try { + if (typeof(code) === 'string') code = js2o(code) || {}; - function getSelectedDevice() { - return api.device.get(); - } + 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 - function selectDevice(devicename) { - if (isLocalDevice(devicename)) { - setDeviceCode(api.conf.get().devices[devicename], devicename); - } else { - let code = devices[api.mode.get_lower()][devicename]; - if (code) { - setDeviceCode(code, devicename); - } - } - } + // fill missing device fields + conf.fill_cull_once(dev, conf.defaults[lmode].d); - // only for local filters - function cloneDevice() { - let name = `${getSelectedDevice().replace(/\./g,' ')}`; - let code = api.clone(api.conf.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); - api.settings.sync.put(); - } - - function updateLaserState() { - const dev = api.conf.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 = api.conf.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; - } + // 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; - let { space } = kiri; - - 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 { - if (newname) { - updateDeviceName(newname); - api.conf.save(); - api.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 ? api.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)); - } + // 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; }); - 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); - } + 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); +} -}); diff --git a/src/kiri/do.js b/src/kiri/do.js index 6989c2be..2432eab3 100644 --- a/src/kiri/do.js +++ b/src/kiri/do.js @@ -1,13 +1,8 @@ /** Copyright Stewart Allen -- 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}; }); - -}); diff --git a/src/kiri/export.js b/src/kiri/export.js index f217f5f7..af88ffc0 100644 --- a/src/kiri/export.js +++ b/src/kiri/export.js @@ -1,23 +1,13 @@ /** Copyright Stewart Allen -- 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 '../kiri-mode/laser/driver.js'; +import { SLA as sla_client } from '../kiri-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); } - -}); diff --git a/src/kiri/files.js b/src/kiri/files.js index ab878c95..0067915e 100644 --- a/src/kiri/files.js +++ b/src/kiri/files.js @@ -1,13 +1,5 @@ /** Copyright Stewart Allen -- 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 }; diff --git a/src/kiri/frame.js b/src/kiri/frame.js index 396135ae..8f5df085 100644 --- a/src/kiri/frame.js +++ b/src/kiri/frame.js @@ -1,14 +1,12 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +import { load } from '../load.js'; +import { kiri } from './kiri.js'; +import { moto } from '../moto.js'; +import { api } from './api.js'; +import { newWidget } from './widget.js'; +import { conf, event, feature, platform, settings, show } from './api.js'; +import { VIEWS } from './consts.js'; -"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) => { // add frame message api listener window.addEventListener('message', msg => { @@ -127,4 +125,5 @@ window.addEventListener('message', msg => { } }); -}); + +// No direct exports; sets up message listener diff --git a/src/kiri/function.js b/src/kiri/function.js index 8c68fd50..a59ec751 100644 --- a/src/kiri/function.js +++ b/src/kiri/function.js @@ -1,30 +1,16 @@ /** Copyright Stewart Allen -- 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, +}; diff --git a/src/kiri/init.js b/src/kiri/init.js index d489ed21..474eb9bc 100644 --- a/src/kiri/init.js +++ b/src/kiri/init.js @@ -1,2169 +1,2169 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { $, $c } from '../moto/webui.js'; +import { api } from '../kiri/api.js'; +import { base } from '../geo/base.js'; +import { local as sdb } from '../data/local.js'; +import { space } from '../moto/space.js'; +import { LOCAL, SETUP } from './main.js'; +import { VIEWS, MODES, SEED } from '../kiri/consts.js'; +import { beta, version } from '../moto/license.js'; +import { settings as set_ctrl } from './settings.js'; +import { init as initCAM } from '../kiri-mode/cam/client.js'; +import { init as initDRAG } from '../kiri-mode/drag/client.js'; +import { init as initFDM } from '../kiri-mode/fdm/client.js'; +import { init as initLaser } from '../kiri-mode/laser/client.js'; +import { init as initSLA } from '../kiri-mode/sla/client.js'; +import { init as initWEDM } from '../kiri-mode/wedm/client.js'; +import { init as initWJET } from '../kiri-mode/wjet/client.js'; -// dep: geo.base -// dep: kiri.api -// dep: kiri.main -// dep: kiri.lang -// dep: kiri.stats -// dep: kiri.consts -// dep: kiri.devices -// dep: kiri.platform -// dep: kiri.selection -// use: kiri.tools -// use: kiri.pack -gapp.register("kiri.init", (root, exports) => { +let { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, + { client, catalog, platform, selection } = api, + LANG = api.language.current, + WIN = self.window, + DOC = self.document, + STARTMODE = SETUP.sm && SETUP.sm.length === 1 ? SETUP.sm[0] : null, + TWOD = [ LASER, DRAG, WJET, WEDM ], + TWONED = [ LASER, DRAG, WJET ], + THREED = [ FDM, CAM, SLA ], + GCODE = [ FDM, CAM, ...TWOD ], + CAM_LZR = [ CAM, ...TWOD ], + FDM_LZN = [ FDM, ...TWONED ], + NO_WEDM = [ FDM, CAM, SLA, LASER, DRAG, WJET ], + FDM_CAM = [ FDM, CAM ], + proto = location.protocol, + platformColor, + separator = true, + hideable = true, + inline = true, + driven = true, + trigger = true, + units = true, + ui = api.ui, + uc = api.uc; - // builds and wires up the UI - let { base, kiri } = root, - { api, catalog, consts, space } = kiri, - { sdb, stats, platform, selection, ui, uc } = api, - { VIEWS, MODES, SEED } = consts, - { LANG, LOCAL, SETUP } = api.const, - { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, - WIN = self.window, - DOC = self.document, - STARTMODE = SETUP.sm && SETUP.sm.length === 1 ? SETUP.sm[0] : null, - TWOD = [LASER, DRAG, WJET, WEDM], - TWONED = [LASER, DRAG, WJET], - THREED = [FDM, CAM, SLA], - GCODE = [FDM, CAM, ...TWOD], - CAM_LZR = [CAM, ...TWOD], - FDM_LZN = [FDM, ...TWONED], - NO_WEDM = [FDM, CAM, SLA, LASER, DRAG, WJET], - FDM_CAM = [FDM, CAM], - proto = location.protocol, - platformColor, - separator = true, - hideable = true, - inline = true, - driven = true, - trigger = true, - units = true; +function settings() { + return api.conf.get(); +} - // copy version from grid app - kiri.version = gapp.version; +function updateTool(ev) { + api.tool.update(ev); +} - function settings() { - return api.conf.get(); - } - - function updateTool(ev) { - api.tool.update(ev); - } - - function checkSeed(then) { - // skip sample object load in onshape (or any script postload) - if (!sdb[SEED]) { - sdb[SEED] = new Date().getTime(); - if (!SETUP.s && api.feature.seed) { - if (SETUP.debug) { - return then(); - } - platform.load_stl("/obj/cube.stl", function(vert) { - catalog.putFile("sample cube.stl", vert); - platform.update_bounds(); - space.view.home(); - setTimeout(() => { api.space.save(true) },500); - then(); - api.help.show(); - }); - return true; +function checkSeed(then) { + // skip sample object load in onshape (or any script postload) + if (!sdb[SEED]) { + sdb[SEED] = new Date().getTime(); + if (!SETUP.s && api.feature.seed) { + if (SETUP.debug) { + return then(); } - } - return false; - } - - function unitsSave() { - api.conf.update({ controller: true }); - platform.update_size(); - } - - function aniMeshSave() { - api.conf.update({ controller: true }); - api.conf.save(); - } - - function lineTypeSave() { - const sel = ui.lineType.options[ui.lineType.selectedIndex]; - if (sel) { - settings().controller.lineType = sel.value; - api.conf.save(); - } - } - - function thinWallSave() { - let opt = ui.sliceDetectThin; - let level = opt.options[opt.selectedIndex]; - if (level) { - settings().process.sliceDetectThin = level.value; - api.conf.save(); - } - } - - function detailSave() { - let level = ui.detail.options[ui.detail.selectedIndex]; - if (level) { - level = level.value; - let rez = base.config.clipperClean; - switch (level) { - case '100': rez = 50; break; - case '75': rez = base.config.clipperClean; break; - case '50': rez = 500; break; - case '25': rez = 1000; break; - } - kiri.client.config({ - base: { clipperClean: rez } - }); - settings().controller.detail = level; - api.conf.save(); - } - } - - function speedSave() { - settings().controller.showSpeeds = ui.showSpeeds.checked; - api.view.update_speeds(); - } - - function zAnchorSave() { - api.conf.update(); - api.platform.update_top_z(); - } - - function setThreaded(bool) { - if (bool) { - kiri.client.pool.start(); - } else { - kiri.client.pool.stop(); - } - return bool; - } - - api.event.on("set.threaded", bool => setThreaded(bool)); - - function booleanSave() { - let control = settings().controller; - let doAlert = ui.ortho.checked !== control.ortho; - if (control.assembly != ui.assembly.checked) { - kiri.client.wasm(ui.assembly.checked); - } - if (control.antiAlias != ui.antiAlias.checked) { - api.show.alert('Page Reload Required to Change Aliasing'); - } - control.shiny = ui.shiny.checked; - control.drawer = ui.drawer.checked; - control.scrolls = ui.scrolls.checked; - control.showOrigin = ui.showOrigin.checked; - control.showRulers = ui.showRulers.checked; - control.autoLayout = ui.autoLayout.checked; - control.freeLayout = ui.freeLayout.checked; - control.spaceRandoX = ui.spaceRandoX.checked; - control.autoSave = ui.autoSave.checked; - control.antiAlias = ui.antiAlias.checked; - control.reverseZoom = ui.reverseZoom.checked; - control.dark = ui.dark.checked; - control.exportOcto = ui.exportOcto.checked; - control.exportGhost = ui.exportGhost.checked; - control.exportLocal = ui.exportLocal.checked; - control.exportThumb = ui.exportThumb.checked; - control.exportPreview = ui.exportPreview.checked; - control.healMesh = ui.healMesh.checked; - control.threaded = setThreaded(ui.threaded.checked); - control.assembly = ui.assembly.checked; - control.ortho = ui.ortho.checked; - control.devel = ui.devel.checked; - space.view.setZoom(control.reverseZoom, control.zoomSpeed); - // platform.layout(); - api.conf.save(); - api.platform.update_size(); - updateStats(); - updateDrawer(); - api.event.emit('boolean.update'); - if (doAlert) { - api.show.alert("change requires page refresh"); - } - } - - function updateDrawer() { - const { drawer, scrolls } = settings().controller; - $c('app', drawer ? 'slideshow' : '', drawer ? '' : 'slideshow'); - $c('app', scrolls ? '' : 'hide-scroll', scrolls ? 'hide-scroll' : ''); - } - - function updateStats() { - if (self.debug !== true) { - return; - } - let { div, fps, rms, rnfo } = ui.stats; - div.style.display = 'flex'; - setInterval(() => { - const nrms = space.view.getRMS().toFixed(1); - const nfps = space.view.getFPS().toFixed(1); - const rend = space.renderInfo(); - const { memory, render } = rend; - if (nfps !== fps.innerText) { - fps.innerText = nfps; - } - if (nrms !== rms.innerText) { - rms.innerText = nrms; - } - if (rnfo.offsetParent !== null) { - rnfo.innerHTML = Object.entries({ ...memory, ...render, render_ms: nrms, frames_sec: nfps }).map(row => { - return `
${row[0]}
` - }).join(''); - } - }, 100); - } - - function onBooleanClick(el) { - // copy some ui elements to target settings - let settings = api.conf.get(); - settings.device.bedBelt = ui.deviceBelt.checked && api.mode.is_fdm(); - settings.device.bedRound = ui.deviceRound.checked && api.mode.is_fdm(); - settings.device.originCenter = ui.deviceOrigin.checked || ui.deviceRound.checked; - settings.device.fwRetract = ui.fwRetract.checked; - // refresh vars and other ui elements - uc.refresh(); - if (el === ui.camStockIndexed) { - api.view.set_arrange(); - } - api.conf.update(); - DOC.activeElement.blur(); - api.event.emit("boolean.click"); - api.devices.update_laser_state(); - } - - function onButtonClick(ev) { - let target = ev.target; - while (target && target.tagName !== 'BUTTON') { - target = target.parentNode; - } - api.event.emit("button.click", target); - } - - function inputHasFocus() { - let active = DOC.activeElement; - return active && (active.nodeName === "INPUT" || active.nodeName === "TEXTAREA"); - } - - function cca(c) { - return c.charCodeAt(0); - } - - function keyUpHandler(evt) { - if (api.feature.on_key) { - if (api.feature.on_key({up:evt})) return; - } - for (let handler of api.feature.on_key2) { - if (handler({up:evt})) return; - } - switch (evt.keyCode) { - // escape - case 27: - // blur text input focus - DOC.activeElement.blur(); - // dismiss modals - api.modal.hide(); - // deselect widgets - platform.deselect(); - // hide all dialogs - api.dialog.hide(); - // cancel slicing - api.function.cancel(); - // and send an event (used by FDM client) - api.event.emit("key.esc"); - break; - } - return false; - } - - function keyDownHandler(evt) { - if (api.modal.visible()) { - return false; - } - if (api.feature.on_key) { - if (api.feature.on_key({down:evt})) return; - } - for (let handler of api.feature.on_key2) { - if (handler({down:evt})) return; - } - let move = evt.altKey ? 5 : 0, - deg = move ? 0 : -Math.PI / (evt.shiftKey ? 36 : 2); - switch (evt.keyCode) { - case 8: // apple: delete/backspace - case 46: // others: delete - if (inputHasFocus()) return false; - platform.delete(selection.meshes()); - evt.preventDefault(); - break; - case 37: // left arrow - if (inputHasFocus()) return false; - if (deg) selection.rotate(0, 0, -deg); - if (move > 0) selection.move(-move, 0, 0); - evt.preventDefault(); - break; - case 39: // right arrow - if (inputHasFocus()) return false; - if (deg) selection.rotate(0, 0, deg); - if (move > 0) selection.move(move, 0, 0); - evt.preventDefault(); - break; - case 38: // up arrow - if (inputHasFocus()) return false; - if (evt.metaKey) return api.show.layer(api.var.layer_at+1); - if (deg) selection.rotate(deg, 0, 0); - if (move > 0) selection.move(0, move, 0); - evt.preventDefault(); - break; - case 40: // down arrow - if (inputHasFocus()) return false; - if (evt.metaKey) return api.show.layer(api.var.layer_at-1); - if (deg) selection.rotate(-deg, 0, 0); - if (move > 0) selection.move(0, -move, 0); - evt.preventDefault(); - break; - case 65: // 'a' for select all - if (evt.metaKey || evt.ctrlKey) { - if (inputHasFocus()) return false; - evt.preventDefault(); - platform.deselect(); - platform.select_all(); - } - break; - case 83: // 's' for save workspace - if (evt.ctrlKey) { - evt.preventDefault(); - api.conf.save(); - console.log("settings saved"); - } else - if (evt.metaKey) { - evt.preventDefault(); - api.space.save(); - api.settings.sync.put(); - } - break; - case 76: // 'l' for restore workspace - if (evt.metaKey) { - evt.preventDefault(); - api.space.restore(); - } - break; - } - } - - function keyHandler(evt) { - let handled = true; - if (api.modal.visible() || inputHasFocus()) { - return false; - } - if (api.feature.on_key) { - if (api.feature.on_key({key:evt})) return; - } - for (let handler of api.feature.on_key2) { - if (handler({key:evt})) return; - } - if (evt.ctrlKey) { - switch (evt.key) { - case 'g': return api.group.merge(); - case 'u': return api.group.split(); - } - } - switch (evt.charCode) { - case cca('`'): api.show.slices(0); break; - case cca('0'): api.show.slices(api.var.layer_max); break; - case cca('1'): api.show.slices(api.var.layer_max/10); break; - case cca('2'): api.show.slices(api.var.layer_max*2/10); break; - case cca('3'): api.show.slices(api.var.layer_max*3/10); break; - case cca('4'): api.show.slices(api.var.layer_max*4/10); break; - case cca('5'): api.show.slices(api.var.layer_max*5/10); break; - case cca('6'): api.show.slices(api.var.layer_max*6/10); break; - case cca('7'): api.show.slices(api.var.layer_max*7/10); break; - case cca('8'): api.show.slices(api.var.layer_max*8/10); break; - case cca('9'): api.show.slices(api.var.layer_max*9/10); break; - case cca('?'): + platform.load_stl("/obj/cube.stl", function(vert) { + catalog.putFile("sample cube.stl", vert); + platform.update_bounds(); + space.view.home(); + setTimeout(() => { api.space.save(true) },500); + then(); api.help.show(); - break; - case cca('Z'): // reset stored state - uc.confirm('clear all settings and preferences?').then(yes => { - if (yes) { - sdb.clear(); - WIN.location.reload(); - } - }); - break; - case cca('C'): // refresh catalog - catalog.refresh(); - break; - case cca('i'): // file import - api.event.import(); - break; - case cca('S'): // slice - case cca('s'): // slice - api.function.slice(); - break; - case cca('P'): // prepare - case cca('p'): // prepare - if (api.mode.get() !== 'SLA') { - // hidden in SLA mode - api.function.print(); + }); + return true; + } + } + return false; +} + +function unitsSave() { + api.conf.update({ controller: true }); + platform.update_size(); +} + +function aniMeshSave() { + api.conf.update({ controller: true }); + api.conf.save(); +} + +function lineTypeSave() { + const sel = ui.lineType.options[ui.lineType.selectedIndex]; + if (sel) { + settings().controller.lineType = sel.value; + api.conf.save(); + } +} + +function thinWallSave() { + let opt = ui.sliceDetectThin; + let level = opt.options[opt.selectedIndex]; + if (level) { + settings().process.sliceDetectThin = level.value; + api.conf.save(); + } +} + +function detailSave() { + let level = ui.detail.options[ui.detail.selectedIndex]; + if (level) { + level = level.value; + let rez = base.config.clipperClean; + switch (level) { + case '100': rez = 50; break; + case '75': rez = base.config.clipperClean; break; + case '50': rez = 500; break; + case '25': rez = 1000; break; + } + client.config({ + base: { clipperClean: rez } + }); + settings().controller.detail = level; + api.conf.save(); + } +} + +function speedSave() { + settings().controller.showSpeeds = ui.showSpeeds.checked; + api.view.update_speeds(); +} + +function zAnchorSave() { + api.conf.update(); + api.platform.update_top_z(); +} + +function setThreaded(bool) { + if (bool) { + client.pool.start(); + } else { + client.pool.stop(); + } + return bool; +} + +// upon restore, seed presets +api.event.emit('preset', api.conf.dbo()); + +api.event.on("set.threaded", bool => setThreaded(bool)); + +function booleanSave() { + let control = settings().controller; + let doAlert = ui.ortho.checked !== control.ortho; + if (control.assembly != ui.assembly.checked) { + client.wasm(ui.assembly.checked); + } + if (control.antiAlias != ui.antiAlias.checked) { + api.show.alert('Page Reload Required to Change Aliasing'); + } + control.shiny = ui.shiny.checked; + control.drawer = ui.drawer.checked; + control.scrolls = ui.scrolls.checked; + control.showOrigin = ui.showOrigin.checked; + control.showRulers = ui.showRulers.checked; + control.autoLayout = ui.autoLayout.checked; + control.freeLayout = ui.freeLayout.checked; + control.spaceRandoX = ui.spaceRandoX.checked; + control.autoSave = ui.autoSave.checked; + control.antiAlias = ui.antiAlias.checked; + control.reverseZoom = ui.reverseZoom.checked; + control.dark = ui.dark.checked; + control.exportOcto = ui.exportOcto.checked; + control.exportGhost = ui.exportGhost.checked; + control.exportLocal = ui.exportLocal.checked; + control.exportThumb = ui.exportThumb.checked; + control.exportPreview = ui.exportPreview.checked; + control.healMesh = ui.healMesh.checked; + control.threaded = setThreaded(ui.threaded.checked); + control.assembly = ui.assembly.checked; + control.ortho = ui.ortho.checked; + control.devel = ui.devel.checked; + space.view.setZoom(control.reverseZoom, control.zoomSpeed); + // platform.layout(); + api.conf.save(); + api.platform.update_size(); + updateStats(); + updateDrawer(); + api.event.emit('boolean.update'); + if (doAlert) { + api.show.alert("change requires page refresh"); + } +} + +function updateDrawer() { + const { drawer, scrolls } = settings().controller; + $c('app', drawer ? 'slideshow' : '', drawer ? '' : 'slideshow'); + $c('app', scrolls ? '' : 'hide-scroll', scrolls ? 'hide-scroll' : ''); +} + +function updateStats() { + if (self.debug !== true) { + return; + } + let { div, fps, rms, rnfo } = ui.stats; + div.style.display = 'flex'; + setInterval(() => { + const nrms = space.view.getRMS().toFixed(1); + const nfps = space.view.getFPS().toFixed(1); + const rend = space.renderInfo(); + const { memory, render } = rend; + if (nfps !== fps.innerText) { + fps.innerText = nfps; + } + if (nrms !== rms.innerText) { + rms.innerText = nrms; + } + if (rnfo.offsetParent !== null) { + rnfo.innerHTML = Object.entries({ ...memory, ...render, render_ms: nrms, frames_sec: nfps }).map(row => { + return `
${row[0]}
` + }).join(''); + } + }, 100); +} + +function onBooleanClick(el) { + // copy some ui elements to target settings + let settings = api.conf.get(); + settings.device.bedBelt = ui.deviceBelt.checked && api.mode.is_fdm(); + settings.device.bedRound = ui.deviceRound.checked && api.mode.is_fdm(); + settings.device.originCenter = ui.deviceOrigin.checked || ui.deviceRound.checked; + settings.device.fwRetract = ui.fwRetract.checked; + // refresh vars and other ui elements + uc.refresh(); + if (el === ui.camStockIndexed) { + api.view.set_arrange(); + } + api.conf.update(); + DOC.activeElement.blur(); + api.event.emit("boolean.click"); + api.devices.update_laser_state(); +} + +function onButtonClick(ev) { + let target = ev.target; + while (target && target.tagName !== 'BUTTON') { + target = target.parentNode; + } + api.event.emit("button.click", target); +} + +function inputHasFocus() { + let active = DOC.activeElement; + return active && (active.nodeName === "INPUT" || active.nodeName === "TEXTAREA"); +} + +function cca(c) { + return c.charCodeAt(0); +} + +function keyUpHandler(evt) { + if (api.feature.on_key) { + if (api.feature.on_key({up:evt})) return; + } + for (let handler of api.feature.on_key2) { + if (handler({up:evt})) return; + } + switch (evt.keyCode) { + // escape + case 27: + // blur text input focus + DOC.activeElement.blur(); + // dismiss modals + api.modal.hide(); + // deselect widgets + platform.deselect(); + // hide all dialogs + api.dialog.hide(); + // cancel slicing + api.function.cancel(); + // and send an event (used by FDM client) + api.event.emit("key.esc"); + break; + } + return false; +} + +function keyDownHandler(evt) { + if (api.modal.visible()) { + return false; + } + if (api.feature.on_key) { + if (api.feature.on_key({down:evt})) return; + } + for (let handler of api.feature.on_key2) { + if (handler({down:evt})) return; + } + let move = evt.altKey ? 5 : 0, + deg = move ? 0 : -Math.PI / (evt.shiftKey ? 36 : 2); + switch (evt.keyCode) { + case 8: // apple: delete/backspace + case 46: // others: delete + if (inputHasFocus()) return false; + platform.delete(selection.meshes()); + evt.preventDefault(); + break; + case 37: // left arrow + if (inputHasFocus()) return false; + if (deg) selection.rotate(0, 0, -deg); + if (move > 0) selection.move(-move, 0, 0); + evt.preventDefault(); + break; + case 39: // right arrow + if (inputHasFocus()) return false; + if (deg) selection.rotate(0, 0, deg); + if (move > 0) selection.move(move, 0, 0); + evt.preventDefault(); + break; + case 38: // up arrow + if (inputHasFocus()) return false; + if (evt.metaKey) return api.show.layer(api.var.layer_at+1); + if (deg) selection.rotate(deg, 0, 0); + if (move > 0) selection.move(0, move, 0); + evt.preventDefault(); + break; + case 40: // down arrow + if (inputHasFocus()) return false; + if (evt.metaKey) return api.show.layer(api.var.layer_at-1); + if (deg) selection.rotate(-deg, 0, 0); + if (move > 0) selection.move(0, -move, 0); + evt.preventDefault(); + break; + case 65: // 'a' for select all + if (evt.metaKey || evt.ctrlKey) { + if (inputHasFocus()) return false; + evt.preventDefault(); + platform.deselect(); + platform.select_all(); + } + break; + case 83: // 's' for save workspace + if (evt.ctrlKey) { + evt.preventDefault(); + api.conf.save(); + console.log("settings saved"); + } else + if (evt.metaKey) { + evt.preventDefault(); + api.space.save(); + set_ctrl.sync.put(); + } + break; + case 76: // 'l' for restore workspace + if (evt.metaKey) { + evt.preventDefault(); + api.space.restore(); + } + break; + } +} + +function keyHandler(evt) { + let handled = true; + if (api.modal.visible() || inputHasFocus()) { + return false; + } + if (api.feature.on_key) { + if (api.feature.on_key({key:evt})) return; + } + for (let handler of api.feature.on_key2) { + if (handler({key:evt})) return; + } + if (evt.ctrlKey) { + switch (evt.key) { + case 'g': return api.group.merge(); + case 'u': return api.group.split(); + } + } + switch (evt.charCode) { + case cca('`'): api.show.slices(0); break; + case cca('0'): api.show.slices(api.var.layer_max); break; + case cca('1'): api.show.slices(api.var.layer_max/10); break; + case cca('2'): api.show.slices(api.var.layer_max*2/10); break; + case cca('3'): api.show.slices(api.var.layer_max*3/10); break; + case cca('4'): api.show.slices(api.var.layer_max*4/10); break; + case cca('5'): api.show.slices(api.var.layer_max*5/10); break; + case cca('6'): api.show.slices(api.var.layer_max*6/10); break; + case cca('7'): api.show.slices(api.var.layer_max*7/10); break; + case cca('8'): api.show.slices(api.var.layer_max*8/10); break; + case cca('9'): api.show.slices(api.var.layer_max*9/10); break; + case cca('?'): + api.help.show(); + break; + case cca('Z'): // reset stored state + uc.confirm('clear all settings and preferences?').then(yes => { + if (yes) { + sdb.clear(); + WIN.location.reload(); } - break; - case cca('X'): // export - case cca('x'): // export - api.function.export(); - break; - case cca('g'): // CAM animate - api.function.animate(); - break; - case cca('O'): // manual rotation - rotateInputSelection(); - break; - case cca('r'): // recent files - api.modal.show('files'); - break; - case cca('q'): // preferences - api.modal.show('prefs'); - break; - case cca('l'): // device - settingsLoad(); - break; - case cca('e'): // device - api.show.devices(); - break; - case cca('o'): // tools - api.show.tools(); - break; - case cca('c'): // local devices - api.show.local(); - break; - case cca('v'): // toggle single slice view mode - if (api.view.get() === VIEWS.ARRANGE) { + }); + break; + case cca('C'): // refresh catalog + catalog.refresh(); + break; + case cca('i'): // file import + api.event.import(); + break; + case cca('S'): // slice + case cca('s'): // slice + api.function.slice(); + break; + case cca('P'): // prepare + case cca('p'): // prepare + if (api.mode.get() !== 'SLA') { + // hidden in SLA mode + api.function.prepare(); + } + break; + case cca('X'): // export + case cca('x'): // export + api.function.export(); + break; + case cca('g'): // CAM animate + api.function.animate(); + break; + case cca('O'): // manual rotation + rotateInputSelection(); + break; + case cca('r'): // recent files + api.modal.show('files'); + break; + case cca('q'): // preferences + api.modal.show('prefs'); + break; + case cca('l'): // device + settingsLoad(); + break; + case cca('e'): // device + api.show.devices(); + break; + case cca('o'): // tools + api.show.tools(); + break; + case cca('c'): // local devices + api.show.local(); + break; + case cca('v'): // toggle single slice view mode + if (api.view.get() === VIEWS.ARRANGE) { + api.space.set_focus(selection.widgets()); + } + if (api.var.layer_hi == api.var.layer_lo) { + api.var.layer_lo = 0; + } else { + api.var.layer_lo = api.var.layer_hi; + } + api.show.slices(); + break; + case cca('d'): // duplicate object + duplicateSelection(); + break; + case cca('m'): // mirror object + mirrorSelection(); + break; + case cca('a'): + if (api.view.get() === VIEWS.ARRANGE) { + // auto arrange items on platform + platform.layout(); + if (!api.conf.get().controller.spaceRandoX) { api.space.set_focus(selection.widgets()); } - if (api.var.layer_hi == api.var.layer_lo) { - api.var.layer_lo = 0; - } else { - api.var.layer_lo = api.var.layer_hi; - } - api.show.slices(); - break; - case cca('d'): // duplicate object - duplicateSelection(); - break; - case cca('m'): // mirror object - mirrorSelection(); - break; - case cca('a'): - if (api.view.get() === VIEWS.ARRANGE) { - // auto arrange items on platform - platform.layout(); - if (!api.conf.get().controller.spaceRandoX) { - api.space.set_focus(selection.widgets()); - } - } else { - // go to arrange view - api.view.set(VIEWS.ARRANGE); - } - break; - default: - api.event.emit('keypress', evt); - handled = false; - break; - } - if (handled) { - evt.preventDefault(); - evt.stopPropagation(); - } - return false; - } - - function duplicateSelection() { - selection.duplicate(); - } - - function mirrorSelection() { - selection.mirror(); - } - - function keys(o) { - let key, list = []; - for (key in o) { if (o.hasOwnProperty(key)) list.push(key) } - return list.sort(); - } - - function clearSelected(children) { - for (let i=0; i { - coord = (coord || '').split(','); - let prod = Math.PI / 180, - x = parseFloat(coord[0] || 0.0) * prod, - y = parseFloat(coord[1] || 0.0) * prod, - z = parseFloat(coord[2] || 0.0) * prod; - selection.rotate(x, y, z); - }); - } - - function positionSelection() { - if (selection.meshes().length === 0) { - api.show.alert("select object to position"); - return; - } - let current = settings(), - { device, process} = current, - center = process.ctOriginCenter || process.camOriginCenter || device.bedRound || device.originCenter, - bounds = boundsSelection(); - - api.uc.prompt("Enter X,Y coordinates for selection","").then(coord => { - coord = (coord || '').split(','); - let x = parseFloat(coord[0] || 0.0), - y = parseFloat(coord[1] || 0.0), - z = parseFloat(coord[2] || 0.0); - - if (!center) { - x = x - device.bedWidth/2 + (bounds.max.x - bounds.min.x)/2; - y = y - device.bedDepth/2 + (bounds.max.y - bounds.min.y)/2 + } else { + // go to arrange view + api.view.set(VIEWS.ARRANGE); } - - selection.move(x, y, z, true); - }); + break; + default: + api.event.emit('keypress', evt); + handled = false; + break; } - - function deviceExport(exp, name) { - name = (name || "device") - .toLowerCase() - .replace(/ /g,'_') - .replace(/\./g,'_'); - uc.prompt("Export Device Filename", name).then(name => { - if (name) { - api.util.download(exp, `${name}.km`); - } - }); + if (handled) { + evt.preventDefault(); + evt.stopPropagation(); } + return false; +} - function objectsExport(format = "stl") { - // return selection.export(); - uc.confirm("Export Filename", {ok:true, cancel: false}, `selected.${format}`).then(name => { - if (!name) return; - if (name.toLowerCase().indexOf(`.${format}`) < 0) { - name = `${name}.${format}`; - } - api.util.download(selection.export(format), name); - }); +function duplicateSelection() { + selection.duplicate(); +} + +function mirrorSelection() { + selection.mirror(); +} + +function keys(o) { + let key, list = []; + for (key in o) { if (o.hasOwnProperty(key)) list.push(key) } + return list.sort(); +} + +function clearSelected(children) { + for (let i=0; i { - if (value === true) { - let proc = api.settings.proc(); - proc.ops && (proc.ops.length = 0); - proc.op2 && (proc.op2.length = 0); - api.platform.clear(); - } - }); +function rotateInputSelection() { + if (selection.meshes().length === 0) { + api.show.alert("select object to rotate"); + return; } + api.uc.prompt("Enter X,Y,Z degrees of rotation","").then(coord => { + coord = (coord || '').split(','); + let prod = Math.PI / 180, + x = parseFloat(coord[0] || 0.0) * prod, + y = parseFloat(coord[1] || 0.0) * prod, + z = parseFloat(coord[2] || 0.0) * prod; + selection.rotate(x, y, z); + }); +} - function profileExport() { - const opt = {pre: [ - "
", - "

Workspace Export

", - " ", - " ", - "  Exclude meshes", - "
" - ]}; - let suggestion = "workspace"; - let file = api.widgets.all()[0]?.meta.file || ''; - if (file) { - suggestion = `${suggestion}_${file.split('.')[0]}`.replaceAll(' ','_'); - }; - uc.confirm("Filename", {ok:true, cancel: false}, suggestion, opt).then(name => { - if (!name) return; - - let work = !$('excwork').checked; - let json = api.conf.export({work, clear:true}); - - kiri.client.zip([ - {name:"workspace.json", data:JSON.stringify(json)} - ], progress => { - api.show.progress(progress.percent/100, "compressing workspace"); - }, output => { - api.show.progress(0); - api.util.download(output, `${name}.kmz`); - }); - }); +function positionSelection() { + if (selection.meshes().length === 0) { + api.show.alert("select object to position"); + return; } + let current = settings(), + { device, process} = current, + center = process.ctOriginCenter || process.camOriginCenter || device.bedRound || device.originCenter, + bounds = boundsSelection(); - function settingsSave(ev, name) { - if (ev) { - ev.stopPropagation(); - ev.preventDefault(); + api.uc.prompt("Enter X,Y coordinates for selection","").then(coord => { + coord = (coord || '').split(','); + let x = parseFloat(coord[0] || 0.0), + y = parseFloat(coord[1] || 0.0), + z = parseFloat(coord[2] || 0.0); + + if (!center) { + x = x - device.bedWidth/2 + (bounds.max.x - bounds.min.x)/2; + y = y - device.bedDepth/2 + (bounds.max.y - bounds.min.y)/2 } - api.dialog.hide(); - let mode = api.mode.get(), - s = settings(), - def = "default", - cp = s.process, - pl = s.sproc[mode], - lp = s.cproc[mode], - saveAs = (name) => { - if (!name) { - return; - } - let np = pl[name] = {}; - cp.processName = name; - pl[name] = Object.clone(cp); - for (let k in cp) { - if (!cp.hasOwnProperty(k)) continue; - np[k] = cp[k]; - } - s.cproc[mode] = name; - s.devproc[s.device.deviceName] = name; - api.conf.save(); - api.conf.update(); - api.event.settings(); - sync_put(); - }; + selection.move(x, y, z, true); + }); +} +function deviceExport(exp, name) { + name = (name || "device") + .toLowerCase() + .replace(/ /g,'_') + .replace(/\./g,'_'); + uc.prompt("Export Device Filename", name).then(name => { if (name) { - saveAs(name); - } else { - uc.prompt("Save Settings As", cp ? lp || def : def).then(saveAs); + api.util.download(exp, `${name}.km`); } - } + }); +} - function settingsLoad() { - api.conf.show(); - } - - function updateDeviceSize() { - api.conf.update(); - platform.update_size(); - platform.update_origin(); - } - - async function sync_put() { - await api.settings.sync.put(); - } - - function dragOverHandler(evt) { - evt.stopPropagation(); - evt.preventDefault(); - - // prevent drop actions when a dialog is open - if (api.modal.visible()) { - return; +function objectsExport(format = "stl") { + // return selection.export(); + uc.confirm("Export Filename", {ok:true, cancel: false}, `selected.${format}`).then(name => { + if (!name) return; + if (name.toLowerCase().indexOf(`.${format}`) < 0) { + name = `${name}.${format}`; } + api.util.download(selection.export(format), name); + }); +} - evt.dataTransfer.dropEffect = 'copy'; - let oldcolor = space.platform.setColor(0x00ff00); - if (oldcolor !== 0x00ff00) platformColor = oldcolor; +function workspaceNew() { + uc.confirm("Clear Workspace?", {ok:true, cancel: false}).then(value => { + if (value === true) { + let proc = set_ctrl.proc(); + proc.ops && (proc.ops.length = 0); + proc.op2 && (proc.op2.length = 0); + api.platform.clear(); + } + }); +} + +function profileExport() { + const opt = {pre: [ + "
", + "

Workspace Export

", + " ", + " ", + "  Exclude meshes", + "
" + ]}; + let suggestion = "workspace"; + let file = api.widgets.all()[0]?.meta.file || ''; + if (file) { + suggestion = `${suggestion}_${file.split('.')[0]}`.replaceAll(' ','_'); + }; + uc.confirm("Filename", {ok:true, cancel: false}, suggestion, opt).then(name => { + if (!name) return; + + let work = !$('excwork').checked; + let json = api.conf.export({work, clear:true}); + + client.zip([ + {name:"workspace.json", data:JSON.stringify(json)} + ], progress => { + api.show.progress(progress.percent/100, "compressing workspace"); + }, output => { + api.show.progress(0); + api.util.download(output, `${name}.kmz`); + }); + }); +} + +function settingsSave(ev, name) { + if (ev) { + ev.stopPropagation(); + ev.preventDefault(); } - function dragLeave() { - space.platform.setColor(platformColor); + api.dialog.hide(); + let mode = api.mode.get(), + s = settings(), + def = "default", + cp = s.process, + pl = s.sproc[mode], + lp = s.cproc[mode], + saveAs = (name) => { + if (!name) { + return; + } + let np = pl[name] = {}; + cp.processName = name; + pl[name] = Object.clone(cp); + for (let k in cp) { + if (!cp.hasOwnProperty(k)) continue; + np[k] = cp[k]; + } + s.cproc[mode] = name; + s.devproc[s.device.deviceName] = name; + api.conf.save(); + api.conf.update(); + api.event.settings(); + sync_put(); + }; + + if (name) { + saveAs(name); + } else { + uc.prompt("Save Settings As", cp ? lp || def : def).then(saveAs); + } +} + +function settingsLoad() { + api.conf.show(); +} + +function updateDeviceSize() { + api.conf.update(); + platform.update_size(); + platform.update_origin(); +} + +async function sync_put() { + await set_ctrl.sync.put(); +} + +function dragOverHandler(evt) { + evt.stopPropagation(); + evt.preventDefault(); + + // prevent drop actions when a dialog is open + if (api.modal.visible()) { + return; } - function dropHandler(evt) { - evt.stopPropagation(); - evt.preventDefault(); + evt.dataTransfer.dropEffect = 'copy'; + let oldcolor = space.platform.setColor(0x00ff00); + if (oldcolor !== 0x00ff00) platformColor = oldcolor; +} - // prevent drop actions when a dialog is open - if (api.modal.visible()) { - return; - } +function dragLeave() { + space.platform.setColor(platformColor); +} - space.platform.setColor(platformColor); +function dropHandler(evt) { + evt.stopPropagation(); + evt.preventDefault(); - let files = evt.dataTransfer.files; + // prevent drop actions when a dialog is open + if (api.modal.visible()) { + return; + } - switch (api.feature.drop_group) { - case true: - return api.platform.load_files(files, []); - case false: - return api.platform.load_files(files, undefined); - } + space.platform.setColor(platformColor); - if (files.length === 1) { - api.platform.load_files(files); - } else if (files.length > 1) { - uc.confirm(`group ${files.length} files?`).then(yes => { - api.platform.load_files(files, yes ? [] : undefined); + let files = evt.dataTransfer.files; + + switch (api.feature.drop_group) { + case true: + return api.platform.load_files(files, []); + case false: + return api.platform.load_files(files, undefined); + } + + if (files.length === 1) { + api.platform.load_files(files); + } else if (files.length > 1) { + uc.confirm(`group ${files.length} files?`).then(yes => { + api.platform.load_files(files, yes ? [] : undefined); + }); + } +} + +function loadCatalogFile(e) { + api.widgets.load(e.target.getAttribute('load'), function(widget) { + platform.add(widget); + api.dialog.hide(); + }); +} + +function updateCatalog(files) { + let table = ui.catalogList, + list = []; + table.innerHTML = ''; + for (let name in files) { + list.push({n:name, ln:name.toLowerCase(), v:files[name].vertices, t:files[name].updated}); + } + list.sort(function(a,b) { + return a.ln < b.ln ? -1 : 1; + }); + for (let i=0; i { + api.uc.prompt(`rename file`, short).then(newname => { + if (newname && newname !== short) { + catalog.rename(name, `${newname}${ext}`, then => { + api.modal.show('files'); + }); + } }); - } + }; + + load.setAttribute('load', name); + load.setAttribute('title', `file: ${name}\nvertices: ${file.v}\ndate: ${date}`); + load.onclick = loadCatalogFile; + load.appendChild(DOC.createTextNode(short)); + + del.setAttribute('del', name); + del.setAttribute('title', "remove '"+name+"'"); + del.onclick = () => { catalog.deleteFile(name) }; + del.innerHTML = ''; + + size.setAttribute("disabled", true); + size.setAttribute("class", "label"); + size.appendChild(DOC.createTextNode(base.util.comma(file.v))); + + row.setAttribute("class", "f-row a-center"); + row.appendChild(renm); + row.appendChild(load); + row.appendChild(size); + row.appendChild(del); + table.appendChild(row); } +} - function loadCatalogFile(e) { - api.widgets.load(e.target.getAttribute('load'), function(widget) { - platform.add(widget); - api.dialog.hide(); - }); - } +function isMultiHead() { + let dev = api.conf.get().device; + return isNotBelt() && dev.extruders && dev.extruders.length > 1; +} - function updateCatalog(files) { - let table = ui.catalogList, - list = []; - table.innerHTML = ''; - for (let name in files) { - list.push({n:name, ln:name.toLowerCase(), v:files[name].vertices, t:files[name].updated}); +function isBelt() { + return api.device.isBelt(); +} + +function isNotBelt() { + return !isBelt(); +} + +// MAIN INITIALIZATION FUNCTION +function init_one() { + let { event, conf, view, show } = api, + { bound, toInt, toFloat } = uc, + { newBlank, newButton, newBoolean, newGroup, newInput } = uc, + { newSelect, newLabel, newValue, newRow, newGCode, newDiv } = uc; + + event.emit('init.one'); + + // ensure we have settings from last session + conf.restore(); + + let container = $('container'), + gcode = $('dev-gcode'), + tracker = $('tracker'), + controller = settings().controller; + + WIN.addEventListener("resize", () => { + event.emit('resize'); + }); + + event.on('resize', () => { + if (WIN.innerHeight < 800) { + ui.modalBox.classList.add('mh85'); + } else { + ui.modalBox.classList.remove('mh85'); } - list.sort(function(a,b) { - return a.ln < b.ln ? -1 : 1; - }); - for (let i=0; i { - api.uc.prompt(`rename file`, short).then(newname => { - if (newname && newname !== short) { - catalog.rename(name, `${newname}${ext}`, then => { - api.modal.show('files'); - }); - } - }); - }; - - load.setAttribute('load', name); - load.setAttribute('title', `file: ${name}\nvertices: ${file.v}\ndate: ${date}`); - load.onclick = loadCatalogFile; - load.appendChild(DOC.createTextNode(short)); - - del.setAttribute('del', name); - del.setAttribute('title', "remove '"+name+"'"); - del.onclick = () => { catalog.deleteFile(name) }; - del.innerHTML = ''; - - size.setAttribute("disabled", true); - size.setAttribute("class", "label"); - size.appendChild(DOC.createTextNode(base.util.comma(file.v))); - - row.setAttribute("class", "f-row a-center"); - row.appendChild(renm); - row.appendChild(load); - row.appendChild(size); - row.appendChild(del); - table.appendChild(row); - } - } - - function isMultiHead() { - let dev = api.conf.get().device; - return isNotBelt() && dev.extruders && dev.extruders.length > 1; - } - - function isBelt() { - return api.device.isBelt(); - } - - function isNotBelt() { - return !isBelt(); - } - - // MAIN INITIALIZATION FUNCTION - function init_one() { - let { event, conf, view, show } = api, - { bound, toInt, toFloat } = uc, - { newBlank, newButton, newBoolean, newGroup, newInput } = uc, - { newSelect, newLabel, newValue, newRow, newGCode, newDiv } = uc; - - event.emit('init.one'); - - // ensure we have settings from last session - conf.restore(); - - let container = $('container'), - gcode = $('dev-gcode'), - tracker = $('tracker'), - controller = settings().controller; - - WIN.addEventListener("resize", () => { - event.emit('resize'); - }); - - event.on('resize', () => { - if (WIN.innerHeight < 800) { - ui.modalBox.classList.add('mh85'); - } else { - ui.modalBox.classList.remove('mh85'); + space.sky.showGrid(false); + space.sky.setColor(controller.dark ? 0 : 0xffffff); + space.setAntiAlias(controller.antiAlias); + space.init(container, function (delta) { + let vars = api.var; + if (vars.layer_max === 0 || !delta) return; + if (controller.reverseZoom) delta = -delta; + let same = vars.layer_hi === vars.layer_lo; + let track = vars.layer_lo > 0; + if (delta > 0) { + vars.layer_hi = Math.max(same ? 0 : vars.layer_lo, vars.layer_hi - 1); + if (track) { + vars.layer_lo = Math.max(0, vars.layer_lo - 1); } - view.update_slider(); - }); - - space.sky.showGrid(false); - space.sky.setColor(controller.dark ? 0 : 0xffffff); - space.setAntiAlias(controller.antiAlias); - space.init(container, function (delta) { - let vars = api.var; - if (vars.layer_max === 0 || !delta) return; - if (controller.reverseZoom) delta = -delta; - let same = vars.layer_hi === vars.layer_lo; - let track = vars.layer_lo > 0; - if (delta > 0) { - vars.layer_hi = Math.max(same ? 0 : vars.layer_lo, vars.layer_hi - 1); - if (track) { - vars.layer_lo = Math.max(0, vars.layer_lo - 1); - } - } else if (delta < 0) { - vars.layer_hi = Math.min(vars.layer_max, vars.layer_hi + 1); - if (track) { - vars.layer_lo = Math.min(vars.layer_hi, vars.layer_lo + 1); - } + } else if (delta < 0) { + vars.layer_hi = Math.min(vars.layer_max, vars.layer_hi + 1); + if (track) { + vars.layer_lo = Math.min(vars.layer_hi, vars.layer_lo + 1); } - if (same) { - vars.layer_lo = vars.layer_hi; - } - view.update_slider(); - show.slices(); - }, controller.ortho); - space.platform.onMove(conf.save); - space.platform.setRound(true); - space.useDefaultKeys(api.feature.on_key === undefined || api.feature.on_key_defaults); - updateDrawer(); - - // api augmentation with local functions - api.device.export = deviceExport; - let anim = ui.anim = {}; - - Object.assign(ui, { - tracker: tracker, - container: container, - - alert: { - dialog: $('alert-area'), - text: $('alert-text') - }, - func: { - slice: $('act-slice'), - preview: $('act-preview'), - animate: $('act-animate'), - export: $('act-export') - }, - label: { - slice: $('label-slice'), - preview: $('label-preview'), - animate: $('label-animate'), - export: $('label-export'), - }, - acct: { - help: $('app-help'), - don8: $('app-don8'), - mesh: $('app-mesh'), - export: $('app-export') - }, - dev: { - header: $('dev-header'), - search: $('dev-search'), - filter: $('dev-filter') - }, - mesh: { - name: $('mesh-name'), - points: $('mesh-points'), - faces: $('mesh-faces'), - }, - - stats: { - fps: $('fps'), - rms: $('rms'), - div: $('stats'), - rnfo: $('rnfo'), - }, - - load: $('load-file'), - speeds: $('speeds'), - speedbar: $('speedbar'), - context: $('context-menu'), - - ltsetup: $('lt-setup'), - ltfile: $('lt-file'), - ltview: $('lt-view'), - ltact: $('lt-start'), - edit: $('lt-tools'), - nozzle: $('menu-nozzle'), - - modal: $('modal'), - modalBox: $('modal-box'), - modals: { - help: $('mod-help'), - setup: $('mod-setup'), - prefs: $('mod-prefs'), - files: $('mod-files'), - saves: $('mod-saves'), - tools: $('mod-tools'), - xany: $('mod-x-any'), - xsla: $('mod-x-sla'), - xlaser: $('mod-x-laser'), - local: $('mod-local'), - don8: $('mod-don8'), - any: $('mod-any'), - }, - - catalogBody: $('catalogBody'), - catalogList: $('catalogList'), - - devices: $('devices'), - deviceAdd: $('device-add'), - deviceDelete: $('device-del'), - deviceRename: $('device-ren'), - deviceExport: $('device-exp'), - deviceSave: $('device-save'), - - toolsSave: $('tools-save'), - toolsClose: $('tools-close'), - toolsImport: $('tools-import'), - toolsExport: $('tools-export'), - toolSelect: $('tool-select'), - toolAdd: $('tool-add'), - toolCopy: $('tool-dup'), - toolDelete: $('tool-del'), - toolType: $('tool-type'), - toolName: $('tool-name'), - toolNum: $('tool-num'), - toolFluteDiam: $('tool-fdiam'), - toolFluteLen: $('tool-flen'), - toolShaftDiam: $('tool-sdiam'), - toolShaftLen: $('tool-slen'), - toolTaperAngle: $('tool-tangle'), - toolTaperTip: $('tool-ttip'), - toolMetric: $('tool-metric'), - - setMenu: $('set-menu'), - settings: $('settings'), - settingsBody: $('settingsBody'), - settingsList: $('settingsList'), - - slider: $('slider'), - sliderMax: $('slider-max'), - sliderMin: $('slider-zero'), - sliderLo: $('slider-lo'), - sliderMid: $('slider-mid'), - sliderHi: $('slider-hi'), - sliderHold: $('slider-hold'), - sliderRange: $('slider-center'), - - loading: $('progress').style, - progress: $('progbar').style, - prostatus: $('progtxt'), - - selection: $('selection'), - sizeX: $('size_x'), - sizeY: $('size_y'), - sizeZ: $('size_z'), - scaleX: $('scale_x'), - scaleY: $('scale_y'), - scaleZ: $('scale_z'), - lockX: $('lock_x'), - lockY: $('lock_y'), - lockZ: $('lock_z'), - stock: $('stock'), - stockWidth: $('stock-width'), - stockDepth: $('stock-width'), - stockHeight: $('stock-width'), - - /** CAM Animation Bar */ - - _____: { - _____: newDiv({ addto: $('layer-animate'), set:true }), - row: newRow([ - anim.replay = newButton(null,"anim.replay",{icon:'',title:"restart"}), - anim.play = newButton(null,"anim.play",{icon:'',title:"play"}), - anim.pause = newButton(null,"anim.pause",{icon:'',title:"pause"}), - anim.step = newButton(null,"anim.step",{icon:'',title:"single step"}), - anim.speed = newButton(null,"anim.fast",{icon:'',title:"toggle speed"}), - anim.labspd = newValue(3, {class:"center padleft"}), - anim.labx = newLabel("X", {class:"padleft"}), - anim.valx = newValue(7, {class:"center"}), - anim.laby = newLabel("Y", {class:"padleft"}), - anim.valy = newValue(7, {class:"center"}), - anim.labz = newLabel("Z", {class:"padleft"}), - anim.valz = newValue(6, {class:"center"}), - anim.laba = newLabel("A", {class:"padleft hide"}), - anim.vala = newValue(6, {class:"center hide"}), - anim.labpro = newLabel("%", {class:"padleft"}), - anim.progress = newValue(5, {class:"center"}), - anim.trans = newButton(null,"anim.trans",{icon:'',title:"transparency",class:"padleft"}), - anim.model = newButton(null,"anim.model",{icon:'',title:"show model"}), - anim.shade = newButton(null,"anim.stock",{icon:'',title:"stock box"}), - ]) - }, - - /** Device Browser / Editor */ - - _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true }), - device: newGroup(LANG.dv_gr_dev, null, {group:"ddev", inline, class:"noshow"}), - - _____: newGroup("workspace", null, {group:"dext", inline}), - bedWidth: newInput('X (width)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), - bedDepth: newInput('Y (depth)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), - maxHeight: newInput('Z (height)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), - resolutionX: newInput(LANG.dv_rezx_s, {title:LANG.dv_rezx_l, convert:toInt, size:6, modes:SLA}), - resolutionY: newInput(LANG.dv_rezy_s, {title:LANG.dv_rezy_l, convert:toInt, size:6, modes:SLA}), - _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:NO_WEDM }), - _____: newGroup("firmware", null, {group:"dext", inline, modes:NO_WEDM}), - fwRetract: newBoolean(LANG.dv_retr_s, onBooleanClick, {title:LANG.dv_retr_l, modes:FDM}), - deviceOrigin: newBoolean(LANG.dv_orgc_s, onBooleanClick, {title:LANG.dv_orgc_l, modes:FDM_LZN, show:() => !ui.deviceRound.checked}), - deviceRound: newBoolean(LANG.dv_bedc_s, onBooleanClick, {title:LANG.dv_bedc_l, modes:FDM, trigger, show:isNotBelt}), - deviceBelt: newBoolean(LANG.dv_belt_s, onBooleanClick, {title:LANG.dv_belt_l, modes:FDM, trigger, show:() => !ui.deviceRound.checked}), - separator: newBlank({class:"pop-sep", modes:FDM, driven}), - spindleMax: newInput(LANG.dv_spmx_s, {title:LANG.dv_spmx_l, convert:toInt, size:5, modes:CAM, trigger}), - deviceZMax: newInput(LANG.dv_zmax_s, {title:LANG.dv_zmax_l, convert:toInt, size:5, modes:FDM}), - gcodeTime: newInput(LANG.dv_time_s, {title:LANG.dv_time_l, convert:toFloat, size:5, modes:FDM}), - _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:FDM }), - extruder: newGroup(LANG.dv_gr_ext, null, {group:"dext", inline}), - extFilament: newInput(LANG.dv_fila_s, {title:LANG.dv_fila_l, convert:toFloat, modes:FDM}), - extNozzle: newInput(LANG.dv_nozl_s, {title:LANG.dv_nozl_l, convert:toFloat, modes:FDM}), - extOffsetX: newInput(LANG.dv_exox_s, {title:LANG.dv_exox_l, convert:toFloat, modes:FDM}), - extOffsetY: newInput(LANG.dv_exoy_s, {title:LANG.dv_exoy_l, convert:toFloat, modes:FDM}), - extPad: newBlank({class:"grow", modes:FDM}), - separator: newBlank({class:"pop-sep", modes:FDM, driven}), - extActions: newRow([ - ui.extPrev = newButton(undefined, undefined, {icon:''}), - ui.extAdd = newButton(undefined, undefined, {icon:''}), - ui.extDel = newButton(undefined, undefined, {icon:''}), - ui.extNext = newButton(undefined, undefined, {icon:''}) - ], {class:"dev-buttons ext-buttons var-row", modes:FDM}), - _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:CAM_LZR }), - _____: newGroup(LANG.dv_gr_out, null, {group:"dgco", inline}), - gcodeStrip: newBoolean(LANG.dv_strc_s, onBooleanClick, {title:LANG.dv_strc_l, modes:CAM}), - gcodeSpace: newBoolean(LANG.dv_tksp_s, onBooleanClick, {title:LANG.dv_tksp_l, modes:CAM_LZR}), - laserMaxPower: newInput(LANG.ou_maxp_s, {title:LANG.ou_maxp_l, modes:LASER, size:7, text:true}), - useLaser: newBoolean(LANG.dv_lazr_s, onBooleanClick, {title:LANG.dv_lazr_l, modes:CAM}), - gcodeFExt: newInput(LANG.dv_fext_s, {title:LANG.dv_fext_l, modes:CAM_LZR, size:7, text:true}), - gcodeEd: newGroup(LANG.dv_gr_gco, $('dg'), {group:"dgcp", inline, modes:GCODE}), - gcodeMacros: newRow([ - (ui.gcodePre = newGCode(LANG.dv_head_s, {title:LANG.dv_head_l, modes:GCODE, area:gcode})).button, - (ui.gcodePost = newGCode(LANG.dv_foot_s, {title:LANG.dv_foot_l, modes:GCODE, area:gcode})).button, - (ui.gcodeLayer = newGCode(LANG.dv_layr_s, {title:LANG.dv_layr_l, modes:FDM, area:gcode})).button, - (ui.gcodeTrack = newGCode(LANG.dv_prog_s, {title:LANG.dv_prog_l, modes:FDM, area:gcode})).button, - (ui.gcodeFan = newGCode(LANG.dv_fanp_s, {title:LANG.dv_fanp_l, modes:FDM, area:gcode})).button, - (ui.gcodeFeature = newGCode(LANG.dv_feat_s, {title:LANG.dv_feat_l, modes:FDM, area:gcode})).button, - (ui.gcodeLaserOn = newGCode(LANG.dv_lzon_s, {title:LANG.dv_lzon_l, modes:LASER, area:gcode})).button, - (ui.gcodeLaserOff = newGCode(LANG.dv_lzof_s, {title:LANG.dv_lzof_l, modes:LASER, area:gcode})).button, - (ui.gcodeWaterOn = newGCode(LANG.dv_waon_s, {title:LANG.dv_waon_l, modes:WJET, area:gcode})).button, - (ui.gcodeWaterOff = newGCode(LANG.dv_waof_s, {title:LANG.dv_waof_l, modes:WJET, area:gcode})).button, - (ui.gcodeKnifeDn = newGCode(LANG.dv_dkon_s, {title:LANG.dv_dkon_l, modes:DRAG, area:gcode})).button, - (ui.gcodeKnifeUp = newGCode(LANG.dv_dkof_s, {title:LANG.dv_dkof_l, modes:DRAG, area:gcode})).button, - (ui.gcodeChange = newGCode(LANG.dv_tool_s, {title:LANG.dv_tool_l, modes:FDM_CAM, area:gcode})).button, - (ui.gcodeDwell = newGCode(LANG.dv_dwll_s, {title:LANG.dv_dwll_l, modes:CAM, area:gcode})).button, - (ui.gcodeSpindle = newGCode(LANG.dv_sspd_s, {title:LANG.dv_sspd_l, modes:CAM, area:gcode, show:() => ui.spindleMax.value > 0})).button - ], {class:"ext-buttons f-row gcode-macros"}), - - /** Preferences Menu */ - - _____: newGroup(LANG.op_menu, $('prefs-gen1'), {inline}), - antiAlias: newBoolean(LANG.op_anta_s, booleanSave, {title:LANG.op_anta_l}), - reverseZoom: newBoolean(LANG.op_invr_s, booleanSave, {title:LANG.op_invr_l}), - ortho: newBoolean(LANG.op_orth_s, booleanSave, {title:LANG.op_orth_l}), - dark: newBoolean(LANG.op_dark_s, booleanSave, {title:LANG.op_dark_l}), - drawer: newBoolean('slide out', booleanSave, {title:'slide out settings drawer'}), - scrolls: newBoolean('scrollbars', booleanSave, {title:'show scrollbars'}), - devel: newBoolean(LANG.op_devl_s, booleanSave, {title:LANG.op_devl_l}), - _____: newGroup(LANG.op_disp, $('prefs-gen2'), {inline}), - showOrigin: newBoolean(LANG.op_shor_s, booleanSave, {title:LANG.op_shor_l}), - showRulers: newBoolean(LANG.op_shru_s, booleanSave, {title:LANG.op_shru_l}), - showSpeeds: newBoolean(LANG.op_sped_s, speedSave, {title:LANG.op_sped_l}), - shiny: newBoolean(LANG.op_shny_s, booleanSave, {title:LANG.op_shny_l, modes:FDM}), - lineType: newSelect(LANG.op_line_s, {title: LANG.op_line_l, action: lineTypeSave, modes:FDM}, "linetype"), - animesh: newSelect(LANG.op_anim_s, {title: LANG.op_anim_l, action: aniMeshSave, modes:CAM}, "animesh"), - units: newSelect(LANG.op_unit_s, {title: LANG.op_unit_l, action: unitsSave, modes:CAM}, "units"), - edgeangle: newInput(LANG.op_spoa_s, {title:LANG.op_spoa_l, convert:toFloat, size:3}), - _____: newGroup(LANG.lo_menu, $('prefs-lay'), {inline}), - autoSave: newBoolean(LANG.op_save_s, booleanSave, {title:LANG.op_save_l}), - autoLayout: newBoolean(LANG.op_auto_s, booleanSave, {title:LANG.op_auto_l}), - freeLayout: newBoolean(LANG.op_free_s, booleanSave, {title:LANG.op_free_l}), - spaceRandoX: newBoolean(LANG.op_spcx_s, booleanSave, {title:LANG.op_spcx_l, show:isBelt}), - spaceLayout: newInput(LANG.op_spcr_s, {title:LANG.op_spcr_l, convert:toFloat, size:3, units}), - _____: newGroup(LANG.xp_menu, $('prefs-xpo'), {inline: true}), - exportLocal: newBoolean(`Grid:Local`, booleanSave, {title:LANG.op_exgl_l}), - exportGhost: newBoolean(`Grid:Host`, booleanSave, {title:LANG.op_exgh_l}), - exportOcto: newBoolean(`OctoPrint`, booleanSave, {title:LANG.op_exop_l}), - exportThumb: newBoolean(`Thumbnail`, booleanSave, {modes:FDM}), - exportPreview: newBoolean(`Code Preview`, booleanSave), - _____: newGroup(LANG.pt_menu, $('prefs-prt'), {inline}), - detail: newSelect(LANG.pt_qual_s, {title: LANG.pt_qual_l, action: detailSave}, "detail"), - healMesh: newBoolean(LANG.pt_heal_s, booleanSave, {title: LANG.pt_heal_l}), - threaded: newBoolean(LANG.pt_thrd_s, booleanSave, {title: LANG.pt_thrd_l, modes:THREED}), - assembly: newBoolean(LANG.pt_assy_s, booleanSave, {title: LANG.pt_assy_l, modes:THREED}), - - prefadd: uc.checkpoint($('prefs-add')), - - /** FDM Settings */ - - _____: newGroup(LANG.sl_menu, $('fdm-layers'), { modes:FDM, driven, hideable, separator, group:"fdm-layers" }), - sliceHeight: newInput(LANG.sl_lahi_s, { title:LANG.sl_lahi_l, convert:toFloat }), - sliceMinHeight: newInput(LANG.ad_minl_s, { title:LANG.ad_minl_l, convert:toFloat, bound:bound(0,3.0), show:() => ui.sliceAdaptive.checked }), - sliceTopLayers: newInput(LANG.sl_ltop_s, { title:LANG.sl_ltop_l, convert:toInt }), - sliceBottomLayers: newInput(LANG.sl_lbot_s, { title:LANG.sl_lbot_l, convert:toInt }), - separator: newBlank({ class:"set-sep", driven }), - sliceAdaptive: newBoolean(LANG.ad_adap_s, onBooleanClick, { title: LANG.ad_adap_l }), - _____: newGroup(LANG.sw_menu, $('fdm-walls'), { modes:FDM, driven, hideable, separator, group:"fdm-walls" }), - sliceShells: newInput(LANG.sl_shel_s, { title:LANG.sl_shel_l, convert:toFloat }), - sliceLineWidth: newInput(LANG.sl_line_s, { title:LANG.sl_line_l, convert:toFloat, bound:bound(0,5) }), - separator: newBlank({ class:"set-sep", driven }), - sliceShellOrder: newSelect(LANG.sl_ordr_s, { title:LANG.sl_ordr_l}, "shell"), - sliceDetectThin: newSelect(LANG.ad_thin_s, { title: LANG.ad_thin_l, action: thinWallSave }, "thin"), - outputAlternating: newBoolean(LANG.ad_altr_s, onBooleanClick, {title:LANG.ad_altr_l}), - sliceZInterleave: newBoolean(LANG.ad_zint_s, onBooleanClick, {title:LANG.ad_zint_l, show:zIntShow}), - _____: newGroup(LANG.fi_menu, $('fdm-fill'), { modes:FDM, driven, hideable, separator, group:"fdm-fill" }), - sliceFillType: newSelect(LANG.fi_type, {trigger}, "infill"), - sliceFillSparse: newInput(LANG.fi_pcnt_s, {title:LANG.fi_pcnt_l, convert:toFloat, bound:bound(0.0,1.0), show:hasInfill}), - sliceFillRepeat: newInput(LANG.fi_rept_s, {title:LANG.fi_rept_l, convert:toInt, bound:bound(1,10), show:fillIsLinear}), - sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,2.0), show:hasInfill}), - separator: newBlank({ class:"set-sep", driven }), - sliceFillRate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt, bound:bound(0,500)}), - sliceSolidRate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt, bound:bound(0,500)}), - separator: newBlank({ class:"set-sep", driven }), - sliceFillGrow: newInput(LANG.fi_grow_s, {title:LANG.fi_grow_l, convert:toFloat}), - sliceFillAngle: newInput(LANG.fi_angl_s, {title:LANG.fi_angl_l, convert:toFloat}), - _____: newGroup(LANG.fh_menu, $('fdm-heat'), { modes:FDM, driven, hideable, separator, group:"fdm-heat" }), - outputTemp: newInput(LANG.ou_nozl_s, {title:LANG.ou_nozl_l, convert:toInt}), - outputBedTemp: newInput(LANG.ou_bedd_s, {title:LANG.ou_bedd_l, convert:toInt}), - _____: newGroup(LANG.fc_menu, $('fdm-cool'), { modes:FDM, driven, hideable, separator, group:"fdm-cool" }), - outputFanLayer: newInput(LANG.ou_fanl_s, { title:LANG.ou_fanl_l, convert:toInt, bound:bound(0,255) }), - outputFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255)}), - _____: newGroup(LANG.sp_menu, $('fdm-support'), { modes:FDM, driven, hideable, separator, group:"fdm-supp" }), - sliceSupportNozzle: newSelect(LANG.sp_nozl_s, {title:LANG.sp_nozl_l, show:isMultiHead}, "extruders"), - sliceSupportDensity: newInput(LANG.sp_dens_s, {title:LANG.sp_dens_l, convert:toFloat, bound:bound(0.0,1.0)}), - sliceSupportSize: newInput(LANG.sp_size_s, {title:LANG.sp_size_l, convert:toFloat, bound:bound(1.0,200.0)}), - sliceSupportOffset: newInput(LANG.sp_offs_s, {title:LANG.sp_offs_l, convert:toFloat, bound:bound(0.0,200.0)}), - sliceSupportGap: newInput(LANG.sp_gaps_s, {title:LANG.sp_gaps_l, convert:toInt, bound:bound(0,5)}), - sliceSupportArea: newInput(LANG.sp_area_s, {title:LANG.sp_area_l, convert:toFloat, bound:bound(0.0,200.0)}), - sliceSupportExtra: newInput(LANG.sp_xpnd_s, {title:LANG.sp_xpnd_l, convert:toFloat, bound:bound(0.0,10.0)}), - sliceSupportGrow: newInput(LANG.sp_grow_s, {title:LANG.sp_grow_l, convert:toFloat, bound:bound(0.0,10.0)}), - sliceSupportAngle: newInput(LANG.sp_angl_s, {title:LANG.sp_angl_l, convert:toFloat, bound:bound(0.0,90.0)}), - sliceSupportSpan: newInput(LANG.sp_span_s, {title:LANG.sp_span_l, convert:toFloat, bound:bound(0.0,200.0), show:() => ui.sliceSupportEnable.checked }), - separator: newBlank({ class:"set-sep", driven }), - sliceSupportEnable: newBoolean(LANG.sp_auto_s, onBooleanClick, {title:LANG.sp_auto_l, show:isNotBelt}), - sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l}), - separator: newBlank({ class:"set-sep", driven }), - sliceSupportGen: newRow([ - ui.ssaGen = newButton(LANG.sp_detect, onButtonClick, {class: "f-col grow a-center"}) - ], { modes: FDM, class: "ext-buttons f-row grow" }), - separator: newBlank({ class:"set-sep", driven }), - sliceSupportManual: newRow([ - (ui.ssmAdd = newButton(undefined, onButtonClick, {icon:''})), - (ui.ssmDun = newButton(undefined, onButtonClick, {icon:''})), - (ui.ssmClr = newButton(undefined, onButtonClick, {icon:''})) - ], {class:"ext-buttons f-row"}), - _____: newGroup(LANG.fl_menu, $('fdm-base'), { modes:FDM, driven, hideable, separator, group:"fdm-base" }), - firstSliceHeight: newInput(LANG.fl_lahi_s, {title:LANG.fl_lahi_l, convert:toFloat, show:isNotBelt}), - firstLayerNozzleTemp:newInput(LANG.fl_nozl_s, {title:LANG.fl_nozl_l, convert:toInt, show:isNotBelt}), - firstLayerBedTemp: newInput(LANG.fl_bedd_s, {title:LANG.fl_bedd_l, convert:toInt, show:isNotBelt}), - separator: newBlank({ class:"set-sep", driven }), - firstLayerFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255), show:isBelt}), - firstLayerYOffset: newInput(LANG.fl_zoff_s, {title:LANG.fl_zoff_l, convert:toFloat, show:isBelt}), - firstLayerFlatten: newInput(LANG.fl_flat_s, {title:LANG.fl_flat_l, convert:toFloat, show:isBelt}), - firstLayerRate: newInput(LANG.fl_rate_s, {title:LANG.fl_rate_l, convert:toFloat}), - firstLayerFillRate: newInput(LANG.fl_frat_s, {title:LANG.fl_frat_l, convert:toFloat, show:isNotBelt}), - separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), - firstLayerLineMult: newInput(LANG.fl_sfac_s, {title:LANG.fl_sfac_l, convert:toFloat, bound:bound(0.5,2), show:isNotBelt}), - firstLayerPrintMult: newInput(LANG.fl_mult_s, {title:LANG.fl_mult_l, convert:toFloat}), - separator: newBlank({ class:"set-sep", driven, show:isBelt }), - firstLayerBrim: newInput(LANG.fl_brim_s, {title:LANG.fl_brim_l, convert:toInt, show:isBelt}), - firstLayerBrimIn: newInput(LANG.fl_brin_s, {title:LANG.fl_brin_l, convert:toInt, show:isBelt}), - firstLayerBrimTrig: newInput(LANG.fl_brmn_s, {title:LANG.fl_brmn_l, convert:toInt, show:isBelt}), - firstLayerBrimGap: newInput(LANG.fl_brgp_s, {title:LANG.fl_brgp_l, convert:toFloat, show:isBelt}), - separator: newBlank({ class:"set-sep", driven, show:isBelt }), - firstLayerBeltLead: newInput(LANG.fl_bled_s, {title:LANG.fl_bled_l, convert:toFloat, show:isBelt}), - firstLayerBeltBump: newInput(LANG.fl_blmp_s, {title:LANG.fl_blmp_l, convert:toFloat, bound:bound(0, 10), show:isBelt}), - separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), - outputBrimCount: newInput(LANG.fl_skrt_s, {title:LANG.fl_skrt_l, convert:toInt, show:isNotBelt}), - outputBrimOffset: newInput(LANG.fl_skro_s, {title:LANG.fl_skro_l, convert:toFloat, show:isNotBelt}), - outputRaftSpacing: newInput(LANG.fr_spac_s, {title:LANG.fr_spac_l, convert:toFloat, bound:bound(0.0,3.0), show: () => ui.outputRaft.checked && isNotBelt() }), - separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), - outputRaft: newBoolean(LANG.fr_nabl_s, onBooleanClick, {title:LANG.fr_nabl_l, trigger, show:() => isNotBelt()}), - outputDraftShield: newBoolean(LANG.fr_draf_s, onBooleanClick, {title:LANG.fr_draf_l, trigger, show:() => isNotBelt()}), - _____: newGroup(LANG.ou_menu, $('fdm-output'), { modes:FDM, driven, hideable, separator, group:"fdm-out" }), - outputFeedrate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt}), - outputFinishrate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt}), - outputSeekrate: newInput(LANG.ou_move_s, {title:LANG.ou_move_l, convert:toInt}), - separator: newBlank({ class:"set-sep", driven }), - outputShellMult: newInput(LANG.ou_shml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), - outputFillMult: newInput(LANG.ou_flml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), - outputSparseMult: newInput(LANG.ou_spml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), - separator: newBlank({ class:"set-sep", driven }), - outputRetractDist: newInput(LANG.ad_rdst_s, {title:LANG.ad_rdst_l, convert:toFloat}), - outputRetractSpeed: newInput(LANG.ad_rrat_s, {title:LANG.ad_rrat_l, convert:toInt}), - outputRetractWipe: newInput(LANG.ad_wpln_s, {title:LANG.ad_wpln_l, convert:toFloat, bound:bound(0.0,10)}), - separator: newBlank({ class:"set-sep", driven }), - sliceLayerStart: newSelect(LANG.sl_strt_s, {title:LANG.sl_strt_l}, "start"), - outputLayerRetract: newBoolean(LANG.ad_lret_s, onBooleanClick, {title:LANG.ad_lret_l}), - outputAvoidGaps: newBoolean(LANG.ad_agap_s, onBooleanClick, {title:LANG.ad_agap_l}), - separator: newBlank({ class:"set-sep", driven, show:isBelt }), - outputBeltFirst: newBoolean(LANG.ad_lbir_s, onBooleanClick, {title:LANG.ad_lbir_l, show:isBelt}), - _____: newGroup(LANG.ad_menu, $('fdm-expert'), { modes:FDM, driven, hideable, separator, group:"fdm-xprt" }), - sliceAngle: newInput(LANG.sl_angl_s, {title:LANG.sl_angl_l, convert:toFloat, show:isBelt}), - outputRetractDwell: newInput(LANG.ad_rdwl_s, {title:LANG.ad_rdwl_l, convert:toInt}), - sliceSolidMinArea: newInput(LANG.ad_msol_s, {title:LANG.ad_msol_l, convert:toFloat}), - outputMinSpeed: newInput(LANG.ad_mins_s, {title:LANG.ad_mins_l, convert:toFloat, bound:bound(1,200)}), - outputShortPoly: newInput(LANG.ad_spol_s, {title:LANG.ad_spol_l, convert:toFloat, bound:bound(0,10000)}), - outputCoastDist: newInput(LANG.ad_scst_s, {title:LANG.ad_scst_l, convert:toFloat, bound:bound(0.0,10)}), - zHopDistance: newInput(LANG.ad_zhop_s, {title:LANG.ad_zhop_l, convert:toFloat, bound:bound(0,3.0)}), - arcTolerance: newInput(LANG.ad_arct_s, {title:LANG.ad_arct_l, convert:toFloat, bound:bound(0,1.0), show:() => { return isNotBelt() }}), - antiBacklash: newInput(LANG.ad_abkl_s, {title:LANG.ad_abkl_l, convert:toInt, bound:bound(0,3)}), - outputLoops: newInput(LANG.ag_loop_s, {title:LANG.ag_loop_l, convert:toInt, bound:bound(-1,1000), show:isBelt}), - outputPurgeTower: newInput(LANG.ad_purg_s, {title:LANG.ad_purg_l, convert:toInt, bound:bound(0,1000)}), - - fdmRanges: $('fdm-ranges'), - - /** CAM Settings */ - - _____: newGroup(LANG.ct_menu, $('cam-tabs'), { modes:CAM, marker:true, driven, separator }), - camTabsWidth: newInput(LANG.ct_wdth_s, {title:LANG.ct_wdth_l, convert:toFloat, bound:bound(0.005,100), units}), - camTabsHeight: newInput(LANG.ct_hght_s, {title:LANG.ct_hght_l, convert:toFloat, bound:bound(0.005,100), units}), - camTabsDepth: newInput(LANG.ct_dpth_s, {title:LANG.ct_dpth_l, convert:toFloat, bound:bound(0.005,100), units}), - separator: newBlank({ class:"set-sep", driven }), - camTabsMidline: newBoolean(LANG.ct_midl_s, onBooleanClick, {title:LANG.ct_midl_l}), - separator: newBlank({ class:"set-sep", driven }), - camTabsManual: newRow([ - (ui.tabAdd = newButton(undefined, onButtonClick, {icon:''})), - (ui.tabDun = newButton(undefined, onButtonClick, {icon:''})), - (ui.tabClr = newButton(undefined, onButtonClick, {icon:''})) - ], {class:"ext-buttons f-row"}), - _____: newGroup(LANG.cs_menu, $('cam-stock'), { modes:CAM, driven, separator }), - camStockX: newInput(LANG.cs_wdth_s, {title:LANG.cs_wdth_l, convert:toFloat, bound:bound(0,9999), units}), - camStockY: newInput(LANG.cs_dpth_s, {title:LANG.cs_dpth_l, convert:toFloat, bound:bound(0,9999), units}), - camStockZ: newInput(LANG.cs_hght_s, {title:LANG.cs_hght_l, convert:toFloat, bound:bound(0,9999), units}), - separator: newBlank({ class:"set-sep", driven }), - camStockOffset: newBoolean(LANG.cs_offs_s, onBooleanClick, {title:LANG.cs_offs_l}), - camStockClipTo: newBoolean(LANG.cs_clip_s, onBooleanClick, {title:LANG.cs_clip_l}), - camStockIndexed: newBoolean(LANG.cs_indx_s, onBooleanClick, {title:LANG.cs_indx_l}), - camStockIndexGrid: newBoolean(LANG.cs_ishg_s, onBooleanClick, {title:LANG.cs_ishg_l, show:() => ui.camStockIndexed.checked}), - _____: newGroup(LANG.cc_menu, $('cam-limits'), { modes:CAM, driven, separator }), - camZAnchor: newSelect(LANG.ou_zanc_s, {title: LANG.ou_zanc_l, action:zAnchorSave, show:() => !ui.camStockIndexed.checked}, "zanchor"), - camZOffset: newInput(LANG.ou_ztof_s, {title:LANG.ou_ztof_l, convert:toFloat, units}), - camZTop: newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:toFloat, units, trigger}), - camZBottom: newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:toFloat, units, trigger}), - camZThru: newInput(LANG.ou_ztru_s, {title:LANG.ou_ztru_l, convert:toFloat, bound:bound(0.0,100), units }), - camZClearance: newInput(LANG.ou_zclr_s, {title:LANG.ou_zclr_l, convert:toFloat, bound:bound(0.01,100), units }), - camFastFeedZ: newInput(LANG.cc_rzpd_s, {title:LANG.cc_rzpd_l, convert:toFloat, units}), - camFastFeed: newInput(LANG.cc_rapd_s, {title:LANG.cc_rapd_l, convert:toFloat, units}), - _____: newGroup(LANG.ou_menu, $('cam-output'), { modes:CAM, driven, separator, group:"cam-output" }), - camConventional: newBoolean(LANG.ou_conv_s, onBooleanClick, {title:LANG.ou_conv_l}), - camEaseDown: newBoolean(LANG.cr_ease_s, onBooleanClick, {title:LANG.cr_ease_l}), - camDepthFirst: newBoolean(LANG.ou_depf_s, onBooleanClick, {title:LANG.ou_depf_l}), - camToolInit: newBoolean(LANG.ou_toin_s, onBooleanClick, {title:LANG.ou_toin_l}), - separator: newBlank({ class:"set-sep", driven }), - camFirstZMax: newBoolean(LANG.ou_z1st_s, onBooleanClick, {title:LANG.ou_z1st_l}), - camForceZMax: newBoolean(LANG.ou_forz_s, onBooleanClick, {title:LANG.ou_forz_l}), - separator: newBlank({ class:"set-sep", driven }), - camEaseAngle: newInput(LANG.ou_eang_s, {title:LANG.ou_eang_l, convert:toFloat, bound:bound(0.1,85), show:() => ui.camEaseDown.checked}), - camFullEngage: newInput(LANG.ou_feng_s, {title:LANG.ou_feng_l, convert:toFloat, bound:bound(0.1,1.0)}), - _____: newGroup(LANG.co_menu, $('cam-origin'), { modes:CAM, driven, separator }), - camOriginTop: newBoolean(LANG.or_topp_s, onBooleanClick, {title:LANG.or_topp_l}), - camOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, {title:LANG.or_cntr_l}), - separator: newBlank({ class:"set-sep", driven }), - camOriginOffX: newInput(LANG.co_offx_s, {title:LANG.co_offx_l, convert:toFloat, units}), - camOriginOffY: newInput(LANG.co_offy_s, {title:LANG.co_offy_l, convert:toFloat, units}), - camOriginOffZ: newInput(LANG.co_offz_s, {title:LANG.co_offz_l, convert:toFloat, units}), - _____: newGroup(LANG.op_xprt_s, $('cam-expert'), { group:"cam_expert", modes:CAM, marker: false, driven, separator }), - camExpertFast: newBoolean(LANG.cx_fast_s, onBooleanClick, {title:LANG.cx_fast_l, show: () => !ui.camTrueShadow.checked }), - camTrueShadow: newBoolean(LANG.cx_true_s, onBooleanClick, {title:LANG.cx_true_l, show: () => !ui.camExpertFast.checked }), - separator: newBlank({ class:"set-sep", driven }), - camArcTolerance: newInput(LANG.cx_arct_s, {title:LANG.cx_arct_l, convert:toFloat, units, bound:bound(0,100)}), - camArcResolution: newInput(LANG.cx_arcr_s, {title:LANG.cx_arcr_l, convert:toFloat, bound:bound(0,180), show:() => ui.camArcTolerance.value > 0}), - - /** LASER/DRAG/WJET/WEDM cut tool Settings */ - - _____: newGroup(LANG.sl_menu, $('lzr-slice'), { modes:TWOD, driven, separator }), - ctSliceKerf: newInput(LANG.ls_offs_s, {title:LANG.ls_offs_l, convert:toFloat}), - ctSliceHeight: newInput(LANG.ls_lahi_s, {title:LANG.ls_lahi_l, convert:toFloat, trigger}), - ctSliceHeightMin: newInput(LANG.ls_lahm_s, {title:LANG.ls_lahm_l, convert:toFloat, show:() => ui.ctSliceHeight.value == 0 && !ui.ctSliceSingle.checked }), - separator: newBlank({ class:"set-sep", driven }), - ctSliceSingle: newBoolean(LANG.ls_sngl_s, onBooleanClick, {title:LANG.ls_sngl_l}), - ctOmitInner: newBoolean(LANG.we_omit_s, onBooleanClick, {title:LANG.we_omit_l, modes:WEDM}), - _____: newGroup('surfaces', $('lzr-surface'), { modes:[-1], driven, separator }), - ctSurfaces: newRow([ - (ui.faceAdd = newButton(undefined, onButtonClick, {icon:''})), - (ui.faceDun = newButton(undefined, onButtonClick, {icon:''})), - (ui.faceClr = newButton(undefined, onButtonClick, {icon:''})) - ], {class:"ext-buttons f-row", modes:WEDM}), - _____: newGroup(LANG.dk_menu, $('lzr-knife'), { modes:DRAG, marker:true, driven, separator }), - ctOutKnifeDepth: newInput(LANG.dk_dpth_s, { title:LANG.dk_dpth_l, convert:toFloat, bound:bound(0.0,5.0) }), - ctOutKnifePasses: newInput(LANG.dk_pass_s, { title:LANG.dk_pass_l, convert:toInt, bound:bound(0,5) }), - ctOutKnifeTip: newInput(LANG.dk_offs_s, { title:LANG.dk_offs_l, convert:toFloat, bound:bound(0.0,10.0) }), - _____: newGroup(LANG.lo_menu, $('lzr-layout'), { modes:TWOD, driven, separator }), - ctOutTileSpacing: newInput(LANG.ou_spac_s, { title:LANG.ou_spac_l, convert:toInt }), - ctOutMerged: newBoolean(LANG.ou_mrgd_s, onBooleanClick, {title:LANG.ou_mrgd_l, modes:TWONED, show:() => !ui.ctOutStack.checked }), - ctOutGroup: newBoolean(LANG.ou_grpd_s, onBooleanClick, {title:LANG.ou_grpd_l, show:() => !(ui.ctOutMark.checked || ui.ctOutStack.checked) }), - _____: newGroup(LANG.ou_menu, $('lzr-output'), { modes:TWOD, driven, separator, group:"lzr-output" }), - ctOutPower: newInput(LANG.ou_powr_s, {title:LANG.ou_powr_l, convert:toInt, bound:bound(1,100), modes:TWONED }), - ctOutSpeed: newInput(LANG.ou_sped_s, {title:LANG.ou_sped_l, convert:toInt }), - ctAdaptive: newBoolean('adaptive speed', onBooleanClick, {modes:WEDM, title:'controller determines best cutting speed based on material feedback at runtime'}), - separator: newBlank({ class:"set-sep", driven }), - ctOriginBounds: newBoolean(LANG.or_bnds_s, onBooleanClick, { title:LANG.or_bnds_l, show:() => !ui.ctOriginCenter.checked }), - ctOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, { title:LANG.or_cntr_l, show:() => !ui.ctOriginBounds.checked }), - separator: newBlank({ class:"set-sep", driven, modes:WEDM, show:() => ui.ctOriginBounds.checked }), - ctOriginOffX: newInput(LANG.or_offx_s, { title:LANG.or_offx_l, convert:toFloat, modes:WEDM, show:() => ui.ctOriginBounds.checked }), - ctOriginOffY: newInput(LANG.or_offy_s, { title:LANG.or_offy_l, convert:toFloat, modes:WEDM, show:() => ui.ctOriginBounds.checked }), - separator: newBlank({ class:"set-sep", driven, modes:TWONED }), - ctOutZColor: newBoolean(LANG.ou_layo_s, onBooleanClick, { title:LANG.ou_layo_l, modes:TWONED, show:() => !ui.ctOutMerged.checked }), - ctOutLayer: newBoolean(LANG.ou_layr_s, onBooleanClick, { title:LANG.ou_layr_l, modes:TWONED, show:() => !ui.ctOutStack.checked }), - ctOutMark: newBoolean(LANG.ou_lays_s, onBooleanClick, { title:LANG.ou_lays_l, modes:TWONED, show:() => !ui.ctOutStack.checked }), - separator: newBlank({ class:"set-sep", driven, modes:LASER }), - ctOutInches: newBoolean(LANG.ou_inch_s, onBooleanClick, { title:LANG.ou_inch_l, modes:LASER }), - ctOutStack: newBoolean(LANG.ou_stak_s, onBooleanClick, { title:LANG.ou_stak_l, modes:LASER }), - ctOutShaper: newBoolean(LANG.ou_shap_s, onBooleanClick, { title:LANG.ou_shap_l, modes:LASER, show:() => ui.ctOutStack.checked }), - - /** SLA SETTINGS */ - - slaProc: newGroup(LANG.sa_menu, $('sla-slice'), { modes:SLA, group:"sla-slice", driven, separator }), - slaSlice: newInput(LANG.sa_lahe_s, {title:LANG.sa_lahe_l, convert:toFloat}), - slaShell: newInput(LANG.sa_shel_s, {title:LANG.sa_shel_l, convert:toFloat}), - slaOpenTop: newBoolean(LANG.sa_otop_s, onBooleanClick, {title:LANG.sa_otop_l}), - slaOpenBase: newBoolean(LANG.sa_obas_s, onBooleanClick, {title:LANG.sa_obas_l}), - slaLayers: newGroup(LANG.sa_layr_m, $('sla-layers'), { modes:SLA, group:"sla-layers", driven, separator }), - slaLayerOn: newInput(LANG.sa_lton_s, {title:LANG.sa_lton_l, convert:toFloat}), - slaLayerOff: newInput(LANG.sa_ltof_s, {title:LANG.sa_ltof_l, convert:toFloat}), - slaPeelDist: newInput(LANG.sa_pldi_s, {title:LANG.sa_pldi_l, convert:toFloat}), - slaPeelLiftRate: newInput(LANG.sa_pllr_s, {title:LANG.sa_pllr_l, convert:toFloat}), - slaPeelDropRate: newInput(LANG.sa_pldr_s, {title:LANG.sa_pldr_l, convert:toFloat}), - slaBase: newGroup(LANG.sa_base_m, $('sla-base'), { modes:SLA, group:"sla-base", driven, separator }), - slaBaseLayers: newInput(LANG.sa_balc_s, {title:LANG.sa_balc_l, convert:toInt}), - slaBaseOn: newInput(LANG.sa_lton_s, {title:LANG.sa_bltn_l, convert:toFloat}), - slaBaseOff: newInput(LANG.sa_ltof_s, {title:LANG.sa_bltf_l, convert:toFloat}), - slaBasePeelDist: newInput(LANG.sa_pldi_s, {title:LANG.sa_pldi_l, convert:toFloat}), - slaBasePeelLiftRate: newInput(LANG.sa_pllr_s, {title:LANG.sa_pllr_l, convert:toFloat}), - slaFill: newGroup(LANG.sa_infl_m, $('sla-fill'), { modes:SLA, group:"sla-infill", driven, separator }), - slaFillDensity: newInput(LANG.sa_ifdn_s, {title:LANG.sa_ifdn_l, convert:toFloat, bound:bound(0,1)}), - slaFillLine: newInput(LANG.sa_iflw_s, {title:LANG.sa_iflw_l, convert:toFloat, bound:bound(0,5)}), - slaSupport: newGroup(LANG.sa_supp_m, $('sla-support'), { modes:SLA, group:"sla-support", driven, separator }), - slaSupportLayers: newInput(LANG.sa_slyr_s, {title:LANG.sa_slyr_l, convert:toInt, bound:bound(5,100)}), - slaSupportGap: newInput(LANG.sa_slgp_s, {title:LANG.sa_slgp_l, convert:toInt, bound:bound(3,30)}), - slaSupportDensity: newInput(LANG.sa_sldn_s, {title:LANG.sa_sldn_l, convert:toFloat, bound:bound(0.01,0.9)}), - slaSupportSize: newInput(LANG.sa_slsz_s, {title:LANG.sa_slsz_l, convert:toFloat, bound:bound(0.1,1)}), - slaSupportPoints: newInput(LANG.sa_slpt_s, {title:LANG.sa_slpt_l, convert:toInt, bound:bound(3,10)}), - slaSupportEnable: newBoolean(LANG.enable, onBooleanClick, {title:LANG.sl_slen_l}), - slaOutput: newGroup(LANG.sa_outp_m, $('sla-output'), { modes:SLA, driven, separator, group:"sla-output" }), - slaFirstOffset: newInput(LANG.sa_opzo_s, {title:LANG.sa_opzo_l, convert:toFloat, bound:bound(0,1)}), - slaAntiAlias: newSelect(LANG.sa_opaa_s, {title:LANG.sa_opaa_l}, "antialias"), - - layers: uc.setGroup($("layers")), - - settingsName: $('settingsName'), - settingsSave: $('settingsSave'), - }); - - // override old style settings two-button menu - ui.settingsSave.onclick = () => { - settingsSave(undefined, ui.settingsName.value); - }; - - function optSelected(sel) { - let opt = sel.options[sel.selectedIndex]; - return opt ? opt.value : undefined; } - - function hasInfill() { - return optSelected(ui.sliceFillType) !== 'none' + if (same) { + vars.layer_lo = vars.layer_hi; } + view.update_slider(); + show.slices(); + }, controller.ortho); + space.platform.onMove(conf.save); + space.platform.setRound(true); + space.useDefaultKeys(api.feature.on_key === undefined || api.feature.on_key_defaults); + updateDrawer(); - function fillIsLinear() { - return hasInfill() && optSelected(ui.sliceFillType) === 'linear'; - } + // api augmentation with local functions + api.device.export = deviceExport; + let anim = ui.anim = {}; - function spindleShow() { - return settings().device.spindleMax > 0; - } + Object.assign(ui, { + tracker: tracker, + container: container, - function zIntShow() { - return settings().controller.devel; - } + alert: { + dialog: $('alert-area'), + text: $('alert-text') + }, + func: { + slice: $('act-slice'), + preview: $('act-preview'), + animate: $('act-animate'), + export: $('act-export') + }, + label: { + slice: $('label-slice'), + preview: $('label-preview'), + animate: $('label-animate'), + export: $('label-export'), + }, + acct: { + help: $('app-help'), + don8: $('app-don8'), + mesh: $('app-mesh'), + export: $('app-export') + }, + dev: { + header: $('dev-header'), + search: $('dev-search'), + filter: $('dev-filter') + }, + mesh: { + name: $('mesh-name'), + points: $('mesh-points'), + faces: $('mesh-faces'), + }, - // slider setup - const mobile = moto.space.info.mob; - const slbar = mobile ? 80 : 30; - const slbar2 = slbar * 2; - const slider = ui.sliderRange; - const drag = { }; + stats: { + fps: $('fps'), + rms: $('rms'), + div: $('stats'), + rnfo: $('rnfo'), + }, - if (mobile) { - ui.slider.classList.add('slider-mobile'); - ui.sliderLo.classList.add('slider-mobile'); - ui.sliderHi.classList.add('slider-mobile'); - // add css style for mobile devices - DOC.body.classList.add('mobile'); - } + load: $('load-file'), + speeds: $('speeds'), + speedbar: $('speedbar'), + context: $('context-menu'), - function pxToInt(txt) { - return txt ? parseInt(txt.substring(0,txt.length-2)) : 0; - } + ltsetup: $('lt-setup'), + ltfile: $('lt-file'), + ltview: $('lt-view'), + ltact: $('lt-start'), + edit: $('lt-tools'), + nozzle: $('menu-nozzle'), - function sliderUpdate() { - let start = drag.low / drag.maxval; - let end = (drag.low + drag.mid - slbar) / drag.maxval; - api.event.emit('slider.pos', { start, end }); - api.var.layer_lo = Math.round(start * api.var.layer_max); - api.var.layer_hi = Math.round(end * api.var.layer_max); - api.show.layer(); - space.scene.active(); - } + modal: $('modal'), + modalBox: $('modal-box'), + modals: { + help: $('mod-help'), + setup: $('mod-setup'), + prefs: $('mod-prefs'), + files: $('mod-files'), + saves: $('mod-saves'), + tools: $('mod-tools'), + xany: $('mod-x-any'), + xsla: $('mod-x-sla'), + xlaser: $('mod-x-laser'), + local: $('mod-local'), + don8: $('mod-don8'), + any: $('mod-any'), + }, - function dragit(el, delta) { - el.ontouchstart = el.onmousedown = (ev) => { - // el.classList.add('sli-drag-el'); - tracker.style.display = 'block'; - ev.stopPropagation(); - let obj = (ev.touches ? ev.touches[0] : ev); - drag.width = slider.clientWidth; - drag.maxval = drag.width - slbar2; - drag.start = obj.screenX; - drag.loat = drag.low = pxToInt(ui.sliderHold.style.marginLeft); - drag.mdat = drag.mid = ui.sliderMid.clientWidth; - drag.hiat = pxToInt(ui.sliderHold.style.marginRight); - drag.mdmax = drag.width - slbar - drag.loat; - drag.himax = drag.width - slbar - drag.mdat; - let cancel_drag = tracker.ontouchend = tracker.onmouseup = (ev) => { - // el.classList.remove('sli-drag-el'); - if (ev) { - ev.stopPropagation(); - ev.preventDefault(); - } - slider.onmousemove = undefined; - tracker.style.display = 'none'; - }; - el.ontouchend = cancel_drag; - el.ontouchmove = tracker.ontouchmove = tracker.onmousemove = (ev) => { - ev.stopPropagation(); - ev.preventDefault(); - if (ev.buttons === 0) { - return cancel_drag(); - } - if (delta) { - let obj = (ev.touches ? ev.touches[0] : ev); - delta(obj.screenX - drag.start); - } - }; - }; - } + catalogBody: $('catalogBody'), + catalogList: $('catalogList'), - dragit(ui.sliderLo, (delta) => { - let midval = drag.mdat - delta; - let lowval = drag.loat + delta; - if (midval < slbar || lowval < 0) { - return; - } - ui.sliderHold.style.marginLeft = `${lowval}px`; - ui.sliderMid.style.width = `${midval}px`; - drag.low = lowval; - drag.mid = midval; - sliderUpdate(); - }); - dragit(ui.sliderMid, (delta) => { - let loval = drag.loat + delta; - let hival = drag.hiat - delta; - if (loval < 0 || hival < 0) return; - ui.sliderHold.style.marginLeft = `${loval}px`; - ui.sliderHold.style.marginRight = `${hival}px`; - drag.low = loval; - sliderUpdate(); - }); - dragit(ui.sliderHi, (delta) => { - let midval = drag.mdat + delta; - let hival = drag.hiat - delta; - if (midval < slbar || midval > drag.mdmax || hival < 0) return; - ui.sliderMid.style.width = `${midval}px`; - ui.sliderHold.style.marginRight = `${hival}px`; - drag.mid = midval; - sliderUpdate(); - }); + devices: $('devices'), + deviceAdd: $('device-add'), + deviceDelete: $('device-del'), + deviceRename: $('device-ren'), + deviceExport: $('device-exp'), + deviceSave: $('device-save'), - ui.load.onchange = function(event) { - api.platform.load_files(event.target.files); - ui.load.value = ''; // reset so you can re-import the same filee - }; + toolsSave: $('tools-save'), + toolsClose: $('tools-close'), + toolsImport: $('tools-import'), + toolsExport: $('tools-export'), + toolSelect: $('tool-select'), + toolAdd: $('tool-add'), + toolCopy: $('tool-dup'), + toolDelete: $('tool-del'), + toolType: $('tool-type'), + toolName: $('tool-name'), + toolNum: $('tool-num'), + toolFluteDiam: $('tool-fdiam'), + toolFluteLen: $('tool-flen'), + toolShaftDiam: $('tool-sdiam'), + toolShaftLen: $('tool-slen'), + toolTaperAngle: $('tool-tangle'), + toolTaperTip: $('tool-ttip'), + toolMetric: $('tool-metric'), - ui.sliderMin.onclick = () => { - api.show.layer(0,0); - } + setMenu: $('set-menu'), + settings: $('settings'), + settingsBody: $('settingsBody'), + settingsList: $('settingsList'), - ui.sliderMax.onclick = () => { - api.show.layer(api.var.layer_max,0); - } + slider: $('slider'), + sliderMax: $('slider-max'), + sliderMin: $('slider-zero'), + sliderLo: $('slider-lo'), + sliderMid: $('slider-mid'), + sliderHi: $('slider-hi'), + sliderHold: $('slider-hold'), + sliderRange: $('slider-center'), - ui.slider.onmouseover = (ev) => { - api.event.emit('slider.label'); - }; + loading: $('progress').style, + progress: $('progbar').style, + prostatus: $('progtxt'), - ui.slider.onmouseleave = (ev) => { - if (!ev.buttons) api.event.emit('slider.unlabel'); - }; + selection: $('selection'), + sizeX: $('size_x'), + sizeY: $('size_y'), + sizeZ: $('size_z'), + scaleX: $('scale_x'), + scaleY: $('scale_y'), + scaleZ: $('scale_z'), + lockX: $('lock_x'), + lockY: $('lock_y'), + lockZ: $('lock_z'), + stock: $('stock'), + stockWidth: $('stock-width'), + stockDepth: $('stock-width'), + stockHeight: $('stock-width'), - api.event.on('slider.unlabel', (values) => { - }); + /** CAM Animation Bar */ - api.event.on('slider.label', (values) => { - let digits = api.var.layer_max.toString().length; - $('slider-zero').style.width = `${digits}em`; - $('slider-max').style.width = `${digits}em`; - $('slider-zero').innerText = api.var.layer_lo; - $('slider-max').innerText = api.var.layer_hi; - }); + _____: { + _____: newDiv({ addto: $('layer-animate'), set:true }), + row: newRow([ + anim.replay = newButton(null,"anim.replay",{icon:'',title:"restart"}), + anim.play = newButton(null,"anim.play",{icon:'',title:"play"}), + anim.pause = newButton(null,"anim.pause",{icon:'',title:"pause"}), + anim.step = newButton(null,"anim.step",{icon:'',title:"single step"}), + anim.speed = newButton(null,"anim.fast",{icon:'',title:"toggle speed"}), + anim.labspd = newValue(3, {class:"center padleft"}), + anim.labx = newLabel("X", {class:"padleft"}), + anim.valx = newValue(7, {class:"center"}), + anim.laby = newLabel("Y", {class:"padleft"}), + anim.valy = newValue(7, {class:"center"}), + anim.labz = newLabel("Z", {class:"padleft"}), + anim.valz = newValue(6, {class:"center"}), + anim.laba = newLabel("A", {class:"padleft hide"}), + anim.vala = newValue(6, {class:"center hide"}), + anim.labpro = newLabel("%", {class:"padleft"}), + anim.progress = newValue(5, {class:"center"}), + anim.trans = newButton(null,"anim.trans",{icon:'',title:"transparency",class:"padleft"}), + anim.model = newButton(null,"anim.model",{icon:'',title:"show model"}), + anim.shade = newButton(null,"anim.stock",{icon:'',title:"stock box"}), + ]) + }, - api.event.on('slider.set', (values) => { - let width = slider.clientWidth; - let maxval = width - slbar2; - let start = Math.max(0, Math.min(1, values.start)); - let end = Math.max(start, Math.min(1, values.end)); - let lowval = start * maxval; - let midval = ((end - start) * maxval) + slbar; - let hival = maxval - end * maxval; - ui.sliderHold.style.marginLeft = `${lowval}px`; - ui.sliderMid.style.width = `${midval}px`; - ui.sliderHold.style.marginRight = `${hival}px`; - }); + /** Device Browser / Editor */ - // store layer preferences - api.event.on('stack.show', label => { - let mode = api.mode.get(); - let view = api.view.get(); - api.conf.get().labels[`${mode}-${view}-${label}`] = true; - }); + _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true }), + device: newGroup(LANG.dv_gr_dev, null, {group:"ddev", inline, class:"noshow"}), - api.event.on('stack.hide', label => { - let mode = api.mode.get(); - let view = api.view.get(); - api.conf.get().labels[`${mode}-${view}-${label}`] = false; - }); + _____: newGroup("workspace", null, {group:"dext", inline}), + bedWidth: newInput('X (width)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), + bedDepth: newInput('Y (depth)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), + maxHeight: newInput('Z (height)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), + resolutionX: newInput(LANG.dv_rezx_s, {title:LANG.dv_rezx_l, convert:toInt, size:6, modes:SLA}), + resolutionY: newInput(LANG.dv_rezy_s, {title:LANG.dv_rezy_l, convert:toInt, size:6, modes:SLA}), + _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:NO_WEDM }), + _____: newGroup("firmware", null, {group:"dext", inline, modes:NO_WEDM}), + fwRetract: newBoolean(LANG.dv_retr_s, onBooleanClick, {title:LANG.dv_retr_l, modes:FDM}), + deviceOrigin: newBoolean(LANG.dv_orgc_s, onBooleanClick, {title:LANG.dv_orgc_l, modes:FDM_LZN, show:() => !ui.deviceRound.checked}), + deviceRound: newBoolean(LANG.dv_bedc_s, onBooleanClick, {title:LANG.dv_bedc_l, modes:FDM, trigger, show:isNotBelt}), + deviceBelt: newBoolean(LANG.dv_belt_s, onBooleanClick, {title:LANG.dv_belt_l, modes:FDM, trigger, show:() => !ui.deviceRound.checked}), + separator: newBlank({class:"pop-sep", modes:FDM, driven}), + spindleMax: newInput(LANG.dv_spmx_s, {title:LANG.dv_spmx_l, convert:toInt, size:5, modes:CAM, trigger}), + deviceZMax: newInput(LANG.dv_zmax_s, {title:LANG.dv_zmax_l, convert:toInt, size:5, modes:FDM}), + gcodeTime: newInput(LANG.dv_time_s, {title:LANG.dv_time_l, convert:toFloat, size:5, modes:FDM}), + _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:FDM }), + extruder: newGroup(LANG.dv_gr_ext, null, {group:"dext", inline}), + extFilament: newInput(LANG.dv_fila_s, {title:LANG.dv_fila_l, convert:toFloat, modes:FDM}), + extNozzle: newInput(LANG.dv_nozl_s, {title:LANG.dv_nozl_l, convert:toFloat, modes:FDM}), + extOffsetX: newInput(LANG.dv_exox_s, {title:LANG.dv_exox_l, convert:toFloat, modes:FDM}), + extOffsetY: newInput(LANG.dv_exoy_s, {title:LANG.dv_exoy_l, convert:toFloat, modes:FDM}), + extPad: newBlank({class:"grow", modes:FDM}), + separator: newBlank({class:"pop-sep", modes:FDM, driven}), + extActions: newRow([ + ui.extPrev = newButton(undefined, undefined, {icon:''}), + ui.extAdd = newButton(undefined, undefined, {icon:''}), + ui.extDel = newButton(undefined, undefined, {icon:''}), + ui.extNext = newButton(undefined, undefined, {icon:''}) + ], {class:"dev-buttons ext-buttons var-row", modes:FDM}), + _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:CAM_LZR }), + _____: newGroup(LANG.dv_gr_out, null, {group:"dgco", inline}), + gcodeStrip: newBoolean(LANG.dv_strc_s, onBooleanClick, {title:LANG.dv_strc_l, modes:CAM}), + gcodeSpace: newBoolean(LANG.dv_tksp_s, onBooleanClick, {title:LANG.dv_tksp_l, modes:CAM_LZR}), + laserMaxPower: newInput(LANG.ou_maxp_s, {title:LANG.ou_maxp_l, modes:LASER, size:7, text:true}), + useLaser: newBoolean(LANG.dv_lazr_s, onBooleanClick, {title:LANG.dv_lazr_l, modes:CAM}), + gcodeFExt: newInput(LANG.dv_fext_s, {title:LANG.dv_fext_l, modes:CAM_LZR, size:7, text:true}), + gcodeEd: newGroup(LANG.dv_gr_gco, $('dg'), {group:"dgcp", inline, modes:GCODE}), + gcodeMacros: newRow([ + (ui.gcodePre = newGCode(LANG.dv_head_s, {title:LANG.dv_head_l, modes:GCODE, area:gcode})).button, + (ui.gcodePost = newGCode(LANG.dv_foot_s, {title:LANG.dv_foot_l, modes:GCODE, area:gcode})).button, + (ui.gcodeLayer = newGCode(LANG.dv_layr_s, {title:LANG.dv_layr_l, modes:FDM, area:gcode})).button, + (ui.gcodeTrack = newGCode(LANG.dv_prog_s, {title:LANG.dv_prog_l, modes:FDM, area:gcode})).button, + (ui.gcodeFan = newGCode(LANG.dv_fanp_s, {title:LANG.dv_fanp_l, modes:FDM, area:gcode})).button, + (ui.gcodeFeature = newGCode(LANG.dv_feat_s, {title:LANG.dv_feat_l, modes:FDM, area:gcode})).button, + (ui.gcodeLaserOn = newGCode(LANG.dv_lzon_s, {title:LANG.dv_lzon_l, modes:LASER, area:gcode})).button, + (ui.gcodeLaserOff = newGCode(LANG.dv_lzof_s, {title:LANG.dv_lzof_l, modes:LASER, area:gcode})).button, + (ui.gcodeWaterOn = newGCode(LANG.dv_waon_s, {title:LANG.dv_waon_l, modes:WJET, area:gcode})).button, + (ui.gcodeWaterOff = newGCode(LANG.dv_waof_s, {title:LANG.dv_waof_l, modes:WJET, area:gcode})).button, + (ui.gcodeKnifeDn = newGCode(LANG.dv_dkon_s, {title:LANG.dv_dkon_l, modes:DRAG, area:gcode})).button, + (ui.gcodeKnifeUp = newGCode(LANG.dv_dkof_s, {title:LANG.dv_dkof_l, modes:DRAG, area:gcode})).button, + (ui.gcodeChange = newGCode(LANG.dv_tool_s, {title:LANG.dv_tool_l, modes:FDM_CAM, area:gcode})).button, + (ui.gcodeDwell = newGCode(LANG.dv_dwll_s, {title:LANG.dv_dwll_l, modes:CAM, area:gcode})).button, + (ui.gcodeSpindle = newGCode(LANG.dv_sspd_s, {title:LANG.dv_sspd_l, modes:CAM, area:gcode, show:() => ui.spindleMax.value > 0})).button + ], {class:"ext-buttons f-row gcode-macros"}), - // bind language choices - $('lset-en').onclick = function() { - sdb.setItem('kiri-lang', 'en-us'); - api.space.reload(); - }; - $('lset-da').onclick = function() { - sdb.setItem('kiri-lang', 'da-dk'); - api.space.reload(); - }; - $('lset-de').onclick = function() { - sdb.setItem('kiri-lang', 'de-de'); - api.space.reload(); - }; - $('lset-fr').onclick = function() { - sdb.setItem('kiri-lang', 'fr-fr'); - api.space.reload(); - }; - $('lset-pl').onclick = function() { - sdb.setItem('kiri-lang', 'pl-pl'); - api.space.reload(); - }; - $('lset-pt').onclick = function() { - sdb.setItem('kiri-lang', 'pt-pt'); - api.space.reload(); - }; - $('lset-es').onclick = function() { - sdb.setItem('kiri-lang', 'es-es'); - api.space.reload(); - }; - $('lset-zh').onclick = function() { - sdb.setItem('kiri-lang', 'zh'); - api.space.reload(); - }; + /** Preferences Menu */ - space.event.addHandlers(self, [ - 'keyup', keyUpHandler, - 'keydown', keyDownHandler, - 'keypress', keyHandler, - 'dragover', dragOverHandler, - 'dragleave', dragLeave, - 'drop', dropHandler - ]); + _____: newGroup(LANG.op_menu, $('prefs-gen1'), {inline}), + antiAlias: newBoolean(LANG.op_anta_s, booleanSave, {title:LANG.op_anta_l}), + reverseZoom: newBoolean(LANG.op_invr_s, booleanSave, {title:LANG.op_invr_l}), + ortho: newBoolean(LANG.op_orth_s, booleanSave, {title:LANG.op_orth_l}), + dark: newBoolean(LANG.op_dark_s, booleanSave, {title:LANG.op_dark_l}), + drawer: newBoolean('slide out', booleanSave, {title:'slide out settings drawer'}), + scrolls: newBoolean('scrollbars', booleanSave, {title:'show scrollbars'}), + devel: newBoolean(LANG.op_devl_s, booleanSave, {title:LANG.op_devl_l}), + _____: newGroup(LANG.op_disp, $('prefs-gen2'), {inline}), + showOrigin: newBoolean(LANG.op_shor_s, booleanSave, {title:LANG.op_shor_l}), + showRulers: newBoolean(LANG.op_shru_s, booleanSave, {title:LANG.op_shru_l}), + showSpeeds: newBoolean(LANG.op_sped_s, speedSave, {title:LANG.op_sped_l}), + shiny: newBoolean(LANG.op_shny_s, booleanSave, {title:LANG.op_shny_l, modes:FDM}), + lineType: newSelect(LANG.op_line_s, {title: LANG.op_line_l, action: lineTypeSave, modes:FDM}, "linetype"), + animesh: newSelect(LANG.op_anim_s, {title: LANG.op_anim_l, action: aniMeshSave, modes:CAM}, "animesh"), + units: newSelect(LANG.op_unit_s, {title: LANG.op_unit_l, action: unitsSave, modes:CAM}, "units"), + edgeangle: newInput(LANG.op_spoa_s, {title:LANG.op_spoa_l, convert:toFloat, size:3}), + _____: newGroup(LANG.lo_menu, $('prefs-lay'), {inline}), + autoSave: newBoolean(LANG.op_save_s, booleanSave, {title:LANG.op_save_l}), + autoLayout: newBoolean(LANG.op_auto_s, booleanSave, {title:LANG.op_auto_l}), + freeLayout: newBoolean(LANG.op_free_s, booleanSave, {title:LANG.op_free_l}), + spaceRandoX: newBoolean(LANG.op_spcx_s, booleanSave, {title:LANG.op_spcx_l, show:isBelt}), + spaceLayout: newInput(LANG.op_spcr_s, {title:LANG.op_spcr_l, convert:toFloat, size:3, units}), + _____: newGroup(LANG.xp_menu, $('prefs-xpo'), {inline: true}), + exportLocal: newBoolean(`Grid:Local`, booleanSave, {title:LANG.op_exgl_l}), + exportGhost: newBoolean(`Grid:Host`, booleanSave, {title:LANG.op_exgh_l}), + exportOcto: newBoolean(`OctoPrint`, booleanSave, {title:LANG.op_exop_l}), + exportThumb: newBoolean(`Thumbnail`, booleanSave, {modes:FDM}), + exportPreview: newBoolean(`Code Preview`, booleanSave), + _____: newGroup(LANG.pt_menu, $('prefs-prt'), {inline}), + detail: newSelect(LANG.pt_qual_s, {title: LANG.pt_qual_l, action: detailSave}, "detail"), + healMesh: newBoolean(LANG.pt_heal_s, booleanSave, {title: LANG.pt_heal_l}), + threaded: newBoolean(LANG.pt_thrd_s, booleanSave, {title: LANG.pt_thrd_l, modes:THREED}), + assembly: newBoolean(LANG.pt_assy_s, booleanSave, {title: LANG.pt_assy_l, modes:THREED}), - function selectionSize(e) { - let dv = parseFloat(e.target.value || 1), - pv = parseFloat(e.target.was || 1), - ra = dv / pv, - xv = parseFloat(ui.sizeX.was ?? ui.scaleX.value) || 1, - yv = parseFloat(ui.sizeY.was ?? ui.scaleY.value) || 1, - zv = parseFloat(ui.sizeZ.was ?? ui.scaleZ.value) || 1, - ta = e.target, - xc = ui.lockX.checked, - yc = ui.lockY.checked, - zc = ui.lockZ.checked, - xt = ta === ui.sizeX, - yt = ta === ui.sizeY, - zt = ta === ui.sizeZ, - tl = (xt && xc) || (yt && yc) || (zt && zc), - xr = ((tl && xc) || (!tl && xt) ? ra : 1), - yr = ((tl && yc) || (!tl && yt) ? ra : 1), - zr = ((tl && zc) || (!tl && zt) ? ra : 1); - // prevent null scale - if (xr * yr * zr === 0) { - return; - } - selection.scale(xr,yr,zr); - ui.sizeX.was = ui.sizeX.value = xv * xr; - ui.sizeY.was = ui.sizeY.value = yv * yr; - ui.sizeZ.was = ui.sizeZ.value = zv * zr; - } + prefadd: uc.checkpoint($('prefs-add')), - function selectionScale(e) { - let dv = parseFloat(e.target.value || 1), - pv = parseFloat(e.target.was || 1), - ra = dv / pv, - xv = parseFloat(ui.scaleX.was ?? ui.scaleX.value) || 1, - yv = parseFloat(ui.scaleY.was ?? ui.scaleY.value) || 1, - zv = parseFloat(ui.scaleZ.was ?? ui.scaleY.value) || 1, - ta = e.target, - xc = ui.lockX.checked, - yc = ui.lockY.checked, - zc = ui.lockZ.checked, - xt = ta === ui.scaleX, - yt = ta === ui.scaleY, - zt = ta === ui.scaleZ, - tl = (xt && xc) || (yt && yc) || (zt && zc), - xr = ((tl && xc) || (!tl && xt) ? ra : 1), - yr = ((tl && yc) || (!tl && yt) ? ra : 1), - zr = ((tl && zc) || (!tl && zt) ? ra : 1); + /** FDM Settings */ - selection.scale(xr,yr,zr); - ui.scaleX.was = ui.scaleX.value = xv * xr; - ui.scaleY.was = ui.scaleY.value = yv * yr; - ui.scaleZ.was = ui.scaleZ.value = zv * zr; - } + _____: newGroup(LANG.sl_menu, $('fdm-layers'), { modes:FDM, driven, hideable, separator, group:"fdm-layers" }), + sliceHeight: newInput(LANG.sl_lahi_s, { title:LANG.sl_lahi_l, convert:toFloat }), + sliceMinHeight: newInput(LANG.ad_minl_s, { title:LANG.ad_minl_l, convert:toFloat, bound:bound(0,3.0), show:() => ui.sliceAdaptive.checked }), + sliceTopLayers: newInput(LANG.sl_ltop_s, { title:LANG.sl_ltop_l, convert:toInt }), + sliceBottomLayers: newInput(LANG.sl_lbot_s, { title:LANG.sl_lbot_l, convert:toInt }), + separator: newBlank({ class:"set-sep", driven }), + sliceAdaptive: newBoolean(LANG.ad_adap_s, onBooleanClick, { title: LANG.ad_adap_l }), + _____: newGroup(LANG.sw_menu, $('fdm-walls'), { modes:FDM, driven, hideable, separator, group:"fdm-walls" }), + sliceShells: newInput(LANG.sl_shel_s, { title:LANG.sl_shel_l, convert:toFloat }), + sliceLineWidth: newInput(LANG.sl_line_s, { title:LANG.sl_line_l, convert:toFloat, bound:bound(0,5) }), + separator: newBlank({ class:"set-sep", driven }), + sliceShellOrder: newSelect(LANG.sl_ordr_s, { title:LANG.sl_ordr_l}, "shell"), + sliceDetectThin: newSelect(LANG.ad_thin_s, { title: LANG.ad_thin_l, action: thinWallSave }, "thin"), + outputAlternating: newBoolean(LANG.ad_altr_s, onBooleanClick, {title:LANG.ad_altr_l}), + sliceZInterleave: newBoolean(LANG.ad_zint_s, onBooleanClick, {title:LANG.ad_zint_l, show:zIntShow}), + _____: newGroup(LANG.fi_menu, $('fdm-fill'), { modes:FDM, driven, hideable, separator, group:"fdm-fill" }), + sliceFillType: newSelect(LANG.fi_type, {trigger}, "infill"), + sliceFillSparse: newInput(LANG.fi_pcnt_s, {title:LANG.fi_pcnt_l, convert:toFloat, bound:bound(0.0,1.0), show:hasInfill}), + sliceFillRepeat: newInput(LANG.fi_rept_s, {title:LANG.fi_rept_l, convert:toInt, bound:bound(1,10), show:fillIsLinear}), + sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,2.0), show:hasInfill}), + separator: newBlank({ class:"set-sep", driven }), + sliceFillRate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt, bound:bound(0,500)}), + sliceSolidRate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt, bound:bound(0,500)}), + separator: newBlank({ class:"set-sep", driven }), + sliceFillGrow: newInput(LANG.fi_grow_s, {title:LANG.fi_grow_l, convert:toFloat}), + sliceFillAngle: newInput(LANG.fi_angl_s, {title:LANG.fi_angl_l, convert:toFloat}), + _____: newGroup(LANG.fh_menu, $('fdm-heat'), { modes:FDM, driven, hideable, separator, group:"fdm-heat" }), + outputTemp: newInput(LANG.ou_nozl_s, {title:LANG.ou_nozl_l, convert:toInt}), + outputBedTemp: newInput(LANG.ou_bedd_s, {title:LANG.ou_bedd_l, convert:toInt}), + _____: newGroup(LANG.fc_menu, $('fdm-cool'), { modes:FDM, driven, hideable, separator, group:"fdm-cool" }), + outputFanLayer: newInput(LANG.ou_fanl_s, { title:LANG.ou_fanl_l, convert:toInt, bound:bound(0,255) }), + outputFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255)}), + _____: newGroup(LANG.sp_menu, $('fdm-support'), { modes:FDM, driven, hideable, separator, group:"fdm-supp" }), + sliceSupportNozzle: newSelect(LANG.sp_nozl_s, {title:LANG.sp_nozl_l, show:isMultiHead}, "extruders"), + sliceSupportDensity: newInput(LANG.sp_dens_s, {title:LANG.sp_dens_l, convert:toFloat, bound:bound(0.0,1.0)}), + sliceSupportSize: newInput(LANG.sp_size_s, {title:LANG.sp_size_l, convert:toFloat, bound:bound(1.0,200.0)}), + sliceSupportOffset: newInput(LANG.sp_offs_s, {title:LANG.sp_offs_l, convert:toFloat, bound:bound(0.0,200.0)}), + sliceSupportGap: newInput(LANG.sp_gaps_s, {title:LANG.sp_gaps_l, convert:toInt, bound:bound(0,5)}), + sliceSupportArea: newInput(LANG.sp_area_s, {title:LANG.sp_area_l, convert:toFloat, bound:bound(0.0,200.0)}), + sliceSupportExtra: newInput(LANG.sp_xpnd_s, {title:LANG.sp_xpnd_l, convert:toFloat, bound:bound(0.0,10.0)}), + sliceSupportGrow: newInput(LANG.sp_grow_s, {title:LANG.sp_grow_l, convert:toFloat, bound:bound(0.0,10.0)}), + sliceSupportAngle: newInput(LANG.sp_angl_s, {title:LANG.sp_angl_l, convert:toFloat, bound:bound(0.0,90.0)}), + sliceSupportSpan: newInput(LANG.sp_span_s, {title:LANG.sp_span_l, convert:toFloat, bound:bound(0.0,200.0), show:() => ui.sliceSupportEnable.checked }), + separator: newBlank({ class:"set-sep", driven }), + sliceSupportEnable: newBoolean(LANG.sp_auto_s, onBooleanClick, {title:LANG.sp_auto_l, show:isNotBelt}), + sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l}), + separator: newBlank({ class:"set-sep", driven }), + sliceSupportGen: newRow([ + ui.ssaGen = newButton(LANG.sp_detect, onButtonClick, {class: "f-col grow a-center"}) + ], { modes: FDM, class: "ext-buttons f-row grow" }), + separator: newBlank({ class:"set-sep", driven }), + sliceSupportManual: newRow([ + (ui.ssmAdd = newButton(undefined, onButtonClick, {icon:''})), + (ui.ssmDun = newButton(undefined, onButtonClick, {icon:''})), + (ui.ssmClr = newButton(undefined, onButtonClick, {icon:''})) + ], {class:"ext-buttons f-row"}), + _____: newGroup(LANG.fl_menu, $('fdm-base'), { modes:FDM, driven, hideable, separator, group:"fdm-base" }), + firstSliceHeight: newInput(LANG.fl_lahi_s, {title:LANG.fl_lahi_l, convert:toFloat, show:isNotBelt}), + firstLayerNozzleTemp:newInput(LANG.fl_nozl_s, {title:LANG.fl_nozl_l, convert:toInt, show:isNotBelt}), + firstLayerBedTemp: newInput(LANG.fl_bedd_s, {title:LANG.fl_bedd_l, convert:toInt, show:isNotBelt}), + separator: newBlank({ class:"set-sep", driven }), + firstLayerFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255), show:isBelt}), + firstLayerYOffset: newInput(LANG.fl_zoff_s, {title:LANG.fl_zoff_l, convert:toFloat, show:isBelt}), + firstLayerFlatten: newInput(LANG.fl_flat_s, {title:LANG.fl_flat_l, convert:toFloat, show:isBelt}), + firstLayerRate: newInput(LANG.fl_rate_s, {title:LANG.fl_rate_l, convert:toFloat}), + firstLayerFillRate: newInput(LANG.fl_frat_s, {title:LANG.fl_frat_l, convert:toFloat, show:isNotBelt}), + separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), + firstLayerLineMult: newInput(LANG.fl_sfac_s, {title:LANG.fl_sfac_l, convert:toFloat, bound:bound(0.5,2), show:isNotBelt}), + firstLayerPrintMult: newInput(LANG.fl_mult_s, {title:LANG.fl_mult_l, convert:toFloat}), + separator: newBlank({ class:"set-sep", driven, show:isBelt }), + firstLayerBrim: newInput(LANG.fl_brim_s, {title:LANG.fl_brim_l, convert:toInt, show:isBelt}), + firstLayerBrimIn: newInput(LANG.fl_brin_s, {title:LANG.fl_brin_l, convert:toInt, show:isBelt}), + firstLayerBrimTrig: newInput(LANG.fl_brmn_s, {title:LANG.fl_brmn_l, convert:toInt, show:isBelt}), + firstLayerBrimGap: newInput(LANG.fl_brgp_s, {title:LANG.fl_brgp_l, convert:toFloat, show:isBelt}), + separator: newBlank({ class:"set-sep", driven, show:isBelt }), + firstLayerBeltLead: newInput(LANG.fl_bled_s, {title:LANG.fl_bled_l, convert:toFloat, show:isBelt}), + firstLayerBeltBump: newInput(LANG.fl_blmp_s, {title:LANG.fl_blmp_l, convert:toFloat, bound:bound(0, 10), show:isBelt}), + separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), + outputBrimCount: newInput(LANG.fl_skrt_s, {title:LANG.fl_skrt_l, convert:toInt, show:isNotBelt}), + outputBrimOffset: newInput(LANG.fl_skro_s, {title:LANG.fl_skro_l, convert:toFloat, show:isNotBelt}), + outputRaftSpacing: newInput(LANG.fr_spac_s, {title:LANG.fr_spac_l, convert:toFloat, bound:bound(0.0,3.0), show: () => ui.outputRaft.checked && isNotBelt() }), + separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), + outputRaft: newBoolean(LANG.fr_nabl_s, onBooleanClick, {title:LANG.fr_nabl_l, trigger, show:() => isNotBelt()}), + outputDraftShield: newBoolean(LANG.fr_draf_s, onBooleanClick, {title:LANG.fr_draf_l, trigger, show:() => isNotBelt()}), + _____: newGroup(LANG.ou_menu, $('fdm-output'), { modes:FDM, driven, hideable, separator, group:"fdm-out" }), + outputFeedrate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt}), + outputFinishrate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt}), + outputSeekrate: newInput(LANG.ou_move_s, {title:LANG.ou_move_l, convert:toInt}), + separator: newBlank({ class:"set-sep", driven }), + outputShellMult: newInput(LANG.ou_shml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), + outputFillMult: newInput(LANG.ou_flml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), + outputSparseMult: newInput(LANG.ou_spml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), + separator: newBlank({ class:"set-sep", driven }), + outputRetractDist: newInput(LANG.ad_rdst_s, {title:LANG.ad_rdst_l, convert:toFloat}), + outputRetractSpeed: newInput(LANG.ad_rrat_s, {title:LANG.ad_rrat_l, convert:toInt}), + outputRetractWipe: newInput(LANG.ad_wpln_s, {title:LANG.ad_wpln_l, convert:toFloat, bound:bound(0.0,10)}), + separator: newBlank({ class:"set-sep", driven }), + sliceLayerStart: newSelect(LANG.sl_strt_s, {title:LANG.sl_strt_l}, "start"), + outputLayerRetract: newBoolean(LANG.ad_lret_s, onBooleanClick, {title:LANG.ad_lret_l}), + outputAvoidGaps: newBoolean(LANG.ad_agap_s, onBooleanClick, {title:LANG.ad_agap_l}), + separator: newBlank({ class:"set-sep", driven, show:isBelt }), + outputBeltFirst: newBoolean(LANG.ad_lbir_s, onBooleanClick, {title:LANG.ad_lbir_l, show:isBelt}), + _____: newGroup(LANG.ad_menu, $('fdm-expert'), { modes:FDM, driven, hideable, separator, group:"fdm-xprt" }), + sliceAngle: newInput(LANG.sl_angl_s, {title:LANG.sl_angl_l, convert:toFloat, show:isBelt}), + outputRetractDwell: newInput(LANG.ad_rdwl_s, {title:LANG.ad_rdwl_l, convert:toInt}), + sliceSolidMinArea: newInput(LANG.ad_msol_s, {title:LANG.ad_msol_l, convert:toFloat}), + outputMinSpeed: newInput(LANG.ad_mins_s, {title:LANG.ad_mins_l, convert:toFloat, bound:bound(1,200)}), + outputShortPoly: newInput(LANG.ad_spol_s, {title:LANG.ad_spol_l, convert:toFloat, bound:bound(0,10000)}), + outputCoastDist: newInput(LANG.ad_scst_s, {title:LANG.ad_scst_l, convert:toFloat, bound:bound(0.0,10)}), + zHopDistance: newInput(LANG.ad_zhop_s, {title:LANG.ad_zhop_l, convert:toFloat, bound:bound(0,3.0)}), + arcTolerance: newInput(LANG.ad_arct_s, {title:LANG.ad_arct_l, convert:toFloat, bound:bound(0,1.0), show:() => { return isNotBelt() }}), + antiBacklash: newInput(LANG.ad_abkl_s, {title:LANG.ad_abkl_l, convert:toInt, bound:bound(0,3)}), + outputLoops: newInput(LANG.ag_loop_s, {title:LANG.ag_loop_l, convert:toInt, bound:bound(-1,1000), show:isBelt}), + outputPurgeTower: newInput(LANG.ad_purg_s, {title:LANG.ad_purg_l, convert:toInt, bound:bound(0,1000)}), - function selectionRotate(e) { - let val = parseFloat(e.target.value) || 0; - e.target.value = val; - } + fdmRanges: $('fdm-ranges'), - space.event.onEnterKey([ - ui.scaleX, selectionScale, - ui.scaleY, selectionScale, - ui.scaleZ, selectionScale, - ui.sizeX, selectionSize, - ui.sizeY, selectionSize, - ui.sizeZ, selectionSize, - ui.toolName, updateTool, - ui.toolNum, updateTool, - ui.toolFluteDiam, updateTool, - ui.toolFluteLen, updateTool, - ui.toolShaftDiam, updateTool, - ui.toolShaftLen, updateTool, - ui.toolTaperTip, updateTool, - ui.toolTaperAngle, updateTool, - $('rot_x'), selectionRotate, - $('rot_y'), selectionRotate, - $('rot_z'), selectionRotate - ], true); + /** CAM Settings */ - $('lab-axis').onclick = () => { - ui.lockX.checked = - ui.lockY.checked = - ui.lockZ.checked = !( - ui.lockX.checked || - ui.lockY.checked || - ui.lockZ.checked - ); - }; + _____: newGroup(LANG.ct_menu, $('cam-tabs'), { modes:CAM, marker:true, driven, separator }), + camTabsWidth: newInput(LANG.ct_wdth_s, {title:LANG.ct_wdth_l, convert:toFloat, bound:bound(0.005,100), units}), + camTabsHeight: newInput(LANG.ct_hght_s, {title:LANG.ct_hght_l, convert:toFloat, bound:bound(0.005,100), units}), + camTabsDepth: newInput(LANG.ct_dpth_s, {title:LANG.ct_dpth_l, convert:toFloat, bound:bound(0.005,100), units}), + separator: newBlank({ class:"set-sep", driven }), + camTabsMidline: newBoolean(LANG.ct_midl_s, onBooleanClick, {title:LANG.ct_midl_l}), + separator: newBlank({ class:"set-sep", driven }), + camTabsManual: newRow([ + (ui.tabAdd = newButton(undefined, onButtonClick, {icon:''})), + (ui.tabDun = newButton(undefined, onButtonClick, {icon:''})), + (ui.tabClr = newButton(undefined, onButtonClick, {icon:''})) + ], {class:"ext-buttons f-row"}), + _____: newGroup(LANG.cs_menu, $('cam-stock'), { modes:CAM, driven, separator }), + camStockX: newInput(LANG.cs_wdth_s, {title:LANG.cs_wdth_l, convert:toFloat, bound:bound(0,9999), units}), + camStockY: newInput(LANG.cs_dpth_s, {title:LANG.cs_dpth_l, convert:toFloat, bound:bound(0,9999), units}), + camStockZ: newInput(LANG.cs_hght_s, {title:LANG.cs_hght_l, convert:toFloat, bound:bound(0,9999), units}), + separator: newBlank({ class:"set-sep", driven }), + camStockOffset: newBoolean(LANG.cs_offs_s, onBooleanClick, {title:LANG.cs_offs_l}), + camStockClipTo: newBoolean(LANG.cs_clip_s, onBooleanClick, {title:LANG.cs_clip_l}), + camStockIndexed: newBoolean(LANG.cs_indx_s, onBooleanClick, {title:LANG.cs_indx_l}), + camStockIndexGrid: newBoolean(LANG.cs_ishg_s, onBooleanClick, {title:LANG.cs_ishg_l, show:() => ui.camStockIndexed.checked}), + _____: newGroup(LANG.cc_menu, $('cam-limits'), { modes:CAM, driven, separator }), + camZAnchor: newSelect(LANG.ou_zanc_s, {title: LANG.ou_zanc_l, action:zAnchorSave, show:() => !ui.camStockIndexed.checked}, "zanchor"), + camZOffset: newInput(LANG.ou_ztof_s, {title:LANG.ou_ztof_l, convert:toFloat, units}), + camZTop: newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:toFloat, units, trigger}), + camZBottom: newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:toFloat, units, trigger}), + camZThru: newInput(LANG.ou_ztru_s, {title:LANG.ou_ztru_l, convert:toFloat, bound:bound(0.0,100), units }), + camZClearance: newInput(LANG.ou_zclr_s, {title:LANG.ou_zclr_l, convert:toFloat, bound:bound(0.01,100), units }), + camFastFeedZ: newInput(LANG.cc_rzpd_s, {title:LANG.cc_rzpd_l, convert:toFloat, units}), + camFastFeed: newInput(LANG.cc_rapd_s, {title:LANG.cc_rapd_l, convert:toFloat, units}), + _____: newGroup(LANG.ou_menu, $('cam-output'), { modes:CAM, driven, separator, group:"cam-output" }), + camConventional: newBoolean(LANG.ou_conv_s, onBooleanClick, {title:LANG.ou_conv_l}), + camEaseDown: newBoolean(LANG.cr_ease_s, onBooleanClick, {title:LANG.cr_ease_l}), + camDepthFirst: newBoolean(LANG.ou_depf_s, onBooleanClick, {title:LANG.ou_depf_l}), + camToolInit: newBoolean(LANG.ou_toin_s, onBooleanClick, {title:LANG.ou_toin_l}), + separator: newBlank({ class:"set-sep", driven }), + camFirstZMax: newBoolean(LANG.ou_z1st_s, onBooleanClick, {title:LANG.ou_z1st_l}), + camForceZMax: newBoolean(LANG.ou_forz_s, onBooleanClick, {title:LANG.ou_forz_l}), + separator: newBlank({ class:"set-sep", driven }), + camEaseAngle: newInput(LANG.ou_eang_s, {title:LANG.ou_eang_l, convert:toFloat, bound:bound(0.1,85), show:() => ui.camEaseDown.checked}), + camFullEngage: newInput(LANG.ou_feng_s, {title:LANG.ou_feng_l, convert:toFloat, bound:bound(0.1,1.0)}), + _____: newGroup(LANG.co_menu, $('cam-origin'), { modes:CAM, driven, separator }), + camOriginTop: newBoolean(LANG.or_topp_s, onBooleanClick, {title:LANG.or_topp_l}), + camOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, {title:LANG.or_cntr_l}), + separator: newBlank({ class:"set-sep", driven }), + camOriginOffX: newInput(LANG.co_offx_s, {title:LANG.co_offx_l, convert:toFloat, units}), + camOriginOffY: newInput(LANG.co_offy_s, {title:LANG.co_offy_l, convert:toFloat, units}), + camOriginOffZ: newInput(LANG.co_offz_s, {title:LANG.co_offz_l, convert:toFloat, units}), + _____: newGroup(LANG.op_xprt_s, $('cam-expert'), { group:"cam_expert", modes:CAM, marker: false, driven, separator }), + camExpertFast: newBoolean(LANG.cx_fast_s, onBooleanClick, {title:LANG.cx_fast_l, show: () => !ui.camTrueShadow.checked }), + camTrueShadow: newBoolean(LANG.cx_true_s, onBooleanClick, {title:LANG.cx_true_l, show: () => !ui.camExpertFast.checked }), + separator: newBlank({ class:"set-sep", driven }), + camArcTolerance: newInput(LANG.cx_arct_s, {title:LANG.cx_arct_l, convert:toFloat, units, bound:bound(0,100)}), + camArcResolution: newInput(LANG.cx_arcr_s, {title:LANG.cx_arcr_l, convert:toFloat, bound:bound(0,180), show:() => ui.camArcTolerance.value > 0}), - $('scale-reset').onclick = $('lab-scale').onclick = () => { - selection.scale(1 / ui.scaleX.was, 1 / ui.scaleY.was, 1 / ui.scaleZ.was); - ui.scaleX.value = ui.scaleY.value = ui.scaleZ.value = - ui.scaleX.was = ui.scaleY.was = ui.scaleZ.was = 1; - }; + /** LASER/DRAG/WJET/WEDM cut tool Settings */ - $('app-xpnd').onclick = () => { - try { - DOC.body.requestFullscreen(); - } catch (e) { - event.emit('resize'); - moto.space.event.onResize(); - } - }; + _____: newGroup(LANG.sl_menu, $('lzr-slice'), { modes:TWOD, driven, separator }), + ctSliceKerf: newInput(LANG.ls_offs_s, {title:LANG.ls_offs_l, convert:toFloat}), + ctSliceHeight: newInput(LANG.ls_lahi_s, {title:LANG.ls_lahi_l, convert:toFloat, trigger}), + ctSliceHeightMin: newInput(LANG.ls_lahm_s, {title:LANG.ls_lahm_l, convert:toFloat, show:() => ui.ctSliceHeight.value == 0 && !ui.ctSliceSingle.checked }), + separator: newBlank({ class:"set-sep", driven }), + ctSliceSingle: newBoolean(LANG.ls_sngl_s, onBooleanClick, {title:LANG.ls_sngl_l}), + ctOmitInner: newBoolean(LANG.we_omit_s, onBooleanClick, {title:LANG.we_omit_l, modes:WEDM}), + _____: newGroup('surfaces', $('lzr-surface'), { modes:[-1], driven, separator }), + ctSurfaces: newRow([ + (ui.faceAdd = newButton(undefined, onButtonClick, {icon:''})), + (ui.faceDun = newButton(undefined, onButtonClick, {icon:''})), + (ui.faceClr = newButton(undefined, onButtonClick, {icon:''})) + ], {class:"ext-buttons f-row", modes:WEDM}), + _____: newGroup(LANG.dk_menu, $('lzr-knife'), { modes:DRAG, marker:true, driven, separator }), + ctOutKnifeDepth: newInput(LANG.dk_dpth_s, { title:LANG.dk_dpth_l, convert:toFloat, bound:bound(0.0,5.0) }), + ctOutKnifePasses: newInput(LANG.dk_pass_s, { title:LANG.dk_pass_l, convert:toInt, bound:bound(0,5) }), + ctOutKnifeTip: newInput(LANG.dk_offs_s, { title:LANG.dk_offs_l, convert:toFloat, bound:bound(0.0,10.0) }), + _____: newGroup(LANG.lo_menu, $('lzr-layout'), { modes:TWOD, driven, separator }), + ctOutTileSpacing: newInput(LANG.ou_spac_s, { title:LANG.ou_spac_l, convert:toInt }), + ctOutMerged: newBoolean(LANG.ou_mrgd_s, onBooleanClick, {title:LANG.ou_mrgd_l, modes:TWONED, show:() => !ui.ctOutStack.checked }), + ctOutGroup: newBoolean(LANG.ou_grpd_s, onBooleanClick, {title:LANG.ou_grpd_l, show:() => !(ui.ctOutMark.checked || ui.ctOutStack.checked) }), + _____: newGroup(LANG.ou_menu, $('lzr-output'), { modes:TWOD, driven, separator, group:"lzr-output" }), + ctOutPower: newInput(LANG.ou_powr_s, {title:LANG.ou_powr_l, convert:toInt, bound:bound(1,100), modes:TWONED }), + ctOutSpeed: newInput(LANG.ou_sped_s, {title:LANG.ou_sped_l, convert:toInt }), + ctAdaptive: newBoolean('adaptive speed', onBooleanClick, {modes:WEDM, title:'controller determines best cutting speed based on material feedback at runtime'}), + separator: newBlank({ class:"set-sep", driven }), + ctOriginBounds: newBoolean(LANG.or_bnds_s, onBooleanClick, { title:LANG.or_bnds_l, show:() => !ui.ctOriginCenter.checked }), + ctOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, { title:LANG.or_cntr_l, show:() => !ui.ctOriginBounds.checked }), + separator: newBlank({ class:"set-sep", driven, modes:WEDM, show:() => ui.ctOriginBounds.checked }), + ctOriginOffX: newInput(LANG.or_offx_s, { title:LANG.or_offx_l, convert:toFloat, modes:WEDM, show:() => ui.ctOriginBounds.checked }), + ctOriginOffY: newInput(LANG.or_offy_s, { title:LANG.or_offy_l, convert:toFloat, modes:WEDM, show:() => ui.ctOriginBounds.checked }), + separator: newBlank({ class:"set-sep", driven, modes:TWONED }), + ctOutZColor: newBoolean(LANG.ou_layo_s, onBooleanClick, { title:LANG.ou_layo_l, modes:TWONED, show:() => !ui.ctOutMerged.checked }), + ctOutLayer: newBoolean(LANG.ou_layr_s, onBooleanClick, { title:LANG.ou_layr_l, modes:TWONED, show:() => !ui.ctOutStack.checked }), + ctOutMark: newBoolean(LANG.ou_lays_s, onBooleanClick, { title:LANG.ou_lays_l, modes:TWONED, show:() => !ui.ctOutStack.checked }), + separator: newBlank({ class:"set-sep", driven, modes:LASER }), + ctOutInches: newBoolean(LANG.ou_inch_s, onBooleanClick, { title:LANG.ou_inch_l, modes:LASER }), + ctOutStack: newBoolean(LANG.ou_stak_s, onBooleanClick, { title:LANG.ou_stak_l, modes:LASER }), + ctOutShaper: newBoolean(LANG.ou_shap_s, onBooleanClick, { title:LANG.ou_shap_l, modes:LASER, show:() => ui.ctOutStack.checked }), - if (!DOC.body.requestFullscreen) { - $('app-xpnd').style.display = 'none'; - } + /** SLA SETTINGS */ - uc.onBlur([ - ui.toolName, - ui.toolNum, - ui.toolFluteDiam, - ui.toolFluteLen, - ui.toolShaftDiam, - ui.toolShaftLen, - ui.toolTaperTip, - ], updateTool); + slaProc: newGroup(LANG.sa_menu, $('sla-slice'), { modes:SLA, group:"sla-slice", driven, separator }), + slaSlice: newInput(LANG.sa_lahe_s, {title:LANG.sa_lahe_l, convert:toFloat}), + slaShell: newInput(LANG.sa_shel_s, {title:LANG.sa_shel_l, convert:toFloat}), + slaOpenTop: newBoolean(LANG.sa_otop_s, onBooleanClick, {title:LANG.sa_otop_l}), + slaOpenBase: newBoolean(LANG.sa_obas_s, onBooleanClick, {title:LANG.sa_obas_l}), + slaLayers: newGroup(LANG.sa_layr_m, $('sla-layers'), { modes:SLA, group:"sla-layers", driven, separator }), + slaLayerOn: newInput(LANG.sa_lton_s, {title:LANG.sa_lton_l, convert:toFloat}), + slaLayerOff: newInput(LANG.sa_ltof_s, {title:LANG.sa_ltof_l, convert:toFloat}), + slaPeelDist: newInput(LANG.sa_pldi_s, {title:LANG.sa_pldi_l, convert:toFloat}), + slaPeelLiftRate: newInput(LANG.sa_pllr_s, {title:LANG.sa_pllr_l, convert:toFloat}), + slaPeelDropRate: newInput(LANG.sa_pldr_s, {title:LANG.sa_pldr_l, convert:toFloat}), + slaBase: newGroup(LANG.sa_base_m, $('sla-base'), { modes:SLA, group:"sla-base", driven, separator }), + slaBaseLayers: newInput(LANG.sa_balc_s, {title:LANG.sa_balc_l, convert:toInt}), + slaBaseOn: newInput(LANG.sa_lton_s, {title:LANG.sa_bltn_l, convert:toFloat}), + slaBaseOff: newInput(LANG.sa_ltof_s, {title:LANG.sa_bltf_l, convert:toFloat}), + slaBasePeelDist: newInput(LANG.sa_pldi_s, {title:LANG.sa_pldi_l, convert:toFloat}), + slaBasePeelLiftRate: newInput(LANG.sa_pllr_s, {title:LANG.sa_pllr_l, convert:toFloat}), + slaFill: newGroup(LANG.sa_infl_m, $('sla-fill'), { modes:SLA, group:"sla-infill", driven, separator }), + slaFillDensity: newInput(LANG.sa_ifdn_s, {title:LANG.sa_ifdn_l, convert:toFloat, bound:bound(0,1)}), + slaFillLine: newInput(LANG.sa_iflw_s, {title:LANG.sa_iflw_l, convert:toFloat, bound:bound(0,5)}), + slaSupport: newGroup(LANG.sa_supp_m, $('sla-support'), { modes:SLA, group:"sla-support", driven, separator }), + slaSupportLayers: newInput(LANG.sa_slyr_s, {title:LANG.sa_slyr_l, convert:toInt, bound:bound(5,100)}), + slaSupportGap: newInput(LANG.sa_slgp_s, {title:LANG.sa_slgp_l, convert:toInt, bound:bound(3,30)}), + slaSupportDensity: newInput(LANG.sa_sldn_s, {title:LANG.sa_sldn_l, convert:toFloat, bound:bound(0.01,0.9)}), + slaSupportSize: newInput(LANG.sa_slsz_s, {title:LANG.sa_slsz_l, convert:toFloat, bound:bound(0.1,1)}), + slaSupportPoints: newInput(LANG.sa_slpt_s, {title:LANG.sa_slpt_l, convert:toInt, bound:bound(3,10)}), + slaSupportEnable: newBoolean(LANG.enable, onBooleanClick, {title:LANG.sl_slen_l}), + slaOutput: newGroup(LANG.sa_outp_m, $('sla-output'), { modes:SLA, driven, separator, group:"sla-output" }), + slaFirstOffset: newInput(LANG.sa_opzo_s, {title:LANG.sa_opzo_l, convert:toFloat, bound:bound(0,1)}), + slaAntiAlias: newSelect(LANG.sa_opaa_s, {title:LANG.sa_opaa_l}, "antialias"), - ui.toolMetric.onclick = updateTool; - ui.toolType.onchange = updateTool; - // default show gcode pre - ui.gcodePre.button.click(); + layers: uc.setGroup($("layers")), - function mksvg(src) { - let svg = DOC.createElement('svg'); - svg.innerHTML = src; - return svg; - } + settingsName: $('settingsName'), + settingsSave: $('settingsSave'), + }); - function mklbl(src) { - let lbl = DOC.createElement('label'); - lbl.innerText = src; - return lbl; - } + // run client mode initializations right after UI is bound + initCAM(); + initDRAG(); + initFDM(); + initLaser(); + initSLA(); + initWEDM(); + initWJET(); - api.platform.update_size(); - - function mouseOnHover(int, event, ints) { - if (!api.feature.hover) return; - if (!int) return api.feature.hovers || api.widgets.meshes(); - api.event.emit('mouse.hover', {int, ints, event, point: int.point, type: 'widget'}); - } - - function platformOnHover(int, event) { - if (!api.feature.hover) return; - if (int) api.event.emit('mouse.hover', {point: int, event, type: 'platform'}); - } - - api.event.on("feature.hover", enable => { - space.mouse.onHover(enable ? mouseOnHover : undefined); - space.platform.onHover(enable ? platformOnHover : undefined); - }); - - // block standard browser context menu - DOC.oncontextmenu = (event) => { - let et = event.target; - if (et.tagName === 'CANVAS' || et.id === 'context-menu' || et.classList.contains('draggable')) { - event.preventDefault(); - event.stopPropagation(); - return false; - } - }; - - space.mouse.downSelect((int, event) => { - if (api.feature.on_mouse_down) { - if (int) { - api.feature.on_mouse_down(int, event); - return; - } - } - if (api.feature.hover) { - if (int) { - return api.event.emit('mouse.hover.down', {int, point: int.point}); - } else { - return selection.meshes(); - } - } - // lay flat with meta or ctrl clicking a selected face - if (int && (event.ctrlKey || event.metaKey || api.feature.on_face_select)) { - let q = new THREE.Quaternion(); - // find intersecting point, look "up" on Z and rotate to face that - q.setFromUnitVectors(int.face.normal, new THREE.Vector3(0,0,-1)); - selection.rotate(q); - } - if (api.view.get() !== VIEWS.ARRANGE) { - // return no selection in modes other than arrange - return null; - } else { - // return selected meshes for further mouse processing - return api.feature.hovers || selection.meshes(); - } - }); - - space.mouse.upSelect((object, event) => { - if (api.feature.on_mouse_up) { - if (event && object) { - return api.feature.on_mouse_up(object, event); - } else { - return api.widgets.meshes(); - } - } - if (event && api.feature.hover) { - api.event.emit('mouse.hover.up', { object, event }); - return; - } - if (event && event.target.nodeName === "CANVAS") { - if (object && object.object) { - if (object.object.widget) { - platform.select(object.object.widget, event.shiftKey, false); - } - } else { - platform.deselect(); - } - } else { - return api.feature.hovers || api.widgets.meshes(); - } - }); - - space.mouse.onDrag(function(delta, offset, up = false) { - if (api.feature.hover) { - return; - } - if (up) { - api.event.emit('mouse.drag.done', offset); - } - if (delta && ui.freeLayout.checked) { - let set = settings(); - let dev = set.device; - let bound = set.bounds_sel; - let width = dev.bedWidth/2; - let depth = dev.bedDepth/2; - let isout = ( - bound.min.x <= -width || - bound.min.y <= -depth || - bound.max.x >= width || - bound.max.y >= depth - ); - if (!isout) { - if (bound.min.x + delta.x <= -width) return; - if (bound.min.y + delta.y <= -depth) return; - if (bound.max.x + delta.x >= width) return; - if (bound.max.y + delta.y >= depth) return; - } - selection.move(delta.x, delta.y, 0); - api.event.emit('selection.drag', delta); - } else { - return selection.meshes().length > 0; - } - }); - - api.space.restore(init_two) || checkSeed(init_two) || init_two(); + // override old style settings two-button menu + ui.settingsSave.onclick = () => { + settingsSave(undefined, ui.settingsName.value); }; - // SECOND STAGE INIT AFTER UI RESTORED - function init_two() { - api.event.emit('init.two'); - - // call driver initializations, if present - Object.values(kiri.driver).forEach(driver => { - if (driver.init) try { - driver.init(kiri, api, driver); - } catch (error) { - console.log({driver_init_fail: driver, error}) - } - }); - - // load script extensions - if (SETUP.s) SETUP.s.forEach(function(lib) { - let scr = DOC.createElement('script'); - scr.setAttribute('defer',true); - scr.setAttribute('src',`/code/${lib}.js?${kiri.version}`); - DOC.body.appendChild(scr); - stats.add('load_'+lib); - api.event.emit('load.lib', lib); - }); - - // load CSS extensions - if (SETUP.ss) SETUP.ss.forEach(function(style) { - style = style.charAt(0) === '/' ? style : `/kiri/style-${style}`; - let ss = DOC.createElement('link'); - ss.setAttribute("type", "text/css"); - ss.setAttribute("rel", "stylesheet"); - ss.setAttribute("href", `${style}.css?${kiri.version}`); - DOC.body.appendChild(ss); - }); - - // override stored settings - if (SETUP.v) SETUP.v.forEach(function(kv) { - kv = kv.split('='); - sdb.setItem(kv[0],kv[1]); - }); - - // import octoprint settings - if (SETUP.ophost) { - let ohost = api.const.OCTO = { - host: SETUP.ophost[0], - apik: SETUP.opkey ? SETUP.opkey[0] : '' - }; - sdb['octo-host'] = ohost.host; - sdb['octo-apik'] = ohost.apik; - console.log({octoprint:ohost}); - } - - // load workspace from url - if (SETUP.wrk) { - api.settings.import_url(`${proto}//${SETUP.wrk[0]}`, false); - } - - // load an object from url - if (SETUP.load) { - console.log({load:SETUP}); - api.platform.load_url(`${proto}//${SETUP.load[0]}`); - } - - // bind this to UI so main can call it on settings import - ui.sync = function() { - const current = settings(); - const control = current.controller; - - if (!control.devel) { - // TODO: hide thin type 3 during development - kiri.consts.LISTS.thin.length = 3; - } - - platform.deselect(); - catalog.addFileListener(updateCatalog); - space.view.setZoom(control.reverseZoom, control.zoomSpeed); - space.platform.setGridZOff(undefined); - space.platform.setZOff(0.05); - - // restore UI state from settings - ui.showOrigin.checked = control.showOrigin; - ui.showRulers.checked = control.showRulers; - ui.showSpeeds.checked = control.showSpeeds; - ui.freeLayout.checked = control.freeLayout; - ui.autoLayout.checked = control.autoLayout; - ui.spaceRandoX.checked = control.spaceRandoX; - ui.antiAlias.checked = control.antiAlias; - ui.reverseZoom.checked = control.reverseZoom; - ui.autoSave.checked = control.autoSave; - ui.healMesh.checked = control.healMesh; - ui.threaded.checked = setThreaded(control.threaded); - ui.assembly.checked = control.assembly; - ui.ortho.checked = control.ortho; - ui.devel.checked = control.devel; - lineTypeSave(); - detailSave(); - updateStats(); - - // optional set-and-lock mode (hides mode menu) - let SETMODE = SETUP.mode ? SETUP.mode[0] : null; - - // optional set-and-lock device (hides device menu) - let DEVNAME = SETUP.dev ? SETUP.dev[0] : null; - - // setup default mode and enable mode locking, if set - api.mode.set(SETMODE || STARTMODE || current.mode, SETMODE); - - // fill device list - api.devices.refresh(); - - // update ui fields from settings - api.conf.update_fields(); - - // default to ARRANGE view mode - api.view.set(VIEWS.ARRANGE); - - // add ability to override - api.show.controls(api.feature.controls); - - // update everything dependent on the platform size - platform.update_size(); - - // load wasm if indicated - kiri.client.wasm(control.assembly === true); - }; - - ui.sync(); - - // clear alerts as they build up - setInterval(api.event.alerts, 1000); - - // add hide-alerts-on-alert-click - ui.alert.dialog.onclick = function() { - api.event.alerts(true); - }; - - // enable modal hiding - $('mod-x').onclick = api.modal.hide; - - if (!SETUP.s) console.log(`kiri | init main | ${kiri.version}`); - - // send init-done event - api.event.emit('init-done', stats); - - // show gdpr if it's never been seen and we're not iframed - const isLocal = LOCAL || WIN.location.host.split(':')[0] === 'localhost'; - if (!sdb.gdpr && WIN.self === WIN.top && !SETUP.debug && !isLocal) { - $('gdpr').style.display = 'flex'; - } - - // warn of degraded functionality when SharedArrayBuffers are missing - if (api.feature.work_alerts && !window.SharedArrayBuffer) { - api.alerts.show("The security context of this", 10); - api.alerts.show("Window blocks important functionality.", 10); - api.alerts.show("Try a Chromium-base Browser instead", 10); - } - - // add keyboard focus handler (must use for iframes) - WIN.addEventListener('load', function () { - WIN.focus(); - DOC.body.addEventListener('click', function() { - WIN.focus(); - },false); - }); - - // dismiss gdpr alert - $('gotit').onclick = () => { - $('gdpr').style.display = 'none'; - sdb.gdpr = Date.now(); - }; - - // lift curtain - $('curtain').style.display = 'none'; - - // bind interface action elements - $('mode-device').onclick = api.show.devices; - $('mode-profile').onclick = settingsLoad; - $('mode-fdm').onclick = () => api.mode.set('FDM'); - $('mode-cam').onclick = () => api.mode.set('CAM'); - $('mode-sla').onclick = () => api.mode.set('SLA'); - $('mode-laser').onclick = () => api.mode.set('LASER'); - $('mode-drag').onclick = () => api.mode.set('DRAG'); - $('mode-wjet').onclick = () => api.mode.set('WJET'); - $('mode-wedm').onclick = () => api.mode.set('WEDM'); - $('set-device').onclick = (ev) => { ev.stopPropagation(); api.show.devices() }; - $('set-profs').onclick = (ev) => { ev.stopPropagation(); api.conf.show() }; - $('set-tools').onclick = (ev) => { ev.stopPropagation(); api.show.tools() }; - $('set-prefs').onclick = (ev) => { ev.stopPropagation(); api.modal.show('prefs') }; - ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() }; - ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; - ui.acct.mesh.onclick = (ev) => { ev.stopPropagation(); WIN.location = "/mesh" }; - ui.acct.export.onclick = (ev) => { ev.stopPropagation(); profileExport() }; - ui.acct.export.title = LANG.acct_xpo; - $('file-new').onclick = (ev) => { ev.stopPropagation(); workspaceNew() }; - $('file-recent').onclick = () => { api.modal.show('files') }; - $('file-import').onclick = (ev) => { api.event.import(ev); }; - ui.func.slice.onclick = (ev) => { ev.stopPropagation(); api.function.slice() }; - ui.func.preview.onclick = (ev) => { ev.stopPropagation(); api.function.print() }; - ui.func.animate.onclick = (ev) => { ev.stopPropagation(); api.function.animate() }; - ui.func.export.onclick = (ev) => { ev.stopPropagation(); api.function.export() }; - $('view-arrange').onclick = api.platform.layout; - $('view-top').onclick = space.view.top; - $('view-home').onclick = space.view.home; - $('view-front').onclick = space.view.front; - $('view-back').onclick = space.view.back; - $('view-left').onclick = space.view.left; - $('view-right').onclick = space.view.right; - $('unrotate').onclick = () => { - api.widgets.for(w => w.unrotate()); - selection.update_info(); - }; - // attach button handlers to support targets - for (let btn of ["don8pt","don8gh","don8pp"]) { - $(btn).onclick = (ev) => { - window.open(ev.target.children[0].href); - } - } - // rotation buttons - let d = (Math.PI / 180); - $('rot_x_lt').onclick = () => { selection.rotate(-d * $('rot_x').value,0,0) }; - $('rot_x_gt').onclick = () => { selection.rotate( d * $('rot_x').value,0,0) }; - $('rot_y_lt').onclick = () => { selection.rotate(0,-d * $('rot_y').value,0) }; - $('rot_y_gt').onclick = () => { selection.rotate(0, d * $('rot_y').value,0) }; - $('rot_z_lt').onclick = () => { selection.rotate(0,0, d * $('rot_z').value) }; - $('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) }; - // rendering options - $('render-edges').onclick = () => { api.view.edges({ toggle: true }); api.conf.save() }; - $('render-ghost').onclick = () => { api.view.wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); }; - $('render-wire').onclick = () => { api.view.wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); }; - $('render-solid').onclick = () => { api.view.wireframe(false, 0, 1); }; - $('mesh-export-stl').onclick = () => { objectsExport('stl') }; - $('mesh-export-obj').onclick = () => { objectsExport('obj') }; - $('mesh-merge').onclick = selection.merge; - $('context-duplicate').onclick = duplicateSelection; - $('context-mirror').onclick = mirrorSelection; - $('context-layflat').onclick = () => { api.event.emit("tool.mesh.lay-flat") }; - $('context-setfocus').onclick = () => { - api.event.emit( - "tool.camera.focus", - ev => api.space.set_focus(undefined, ev.object.point) - ); - }; - - // ui.modal.onclick = api.modal.hide; - ui.modalBox.onclick = (ev) => { ev.stopPropagation() }; - - // add app name hover info - $('app-info').innerText = kiri.version; - - // show topline separator when iframed - try { if (WIN.self !== WIN.top) $('top-sep').style.display = 'flex' } catch (e) { console.log(e) } - - // warn users they are running a beta release - if (kiri.beta && kiri.beta > 0 && sdb.kiri_beta != kiri.beta) { - api.show.alert("CAUTION"); - api.show.alert("this is a development release"); - api.show.alert("and may not function properly"); - sdb.kiri_beta = kiri.beta; - } + function optSelected(sel) { + let opt = sel.options[sel.selectedIndex]; + return opt ? opt.value : undefined; } - // update static html elements with language overrides - ui.lang = function() { - // lk attribute causes inner text to be replaced with lang value - for (let el of [...DOC.querySelectorAll("[lk]")]) { - let key = el.getAttribute('lk'); - let val = LANG[key]; - if (val) { - el.innerText = val; - } else { - console.log({missing_ln: key}); - } - } - // lkt attribute causes a title attribute to be set from lang value - for (let el of [...DOC.querySelectorAll("[lkt]")]) { - let key = el.getAttribute('lkt'); - let val = LANG[key]; - if (val) { - el.setAttribute("title", val); - } else { - console.log({missing_ln: key}); - } - } + function hasInfill() { + return optSelected(ui.sliceFillType) !== 'none' } - // init lang must happen before all other init functions - function init_lang() { - // if a language needs to load, the script is injected and loaded - // first. once this loads, or doesn't, the initialization begins - let lang = SETUP.ln ? SETUP.ln[0] : sdb.getItem('kiri-lang') || kiri.lang.get(); - // inject language script if not english - if (lang && lang !== 'en' && lang !== 'en-us') { - let map = kiri.lang.map(lang); - let scr = DOC.createElement('script'); - // scr.setAttribute('defer',true); - scr.setAttribute('src',`/kiri/lang/${map}.js?${kiri.version}`); - (DOC.body || DOC.head).appendChild(scr); - stats.set('ll',lang); - scr.onload = function() { - kiri.lang.set(map); - ui.lang(); - init_one(); + function fillIsLinear() { + return hasInfill() && optSelected(ui.sliceFillType) === 'linear'; + } + + function spindleShow() { + return settings().device.spindleMax > 0; + } + + function zIntShow() { + return settings().controller.devel; + } + + // slider setup + const mobile = space.info.mob; + const slbar = mobile ? 80 : 30; + const slbar2 = slbar * 2; + const slider = ui.sliderRange; + const drag = { }; + + if (mobile) { + ui.slider.classList.add('slider-mobile'); + ui.sliderLo.classList.add('slider-mobile'); + ui.sliderHi.classList.add('slider-mobile'); + // add css style for mobile devices + DOC.body.classList.add('mobile'); + } + + function pxToInt(txt) { + return txt ? parseInt(txt.substring(0,txt.length-2)) : 0; + } + + function sliderUpdate() { + let start = drag.low / drag.maxval; + let end = (drag.low + drag.mid - slbar) / drag.maxval; + api.event.emit('slider.pos', { start, end }); + api.var.layer_lo = Math.round(start * api.var.layer_max); + api.var.layer_hi = Math.round(end * api.var.layer_max); + api.show.layer(); + space.scene.active(); + } + + function dragit(el, delta) { + el.ontouchstart = el.onmousedown = (ev) => { + // el.classList.add('sli-drag-el'); + tracker.style.display = 'block'; + ev.stopPropagation(); + let obj = (ev.touches ? ev.touches[0] : ev); + drag.width = slider.clientWidth; + drag.maxval = drag.width - slbar2; + drag.start = obj.screenX; + drag.loat = drag.low = pxToInt(ui.sliderHold.style.marginLeft); + drag.mdat = drag.mid = ui.sliderMid.clientWidth; + drag.hiat = pxToInt(ui.sliderHold.style.marginRight); + drag.mdmax = drag.width - slbar - drag.loat; + drag.himax = drag.width - slbar - drag.mdat; + let cancel_drag = tracker.ontouchend = tracker.onmouseup = (ev) => { + // el.classList.remove('sli-drag-el'); + if (ev) { + ev.stopPropagation(); + ev.preventDefault(); + } + slider.onmousemove = undefined; + tracker.style.display = 'none'; }; - scr.onerror = function(err) { - console.log({language_load_error: err, lang}) - kiri.lang.set(); - ui.lang(); - init_one(); + el.ontouchend = cancel_drag; + el.ontouchmove = tracker.ontouchmove = tracker.onmousemove = (ev) => { + ev.stopPropagation(); + ev.preventDefault(); + if (ev.buttons === 0) { + return cancel_drag(); + } + if (delta) { + let obj = (ev.touches ? ev.touches[0] : ev); + delta(obj.screenX - drag.start); + } + }; + }; + } + + dragit(ui.sliderLo, (delta) => { + let midval = drag.mdat - delta; + let lowval = drag.loat + delta; + if (midval < slbar || lowval < 0) { + return; + } + ui.sliderHold.style.marginLeft = `${lowval}px`; + ui.sliderMid.style.width = `${midval}px`; + drag.low = lowval; + drag.mid = midval; + sliderUpdate(); + }); + dragit(ui.sliderMid, (delta) => { + let loval = drag.loat + delta; + let hival = drag.hiat - delta; + if (loval < 0 || hival < 0) return; + ui.sliderHold.style.marginLeft = `${loval}px`; + ui.sliderHold.style.marginRight = `${hival}px`; + drag.low = loval; + sliderUpdate(); + }); + dragit(ui.sliderHi, (delta) => { + let midval = drag.mdat + delta; + let hival = drag.hiat - delta; + if (midval < slbar || midval > drag.mdmax || hival < 0) return; + ui.sliderMid.style.width = `${midval}px`; + ui.sliderHold.style.marginRight = `${hival}px`; + drag.mid = midval; + sliderUpdate(); + }); + + ui.load.onchange = function(event) { + api.platform.load_files(event.target.files); + ui.load.value = ''; // reset so you can re-import the same filee + }; + + ui.sliderMin.onclick = () => { + api.show.layer(0,0); + } + + ui.sliderMax.onclick = () => { + api.show.layer(api.var.layer_max,0); + } + + ui.slider.onmouseover = (ev) => { + api.event.emit('slider.label'); + }; + + ui.slider.onmouseleave = (ev) => { + if (!ev.buttons) api.event.emit('slider.unlabel'); + }; + + api.event.on('slider.unlabel', (values) => { + }); + + api.event.on('slider.label', (values) => { + let digits = api.var.layer_max.toString().length; + $('slider-zero').style.width = `${digits}em`; + $('slider-max').style.width = `${digits}em`; + $('slider-zero').innerText = api.var.layer_lo; + $('slider-max').innerText = api.var.layer_hi; + }); + + api.event.on('slider.set', (values) => { + let width = slider.clientWidth; + let maxval = width - slbar2; + let start = Math.max(0, Math.min(1, values.start)); + let end = Math.max(start, Math.min(1, values.end)); + let lowval = start * maxval; + let midval = ((end - start) * maxval) + slbar; + let hival = maxval - end * maxval; + ui.sliderHold.style.marginLeft = `${lowval}px`; + ui.sliderMid.style.width = `${midval}px`; + ui.sliderHold.style.marginRight = `${hival}px`; + }); + + // store layer preferences + api.event.on('stack.show', label => { + let mode = api.mode.get(); + let view = api.view.get(); + api.conf.get().labels[`${mode}-${view}-${label}`] = true; + }); + + api.event.on('stack.hide', label => { + let mode = api.mode.get(); + let view = api.view.get(); + api.conf.get().labels[`${mode}-${view}-${label}`] = false; + }); + + // bind language choices + $('lset-en').onclick = function() { + sdb.setItem('kiri-lang', 'en-us'); + api.space.reload(); + }; + $('lset-da').onclick = function() { + sdb.setItem('kiri-lang', 'da-dk'); + api.space.reload(); + }; + $('lset-de').onclick = function() { + sdb.setItem('kiri-lang', 'de-de'); + api.space.reload(); + }; + $('lset-fr').onclick = function() { + sdb.setItem('kiri-lang', 'fr-fr'); + api.space.reload(); + }; + $('lset-pl').onclick = function() { + sdb.setItem('kiri-lang', 'pl-pl'); + api.space.reload(); + }; + $('lset-pt').onclick = function() { + sdb.setItem('kiri-lang', 'pt-pt'); + api.space.reload(); + }; + $('lset-es').onclick = function() { + sdb.setItem('kiri-lang', 'es-es'); + api.space.reload(); + }; + $('lset-zh').onclick = function() { + sdb.setItem('kiri-lang', 'zh'); + api.space.reload(); + }; + + space.event.addHandlers(self, [ + 'keyup', keyUpHandler, + 'keydown', keyDownHandler, + 'keypress', keyHandler, + 'dragover', dragOverHandler, + 'dragleave', dragLeave, + 'drop', dropHandler + ]); + + function selectionSize(e) { + let dv = parseFloat(e.target.value || 1), + pv = parseFloat(e.target.was || 1), + ra = dv / pv, + xv = parseFloat(ui.sizeX.was ?? ui.scaleX.value) || 1, + yv = parseFloat(ui.sizeY.was ?? ui.scaleY.value) || 1, + zv = parseFloat(ui.sizeZ.was ?? ui.scaleZ.value) || 1, + ta = e.target, + xc = ui.lockX.checked, + yc = ui.lockY.checked, + zc = ui.lockZ.checked, + xt = ta === ui.sizeX, + yt = ta === ui.sizeY, + zt = ta === ui.sizeZ, + tl = (xt && xc) || (yt && yc) || (zt && zc), + xr = ((tl && xc) || (!tl && xt) ? ra : 1), + yr = ((tl && yc) || (!tl && yt) ? ra : 1), + zr = ((tl && zc) || (!tl && zt) ? ra : 1); + // prevent null scale + if (xr * yr * zr === 0) { + return; + } + selection.scale(xr,yr,zr); + ui.sizeX.was = ui.sizeX.value = xv * xr; + ui.sizeY.was = ui.sizeY.value = yv * yr; + ui.sizeZ.was = ui.sizeZ.value = zv * zr; + } + + function selectionScale(e) { + let dv = parseFloat(e.target.value || 1), + pv = parseFloat(e.target.was || 1), + ra = dv / pv, + xv = parseFloat(ui.scaleX.was ?? ui.scaleX.value) || 1, + yv = parseFloat(ui.scaleY.was ?? ui.scaleY.value) || 1, + zv = parseFloat(ui.scaleZ.was ?? ui.scaleY.value) || 1, + ta = e.target, + xc = ui.lockX.checked, + yc = ui.lockY.checked, + zc = ui.lockZ.checked, + xt = ta === ui.scaleX, + yt = ta === ui.scaleY, + zt = ta === ui.scaleZ, + tl = (xt && xc) || (yt && yc) || (zt && zc), + xr = ((tl && xc) || (!tl && xt) ? ra : 1), + yr = ((tl && yc) || (!tl && yt) ? ra : 1), + zr = ((tl && zc) || (!tl && zt) ? ra : 1); + + selection.scale(xr,yr,zr); + ui.scaleX.was = ui.scaleX.value = xv * xr; + ui.scaleY.was = ui.scaleY.value = yv * yr; + ui.scaleZ.was = ui.scaleZ.value = zv * zr; + } + + function selectionRotate(e) { + let val = parseFloat(e.target.value) || 0; + e.target.value = val; + } + + space.event.onEnterKey([ + ui.scaleX, selectionScale, + ui.scaleY, selectionScale, + ui.scaleZ, selectionScale, + ui.sizeX, selectionSize, + ui.sizeY, selectionSize, + ui.sizeZ, selectionSize, + ui.toolName, updateTool, + ui.toolNum, updateTool, + ui.toolFluteDiam, updateTool, + ui.toolFluteLen, updateTool, + ui.toolShaftDiam, updateTool, + ui.toolShaftLen, updateTool, + ui.toolTaperTip, updateTool, + ui.toolTaperAngle, updateTool, + $('rot_x'), selectionRotate, + $('rot_y'), selectionRotate, + $('rot_z'), selectionRotate + ], true); + + $('lab-axis').onclick = () => { + ui.lockX.checked = + ui.lockY.checked = + ui.lockZ.checked = !( + ui.lockX.checked || + ui.lockY.checked || + ui.lockZ.checked + ); + }; + + $('scale-reset').onclick = $('lab-scale').onclick = () => { + selection.scale(1 / ui.scaleX.was, 1 / ui.scaleY.was, 1 / ui.scaleZ.was); + ui.scaleX.value = ui.scaleY.value = ui.scaleZ.value = + ui.scaleX.was = ui.scaleY.was = ui.scaleZ.was = 1; + }; + + $('app-xpnd').onclick = () => { + try { + DOC.body.requestFullscreen(); + } catch (e) { + event.emit('resize'); + moto.space.event.onResize(); + } + }; + + if (!DOC.body.requestFullscreen) { + $('app-xpnd').style.display = 'none'; + } + + uc.onBlur([ + ui.toolName, + ui.toolNum, + ui.toolFluteDiam, + ui.toolFluteLen, + ui.toolShaftDiam, + ui.toolShaftLen, + ui.toolTaperTip, + ], updateTool); + + ui.toolMetric.onclick = updateTool; + ui.toolType.onchange = updateTool; + // default show gcode pre + ui.gcodePre.button.click(); + + function mksvg(src) { + let svg = DOC.createElement('svg'); + svg.innerHTML = src; + return svg; + } + + function mklbl(src) { + let lbl = DOC.createElement('label'); + lbl.innerText = src; + return lbl; + } + + api.platform.update_size(); + + function mouseOnHover(int, event, ints) { + if (!api.feature.hover) return; + if (!int) return api.feature.hovers || api.widgets.meshes(); + api.event.emit('mouse.hover', {int, ints, event, point: int.point, type: 'widget'}); + } + + function platformOnHover(int, event) { + if (!api.feature.hover) return; + if (int) api.event.emit('mouse.hover', {point: int, event, type: 'platform'}); + } + + api.event.on("feature.hover", enable => { + space.mouse.onHover(enable ? mouseOnHover : undefined); + space.platform.onHover(enable ? platformOnHover : undefined); + }); + + // block standard browser context menu + DOC.oncontextmenu = (event) => { + let et = event.target; + if (et.tagName === 'CANVAS' || et.id === 'context-menu' || et.classList.contains('draggable')) { + event.preventDefault(); + event.stopPropagation(); + return false; + } + }; + + space.mouse.downSelect((int, event) => { + if (api.feature.on_mouse_down) { + if (int) { + api.feature.on_mouse_down(int, event); + return; + } + } + if (api.feature.hover) { + if (int) { + return api.event.emit('mouse.hover.down', {int, point: int.point}); + } else { + return selection.meshes(); + } + } + // lay flat with meta or ctrl clicking a selected face + if (int && (event.ctrlKey || event.metaKey || api.feature.on_face_select)) { + let q = new THREE.Quaternion(); + // find intersecting point, look "up" on Z and rotate to face that + q.setFromUnitVectors(int.face.normal, new THREE.Vector3(0,0,-1)); + selection.rotate(q); + } + if (api.view.get() !== VIEWS.ARRANGE) { + // return no selection in modes other than arrange + return null; + } else { + // return selected meshes for further mouse processing + return api.feature.hovers || selection.meshes(); + } + }); + + space.mouse.upSelect((object, event) => { + if (api.feature.on_mouse_up) { + if (event && object) { + return api.feature.on_mouse_up(object, event); + } else { + return api.widgets.meshes(); + } + } + if (event && api.feature.hover) { + api.event.emit('mouse.hover.up', { object, event }); + return; + } + if (event && event.target.nodeName === "CANVAS") { + if (object && object.object) { + if (object.object.widget) { + platform.select(object.object.widget, event.shiftKey, false); + } + } else { + platform.deselect(); } } else { - // set to browser default which will be overridden - // by any future script loads (above) - kiri.lang.set(); - ui.lang(); + return api.feature.hovers || api.widgets.meshes(); + } + }); + + space.mouse.onDrag(function(delta, offset, up = false) { + if (api.feature.hover) { + return; + } + if (up) { + api.event.emit('mouse.drag.done', offset); + } + if (delta && ui.freeLayout.checked) { + let set = settings(); + let dev = set.device; + let bound = set.bounds_sel; + let width = dev.bedWidth/2; + let depth = dev.bedDepth/2; + let isout = ( + bound.min.x <= -width || + bound.min.y <= -depth || + bound.max.x >= width || + bound.max.y >= depth + ); + if (!isout) { + if (bound.min.x + delta.x <= -width) return; + if (bound.min.y + delta.y <= -depth) return; + if (bound.max.x + delta.x >= width) return; + if (bound.max.y + delta.y >= depth) return; + } + selection.move(delta.x, delta.y, 0); + api.event.emit('selection.drag', delta); + } else { + return selection.meshes().length > 0; + } + }); + + api.space.restore(init_two) || checkSeed(init_two) || init_two(); +}; + +// SECOND STAGE INIT AFTER UI RESTORED +function init_two() { + api.event.emit('init.two'); + + // load script extensions + if (SETUP.s) SETUP.s.forEach(function(lib) { + let scr = DOC.createElement('script'); + scr.setAttribute('defer',true); + scr.setAttribute('src',`/code/${lib}.js?${version}`); + DOC.body.appendChild(scr); + stats.add('load_'+lib); + api.event.emit('load.lib', lib); + }); + + // load CSS extensions + if (SETUP.ss) SETUP.ss.forEach(function(style) { + style = style.charAt(0) === '/' ? style : `/kiri/style-${style}`; + let ss = DOC.createElement('link'); + ss.setAttribute("type", "text/css"); + ss.setAttribute("rel", "stylesheet"); + ss.setAttribute("href", `${style}.css?${version}`); + DOC.body.appendChild(ss); + }); + + // override stored settings + if (SETUP.v) SETUP.v.forEach(function(kv) { + kv = kv.split('='); + sdb.setItem(kv[0],kv[1]); + }); + + // import octoprint settings + if (SETUP.ophost) { + let ohost = api.const.OCTO = { + host: SETUP.ophost[0], + apik: SETUP.opkey ? SETUP.opkey[0] : '' + }; + sdb['octo-host'] = ohost.host; + sdb['octo-apik'] = ohost.apik; + console.log({octoprint:ohost}); + } + + // load workspace from url + if (SETUP.wrk) { + set_ctrl.import_url(`${proto}//${SETUP.wrk[0]}`, false); + } + + // load an object from url + if (SETUP.load) { + console.log({load:SETUP}); + api.platform.load_url(`${proto}//${SETUP.load[0]}`); + } + + // bind this to UI so main can call it on settings import + ui.sync = function() { + const current = settings(); + const control = current.controller; + + if (!control.devel) { + // TODO: hide thin type 3 during development + api.const.LISTS.thin.length = 3; + } + + platform.deselect(); + catalog.addFileListener(updateCatalog); + space.view.setZoom(control.reverseZoom, control.zoomSpeed); + space.platform.setGridZOff(undefined); + space.platform.setZOff(0.05); + + // restore UI state from settings + ui.showOrigin.checked = control.showOrigin; + ui.showRulers.checked = control.showRulers; + ui.showSpeeds.checked = control.showSpeeds; + ui.freeLayout.checked = control.freeLayout; + ui.autoLayout.checked = control.autoLayout; + ui.spaceRandoX.checked = control.spaceRandoX; + ui.antiAlias.checked = control.antiAlias; + ui.reverseZoom.checked = control.reverseZoom; + ui.autoSave.checked = control.autoSave; + ui.healMesh.checked = control.healMesh; + ui.threaded.checked = setThreaded(control.threaded); + ui.assembly.checked = control.assembly; + ui.ortho.checked = control.ortho; + ui.devel.checked = control.devel; + lineTypeSave(); + detailSave(); + updateStats(); + + // optional set-and-lock mode (hides mode menu) + let SETMODE = SETUP.mode ? SETUP.mode[0] : null; + + // optional set-and-lock device (hides device menu) + let DEVNAME = SETUP.dev ? SETUP.dev[0] : null; + + // setup default mode and enable mode locking, if set + api.mode.set(SETMODE || STARTMODE || current.mode, SETMODE); + + // fill device list + api.devices.refresh(); + + // update ui fields from settings + api.conf.update_fields(); + + // default to ARRANGE view mode + api.view.set(VIEWS.ARRANGE); + + // add ability to override + api.show.controls(api.feature.controls); + + // update everything dependent on the platform size + platform.update_size(); + + // load wasm if indicated + client.wasm(control.assembly === true); + }; + + ui.sync(); + + // clear alerts as they build up + setInterval(api.event.alerts, 1000); + + // add hide-alerts-on-alert-click + ui.alert.dialog.onclick = function() { + api.event.alerts(true); + }; + + // enable modal hiding + $('mod-x').onclick = api.modal.hide; + + if (!SETUP.s) console.log(`kiri | init main | ${version}`); + + // send init-done event + api.event.emit('init-done', stats); + + // show gdpr if it's never been seen and we're not iframed + const isLocal = LOCAL || WIN.location.host.split(':')[0] === 'localhost'; + if (!sdb.gdpr && WIN.self === WIN.top && !SETUP.debug && !isLocal) { + $('gdpr').style.display = 'flex'; + } + + // warn of degraded functionality when SharedArrayBuffers are missing + if (api.feature.work_alerts && !window.SharedArrayBuffer) { + api.alerts.show("The security context of this", 10); + api.alerts.show("Window blocks important functionality.", 10); + api.alerts.show("Try a Chromium-base Browser instead", 10); + } + + // add keyboard focus handler (must use for iframes) + WIN.addEventListener('load', function () { + WIN.focus(); + DOC.body.addEventListener('click', function() { + WIN.focus(); + },false); + }); + + // dismiss gdpr alert + $('gotit').onclick = () => { + $('gdpr').style.display = 'none'; + sdb.gdpr = Date.now(); + }; + + // bind interface action elements + $('mode-device').onclick = api.show.devices; + $('mode-profile').onclick = settingsLoad; + $('mode-fdm').onclick = () => api.mode.set('FDM'); + $('mode-cam').onclick = () => api.mode.set('CAM'); + $('mode-sla').onclick = () => api.mode.set('SLA'); + $('mode-laser').onclick = () => api.mode.set('LASER'); + $('mode-drag').onclick = () => api.mode.set('DRAG'); + $('mode-wjet').onclick = () => api.mode.set('WJET'); + $('mode-wedm').onclick = () => api.mode.set('WEDM'); + $('set-device').onclick = (ev) => { ev.stopPropagation(); api.show.devices() }; + $('set-profs').onclick = (ev) => { ev.stopPropagation(); api.conf.show() }; + $('set-tools').onclick = (ev) => { ev.stopPropagation(); api.show.tools() }; + $('set-prefs').onclick = (ev) => { ev.stopPropagation(); api.modal.show('prefs') }; + ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() }; + ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; + ui.acct.mesh.onclick = (ev) => { ev.stopPropagation(); WIN.location = "/mesh" }; + ui.acct.export.onclick = (ev) => { ev.stopPropagation(); profileExport() }; + ui.acct.export.title = LANG.acct_xpo; + $('file-new').onclick = (ev) => { ev.stopPropagation(); workspaceNew() }; + $('file-recent').onclick = () => { api.modal.show('files') }; + $('file-import').onclick = (ev) => { api.event.import(ev); }; + ui.func.slice.onclick = (ev) => { ev.stopPropagation(); api.function.slice() }; + ui.func.preview.onclick = (ev) => { ev.stopPropagation(); api.function.print() }; + ui.func.animate.onclick = (ev) => { ev.stopPropagation(); api.function.animate() }; + ui.func.export.onclick = (ev) => { ev.stopPropagation(); api.function.export() }; + $('view-arrange').onclick = api.platform.layout; + $('view-top').onclick = space.view.top; + $('view-home').onclick = space.view.home; + $('view-front').onclick = space.view.front; + $('view-back').onclick = space.view.back; + $('view-left').onclick = space.view.left; + $('view-right').onclick = space.view.right; + $('unrotate').onclick = () => { + api.widgets.for(w => w.unrotate()); + selection.update_info(); + }; + // attach button handlers to support targets + for (let btn of ["don8pt","don8gh","don8pp"]) { + $(btn).onclick = (ev) => { + window.open(ev.target.children[0].href); + } + } + // rotation buttons + let d = (Math.PI / 180); + $('rot_x_lt').onclick = () => { selection.rotate(-d * $('rot_x').value,0,0) }; + $('rot_x_gt').onclick = () => { selection.rotate( d * $('rot_x').value,0,0) }; + $('rot_y_lt').onclick = () => { selection.rotate(0,-d * $('rot_y').value,0) }; + $('rot_y_gt').onclick = () => { selection.rotate(0, d * $('rot_y').value,0) }; + $('rot_z_lt').onclick = () => { selection.rotate(0,0, d * $('rot_z').value) }; + $('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) }; + // rendering options + $('render-edges').onclick = () => { api.view.edges({ toggle: true }); api.conf.save() }; + $('render-ghost').onclick = () => { api.view.wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); }; + $('render-wire').onclick = () => { api.view.wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); }; + $('render-solid').onclick = () => { api.view.wireframe(false, 0, 1); }; + $('mesh-export-stl').onclick = () => { objectsExport('stl') }; + $('mesh-export-obj').onclick = () => { objectsExport('obj') }; + $('mesh-merge').onclick = selection.merge; + $('context-duplicate').onclick = duplicateSelection; + $('context-mirror').onclick = mirrorSelection; + $('context-layflat').onclick = () => { api.event.emit("tool.mesh.lay-flat") }; + $('context-setfocus').onclick = () => { + api.event.emit( + "tool.camera.focus", + ev => api.space.set_focus(undefined, ev.object.point) + ); + }; + + // ui.modal.onclick = api.modal.hide; + ui.modalBox.onclick = (ev) => { ev.stopPropagation() }; + + // add app name hover info + $('app-info').innerText = version; + + // show topline separator when iframed + try { if (WIN.self !== WIN.top) $('top-sep').style.display = 'flex' } catch (e) { console.log(e) } + + // warn users they are running a beta release + if (beta && beta > 0 && sdb.kiri_beta != beta) { + api.show.alert("CAUTION"); + api.show.alert("this is a development release"); + api.show.alert("and may not function properly"); + sdb.kiri_beta = beta; + } + + // hide url params + history.replaceState({}, '', '/kiri'); + + // lift curtain + $('curtain').style.display = 'none'; +} + +// update static html elements with language overrides +function lang_rewrite() { + // lk attribute causes inner text to be replaced with lang value + for (let el of [...DOC.querySelectorAll("[lk]")]) { + let key = el.getAttribute('lk'); + let val = LANG[key]; + if (val) { + el.innerText = val; + } else { + console.log({missing_ln: key}); + } + } + // lkt attribute causes a title attribute to be set from lang value + for (let el of [...DOC.querySelectorAll("[lkt]")]) { + let key = el.getAttribute('lkt'); + let val = LANG[key]; + if (val) { + el.setAttribute("title", val); + } else { + console.log({missing_ln: key}); + } + } +} + +// init lang must happen before all other init functions +function init_lang() { + // if a language needs to load, the script is injected and loaded + // first. once this loads, or doesn't, the initialization begins + let lang = SETUP.ln ? SETUP.ln[0] : sdb.getItem('kiri-lang') || api.language.get(); + api.event.emit('init.lang', lang); + // inject language script if not english + if (lang && lang !== 'en' && lang !== 'en-us') { + let map = api.language.map(lang); + let scr = DOC.createElement('script'); + // scr.setAttribute('defer',true); + scr.setAttribute('src',`/kiri/lang/${map}.js?${version}`); + (DOC.body || DOC.head).appendChild(scr); + stats.set('ll',lang); + scr.onload = function() { + api.language.set(map); + lang_rewrite(); + init_one(); + }; + scr.onerror = function(err) { + console.log({language_load_error: err, lang}) + api.language.set(); + lang_rewrite(); init_one(); } + } else { + // set to browser default which will be overridden + // by any future script loads (above) + api.language.set(); + lang_rewrite(); + init_one(); } +} - // setup init() trigger when dom + scripts complete - DOC.onreadystatechange = function() { - if (DOC.readyState === 'complete') { - init_lang(); - } - } - -}); +export function run() { + init_lang(); + return api; +} diff --git a/src/kiri/lang-en.js b/src/kiri/lang-en.js deleted file mode 120000 index 92cc3a27..00000000 --- a/src/kiri/lang-en.js +++ /dev/null @@ -1 +0,0 @@ -../../web/kiri/lang/en.js \ No newline at end of file diff --git a/src/kiri/lang-en.js b/src/kiri/lang-en.js new file mode 100644 index 00000000..85179cdd --- /dev/null +++ b/src/kiri/lang-en.js @@ -0,0 +1,932 @@ +// English language map. Other localization maps will +// defer to English map for any missing key/value pairs +self.lang['en'] = +self.lang['en-us'] = { + // common keys and menus + animate: "animate", // CAM animate button + arrange: "arrange", // layout workspace objects + axis: "axis", // left object scale pop menu + back: "back", + clear: "clear", // clear workspace (remove all objects) + copy: "copy", + clone: "clone", // create duplicate + delete: "delete", + detail: "detail", + done: "done", + edit: "edit", + enable: "enable", + disable: "disable", + donate: "donate", + export: "export", + files: "files", + filter: "filter", + front: "front", + fullscreen: "fullscreen", + help: "help", + ghost: "ghost", // left render pop menu (wireframe) + hide: "hide", // left render pop menu (invisible) + home: "home", + import: "import", + language: "language", + left: "left", + machines: "machines", // device or machine + mesh: "mesh", // mesh editing + metric: "metric", + name: "name", + new: "new", // file new + off: "off", // turn something off (like a lightbulb) + on: "on", // turn something on (like a lightbulb) + profs: "profiles", // right menu "profiles" + prefs: "preferences", // right menu "preferences" + preview: "preview", + recent: "recent", + rename: "rename", + render: "render", // left render pop menu + reset: "reset", + right: "right", + rotate: "rotate", // left rotate pop menu + save: "save", + scale: "scale", // left object scale pop menu + select: "select", + setup: "setup", + settings: "settings", + size: "size", + slice: "slice", + solid: "solid", // view type pop menu + start: "start", + tool: "tool", + tools: "tools", // CAM tool menu button + top: "top", + type: "type", // CAM tool type + version: "version", + view: "view", // left view pop menu + wire: "wire", // left render pop menu + volume: "volume", // device build area + + acct_xpo: ["make a backup of your device","and device profiles with the","option to include workspace","objects and positions"], + + // RIGHT-CLICK CONTEXT MENU + rc_clws: "clear workspace", + rc_xpws: "export workspace", + rc_lafl: "lay flat", + rc_mirr: "mirror", + rc_dupl: "duplicate", + rc_focs: "set focus", + rc_heal: "heal meshes", + rc_swap: "replace vertices", + rc_xobj: "export OBJ", + rc_xstl: "export STL", + sb_info: ["print speed","in mm/s"], + rc_merg: "merge object meshes", + + // DEVICE MENU and related dialogs + dm_sldt: "select a device type", + dm_stdd: "standard devices", + dm_mydd: "my devices", + dm_seld: "selected device", + dm_rcnt: "recent files", + dm_savs: "settings", + dm_appp: "Application Preferences", + + // CAM Tool Dialog + td_tyem: "end", // end mill + td_tybm: "ball", // ball mill + td_tytm: "taper", // taper mill + td_tydr: "drill", // drill + td_tonm: "tool #", + td_shft: "shaft", // endmill shaft specs + td_flut: "flute", // endmill flute specs + td_tapr: "taper", // endmill taper specs + + // DEVICE dialog groups + dv_gr_dev: "device", + dv_gr_ext: "extruder", + dv_gr_out: "output", + dv_gr_gco: "gcode macros", + + // DEVICE dialog (_s = label, _l = hover help) + dv_name_s: "name", + dv_name_l: "device name", + dv_fila_s: "filament", + dv_fila_l: "diameter in millimeters", + dv_nozl_s: "nozzle", + dv_nozl_l: "diameter in millimeters", + dv_bedw_s: "width", + dv_bedw_l: "workspace units", + dv_bedd_s: "depth", + dv_bedd_l: "workspace units", + dv_bedh_s: "height", + dv_bedh_l: ["max build height","in workspace units"], + dv_rezx_s: "resolution X", + dv_rezx_l: ["x resolution"], + dv_rezy_s: "resolution Y", + dv_rezy_l: ["y resolution"], + dv_zmax_s: "Z axis speed limit", + dv_zmax_l: ["maximum Z axis speed","in millimeters / second","0 to disable"], + dv_time_s: "output time factor", + dv_time_l: ["modifier to print time estimates","in the gcode export dialog.","raise or lower value based on your","predicted vs observed print times"], + dv_spmx_s: "max spindle", + dv_spmx_l: ["max spindle rpm speed","0 to disable"], + dv_xtab_s: "absolute positioning", + dv_xtab_l: "extrusion moves absolute", + dv_orgc_s: "origin center", + dv_orgc_l: "bed origin center", + // dv_orgt_s: "origin top", + // dv_orgt_l: "part z origin top", + dv_bedc_s: "circular bed", + dv_bedc_l: "device bed is circular", + dv_belt_s: "belt bed", + dv_belt_l: "continuous printing bed", + dv_fsrc_s: "filament source", + dv_fsrc_l: ["how filament is fed\ninto the extruder"], + dv_retr_s: "firmware retract", + dv_retr_l: ["device fimware supports G10/G11"], + dv_fanp_s: "cooling", + dv_fanp_l: "set cooling fan power", + dv_feat_s: "feature", + dv_feat_l: "inject code for specified features", + dv_prog_s: "progress", + dv_prog_l: "output on each % progress", + dv_layr_s: "layer", + dv_layr_l: ["output at each","layer change"], + dv_tksp_s: "token spacer", + dv_tksp_l: ["add a space between","gcode axis parameters","G0X0Y0X0","vs","G0 X0 Y0 Z0"], + dv_strc_s: "strip comments", + dv_strc_l: ["strip gcode comments","comments begin with ;"], + dv_fext_s: "file extension", + dv_fext_l: "file name extension", + dv_lazr_s: "enable laser", + dv_lazr_l: ["enable laser operations"], + + dv_dwll_s: "dwell", + dv_dwll_l: "gcode dwell script", + dv_tool_s: "tool", + dv_tool_l: "tool change script", + dv_sspd_s: "spindle", + dv_sspd_l: "set spindle speed", + dv_paus_s: "pause", + dv_paus_l: "gcode pause script", + dv_head_s: "header", + dv_head_l: "gcode header script", + dv_foot_s: "footer", + dv_foot_l: "gcode footer script", + dv_pext_s: "exterior", + dv_pext_l: ["gcode script run before","printing exterior lines"], + dv_pint_s: "interior", + dv_pint_l: ["gcode script run before","printing interior lines"], + dv_lzon_s: "laser on", + dv_lzon_l: "gcode laser on script", + dv_lzof_s: "laser off", + dv_lzof_l: "gcode laser off script", + + dv_waon_s: "water on", + dv_waon_l: "gcode water pump on script", + dv_waof_s: "water off", + dv_waof_l: "gcode water pump off script", + + dv_dkon_s: "knife down", + dv_dkon_l: "gcode knife down script", + dv_dkof_s: "knife up", + dv_dkof_l: "gcode knife up script", + + dv_exts_s: "select", + dv_exts_l: "gcode run to enable this extruder", + dv_dext_s: "deselect", + dv_dext_l: "gcode run before enabling another extruder", + dv_extd_s: "deselect", + dv_extd_l: "gcode to deselect this extruder", + dv_exox_s: "offset x", + dv_exox_l: "nozzle offset x", + dv_exoy_s: "offset y", + dv_exoy_l: "nozzle offset y", + + dv_paid_s: "printer id", + dv_paid_l: ["unique printer identifier","may be made up"], + dv_paps_s: "ping spacing", + dv_paps_l: ["minimum filament consumption","between pings in millimeters","recommended 500 - 1000","0 = disable pings"], + dv_pafe_s: "feed length", + dv_pafe_l: ["feeder tube length in millimeters","short=570, medium=800, long=1100"], + dv_papl_s: "push length", + dv_papl_l: ["distance between extruder gear","and nozzle tip in millimeters"], + dv_paof_s: "offset length", + dv_paof_l: ["length to add or remove from","the first segment in millimeters","usually 0"], + dv_pahe_s: "heating time", + dv_pahe_l: ["offset from default"], + dv_paco_s: "cooling time", + dv_paco_l: ["offset from default"], + dv_pacm_s: "compression time", + dv_pacm_l: ["offset from default"], + + // MODE + mo_menu: "mode", + mo_fdmp: "FDM Print", + mo_slap: "SLA Print", + mo_lazr: "Laser Cut", + mo_cncm: "CNC Mill", + + // SETUP + su_menu: "setup", + su_devi: "Devices", + su_tool: "Tools", + su_locl: "Local", + su_xprt: "Export", + su_help: "Help", + + // LOAD + fe_menu: "file", + fn_recn: "Recent", + fn_impo: "Import", + + // FUNCTION + fn_menu: "action", + fn_arra: "Arrange", + fn_slic: "Slice", + fn_prev: "Preview", + fn_expo: "Export", + + // VIEW + vu_menu: "view", + vu_home: "Home", + vu_rset: "Reset", + vu_sptp: "Top", + vu_spfr: "Front", + vu_splt: "Left", + vu_sprt: "Right", + + // WORKSPACE + ws_menu: "view", + ws_save: "Save", + ws_cler: "Clear", + + // OPTIONS + op_menu: "interface", + op_disp: "display", + op_xprt_s: "expert", + op_xprt_l: "show more settings options", + op_decl_s: "show decals", + op_decl_l: "show device decals and logos", + op_shny_s: "detail render", + op_shny_l: "choose a slice render method that is slower and uses specular highlighting. this makes it easier to see detail in some models at the expense of render speed", + op_devl_s: "developer", + op_devl_l: ["show developer options","enable developer debugging"], + op_hopo_s: "hover pop", + op_hopo_l: ["enable menu hover","to activate"], + op_dark_s: "dark mode", + op_dark_l: "dark mode interface", + op_comp_s: "compact ui", + op_comp_l: ["compact user interface","better for small screens","and tablets"], + op_shor_s: "show origin", + op_shor_l: "show device or process origin", + op_shru_s: "show rulers", + op_shru_l: ["show axes rulers","on major gridlines"], + op_sped_s: "show speeds", + op_sped_l: ["show speed to color bar","in preview mode"], + op_auto_s: "auto layout", + op_auto_l: ["automatically layout platform","when new items added"], + op_free_s: "free layout", + op_free_l: ["permit dragable layout","no effect in laser mode"], + op_spcr_s: "spacing", + op_spcr_l: ["spacing between objects","during auto layout","in workspace units","in belt mode, use 0","to auto-space by height"], + op_spoa_s: "edge angle", + op_spoa_l: ["surface angle difference triggering an edge trace"], + op_spcx_s: "random X", + op_spcx_l: ["in belt mode, randomly","layout the part on X","to even belt wear"], + op_orth_s: "orthographic", + op_orth_l: ["orthographic display","requires page refresh"], + op_anta_s: "anti-alias", + op_anta_l: ["smooth display. better but slower","requires a page refresh or reload"], + op_invr_s: "invert zoom", + op_invr_l: ["invert mouse wheel","scroll zoom"], + op_save_s: "auto save", + op_save_l: ["preserve objects in workspace","between application reloads"], + op_line_s: "line type", + op_line_l: ["line style for path rendering","impacts 3d performance","path: 3d best","flat: 2d good","line = 1d fast"], + op_unit_s: "units", + op_unit_l: ["workspace units affects","speeds and distances"], + op_anim_s: "animate", + op_anim_l: ["animation mesh density","higher is more dense","takes more memory","and is slower"], + op_exgl_l: ["show Grid:Local devices in export dialog","includes Grid:Bots and OctoPrint plugins"], + op_exgh_l: ["legacy Grid:Host local network support"], + op_exop_l: ["legacy OctoPrint local network support","Grid:Space OctoPrint plugin recommended"], + + lo_menu: "layout", + + pt_menu: "slicer", + pt_deci_s: "decimate", + pt_deci_l: ["enable or disable point decimation","during port import. for faster slicing","and lower memory usage"], + pt_qual_s: "detail", + pt_qual_l: ["level of detail to retain during slicing operations. lower is faster and uses less memory. large scale models often do no benefit from higher detail. instead it my only result in memory and processing problems. reduce this value if you are getting out of memory errors."], + pt_heal_s: "heal mesh", + pt_heal_l: ["attempt to heal non-manifold meshes during slicing. may increase slicing time. enables a warning when non-manifold geometries are detected."], + pt_thrd_s: "threaded", + pt_thrd_l: ["use parallel processing when browser supports nested workers. may increase memory pressure on larger models. disable if you are getting out of memory errors"], + pt_assy_s: "assembly", + pt_assy_l: ["use web assembly when supported by browser. if you experience artifacts or memory errors, try disabling this."], + + xp_menu: "exports", + + // SETTINGS + se_menu: "profile", + se_load: "load", + se_save: "save", + + // FDM LAYERS + sl_menu: "paths", + sl_angl_s: "slice angle", + sl_angl_l: ["slice angle bias in degrees"], + sl_lahi_s: "layer height", + sl_lahi_l: ["height of each slice","layer in millimeters"], + ad_minl_s: "layer minimum", + ad_minl_l: ["adaptive min layer height","in millimeters","must be non-zero"], + ad_adap_s: "adaptive", + ad_adap_l: ["use adaptive layer heights","with 'layer height' as max","and 'layer min' as the min"], + sl_ltop_s: "top layers", + sl_ltop_l: ["number of solid layers at the top of the print or on flat areas facing up"], + sl_lsld_s: "solid layers", + sl_lsld_l: ["solid fill areas computed","from bridges and flat areas","detected betwen sliced layers"], + sl_lbot_s: "base layers", + sl_lbot_l: ["number of solid layers at bottom of the print or under bridge spans"], + + // FDM WALLS + sw_menu: "shells", + sl_shel_s: "shell count", + sl_shel_l: ["number of perimeter","walls to generate"], + ad_thin_s: "thin walls", + ad_thin_l: ["detect and fill gaps","between shell walls"], + + // FDM SHELLS + sl_line_s: "shell width", + sl_line_l: ["extrusion width in millimeters","0 = nozzle diameter"], + sl_ordr_s: "shell order", + sl_ordr_l: ["output shell order","inside to outside","or outside to inside","affects surface quality"], + sl_strt_s: "layer start", + sl_strt_l: ["layer starting point","last = last layer end","center = part center","origin = device origin"], + + // FDM FILL + fi_menu: "sparse fill", + fi_type: "fill type", + fi_pcnt_s: "fill amount", + fi_pcnt_l: ["fill density values","0.0 - 1.0"], + fi_rept_s: "fill repeat", + fi_rept_l: ["number of times to repeat","unchanged fill layers"], + fi_angl_s: "solid angle", + fi_angl_l: ["starting angle in degrees","90 degrees added to ","each following layer","applies only to solid layers"], + fi_grow_s: "solid expand", + fi_grow_l: ["expand projected solids","can help with solid areas","over sparse infill areas","units in millimeters"], + fi_over_s: "fill overlap", + fi_over_l: ["overlap with shell and other fill","for improved extrusion bonding","as fraction of nozzle diameter","0.0 - 2.0"], + // fi_rate_s: "print speed", + fi_rate_l: ["extrusion speed for infill","set to 0 to use default","output print speeds"], + + // FDM HEATING + fh_menu: "heating", + + // FDM COOLING + fc_menu: "cooling", + + // FDM FIRST LAYER + fl_menu: "base", + fl_lahi_s: "layer height", + fl_lahi_l: ["height of each slice","in millimeters","should be >= slice height"], + fl_rate_s: "shell speed", + fl_rate_l: ["shell printing max speed","in millimeters / second"], + fl_frat_s: "fill speed", + fl_frat_l: ["fill printing max speed","in millimeters / second"], + fl_mult_s: "flow factor", + fl_mult_l: ["extrusion multiplier","0.0 - 2.0"], + fl_sfac_s: "width factor", + fl_sfac_l: ["multiplier on nozzle size","changes line spacing"], + fl_skrt_s: "skirt count", + fl_skrt_l: ["number of first-layer offset","brims to generate"], + fl_skro_s: "skirt offset", + fl_skro_l: ["skirt offset from part","in millimeters","when rafts are used","this is the raft offset"], + fl_nozl_s: "nozzle temp", + fl_nozl_l: ["degrees in celsius","output setting used","when this is zero"], + fl_bedd_s: "bed temp", + fl_bedd_l: ["degrees in celsius","output setting used","when this is zero"], + fr_spac_s: "raft gap", + fr_spac_l: ["additional layer spacing","between 1st layer and raft","in millimeters"], + fr_nabl_s: "raft enable", + fr_nabl_l: ["create a raft under the","model for better adhesion","uses skirt offset and","disables skirt output"], + fr_draf_s: "draft shield", + fr_draf_l: ["extend skirt to height of part","which acts as a shield against","drafts and traps hot air"], + + // FDM BELT ONLY + fl_zoff_s: "belt offset", + fl_zoff_l: ["height offset from belt","of lowest extrusion","in millimeters"], + fl_flat_s: "flatten", + fl_flat_l: ["flatten to the belt any point","height less than this value","in millimeters"], + fl_brim_s: "brim sides", + fl_brim_l: ["add brim to part perimeter","size is width in millimeters","0 to disable"], + fl_brin_s: "brim interior", + fl_brin_l: ["add brim to part interior","when interior voids exist","size is width in millimeters","0 to disable"], + fl_brmn_s: "brim trigger", + fl_brmn_l: ["add brims only when segment","facing belt is shorter than this","value in millimeters","0 = Infinity"], + fl_brco_s: "brim comb", + fl_brco_l: ["add brim gaps","every n lines","0 to disable"], + fl_brgp_s: "brim gap", + fl_brgp_l: ["spacing between brim and part","in millimeters"], + fl_bled_s: "part anchor", + fl_bled_l: ["belt part anchor","at start of print","in millimeters"], + fl_blmp_s: "anchor bump", + fl_blmp_l: ["add a bump to the start of","an anchor to help it peel","off at the end of the belt","in millimeters"], + + // FDM SUPPORT + sp_menu: "support", + sp_detect: "detect", + sp_dens_s: "density", + sp_dens_l: ["fraction 0.0 - 1.0","recommended 0.15","0 to disable"], + sp_size_s: "pillar size", + sp_size_l: ["pillar width in millimeters","smaller values exponentially","increase detection times"], + sp_offs_s: "part offset", + sp_offs_l: ["offset from part","in millimeters"], + sp_gaps_s: "gap layers", + sp_gaps_l: ["number of layers","offset from part"], + sp_span_s: "max span", + sp_span_l: ["unsupported span that causes","a new support to be generated","in millimeters"], + sp_angl_s: "max angle", + sp_angl_l: ["max overhang angle before","supporting pillar is generated","normal values 45-60","belt values 0-10"], + sp_area_s: "min area", + sp_area_l: ["minimum area for","a support column","in millimeters","smaller values will","slow detection"], + sp_xpnd_s: "expand", + sp_xpnd_l: ["expand support area","beyond part boundary","in millimeters"], + sp_grow_s: "grow", + sp_grow_l: ["grow support column to fill small gaps","in millimeters"], + sp_nozl_s: "extruder", + sp_nozl_l: ["in multi-extruder systems","the extruder to use for","support material"], + sp_outl_s: "enclosed", + sp_outl_l: ["enclose support pattern","inside a perimeter shell"], + sp_auto_s: "automatic", + sp_auto_l: ["enable generated supports","using at-slice-time geometry","supports will only appear","after slicing completes"], + + // CNC COMMON terms + cc_tool: "tool", + cc_offs_s: "offset", + cc_offs_l: ["offset tool center","from chosen path"], + cc_spnd_s: "spindle rpm", + cc_spnd_l: ["spindle speed in","revolutions / minute"], + cc_sovr_s: "step over", + cc_sovr_l: ["a fraction of tool diameter between 0 and 1. values greater than 0.49 may result in uncut areas or undesirable artifacts."], + cc_sovc_s: "wide steps", + cc_sovc_l: ["number of wide cutout steps"], + cc_sang_s: "step angle", + cc_sang_l: ["rotation in degrees between lathe axial points"], + cc_sdwn_s: "step down", + cc_sdwn_l: ["step down depth","for each pass","in workspace units","0 to disable"], + cc_thru_s: "cut thru", + cc_thru_l: ["additional cut depth below trace bottom"], + cc_offd_s: "offset", + cc_offd_l: ["overrides default offset distance (which is the tool radius) when set to any value but zero"], + cc_feed_s: "feed rate", + cc_feed_l: ["max cutting speed in","workspace units / minute"], + cc_plng_s: "plunge rate", + cc_plng_l: ["max z axis speed in","workspace units / minute"], + cc_sngl_s: "select lines only", + cc_sngl_l: ["select only single edges","instead of connected polylines"], + cc_sele_s: "select", + cc_sele_l: ["select lines for follow","select loops for clear","select surface for pocket"], + + // CNC COMMON + cc_menu: "limits", + cc_rapd_s: "xy feed", + cc_rapd_l: ["max xy moves feedrate","in workspace units / minute"], + cc_rzpd_s: "z feed", + cc_rzpd_l: ["max z moves feedrate","in workspace units / minute"], + + // CNC LEVELING + cc_loff_s: "z offset", + cc_loff_l: ["distance from stock face","in workspace units"], + cc_lxyo_s: "xy offset", + cc_lxyo_l: ["xy offset from part boundary as a fraction of the tool diameter (0 - 1.0)"], + cc_lsto_s: "stock", + cc_lsto_l: ["use stock area instead of part area"], + + // CNC ROUGHING + cr_menu: "rough", + cr_lsto_s: "leave stock", + cr_lsto_l: ["horizontal offset from vertical faces","stock to leave for finishing pass","in workspace units"], + cr_lstz_s: "leave stock z", + cr_lstz_l: ["vertical offset for roughing passes in workspace units. best for roughing curved surfaces."], + cr_ease_s: "ease down", + cr_ease_l: ["plunge cuts will","spiral down or ease","along a linear path"], + cr_clst_s: "clear stock", + cr_clst_l: ["in indexed mode, clear entire stock area, not just to part periemter"], + cr_clrt_s: "clear top", + cr_clrt_l: ["run a clearing pass over","the bounding area of the part","at z = 0"], + cr_clrp_s: "clear voids", + cr_clrp_l: ["mill out through pockets","instead of just the outline"], + cr_clrf_s: "clear faces", + cr_clrf_l: ["interpolate step down to","clear any detected flat areas"], + cr_olin_s: "inside only", + cr_olin_l: ["limit cutting to","inside part boundaries"], + + // CNC OUTLINE + co_menu: "outline", + co_merg_s: "merge overlap", + co_merg_l: ["merge overlapping lines to prevent overcutting into adjacent solids"], + co_dogb_s: "dogbones", + co_dogb_l: ["insert dogbone cuts","into inside corners"], + co_dogr_s: "reverse bones", + co_dogr_l: ["reverse dogbone direction"], + co_clrt_s: "clear top", + co_clrt_l: ["cut starting at the top of stock","when stock is enabled"], + co_wide_s: "wide cutout", + co_wide_l: ["widen outside cutout paths","for deep cuts in hard material"], + co_olin_s: "inside only", + co_olin_l: ["limit cutting to","inside part boundaries"], + co_olot_s: "outside only", + co_olot_l: ["limit cutting to","exterior part boundaries","which can be thought of","as the shadow outline"], + co_omit_s: "omit through", + co_omit_l: "eliminate thru holes", + co_omvd_s: "omit pocket", + co_omvd_l: "eliminate interior pockets", + co_olen_s: "enable", + co_olen_l: "enabled outline cutting", + + // CNC CONTOUR + cn_menu: "contour", + cf_angl_s: "max angle", + cf_angl_l: ["changes in z that result in angles greater than this are considered vertical and omitted from curves"], + cf_leav_s: "leave stock", + cf_leav_l: ["amount of stock to leave above part surface"], + cf_botm_s: "z bottom", + cf_botm_l: ["obey z bottom limit"], + cf_curv_s: "curves only", + cf_curv_l: ["limit linear cleanup","to curved surfaces"], + cf_olin_s: "inside only", + cf_olin_l: ["limit cutting to","inside part boundaries"], + cf_linx_s: "enable y pass", + cf_linx_l: "linear y-axis finishing", + cf_liny_s: "enable x pass", + cf_liny_l: "linear x-axis finishing", + + // CNC TRACE + cu_menu: "trace", + cu_type_s: "type", + cu_type_l: ["clear = clear a closed-line pocket","follow = tool tip follows line","with offset option when closed"], + + // CNC POCKET + cp_xpnd_s: "expand", + cp_xpnd_l: ["grow (+) or shrink (-) selected areas","contour uses entire selected area","non-contour defaults to tool radius","in workspace units"], + cp_smoo_s: "smooth", + cp_smoo_l: ["clean up jagged edges caused by angled walls. can speedup and improve pocketing for detailed geometries. recommend starting with values around 1 if slicing takes too long or the contours look jagged"], + cp_foll_s: "selection", + cp_foll_l: ["curvature selection limit in degrees. values closer to 0 will restrict selections to flatter surfaces"], + cp_refi_s: "refine", + cp_refi_l: ["number of refining passes to perform on contoured polylines. meant to address Z delta sawtoothing caused by faced geometries whereas smoothing handles XY. values > 10 work well for gently contoured geometries with significant Z movement."], + cp_cont_s: "contour", + cp_cont_l: ["ignore interior voids and features"], + cp_engr_s: "engrave", + cp_engr_l: ["sets up a single pass around the perimeter of the selected area. also useful for 3D laser marking"], + cp_outl_s: "outline only", + cp_outl_l: ["ignore interior voids and features"], + + // CNC DRILLING + cd_menu: "drill", + cd_axis: "axis", + cd_points: "points", + // cd_plpr_s: "plunge per", + // cd_plpr_l: ["max plunge between","dwell periods","in workspace units","0 to disable"], + cd_dwll_s: "dwell time", + cd_dwll_l: ["dwell time","between plunges in","in milliseconds"], + cd_lift_s: "drill lift", + cd_lift_l: ["lift between plunges","after dwell period","in workspace units","0 to disable"], + cd_regi_s: "register", + cd_regi_l: ["drill registration holes","for double-sided parts","independent of enable","drilling but uses same","tool and settings"], + cd_thru_s: "z extend", + cd_thru_l: ["extend downward cut through stock","in workspace units"], + cd_dtru_s: "drill through", + cd_dtru_l: ["additional drill depth below part bottom","overrides global z thru","0 to disable"], + cd_mark_s: "marking", + cd_mark_l: ["only mark holes, do not drill out","the step down setting is used","to determine how deep to mark"], + cd_ftop_s: "from stock top", + cd_ftop_l: ["drill from stock top","instead of part surface"], + cd_prcn_s: "precision", + cd_prcn_l: ["distance between slices","to search for holes","in workspace units","lower is slower and","uses more memory","0 = only flats"], + + cd_seli_l: ["individually select holes","to drill"], + cd_sela_s: "Select Matching Diameter", + cd_sela_l: ["select all holes that", "match selected tool diameter"], + + // CNC FLIP + cf_menu: "flip", + cf_nvrt_s: "z bottom", + cf_nvrt_l: "invert z bottom setting to follow stock", + + // CNC INDEX AXIS + ci_degr_s: "degrees", + ci_degr_l: "degrees to rotate the A (indexed) axis", + ci_abso_s: "absolute", + ci_abso_l: "degree rotation is absolute", + ci_face_s: "face", + ci_face_l: "select face to rotate facing up", + ci_line_s: "linear", + ci_line_l: "make linear passes along X then rotate Y", + + // CNC LASER On/Off Operations (Carvera) + cl_powr_s: "power", + cl_powr_l: "laser power from 0 to 1.0", + cl_adap_s: "adaptive", + cl_adap_l: "use adaptive power based on Z height", + cl_adrp_s: "repeating", + cl_adrp_l: "apply a moduluse of Z height using the given band to make repeating patterns", + cl_flat_s: "flatten", + cl_flat_l: "flatten output to a given Z height", + cl_flaz_s: "flat height", + cl_flaz_l: ["flatten output to this Z height", "in workspace units"], + cl_minp_s: "z min power", + cl_minp_l: ["power setting at the lowest part of the Z band. may be higher or lower than the other end of the Z band."], + cl_maxp_s: "z max power", + cl_maxp_l: ["power setting at the highest part of the Z band. may be higher or lower than the other end of the Z band."], + cl_minz_s: "min z height", + cl_minz_l: ["lowest Z band position"], + cl_maxz_s: "max z height", + cl_maxz_l: ["highest Z band position. when set to 0, uses the highest Z point of the workspace"], + + // CNC CUTOUT TABS + ct_menu: "tabs", + ct_angl_s: "angle", + ct_angl_l: ["starting angle for tab spacing","in degrees (0-360)"], + ct_numb_s: "count", + ct_numb_l: ["number of tabs to use","will be spaced evenly","around the part"], + ct_wdth_s: "width", + ct_wdth_l: "width in workspace units", + ct_hght_s: "height", + ct_hght_l: "height in workspace units", + ct_dpth_s: "depth", + ct_dpth_l: ["distance in workspace units","that the tab projects from","the part surface"], + ct_midl_s: "midline", + ct_midl_l: ["use midline of tab","instead of z bottom","for double-sided work"], + ct_nabl_s: "auto", + ct_nabl_l: ["auto generate radial tabs","projected from part center","using count and angle offset"], + + // OUTPUT + ou_menu: "output", + + // LASER SLICING + ls_offs_s: "offset / kerf", + ls_offs_l: ["distance between the center of the cutting","path and the edge of remaining material","in millimeters"], + ls_lahi_s: "slice height", + ls_lahi_l: ["layer height","in millimeters","0 = auto/detect"], + ls_lahm_s: "slice minimum", + ls_lahm_l: ["layer height minimum","will merge auto slices","under this thickness","in millimeters"], + ls_sngl_s: "single slice", + ls_sngl_l: ["perform only one slice","at specified layer height"], + + // LASER KNIFE + dk_menu: "drag knife", + dk_dpth_s: "cut depth", + dk_dpth_l: ["final cut depth","in millimeters"], + dk_pass_s: "cut passes", + dk_pass_l: ["number of passes","down to cut depth"], + dk_offs_s: "tip offset", + dk_offs_l: ["distance from blade tip","to center of tool","in millimeters"], + + // WIRE EDM + we_omit_s: "omit inner", + we_omit_l: "omit inner polygons", + + // LASER OUTPUT + ou_spac_s: "spacing", + ou_spac_l: ["distance between layer output","in millimeters"], + ou_scal_s: "scaling", + ou_scal_l: "multiplier (0.1 to 100)", + ou_powr_s: "power", + ou_powr_l: ["0 - 100","represents %"], + ou_sped_s: "speed", + ou_sped_l: "millimeters / second", + ou_mrgd_s: "merged", + ou_mrgd_l: ["merge all layers using","color coding to denote","stacking depth"], + ou_grpd_s: "grouped", + ou_grpd_l: ["retain each layer as","a unified grouping","instead of separated","polygons"], + ou_layr_s: "layer order", + ou_layr_l: ["output layer order","from top right to","bottom left"], + ou_layo_s: "layer color", + ou_layo_l: ["output layer colors","for each z index","overridden by merged"], + ou_lays_s: "layer marking", + ou_lays_l: ["mark layers for stacking. the layer above will be output in a different color which the laser can mark with a lower power. turns on layer grouping."], + ou_drkn_s: "drag knife", + ou_drkn_l: ["enable drag knife","output in gcode","cut radii are added","to corners with","cut down passes"], + ou_stak_s: "3d stack", + ou_stak_l: ["output is a 3D stack of 2D paths rather than packing layers flat in 2D"], + ou_maxp_s: "max power", + ou_maxp_l: ["max power value in gcode. power % from settings will scale from 0 to this value"], + ou_inch_s: "imperial", + ou_inch_l: ["output values in inches"], + ou_shap_s: "shaper tool", + ou_shap_l: ["annotate SVG with Shaper Tool custom attributes"], + + // OUTPUT FDM + ou_nozl_s: "nozzle temp", + ou_nozl_l: "degrees in celsius", + ou_bedd_s: "bed temp", + ou_bedd_l: "degrees in celsius", + ou_feed_s: "print speed", + ou_feed_l: ["max printing speed","set to 0 to use default","millimeters / second"], + ou_fini_s: "finish speed", + ou_fini_l: ["outermost shell speed","bridge/flat solid layers","set to 0 to use default","millimeters / second"], + ou_move_s: "move speed", + ou_move_l: ["non-print move speed","millimeters / second","0 = enable G0 moves"], + ou_shml_s: "shell factor", + ou_flml_s: "solid factor", + ou_spml_s: "infill factor", + ou_exml_l: ["extrusion multiplier","0.0 - 2.0"], + ou_fanl_s: "fan on layer", + ou_fanl_l: ["layer # to turn on fan","layers start at 0"], + ou_fans_s: "fan speed", + ou_fans_l: "0 - 255", + + // OUTPUT CAM + ou_toll_s: "precision", + ou_toll_l: ["surface precision","in workspace units","lower is slower and","uses more memory","0 = automatic based","on animate preference"], + ou_flat_s: "flatness", + ou_flat_l: ["delta z below which two adjacent points are considered co-linear. 0 = compute using tolerance"], + ou_redu_s: "reduction", + ou_redu_l: ["number of axis steps to skip for each unit of precision. higher precision may require reduction to produce reasonable file sizes. start at 0 and test in increments of 1."], + ou_brdg_s: "bridging", + ou_brdg_l: ["flat distance between curved","areas to bridge with a cut"], + ou_zanc_s: "z anchor", + ou_zanc_l: ["controls the position of the part","when stock Z exceeds part Z"], + ou_ztof_s: "z offset", + ou_ztof_l: ["offset z anchor","in workspace units"], + ou_ztop_s: "z top", + ou_ztop_l: ["offset from stock bottom","to set start of cutting depth","in workspace units","* drill/contour ignore this *"], + ou_zbot_s: "z bottom", + ou_zbot_l: ["offset from stock bottom","to limit cutting depth","in workspace units"], + ou_zclr_s: "z clearance", + ou_zclr_l: ["safe travel offset","from top of part","in workspace units"], + ou_ztru_s: "z thru", + ou_ztru_l: ["extend cutout pass down","in workspace units"], + ou_conv_s: "conventional", + ou_conv_l: ["milling direction","uncheck for 'climb'"], + ou_depf_s: "depth first", + ou_depf_l: ["optimize pocket cuts","with depth priority"], + ou_toin_s: "tool init", + ou_toin_l: "add tool change with first operation. when disabled, the gcode assumes the first tool is loaded and offset probed", + ou_forz_s: "force z max", + ou_forz_l: "all moves between pockets will go to z max (above stock). this is usually required if you are not performing a roughing operation before pocketing operations.", + ou_z1st_s: "first z max", + ou_z1st_l: "first move in the job will be to Z max (travel distance above stock)", + ou_feng_s: "engage factor", + ou_feng_l: "speed scale (0.1-1) when endmill is fully engaged on a new cut. stepped over cuts when the endmill is less than fully engaged run at a factor of 1 relative to output speed. typically this relates to roughing or any operation with step over.", + ou_eang_s: "ease angle", + ou_eang_l: "descent angle for ease down in degrees", + + // CAM STOCK + cs_menu: "stock", + cs_wdth_s: "width", + cs_wdth_l: ["width (x) in workspace units","0 defaults to part size"], + cs_dpth_s: "depth", + cs_dpth_l: ["depth (y) in workspace units","0 defaults to part size"], + cs_hght_s: "height", + cs_hght_l: ["height (z) in workspace units","0 defaults to part size"], + cs_offs_s: "offset", + cs_offs_l: ["use width, depth, height","as offsets from max","part size on platform"], + cs_clip_s: "clip to", + cs_clip_l: ["rough and outline","clip cutting paths","to defined stock"], + cs_ishg_s: "show grid", + cs_ishg_l: ["show platform grid in indexed mode"], + cs_indx_s: "indexed", + cs_indx_l: ["stock is mounted to a rotatary indexer"], + cs_offe_s: "enable", + cs_offe_l: "enable milling stock", + + // ORIGIN (CAM & LASER) + or_bnds_s: "origin bounds", + or_bnds_l: ["origin is relative to","boundary of all objects"], + or_cntr_s: "origin center", + or_cntr_l: "origin is referenced from the center", + or_topp_s: "origin top", + or_topp_l: "origin is references from the top of objects", + or_offx_s: "origin offset X", + or_offx_l: "origin offset X", + or_offy_s: "origin offset Y", + or_offy_l: "origin offset Y", + + // CAM ORIGIN + co_menu: "origin", + co_offx_s: "offset x", + co_offx_l: ["offset origin along x axis","in workspace units"], + co_offy_s: "offset y", + co_offt_l: ["offset origin along y axis","in workspace units"], + co_offz_s: "offset z", + co_offz_l: ["offset origin along z axis","in workspace units"], + + // FDM ADVANCED + ad_menu: "expert", + ad_rdst_s: "retract dist", + ad_rdst_l: ["amount to retract filament","for long moves. in millimeters"], + ad_rrat_s: "retract rate", + ad_rrat_l: ["speed of filament","retraction in mm/s"], + ad_rdwl_s: "engage dwell", + ad_wpln_s: "retract wipe", + ad_wpln_l: ["non-printing move","after retraction","in millimeters"], + ad_rdwl_l: ["time between re-engaging","filament and movement","in milliseconds"], + ad_scst_s: "shell coast", + ad_scst_l: ["non-printing end","of perimeter shells","in millimeters"], + ad_msol_s: "min solid", + ad_msol_l: ["minimum area (mm^2)","required to keep solid","must be > 0.1"], + ad_mins_s: "min speed", + ad_mins_l: ["minimum speed","for short segments"], + ad_spol_s: "short path", + ad_spol_l: ["polygons shorter than this","will have their print speed","scaled down to min speed","in millimeters"], + ad_arct_s: "arc tolerance", + ad_arct_l: ["convert faceted lines to arcs","center point drift tolerance","when matching arc points","consider values around 0.15","in millimeters","0 to disable"], + ad_zhop_s: "z hop dist", + ad_zhop_l: ["amount to raise z","on retraction moves","in millimeters","0 to disable"], + ad_abkl_s: "anti-backlash", + ad_abkl_l: ["for better flat surface finish","use micro-movements to cancel","backlash in solid layer output","in millimeters","0 to disable","if your firmware has M425","put that in the gcode header","and leave this as 0"], + ad_lbir_s: "belt first", + ad_lbir_l: ["always print shells touching the belt before any other shells. this is no longer a recommended setting based on extensive testing."], + ad_altr_s: "alternating", + ad_altr_l: ["alternate shell winding order","clockwise / counter-clockwise","may reduce warping in thin areas","and help with belt edge adhesion"], + ad_zint_s: "interleave z", + ad_zint_l: ["interleave Z heights with odd number shells"], + ad_lret_s: "layer retract", + ad_lret_l: ["force filament retraction","between layers"], + ad_agap_s: "avoid gaps", + ad_agap_l: ["route around gaps rather","than perform a retract","when possible"], + ad_play_s: "polish layers", + ad_play_l: ["polish up to specified","# of layers at a time"], + ad_pspd_s: "polish speed", + ad_pspd_l: ["polishing speed","in millimeters / second"], + ad_purg_s: "purge tower", + ad_purg_l: ["purge tower area in mm^2","for multi extruder setups","0 = disabled"], + + // CAM EXPERT + cx_fast_s: "skip shadow", + cx_fast_l: ["disable overhang detection","can be faster and use less","memory with complex models","but fails with overhangs","try enabling if slicing","hangs during shadowing"], + cx_true_s: "true shadow", + cx_true_l: ["computationally correct shadow","will be slower but produce","better cuts for complex parts"], + cx_arct_s: "arc tolerance", + cx_arct_l: ["convert cicrular paths to arcs;","center point drift tolerance","when matching arc points","consider values around 0.15","in mm, or 0.006 inches;","High values may cause","unexpected behavior","0 to disable"], + cx_arcr_s: "arc resolution", + cx_arcr_l: ["change in line direction","aceptible in an arc","consider values around 5","High values may cause","unexpected behavior",], + + // FDM GCODE + ag_menu: "gcode", + ag_nozl_s: "nozzle", + ag_nozl_l: "select output nozzle or head", + ag_peel_s: "peel guard", + ag_peel_l: ["starting at this belt z position","periodically roll the print off and","back on to the belt to unstick it","and prevent rolling deflection"], + // ag_paws_s: "pause layers", + // ag_paws_l: ["comma-separated list of layers","to inject pause commands before"], + // ag_loop_s: "loop layers", + // ag_loop_l: ["layer ranges to repeat in the format","first-last-count,first-last-count,...","omitted count = 1"], + ag_loop_s: "loop count", + ag_loop_l: ["number of times to repeat","the selected range or the","entire print if no range"], + + // SLA MENU + sa_menu: "slicing", + sa_lahe_s: "layer height", + sa_lahe_l: ["layer height","in millimeters"], + sa_shel_s: "hollow shell", + sa_shel_l: ["shell thickness in mm","use multiple of layer height","use 0 for solid (disabled)"], + sa_otop_s: "open top", + sa_otop_l: ["if shell is enabled","results in an open top"], + sa_obas_s: "open base", + sa_obas_l: ["if shell is enabled","results in an open base","disabled with supports"], + + sa_layr_m: "layers", + sa_lton_s: "light on time", + sa_lton_l: ["layer light on","time in seconds"], + sa_ltof_s: "light off time", + sa_ltof_l: ["layer light off","time in seconds"], + sa_pldi_s: "peel distance", + sa_pldi_l: ["peel distance","in millimeters"], + sa_pllr_s: "peel lift rate", + sa_pllr_l: ["peel lift speed","in mm/sec"], + sa_pldr_s: "peel drop rate", + sa_pldr_l: ["peel drop speed","in mm/sec"], + + sa_base_m: "base", + sa_balc_s: "layer count", + sa_balc_l: ["number of","base layers"], + sa_bltn_l: ["base layer light on","time in seconds"], + sa_bltf_l: ["base layer light off","time in seconds"], + + sa_infl_m: "infill", + sa_ifdn_s: "density", + sa_ifdn_l: ["percent infill","requires shell","0 = disabled","valid 0.0 - 1.0"], + sa_iflw_s: "line width", + sa_iflw_l: ["hatch line width","in millimeters"], + + sa_supp_m: "support", + sa_slyr_s: "base layers", + sa_slyr_l: ["base support layers","value range 0-10"], + sa_slgp_s: "gap layers", + sa_slgp_l: ["number of layers between","raft and bottom of object"], + sa_sldn_s: "density", + sa_sldn_l: ["used to compute the","number of support pillars","0.0-1.0 (0 = disable)"], + sa_slsz_s: "size", + sa_slsz_l: ["max size of a","support pillar","in millimeters"], + sa_slpt_s: "points", + sa_slpt_l: ["number of points in","each support pillar","in millimeters"], + sl_slen_l: "enable supports", + + sa_outp_m: "output", + sa_opzo_s: "z offset", + sa_opzo_l: ["z layer offset","almost always 0.0","0.0-1.0 in millimeters"], + sa_opaa_s: "anti alias", + sa_opaa_l: ["enable anti-aliasing","produces larger files","can blur details"] +}; diff --git a/src/kiri/lang.js b/src/kiri/lang.js index 8b72c10e..187f590b 100644 --- a/src/kiri/lang.js +++ b/src/kiri/lang.js @@ -1,11 +1,6 @@ /** Copyright Stewart Allen -- 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 }; diff --git a/src/kiri/layers.js b/src/kiri/layers.js index bbf05ac5..7e9c3c7f 100644 --- a/src/kiri/layers.js +++ b/src/kiri/layers.js @@ -1,25 +1,9 @@ /** Copyright Stewart Allen -- 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 -- All Rights Reserved */ -"use strict"; +import lang from './lang.js'; +import STACKS from './stacks.js'; -// dep: main.kiri -// dep: moto.license -// dep: geo.base -// dep: kiri.ui -// dep: kiri.api -// dep: kiri.conf -// dep: kiri.lang -// dep: kiri.utils -// dep: kiri.files -// dep: kiri.consts -// dep: kiri.widget -// dep: kiri.stats -// dep: kiri.stacks -// dep: kiri.devices -// dep: kiri.function -// dep: kiri.platform -// dep: kiri.selection -// dep: kiri.settings -// use: kiri.alerts -// use: kiri.files -// use: kiri.frame -// use: moto.ajax -gapp.register("kiri.main", (root, exports) => { +import { $ } from '../moto/webui.js'; +import { api } from '../kiri/api.js'; +import { broker } from '../moto/broker.js'; +import { consts } from '../kiri/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 '../kiri/stats.js'; +import { noop, utils } from '../kiri/utils.js'; +import { version } from '../moto/license.js'; +import { Widget } from './widget.js'; +import { showTools } from '../kiri-mode/cam/tools.js'; - let { data, kiri, moto, noop } = root, - { api, consts, lang, Widget, newWidget, utils, stats } = kiri, - { parseOpt, encodeOpt, o2js, js2o, ls2o } = utils, - { platform, selection, settings } = api, - { 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 = kiri.broker = gapp.broker, - SDB = data.local, - SPACE = kiri.space = moto.space, - FILES = kiri.catalog = kiri.openFiles(new data.Index(SETUP.d ? SETUP.d[0] : 'kiri')), - clone = Object.clone, - UI = {}, - UC = kiri.ui.prefix('kiri').inputAction(api.conf.update), - MODE = MODES.FDM, - STACKS = kiri.stacks, - DRIVER = undefined, - viewMode = VIEWS.ARRANGE, - autoSaveTimer = null, - busy = 0, - { assign } = Object; +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; - // extend API - assign(api, { - ui: UI = assign(api.ui, UI), - uc: UC = assign(api.uc, UC), - stats, - catalog: FILES, - busy: assign(api.busy, { - val() { return busy }, - inc() { kiri.api.event.emit("busy", ++busy) }, - dec() { kiri.api.event.emit("busy", --busy) } - }), - color: COLOR, - const: assign(api.const, { - LANG, - LOCAL, - SETUP, - SECURE, - STACKS, - }), - dialog: assign(api.dialog, { - show: showModal, - hide: hideModal, - update_process_list: updateProcessList - }), - help: assign(api.help, { - show: showHelp, - file: showHelpFile - }), - event: assign(api.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 - }), - group: assign(api.group, { - merge: groupMerge, - split: groupSplit, - }), - hide: assign(api.hide, { - alert(rec, recs) { api.alerts.hide(...arguments) }, - import: noop, - slider: hideSlider - }), - image: assign(api.image, { - dialog: loadImageDialog, - convert: loadImageConvert - }), - language: kiri.lang, - modal: assign(api.modal, { - show: showModal, - hide: hideModal, - visible: modalShowing - }), - mode: assign(api.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() +// 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 (busy === 0) { + LOC.reload(); + } else { + console.log(`reload deferred on busy=${busy}`); + 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(``); + }); + 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
Consider resizing under 1000 x 1000
Proceed with import?").then(ok => { + if (ok) { + loadImageDialog(image, name, true); } - }), - probe: assign(api.probe, { - live: "https://live.grid.space", - grid: noop, - local: noop - }), - process: assign(api.process, { - code: currentProcessCode, - get: currentProcessName - }), - show: assign(api.show, { - alert() { return api.alerts.show(...arguments) }, - progress: setProgress, - controls: setControlsVisible, - slices: showSlices, - layer: setVisibleLayer, - local: showLocal, - import: function() { UI.import.style.display = '' } - }), - space: assign(api.space, { - reload, - auto_save, - restore: restoreWorkspace, - clear: clearWorkspace, - save: saveWorkspace, - set_focus: setFocus, - update: SPACE.update, - is_dark() { return settings.ctrl().dark } - }), - util: assign(api.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))) } - }), - view: assign(api.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, - }), - work: kiri.client + }); + } + const opt = {pre: [ + "
", + "

Image Conversion

", + "

", + " 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.", + "

", + "
", + " ", + " ", + " ", + " ", + " ", + " ", + "
blur value invert image
base size invert alpha
border size
", + "
" + ]}; + 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 = ``; + + 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); }); - // 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(); - const { stacks } = kiri; - // match label checkboxes to preference - for (let label of stacks.getLabels()) { - let check = `${settings.mode}-${api.view.get()}-${label}`; - 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 (busy === 0) { - LOC.reload(); - } else { - console.log(`reload deferred on busy=${busy}`); - do_reload(250); + // 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; } - }, time || 100); + 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); } - function auto_save() { - if (!settings.ctrl().autoSave) { - return; - } - clearTimeout(autoSaveTimer); - autoSaveTimer = setTimeout(() => { - api.space.save(true); - }, 1000); + if (visible) { + return ondone(); } - // update version and init count - let inits = parseInt(SDB.getItem('kiri-init') || stats.get('init') || 0) + 1; - SDB.setItem('kiri-init', inits); - stats.set('init', inits); - stats.set('kiri', kiri.version || gapp.version); + style.height = '0'; + style.display = 'flex'; - // remove version from url, preserve other settings - WIN.history.replaceState({},'','/kiri/' + encodeOpt(SETUP) + LOC.hash); + new TWEEN.Tween(info). + easing(TWEEN.Easing.Quadratic.InOut). + to({ pct: 100 }, 100). + onUpdate(() => { style.height = `${info.pct}%` }). + onComplete(ondone). + start(); +} - /** ****************************************************************** - * Utility Functions - ******************************************************************* */ - - function unitScale() { - return api.mode.is_cam() && settings.ctrl().units === 'in' ? 25.4 : 1; +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 triggerSettingsEvent() { - api.event.emit('settings', settings.get()); - } +function showCatalog() { + showModal("files"); +} - function isSecure(proto) { - return proto.toLowerCase().indexOf("https") === 0; - } - - function setProgress(value = 0, msg) { - value = (value * 100).round(4); - UI.progress.width = value+'%'; - if (self.debug) { - // console.log(msg, value.round(2)); - UI.prostatus.style.display = 'flex'; - if (msg) { - UI.prostatus.innerHTML = msg; - } else { - UI.prostatus.innerHTML = ''; +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 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))); - } - kiri.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(``); - }); - 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) +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.conf.update_fields_from_range(); - } + api.uc.prompt("Export Process Filename", name).then(name => { + if (name) { + api.util.download(data, `${name}.km`); + } + }); +} - 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 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 setWireframe(bool, color, opacity) { - api.widgets.each(function(w) { w.setWireframe(bool, color, opacity) }); - SPACE.update(); +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; - function setEdges(bool) { - if (bool && bool.toggle) { - api.local.toggle('model.edges'); + 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 = ''; + del.onclick = deleteSettings; + + edit.setAttribute('name', sk); + edit.setAttribute('title', 'edit'); + edit.innerHTML = ''; + edit.onclick = editSettings; + + xprt.setAttribute('name', sk); + xprt.setAttribute('title', 'export'); + xprt.innerHTML = ''; + 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; + } + $('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 = ['']; + html.push(``); + html.push(``); + 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(``); + html.push(``); + html.push(``); + html.push(``); + html.push(``); + html.push(``); + devc++; + } + html.push(``); + html.push(`
devicetypestatus
${r.device.name}${r.device.mode}${r.state}
`); + if (devc) { + $('mod-local').innerHTML = html.join(''); } else { - api.local.set('model.edges', bool); + $('mod-local').innerHTML = `
no local devices`; } - 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 = 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); + bind.forEach(rec => { + $(rec.uuid).onclick = () => { + window.open(`http://${rec.host}:${rec.port||4080}/`); + }; }); - } + }); +} - 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) { +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(); +} - showSlider(); +function getMode() { + return settings.mode(); +} - if (typeof(layer) === 'string' || typeof(layer) === 'number') { - layer = parseInt(layer); +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 { - layer = api.var.layer_hi; + cl.remove('selected'); } - - 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(); + }); + // 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()); } - - function showSlider() { - UI.layers.style.display = 'flex'; - UI.slider.style.display = 'flex'; + // 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(); } - - function hideSlider() { - UI.layers.style.display = 'none'; - UI.slider.style.display = 'none'; - UI.speeds.style.display = 'none'; + api.space.restore(null, true); + api.event.emit("mode.set", mode); + if (then) { + then(); } +} - function loadImageDialog(image, name, force) { - if (!force && image.byteLength > 2500000) { - return UC.confirm("Large images may fail to import
Consider resizing under 1000 x 1000
Proceed with import?").then(ok => { - if (ok) { - loadImageDialog(image, name, true); - } - }); - } - const opt = {pre: [ - "
", - "

Image Conversion

", - "

", - " 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.", - "

", - "
", - " ", - " ", - " ", - " ", - " ", - " ", - "
blur value invert image
base size invert alpha
border size
", - "
" - ]}; - 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 currentProcessName() { + return settings.get().cproc[getMode()]; +} - function loadImage(image, opt = {}) { - const info = assign({settings: settings.get(), png:image}, opt); - kiri.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); - }); - } +function currentProcessCode() { + return settings.get().sproc[getMode()][currentProcessName()]; +} - /** ****************************************************************** - * Selection Functions - ******************************************************************* */ +function setControlsVisible(show) { + // TODO fix + // $('mid-left').style.display = show ? 'flex' : 'none'; + // $('mid-right').style.display = show ? 'flex' : 'none'; +} - function groupMerge() { - Widget.Groups.merge(selection.widgets(true)); - } +function downloadBlob(data, filename) { + let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"})); + $('mod-any').innerHTML = `x`; + $('_dexport_').click(); +} - function groupSplit() { - Widget.Groups.split(selection.widgets(false)); - } +// prevent safari from exiting full screen mode +DOC.onkeydown = function (evt) { if (evt.keyCode == 27) evt.preventDefault() } - /** ****************************************************************** - * Settings Functions - ******************************************************************* */ - - // convert any image type to png - function loadImageConvert(res, name) { - let url = URL.createObjectURL(new Blob([res])); - - $('mod-any').innerHTML = ``; - - 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 - kiri.client.clear(); - platform.select_all(); - platform.delete(selection.meshes()); - } - - function modalShowing() { - return UI.modal.style.display === 'flex'; - } - - function showModal(which) { - let mod = UI.modal, - style = mod.style, - visible = modalShowing(), - info = { pct: 0 }; - - // hide all modals befroe showing another - Object.keys(UI.modals).forEach(name => { - 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 = 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'); - 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 }); - 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: kiri.version, - moto: moto.id, - time: Date.now(), - mode, - name - }); - 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 = 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') - } - UI.settingsName.value = settings.proc().processName; - - del.setAttribute('del', sk); - del.setAttribute('title', "remove '"+sk+"'"); - del.innerHTML = ''; - del.onclick = deleteSettings; - - edit.setAttribute('name', sk); - edit.setAttribute('title', 'edit'); - edit.innerHTML = ''; - edit.onclick = editSettings; - - xprt.setAttribute('name', sk); - xprt.setAttribute('title', 'export'); - xprt.innerHTML = ''; - 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; - } - $('kiri-version').innerHTML = `${LANG.version} ${kiri.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 = ['']; - html.push(``); - html.push(``); - 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(``); - html.push(``); - html.push(``); - html.push(``); - html.push(``); - html.push(``); - devc++; - } - html.push(``); - html.push(`
devicetypestatus
${r.device.name}${r.device.mode}${r.state}
`); - if (devc) { - $('mod-local').innerHTML = html.join(''); - } else { - $('mod-local').innerHTML = `
no local devices`; - } - 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(); - kiri.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]; - DRIVER = kiri.driver[mode]; - kiri.driver.current = DRIVER; - // 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); - 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 = `x`; - $('_dexport_').click(); - } - - // prevent safari from exiting full screen mode - DOC.onkeydown = function (evt) { if (evt.keyCode == 27) evt.preventDefault() } - - // upon restore, seed presets - api.event.emit('preset', api.conf.dbo()); - -}); +export { FILES as catalog, LOCAL, SETUP, SECURE }; diff --git a/src/kiri/pack.js b/src/kiri/pack.js index 9d1a1121..2d35e626 100644 --- a/src/kiri/pack.js +++ b/src/kiri/pack.js @@ -1,17 +1,6 @@ /** Copyright Stewart Allen -- 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; - -}); diff --git a/src/kiri/platform.js b/src/kiri/platform.js index b5eb769c..8570dc85 100644 --- a/src/kiri/platform.js +++ b/src/kiri/platform.js @@ -1,26 +1,16 @@ /** Copyright Stewart Allen -- 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 } 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); @@ -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) } -}); - -}); +}; diff --git a/src/kiri/print.js b/src/kiri/print.js index a690011e..8a39bde5 100644 --- a/src/kiri/print.js +++ b/src/kiri/print.js @@ -1,19 +1,13 @@ /** Copyright Stewart Allen -- 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}); } @@ -680,9 +674,7 @@ function newPrint(settings, widgets, id) { return new Print(settings, widgets, id); }; -gapp.overlay(kiri, { +export { Print, newPrint -}); - -}); +}; diff --git a/src/kiri/render.js b/src/kiri/render.js index 0e0c1090..087d788e 100644 --- a/src/kiri/render.js +++ b/src/kiri/render.js @@ -1,35 +1,24 @@ /** Copyright Stewart Allen -- 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 +}; diff --git a/src/kiri/selection.js b/src/kiri/selection.js index c889582b..f19c350d 100644 --- a/src/kiri/selection.js +++ b/src/kiri/selection.js @@ -1,16 +1,8 @@ /** Copyright Stewart Allen -- 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() }, -}); - -}); +}; diff --git a/src/kiri/settings.js b/src/kiri/settings.js index 768251d1..b01d8570 100644 --- a/src/kiri/settings.js +++ b/src/kiri/settings.js @@ -1,23 +1,19 @@ /** Copyright Stewart Allen -- 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) { @@ -761,7 +757,7 @@ function setEnableWASM(bool) { } // extend API (api.conf) -Object.assign(api.conf, { +const apiConf = { dbo: () => { return ls2o('ws-settings') }, get: getSettings, put: putSettings, @@ -777,10 +773,10 @@ Object.assign(api.conf, { restore: restoreSettings, export: settingsExport, import: settingsImport, -}); +}; // extend API (api.settings) -Object.assign(api.settings, { +const apiSet = { get: getSettings, import: settingsImport, import_zip: settingsImportZip, @@ -796,6 +792,16 @@ Object.assign(api.settings, { async put() {}, status: false } -}); +}; -}); +export default { + conf: apiConf, + set: apiSet, + settings: apiSet +}; + +export { + apiConf as conf, + apiSet as set, + apiSet as settings +}; diff --git a/src/kiri/slice.js b/src/kiri/slice.js index c858c082..dce114bf 100644 --- a/src/kiri/slice.js +++ b/src/kiri/slice.js @@ -1,14 +1,8 @@ /** Copyright Stewart Allen -- 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 { base } from '../geo/base.js'; +import { Layers } from './layers.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); } @@ -251,12 +245,10 @@ function newSlice(z, view) { return new Slice(z, view); } -gapp.overlay(kiri, { +export { Top, Slice, newTop, newSlice, setSliceTracker -}); - -}); +}; diff --git a/src/kiri/stack.js b/src/kiri/stack.js index f4866822..74a47d5b 100644 --- a/src/kiri/stack.js +++ b/src/kiri/stack.js @@ -1,15 +1,9 @@ /** Copyright Stewart Allen -- 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 }); } - -}); diff --git a/src/kiri/stacks.js b/src/kiri/stacks.js index 9e9adf22..562a0e32 100644 --- a/src/kiri/stacks.js +++ b/src/kiri/stacks.js @@ -1,13 +1,8 @@ /** Copyright Stewart Allen -- 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 +}; diff --git a/src/kiri/stats.js b/src/kiri/stats.js index abec28ff..cfe201d3 100644 --- a/src/kiri/stats.js +++ b/src/kiri/stats.js @@ -1,20 +1,12 @@ /** Copyright Stewart Allen -- 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 }; diff --git a/src/kiri/tools.js b/src/kiri/tools.js index ef19635d..25f28c41 100644 --- a/src/kiri/tools.js +++ b/src/kiri/tools.js @@ -1,12 +1,10 @@ /** Copyright Stewart Allen -- 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 }; diff --git a/src/kiri/ui.js b/src/kiri/ui.js index bb18c9e6..1166ec98 100644 --- a/src/kiri/ui.js +++ b/src/kiri/ui.js @@ -1,857 +1,846 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { $ } from '../moto/webui.js'; +import { api } from './api.js'; -// use: kiri.init -// use: kiri.main -gapp.register("kiri.ui", [], (root, exports) => { +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; - const { kiri } = root; +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, + 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); + } + } +}; - 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; +function setHidden(map) { + hidden = map; + refresh(); + for (let ctrl of Object.values(heads)) { + ctrl.update(); + } +} - self.$ = function (id) { return DOC.getElementById(id) }; +function onBlur(obj, fn) { + if (Array.isArray(obj)) { + for (let o of obj) onBlur(o, fn); + return; + } + obj.addEventListener('blur', fn); +} - kiri.ui = { - prefix: function(pre) { prefix = pre; return kiri.ui }, - inputAction: function(fn) { inputAction = fn; return kiri.ui }, - lastChange: function() { return lastChange }, - checkpoint, - restore, - refresh, - setHidden, - setMode, - bound, - toInt, - toFloat, - 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',''); +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 = [ + `
` + ]; + if (message) { + html.push(``); + } + if (opt.pre) { + html = opt.pre.appendAll(html); + } + let iid; + if (Array.isArray(input)) { + iid = `confirm-input-${rnd}`; + html.append(`
`); + } else if (input !== undefined) { + iid = `confirm-input-${rnd}`; + html.append(`
`); + } + html.append(`
`); + Object.entries(btns).forEach((row,i) => { + html.append(``); + }); + html.append(`
`); + 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); } - }, - setVisible(el, bool) { - kiri.ui.setClass(el, 'hide', !bool); - }, - setClass(el, clazz, bool) { - if (bool) { - el.classList.add(clazz); + } + 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 = ''; + row.classList.add('hidden'); + } else { + arr.innerHTML = ''; + 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 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 = ``; + + 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 { - el.classList.remove(clazz); + inputAction(); } } }; - function setHidden(map) { - hidden = map; - refresh(); - for (let ctrl of Object.values(heads)) { - ctrl.update(); + 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}`); } } - - 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 { api } = kiri; - 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 = [ - `
` - ]; - if (message) { - html.push(``); + 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; } - if (opt.pre) { - html = opt.pre.appendAll(html); - } - let iid; - if (Array.isArray(input)) { - iid = `confirm-input-${rnd}`; - html.append(`
`); - } else if (input !== undefined) { - iid = `confirm-input-${rnd}`; - html.append(`
`); - } - html.append(`
`); - Object.entries(btns).forEach((row,i) => { - html.append(``); - }); - html.append(`
`); - 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); + event.preventDefault(); + event.stopPropagation(); }); - } - - function refresh() { - setMode(lastMode); - setters.forEach(input => { - if (input.setv) { - input.setv(input.real); + ip.addEventListener('keyup', function(event) { + if (event.keyCode === 13) { + ip.blur(); } }); - } - - 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); + 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 (opt.attr) { - for (let [key, val] of Object.entries(opt.attr)) { - el.setAttribute(key, val); - } - } - return el; } + if (!ip.convert) ip.convert = raw.bind(ip); + ip.setVisible = row.setVisible; - function newGroup(label, div, opt = {}) { - lastDiv = div = (div || lastDiv); + return ip; +} - 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); - }); +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 { - row.setAttribute("class", "set-header"); + input.value = value; } + return input.real; + }; + return input; +} - if (div && opt.driven) { - addModeControls(div, opt); +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); } - - 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 = ''; - row.classList.add('hidden'); - } else { - arr.innerHTML = ''; - row.classList.remove('hidden'); - } - } - }; - row.onclick = () => { - hidden[group] = !hidden[group]; - refresh(); - ctrl.update(); - }; - } - - return row; + if (options.action) action = options.action; } + ip.setVisible = row.setVisible; - function addCollapsableElement(parent, options = {}) { - let row = newDiv(options); - if (parent) parent.appendChild(row); - if (lastGroup) lastGroup.push(row); - return row; + 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"); } - - 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 raw() { - return this.value !== '' ? this.value : null; - } - - function setUnits(v) { - if (v !== units) { - units = v; + 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; + }; - 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; - } + return ip; +} - 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 = ``; - - 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; +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", 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; + 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"); } - - 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; + if (opt.title) { + ip.setAttribute("title", opt.title); + row.setAttribute("title", opt.title); } - - 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) { + if (action) { + ip.onclick = function(ev) { + action(ip); + if (opt.trigger) { refresh(); } }; - ip.onclick = (ev) => { - groupSticky = true; - }; - // because firefox - ip.onmouseenter = (ev) => { - groupSticky = true; - }; - - return ip; } + ip.setVisible = row.setVisible; - function newBoolean(label, action = bindTo, opt = {}) { - let row = newDiv(opt), - ip = DOC.createElement('input'), - hide = opt.hide; + return ip; +} - if (label) { - row.appendChild(newLabel(label)); - } - row.appendChild(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"); - 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; + if (opt.class) { + opt.class.split(' ').forEach(ce => { + row.classList.add(ce); + }); } - // unlike other elements, does not auto-add to a row - function newButton(label, action, opt = {}) { - let b = DOC.createElement('button'); - let { api } = kiri; + return row; +} - b.onclick = function() { - switch (typeof action) { - case "string": - api.event.emit(action); - break; - case "function": - action(...arguments); - break; - } - }; +// unlike other elements, does not auto-add to a row +function newButton(label, action, opt = {}) { + let b = DOC.createElement('button'); - 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)); + b.onclick = function() { + switch (typeof action) { + case "string": + api.event.emit(action); + break; + case "function": + action(...arguments); + break; } + }; - addModeControls(b, opt); - addId(b, opt); - - return b; + 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)); } - 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; - } + 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; +} diff --git a/src/kiri/utils.js b/src/kiri/utils.js index d75db786..eb29046a 100644 --- a/src/kiri/utils.js +++ b/src/kiri/utils.js @@ -1,8 +1,6 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -gapp.register("kiri.utils", [], (root, exports) => { +import { local as dataLocal } from '../data/local.js'; function parseOpt(ov) { let opt = {}, kv, kva; @@ -46,7 +44,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 +97,14 @@ function trackFn(fn, name) { } } -exports({ +function noop() {} + +export { trackFn, areEqual, parseOpt, encodeOpt, + noop, ajax, o2js, js2o, @@ -111,6 +112,19 @@ exports({ avgc, rgb, a2c -}); +}; -}); +export let utils = { + trackFn, + areEqual, + parseOpt, + encodeOpt, + noop, + ajax, + o2js, + js2o, + ls2o, + avgc, + rgb, + a2c +}; diff --git a/src/kiri/widget.js b/src/kiri/widget.js index c55df903..21b0cf71 100644 --- a/src/kiri/widget.js +++ b/src/kiri/widget.js @@ -1,23 +1,11 @@ /** Copyright Stewart Allen -- 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 }; diff --git a/src/kiri/widgets.js b/src/kiri/widgets.js index 58d939cd..dce88a44 100644 --- a/src/kiri/widgets.js +++ b/src/kiri/widgets.js @@ -1,14 +1,9 @@ /** Copyright Stewart Allen -- 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] } -}); - -}); +}; diff --git a/src/load/3mf.js b/src/load/3mf.js index c1d59bc3..a617c716 100644 --- a/src/load/3mf.js +++ b/src/load/3mf.js @@ -2,21 +2,8 @@ 'use strict'; -// dep: add.three -// dep: ext.jszip -gapp.register('load.3mf', [], (root, exports) => { - -let load = self.load = self.load || {}; - -if (load.TMF) return; - -load.TMF = { - parseAsync -}; - -load.XML = { - query -}; +import { THREE } from '../ext/three.js'; +import * as JSZip from '../ext/jszip.js'; let { BufferAttribute, Matrix4 } = THREE; @@ -29,8 +16,7 @@ let scaleMap = { "centimeter": (1 / 10) }; -// simple query api for xml structures -function query(node, path, fn) { +export function query(node, path, fn) { let collect = {}; let match = path[0].split('|').map(key => { if (key[0] === '+') { @@ -230,9 +216,9 @@ function extractItems(records) { * @param {Object} data binary file * @returns {Array} vertex face array */ -function parseAsync(data) { +export function parseAsync(data) { return new Promise(async (resolve, reject) => { - let zip = await JSZip.loadAsync(data); + let zip = await JSZip.default.loadAsync(data); let models = {}; for (let [key, value] of Object.entries(zip.files)) { if (key.endsWith(".model")) { @@ -243,6 +229,4 @@ function parseAsync(data) { } resolve(extractItems(models)); }); -} - -}); \ No newline at end of file +} \ No newline at end of file diff --git a/src/load/file.js b/src/load/file.js index f92aa5ad..c70ca280 100644 --- a/src/load/file.js +++ b/src/load/file.js @@ -2,36 +2,29 @@ 'use strict'; -(function() { - -let load = self.load = self.load || {}; -if (load.File) return; - -// dep: load.3mf -// dep: load.obj -// dep: load.stl -// dep: load.svg -// dep: load.url -// dep: load.png -// dep: load.gbr -gapp.register('load.file', []); +import { STL } from './stl.js'; +import * as OBJ from './obj.js'; +import * as TMF from './3mf.js'; +import * as SVG from './svg.js'; +import * as GBR from './gbr.js'; +import { load as pngLoad } from './png.js'; const types = { stl(data, file, resolve, reject, opt = {}) { resolve([{ - mesh: new load.STL().parse(data), file + mesh: new STL().parse(data), file }]); }, obj(data, file, resolve, reject, opt = {}) { - resolve(load.OBJ.parse(data).map(m => { + resolve(OBJ.parse(data).map(m => { return { mesh: m.toFloat32(), file: nameOf(file, m.name, 1) } })); }, "3mf"(data, file, resolve, reject, opt = {}) { let i = 1; - load.TMF.parseAsync(data).then((meshes) => { + TMF.parseAsync(data).then((meshes) => { resolve(meshes.map(m => { return { mesh: m.faces.toFloat32(), file: nameOf(file, m.name, i++) } })); @@ -39,37 +32,29 @@ const types = { }, svg(data, file, resolve, reject, opt = {}) { - let out = load.SVG.parse(data, opt); + let out = SVG.parse(data, opt); resolve(opt.flat ? out : out.map(m => { return { mesh: m.toFloat32(), file } })); }, png(data, file, resolve, reject, opt = {}) { - load.PNG.parse(data, { + pngLoad.PNG.parse(data, { ...opt, - done(data) { - resolve({ mesh: data, file }); - } + done(vertices) { resolve({ mesh: vertices, file }) }, + error(err) { reject(err) } }); }, gbr(data, file, resolve, reject, opt = {}) { if (opt.flat) { - resolve([ load.GBR.parse(data) ]) + resolve([ GBR.parse(data) ]) } else { - resolve([{ mesh: load.GBR.toMesh(data).toFloat32(), file }]) + resolve([{ mesh: GBR.toMesh(data).toFloat32(), file }]) } } }; const as_buffer = [ "stl", "png", "3mf" ]; -load.File = { - types, - as_buffer, - load_data, - load: load_file -}; - function nameOf(file, part, i) { let lid = file.lastIndexOf('.'); if (!part && lid > 0) { @@ -79,7 +64,7 @@ function nameOf(file, part, i) { } function load_data(data, file, ext, opt = {}) { - ext = ext || name.toLowerCase().split('.').pop(); + ext = ext || file.name.toLowerCase().split('.').pop(); return new Promise((resolve, reject) => { let fn = types[ext]; if (fn) { @@ -115,4 +100,4 @@ function load_file(file, opt) { }); } -})(); +export { types, as_buffer, load_data, load_file, load_file as load }; diff --git a/src/load/gbr.js b/src/load/gbr.js index 45497112..eac48dcd 100644 --- a/src/load/gbr.js +++ b/src/load/gbr.js @@ -1,22 +1,12 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -// use: geo.bounds -// use: geo.polygon -// use: geo.polygons -// use: ext.gerber -// use: geo.csg -gapp.register("load.gbr", (root, exports) => { +import { polygons } from '../geo/polygons.js'; +import { CSG } from '../geo/csg.js'; +import * as TracespaceParser from '../ext/gerber.js'; +import { newPolygon } from '../geo/polygon.js'; +import { newBounds } from '../geo/bounds.js'; -const { load } = root; -const POLYS = base.polygons; -const CSG = base.CSG; - -load.GBR = { - parse, - toMesh -}; - -function toMesh(text, opt = {}) { +export function toMesh(text, opt = {}) { const progress = opt.progress || function(){}; const debug = opt.debug ? console.log : function(){}; @@ -29,7 +19,7 @@ function toMesh(text, opt = {}) { // should produce one top level poly which is the board outline // next level down should be the remaining copper trace which we // subtract from the board to produce paths that can be milled - const nest = POLYS.nest([...closed, ...circs, ...rects], true); + const nest = polygons.nest([...closed, ...circs, ...rects], true); debug({ nest }); if (nest.length === 0) return [].toFloat32(); @@ -49,7 +39,7 @@ function toMesh(text, opt = {}) { for (let p of nest) { bounds.merge(p.bounds); } - const rect = base.newPolygon().centerRectangle(bounds.center(0), bounds.width() + offset, bounds.height() + offset); + const rect = newPolygon().centerRectangle(bounds.center(0), bounds.width() + offset, bounds.height() + offset); for (let n of nest) { n.depth++; } @@ -92,7 +82,7 @@ function toMesh(text, opt = {}) { // union open trace expansions const opn = []; - for (let poly of POLYS.union(open_exp, 0, true)) { + for (let poly of polygons.union(open_exp, 0, true)) { const tool = poly.tool; opn.push(poly.extrude(z0 + (opt.raise || 0))); } @@ -133,7 +123,7 @@ function toMesh(text, opt = {}) { return CSG.toPositionArray(meshOut); } -function parse(text) { +export function parse(text) { let fact = 1000000; let tools = {}; let tool; @@ -142,7 +132,7 @@ function parse(text) { let circs = []; let rects = []; let last = {}; - let bounds = base.newBounds(); + let bounds = newBounds(); let p = TracespaceParser.createParser(); p.feed(text); let r = p.results(); @@ -178,7 +168,7 @@ function parse(text) { if (true && poly && last.x === x && last.y === y && last.tool === tool) { // console.log('continuation'); } else { - poly = base.newPolygon().setOpen().add(pos.x, pos.y, 0); + poly = newPolygon().setOpen().add(pos.x, pos.y, 0); poly.tool = tool; polys.push(poly); } @@ -192,16 +182,16 @@ function parse(text) { switch (shape.type) { case 'obround': if (shape.xSize === shape.ySize) { - circs.push(base.newPolygon().centerCircle(pos, shape.xSize / 2, 20)); + circs.push(newPolygon().centerCircle(pos, shape.xSize / 2, 20)); } else { console.log('TODO', shape); } break; case 'circle': - circs.push(base.newPolygon().centerCircle(pos, shape.diameter / 2, 20)); + circs.push(newPolygon().centerCircle(pos, shape.diameter / 2, 20)); break; case 'rectangle': - rects.push(base.newPolygon().centerRectangle(pos, shape.xSize, shape.ySize)); + rects.push(newPolygon().centerRectangle(pos, shape.xSize, shape.ySize)); break; default: console.log('TODO', shape); @@ -233,9 +223,5 @@ function parse(text) { open.push(poly); } } - // const nest = base.polygons.nest([...closed, ...circs, ...rects], true); - // console.log({ nest }); return { open, closed, circs, rects }; } - -}); diff --git a/src/load/obj.js b/src/load/obj.js index 2986e3f6..4a8b1f01 100644 --- a/src/load/obj.js +++ b/src/load/obj.js @@ -2,36 +2,34 @@ 'use strict'; -(function() { - -let load = self.load = self.load || {}; -if (load.OBJ) return; - -gapp.register('load.obj'); - -load.OBJ = { - parse, - parseAsync -}; +import { util } from '../geo/base.js'; /** * @param {String} text * @returns {Array} vertex face array */ - -function parseAsync(text) { +export function parseAsync(text) { return new Promise((resolve,reject) => { resolve(parse(text)); }); } -function parse(text) { - +export function parse(text) { let lines = text.replaceAll(/ +/g,' ').split('\n').map(l => l.trim()); let verts = []; let faces = []; let objs = [ faces ]; + // Polyfill for appendAll if not present + if (!Array.prototype.appendAll) { + Object.defineProperty(Array.prototype, 'appendAll', { + value: function(arr) { + for (let v of arr) this.push(v); + }, + enumerable: false + }); + } + for (let line of lines) { let toks = line.split(' '); switch (toks.shift()) { @@ -46,7 +44,7 @@ function parse(text) { let tok = toks.map(f => parseInt(f.split('/')[0])); if (tok.length > 3) { const p = tok.map(t => verts[t-1]).flat(); - const t = base.util.triangulate(p, [], 3, true); + const t = util.triangulate(p, [], 3, true); const r = t.map(i => tok[i]); for (let v of r) { faces.appendAll(verts[v - 1]); @@ -70,5 +68,3 @@ function parse(text) { } return objs; } - -})(); diff --git a/src/load/png.js b/src/load/png.js index c01b63cf..bd029144 100644 --- a/src/load/png.js +++ b/src/load/png.js @@ -1,17 +1,16 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -// use: ext.pngjs -gapp.register("load.png", (root, exports) => { +import { PNG } from '../ext/pngjs.esm.js'; -const { load } = root; - -load.PNG = { - parseAsync, - parse +export const load = { + PNG: { + parseAsync, + parse + } }; function parseAsync(bin, opt) { - return new Promise((resolve,reject) => { + return new Promise((resolve, reject) => { parse(bin, { ...opt, done(vertices) { resolve(vertices) } @@ -255,4 +254,4 @@ function parse(bin, opt = {}) { }); } -}); +function noop() {} diff --git a/src/load/stl.js b/src/load/stl.js index 866fd86a..155b8c90 100644 --- a/src/load/stl.js +++ b/src/load/stl.js @@ -2,267 +2,249 @@ 'use strict'; -(function() { - -let load = self.load = self.load || {}; -if (load.STL) return; - -gapp.register('load.stl'); - -const SP = STL.prototype; -const CDH = 'Content-Disposition'; - -load.STL = STL; - -/** - * @constructor - */ -function STL() { - this.vertices = null; - this.normals = null; - this.colors = null; +// Polyfill for appendAll if not present +if (!Array.prototype.appendAll) { + Object.defineProperty(Array.prototype, 'appendAll', { + value: function(arr) { + for (let v of arr) this.push(v); + }, + enumerable: false + }); } -SP.load = function(url, callback, formdata, scale, credentials, headers) { - const stl = this; - - fetch(url, { - method: formdata ? 'POST' : 'GET', - credentials: credentials ? credentials : 'same-origin', - body: formdata, - ...(headers ? {headers: headers} : {}) - }).then(response => { - if (response.status === 200 || response.status === 0) { - response.arrayBuffer().then(buffer => { - stl.parse(buffer, scale); - - let cdhVal = response.headers.get(CDH); - if (typeof cdhVal === "string") { - cdhVal = cdhVal.split(';').map(v => v.trim()).filter(v => { - return v.indexOf('filename=') === 0; - }).map(v => { - return v.substring(10,v.length-1); - })[0]; - } - if (callback) callback(stl.vertices, cdhVal); - }); - } else { - if (callback) callback(null, response.statusText); - } - }).catch(err => { - if (callback) callback(null, err) - }); -}; - -SP.encode = function(vertices, normals) { - if (!(vertices && vertices.length % 3 === 0)) throw "invalid vertices"; - - let vc = vertices.length / 3, - bs = (vc * 16) + (vc * (2/3)) + 84, - bin = new ArrayBuffer(bs), - writer = new DataView(bin), - i = 0, - j = 0, - pos = 80; - - function writeInt16(val) { - writer.setUint16(pos, val, true); - pos += 2; +export class STL { + constructor() { + this.vertices = null; + this.normals = null; + this.colors = null; } + load(url, callback, formdata, scale, credentials, headers) { + const stl = this; - function writeInt32(val) { - writer.setUint32(pos, val, true); - pos += 4; - } + fetch(url, { + method: formdata ? 'POST' : 'GET', + credentials: credentials ? credentials : 'same-origin', + body: formdata, + ...(headers ? {headers: headers} : {}) + }).then(response => { + if (response.status === 200 || response.status === 0) { + response.arrayBuffer().then(buffer => { + stl.parse(buffer, scale); - function writeFloat(val) { - writer.setFloat32(pos, val, true); - pos += 4; - } - - function writeVertex() { - writeFloat(vertices[i++]); // x - writeFloat(vertices[i++]); // y - writeFloat(vertices[i++]); // z - } - - writeInt32(vc / 3); - while (i < vertices.length) { - writeFloat(normals ? normals[j++] : 0); // norm x - writeFloat(normals ? normals[j++] : 0); // norm y - writeFloat(normals ? normals[j++] : 0); // norm z - writeVertex(); // p1 - writeVertex(); // p2 - writeVertex(); // p3 - writeInt16(0); // attributes - } - - return bin; -}; - -/** - * @param {ArrayBuffer} data - * @param {number} scale - * @returns vertex array - */ -SP.parse = function(data, scale) { - let binData = this.convertToBinary(data); - - let isBinary = function () { - let expect, face_size, n_faces, reader; - reader = new DataView(binData); - face_size = (32 / 8 * 3) + ((32 / 8 * 3) * 3) + (16 / 8); - n_faces = reader.getUint32(80,true); - expect = 80 + (32 / 8) + (n_faces * face_size); - return expect === reader.byteLength; - }; - - return isBinary() - ? this.parseBinary(binData, scale) - : this.parseASCII(this.convertToString(data), scale); -}; - -SP.parseAsync = function(data, scale) { - return new Promise((resolve, reject) => { - resolve(this.parse(data, scale)); - }); -}; - -SP.parseBinary = function(data, scale = 1) { - let reader = new DataView(data), - faces = reader.getUint32 (80, true), - r, g, b, hasColors = false, colors, - defaultR, defaultG, defaultB, alpha; - - // check for default color in STL header ("COLOR=rgba" sequence). - for (let index = 0; index < 80 - 10; index++) { - if ((reader.getUint32(index, false) == 0x434F4C4F /*COLO*/) && - (reader.getUint8(index + 4) == 0x52 /*'R'*/) && - (reader.getUint8(index + 5) == 0x3D /*'='*/)) { - hasColors = true; - colors = new Float32Array(faces * 3 * 3); - defaultR = reader.getUint8(index + 6) / 255; - defaultG = reader.getUint8(index + 7) / 255; - defaultB = reader.getUint8(index + 8) / 255; - alpha = reader.getUint8(index + 9) / 255; - } - } - - let offset = 0, - dataOffset = 84, - faceLength = 12 * 4 + 2, - vertices = new Float32Array(faces * 3 * 3), - normals = new Float32Array(faces * 3 * 3); - - colors = hasColors ? new Uint16Array(faces * 3 * 3) : null; - - for (let face = 0; face < faces; face ++) { - - let start = dataOffset + face * faceLength, - normalX = reader.getFloat32(start, true), - normalY = reader.getFloat32(start + 4, true), - normalZ = reader.getFloat32(start + 8, true); - - if (hasColors) { - let packedColor = reader.getUint16(start + 48, true); - if ((packedColor & 0x8000) === 0) { // facet has its own unique color - r = (packedColor & 0x1F) / 31; - g = ((packedColor >> 5) & 0x1F) / 31; - b = ((packedColor >> 10) & 0x1F) / 31; + let cdhVal = response.headers.get(CDH); + if (typeof cdhVal === "string") { + cdhVal = cdhVal.split(';').map(v => v.trim()).filter(v => { + return v.indexOf('filename=') === 0; + }).map(v => { + return v.substring(10,v.length-1); + })[0]; + } + if (callback) callback(stl.vertices, cdhVal); + }); } else { - r = defaultR; - g = defaultG; - b = defaultB; + if (callback) callback(null, response.statusText); + } + }).catch(err => { + if (callback) callback(null, err) + }); + } + encode(vertices, normals) { + if (!(vertices && vertices.length % 3 === 0)) throw "invalid vertices"; + + let vc = vertices.length / 3, + bs = (vc * 16) + (vc * (2/3)) + 84, + bin = new ArrayBuffer(bs), + writer = new DataView(bin), + i = 0, + j = 0, + pos = 80; + + function writeInt16(val) { + writer.setUint16(pos, val, true); + pos += 2; + } + + function writeInt32(val) { + writer.setUint32(pos, val, true); + pos += 4; + } + + function writeFloat(val) { + writer.setFloat32(pos, val, true); + pos += 4; + } + + function writeVertex() { + writeFloat(vertices[i++]); // x + writeFloat(vertices[i++]); // y + writeFloat(vertices[i++]); // z + } + + writeInt32(vc / 3); + while (i < vertices.length) { + writeFloat(normals ? normals[j++] : 0); // norm x + writeFloat(normals ? normals[j++] : 0); // norm y + writeFloat(normals ? normals[j++] : 0); // norm z + writeVertex(); // p1 + writeVertex(); // p2 + writeVertex(); // p3 + writeInt16(0); // attributes + } + + return bin; + } + parse(data, scale) { + let binData = this.convertToBinary(data); + + let isBinary = function () { + let expect, face_size, n_faces, reader; + reader = new DataView(binData); + face_size = (32 / 8 * 3) + ((32 / 8 * 3) * 3) + (16 / 8); + n_faces = reader.getUint32(80,true); + expect = 80 + (32 / 8) + (n_faces * face_size); + return expect === reader.byteLength; + }; + + return isBinary() + ? this.parseBinary(binData, scale) + : this.parseASCII(this.convertToString(data), scale); + } + parseAsync(data, scale) { + return new Promise((resolve, reject) => { + resolve(this.parse(data, scale)); + }); + } + parseBinary(data, scale = 1) { + let reader = new DataView(data), + faces = reader.getUint32 (80, true), + r, g, b, hasColors = false, colors, + defaultR, defaultG, defaultB, alpha; + + // check for default color in STL header ("COLOR=rgba" sequence). + for (let index = 0; index < 80 - 10; index++) { + if ((reader.getUint32(index, false) == 0x434F4C4F /*COLO*/) && + (reader.getUint8(index + 4) == 0x52 /*'R'*/) && + (reader.getUint8(index + 5) == 0x3D /*'='*/)) { + hasColors = true; + colors = new Float32Array(faces * 3 * 3); + defaultR = reader.getUint8(index + 6) / 255; + defaultG = reader.getUint8(index + 7) / 255; + defaultB = reader.getUint8(index + 8) / 255; + alpha = reader.getUint8(index + 9) / 255; } } - let i = 1, vertexstart; + let offset = 0, + dataOffset = 84, + faceLength = 12 * 4 + 2, + vertices = new Float32Array(faces * 3 * 3), + normals = new Float32Array(faces * 3 * 3); + + colors = hasColors ? new Uint16Array(faces * 3 * 3) : null; + + for (let face = 0; face < faces; face ++) { + + let start = dataOffset + face * faceLength, + normalX = reader.getFloat32(start, true), + normalY = reader.getFloat32(start + 4, true), + normalZ = reader.getFloat32(start + 8, true); - while (i <= 3) { - vertexstart = start + (i++) * 12; - vertices[offset ] = reader.getFloat32 (vertexstart, true) * scale; - vertices[offset + 1] = reader.getFloat32 (vertexstart + 4, true) * scale; - vertices[offset + 2] = reader.getFloat32 (vertexstart + 8, true) * scale; - normals[offset ] = normalX; - normals[offset + 1] = normalY; - normals[offset + 2] = normalZ; if (hasColors) { - colors[offset ] = r; - colors[offset + 1] = g; - colors[offset + 2] = b; + let packedColor = reader.getUint16(start + 48, true); + if ((packedColor & 0x8000) === 0) { // facet has its own unique color + r = (packedColor & 0x1F) / 31; + g = ((packedColor >> 5) & 0x1F) / 31; + b = ((packedColor >> 10) & 0x1F) / 31; + } else { + r = defaultR; + g = defaultG; + b = defaultB; + } } - offset += 3; + + let i = 1, vertexstart; + + while (i <= 3) { + vertexstart = start + (i++) * 12; + vertices[offset ] = reader.getFloat32 (vertexstart, true) * scale; + vertices[offset + 1] = reader.getFloat32 (vertexstart + 4, true) * scale; + vertices[offset + 2] = reader.getFloat32 (vertexstart + 8, true) * scale; + normals[offset ] = normalX; + normals[offset + 1] = normalY; + normals[offset + 2] = normalZ; + if (hasColors) { + colors[offset ] = r; + colors[offset + 1] = g; + colors[offset + 2] = b; + } + offset += 3; + } + } + + this.vertices = vertices; + this.normals = normals; + this.colors = colors; + + return vertices; + } + parseASCII(data, scale = 1) { + let result, + resultText, + patternNormal, + patternVertex, + vertices = [], + normals = [], + patternFace = /facet([\s\S]*?)endfacet/g; + + while ((result = patternFace.exec(data)) !== null) { + resultText = result[0]; + patternNormal = /normal[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g; + patternVertex = /vertex[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g; + while ((result = patternNormal.exec(resultText)) !== null) { + normals.push(parseFloat(result[1])); + normals.push(parseFloat(result[3])); + normals.push(parseFloat(result[5])); + } + while ((result = patternVertex.exec(resultText)) !== null) { + vertices.push(parseFloat(result[1]) * scale); + vertices.push(parseFloat(result[3]) * scale); + vertices.push(parseFloat(result[5]) * scale); + } + } + + let vToFloat32 = new Float32Array(vertices.length), + nToFloat32 = new Float32Array(normals.length), + i; + + for (i=0; i -- All Rights Reserved */ -// use: add.three -gapp.register("load.svg", (root, exports) => { +import { newPolygon } from '../geo/polygon.js'; +import { polygons } from '../geo/polygons.js'; +import { THREE } from '../ext/three.js'; -const { load } = root; - -load.SVG = { - parse, - parseAsync -}; - -function parseAsync(text, opt) { +export function parseAsync(text, opt) { return new Promise((resolve,reject) => { resolve(parse(text, opt)); }); } -function parse(text, opt = { }) { +export function parse(text, opt = { }) { const justPoly = opt.flat || false; const fromSoup = opt.soup !== false || justPoly; const rez = (opt.resolution || 1); @@ -47,10 +41,9 @@ function parse(text, opt = { }) { return curve.getPoints(segs); }).flat(); if (points.length < 3) { - // console.log({ sub, length, points }); continue; } - let poly = base.newPolygon().addPoints(points.map(p => base.newPoint(p.x, -p.y, 0))); + let poly = newPolygon().addPoints(points.map(p => newPolygon.newPoint(p.x, -p.y, 0))); if (poly.appearsClosed()) poly.points.pop(); if (type === 'polyline') poly.setOpen(true); poly._svg = { width, miter }; @@ -61,9 +54,8 @@ function parse(text, opt = { }) { } } - const sub = fromSoup ? base.polygons.nest(polys) : polys; + const sub = fromSoup ? polygons.nest(polys) : polys; const nest = sub.filter(p => { - // filter duplicates for (let pc of polys) { if (pc === p) { return true; @@ -84,5 +76,3 @@ function parse(text, opt = { }) { return objs; } - -}); diff --git a/src/load/url.js b/src/load/url.js index d2021ea4..3cb8d711 100644 --- a/src/load/url.js +++ b/src/load/url.js @@ -2,20 +2,14 @@ 'use strict'; -(function() { - -let load = self.load = self.load || {}; -if (load.URL) return; - -gapp.register('load.url'); - -load.URL = { - load: load_url -}; +import { STL } from './stl.js'; +import { OBJ } from './obj.js'; +import { TMF } from './3mf.js'; +import { SVG } from './svg.js'; const CDH = 'Content-Disposition'; -function load_url(url, options = {}) { +export function load_url(url, options = {}) { return new Promise((resolve, reject) => { let xhr = new XMLHttpRequest(); let file = options.file || options.filename || (((url.split('?')[0]).split('#')[0]).split('/')).pop(); @@ -47,21 +41,21 @@ function load_url(url, options = {}) { switch (ext) { case "stl": resolve([{ - mesh: new load.STL().parse(data), file + mesh: new STL().parse(data), file }]); break; case "obj": - resolve(load.OBJ.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); + resolve(OBJ.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); break; case "3mf": - load.TMF.parseAsync(data).then((meshes) => { + TMF.parseAsync(data).then((meshes) => { resolve(meshes.map(m => { return { mesh: m.faces.toFloat32(), file } })); }); break; case "svg": - resolve(load.SVG.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); + resolve(SVG.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); break; default: reject(`unknown file type: "${ext}" from ${url}`); @@ -84,6 +78,4 @@ function load_url(url, options = {}) { xhr.responseType = datatype; xhr.send(formdata); }); -}; - -})(); +} diff --git a/src/main/kiri.js b/src/main/kiri.js index 69e5ba14..c8bd344c 100644 --- a/src/main/kiri.js +++ b/src/main/kiri.js @@ -1,41 +1,39 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import '../add/array.js'; +import '../add/class.js'; +import '../add/three.js'; +import '../kiri/lang.js'; +import '../kiri/lang-en.js'; -gapp.main({ - app: "kiri", +import { run } from '../kiri/init.js'; - pre(root) { - let mods = root.kirimod = ( root.kirimod || [] ); +const load = []; - let kiri = root.kiri = { - beta: 0, - driver: { - // attached driver modules - }, - load(fn) { - // modules register exec() functions - mods.push(fn); - }, - load_exec(api) { - // process all module exec() functions - const saferun = (fn) => { - try { - fn(api || kiri.api); - } catch (error) { - console.log({ module_error: error }); - } - }; - // complete module loading - mods.forEach(fn => saferun(fn)); - // rewrite load() to run immediately post-finalize - kiri.load = (fn) => saferun(fn); +function checkReady() { + if (document.readyState === 'complete') { + let api = run(); + kiri.api = api; + for (let fn of load) { + try { + fn(api); + } catch (error) { + console.log('load error', fn, error); } - }; - }, - - post(root) { - // complete module loading - root.kiri.load_exec(); + } } -}); +} + +self.kiri = { + load(fn) { + console.log('KIRI LOAD', [...arguments]); + load.push(fn); + } +}; + +self.moto = { }; + +// when dom + scripts complete +document.onreadystatechange = checkReady; + +checkReady(); diff --git a/src/main/mesh.js b/src/main/mesh.js index 128fc4f4..96c077f6 100644 --- a/src/main/mesh.js +++ b/src/main/mesh.js @@ -1,49 +1,50 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import '../add/array.js'; +import '../add/class.js'; +import '../add/three.js'; +import '../mesh/build.js'; -// dep: moto.license -// dep: moto.webui -// dep: moto.client -// dep: moto.broker -// dep: moto.space -// dep: data.index -// dep: mesh.api -// dep: mesh.split -// dep: mesh.edges -// dep: mesh.model -// dep: mesh.build -// dep: load.file -// use: geo.polygons -gapp.main("main.mesh", [], (root) => { +import { api } from '../mesh/api.js'; +import { space } from '../moto/space.js'; +import { broker } from '../moto/broker.js'; +import { $, $d, h, estop } from '../moto/webui.js'; +import { handles } from '../mesh/handles.js'; +import { client as motoClient } from '../moto/client.js'; +import { split as meshSplit } from '../mesh/split.js'; +import { model as meshModel, materials } from '../mesh/model.js'; +import { edges as meshEdges } from '../mesh/edges.js'; +import { open as dataOpen } from '../data/index.js'; +import { load as fileLoad } from '../load/file.js'; +import { THREE } from '../ext/three.js'; -const { Quaternion } = THREE; -const { mesh, moto } = root; -const { broker } = gapp; -const { space } = moto; - -const version = mesh.version = '1.5.6'; +const version = '1.5.7'; const call = broker.send; const dbindex = [ "admin", "space" ]; +const { Quaternion } = THREE; + function log() { - return mesh.api.log.emit(...arguments); + return api.log.emit(...arguments); } // set below. called once the DOM readyState = complete // this is the main() entrypoint called after all dependents load function init() { - let stores = data.open('mesh', { stores: dbindex, version: 4 }).init(), + let stores = dataOpen('mesh', { stores: dbindex, version: 4 }).init(), dark = false, ortho = false, zoomrev = true, zoomspd = 1, platform = space.platform, - db = mesh.db = { + db = api.db = { admin: stores.promise('admin'), space: stores.promise('space') }; + // initialize the API (to avoid circular dependencies) + api.init(); + // mark init time and use count db.admin.put("init", Date.now()); db.admin.get("uses").then(v => db.admin.put("uses", (v||0) + 1)); @@ -78,24 +79,25 @@ function init() { space.view.setZoom(zoomrev, zoomspd); // reload stored space when worker is ready - moto.client.on('ready', restore_space); + motoClient.on('ready', restore_space); // start worker - moto.client.start(`/code/mesh_work?${gapp.version}`); + motoClient.start('/lib/mesh/work.js?' + version); // trigger space event binding - call.space_init({ space, platform }); + call.space_init({ space: space, platform }); // trigger ui building call.ui_build(); + + // hide url params + history.replaceState({}, '', '/mesh'); } // restore space layout and view from previous session async function restore_space() { - const { api } = mesh; - const space = moto.space; - const db_admin = mesh.db.admin; - const db_space = mesh.db.space; + const db_admin = api.db.admin; + const db_space = api.db.space; // let mcache = {}; await db_admin.get("camera") .then(saved => { @@ -126,17 +128,17 @@ async function restore_space() { return { id, md: cached[id] } }) .filter(r => r.md) // filter cache misses - .map(r => new mesh.model(r.md, r.id).applyMeta(mcache[r.id])) + .map(r => new meshModel(r.md, r.id).applyMeta(mcache[r.id])) if (models.length) { log(`restored ${models.length} model(s)`); - mesh.api.group.new(models, id).applyMeta(mcache[id]) + api.group.new(models, id).applyMeta(mcache[id]) } else { log(`removed empty group ${id}`); db_space.remove(id); } } else if (data.type === 'sketch') { claimed.push(id); - mesh.api.add.sketch({ id, ...data }); + api.add.sketch({ id, ...data }); } } for (let id of claimed) { @@ -177,9 +179,12 @@ async function restore_space() { }).finally(() => { // hide loading curtain $d('curtain','none'); + // restore handles visibility + handles.setEnabled(api.prefs.map.space.bounds); if (api.prefs.map.info.welcome !== false) { api.welcome(version); } + broker.publish("app_ready"); }); } @@ -187,9 +192,7 @@ async function restore_space() { function space_init(data) { let platcolor = 0x00ff00; let { space, platform } = data; - let { api } = mesh; let { selection } = api; - // add file drop handler space.event.addHandlers(self, [ 'drop', (evt) => { @@ -228,54 +231,51 @@ function space_init(data) { return api.mode.face(); case 'Digit6': return api.mode.edge(); - case 'KeyQ': - return api.settings(); - case 'KeyI': - return shiftKey ? api.tool.invert() : api.file.import(); - case 'KeyX': - return api.file.export(); - case 'KeyD': - return shiftKey && api.tool.duplicate(); + case 'KeyB': + return selection.boundsBox({toggle:true}); case 'KeyC': return selection.centerXY().focus(); - case 'KeyF': - return shiftKey ? selection.focus() : selection.floor().focus(); - case 'KeyM': - return shiftKey ? api.tool.merge() : api.tool.mirror(); - case 'KeyU': - return shiftKey && api.tool.union(); - case 'KeyA': - if (api.mode.is([ api.modes.edge ])) return mesh.edges.add(); - return shiftKey && api.tool.analyze(); + case 'KeyD': + return shiftKey && api.tool.duplicate(); case 'KeyE': if (api.mode.is([ api.modes.sketch ])) { estop(evt); return api.sketch.extrude(); } return; - case 'KeyV': - return shiftKey ? selection.show() : selection.focus(); - case 'KeyN': - return shiftKey ? estop(evt, api.tool.rename()) : api.normals(); - case 'KeyW': - return api.wireframe(); + case 'KeyF': + return shiftKey ? selection.focus() : selection.floor().focus(); case 'KeyG': return shiftKey ? (api.mode.is([ api.modes.sketch ]) ? api.sketch.arrange.group() : api.tool.regroup()) : api.grid(); - case 'KeyL': - return api.log.toggle({ spinner: false }); - case 'KeyS': - if (!api.mode.is([ api.modes.object ])) return; - return shiftKey ? selection.visible({toggle:true}) : mesh.split.start(); - case 'KeyB': - return selection.boundsBox({toggle:true}); case 'KeyH': return shiftKey ? selection.hide() : space.view.home(); + case 'KeyI': + return api.file.import(); + case 'KeyL': + return api.log.toggle({ spinner: false }); + case 'KeyM': + return shiftKey ? api.tool.merge() : api.tool.mirror(); + case 'KeyN': + return shiftKey ? api.tool.invert() : api.normals(); + case 'KeyQ': + return api.settings(); + case 'KeyR': + return estop(evt, api.tool.rename()); + case 'KeyS': + if (!api.mode.is([ api.modes.object ])) return; + return shiftKey ? selection.visible({toggle:true}) : meshSplit.start(); case 'KeyT': return shiftKey ? api.tool.triangulate() : space.view.top(); - case 'KeyZ': - return space.view.reset(); + case 'KeyU': + return shiftKey && api.tool.union(); + case 'KeyV': + return shiftKey ? selection.show() : selection.focus(); + case 'KeyW': + return shiftKey ? api.wireframe() : api.wireedges(); + case 'KeyX': + return api.file.export(); } }, 'keydown', evt => { @@ -295,18 +295,35 @@ function space_init(data) { let rot, floor = api.prefs.map.space.floor !== false; switch (code) { case 'KeyA': + estop(evt); + if (api.mode.is([ api.modes.edge ])) { + return meshEdges.add(); + } if (metaKey || ctrlKey) { if (!selection.sketch()?.selection.all()) { api.mode.object(); selection.set(api.group.list()); } - estop(evt); + } else if (shiftKey) { + api.tool.analyze(); } break; + case 'KeyY': + estop(evt); + if (metaKey || ctrlKey) { + return shiftKey ? api.history.undo() : api.history.redo(); + } + case 'KeyZ': + estop(evt); + if (metaKey || ctrlKey) { + return shiftKey ? api.history.redo() : api.history.undo(); + } else { + return space.view.reset(); + } case 'Escape': if (selection.clear()) { - mesh.edges.clear(); - mesh.split.end(); + meshEdges.clear(); + meshSplit.end(); } estop(evt); break; @@ -360,11 +377,7 @@ function space_init(data) { let obj = int?.object; if (obj) api.selection.drag({ start: int.object }); if (obj) obj.sketch_item?.sketch.drag({ start: int.object }); - if (event?.shiftKey) { - return api.objects(); - } else { - return undefined; - } + return selection.count() && !event.altKey ? api.objects(true) : undefined; }); // called two ways: @@ -380,8 +393,8 @@ function space_init(data) { // a selection was made const { model, sketch, sketch_item } = int?.object || {}; const { altKey, ctrlKey, metaKey, shiftKey } = event; - if (mesh.split.active()) { - return mesh.split.select(model); + if (meshSplit.active()) { + return meshSplit.select(model); } if (model) { const group = model.group; @@ -392,7 +405,7 @@ function space_init(data) { // rotate normal using group's matrix const normal = shiftKey ? int.face.normal.applyQuaternion(q) : undefined; // y,z swap due to world rotation for orbit controls - api.focus({center: { x, y:-z, z:y }, normal}); + api.focus({ center: { x, y:-z, z:y }, normal }); let one = api.sketch.selected.one; if (one && confirm('attach sketch to face?')) { one.center = { @@ -405,7 +418,7 @@ function space_init(data) { }; one.render(); } - } else if (ctrlKey) { + } else if (ctrlKey && selection.contains(model)) { // rotate selected face towawrd z "floor" group.rotateTowardZ(int.face.normal); selection.update(); @@ -431,7 +444,7 @@ function space_init(data) { opt); break; case modes.edge: - mesh.edges.select(); + meshEdges.select(); break; } } @@ -450,6 +463,8 @@ function space_init(data) { } } else if (sketch_item) { sketch_item.toggle(); + } else { + console.log('unknown selection'); } } else { // return objects upSelect can choose from @@ -466,7 +481,7 @@ function space_init(data) { space.mouse.onDrag((delta, offset, up = false) => { let { mode, modes } = api; if (delta) { - if (delta.event?.shiftKey && selection.count()) { + if (selection.count()) { selection.drag({ delta, offset }); } } else if (up) { @@ -482,7 +497,6 @@ function space_init(data) { function load_files(files) { log(`loading file...`); - let api = mesh.api; let has_image = false; let has_svg = false; let has_gbr = false; @@ -493,7 +507,7 @@ function load_files(files) { has_gbr = has_gbr || file.name.toLowerCase().endsWith(".gbr") > 0; } if (sketch && has_gbr) { - load.File.load([...files], { flat: true }).then(layers => { + fileLoad([...files], { flat: true }).then(layers => { for (let layer of layers.flat()) { let { circs, closed, open, rects } = layer; open = open.map(poly => { @@ -501,7 +515,7 @@ function load_files(files) { return diam ? poly.offset_open(diam / 2, 'round') : null; }).filter(p => p).flat(); for (let set of [ closed, open, circs, rects ]) { - let group = mesh.util.uuid(); + let group = api.util.uuid(); for (let poly of set) { sketch.add.polygon({ poly, group }); } @@ -510,13 +524,13 @@ function load_files(files) { }); } else if (sketch && has_svg) { - load.File.load([...files], { flat: true }) + fileLoad([...files], { flat: true }) .then(polys => polys.forEach(set => { - let group = mesh.util.uuid(); + let group = api.util.uuid(); set.forEach(poly => sketch.add.polygon({ poly, group })) })) .catch(error => dbug.error(error)) - .finally(() => mesh.api.log.hide()); + .finally(() => api.log.hide()); } else if (has_svg) { api.modal.dialog({ @@ -588,16 +602,16 @@ function load_files(files) { } function load_files_opt(files, opt) { - return load.File.load([...files], opt) + return fileLoad([...files], opt) .then(data => call.space_load(data)) .catch(error => log(error).pin({}) && dbug.error(error)) - // .finally(() => mesh.api.log.hide()); + // .finally(() => api.log.hide()); } // add object loader function space_load(data) { if (data && data.length && (data = data.flat()).length) - mesh.api.group.new(data.map(el => new mesh.model(el))) + api.group.new(data.map(el => new meshModel(el))) .promote() .focus(); } @@ -615,7 +629,7 @@ function key_once_cancel(code) { } function store_meta() { - mesh.db.admin.put("meta", metaCache); + api.db.admin.put("meta", metaCache); } function update_meta(id, data) { @@ -643,16 +657,16 @@ function object_destroy(id) { // listen for changes like dark mode toggle function set_darkmode(dark) { - let { prefs, model } = mesh.api; - let { sky, platform } = moto.space; + let { prefs, model } = api; + let { sky, platform } = space; prefs.map.space.dark = dark; if (dark) { - mesh.material.wireframe.color.set(0xaaaaaa); - mesh.material.wireline.color.set(0xaaaaaa); + materials.wireframe.color.set(0xaaaaaa); + materials.wireline.color.set(0xaaaaaa); $('app').classList.add('dark'); } else { - mesh.material.wireframe.color.set(0,0,0); - mesh.material.wireline.color.set(0,0,0); + materials.wireframe.color.set(0,0,0); + materials.wireline.color.set(0,0,0); $('app').classList.remove('dark'); } sky.set({ @@ -669,7 +683,7 @@ function set_darkmode(dark) { colorMinor: 0xeeeeee, }, }); - mesh.api.updateFog(); + api.updateFog(); platform.setSize(); for (let m of model.list()) { m.normals({ refresh: true }); @@ -678,13 +692,19 @@ function set_darkmode(dark) { } function set_normals_length(length) { - let { prefs } = mesh.api; + let { prefs, model } = api; prefs.map.normals.length = length || 1; prefs.save(); + // Update existing normals + for (let m of model.list()) { + if (m.normals()) { + m.normals({ refresh: true }); + } + } } function set_normals_color(color) { - let { prefs } = mesh.api; + let { prefs, model } = api; let { map } = prefs; if (map.space.dark) { map.normals.color_dark = color || 0; @@ -692,34 +712,47 @@ function set_normals_color(color) { map.normals.color_lite = color || 0; } prefs.save(); + // Update existing normals + for (let m of model.list()) { + if (m.normals()) { + m.normals({ refresh: true }); + } + } } function set_surface_radians(radians) { - let { prefs } = mesh.api; + let { prefs } = api; prefs.map.surface.radians = parseFloat(radians || 0.1); prefs.save(); } function set_surface_radius(radius) { - let { prefs } = mesh.api; + let { prefs } = api; prefs.map.surface.radius = parseFloat(radius || 0.2); prefs.save(); } function set_wireframe_opacity(opacity) { - let { prefs } = mesh.api; + let { prefs, model } = api; prefs.map.wireframe.opacity = parseFloat(opacity || 0.15); prefs.save(); + // Update existing wireframes + for (let m of model.list()) { + if (m.wireframe().enabled) { + m.opacity({temp: prefs.map.wireframe.opacity}); + } + } } function set_wireframe_fog(fogx) { - let { prefs } = mesh.api; + let { prefs } = api; prefs.map.wireframe.fog = parseFloat(fogx || 3); prefs.save(); + api.updateFog(); } function set_snap_value(snap) { - let { prefs } = mesh.api; + let { prefs } = api; prefs.map.space.snap = parseFloat(snap || 1); prefs.save(); } @@ -744,14 +777,30 @@ broker.listeners({ set_snap_value }); -// remove version cache bust from url -window.history.replaceState({},'','/mesh/'); +init(); -// setup init() trigger when dom + scripts complete -document.onreadystatechange = function() { - if (document.readyState === 'complete') { - init(); - } -} - -}); +export { + version, + init, + log, + restore_space, + space_init, + load_files, + load_files_opt, + space_load, + key_once, + key_once_cancel, + store_meta, + update_meta, + object_visible, + object_meta, + object_destroy, + set_darkmode, + set_normals_length, + set_normals_color, + set_surface_radians, + set_surface_radius, + set_wireframe_opacity, + set_wireframe_fog, + set_snap_value +}; diff --git a/src/main/service.js b/src/main/service.js index a34d1595..6d8af693 100644 --- a/src/main/service.js +++ b/src/main/service.js @@ -1,7 +1,8 @@ const service_worker = navigator.serviceWorker; +import { version } from '../moto/license.js'; + async function start_service_worker() { - const version = self.gapp.version; function debug() { console.log(`[${version}]`, ...arguments); } diff --git a/src/mesh/api.js b/src/mesh/api.js index bf67a9dc..75bc7381 100644 --- a/src/mesh/api.js +++ b/src/mesh/api.js @@ -1,27 +1,22 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -// dep: moto.license -// dep: moto.client -// dep: moto.broker -// dep: moto.space -// dep: mesh.util -// dep: mesh.edges -// dep: mesh.group -// dep: mesh.model -// dep: mesh.sketch -// dep: add.three -// use: add.array -gapp.register("mesh.api", [], (root, exports) => { +import { broker } from '../moto/broker.js'; +import { history } from './history.js'; +import { client as motoClient } from '../moto/client.js'; +import { space as motoSpace } from '../moto/space.js'; +import { base } from '../geo/base.js'; +import { handles } from './handles.js'; +import { util as meshUtil } from './util.js'; +import { edges as meshEdges } from './edges.js'; +import { group as meshGroup } from './group.js'; +import { model as meshModel } from './model.js'; +import { sketch as meshSketch } from './sketch.js'; +import { newPolygon } from '../geo/polygon.js'; +import { THREE } from '../ext/three.js'; +import { $, $C, h } from '../moto/webui.js'; const { Matrix4, Vector3 } = THREE; -const { base, mesh, moto } = root; -const { space } = moto; -const { util } = mesh; -const { newPolygon } = base; - -const worker = moto.client.fn +const worker = motoClient.fn; const groups = []; const sketches = []; @@ -41,8 +36,8 @@ const selection = { // return selected groups + groups from selected models groups(strict = true) { let all = selection.list(strict); - let grp = all.filter(s => s instanceof mesh.group); - let mdl = all.filter(s => s instanceof mesh.model); + let grp = all.filter(s => s instanceof meshGroup); + let mdl = all.filter(s => s instanceof meshModel); for (let m of mdl) { grp.addOnce(m.group); } @@ -52,8 +47,8 @@ const selection = { // return selected models + models from selected groups models(strict = true) { let all = selection.list(strict); - let grp = all.filter(s => s instanceof mesh.group); - let mdl = all.filter(s => s instanceof mesh.model); + let grp = all.filter(s => s instanceof meshGroup); + let mdl = all.filter(s => s instanceof meshModel); for (let g of grp) { for (let m of g.models) { mdl.addOnce(m); @@ -83,7 +78,7 @@ const selection = { // flatten groups into model lists when present selected = objects.map(o => o.models ? o.models : o).flat(); tools = toolist || []; - util.defer(selection.update); + meshUtil.defer(selection.update); }, // @param group {MeshObject} @@ -94,7 +89,7 @@ const selection = { mode.sketch(); tools.length = 0; selected.length = 0; - mesh.edges?.end(); + meshEdges?.end(); break; case 'group': case 'model': @@ -115,7 +110,7 @@ const selection = { tools.addOnce(object); } } - util.defer(selection.update); + meshUtil.defer(selection.update); }, // @param group {MeshObject} @@ -129,7 +124,7 @@ const selection = { selected.remove(object); tools.remove(object); } - util.defer(selection.update); + meshUtil.defer(selection.update); }, // remove all @@ -140,7 +135,6 @@ const selection = { for (let s of selection.list()) { selection.remove(s); tools.remove(s); - s.showBounds(false); s.remove(); } }, @@ -180,7 +174,7 @@ const selection = { for (let m of selection.models()) { m.visible(...arguments); } - util.defer(selection.update); + meshUtil.defer(selection.update); }, // update selection and wireframe for all objects @@ -190,15 +184,16 @@ const selection = { group.select(false); group.normals(prefs.map.space.norm || false); group.wireframe(prefs.map.space.wire || false, { - opacity: prefs.map.wireframe.opacity} - ); + opacity: prefs.map.wireframe.opacity, + edges: prefs.map.space.wire_edge + }); } for (let sketch of sketches) { sketch.select(false); } api.updateFog(); // prevent selection of model and its group - let mgsel = selected.filter(s => s instanceof mesh.model).map(m => m.group); + let mgsel = selected.filter(s => s instanceof meshModel).map(m => m.group); selected = selected.filter(sel => !mgsel.contains(sel)).filter(v => v); // highlight selected for (let object of selected) { @@ -208,24 +203,33 @@ const selection = { prefs.save( prefs.map.space.select = selected.map(s => s.id) ); prefs.save( prefs.map.space.tools = tools.map(s => s.id) ); // force repaint in case of idle - space.update(); + motoSpace.update(); return selection; }, update_defer() { - util.defer(selection.update); + meshUtil.defer(selection.update); return selection; }, drag(opt = {}) { + if (opt.start) { + broker.publish('drag_start', selected); + } else if (opt.end) { + broker.publish('drag_end', selected); + } else { + broker.publish('drag', opt); + } selected.forEach(s => s.drag(opt)); return selection; }, move(dx = 0, dy = 0, dz = 0) { - for (let s of [...selection.groups(), ...selection.sketches()]) { + let set = [...selection.groups(), ...selection.sketches()]; + for (let s of set) { s.move(dx, dy, dz); } + broker.publish('move', { set, dx, dy, dz }); return selection; }, @@ -233,6 +237,7 @@ const selection = { for (let s of selection.models()) { s.move(dx, dy, dz); } + broker.publish('move', { set: selection.models(), dx, dy, dz }); return selection; }, @@ -240,6 +245,7 @@ const selection = { for (let s of selection.groups()) { s.rotate(dx, dy, dz); } + broker.publish('rotate', { set: selection.models(), dx, dy, dz }); return selection; }, @@ -247,14 +253,15 @@ const selection = { for (let s of selection.groups()) { s.qrotate(q); } + broker.publish('qrotate', { set: selection.groups(), q }); return selection; }, scale(dx = 0, dy = 0, dz = 0) { for (let s of selection.groups()) { - let { x, y, z } = s.scale(); - s.scale(x * dx, y * dy, z * dz); + s.scale(dx, dy, dz); } + broker.publish('scale', { set: selection.groups(), dx, dy, dz }); return selection; }, @@ -275,9 +282,7 @@ const selection = { }, boundsBox() { - for (let m of selection.groups()) { - m.showBounds(...arguments); - } + handles.toggleBounds(); return selection; }, @@ -286,11 +291,11 @@ const selection = { }, focus() { - api.focus(selection.list()); + selection.count() && api.focus(selection.list()); }, bounds() { - return util.bounds(selected.map(s => s.object)); + return meshUtil.bounds(selected.map(s => s.object)); }, show() { @@ -317,7 +322,7 @@ const pattern = { let models = selection.models(); let [ cx, cy ] = center; for (let model of models) { - let pos = model.world_bounds.mid; + let pos = model.bounds.mid; let stepr = (Math.PI * 2) / count; let modnu = [ model ]; for (let i=1; i parseFloat(v)).toFloat32(); - const nmdl = new mesh.model({ file: "input", mesh: vert }); - const ngrp = group.new([ nmdl ]); + const nmdl = new meshModel({ file: "input", mesh: vert }); + const ngrp = api.group.new([ nmdl ]); api.modal.hide(); } }) ]) ] @@ -768,8 +773,8 @@ let add = { cube() { const box = new THREE.BoxGeometry(1,1,1).toNonIndexed(); const vert = box.attributes.position.array; - const nmdl = new mesh.model({ file: "box", mesh: vert }); - const ngrp = group.new([ nmdl ]); + const nmdl = new meshModel({ file: "box", mesh: vert }); + const ngrp = api.group.new([ nmdl ]); ngrp.scale(10, 10, 10).floor(); selection.set([ nmdl ]); return ngrp; @@ -788,8 +793,8 @@ let add = { cyl.addInner(newPolygon().centerCircle(center, bore/2, facets)); } const vert = cyl.extrude(height, { chamfer }).toFloat32(); - const nmdl = new mesh.model({ file: "cylinder", mesh: vert }); - const ngrp = group.new([ nmdl ]); + const nmdl = new meshModel({ file: "cylinder", mesh: vert }); + const ngrp = api.group.new([ nmdl ]); selection.set([ nmdl ]); ngrp.floor(); } @@ -838,7 +843,7 @@ let add = { height: parseFloat(_height.value), chamfer: parseFloat(_chamfer.value), }).then(gear => { - const nmdl = new mesh.model({ file: "gear", mesh: gear.toFloat32() }); + const nmdl = new meshModel({ file: "gear", mesh: gear.toFloat32() }); const ngrp = group.new([ nmdl ]); selection.set([ nmdl ]); ngrp.floor(); @@ -889,7 +894,7 @@ let add = { steps: parseFloat(_steps.value), taper: _taper.checked ? true : false, }).then(verts => { - const nmdl = new mesh.model({ file: "threads", mesh: verts.toFloat32() }); + const nmdl = new meshModel({ file: "threads", mesh: verts.toFloat32() }); const ngrp = group.new([ nmdl ]); selection.set([ nmdl ]); ngrp.floor(); @@ -946,7 +951,7 @@ let file = { recs, format: ext }).then(data => { if (data.length) { - util.download(data, file); + meshUtil.download(data, file); } }).finally( api.modal.hide ); } @@ -983,7 +988,7 @@ const tool = { return { id: m.id, matrix: m.matrix } })) .then(data => { - let group = api.group.new([new mesh.model({ + let group = api.group.new([new meshModel({ file: `merged`, mesh: data })]).promote(); @@ -1002,7 +1007,7 @@ const tool = { worker .model_union(models.map(m => m.id)) .then(data => { - let group = api.group.new([new mesh.model({ + let group = api.group.new([new meshModel({ file: `union`, mesh: data })]).promote(); @@ -1025,7 +1030,7 @@ const tool = { log(`diff ${models.length} models`).pin(); worker.model_difference(models.map(m => m.id)) .then(data => { - let group = api.group.new([new mesh.model({ + let group = api.group.new([new meshModel({ file: `diff`, mesh: data })]).promote(); @@ -1048,7 +1053,7 @@ const tool = { log(`intersect ${models.length} models`).pin(); worker.model_intersect(models.map(m => m.id)) .then(data => { - let group = api.group.new([new mesh.model({ + let group = api.group.new([new meshModel({ file: `intersect`, mesh: data })]).promote(); @@ -1073,7 +1078,7 @@ const tool = { return { id: m.id, tool: m.tool() } })) .then(data => { - let group = api.group.new([new mesh.model({ + let group = api.group.new([new meshModel({ file: `subtract`, mesh: data })]).promote(); @@ -1123,7 +1128,7 @@ const tool = { }, toSketch() { - for (let m of selection.models()) { + for (let m of api.model.list()) { m.selectionToSketch(); } }, @@ -1143,7 +1148,7 @@ const tool = { return; } model.rename( el.value.trim() ); - util.defer(selection.update); + meshUtil.defer(selection.update); api.modal.hide(); }; let { tempedit } = api.modal.show(`rename ${type}`, h.div({ class: "rename"}, [ @@ -1158,7 +1163,7 @@ const tool = { models = fallback(models, true); if (models.length) { log(`regrouping ${models.length} model(s)`); - let group = mesh.api.group.new(models.map(m => m.ungroup())); + let group = api.group.new(models.map(m => m.ungroup())); let bounds = group.bounds; models.forEach(m => m.centerTo(bounds.mid)); return group; @@ -1173,12 +1178,12 @@ const tool = { for (let m of models) { let p = worker.model_analyze({ id: m.id, opt }).then(data => { let { areas, edges } = data.mapped; - let nm = areas.map(area => new mesh.model({ + let nm = areas.map(area => new meshModel({ file: (area.length/3).toString(), mesh: area.toFloat32() })).map( nm => nm.applyMatrix4(mcore.clone().multiply(m.mesh.matrixWorld)) ); if (nm.length) { - mesh.api.group.new(nm, undefined, "patch"); + api.group.new(nm, undefined, "patch"); } }); promises.push(p); @@ -1196,11 +1201,11 @@ const tool = { let mark = Date.now(); for (let m of models) { let p = worker.model_isolate({ id: m.id }).then(bodies => { - bodies = bodies.map(vert => new mesh.model({ + bodies = bodies.map(vert => new meshModel({ file: m.file, mesh: vert.toFloat32() })).map( nm => nm.applyMatrix4(mcore.clone().multiply(m.mesh.matrixWorld)) ); - mesh.api.group.new(bodies, undefined, "isolate"); + api.group.new(bodies, undefined, "isolate"); }); promises.push(p); } @@ -1249,7 +1254,7 @@ const tool = { log('repairing mesh(es)').pin(); tool.heal(models, { merge: true }).then(() => { log('repair commplete').unpin(); - util.defer(selection.update); + meshUtil.defer(selection.update); }); }, @@ -1268,7 +1273,7 @@ const tool = { log(`${vertices.length/3} vertices, ${zlist.length} unique Z`); } log('cleaning complete').unpin(); - util.defer(selection.update); + meshUtil.defer(selection.update); }); }, @@ -1296,7 +1301,7 @@ const mode = { $(`mode-${mode}`).classList.add('selected'); $('mode-label').innerText = mode; api.mode.check(); - mesh.edges?.end(); + meshEdges?.end(); if (mode === 'sketch') { $C('sketch-on', null, 'hide'); $C('sketch-off', 'hide', null); @@ -1356,7 +1361,7 @@ const mode = { selection.clear(); } mode.set(modes.edge); - mesh.edges.start(); + meshEdges.start(); }, sketch() { @@ -1425,22 +1430,65 @@ const prefs = { // persist to data store save() { if (prefs.changed()) { - mesh.db.admin.put("prefs", prefs.map); + api.db.admin.put("prefs", prefs.map); prefsig = prefsignew; } }, // reload from data store load() { - return mesh.db.admin.get("prefs").then(data => { + return api.db.admin.get("prefs").then(data => { // handle/ignore incompatible older prefs if (data) Object.assign(prefs.map, data); }); } }; +let mark = Date.now(); + +let since = () => { + return ((Date.now() - mark) / 1000).toFixed(2).padStart(9,0); +}; + +let dbug = self.dbug = self.dbug || { + level: 1, // 0=debug, 1=normal, 2=warn, 3=error + + set: (level) => { + if (level >= 0) dbug.level = Math.min(level, 3); + }, + + debug: function() { + if (dbug.level <= 0) return console.log(since(),'[DBG]', ...arguments); + }, + + log: function() { + if (dbug.level <= 1) return console.log(since(),'|',...arguments); + }, + + warn: function() { + if (dbug.level <= 2) return console.log(since(),'[WRN]', ...arguments); + }, + + error: function() { + if (dbug.level <= 3) return console.trace(since(),'[ERR]', ...arguments); + }, + + since +}; + + // api is augmented in mesh.build (log, modal, download) -const api = exports({ +const api = { + init() { + // publish messages when api functions are called + broker.wrapObject(selection, 'selection'); + // broker.wrapObject(model, 'model'); + // broker.wrapObject(group, 'group'); + + // optimize db writes by merging updates + prefs.save = meshUtil.deferWrap(prefs.save, 100); + }, + help() { window.open("https://docs.grid.space/projects/mesh-tool"); }, @@ -1471,9 +1519,18 @@ const api = exports({ } }, - // @param object {THREE.Object3D | THREE.Object3D[] | Point} + history: { + undo() { + history.undo(); + }, + redo() { + history.redo(); + } + }, + + // @param object {MeshObject | MeshObject[] | Object} focus(object) { - let { center } = object.center ? object : util.bounds(object); + let { center } = object.center ? object : meshUtil.bounds(object); // when no valid objects supplied, set origin if (isNaN(center.x * center.y * center.z)) { center = { x: 0, y: 0, z: 0 }; @@ -1487,13 +1544,13 @@ const api = exports({ if (x < 0) left = -left; } // sets "home" views (front, back, home, reset) - space.platform.setCenter(center.x, -center.y, center.z); + motoSpace.platform.setCenter(center.x, -center.y, center.z); // sets camera focus - space.view.panTo(center.x, center.z, -center.y, left, up); + motoSpace.view.panTo(center.x, center.z, -center.y, left, up); }, grid(state = { toggle:true }) { - let { platform } = space; + let { platform } = motoSpace; let { map, save } = prefs; if (state.toggle) { platform.showGrid(!platform.isGridVisible()); @@ -1504,9 +1561,19 @@ const api = exports({ save(); }, - wireframe(state = { toggle:true }, opt = { opacity: prefs.map.wireframe.opacity }) { + wireedges(state = { toggle:true }, opt = { + opacity: prefs.map.wireframe.opacity, + edges: base.util.toDegrees(api.prefs.map.surface.radians) + }) { + api.wireframe(state, opt); + }, + + wireframe(state = { toggle:true }, opt = { + opacity: prefs.map.wireframe.opacity, + edges: prefs.map.space.wire_edge + }) { const mspace = prefs.map.space; - let wire = mspace.wire; + let { wire } = mspace; if (state.toggle) { wire = !wire; } else { @@ -1515,7 +1582,9 @@ const api = exports({ for (let m of model.list()) { m.wireframe(wire, opt); } - prefs.save( mspace.wire = wire ); + mspace.wire = wire; + mspace.wire_edge = wire ? opt.edges : 0; + prefs.save(); api.updateFog(); }, @@ -1523,9 +1592,9 @@ const api = exports({ const mspace = prefs.map.space; const mwire = prefs.map.wireframe; if (mspace.wire) { - space.scene.setFog(mwire.fog, mspace.dark ? 0 : 0xffffff); + motoSpace.scene.setFog(mwire.fog, mspace.dark ? 0 : 0xffffff); } else { - space.scene.setFog(false); + motoSpace.scene.setFog(false); } }, @@ -1542,52 +1611,59 @@ const api = exports({ prefs.save( prefs.map.space.norm = norm ); }, + objects(selected = false) { + // return model objects suitable for finding ray intersections + if (selected) { + return [ + ...selection.models().map(o => o.mesh), + ...selection.sketches().map(s => s.meshes).flat() + ]; + } else { + return [ + ...group.list().map(o => o.models).flat().map(o => o.mesh), + ...sketches.map(s => s.meshes).flat() + ]; + } + }, + + add, + + dbug, + + file, + + group, + mode, modes, - selection, + model, pattern, - group, + prefs, - model, + selection, sketch, - add, - - file, - tool, - prefs, - - objects() { - // return model objects suitable for finding ray intersections - return [ - ...group.list().map(o => o.models).flat().map(o => o.mesh), - ...sketches.map(s => s.meshes).flat() - ]; - }, - isDebug: self.debug === true -}); +}; function log() { return api.log.emit(...arguments); } -const { broker } = gapp; const call = broker.send; -// publish messages when api functions are called -broker.wrapObject(selection, 'selection'); -broker.wrapObject(model, 'model'); -broker.wrapObject(group, 'group'); +self.mesh_api = api; -// optimize db writes by merging updates -prefs.save = util.deferWrap(prefs.save, 100); - -}); +export { + sketches, + selection, + groups, + api +}; diff --git a/src/mesh/build.js b/src/mesh/build.js index 7be0ae46..d4ea9918 100644 --- a/src/mesh/build.js +++ b/src/mesh/build.js @@ -1,22 +1,21 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { broker } from '../moto/broker.js'; +import { $, h, estop } from '../moto/webui.js'; +import { api } from './api.js'; +import { util } from './util.js'; +import { space } from '../moto/space.js'; +import { split } from './split.js'; +import { newPolygon } from '../geo/polygon.js'; +import { version } from '../moto/license.js'; -// dep: moto.broker -// dep: mesh.util -// dep: mesh.api -gapp.register("mesh.build", [], (root, exports) => { - -const { broker } = gapp; -const { base, mesh, moto } = root; -const { api, util } = mesh; -const { space } = moto; -const { bind, div, label, input, hr } = h; - -const devel = api.isDebug; +const { div, label, input, hr } = h; +const { dbug } = api; +const isDebug = api.isDebug; let call = broker.send; let rad = 180 / Math.PI; +let deg = Math.PI / 180; let und = undefined; broker.listeners({ @@ -38,7 +37,7 @@ const modal = api.modal = { if (this.info.showing) { throw `modal conflict showing "${title}"`; } - let bound = bind($('modal'), contents); + let bound = h.bind($('modal'), contents); $('modal_title_text').innerText = title; $('modal_page').style.display = 'flex'; $('modal_frame').style.display = 'flex'; @@ -101,7 +100,7 @@ const modal = api.modal = { let { title, body } = opt; let contents = []; if (typeof body === 'string') { - contents.push(div(body)) + contents.push(h.div(body)) } else if (Array.isArray(body)) { contents.appendAll(body); } else { @@ -137,7 +136,7 @@ const log = api.log = { text: `${dbug.since()} | ${[...arguments].join(' ')}`, time: now }); - if (api.isDebug) { + if (isDebug) { console.log(...data.peek().objs); } while (!log.pinned && data.length && (data.length > lines || now - data[0].time > age)) { @@ -184,14 +183,14 @@ const log = api.log = { // re-render and show current log messages render() { let area = $('logtext'); - bind(area, log.data.map(rec => div({ _: rec.text }))); + h.bind(area, log.data.map(rec => h.div({ _: rec.text }))); area.scrollTop = area.scrollHeight; return log.show(); }, }; // bind endpoint for worker to log in the ui -gapp.broker.subscribe("mesh.log", msg => { +broker.subscribe("mesh.log", msg => { log.emit(msg); }); @@ -199,14 +198,14 @@ gapp.broker.subscribe("mesh.log", msg => { api.welcome = function(version = "unknown") { const { prefs } = api; const { map } = prefs; - modal.show('About Mesh:Tool', div({ class:"welcome" }, [ + modal.show('About Mesh:Tool', h.div({ class:"welcome" }, [ h.a({ target: "site", _: "Grid.Space", href: "https://grid.space/" }), - div(`Version: ${version}`), - hr({ width: "100%" }), + h.div(`Version: ${version}`), + h.hr({ width: "100%" }), h.a({ target: "docs", _: "Guide & Documentation", @@ -222,9 +221,9 @@ api.welcome = function(version = "unknown") { _: "Discord Server", href: "https://discord.gg/suyCCgr" }), - hr({ width: "100%" }), - div({ class: "choice" }, [ - input({ + h.hr({ width: "100%" }), + h.div({ class: "choice" }, [ + h.input({ type: "checkbox", [ map.info.welcome !== false ? 'checked' : 'unchecked' ] : 1, onchange: (ev) => { @@ -232,14 +231,13 @@ api.welcome = function(version = "unknown") { prefs.save(); } }), - div("show at startup") + h.div("show at startup") ]), ])); }; // bind settings endpoint to api api.settings = function() { - const { util } = mesh; const { prefs } = api; const { surface, normals, space, sketch, wireframe } = prefs.map; const { dark } = space; @@ -352,7 +350,7 @@ api.settings = function() { modal.show('settings', div({ class: "settings" }, [ set1, set2, set3, set4, set5, set6, set7 ] )); -} +}; // bootstrap icons function bicon(name) { @@ -373,18 +371,19 @@ function editable() { function menu_item(text, fn, short, id) { if (short) { short = Array.isArray(short) ? short : [ short ]; - short = div({ class: "short" }, short.map(s => s.startsWith('bi-') ? - bicon(s) : div(s) + short = h.div({ class: "short" }, short.map(s => s.startsWith('bi-') ? + bicon(s) : h.div(s) )); } - return div({ id, onclick: fn }, [ - div({ _: text, class: "grow" }), + return h.div({ id, onclick: fn }, [ + h.div({ _: text, class: "grow" }), short ].filter(o => o)); } // create html elements function ui_build() { + let { bind, div, input, hr, label } = h; let { file, selection, mode, tool, sketch, prefs, add } = api; let trash = FontAwesome.icon({ prefix: "fas", iconName: "trash" }).html[0]; let eye_open = FontAwesome.icon({ prefix: "fas", iconName: "eye" }).html[0]; @@ -410,6 +409,9 @@ function ui_build() { div({ class: "menu sketch-on" }, [ div('Edit'), div({ class: "menu-items" }, [ + menu_item('Undo', api.history.undo, ['bi-command','Z']), + menu_item('Redo', api.history.redo, ['bi-command','Y']), + hr(), menu_item('Add Circle', api.add.circle), menu_item('Add Rectangle', api.add.rectangle), hr(), @@ -421,13 +423,16 @@ function ui_build() { div({ class: "menu sketch-off" }, [ div('Edit'), div({ class: "menu-items" }, [ + menu_item('Undo', api.history.undo, ['bi-command','Z']), + menu_item('Redo', api.history.redo, ['bi-command','Y']), + hr(), menu_item('Add Cylinder', add.cylinder), menu_item('Add Cube', add.cube), menu_item('Add Gear', add.gear), menu_item('Add Threads', add.threads), hr(), menu_item('Add Sketch', add.sketch), - devel ? menu_item('Add Vertices', add.input) : undefined, + isDebug ? menu_item('Add Vertices', add.input) : undefined, hr(), menu_item('Delete', api.selection.delete, 'bi-backspace'), ]) @@ -437,7 +442,9 @@ function ui_build() { div({ class: "menu-items" }, [ menu_item('Bounds', () => { selection.boundsBox({ toggle: true }) }, 'B'), menu_item('Normals', () => { api.normals() }, 'N'), - menu_item('Wireframes', () => { api.wireframe() }, 'W'), + hr(), + menu_item('WireEdges', () => { api.wireedges() }, 'W'), + menu_item('WireFaces', () => { api.wireframe() }, [ 'bi-shift', 'W' ]), hr(), menu_item('Gridlines', () => { api.grid() }, 'G'), menu_item('Messages', () => { api.log.toggle({ spinner: false }) }, 'L'), @@ -462,7 +469,7 @@ function ui_build() { div({ id: "mode-label" }) ]), div({ class: "menu sketch-on" }, [ - div('Object'), + div('Items'), div({ class: "menu-items" }, [ menu_item('Group', sketch.arrange.group), menu_item('Ungroup', sketch.arrange.ungroup), @@ -480,7 +487,7 @@ function ui_build() { ]) ]), div({ class: "menu sketch-off" }, [ - div('Object'), + div('Objects'), div({ class: "menu-items" }, [ menu_item('Regroup', tool.regroup), menu_item('Isolate', tool.isolate), @@ -488,7 +495,7 @@ function ui_build() { menu_item('Duplicate', tool.duplicate, ['bi-shift','D']), menu_item('Mirror', tool.mirror, 'M'), menu_item('Merge', tool.merge), - menu_item('Split', mesh.split.start, 'S'), + menu_item('Split', split.start, 'S'), hr(), menu_item('Union', tool.union), menu_item('Subtract', tool.subtract), @@ -505,13 +512,14 @@ function ui_build() { menu_item('Flip Normals', tool.invert, ['bi-shift','I']), menu_item('Triangulate', tool.triangulate, ['bi-shift','T']), menu_item('To Sketch', tool.toSketch), + hr(), menu_item('Analyze', tool.analyze), menu_item('Flatten', tool.flatten), menu_item('Clean', tool.clean), ]) ]), div({ class: "menu sketch-on" }, [ - div('Shape'), + div('Shapes'), div({ class: "menu-items" }, [ menu_item('Duplicate', tool.duplicate, ['bi-shift','D']), menu_item('Extrude', () => sketch.extrude(), ['bi-shift','E']), @@ -528,7 +536,7 @@ function ui_build() { div({ class: "menu" }, [ div('Help'), div({ class: "menu-items" }, [ - menu_item('About', () => { api.welcome(mesh.version) }), + menu_item('About', () => { api.welcome(version) }), hr(), menu_item('Documentation', api.help), menu_item('Report Bugs', api.bugs), @@ -761,23 +769,14 @@ function ui_build() { function field_edit(title, set, opt = {}) { return function(ev) { let floor = api.prefs.map.space.floor !== false; - let value = ev.target.innerText; + let value = parseFloat(ev.target.innerText); let onclick = (ev) => { let tempval = parseFloat($('tempval').value); api.modal.hide(modal.info.cancelled); if (modal.info.cancelled) { return; } - if (opt.sketch) { - defer_selection(); - return set(tempval, value); - } - for (let g of api.selection.groups()) { - set(g, tempval, value); - if (floor && opt.floor !== false) { - g.floor(mesh.group); - } - } + set(tempval, value); defer_selection(); }; let onkeydown = (ev) => { @@ -899,7 +898,7 @@ function ui_build() { el2.onclick = field_edit(`${axis} size`, (nval, oval) => { if (sel_item.type === 'polygon') { let scale = {x:1,y:1,z:1}; - let poly = base.newPolygon().fromObject(sel_item); + let poly = newPolygon().fromObject(sel_item); let bounds = poly.bounds; scale[axis] = nval/oval; poly.move(bounds.center().scale(-1,-1,-1)); @@ -919,7 +918,7 @@ function ui_build() { sel_item[lab] = nval; sketch.render(); }, { sketch: true }); - editable(el, el2, e3); + editable(el, el2, el3); } return; @@ -984,39 +983,39 @@ function ui_build() { // bind rotation editable fields let { group_val_X_rot, group_val_Y_rot, group_val_Z_rot } = bound; editable(group_val_X_rot, group_val_Y_rot, group_val_Z_rot); - group_val_X_rot.onclick = field_edit('x rotation', (group, val) => { - group.rotate(val,0,0); + group_val_X_rot.onclick = field_edit('x rotation', (val) => { + selection.rotate(val * deg,0,0); }); - group_val_Y_rot.onclick = field_edit('y rotation', (group, val) => { - group.rotate(0,val,0); + group_val_Y_rot.onclick = field_edit('y rotation', (val) => { + selection.rotate(0,val * deg,0); }); - group_val_Z_rot.onclick = field_edit('z rotation', (group, val) => { - group.rotate(0,0,val); + group_val_Z_rot.onclick = field_edit('z rotation', (val) => { + selection.rotate(0,0,val * deg); }); // bind position editable fields let { group_val_X_pos, group_val_Y_pos, group_val_Z_pos } = bound; editable(group_val_X_pos, group_val_Y_pos, group_val_Z_pos); - group_val_X_pos.onclick = field_edit('x position', (group, val) => { + group_val_X_pos.onclick = field_edit('x position', (val) => { selection.move_models(val,0,0); }, { floor: false }); - group_val_Y_pos.onclick = field_edit('y position', (group, val) => { + group_val_Y_pos.onclick = field_edit('y position', (val) => { selection.move_models(0,val,0); }, { floor: false }); - group_val_Z_pos.onclick = field_edit('z position', (group, val) => { + group_val_Z_pos.onclick = field_edit('z position', (val) => { selection.move_models(0,0,val); }, { floor: false }); // bind center editable fields let { span_val_X_center, span_val_Y_center, span_val_Z_center } = bound; editable(span_val_X_center, span_val_Y_center, span_val_Z_center); - span_val_X_center.onclick = field_edit('x center', (group, val, oval) => { + span_val_X_center.onclick = field_edit('x center', (val, oval) => { selection.move_models(val - parseFloat(oval),0,0); }); - span_val_Y_center.onclick = field_edit('y center', (group, val, oval) => { + span_val_Y_center.onclick = field_edit('y center', (val, oval) => { selection.move_models(0,val - parseFloat(oval),0); }); - span_val_Z_center.onclick = field_edit('z center', (group, val, oval) => { + span_val_Z_center.onclick = field_edit('z center', (val, oval) => { selection.move_models(0,0,val - parseFloat(oval)); }); @@ -1042,28 +1041,28 @@ function ui_build() { }; editable(lbl); }); - span_val_X_size.onclick = field_edit('x size', (group, val, oval) => { - let rel = val / parseFloat(oval); + span_val_X_size.onclick = field_edit('x size', (val, oval) => { + let rel = val / oval; if (axgrp.X) { - group.scale(rel, axgrp.Y ? rel : 1, axgrp.Z ? rel : 1); + selection.scale(rel, axgrp.Y ? rel : 1, axgrp.Z ? rel : 1); } else { - group.scale(rel, 1, 1); + selection.scale(rel, 1, 1); } }); - span_val_Y_size.onclick = field_edit('y size', (group, val, oval) => { - let rel = val / parseFloat(oval); + span_val_Y_size.onclick = field_edit('y size', (val, oval) => { + let rel = val / oval; if (axgrp.Y) { - group.scale(axgrp.X ? rel : 1, rel, axgrp.Z ? rel : 1); + selection.scale(axgrp.X ? rel : 1, rel, axgrp.Z ? rel : 1); } else { - group.scale(1, rel, 1); + selection.scale(1, rel, 1); } }); - span_val_Z_size.onclick = field_edit('z size', (group, val, oval) => { - let rel = val / parseFloat(oval); + span_val_Z_size.onclick = field_edit('z size', (val, oval) => { + let rel = val / oval; if (axgrp.Z) { - group.scale(axgrp.X ? rel : 1, axgrp.Y ? rel : 1, rel); + selection.scale(axgrp.X ? rel : 1, axgrp.Y ? rel : 1, rel); } else { - group.scale(1, 1, rel); + selection.scale(1, 1, rel); } }); @@ -1094,11 +1093,13 @@ function ui_build() { // listen for api calls // create a deferred wrapper to merge multiple rapid events - let defer_all = util.deferWrap(update_all); - let defer_selector = util.deferWrap(update_selector); - let defer_selection = util.deferWrap(update_selection); + const defer_all = util.deferWrap(update_all); + const defer_selector = util.deferWrap(update_selector); + const defer_selection = util.deferWrap(update_selection); broker.listeners({ + history_undo: defer_all, + history_redo: defer_all, model_add: defer_all, group_add: defer_all, model_remove: defer_all, @@ -1113,4 +1114,4 @@ function ui_build() { }); } -}); +export { }; diff --git a/src/mesh/edges.js b/src/mesh/edges.js index ec3d4966..248ef16c 100644 --- a/src/mesh/edges.js +++ b/src/mesh/edges.js @@ -1,21 +1,16 @@ /** Copyright Stewart Allen -- All Rights Reserved */ /** - * facet edge selecetor. allows for creating new facets + * facet edge selector. allows for creating new facets * when two connected edges share a single point. */ -"use strict"; +import { THREE, Line2, LineSegmentsGeometry, LineSegments2, LineGeometry, LineMaterial } from '../ext/three.js'; +import { space as motoSpace } from '../moto/space.js'; +import { newPoint } from '../geo/point.js'; +import { api } from './api.js'; - -// dep: moto.space -gapp.register("mesh.edges", [], (root, exports) => { - -const { Line2, LineMaterial, LineGeometry, Vector3, Group } = THREE; -const { LineSegments2, LineSegmentsGeometry } = THREE; -const { base, mesh, moto } = root; -const { space } = moto; -const { newPoint } = base; +const { Vector3, Group } = THREE; let isActive; let hovered; @@ -49,14 +44,14 @@ function cmp(v1, v2) { // points equal if all axis values are within tolerance of each other function eq(p1, p2) { - return + return true && cmp(p1.x, p2.x) && cmp(p1.y, p2.y) && cmp(p1.z, p2.z); } // split functions -let edges = { +const edges = { active() { return isActive ? true : false; }, @@ -66,21 +61,19 @@ let edges = { return; } - let { api } = mesh; - isActive = true; selected.length = 0; obj.visible = false; - space.scene.add(obj); + motoSpace.scene.add(obj); // enable temp mode let state = edges.state = { obj }; - let models = state.models = api.selection.models(); + let models = state.models = api.model.list(); let meshes = models.map(m => m.mesh); // find closest facet edge to hover location - space.mouse.onHover((int, event, ints) => { + motoSpace.mouse.onHover((int, event, ints) => { if (!event) { obj.visible = false; return meshes; @@ -105,12 +98,12 @@ let edges = { if (!isActive) { return; } - let space = moto.space; + let space = motoSpace; space.scene.remove(obj); space.scene.remove(edges.selected); space.mouse.onHover(undefined); isActive = edges.state = undefined; - mesh.api.selection.update(); + api.selection.update(); }, async add() { @@ -163,11 +156,11 @@ let edges = { clear() { selected.length = 0; - space.scene.remove(edges.selected); + motoSpace.scene.remove(edges.selected); }, select() { - space.scene.remove(edges.selected); + motoSpace.scene.remove(edges.selected); if (!hovered) return; const { object, face, side } = hovered; @@ -199,7 +192,7 @@ let edges = { } const group = edges.selected = new Group(); - space.scene.add(edges.selected); + motoSpace.scene.add(edges.selected); let verts = selected.map(s => s.side.verts).flat(); group.add(line2(verts)); }, @@ -216,8 +209,7 @@ let edges = { let l2 = { p0:v2, p1:v0, d:point.distToLine3D(v2, v0), v:[face.c,face.a].sort() }; return [ l0, l1, l2 ].sort((a,b) => a.d - b.d)[0]; } -} +}; -exports(edges); - -}); +export default edges; +export { edges }; diff --git a/src/mesh/geom.js b/src/mesh/geom.js index 99928024..a3041072 100644 --- a/src/mesh/geom.js +++ b/src/mesh/geom.js @@ -1,17 +1,11 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -// dep: moto.license -// dep: add.array -// dep: add.three -gapp.register("mesh.geom", [], (root, exports) => { - +import { THREE } from '../ext/three.js'; const { Matrix4, Matrix3, Vector3, Box3 } = THREE; +import { license as motoLicense } from '../moto/license.js'; // geometry helper functions -const geom = exports({ - +const geom = { // given a closed polyline as an expanded point array // return enclosed area with sign indicating winding order areaSigned(points, axes = 3) { @@ -93,6 +87,6 @@ const geom = exports({ return recs; } -}); +}; -}); +export { geom }; diff --git a/src/mesh/group.js b/src/mesh/group.js index 4b0449a1..37ec9077 100644 --- a/src/mesh/group.js +++ b/src/mesh/group.js @@ -1,27 +1,19 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -// dep: add.array -// dep: add.three -// dep: moto.license -// dep: moto.broker -// dep: moto.client -// dep: mesh.object -// use: mesh.api -// use: mesh.model -gapp.register("mesh.group", [], (root, exports) => { - -const { mesh, moto } = root; +import { THREE } from '../ext/three.js'; const { Group, Box3 } = THREE; -const { broker } = gapp; -const { space } = moto; +import { license as motoLicense } from '../moto/license.js'; +import { broker as motoBroker } from '../moto/broker.js'; +import { client as motoClient } from '../moto/client.js'; +import { space as motoSpace } from '../moto/space.js'; +import { object as meshObject } from './object.js'; +import { api as meshApi } from './api.js'; +import { model as meshModel } from './model.js'; -const call = broker.send; -const worker = moto.client.fn; - -mesh.group = class MeshGroup extends mesh.object { +const call = motoBroker.send; +const worker = motoClient.fn; +class MeshGroup extends meshObject { // @param group {MeshModel[]} constructor(models = [], id, name) { super(id); @@ -44,7 +36,7 @@ mesh.group = class MeshGroup extends mesh.object { get bounds() { let box3 = new Box3(); for (let model of this.models) { - box3.union(model.world_bounds); + box3.union(model.bounds); } return box3; } @@ -54,11 +46,11 @@ mesh.group = class MeshGroup extends mesh.object { model.group = this; this.models.addOnce(model); this.group3.add(model.mesh); - space.update(); + motoSpace.update(); call.model_add({model, group:this}); worker.group_add({id: this.id, model:model.id}); // update data store - mesh.db.space.put(this.id, this.models.map(m => m.id)); + meshApi.db.space.put(this.id, this.models.map(m => m.id)); return this; } @@ -82,14 +74,14 @@ mesh.group = class MeshGroup extends mesh.object { if (opt.free) model.remove(true); // auto-remove group when empty if (this.group3.children.length === 0) { - mesh.api.group.remove(this); + meshApi.group.remove(this); // manage lifecycle with worker, mesh app caches, etc this.destroy(); } else { // update data store - mesh.db.space.put(this.id, this.models.map(m => m.id)); + meshApi.db.space.put(this.id, this.models.map(m => m.id)); } - space.update(); + motoSpace.update(); return this; } @@ -98,10 +90,10 @@ mesh.group = class MeshGroup extends mesh.object { let { x, y, z } = dim; let max = Math.max(x, y, z); if (max < 2) { - mesh.api.log.emit(`auto-scaling import from ${max.round(5)}`); + meshApi.log.emit(`auto-scaling import from ${max.round(5)}`); this.scale(1000, 1000, 1000); } - let { center, floor } = mesh.api.prefs.map.space; + let { center, floor } = meshApi.prefs.map.space; if (center !== false) this.centerXY(); if (floor !== false) this.floor(); return this; @@ -173,6 +165,6 @@ mesh.group = class MeshGroup extends mesh.object { model.select(...arguments); } } -}; +} -}); +export { MeshGroup as group }; diff --git a/src2/mesh/handles.js b/src/mesh/handles.js similarity index 100% rename from src2/mesh/handles.js rename to src/mesh/handles.js diff --git a/src2/mesh/history.js b/src/mesh/history.js similarity index 100% rename from src2/mesh/history.js rename to src/mesh/history.js diff --git a/src/mesh/model.js b/src/mesh/model.js index 536a3253..70a5a92d 100644 --- a/src/mesh/model.js +++ b/src/mesh/model.js @@ -1,33 +1,21 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { THREE } from '../ext/three.js'; +import { client as motoClient } from '../moto/client.js'; +import { api as meshApi } from './api.js'; +import { object as meshObject } from './object.js'; +import { model as meshModel } from './model.js'; +import { newPolygon } from '../geo/polygon.js'; +import { polygons } from '../geo/polygons.js'; +import { space } from '../moto/space.js'; +import { util } from './util.js'; -// dep: add.array -// dep: add.three -// dep: geo.polygon -// dep: geo.polygons -// dep: moto.license -// dep: moto.client -// dep: mesh.object -// use: mesh.api -// use: mesh.util -// use: mesh.group -gapp.register("mesh.model", [], (root, exports) => { - -const { MeshPhongMaterial, MeshBasicMaterial, LineBasicMaterial } = THREE; -const { BufferGeometry, BufferAttribute, DoubleSide, Mesh } = THREE; -const { Box3, Vector3, Triangle } = THREE; -const { base, mesh, moto } = root; -const { space } = moto; -const { api } = mesh; -const { newBounds, newPolygon, polygons } = base; - -const mapp = mesh; -const worker = moto.client.fn; +const { MeshPhongMaterial, MeshBasicMaterial, LineBasicMaterial, BufferGeometry, BufferAttribute, DoubleSide, Mesh, Box3, Vector3, Triangle } = THREE; +const worker = motoClient.fn; const zero = new Vector3(0,0,0); /** default materials **/ -let materials = mesh.material = { +const materials = { // model unselected normal: new MeshPhongMaterial({ flatShading: true, @@ -73,24 +61,24 @@ let materials = mesh.material = { wireframe: true, color: 0x0, transparent: true, - opacity: 0.5 + opacity: 0.4 }), wireline: new LineBasicMaterial({ side: DoubleSide, color: 0x0, transparent: true, - opacity: 0.5 + opacity: 0.4 }), }; /** 3D model rendered on plaform **/ -mesh.model = class MeshModel extends mesh.object { +class MeshModel extends meshObject { constructor(data, id) { super(id); let { file, mesh, vertices } = data; - if (!mesh) { - dbug.error(`'${file}' missing mesh data`); + if (!(mesh || vertices)) { + meshApi.dbug.error(`'${file}' missing mesh data`); return; } @@ -127,37 +115,29 @@ mesh.model = class MeshModel extends mesh.object { return this.mesh; } - get bounds() { + get geo_bounds() { return this.geometry.boundingBox.clone(); } - get world_bounds() { - return this.bounds.translate(this.position()); + get bounds() { + // console.log('bounds', + // this.geometry.boundingBox.clone(), + // this.position().clone() + // ); + return this.geometry.boundingBox.clone().translate(this.position()); } - // get world_positions() { - // let pos = this.position(); - // return this.geometry - // .clone() - // .translate(pos.x,pos.y,pos.z) - // .attributes.position.array; - // } - get positions() { return this.geometry.attributes.position.array; } - // get matrix() { - // return this.matrixWorld.elements; - // } - get matrixWorld() { return this.mesh.matrixWorld; } drag(opt = {}) { let { start, delta, offset, end } = opt; - let { snap, snapon } = api.prefs.map.space; + let { snap, snapon } = meshApi.prefs.map.space; if (start) { let mid = this.bounds.mid; this._save = { @@ -194,7 +174,6 @@ mesh.model = class MeshModel extends mesh.object { qrotate(quaternion) { this.log('model-rotate', quaternion.toArray()); this.geometry.applyQuaternion(quaternion); - this.geometry._model_invalid = true; this.updateBounds(); } @@ -202,7 +181,6 @@ mesh.model = class MeshModel extends mesh.object { if (x === 1 && y === 1 && z === 1) return; this.log('model-scale', ...arguments, this.bounds); this.geometry.scale(x, y, z); - this.geometry._model_invalid = true; this.updateBounds(); } @@ -210,7 +188,6 @@ mesh.model = class MeshModel extends mesh.object { if (!(x || y || z)) return; this.log('model-translate', ...arguments); this.geometry.translate(x, y, z); - this.geometry._model_invalid = true; this.updateBounds(); } @@ -248,13 +225,15 @@ mesh.model = class MeshModel extends mesh.object { // moves mesh object center to target centerTo(to) { this.log('model-centerto', to); - let { mid } = this.bounds; - let abs = this.world_bounds.mid; - this.translate( - -mid.x + (abs.x - to.x), - -mid.y + (abs.y - to.y), - -mid.z + (abs.z - to.z), - ); + let geo = this.geo_bounds; + let { mid } = this.bounds.translate({ + x: -to.x, + y: -to.y, + z: -to.z + }); + // this.translate(-geo.mid.x, -geo.mid.y, -geo.mid.z); + // this.translate(mid.x, mid.y, mid.z); + this.translate(-geo.mid.x+mid.x, -geo.mid.y+mid.y, -geo.mid.z+mid.z); this.position(to.x, to.y, to.z); return this; } @@ -262,22 +241,19 @@ mesh.model = class MeshModel extends mesh.object { // when geometry updates, recompute bounds for ray intersections // and sync data to worker and indexed db updateBounds() { - if (this._wire) - this._wire.geometry.attributes.position.needsUpdate = true; + if (this._wire) { + this._wire.geometry.attributes.position.needsUpdate = true; + this.updateWireEdges(); + } this.attributes.position.needsUpdate = true; this.geometry.computeBoundingBox(); this.geometry.computeBoundingSphere(); this.log('update-bounds', this.geometry.boundingBox); this.normals({ refresh: true }); - this.updateBoundsBox(); - moto.space.update(); + space.update(); this.sync(); } - updateBoundsBox() { - this.group?.updateBoundsBox(); - } - mirror() { return this.duplicate({ mirror: true }); } @@ -291,14 +267,21 @@ mesh.model = class MeshModel extends mesh.object { let data = this.attributes.position.clone().array; if (opt.append) data = [...data, ...opt.append].toFloat32(); if (opt.x) return this.reload(data); - let model = new mesh.model({ file: `${this.file}`, mesh: data }); - let group = opt.group || mesh.api.group.new(); + let model = new meshModel({ file: `${this.file}`, mesh: data }); + let group = opt.group || meshApi.group.new(); let bounds = this.group.bounds; group.add(model); model.position(pos.x, pos.y, pos.z); - model.wireframe(this.wireframe()); + // preserve wireframe settings + let wireSettings = this.wireframe(); + if (wireSettings.enabled) { + model.wireframe(true, { + edges: wireSettings.edges, + opacity: wireSettings.opacity + }); + } if (opt.select) { - api.selection.add(model); + meshApi.selection.add(model); } if (opt.mirror) { group.move(0, 0, bounds.dim.z); @@ -332,10 +315,12 @@ mesh.model = class MeshModel extends mesh.object { this.log('reload'); let was = this.wireframe(false); let geo = this.mesh.geometry; + const oldGeo = geo; + geo = new BufferGeometry(); geo.setAttribute('position', new BufferAttribute(vertices, 3)); - // signal util.box3expand that geometry changed - geo._model_invalid = true; geo.computeVertexNormals(); + this.mesh.geometry = geo; + oldGeo.dispose(); // restore wireframe state this.wireframe(was); // fixup normals @@ -343,7 +328,7 @@ mesh.model = class MeshModel extends mesh.object { // sync worker, allows raycasting to work this.updateBounds(); // re-gen face index in surface mode - mesh.api.mode.check(); + meshApi.mode.check(); } rename(file) { @@ -356,7 +341,7 @@ mesh.model = class MeshModel extends mesh.object { // sync to worker worker.model_load({ id: this.id, name: this.file, vertices: this.positions }); // persist in db so it can be restored on page load - mapp.db.space.put(this.id, { + meshApi.db.space.put(this.id, { file: this.file, mesh: this.attributes.position.array }); @@ -461,6 +446,8 @@ mesh.model = class MeshModel extends mesh.object { enabled: true, opacity: this.opacity(), color: was ? this._wire.material.color : undefined, + edges: this._wire._edges, + isLineSegments: this._wire.isLineSegments } : { enabled: false }; @@ -468,29 +455,42 @@ mesh.model = class MeshModel extends mesh.object { if (bool.toggle) { bool = !was; } - // no change if (was === bool) { return was; } if (was) { this.mesh.remove(this._wire); + if (this._wire) { + this._wire.geometry.dispose(); + this._wire.material.dispose(); + } this._wire = undefined; this.opacity({restore: true}); } - if (bool === 'edges') { - let edges = new THREE.EdgesGeometry(this.mesh.geometry, 5); - this._wire = new THREE.LineSegments(edges, materials.wireframe); + if (bool && opt.edges) { + let edges = new THREE.EdgesGeometry(this.mesh.geometry, opt.edges); + this._wire = new THREE.LineSegments(edges, materials.wireline.clone()); + this._wire._edges = opt.edges; this.mesh.add(this._wire); - this.opacity({temp: opt.opacity || 0.15}); + this.opacity({temp: opt.opacity || meshApi.prefs.map.wireframe.opacity}); } else if (bool) { - this._wire = new Mesh(this.mesh.geometry.shallowClone(), materials.wireframe); + this._wire = new Mesh(this.mesh.geometry.shallowClone(), materials.wireframe.clone()); this.mesh.add(this._wire); - this.opacity({temp: opt.opacity || 0.15}); + this.opacity({temp: opt.opacity || meshApi.prefs.map.wireframe.opacity}); } space.update(); return was; } + updateWireEdges() { + if (this._wire?.isLineSegments) { + const newEdges = new THREE.EdgesGeometry(this.mesh.geometry, this._wire._edges); + this._wire.geometry.dispose(); + this._wire.material.dispose(); + this._wire.geometry = newEdges; + } + } + normals(bool) { let was = this._norm ? true : false; if (bool === undefined) { @@ -511,7 +511,7 @@ mesh.model = class MeshModel extends mesh.object { this._norm = undefined; } if (bool) { - this.mesh.add(this._norm = mesh.util.faceNormals(this.mesh)); + this.mesh.add(this._norm = util.faceNormals(this.mesh)); } } @@ -521,7 +521,7 @@ mesh.model = class MeshModel extends mesh.object { // invert normals for entire mesh or selected faces depending on mode invert(mode) { - let { modes } = mesh.api; + let { modes } = meshApi; let geo = this.mesh.geometry; let pos = geo.attributes.position; let arr = pos.array; @@ -565,12 +565,12 @@ mesh.model = class MeshModel extends mesh.object { return resolve(group); } if (o1?.length) - group.add(new mesh.model({ + group.add(new meshModel({ file: `${this.file}-bot`, mesh: o1 }).reCenter()); if (o2?.length) - group.add(new mesh.model({ + group.add(new meshModel({ file: `${this.file}-top`, mesh: o2 }).reCenter()); @@ -598,6 +598,25 @@ mesh.model = class MeshModel extends mesh.object { this.group = undefined; this.removed = 'pending'; } else { + // Clean up geometries and materials + if (this._wire) { + this._wire.geometry.dispose(); + this._wire.material.dispose(); + this._wire = undefined; + } + if (this._norm) { + this._norm.geometry.dispose(); + this._norm.material.dispose(); + this._norm = undefined; + } + if (this.mesh) { + this.mesh.geometry.dispose(); + if (Array.isArray(this.mesh.material)) { + this.mesh.material.forEach(m => m.dispose()); + } else { + this.mesh.material.dispose(); + } + } // manage lifecycle with worker, mesh app caches, etc this.destroy(); // tag removed for debugging @@ -652,7 +671,7 @@ mesh.model = class MeshModel extends mesh.object { let center = bounds.getCenter(new Vector3); polys.forEach(p => p.move({ x: -center.x, y: -center.y, z: -center.z })); let normal = new Vector3(0,0,1).applyQuaternion(quaternion); - let sketch = mesh.api.add.sketch({ + let sketch = meshApi.add.sketch({ normal, center: center.applyQuaternion(quaternion).add(this.position()) }); @@ -729,7 +748,7 @@ mesh.model = class MeshModel extends mesh.object { } updateSelections() { - let groups = mesh.util.facesToGroups(this.sel.faces); + let groups = util.facesToGroups(this.sel.faces); let geo = this.mesh.geometry; geo.clearGroups(); for (let group of groups) { @@ -802,13 +821,13 @@ mesh.model = class MeshModel extends mesh.object { let { a, b, c } = face; let timer = setTimeout(() => { timer = undefined; - mesh.api.log.emit("matching surface").pin(); + meshApi.log.emit("matching surface").pin(); }, 150); // let mark = Date.now(); worker.model_select({ id: this.id, x, y:-z, z:y, a, b, c, surface }).then(data => { - // mesh.api.log.emit(`... data time = ${Date.now() - mark}`); mark = Date.now(); + // meshApi.log.emit(`... data time = ${Date.now() - mark}`); mark = Date.now(); if (timer) { clearTimeout(timer); } @@ -816,16 +835,16 @@ mesh.model = class MeshModel extends mesh.object { // console.log({data}); // this.toggleSelectedVertices(verts); this.selectFaces(faces, action); - // mesh.api.log.emit(`... select time = ${Date.now() - mark}`); mark = Date.now(); + // meshApi.log.emit(`... select time = ${Date.now() - mark}`); mark = Date.now(); this.updateSelections(); if (!timer) { - mesh.api.log.emit("surface match complete").unpin(); - moto.space.refresh(); + api.log.emit("surface match complete").unpin(); + space.refresh(); } - // mesh.api.log.emit(`... paint time = ${Date.now() - mark}`); + // meshApi.log.emit(`... paint time = ${Date.now() - mark}`); }); } -}; +} -}); +export { MeshModel as model, materials }; diff --git a/src/mesh/object.js b/src/mesh/object.js index 9c1bdea1..349d6220 100644 --- a/src/mesh/object.js +++ b/src/mesh/object.js @@ -1,32 +1,26 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -// dep: add.three -// dep: moto.license -// dep: moto.broker -// dep: mesh.api -// use: mesh.util -gapp.register("mesh.object", [], (root, exports) => { +import { THREE } from '../ext/three.js'; +import { api } from './api.js'; +import { broker as motoBroker } from '../moto/broker.js'; +import { client as motoClient } from '../moto/client.js'; +import { space as motoSpace } from '../moto/space.js'; +import { util as meshUtil } from './util.js'; const { Matrix4, Vector3, Box3, Box3Helper, Quaternion } = THREE; -const { mesh, moto } = root; -const { space } = moto; - -const worker = moto.client.fn; +const worker = motoClient.fn; const lookUp = new Vector3(0,0,-1); // broker updates generated by objects -let publish = { - meta: gapp.broker.bind("object_meta"), - destroy: gapp.broker.bind("object_destroy"), - visible: gapp.broker.bind("object_visible"), +const publish = { + meta: motoBroker.bind("object_meta"), + destroy: motoBroker.bind("object_destroy"), + visible: motoBroker.bind("object_visible"), }; -mesh.object = class MeshObject { - +class MeshObject { constructor(id) { - this.id = id || mesh.util.uuid(); + this.id = id || meshUtil.uuid(); this.meta = { pos: [0,0,0] }; worker.object_create({ id: this.id, type: this.type }); worker.object_meta({ id: this.id, meta: this.meta }); @@ -34,7 +28,7 @@ mesh.object = class MeshObject { } log() { - // mesh.api.log.emit(this.id, this.type, ...arguments); + api.log.emit(this.id, this.type, ...arguments); } get type() { @@ -55,7 +49,7 @@ mesh.object = class MeshObject { // update worker state worker.object_destroy({id: this.id}); // update object store - mesh.db.space.remove(this.id); + api.db.space.remove(this.id); // main app cache workspace updates publish.destroy(this.id); } @@ -92,7 +86,7 @@ mesh.object = class MeshObject { } focus() { - mesh.api.focus(this.object); + api.focus(this); return this; } @@ -110,20 +104,15 @@ mesh.object = class MeshObject { } floor() { - let b = this.bounds; - return this.move(0, 0, -b.min.z); + return this.move(0, 0, -this.bounds.min.z); } center() { throw "center() requires implementation"; - let b = this.bounds; - return this.move(-b.mid.x, -b.mid.y, -b.mid.z); } centerXY() { throw "centerXY() requires implementation"; - let b = this.bounds; - return this.move(-b.mid.x, -b.mid.y, 0); } move() { @@ -163,41 +152,9 @@ mesh.object = class MeshObject { throw "position() requires implementation"; } - showBounds(bool) { - let was = this._showBounds; - if (bool && bool.toggle) { - bool = !this._showBounds; - } - if (was === bool) { - return; - } - this._showBounds = bool; - this.updateBoundsBox(); - } - - updateBoundsBox() { - this.log('update-bounds-box', this._showBounds); - let helper = this._boundsBox; - let world = space.world; - if (helper) { - world.remove(helper); - } - if (this._showBounds) { - let { mid, dim } = this.bounds; - let b3 = new Box3().setFromCenterAndSize( - new Vector3(mid.x, mid.y, mid.z), - new Vector3(dim.x, dim.y, dim.z) - ); - let helper = this._boundsBox = new Box3Helper(b3, 0x555555); - world.add(helper); - return true; - } - } - metaChanged(values = {}) { this.object.updateMatrix(); - this.updateBoundsBox(); - space.update(); + motoSpace.update(); Object.assign(this.meta, values, { // matrix: this.object.matrix.elements }); @@ -205,6 +162,6 @@ mesh.object = class MeshObject { publish.meta({ id: this.id, meta: this.meta }); return this; } -}; +} -}); +export { MeshObject as object }; diff --git a/src/mesh/pool.js b/src/mesh/pool.js index 08863338..d4c79733 100644 --- a/src/mesh/pool.js +++ b/src/mesh/pool.js @@ -1,9 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { license as motoLicense } from '../moto/license.js'; +import { worker as motoWorker } from '../moto/worker.js'; -// dep: moto.license -// dep: moto.worker -gapp.main("mesh.pool", [], (root) => { - // todo -}); +// todo +export const pool = {}; diff --git a/src/mesh/sketch.js b/src/mesh/sketch.js index bdf0fdf9..be5504ba 100644 --- a/src/mesh/sketch.js +++ b/src/mesh/sketch.js @@ -1,30 +1,18 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { client as motoClient } from '../moto/client.js'; +import { space as motoSpace } from '../moto/space.js'; +import { broker } from '../moto/broker.js'; +import { object as meshObject } from './object.js'; +import { model as meshModel, materials } from './model.js'; +import { util as meshUtil } from './util.js'; +import { api as meshApi } from './api.js'; +import { newPolygon } from '../geo/polygon.js'; +import { polygons } from '../geo/polygons.js'; +import { THREE } from '../ext/three.js'; -// dep: add.array -// dep: add.three -// dep: geo.polygon -// dep: geo.polygons -// dep: moto.license -// dep: moto.client -// dep: mesh.object -// dep: mesh.api -// use: mesh.util -// use: mesh.group -gapp.register("mesh.sketch", [], (root, exports) => { - -const { BufferGeometry, BufferAttribute, Quaternion } = THREE; -const { MeshBasicMaterial, LineBasicMaterial, LineSegments, DoubleSide } = THREE; -const { PlaneGeometry, EdgesGeometry, SphereGeometry, Vector3, Mesh, Group } = THREE; -const { base, mesh, moto } = root; -const { space, broker } = moto; -const { api, util } = mesh; -const { newPolygon } = base; - -const mapp = mesh; -const worker = moto.client.fn; -const POLYS = base.polygons; +const { BufferGeometry, BufferAttribute, Quaternion, MeshBasicMaterial, LineBasicMaterial, LineSegments, DoubleSide, PlaneGeometry, EdgesGeometry, SphereGeometry, Vector3, Box3, Mesh, Group } = THREE; +const worker = motoClient.fn; const drag = {}; const hpos = [ [-1, 1, 1], @@ -40,11 +28,11 @@ const material = { }; function log() { - mesh.api.log.emit(...arguments); + meshApi.log.emit(...arguments); } /** 2D plane containing open and closed polygons which can be extruded **/ -mesh.sketch = class MeshSketch extends mesh.object { +class MeshSketch extends meshObject { constructor(opt = {}) { super(opt.id); @@ -101,7 +89,7 @@ mesh.sketch = class MeshSketch extends mesh.object { ); } - normal = new THREE.Vector3(normal.x, normal.y, normal.z).normalize(); + normal = new Vector3(normal.x, normal.y, normal.z).normalize(); group.quaternion.copy(new Quaternion().setFromUnitVectors(new Vector3(0, 0, 1), normal)); this.#db_save(); @@ -109,17 +97,17 @@ mesh.sketch = class MeshSketch extends mesh.object { lookat(x,y,z) { this.group.lookAt(new Vector3(x,y,z)); - moto.space.update(); + motoSpace.update(); } #db_save() { let { center, normal, scale, type, file, items } = this; - mapp.db.space.put(this.id, { center, normal, scale, type, file, items }); + meshApi.db.space.put(this.id, { center, normal, scale, type, file, items }); } #db_remove() { - mapp.api.sketch.remove(this); - mapp.db.space.remove(this.id); + meshApi.sketch.remove(this); + meshApi.db.space.remove(this.id); } get type() { @@ -130,6 +118,22 @@ mesh.sketch = class MeshSketch extends mesh.object { return this.group; } + get bounds() { + const { scale, center } = this; + return new Box3( + new Vector3( + center.x - scale.x/2, + center.y - scale.y/2, + center.z + ), + new Vector3( + center.x + scale.x/2, + center.y + scale.y/2, + center.z + ) + ); + } + get meshes() { return this.group.children.filter(c => { return c.sketch || (c.sketch_item && c.sketch_item.selected) ? c : undefined @@ -233,10 +237,10 @@ mesh.sketch = class MeshSketch extends mesh.object { }, item(item) { - item.group = item.group || mesh.util.uuid(); + item.group = item.group || meshUtil.uuid(); sketch.items.push(item); sketch.render_defer(); - api.selection.update_defer(); + meshApi.selection.update_defer(); return item; } } @@ -249,7 +253,7 @@ mesh.sketch = class MeshSketch extends mesh.object { let items = sketch.selection.mesh_items(); if (items.length < 2) return log('operation requires at least 2 items'); let polys = items.map(i => i.sketch_item.poly); - let union = POLYS.union(polys, 0, true); + let union = polygons.union(polys, 0, true); sketch.selection.delete(); sketch.arrange.group(union.map(poly => sketch.add.polygon({ poly }))); }, @@ -258,7 +262,7 @@ mesh.sketch = class MeshSketch extends mesh.object { let items = sketch.selection.mesh_items(); if (items.length !== 2) return log('operation requires 2 items'); let polys = items.map(i => i.sketch_item.poly); - let trim = POLYS.trimTo([polys[0]], [polys[1]]); + let trim = polygons.trimTo([polys[0]], [polys[1]]); sketch.selection.delete(); for (let poly of trim) { sketch.add.polygon({ poly }); @@ -269,8 +273,8 @@ mesh.sketch = class MeshSketch extends mesh.object { let items = sketch.selection.mesh_items(); if (items.length !== 2) return log('operation requires 2 items'); let polys = items.map(i => i.sketch_item.poly); - let diff1 = POLYS.diff([polys[0]], [polys[1]]); - let diff2 = POLYS.diff([polys[1]], [polys[0]]); + let diff1 = polygons.diff([polys[0]], [polys[1]]); + let diff2 = polygons.diff([polys[1]], [polys[0]]); sketch.selection.delete(); for (let poly of [...diff1, ...diff2]) { sketch.add.polygon({ poly }); @@ -281,7 +285,7 @@ mesh.sketch = class MeshSketch extends mesh.object { let items = sketch.selection.items(); if (items.length < 2) return log('operation requires at least 2 items'); let polys = items.map(si => si.poly.annotate({group:si.group}).clone(true, ['group'])); - let union = POLYS.nest(POLYS.flatten(polys, [], true)); + let union = polygons.nest(polygons.flatten(polys, [], true)); sketch.selection.delete(); for (let poly of union) { sketch.add.polygon({ poly, group:poly.group }); @@ -291,8 +295,8 @@ mesh.sketch = class MeshSketch extends mesh.object { evenodd() { let items = sketch.selection.items(); let polys = items.map(si => si.poly.annotate({group:si.group})); - let even = POLYS.nest(POLYS.flatten(polys.clone(true,['group']), [], true)); - let odd = POLYS.nest(POLYS.flatten(even.clone(true,['group']), [], true).filter(p => p.depth > 0)); + let even = polygons.nest(polygons.flatten(polys.clone(true,['group']), [], true)); + let odd = polygons.nest(polygons.flatten(even.clone(true,['group']), [], true).filter(p => p.depth > 0)); sketch.selection.delete(); for (let poly of [...even, ...odd]) { sketch.add.polygon({ poly, group:poly.group }); @@ -302,7 +306,7 @@ mesh.sketch = class MeshSketch extends mesh.object { flatten() { let items = sketch.selection.items(); let polys = items.map(si => si.poly.annotate({group:si.group}).clone(true, ['group'])); - let flat = POLYS.flatten(polys, [], true); + let flat = polygons.flatten(polys, [], true); sketch.selection.delete(); for (let poly of flat) { sketch.add.polygon({ poly }); @@ -356,7 +360,7 @@ mesh.sketch = class MeshSketch extends mesh.object { }, ungroup(items) { - items.forEach(i => i.group = mesh.util.uuid()); + items.forEach(i => i.group = meshUtil.uuid()); sketch.render(); } } @@ -407,8 +411,20 @@ mesh.sketch = class MeshSketch extends mesh.object { return bool; } - move(x, y, z = 0) { + position() { const { target } = drag; + console.log({ sk_pos: [ ...arguments ], target }); + const [ nx, ny, nz ] = [ ...arguments ]; + if (nx || ny || nz) { + this.center = { x: nx, y: ny, z: nz }; + this.render(); + } else { + const { x, y, z } = this.center; + return new Vector3(x, y, z); + } + } + + move(x, y, z = 0, target = drag.target) { const { center, scale, plane, handles } = this; const handle = handles.indexOf(target); if (target === plane) { @@ -473,7 +489,7 @@ mesh.sketch = class MeshSketch extends mesh.object { item?.center ?? center); } else if (offset) { let { start, item, handle } = drag; - let { snap, snapon } = api.prefs.map.space; + let { snap, snapon } = meshApi.prefs.map.space; let pos = handle >= 0 ? this.handle_pos(handle) : item?.center ?? center; @@ -518,11 +534,12 @@ mesh.sketch = class MeshSketch extends mesh.object { group.add(...sketch_item.outs); } this.update(); - space.refresh(); + motoSpace.refresh(); + broker.publish('sketch_render', this); } render_defer() { - util.defer(() => this.render()); + meshUtil.defer(() => this.render()); } extrude(opt = {}) { @@ -537,13 +554,13 @@ mesh.sketch = class MeshSketch extends mesh.object { chamfer_top, chamfer_bottom }); - let nmdl = new mesh.model({ file: "item", mesh: vert.toFloat32() }); + let nmdl = new meshModel({ file: "item", mesh: vert.toFloat32() }); models.push(nmdl); } if (models.length) { log('extrude', this.file || this.id, 'into', models.length, 'solid(s)'); let { center } = this; - let ngrp = api.group.new(models); + let ngrp = meshApi.group.new(models); ngrp.move(center.x, center.y, center.z); // align extrusion with sketch plane const euler = this.group.rotation; @@ -599,9 +616,8 @@ class SketchItem { update() { let { item, sketch, order } = this; - let { material } = mesh; let { type, center, width, height, miter, radius, points, spacing, poly, selected } = item; - let { open_close, open_width, open_type } = api.prefs.map.sketch; + let { open_close, open_width, open_type } = meshApi.prefs.map.sketch; let base = 0.025; let bump = sketch.scale.z || 0.0001; if (type === 'circle') { @@ -622,7 +638,7 @@ class SketchItem { this.poly = poly; let isSelected = selected && sketch.select(); // create solid filled area - let mat = (isSelected ? material.select : material.normal).clone(); + let mat = (isSelected ? materials.select : materials.normal).clone(); mat.transparent = true; mat.opacity = 0.5; let vrt = poly.extrude(0).toFloat32(); @@ -639,7 +655,7 @@ class SketchItem { let lpt = p.points.map(p => new Vector3(p.x, p.y, p.z)); lpt.push(lpt[0]); let lge = new BufferGeometry().setFromPoints(lpt); - let out = new THREE.Line(lge, material.wireline); + let out = new THREE.Line(lge, materials.wireline); out.renderOrder = -1; out.sketch_line = this; out.position.z += base + (order * bump); @@ -649,4 +665,8 @@ class SketchItem { } } -}); \ No newline at end of file +function target() { + return drag.target; +} + +export { MeshSketch as sketch, SketchItem, target }; \ No newline at end of file diff --git a/src/mesh/split.js b/src/mesh/split.js index 065e27bc..229dcf60 100644 --- a/src/mesh/split.js +++ b/src/mesh/split.js @@ -6,24 +6,21 @@ * click to split the selected mesh(es) */ -"use strict"; +import { THREE } from '../ext/three.js'; +import { space as motoSpace } from '../moto/space.js'; +import { broker as motoBroker } from '../moto/broker.js'; +import { api as meshApi } from './api.js'; +import { util as meshUtil } from './util.js'; -// dep: moto.space -// dep: moto.broker -gapp.register("mesh.split", [], (root, exports) => { - -const { broker } = gapp; const { Mesh, MeshPhongMaterial, PlaneGeometry, DoubleSide, Vector3 } = THREE; -const { mesh, moto } = root; -const { space } = moto; let isActive; -const key_once = broker.bind('key_once'); -const key_cancel = broker.bind('key_once_cancel'); +const key_once = motoBroker.bind('key_once'); +const key_cancel = motoBroker.bind('key_once_cancel'); // split functions -let split = { +const split = { active() { return isActive ? true : false; }, @@ -32,9 +29,8 @@ let split = { if (isActive) { return; } - let { api, util } = mesh; - let { log } = api; - if (api.selection.models().length === 0) { + let { log } = meshApi; + if (meshApi.selection.models().length === 0) { log.emit('no models selected for splitting'); return; } @@ -48,15 +44,15 @@ let split = { opacity: 0.5 }) ); - space.scene.add(obj); + motoSpace.scene.add(obj); // hide until first hover obj.visible = false; // enable temp mode let state = split.state = { obj }; - let models = state.models = api.selection.models(); + let models = state.models = meshApi.selection.models(); let meshes = models.map(m => m.mesh); // for split and lay flat modes - space.mouse.onHover((int, event, ints) => { + motoSpace.mouse.onHover((int, event, ints) => { if (!event) { return meshes; } @@ -65,11 +61,11 @@ let split = { if (buttons) { return; } - let { dim, mid } = util.bounds(meshes); + let { dim, mid } = meshUtil.bounds(models); let { point, face, object } = int; let { x, y, z } = point; // set appearance of split plane - mat.color.set(api.prefs.map.space.dark ? 0x0059bb : 0x0079ff); + mat.color.set(meshApi.prefs.map.space.dark ? 0x0059bb : 0x0079ff); obj.visible = true; if (event.shiftKey) { y = split.closestZ(y, object, face).y; @@ -92,31 +88,31 @@ let split = { state.plane = { z }; split.select(state); } - api.modal.dialog({ + meshApi.modal.dialog({ title: "split object Z", body: [ h.div({ class: "additem" }, [ h.label('Z value'), h.input({ value: 0, size: 5, id: "_value" }), h.button({ _: "split", onclick() { - const { _value } = api.modal.bound; + const { _value } = meshApi.modal.bound; doit(parseFloat(_value.value)); - api.modal.hide(); + meshApi.modal.hide(); } }) ]) ] }); - api.modal.bound._value.focus(); + meshApi.modal.bound._value.focus(); } }); }, select(state) { - let { log } = mesh.api; + let { log } = meshApi; let { models, plane } = split.state || state; if (!(models && plane)) { return split.end(); } log.emit(`splitting ${models.length} model(s) at ${plane.z.round(3)}`).pin(); Promise.all(models.map(m => m.split(plane))).then(models => { - mesh.api.selection.set(models.filter(m => m)); + meshApi.selection.set(models.filter(m => m)); log.emit('split complete').unpin(); split.end(); }); @@ -128,12 +124,11 @@ let split = { } key_cancel('KeyS'); key_cancel('KeyV'); - let space = moto.space; let { obj } = split.state; - space.scene.remove(obj); - space.mouse.onHover(undefined); + motoSpace.scene.remove(obj); + motoSpace.mouse.onHover(undefined); isActive = split.state = undefined; - mesh.api.selection.update(); + meshApi.selection.update(); }, closestZ(z, object, face) { @@ -147,8 +142,6 @@ let split = { v2._d = Math.abs(v2.y - z); return [ v0, v1, v2 ].sort((a,b) => a._d - b._d)[0]; } -} +}; -exports(split); - -}); +export { split }; diff --git a/src/mesh/tool.js b/src/mesh/tool.js index 851058bd..957b3d7d 100644 --- a/src/mesh/tool.js +++ b/src/mesh/tool.js @@ -1,14 +1,9 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -// dep: ext.earcut -// dep: mesh.geom -gapp.register("mesh.tool", [], (root, exports) => { - -const { mesh } = root; -const { geom } = mesh; +import { THREE } from '../ext/three.js'; const { Vector3 } = THREE; +import { geom } from './geom.js'; + const empty = 0xffffffff; /** @@ -17,7 +12,7 @@ const empty = 0xffffffff; * two need to be merged. earcut needs to be migrated * to base.util.triagulate */ -mesh.tool = class MeshTool { +class MeshTool { constructor(params = {}) { this.precision = Math.pow(10, params.precision || 6); } @@ -249,22 +244,17 @@ mesh.tool = class MeshTool { const s0 = faces[foff + 3]; const s1 = faces[foff + 4]; const s2 = faces[foff + 5]; - const farr = [ - sides[s0 * 4 + 2], - sides[s0 * 4 + 3], - sides[s1 * 4 + 2], - sides[s1 * 4 + 3], - sides[s2 * 4 + 2], - sides[s2 * 4 + 3] - ].filter(f => f !== empty && f !== face); - if (!farr.length) { - console.log(`no adjacent faces to ${face}`); - return []; + const adj = []; + for (let s of [s0, s1, s2]) { + const soff = s * 4; + const f1 = sides[soff + 2]; + const f2 = sides[soff + 3]; + if (f1 !== empty) adj.push(f1); + if (f2 !== empty) adj.push(f2); } - return farr; + return adj; } - // depends on index() being run first findConnectedSurface(faces, radians, filterZ, found = {}) { const norms = this.getIndex().faces; if (filterZ !== undefined) { @@ -304,8 +294,6 @@ mesh.tool = class MeshTool { return faces; } - // given a list of faces (indices), return an array of closed - // polylines that represent the outlines of each discrete island generateOutlines(list) { const { faces, sides } = this.getIndex(); const prec = this.precision; @@ -380,7 +368,6 @@ mesh.tool = class MeshTool { return outs; } - // depends on index() being run first isolateBodies() { const verts = this.checkVertices(this.vertices); const bodies = []; @@ -403,12 +390,6 @@ mesh.tool = class MeshTool { return bodies; } - /** - * finds edge lines which are line segments on a single face. - * construct ordered line maps with array of connected edges. - * connect lines into polys. earcut polys into new faces. - * requires generateFaces() be run first - */ patch(opt = { merge: true }) { let vertices = this.checkVertices(this.uvert); let faces = this.faces; @@ -737,7 +718,6 @@ mesh.tool = class MeshTool { return this; } - // add vertex data from given index into array appendVertex(index, array = []) { let vertx = this.uvert; array.push(vertx[index++]); @@ -746,7 +726,6 @@ mesh.tool = class MeshTool { return array; } - // add vertices from a vertex index list to an array expandArray(indices, array = []) { for (let i of indices) { this.appendVertex(i, array); @@ -754,7 +733,6 @@ mesh.tool = class MeshTool { return array; } - // turn loop into XYZ vertex array expandLoop(loop, array = []) { for (let line of loop) { this.appendVertex(line.v1, array); @@ -763,14 +741,12 @@ mesh.tool = class MeshTool { return array; } - // merge generated poly areas into faces merge() { for (let area of this.areas || []) { this.faces.appendAll(area); } } - // return non-indexed vertex list (for compatibility) unrolled() { let out = []; for (let face of this.faces) { @@ -779,8 +755,6 @@ mesh.tool = class MeshTool { return out; } - // returns points array for a polygon - // extrusion and twist is handled in work.js generateGear(numTeeth, module, pressureAngle, offset) { // Adapted from: Public Domain Parametric Involute Spur Gear by Leemon Baird, 2011, Leemon@Leemon.com http://www.thingiverse.com/thing:5505 // see also http://grabcad.com/questions/tutorial-how-to-model-involute-gears-in-solidworks-and-show-design-intent @@ -858,7 +832,7 @@ mesh.tool = class MeshTool { } pitch = p.round(3); - mesh.log(`gear pitch radius: ${pitch}`); + geom.log(`gear pitch radius: ${pitch}`); } return { gear, pitch }; @@ -910,7 +884,6 @@ mesh.tool = class MeshTool { } return verts; } +} -}; - -}); +export { MeshTool as tool }; diff --git a/src/mesh/util.js b/src/mesh/util.js index e5de2d01..6f931ada 100644 --- a/src/mesh/util.js +++ b/src/mesh/util.js @@ -1,20 +1,13 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -// dep: moto.license -// dep: add.array -// dep: add.three -gapp.register("mesh.util", [], (root, exports) => { - -const { Matrix4, Matrix3, Vector3, Box3 } = THREE; -const { mesh } = root; +import { THREE } from '../ext/three.js'; +import { api as meshApi } from './api.js'; +const { Matrix3, Vector3, Box3, LineSegments, BufferGeometry, Float32BufferAttribute, LineBasicMaterial } = THREE; const deferFn = []; -const boundsCache = {}; // util functions augmented in build (download) -const util = exports({ +const util = { uuid(segs = 1) { let uid = []; @@ -71,81 +64,20 @@ const util = exports({ } }, - // @param object {THREE.Object3D | THREE.Object3D[] | MeshObject | MeshObject[]} - // @returns bounds modified for moto.space + // @param object {MeshObject | MeshObject[]} bounds(object) { + // console.log({ bounds: object }); let box = new Box3(); if (Array.isArray(object)) { for (let o of object) { - util.box3expand(box, o instanceof mesh.object ? o.object : o); + box.union(o.bounds); } } else if (object) { - util.box3expand(box, object instanceof mesh.object ? object.object : object); - } else { - return box; - } - let bnd = { - min: { - x: box.min.x, - y: box.min.z, - z: box.min.y - }, - max: { - x: box.max.x, - y: box.max.z, - z: box.max.y - } - }; - bnd.size = bnd.dim = { - x: bnd.max.x - bnd.min.x, - y: bnd.max.y - bnd.min.y, - z: bnd.max.z - bnd.min.z - }; - bnd.center = bnd.mid = { - x: (bnd.max.x + bnd.min.x) / 2, - y: -(bnd.max.y + bnd.min.y) / 2, - z: (bnd.max.z + bnd.min.z) / 2 - }; - return bnd; - }, - - // bounding box workaround adapted from: - // https://discourse.threejs.org/t/bounding-box-bigger-than-concave-object-extrudegeometry/26073/2 - // https://discourse.threejs.org/t/invalid-bounds-generated-for-some-orientations/33205 - box3expand(box3, object) { - if (object._no_bounds) { - return; - } - - let geometry = object.geometry; - object.updateWorldMatrix(geometry ? true : false, false); - - if (geometry) { - let matrix = object.matrixWorld; - let bkey = [matrix.elements.map(v => v.round(5))].join(',') - let cached = boundsCache[object.id]; - // geometry._model_invalid set on model.reload(), usually after a split - if (!cached || cached.bkey !== bkey || geometry._model_invalid) { - let position = geometry.attributes.position.clone(); - position.applyMatrix4(new Matrix4().extractRotation(matrix)); - let bounds = new Box3().setFromBufferAttribute(position); - // let scale = new Vector3().setFromMatrixScale(matrix); - // bounds.min.multiply(scale); - // bounds.max.multiply(scale); - cached = boundsCache[object.id] = { bkey, bounds }; - geometry._model_invalid = undefined; - } - let bt = cached.bounds.clone(); - let m4 = new Matrix4(); - m4.setPosition(new Vector3().setFromMatrixPosition(object.matrixWorld)); - bt.applyMatrix4(m4); - box3.union(bt); - } - - let children = object.children; - for (let i = 0, l = children.length; i < l; i++) { - util.box3expand(box3, children[i]); + box.union(object.bounds); } + box.size = box.dim; + box.center = box.mid; + return box; }, // extract object fields into an array with optional rounding @@ -183,25 +115,25 @@ const util = exports({ }, faceNormals(obj, opt = { }) { - const _va = new THREE.Vector3(); - const _vb = new THREE.Vector3(); - const _vc = new THREE.Vector3(); - const _v1 = new THREE.Vector3(); - const _v2 = new THREE.Vector3(); - const _normalMatrix = new THREE.Matrix3(); - const prefs = mesh.api.prefs.map; + const _va = new Vector3(); + const _vb = new Vector3(); + const _vc = new Vector3(); + const _v1 = new Vector3(); + const _v2 = new Vector3(); + const _normalMatrix = new Matrix3(); + const prefs = meshApi.prefs.map; const norms = prefs.normals; const defcolor = prefs.space.dark ? norms.color_dark : norms.color_lite; const normlen = norms.length || 1; - class FaceNormalsHelper extends THREE.LineSegments { + class FaceNormalsHelper extends LineSegments { constructor(object, size = opt.size || normlen, color = opt.color || defcolor) { const objGeometry = object.geometry; const nNormals = objGeometry.attributes.position.count / 3; - const geometry = new THREE.BufferGeometry(); - const positions = new THREE.Float32BufferAttribute(nNormals * 3 * 2, 3); + const geometry = new BufferGeometry(); + const positions = new Float32BufferAttribute(nNormals * 3 * 2, 3); geometry.setAttribute('position', positions); - super(geometry, new THREE.LineBasicMaterial({ + super(geometry, new LineBasicMaterial({ color, toneMapped: false })); this.object = object; @@ -240,20 +172,21 @@ const util = exports({ const _v2 = new Vector3(); const _normalMatrix = new Matrix3(); - class VertexNormalsHelper extends THREE.LineSegments { + class VertexNormalsHelper extends LineSegments { constructor(object, size = 1, color = 0xff0000) { const objGeometry = object.geometry; const nNormals = objGeometry.attributes.normal.count; - const geometry = new THREE.BufferGeometry(); - const positions = new THREE.Float32BufferAttribute(nNormals * 2 * 3, 3); + const geometry = new BufferGeometry(); + const positions = new Float32BufferAttribute(nNormals * 2 * 3, 3); geometry.setAttribute('position', positions); - super(geometry, new THREE.LineBasicMaterial({ - color,toneMapped: false + super(geometry, new LineBasicMaterial({ + color, toneMapped: false })); this.object = object; this.size = size; this.type = 'VertexNormalsHelper'; this.matrixAutoUpdate = false; + this._no_bounds = true; this.update(); } @@ -261,12 +194,13 @@ const util = exports({ this.object.updateMatrixWorld(true); const position = this.geometry.attributes.position; const objGeometry = this.object.geometry; - const objPos = objGeometry.attributes.position; - const objNorm = objGeometry.attributes.normal; - for (let idx = 0, j = 0, jl = objPos.count; j < jl; j++) { - _v1.set(objPos.getX(j), objPos.getY(j), objPos.getZ(j)); - _v2.set(objNorm.getX(j), objNorm.getY(j), objNorm.getZ(j)); - _v2.applyMatrix3(_normalMatrix).normalize().multiplyScalar(this.size).add(_v1); + const objArr = objGeometry.attributes.position.array; + const normArr = objGeometry.attributes.normal.array; + let j=0; + for (let idx = 0, jl = objArr.length; j < jl; ) { + _v1.set(objArr[j++], objArr[j++], objArr[j++]); + _v2.set(normArr[j-3], normArr[j-2], normArr[j-1]); + _v2.multiplyScalar(this.size).add(_v1); position.setXYZ(idx++, _v1.x, _v1.y, _v1.z); position.setXYZ(idx++, _v2.x, _v2.y, _v2.z); } @@ -332,6 +266,6 @@ const util = exports({ } } -}); +}; -}); +export { util }; diff --git a/src/mesh/work.js b/src/mesh/work.js index ab242d52..1c0c3c1c 100644 --- a/src/mesh/work.js +++ b/src/mesh/work.js @@ -1,43 +1,36 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import "../ext/clip2.js"; +import "../ext/earcut.js"; +import '../add/array.js'; +import '../add/class.js'; +import '../add/three.js'; -// dep: moto.license -// dep: ext.clip2 -// dep: add.three -// dep: add.array -// dep: moto.client -// dep: moto.worker -// dep: mesh.util -// dep: geo.base -// dep: geo.line -// dep: geo.point -// dep: geo.polygon -// dep: geo.polygons -// dep: geo.bounds -// dep: geo.slicer -// dep: geo.csg -// dep: mesh.tool -gapp.main("mesh.work", [], (root) => { +import { version } from '../moto/license.js'; +import { client as motoClient } from '../moto/client.js'; +import { worker } from '../moto/worker.js'; +import { sliceConnect } from '../geo/slicer.js'; +import { newPolygon } from '../geo/polygon.js'; +import { newPoint } from "../geo/point.js"; +import { polygons } from '../geo/polygons.js'; +import { CSG } from '../geo/csg.js'; +import { util } from './util.js'; +import { tool as meshTool } from './tool.js'; const { Triangle, Vector3, BufferGeometry, BufferAttribute, computeFaceNormal } = THREE; -const { base, mesh, moto } = root; -const { client, worker } = moto; -const { CSG, newPoint, newPolygon, sliceConnect, polygons } = base; + const cache = {}; // add scoped access to cache -mesh.work = { cache }; +const work = { cache }; // start worker pool (disabled for now with *0) -client.start(`/code/mesh_pool?${gapp.version}`, client.max() * 0); +motoClient.start(`/lib/mesh/pool.js?${version}`, motoClient.max() * 0); function log(msg) { return worker.publish("mesh.log", msg); } -mesh.log = log; - function cacheUpdate(id, data) { return Object.assign(cache[id], data); } @@ -60,7 +53,7 @@ function analyze(id, opt = {}) { let rec = cache[id]; let { geo, tool } = rec; if (!tool) { - tool = rec.tool = new mesh.tool(opt); + tool = rec.tool = new meshTool(opt); } if (tool.faces) { log(`${id} | analysis cached`); @@ -83,7 +76,7 @@ function indexFaces(id) { let rec = cache[id]; let { geo, tool } = rec; if (!tool) { - tool = rec.tool = new mesh.tool(); + tool = rec.tool = new meshTool(); } if (!tool.indexed) { log(`${id} | indexing mesh`); @@ -159,7 +152,7 @@ function splitHeal(z, o1, edges, rev) { return edges; } -let model = { +const model = { load(data) { let { vertices, name, id } = data; let geo = new BufferGeometry(); @@ -207,12 +200,8 @@ let model = { let tools = recs.filter(rec => rec.tool) .map(rec => translate_encode(rec.id)) .map(a => CSG.fromPositionArray(a)); - let subs = []; - for (let obj of bases) { - let sub = CSG.subtract(obj, ...tools); - subs.appendAll(CSG.toPositionArray(sub)); - } - return subs.toFloat32(); + let diff = CSG.difference(...bases, ...tools); + return CSG.toPositionArray(diff); }, // split a model along an axis at a given point @@ -444,7 +433,7 @@ let model = { gen_gear(data, send) { const { teeth, module, angle, twist, shaft, offset, height, chamfer } = data; - const { gear, pitch } = new mesh.tool().generateGear(teeth, module, angle, offset); + const { gear, pitch } = new meshTool().generateGear(teeth, module, angle, offset); const points = gear.map(v => [...v, 0]).flat(); const poly = newPolygon().addVerts(points); if (shaft) { @@ -455,9 +444,9 @@ let model = { const zh = height || 15; const verts = poly.extrude(zh, { chamfer }); if (twist) { - const rad = base.util.toRadians(twist); + const rad = util.toRadians(twist); for (let i=0; i v.round(5)); + vtot += (vs.length / 3); + } + switch (format) { + case "obj": + let p = 1; + let obj = [header]; + for (let rec of recs) { + let { file, varr } = rec; + obj.push(`g ${file}`); + for (let i=0; i { + dat.setUint8(i, c.charCodeAt(0)); + }); + // todo put Mesh:Tool info in header + dat.setInt32(80, vtot/3, true); + for (let rec of recs) { + let { id, matrix, file, varr } = rec; + for (let i=0, l=varr.length; i -- All Rights Reserved */ -"use strict"; - -// dep: data.local -gapp.register("moto.ajax", [], (root, exports) => { +import { local } from '../data/local.js'; function rnd() { return Math.round(Math.random()*0xffffffff).toString(36); } -const { data } = root; -const { local } = data; - const KEY = "moto-ajax"; -const MOID = moto.id = local.getItem(KEY) || (Date.now().toString(36)+rnd()+rnd()); +const MOID = local.getItem(KEY) || (Date.now().toString(36)+rnd()+rnd()); local.setItem(KEY, MOID); @@ -69,14 +63,13 @@ function call(url, handler) { } function restore(id) { - MOID = moto.id = id; + MOID = id; local.setItem(KEY, MOID); } -exports({ - new(cb, rt) { return new Ajax(cb, rt) }, +export const ajax = { + new: (cb, rt) => new Ajax(cb, rt), call, restore, -}); - -}); + id: MOID +}; diff --git a/src/moto/broker.js b/src/moto/broker.js index 6e73a75b..55b4c4a6 100644 --- a/src/moto/broker.js +++ b/src/moto/broker.js @@ -1,15 +1,12 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -gapp.register("moto.broker", [], (root, exports) => { - class Broker { + #debug = false; + #topics = {}; + #used = {}; + #send = {}; constructor() { - this.topics = {}; - this.used = {}; - this.send = {}; // using the pattern "broker.send(msg)" induces runtime errors // when there is not at least one registered listener for a topic // while also allowing for a more natural function call interface @@ -17,7 +14,11 @@ class Broker { } topics() { - return Object.keys(this.topics); + return Object.keys(this.#topics); + } + + get send() { + return this.#send; } // create subscriptions for all functions in an object @@ -42,16 +43,19 @@ class Broker { return this; } if (typeof topic !== 'string') { - console.trace({invalid_topic: topic}); + for (let [ key, fn ] of Object.entries(topic)) { + this.subscribe(key, fn.bind(listener)); + } + // console.trace({invalid_topic: topic}); return; } - let topics = this.topics; + let topics = this.#topics; let channel = topics[topic]; if (!channel) { channel = topics[topic] = []; } if (channel.indexOf(listener) < 0) { - let send = this.send; + let send = this.#send; let name = topic.replace(/[\\ \.-]/g, '_'); send[name] = send[name] = this.bind(topic); channel.push(listener); @@ -61,7 +65,7 @@ class Broker { } unsubscribe(topic, listener) { - let channel = this.topics[topic]; + let channel = this.#topics[topic]; if (!channel) { return; } @@ -71,7 +75,7 @@ class Broker { } channel.splice(index,1); if (channel.length === 0) { - delete this.topics[topic]; + delete this.#topics[topic]; this.publish(".topic.remove", topic); } } @@ -86,10 +90,13 @@ class Broker { if (topic !== ".topic.publish") { this.publish(".topic.publish", {topic, message, options}); } + if (this.#debug && !topic.startsWith(".")) { + console.log({ topic, message }); + } // store last seen message on a topic // acts as a tracker for all used topics - this.used[topic] = message; - let channel = this.topics[topic]; + this.#used[topic] = message; + let channel = this.#topics[topic]; if (channel && channel.length) { for (let listener of channel) { try { @@ -136,10 +143,6 @@ class Broker { } } } - } -const { moto } = root; -gapp.broker = moto.broker = new Broker(); - -}); +export const broker = new Broker(); diff --git a/src/moto/client.js b/src/moto/client.js index 0392739e..f9ea23c2 100644 --- a/src/moto/client.js +++ b/src/moto/client.js @@ -2,9 +2,7 @@ "use strict"; -// dep: add.array -// dep: moto.broker -gapp.register("moto.client", [], (root, exports) => { +import { broker } from './broker.js'; let ccvalue = self.navigator ? navigator.hardwareConcurrency || 1 : 1, ccmax = ccvalue > 3 ? ccvalue - 1 : 1, @@ -22,8 +20,7 @@ function dispatch(topic, message) { } // code is running in the browser / client context -const client = exports({ - +const client = { /** @returns [integer] total number of workers **/ live: () => { return workers.length; @@ -153,7 +150,7 @@ const client = exports({ if (self.createWorker) { return self.createWorker(); } else { - return new Worker(workurl = url); + return new Worker(workurl = url, { type: 'module' }); } }, @@ -191,9 +188,14 @@ const client = exports({ let worker = client.spawn(workurl); worker.run = null; worker.index = i; + worker.onerror = function(e) { + console.log({ worker_error: e }); + } + worker.onmessageerror = function(e) { + console.log({ worker_message_error: e }); + } worker.onmessage = function(e) { let reply = e.data; - // handle special case for task binding if (reply.bind) { client.bind(reply.bind); @@ -208,7 +210,7 @@ const client = exports({ // handle special case for broker publish if (reply.publish) { - gapp.broker.publish(reply.publish, reply.message); + broker.publish(reply.publish, reply.message); return; } @@ -251,7 +253,6 @@ const client = exports({ client.kick(); } +}; -}); - -}); +export { client }; diff --git a/src/moto/license.js b/src/moto/license.js index 768c94d4..ae5ed98c 100644 --- a/src/moto/license.js +++ b/src/moto/license.js @@ -1,27 +1,11 @@ -(function() { +/** Copyright Stewart Allen -- All Rights Reserved */ - const is_self = typeof(self) !== 'undefined'; +const terms = { + COPYRIGHT: "Copyright (C) Stewart Allen - All Rights Reserved", + LICENSE: "See the license.md file included with the source distribution", + VERSION: "4.3.0" +}; - let terms = { - COPYRIGHT: "Copyright (C) Stewart Allen - All Rights Reserved", - LICENSE: "See the license.md file included with the source distribution", - VERSION: (is_self ? self : this).debug_version || "4.2.0" - }; - - if (typeof(module) === 'object') { - module.exports = terms; - } - - // allow license to be required() by app without gapp - if (is_self && self.gapp) { - let app = self.gapp; - app.license = terms.LICENSE; - app.version = terms.VERSION; - app.copyright = terms.COPYRIGHT; - // satisfy resolver - gapp.register('moto.license'); - // for earcut and other modules - (self.module = self.module || {}).exports = terms; - } - -})(); +export const beta = 1; +export const license = terms; +export const version = terms.VERSION; diff --git a/src/moto/orbit.js b/src/moto/orbit.js index ccb2b8e3..893fcc34 100644 --- a/src/moto/orbit.js +++ b/src/moto/orbit.js @@ -2,552 +2,393 @@ "use strict"; -// dep: add.three -gapp.register("moto.orbit", [], (root, exports) => { +import { THREE } from '../ext/three.js'; -const { moto } = root; +const { + Vector3, + Vector2, + Quaternion, + EventDispatcher, + MOUSE +} = THREE; /** * Adapted from THREE.OrbitControls */ -moto.Orbit = function (object, domElement, notify, slider) { +class Orbit extends EventDispatcher { + constructor(object, domElement, notify, slider) { + super(); - this.object = object; - this.domElement = ( domElement !== undefined ) ? domElement : document; + this.object = object; + this.domElement = ( domElement !== undefined ) ? domElement : document; - // Set to false to disable this control - this.enabled = true; + // Set to false to disable this control + this.enabled = true; - // "target" sets the location of focus, where the control orbits around - // and where it pans with respect to. - this.target = new THREE.Vector3(); + // "target" sets the location of focus, where the control orbits around + // and where it pans with respect to. + this.target = new Vector3(); - // center is old, deprecated; use "target" instead - this.center = this.target; + // center is old, deprecated; use "target" instead + this.center = this.target; - // This option actually enables dollying in and out; left as "zoom" for - // backwards compatibility - this.noZoom = false; - this.zoomSpeed = 1.0; - this.reverseZoom = false; + // This option actually enables dollying in and out; left as "zoom" for + // backwards compatibility + this.noZoom = false; + this.zoomSpeed = 1.0; + this.reverseZoom = false; - // Limits to how far you can dolly in and out - this.minDistance = 0; - this.maxDistance = Infinity; + // Limits to how far you can dolly in and out + this.minDistance = 0; + this.maxDistance = Infinity; - // Set to true to disable this control - this.noRotate = false; - this.rotateSpeed = 1.0; + // Set to true to disable this control + this.noRotate = false; + this.rotateSpeed = 1.0; - // Set to true to disable this control - this.noPan = false; - this.keyPanSpeed = 7.0; // pixels moved per arrow key push + // Set to true to disable this control + this.noPan = false; + this.keyPanSpeed = 7.0; // pixels moved per arrow key push - // How far you can orbit vertically, upper and lower limits. - // Range is 0 to Math.PI radians. - this.minPolarAngle = 0; // radians - this.maxPolarAngle = Math.PI; // radians + // How far you can orbit vertically, upper and lower limits. + // Range is 0 to Math.PI radians. + this.minPolarAngle = 0; // radians + this.maxPolarAngle = Math.PI; // radians - // How far you can orbit horizontally, upper and lower limits. - // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. - this.minAzimuthAngle = -Infinity; // radians - this.maxAzimuthAngle = Infinity; // radians + // How far you can orbit horizontally, upper and lower limits. + // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. + this.minAzimuthAngle = -Infinity; // radians + this.maxAzimuthAngle = Infinity; // radians - // Set to true to disable use of the keys - this.noKeys = false; + // Set to true to disable use of the keys + this.noKeys = false; - // The four arrow keys - this.keys = { - UP: 38, - LEFT: 37, - RIGHT: 39, - BOTTOM: 40 - }; + // The four arrow keys + this.keys = { + UP: 38, + LEFT: 37, + RIGHT: 39, + BOTTOM: 40 + }; - // default mouse buttons - this.mouseDefault = { - ORBIT: THREE.MOUSE.LEFT, - ZOOM: THREE.MOUSE.MIDDLE, - PAN: THREE.MOUSE.RIGHT - }; + // default mouse buttons + this.mouseDefault = { + ORBIT: MOUSE.LEFT, + ZOOM: MOUSE.MIDDLE, + PAN: MOUSE.RIGHT + }; - // Onshape - this.mouseOnshape = { - ORBIT: THREE.MOUSE.RIGHT, - ZOOM: THREE.MOUSE.LEFT, - PAN: THREE.MOUSE.MIDDLE - }; + // Onshape + this.mouseOnshape = { + ORBIT: MOUSE.RIGHT, + ZOOM: MOUSE.LEFT, + PAN: MOUSE.MIDDLE + }; - this.mouseButtons = this.mouseDefault; + this.mouseButtons = this.mouseDefault; - this.setMouse = function(bindings) { - this.mouseButtons = bindings; - }; + this.setMouse = function(bindings) { + this.mouseButtons = bindings; + }; - let scope = this, - domEl = scope.domElement, - EPS = 0.000001, - rotateStart = new THREE.Vector2(), - rotateEnd = new THREE.Vector2(), - rotateDelta = new THREE.Vector2(), - panStart = new THREE.Vector2(), - panEnd = new THREE.Vector2(), - panDelta = new THREE.Vector2(), - panOffset = new THREE.Vector3(), - offset = new THREE.Vector3(), - dollyStart = new THREE.Vector2(), - dollyEnd = new THREE.Vector2(), - dollyDelta = new THREE.Vector2(), - theta, - thetaDelta = 0, - thetaSet = null, - phi, - phiDelta = 0, - phiSet = null, - scale = 1, - scaleSave = 1, - pan = new THREE.Vector3(), - lastPosition = new THREE.Vector3(), - lastQuaternion = new THREE.Quaternion(), - // so camera.up is the orbit axis - quat = new THREE.Quaternion().setFromUnitVectors(object.up, new THREE.Vector3(0, 1, 0)), - quatInverse = quat.clone().invert(), - // touch tracking - firstTouch = [], - touchSlide = false, - lastDY = 0, - // events - changeEvent = { type: 'change'}, - startEvent = { type: 'start'}, - endEvent = { type: 'end'}; + let scope = this, + domEl = scope.domElement, + EPS = 0.000001, + rotateStart = new Vector2(), + rotateEnd = new Vector2(), + rotateDelta = new Vector2(), + panStart = new Vector2(), + panEnd = new Vector2(), + panDelta = new Vector2(), + panOffset = new Vector3(), + offset = new Vector3(), + dollyStart = new Vector2(), + dollyEnd = new Vector2(), + dollyDelta = new Vector2(), + theta, + thetaDelta = 0, + thetaSet = null, + phi, + phiDelta = 0, + phiSet = null, + scale = 1, + scaleSave = 1, + pan = new Vector3(), + lastPosition = new Vector3(), + lastQuaternion = new Quaternion(), + // so camera.up is the orbit axis + quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0)), + quatInverse = quat.clone().invert(), + // touch tracking + firstTouch = [], + touchSlide = false, + lastDY = 0, + // events + changeEvent = { type: 'change'}, + startEvent = { type: 'start'}, + endEvent = { type: 'end'}; - let STATE = { NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5}, - state = STATE.NONE, - MODE = { PERSPECTIVE: 1, ORTHOGRAPHIC: 2, UNKNOWN: 3}, - mode = isValue(object.fov) ? MODE.PERSPECTIVE : isValue(object.top) ? MODE.ORTHOGRAPHIC : MODE.UNKNOWN; + let STATE = { NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5}, + state = STATE.NONE, + MODE = { PERSPECTIVE: 1, ORTHOGRAPHIC: 2, UNKNOWN: 3}, + mode = isValue(object.fov) ? MODE.PERSPECTIVE : isValue(object.top) ? MODE.ORTHOGRAPHIC : MODE.UNKNOWN; - // for reset - this.target0 = this.target.clone(); - this.position0 = this.object.position.clone(); - - this.rotateLeft = function (angle) { - thetaDelta -= angle; - }; - - this.rotateUp = function (angle) { - phiDelta -= angle; - }; - - // pass in distance in world space to move left - this.panLeft = function (distance) { - let te = this.object.matrix.elements; - - // get X column of matrix - panOffset.set(te[ 0 ], te[ 1 ], te[ 2 ]); - panOffset.multiplyScalar(-distance); - pan.add(panOffset); - }; - - // pass in distance in world space to move up - this.panUp = function (distance) { - let te = this.object.matrix.elements; - - // get Y column of matrix - panOffset.set(te[ 4 ], te[ 5 ], te[ 6 ]); - panOffset.multiplyScalar(distance); - pan.add(panOffset); - }; - - // pass in distance in world space to move backward - this.panBack = function (distance) { - let te = this.object.matrix.elements; - - // get Y column of matrix - panOffset.set(te[ 8 ], te[ 9 ], te[ 10 ]); - panOffset.multiplyScalar(distance); - pan.add(panOffset); - }; - - // pass in x,y of change desired in pixel space, - // right and down are positive - this.pan = function (deltaX, deltaY) { - let element = scope.domElement === document ? scope.domElement.body : scope.domElement; - - switch (mode) { - case MODE.PERSPECTIVE: - let position = scope.object.position; - let offset = position.clone().sub(scope.target); - let targetDistance = offset.length(); - - // half of the fov is center to top of screen - targetDistance *= Math.tan(( scope.object.fov / 2 ) * Math.PI / 180.0); - - // we actually don't use screenWidth, since perspective camera is fixed to screen height - scope.panLeft(2 * deltaX * targetDistance / element.clientHeight); - scope.panUp(2 * deltaY * targetDistance / element.clientHeight); - break; - case MODE.ORTHOGRAPHIC: - scope.panLeft(deltaX * (scope.object.right - scope.object.left) / element.clientWidth); - scope.panUp(deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight); - break; - } - }; - - this.getTarget = function() { - return this.target; - }; - - this.setTarget = function(t) { - this.target = new THREE.Vector3().add(t); + // for reset this.target0 = this.target.clone(); - } + this.position0 = this.object.position.clone(); - function isValue(v) { - return v !== null && v !== undefined; - } + this.rotateLeft = function (angle) { + thetaDelta -= angle; + }; - function firstValue(choices) { - for (let i=0; i EPS - // using small-angle approximation cos(x/2) = 1 - x^2 / 8 - if (lastPosition.distanceToSquared(this.object.position) > EPS - || 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS) { - - this.dispatchEvent(changeEvent); - lastPosition.copy(this.object.position); - lastQuaternion.copy(this.object.quaternion); - if (notify) notify(position, true); - } else { - if (notify) notify(position, false); - } - }; - - this.reset = function () { - state = STATE.NONE; - scale = 1; - scaleSave = 1; - this.target.copy(this.target0); - this.object.position.copy(this.position0); - }; - - this.getPolarAngle = function () { - return phi; - }; - - this.getAzimuthalAngle = function () { - return theta - }; - - function getZoomScale(factor = 1) { - return Math.pow(0.95, factor * scope.zoomSpeed); - } - - function onMouseDown(event) { - if (scope.enabled === false) return; - event.preventDefault(); - - switch (event.button) { - case scope.mouseButtons.ORBIT: - state = event.metaKey ? STATE.PAN : STATE.ROTATE; - break; - case scope.mouseButtons.ZOOM: - state = STATE.DOLLY; - break; - case scope.mouseButtons.PAN: - state = STATE.PAN; - break; + function firstValue(choices) { + for (let v of choices) { + if (isValue(v)) return v; + } + return null; } - switch (state) { - case STATE.ROTATE: - if (scope.noRotate === true) return state = STATE.NONE; - rotateStart.set(event.clientX, event.clientY); - break; - case STATE.DOLLY: - if (scope.noZoom === true) return state = STATE.NONE; - dollyStart.set(event.clientX, event.clientY); - break; - case STATE.PAN: - if (scope.noPan === true) return state = STATE.NONE; - panStart.set(event.clientX, event.clientY); - break; - } + this.setZoom = function(reverse, speed) { + scope.reverseZoom = reverse; + scope.zoomSpeed = speed || 1.0; + }; - document.addEventListener('mousemove', onMouseMove, false); - document.addEventListener('mouseup', onMouseUp, false); - scope.dispatchEvent(startEvent); - } + this.setPosition = function(set) { + thetaSet = firstValue([set.left, set.theta, thetaSet]); + phiSet = firstValue([set.up, set.phi, phiSet]); + if (set.panX) this.target.x = set.panX; + if (set.panY) this.target.y = set.panY; + if (set.panZ) this.target.z = set.panZ; + if (set.scale) scale = set.scale; + }; - function onMouseMove(event) { - if (scope.enabled === false) return; - event.preventDefault(); + this.getPosition = function(scaled) { + let t = this.target, + pos = { left:theta, up:phi, panX:t.x, panY:t.y, panZ:t.z, scale:scaled ? scaleSave : 1 }; + return pos; + }; - let element = scope.domElement === document ? scope.domElement.body : scope.domElement; + this.dollyIn = function (factor = 1) { + let dollyScale = getZoomScale(factor); + scale = scale * dollyScale; + }; - if (state === STATE.ROTATE) { - if (scope.noRotate === true) return; + this.dollyOut = function (factor = 1) { + let dollyScale = getZoomScale(factor); + scale = scale / dollyScale; + }; - rotateEnd.set(event.clientX, event.clientY); - rotateDelta.subVectors(rotateEnd, rotateStart); + this.update = function () { + let position = this.object.position; - // rotating across whole screen goes 360 degrees around - scope.rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed); - // rotating up and down along whole screen attempts to go 360, but limited to 180 - scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed); + offset.copy(position).sub(this.target); - rotateStart.copy(rotateEnd); + // rotate offset to "y-axis-is-up" space + offset.applyQuaternion(quat); - } else if (state === STATE.DOLLY) { - if (scope.noZoom === true) return; + // angle from z-axis around y-axis + theta = isValue(thetaSet) ? thetaSet : Math.atan2(offset.x, offset.z); - dollyEnd.set(event.clientX, event.clientY); - dollyDelta.subVectors(dollyEnd, dollyStart); + // angle from y-axis + phi = isValue(phiSet) ? phiSet : Math.atan2(Math.sqrt(offset.x * offset.x + offset.z * offset.z), offset.y); - if (dollyDelta.y > 0) { - scope.dollyIn(); + theta += thetaDelta; + phi += phiDelta; + + // restrict theta to be between desired limits + theta = Math.max(this.minAzimuthAngle, Math.min(this.maxAzimuthAngle, theta)); + + // restrict phi to be between desired limits + phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, phi)); + + // restrict phi to be betwee EPS and PI-EPS + phi = Math.max(EPS, Math.min(Math.PI - EPS, phi)); + + let radius = offset.length() * (mode === MODE.PERSPECTIVE ? scale : 1); + + // restrict radius to be between desired limits + radius = Math.max(this.minDistance, Math.min(this.maxDistance, radius)); + + // move target to panned location + this.target.add(pan); + + offset.x = radius * Math.sin(phi) * Math.sin(theta); + offset.y = radius * Math.cos(phi); + offset.z = radius * Math.sin(phi) * Math.cos(theta); + + // rotate offset back to "camera-up-vector-is-up" space + offset.applyQuaternion(quatInverse); + + position.copy(this.target).add(offset); + this.object.lookAt(this.target); + + thetaDelta = 0; + thetaSet = null; + phiDelta = 0; + phiSet = null; + pan.set(0, 0, 0); + + scaleSave *= scale; + + if (mode === MODE.ORTHOGRAPHIC) { + scope.object.zoom = 1/scale; + scope.object.updateProjectionMatrix(); } else { - scope.dollyOut(); + scale = 1; } - dollyStart.copy(dollyEnd); + // update condition is: + // min(camera displacement, camera rotation in radians)^2 > EPS + // using small-angle approximation cos(x/2) = 1 - x^2 / 8 + if (lastPosition.distanceToSquared(this.object.position) > EPS + || 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS) { - } else if (state === STATE.PAN) { - if (scope.noPan === true) return; + this.dispatchEvent(changeEvent); + lastPosition.copy(this.object.position); + lastQuaternion.copy(this.object.quaternion); + if (notify) notify(position, true); + } else { + if (notify) notify(position, false); + } + }; - panEnd.set(event.clientX, event.clientY); - panDelta.subVectors(panEnd, panStart); - scope.pan(panDelta.x, panDelta.y); - panStart.copy(panEnd); + this.reset = function () { + state = STATE.NONE; + scale = 1; + scaleSave = 1; + this.target.copy(this.target0); + this.object.position.copy(this.position0); + }; + + this.getPolarAngle = function () { + return phi; + }; + + this.getAzimuthalAngle = function () { + return theta + }; + + function getZoomScale(factor = 1) { + return Math.pow(0.95, factor); } - scope.update(); - } + function onMouseDown(event) { + if (scope.enabled === false) return; + event.preventDefault(); - function onMouseUp(event) { - if (scope.enabled === false) return; + switch (event.button) { + case scope.mouseButtons.ORBIT: + state = event.metaKey ? STATE.PAN : STATE.ROTATE; + break; + case scope.mouseButtons.ZOOM: + state = STATE.DOLLY; + break; + case scope.mouseButtons.PAN: + state = STATE.PAN; + break; + } - document.removeEventListener('mousemove', onMouseMove, false); - document.removeEventListener('mouseup', onMouseUp, false); - scope.dispatchEvent(endEvent); - state = STATE.NONE; - } + switch (state) { + case STATE.ROTATE: + if (scope.noRotate === true) return state = STATE.NONE; + rotateStart.set(event.clientX, event.clientY); + break; + case STATE.DOLLY: + if (scope.noZoom === true) return state = STATE.NONE; + dollyStart.set(event.clientX, event.clientY); + break; + case STATE.PAN: + if (scope.noPan === true) return state = STATE.NONE; + panStart.set(event.clientX, event.clientY); + break; + } - function onMouseWheel(event) { - if (scope.enabled === false || scope.noZoom === true) return; - - event.preventDefault(); - event.stopPropagation(); - - if (event.shiftKey && slider) { - slider(event.deltaX || event.wheelDelta || event.detail); - return; + document.addEventListener('mousemove', onMouseMove, false); + document.addEventListener('mouseup', onMouseUp, false); + scope.dispatchEvent(startEvent); } - slider(null); + function onMouseMove(event) { + if (scope.enabled === false) return; + event.preventDefault(); - let delta = -(event.deltaY || event.wheelDelta || event.detail || 0); + let element = scope.domElement === document ? scope.domElement.body : scope.domElement; - if (delta === 0) return; - - if (scope.reverseZoom) { - delta = -delta; - } - - if (delta > 0) { - scope.dollyOut(); - } else if (delta < 0) { - scope.dollyIn(); - } - - scope.update(); - scope.dispatchEvent(startEvent); - scope.dispatchEvent(endEvent); - } - - function onKeyDown(event) { - if (scope.enabled === false || scope.noKeys === true || scope.noPan === true) return; - - switch (event.keyCode) { - - case scope.keys.UP: - scope.pan(0, scope.keyPanSpeed); - scope.update(); - break; - - case scope.keys.BOTTOM: - scope.pan(0, -scope.keyPanSpeed); - scope.update(); - break; - - case scope.keys.LEFT: - scope.pan(scope.keyPanSpeed, 0); - scope.update(); - break; - - case scope.keys.RIGHT: - scope.pan(-scope.keyPanSpeed, 0); - scope.update(); - break; - } - } - - function touchstart(event) { - if (scope.enabled === false) return; - - const touches = [...event.touches]; - - switch (event.touches.length) { - case 1: // one-fingered touch: rotate + if (state === STATE.ROTATE) { if (scope.noRotate === true) return; - state = STATE.TOUCH_ROTATE; - - rotateStart.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); - break; - - case 2: // two-fingered touch: dolly - if (scope.noZoom === true) return; - - state = STATE.TOUCH_DOLLY; - - let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; - let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; - let distance = Math.sqrt(dx * dx + dy * dy); - - firstTouch = [...event.touches].map(v => { return {x:v.pageX, y:v.pageY} }); - touchSlide = false; - lastDY = 0; - - dollyStart.set(0, distance); - break; - - case 3: // three-fingered touch: pan - case 4: // four-fingered touch: pan - if (scope.noPan === true) return; - - state = STATE.TOUCH_PAN; - const px = touches.map(t => t.pageX).reduce((a,b) => a+b) / touches.length; - const py = touches.map(t => t.pageY).reduce((a,b) => a+b) / touches.length; - - panStart.set(px, py); - break; - - default: - state = STATE.NONE; - } - scope.dispatchEvent(startEvent); - } - - function touchmove(event) { - if (scope.enabled === false) return; - - event.preventDefault(); - event.stopPropagation(); - - const element = scope.domElement === document ? scope.domElement.body : scope.domElement; - - switch (event.touches.length) { - case 1: // one-fingered touch: rotate - if (scope.noRotate === true) return; - if (state !== STATE.TOUCH_ROTATE) return; - - rotateEnd.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); + rotateEnd.set(event.clientX, event.clientY); rotateDelta.subVectors(rotateEnd, rotateStart); // rotating across whole screen goes 360 degrees around @@ -556,83 +397,246 @@ moto.Orbit = function (object, domElement, notify, slider) { scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed); rotateStart.copy(rotateEnd); - scope.update(); - break; - case 2: // two-fingered touch: dolly + } else if (state === STATE.DOLLY) { if (scope.noZoom === true) return; - if (state !== STATE.TOUCH_DOLLY) return; - let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; - let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; - let distance = Math.sqrt(dx * dx + dy * dy); - - let touches = [...event.touches].map(v => { return {x:v.pageX, y:v.pageY} }); - let ddx = ( (touches[0].x - firstTouch[0].x) + (touches[1].x - firstTouch[1].x) ) / 2; - let ddy = ( (touches[0].y - firstTouch[0].y) + (touches[1].y - firstTouch[1].y) ) / 2; - - if (scope.reverseZoom) distance = -distance; - - dollyEnd.set(0, distance); + dollyEnd.set(event.clientX, event.clientY); dollyDelta.subVectors(dollyEnd, dollyStart); - if (touchSlide || (Math.abs(dy/dx) < 1 && slider && dx < 200)) { - touchSlide = true; - slider(lastDY ? (lastDY - ddy) : ddy); - lastDY = ddy; + if (dollyDelta.y > 0) { + scope.dollyIn(); } else { - if (dollyDelta.y > 0) { - scope.dollyOut(0.5); - } else if (dollyDelta.y < 0) { - scope.dollyIn(0.5); - } + scope.dollyOut(); } dollyStart.copy(dollyEnd); - scope.update(); - break; - case 3: // three-fingered touch: pan - case 4: // four-fingered touch: pan + } else if (state === STATE.PAN) { if (scope.noPan === true) return; - if (state !== STATE.TOUCH_PAN) return; - const tlist = [...event.touches]; - const px = tlist.map(t => t.pageX).reduce((a,b) => a+b) / tlist.length; - const py = tlist.map(t => t.pageY).reduce((a,b) => a+b) / tlist.length; - - panEnd.set(px, py); + panEnd.set(event.clientX, event.clientY); panDelta.subVectors(panEnd, panStart); scope.pan(panDelta.x, panDelta.y); panStart.copy(panEnd); + } - scope.update(); - break; - - default: - state = STATE.NONE; + scope.update(); } + + function onMouseUp(event) { + if (scope.enabled === false) return; + + document.removeEventListener('mousemove', onMouseMove, false); + document.removeEventListener('mouseup', onMouseUp, false); + scope.dispatchEvent(endEvent); + state = STATE.NONE; + } + + function onMouseWheel(event) { + if (scope.enabled === false || scope.noZoom === true) return; + + event.preventDefault(); + event.stopPropagation(); + + if (event.shiftKey && slider) { + slider(event.deltaX || event.wheelDelta || event.detail); + return; + } + + slider(null); + + let delta = -(event.deltaY || event.wheelDelta || event.detail || 0); + + if (delta === 0) return; + + if (scope.reverseZoom) { + delta = -delta; + } + + if (delta > 0) { + scope.dollyOut(); + } else if (delta < 0) { + scope.dollyIn(); + } + + scope.update(); + scope.dispatchEvent(startEvent); + scope.dispatchEvent(endEvent); + } + + function onKeyDown(event) { + if (scope.enabled === false || scope.noKeys === true || scope.noPan === true) return; + + switch (event.keyCode) { + + case scope.keys.UP: + scope.pan(0, scope.keyPanSpeed); + scope.update(); + break; + + case scope.keys.BOTTOM: + scope.pan(0, -scope.keyPanSpeed); + scope.update(); + break; + + case scope.keys.LEFT: + scope.pan(scope.keyPanSpeed, 0); + scope.update(); + break; + + case scope.keys.RIGHT: + scope.pan(-scope.keyPanSpeed, 0); + scope.update(); + break; + } + } + + function touchstart(event) { + if (scope.enabled === false) return; + + const touches = [...event.touches]; + + switch (event.touches.length) { + case 1: // one-fingered touch: rotate + if (scope.noRotate === true) return; + + state = STATE.TOUCH_ROTATE; + + rotateStart.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); + break; + + case 2: // two-fingered touch: dolly + if (scope.noZoom === true) return; + + state = STATE.TOUCH_DOLLY; + + let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + let distance = Math.sqrt(dx * dx + dy * dy); + + firstTouch = [...event.touches].map(v => { return {x:v.pageX, y:v.pageY} }); + touchSlide = false; + lastDY = 0; + + dollyStart.set(0, distance); + break; + + case 3: // three-fingered touch: pan + case 4: // four-fingered touch: pan + if (scope.noPan === true) return; + + state = STATE.TOUCH_PAN; + const px = touches.map(t => t.pageX).reduce((a,b) => a+b) / touches.length; + const py = touches.map(t => t.pageY).reduce((a,b) => a+b) / touches.length; + + panStart.set(px, py); + break; + + default: + state = STATE.NONE; + } + scope.dispatchEvent(startEvent); + } + + function touchmove(event) { + if (scope.enabled === false) return; + + event.preventDefault(); + event.stopPropagation(); + + const element = scope.domElement === document ? scope.domElement.body : scope.domElement; + + switch (event.touches.length) { + case 1: // one-fingered touch: rotate + if (scope.noRotate === true) return; + if (state !== STATE.TOUCH_ROTATE) return; + + rotateEnd.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); + rotateDelta.subVectors(rotateEnd, rotateStart); + + // rotating across whole screen goes 360 degrees around + scope.rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed); + // rotating up and down along whole screen attempts to go 360, but limited to 180 + scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed); + + rotateStart.copy(rotateEnd); + scope.update(); + break; + + case 2: // two-fingered touch: dolly + if (scope.noZoom === true) return; + if (state !== STATE.TOUCH_DOLLY) return; + + let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + let distance = Math.sqrt(dx * dx + dy * dy); + + let touches = [...event.touches].map(v => { return {x:v.pageX, y:v.pageY} }); + let ddx = ( (touches[0].x - firstTouch[0].x) + (touches[1].x - firstTouch[1].x) ) / 2; + let ddy = ( (touches[0].y - firstTouch[0].y) + (touches[1].y - firstTouch[1].y) ) / 2; + + if (scope.reverseZoom) distance = -distance; + + dollyEnd.set(0, distance); + dollyDelta.subVectors(dollyEnd, dollyStart); + + if (touchSlide || (Math.abs(dy/dx) < 1 && slider && dx < 200)) { + touchSlide = true; + slider(lastDY ? (lastDY - ddy) : ddy); + lastDY = ddy; + } else { + if (dollyDelta.y > 0) { + scope.dollyOut(0.5); + } else if (dollyDelta.y < 0) { + scope.dollyIn(0.5); + } + } + + dollyStart.copy(dollyEnd); + scope.update(); + break; + + case 3: // three-fingered touch: pan + case 4: // four-fingered touch: pan + if (scope.noPan === true) return; + if (state !== STATE.TOUCH_PAN) return; + + const tlist = [...event.touches]; + const px = tlist.map(t => t.pageX).reduce((a,b) => a+b) / tlist.length; + const py = tlist.map(t => t.pageY).reduce((a,b) => a+b) / tlist.length; + + panEnd.set(px, py); + panDelta.subVectors(panEnd, panStart); + scope.pan(panDelta.x, panDelta.y); + panStart.copy(panEnd); + + scope.update(); + break; + + default: + state = STATE.NONE; + } + } + + function touchend(/* event */) { + if (scope.enabled === false) return; + scope.dispatchEvent(endEvent); + state = STATE.NONE; + } + + this.onMouseUp = onMouseUp; + + domEl.addEventListener('contextmenu', function (event) { event.preventDefault() }, false); + domEl.addEventListener('mousedown', onMouseDown, false); + domEl.addEventListener('mousewheel', onMouseWheel, false); + domEl.addEventListener('DOMMouseScroll', onMouseWheel, false); // firefox + domEl.addEventListener('touchstart', touchstart, false); + domEl.addEventListener('touchend', touchend, false); + domEl.addEventListener('touchmove', touchmove, false); + + window.addEventListener('keydown', onKeyDown, false); } +} - function touchend(/* event */) { - if (scope.enabled === false) return; - scope.dispatchEvent(endEvent); - state = STATE.NONE; - } - - this.onMouseUp = onMouseUp; - - domEl.addEventListener('contextmenu', function (event) { event.preventDefault() }, false); - domEl.addEventListener('mousedown', onMouseDown, false); - domEl.addEventListener('mousewheel', onMouseWheel, false); - domEl.addEventListener('DOMMouseScroll', onMouseWheel, false); // firefox - domEl.addEventListener('touchstart', touchstart, false); - domEl.addEventListener('touchend', touchend, false); - domEl.addEventListener('touchmove', touchmove, false); - - window.addEventListener('keydown', onKeyDown, false); -}; - -moto.Orbit.prototype = Object.create(THREE.EventDispatcher.prototype); - -}); +export { Orbit }; diff --git a/src/moto/service.js b/src/moto/service.js index 7c8e1f3b..bc5b9669 100644 --- a/src/moto/service.js +++ b/src/moto/service.js @@ -1,6 +1,7 @@ // code for a ServiceWorker that provides support for running cached/offline -const version = self.gapp.version; +import { version } from './license.js'; + const origin = self.location.origin; const stats = { hit: 0, diff --git a/src/moto/space.js b/src/moto/space.js index cb2e1a8e..54a7c4d4 100644 --- a/src/moto/space.js +++ b/src/moto/space.js @@ -1,137 +1,141 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { broker } from './broker.js'; +import { THREE } from '../ext/three.js'; +import { Orbit } from './orbit.js'; +import '../ext/tween.js'; -// dep: ext.tween -// dep: add.three -// dep: add.array -// dep: moto.orbit -gapp.register("moto.space", [], (root, exports) => { +const { + WebGLRenderer, + Scene, + Group, + MeshPhongMaterial, + DoubleSide +} = THREE; - const { moto } = root; - const { WebGLRenderer } = THREE; - const nav = navigator; +const nav = navigator; - let WIN = window, - DOC = document, - SCENE = new THREE.Scene(), - WORLD = new THREE.Group(), - PI = Math.PI, - PI2 = PI / 2, - PI4 = PI / 4, - panX = 0, - panY = 0, - panZ = 0, - home = 0, - up = PI4, - gridZOff = 0, - tweenTime = 500, - tweenDelay = 20, - platformZOff = 0, - perspective = 35, - refreshTimeout = null, - refreshRequested = false, - selectRecurse = false, - defaultKeys = true, - initialized = false, - alignedTracking = false, - skyAmbient, - skyGridColor = 0xcccccc, - skyGridMaterial = undefined, - showSkyGrid = false, - showPlatform = true, - hidePlatformBelow = true, - hideGridBelow = false, - showGrid = true, - showGridLines = true, - showFocus = 0, - focalPoint = undefined, - lightInfo = { - mode: 2, - array: [], - intensity: 0.09, - debug: false - }, - cameraLight = addLight(0, 0, 0, lightInfo.intensity), - origin = {x:0, y:0, z: 0}, - mouse = {x: 0, y: 0}, - mouseStart = null, - mouseDragPoint = null, - mouseDragStart = null, - mouseDownSelect, - mouseUpSelect, - mouseUp, - mouseDown, - mouseHover, - mouseDrag, - grid = { - origin: origin, - unitMinor: 0, - unitMajor: 0, - colorMinor: 0xeeeeee, - colorMajor: 0xcccccc, - colorX: 0xff6666, - colorY: 0x6666ff, - zoff: 0, - opacity: 1, - view: undefined, - axes: undefined, - lines: undefined - }, - ruler = { - x1: 0, - x2: 0, - y1: 0, - y2: 0, - xlabel: 'X', - ylabel: 'Y', - xon: undefined, - yon: undefined, - factor: undefined, - view: undefined - }, - psize = {}, - timers = {}, - fontColor = '#333333', - fontScale = 1.4, // computed relative to grid size - rulerColor, - axisColor, - axesOn = true, - volumeOn = true, - viewControl, - trackPlane, - platform, - platformHover, - platformClick, - platformClickAt, - platformOnMove, - platformOnMoveTime = 500, - platformMoveTimer, - platformBelt = false, - volume, - camera, - renderer, - container, - raycaster, - freezeTo, - freeze, - isRound = false, - platformMaterial = new THREE.MeshPhongMaterial({ - color: 0xeeeeee, - specular: 0xeeeeee, - shininess: 0, - transparent: true, - opacity: 0.6, - side: THREE.DoubleSide - }), - hiddenKey, - vizChange, - docVisible = true, - antiAlias = window.devicePixelRatio <= 1, - lastAction = Date.now(), - renderTime = 0, - fps = 0; +let WIN = self.window || {}, + DOC = self.document, + SCENE = new Scene(), + WORLD = new Group(), + PI = Math.PI, + PI2 = PI / 2, + PI4 = PI / 4, + panX = 0, + panY = 0, + panZ = 0, + home = 0, + up = PI4, + gridZOff = 0, + tweenTime = 500, + tweenDelay = 20, + platformZOff = 0, + perspective = 35, + refreshTimeout = null, + refreshRequested = false, + selectRecurse = false, + defaultKeys = true, + initialized = false, + alignedTracking = false, + skyAmbient, + skyGridColor = 0xcccccc, + skyGridMaterial = undefined, + showSkyGrid = false, + showPlatform = true, + hidePlatformBelow = true, + hideGridBelow = false, + showGrid = true, + showGridLines = true, + showFocus = 0, + focalPoint = undefined, + lightInfo = { + mode: 2, + array: [], + intensity: 0.09, + debug: false + }, + cameraLight = addLight(0, 0, 0, lightInfo.intensity), + origin = {x:0, y:0, z: 0}, + mouse = {x: 0, y: 0}, + mouseStart = null, + mouseDragPoint = null, + mouseDragStart = null, + mouseDownSelect, + mouseUpSelect, + mouseUp, + mouseDown, + mouseHover, + mouseDrag, + grid = { + origin: origin, + unitMinor: 0, + unitMajor: 0, + colorMinor: 0xeeeeee, + colorMajor: 0xcccccc, + colorX: 0xff6666, + colorY: 0x6666ff, + zoff: 0, + opacity: 1, + view: undefined, + axes: undefined, + lines: undefined + }, + ruler = { + x1: 0, + x2: 0, + y1: 0, + y2: 0, + xlabel: 'X', + ylabel: 'Y', + xon: undefined, + yon: undefined, + factor: undefined, + view: undefined + }, + psize = {}, + timers = {}, + fontColor = '#333333', + fontScale = 1.4, // computed relative to grid size + rulerColor, + axisColor, + axesOn = true, + volumeOn = true, + viewControl, + trackPlane, + platform, + platformHover, + platformClick, + platformClickAt, + platformOnMove, + platformOnMoveTime = 500, + platformMoveTimer, + platformBelt = false, + volume, + camera, + renderer, + container, + raycaster, + freezeTo, + freeze, + isRound = false, + platformMaterial = new MeshPhongMaterial({ + color: 0xeeeeee, + specular: 0xeeeeee, + shininess: 0, + transparent: true, + opacity: 0.6, + side: DoubleSide + }), + hiddenKey, + vizChange, + docVisible = true, + antiAlias = WIN.devicePixelRatio <= 1, + lastAction = Date.now(), + renderTime = 0, + fps = 0; +if (DOC) { if (typeof DOC.hidden !== "undefined") { hiddenKey = "hidden"; vizChange = "visibilitychange"; @@ -146,1365 +150,1368 @@ gapp.register("moto.space", [], (root, exports) => { DOC.addEventListener(vizChange, () => { docVisible = DOC[hiddenKey] ? false : true; }, false); +} - function updateLastAction() { - lastAction = Date.now(); - } +function updateLastAction() { + lastAction = Date.now(); +} - function delayed(key, time, fn) { - clearTimeout(timers[key]); - timers[key] = setTimeout(fn, time); - } +function delayed(key, time, fn) { + clearTimeout(timers[key]); + timers[key] = setTimeout(fn, time); +} - function valueOr(val, def) { - return val !== undefined ? val : def; - } +function valueOr(val, def) { + return val !== undefined ? val : def; +} - WORLD.contains = (obj) => { - return WORLD.children.contains(obj); - }; +WORLD.contains = (obj) => { + return WORLD.children.contains(obj); +}; - /** ****************************************************************** - * TWEENing Functions - ******************************************************************* */ +/** ****************************************************************** + * TWEENing Functions + ******************************************************************* */ - function tweenit() { - TWEEN.update(); - setTimeout(tweenit, tweenDelay); - } +function tweenit() { + self.TWEEN?.update(); + setTimeout(tweenit, tweenDelay); +} - tweenit(); +tweenit(); - function tweenCamPan(x,y,z,left,up) { +function tweenCamPan(x,y,z,left,up) { + updateLastAction(); + let pos = viewControl.getPosition(); + pos.panX = x; + pos.panY = y; + pos.panZ = z; + if (left !== undefined) pos.left = left; + if (up !== undefined) pos.up = up; + tweenCam(pos); +} + +function tweenCam(pos) { + let tf = function () { + viewControl.setPosition(this); updateLastAction(); - let pos = viewControl.getPosition(); - pos.panX = x; - pos.panY = y; - pos.panZ = z; - if (left !== undefined) pos.left = left; - if (up !== undefined) pos.up = up; - tweenCam(pos); + refresh(); + }; + let from = Object.clone(viewControl.getPosition()); + let to = Object.clone(pos); + let dist = Math.abs(from.left - to.left); + if (dist > Math.PI) { + if (from.left < to.left) { + from.left += Math.PI * 2; + } else { + from.left -= Math.PI * 2; + } } - - function tweenCam(pos) { - let tf = function () { - viewControl.setPosition(this); + new TWEEN.Tween(from). + to(to, tweenTime). + onUpdate(tf). + onComplete(() => { + viewControl.setPosition(pos); updateLastAction(); refresh(); - }; - let from = Object.clone(viewControl.getPosition()); - let to = Object.clone(pos); - let dist = Math.abs(from.left - to.left); - if (dist > Math.PI) { - if (from.left < to.left) { - from.left += Math.PI * 2; - } else { - from.left -= Math.PI * 2; + let { then } = pos; + if (typeof then === 'function') { + then(); } - } - new TWEEN.Tween(from). - to(to, tweenTime). - onUpdate(tf). - onComplete(() => { - viewControl.setPosition(pos); - updateLastAction(); - refresh(); - let { then } = pos; - if (typeof then === 'function') { - then(); - } - }). - start(); + }). + start(); +} + +/** ****************************************************************** + * Utility Functions + ******************************************************************* */ + +function width() { return WIN.innerWidth } + +function height() { return WIN.innerHeight } + +function aspect() { return width() / height() } + +function addEventListener(el, key, fn) { + el.addEventListener(key, fn); +} + +function addEventHandlers(el, pairs) { + for (let i=0; i { freeze = undefined }, 10); + if (maxz) { + const points = [ + // pillars + {x: -width/2, z: -depth/2, y: 0}, + {x: -width/2, z: -depth/2, y: maxz}, + {x: width/2, z: depth/2, y: 0}, + {x: width/2, z: depth/2, y: maxz}, + {x: -width/2, z: depth/2, y: 0}, + {x: -width/2, z: depth/2, y: maxz}, + {x: width/2, z: -depth/2, y: 0}, + {x: width/2, z: -depth/2, y: maxz}, + // top + {x: -width/2, z: -depth/2, y: maxz}, + {x: -width/2, z: depth/2, y: maxz}, + {x: -width/2, z: -depth/2, y: maxz}, + {x: width/2, z: -depth/2, y: maxz}, + {x: width/2, z: depth/2, y: maxz}, + {x: width/2, z: -depth/2, y: maxz}, + {x: width/2, z: depth/2, y: maxz}, + {x: -width/2, z: depth/2, y: maxz}, + ]; + SCENE.add(volume = makeLinesFromPoints(points, grid.colorMinor)); + showVolume(volumeOn); } +} - function setPlatformColor(color) { - let was = platform.material.color.getHex(); - platform.material.color.set(color); - requestRefresh(); - return was; +function setPlatformSizeUpdateGrid(width, depth, height, maxz) { + freeze = { width, depth, height, maxz }; + setPlatformSize(width, depth, height, maxz); + setGrid(grid.unitMajor, grid.unitMinor); + clearTimeout(freezeTo); + freezeTo = setTimeout(() => { freeze = undefined }, 10); +} + +function setPlatformColor(color) { + let was = platform.material.color.getHex(); + platform.material.color.set(color); + requestRefresh(); + return was; +} + +function setFont(options) { + if (options.color) fontColor = options.color; + if (options.scale) fontScale = options.scale; + if (options.axisColor) axisColor = options.axisColor; + if (options.rulerColor) rulerColor = options.rulerColor; + updateRulers(); +} + +function showAxes(bool) { + axesOn = bool; + updateRulers(); +} + +function showVolume(bool) { + volumeOn = bool; + if (volume) volume.visible = bool; + requestRefresh(); +} + +function canvasInMesh(w, h, textAlign, textBaseline, color, size) { + let canvas = document.createElement('canvas'), + canvasTexture = new THREE.CanvasTexture(canvas), + plane = new THREE.PlaneGeometry(w, h), + context = canvas.getContext('2d'), + scale = 8; + + canvas.width = w * scale; + canvas.height = h * scale; + + canvas.addEventListener("webglcontextlost", event => { + console.log('WEB GL CONTEXT LOST'); + event.preventDefault(); + }, false); + + canvas.addEventListener("webglcontextrestored", event => { + console.log('WEB GL CONTEXT RESTORED'); + }, false); + + try { + context.scale(scale, scale); + } catch (e) { + console.log('unable to create label canvas', e ? e.name : 'unknown'); + return {}; } + context.fillStyle = color || fontColor; + context.font = `${size}px sans-serif`; + context.textAlign = textAlign; + context.textBaseline = textBaseline; + canvasTexture.minFilter = THREE.LinearFilter; - function setFont(options) { - if (options.color) fontColor = options.color; - if (options.scale) fontScale = options.scale; - if (options.axisColor) axisColor = options.axisColor; - if (options.rulerColor) rulerColor = options.rulerColor; + // set 'transparent' to false to debug mesh bounds + let material = new THREE.MeshBasicMaterial({transparent: true, map: canvasTexture}); + let mesh = new THREE.Mesh(plane, material); + + return { context, mesh }; +} + + +function setRulers( + xon = ruler.xon, + yon = ruler.yon, + factor = ruler.factor || 1, + xl = ruler.xlabel || 'X', + yl = ruler.ylabel || 'Y') +{ + if (xon !== ruler.xon || yon !== ruler.yon || factor !== ruler.factor || xl !== ruler.xlabel || yl !== ruler.ylabel) { + ruler.factor = factor; + ruler.xon = xon; + ruler.yon = yon; + ruler.xlabel = xl; + ruler.ylabel = yl; updateRulers(); } +} - function showAxes(bool) { - axesOn = bool; - updateRulers(); - } +function updateRulers() { + let { xon, yon, factor, xlabel, ylabel } = ruler; + let x = platform.scale.x, + y = isRound ? platform.scale.z : platform.scale.y, + z = isRound ? platform.scale.y : platform.scale.z, + w = x / 2, + h = y / 2, + d = z / 2, + zp = -d - platformZOff + numOrDef(gridZOff, (z/2-0.1)), + labelSize = grid.unitMinor * fontScale, + oldView = ruler.view, + view = ruler.view = new THREE.Group(); - function showVolume(bool) { - volumeOn = bool; - if (volume) volume.visible = bool; - requestRefresh(); - } - - function canvasInMesh(w, h, textAlign, textBaseline, color, size) { - let canvas = document.createElement('canvas'), - canvasTexture = new THREE.CanvasTexture(canvas), - plane = new THREE.PlaneGeometry(w, h), - context = canvas.getContext('2d'), - scale = 8; - - canvas.width = w * scale; - canvas.height = h * scale; - - canvas.addEventListener("webglcontextlost", event => { - console.log('WEB GL CONTEXT LOST'); - event.preventDefault(); - }, false); - - canvas.addEventListener("webglcontextrestored", event => { - console.log('WEB GL CONTEXT RESTORED'); - }, false); - - try { - context.scale(scale, scale); - } catch (e) { - console.log('unable to create label canvas', e ? e.name : 'unknown'); - return {}; - } - context.fillStyle = color || fontColor; - context.font = `${size}px sans-serif`; - context.textAlign = textAlign; - context.textBaseline = textBaseline; - canvasTexture.minFilter = THREE.LinearFilter; - - // set 'transparent' to false to debug mesh bounds - let material = new THREE.MeshBasicMaterial({transparent: true, map: canvasTexture}); - let mesh = new THREE.Mesh(plane, material); - - return { context, mesh }; - } - - - function setRulers( - xon = ruler.xon, - yon = ruler.yon, - factor = ruler.factor || 1, - xl = ruler.xlabel || 'X', - yl = ruler.ylabel || 'Y') - { - if (xon !== ruler.xon || yon !== ruler.yon || factor !== ruler.factor || xl !== ruler.xlabel || yl !== ruler.ylabel) { - ruler.factor = factor; - ruler.xon = xon; - ruler.yon = yon; - ruler.xlabel = xl; - ruler.ylabel = yl; - updateRulers(); - } - } - - function updateRulers() { - let { xon, yon, factor, xlabel, ylabel } = ruler; - let x = platform.scale.x, - y = isRound ? platform.scale.z : platform.scale.y, - z = isRound ? platform.scale.y : platform.scale.z, - w = x / 2, - h = y / 2, - d = z / 2, - zp = -d - platformZOff + numOrDef(gridZOff, (z/2-0.1)), - labelSize = grid.unitMinor * fontScale, - oldView = ruler.view, - view = ruler.view = new THREE.Group(); - - if (xon && axesOn) { - let xPadding = labelSize * 4, - canvas = canvasInMesh(x + xPadding, labelSize * 3, 'center', 'top', rulerColor, labelSize), - context = canvas.context, - mesh = canvas.mesh; - if (!(context && mesh)) return; - - if (platformBelt) { - for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { - context.fillText((i * factor).round(1).toString(), ruler.x2 - i + xPadding / 2, 0); - } - } else { - for (let i = 0; i >= ruler.x1; i -= grid.unitMajor) { - context.fillText((i * factor).round(1).toString(), ruler.xo + i + xPadding / 2, 0); - } - for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { - context.fillText((i * factor).round(1).toString(), ruler.xo + i + xPadding / 2, 0); - } - } - - context.font = (labelSize * 0.75) + 'px sans-serif'; - context.fillText(xlabel, (x + xPadding) / 2, labelSize * 1.5); - mesh.position.set(0, - h - labelSize * 2, zp); - view.add(mesh); - } - - if (yon && axesOn) { - let yPadding = labelSize, - canvas = canvasInMesh(labelSize * 4, y + yPadding, 'end', 'middle', rulerColor, labelSize), - context = canvas.context, - mesh = canvas.mesh; + if (xon && axesOn) { + let xPadding = labelSize * 4, + canvas = canvasInMesh(x + xPadding, labelSize * 3, 'center', 'top', rulerColor, labelSize), + context = canvas.context, + mesh = canvas.mesh; if (!(context && mesh)) return; - for (let i = 0; i >= ruler.y1; i -= grid.unitMajor) { - context.fillText((i * factor).round(1), labelSize * 4, y - (ruler.yo + i) + yPadding / 2); - } - for (let i = 0; i <= ruler.y2; i += grid.unitMajor) { - context.fillText((i * factor).round(1), labelSize * 4, y - (ruler.yo + i) + yPadding / 2); - } - - context.font = (labelSize * 0.75) + 'px sans-serif'; - context.fillText(ylabel, labelSize * 1.25, (y + yPadding) / 2); - mesh.position.set(-w - labelSize * 2 - 5, 0, zp); - view.add(mesh); - } - - Space.scene.remove(oldView); - Space.scene.add(view); - requestRefresh(); - } - - function setGrid( - unitMajor = grid.unitMajor, - unitMinor = grid.unitMinor, - colorMajor = grid.colorMajor, - colorMinor = grid.colorMinor) - { - if (!unitMajor) { - return; - } - if ( - unitMajor !== grid.unitMajor || unitMinor !== grid.unitMinor || - colorMajor !== grid.colorMajor || colorMinor !== grid.colorMinor - ) { - grid.unitMajor = unitMajor; - grid.unitMinor = unitMinor; - grid.colorMajor = colorMajor || grid.colorMajor; - grid.colorMinor = colorMinor || grid.colorMinor; - updateGrid(); - } - } - - function setGridColor(opt = {}) { - grid.colorMajor = valueOr(opt.major || opt.colorMajor, grid.colorMajor); - grid.colorMinor = valueOr(opt.minor || opt.colorMinor, grid.colorMinor); - grid.colorX = valueOr(opt.colorX, grid.colorX); - grid.colorY = valueOr(opt.colorY, grid.colorY); - updateGrid(); - } - - function numOrDef(v, dv) { - return v !== undefined ? v : dv; - } - - function modMatch(val, mod) { - let mv = Math.abs(val) % mod; - return (mv < 1) || ((mod - mv) < 1); - } - - function updateGrid() { - let { view, unitMinor, unitMajor, colorMajor, colorMinor, colorX, colorY } = grid; - let oldView = view; - let axes = grid.axes = new THREE.Group(); - let lines = grid.lines = new THREE.Group(); - - view = grid.view = new THREE.Group(); - view.visible = oldView ? oldView.visible : true; - view.add(lines); - view.add(axes); - - let majors = [], - minors = [], - x = platform.scale.x, - y = isRound ? platform.scale.z : platform.scale.y, - z = isRound ? platform.scale.y : platform.scale.z, - zp = -(z / 2) - platformZOff + numOrDef(gridZOff, (z/2-0.1)), - xh = x / 2, - yh = y / 2, - x1 = -xh - origin.x, - x2 = xh - origin.x, - y1 = -yh + origin.y, - y2 = yh + origin.y, - xo = x1 + xh, - yo = y1 + yh; - - ruler.x1 = x1; - ruler.x2 = x2; - ruler.y1 = y1; - ruler.y2 = y2; - ruler.xo = xh - xo; - ruler.yo = yh - yo; - - for (let x=-unitMinor; x>x1; x -= unitMinor) { - let oh = isRound ? Math.sqrt(1-(x/xh)*(x/xh)) * yh : yh; - let arr = modMatch(x, unitMajor) ? majors : minors; - arr.append({x:x-xo, y:-oh, z:zp}).append({x:x-xo, y:oh, z:zp}); - } - for (let x=unitMinor; xy1; y -= unitMinor) { - let ow = isRound ? Math.sqrt(1-(y/yh)*(y/yh)) * xh : xh; - let arr = modMatch(y, unitMajor) ? majors : minors; - arr.append({x:-ow, y:y-yo, z:zp}).append({x:ow, y:y-yo, z:zp}); - } - for (let y=unitMinor; y { - m.renderOrder = 3; - }); - Space.scene.add(group); - const scale = grid.origin.scale = () => { - const dist = camera.position.distanceTo(group.position); - const scale = Math.min(dist / 100, 0.5); - group.scale.set(scale, scale, scale); - }; - scale(); - updateDraws(); - } - - function setRound(bool) { - let current = platform; - isRound = bool; - if (bool) { - platform = new THREE.Mesh( - new THREE.CylinderGeometry(.5, .5, 1, 60), - platformMaterial - ); - platform.rotation.x = 0; - } else { - platform = new THREE.Mesh( - new THREE.BoxGeometry(1, 1, 1), - platformMaterial - ); - platform.rotation.x = -PI2; - } - - platform.position.y = current.position.y; - platform.visible = current.visible; - - SCENE.remove(current); - SCENE.add(platform); - THREE.dispose(current); - } - - function refresh() { - refreshRequested = false; - clearTimeout(refreshTimeout); - updateLastAction(); - viewControl.update(); - } - - /** deferred refresh that collapses multiple requests */ - function requestRefresh(timeout) { - if (refreshRequested === false) { - refreshRequested = true; - clearTimeout(refreshTimeout); - refreshTimeout = setTimeout(refresh, timeout || 10); - } - } - - function onResize() { - updateLastAction(); - camera.aspect = aspect(); - camera.updateProjectionMatrix(); - renderer.setSize(width(), height()); - container.style.width = width(); - container.style.height = height(); - requestRefresh(); - } - - function cca(c) { - return c.charCodeAt(0); - } - - function inputHasFocus() { - return DOC.activeElement && (DOC.activeElement != DOC.body); - } - - function keyHandler(evt) { - updateLastAction(); - if (!defaultKeys || inputHasFocus()) return false; - if (evt.metaKey) return false; - let handled = true; - switch (evt.charCode) { - case cca('z'): - Space.view.reset(); - break; - case cca('h'): - Space.view.home(); - break; - case cca('t'): - Space.view.top(); - break; - case cca('f'): - Space.view.front(); - cycleInd = 0; - break; - case cca('b'): - Space.view.back(); - cycleInd = 2; - break; - case cca('>'): - cycleInd = (++cycleInd % cycle.length); - cycle[cycleInd](); - break; - case cca('<'): - cycleInd--; - if (cycleInd < 0) { - cycleInd += cycle.length; - } - cycle[cycleInd](); - break; - default: - handled = false; - break; - } - if (handled) evt.preventDefault(); - return false; - } - - /** ****************************************************************** - * ThreeJS Helper Functions - ******************************************************************* */ - - function makeLinesFromPoints(points, color) { - if (points.length % 2 != 0) { - throw "invalid line : "+points.length; - } - const geo = new THREE.BufferGeometry(); - const vrt = new Float32Array(points.length * 3); - let vi = 0; - for (let p of points) { - vrt[vi++] = p.x; - vrt[vi++] = p.y; - vrt[vi++] = p.z; - } - geo.setAttribute('position', new THREE.BufferAttribute(vrt, 3)); - return new THREE.LineSegments(geo, new THREE.LineBasicMaterial({ color })); - } - - function intersect(objects, recurse) { - // console.log(({int: objects})); - let ints = raycaster.intersectObjects(objects, recurse); - // console.trace({ints}); - return ints; - } - - /** ****************************************************************** - * Mouse Functions - ******************************************************************* */ - - function onMouseDown(event) { - updateLastAction(); - if (event.target === renderer.domElement) { - DOC.activeElement.blur(); - event.preventDefault(); - let selection = null, - trackTo = alignedTracking ? trackPlane : platform, - isVis = trackTo.visible; - if (mouseDownSelect) selection = mouseDownSelect(undefined, event); - if (selection && selection.length > 0) { - // selection = selection.map(o => o.isGroup ? o.children : o).flat(); - // console.log({ selection }); - trackTo.visible = true; - let int = intersect(selection.slice().append(trackTo), false); - trackTo.visible = isVis; - if (int.length > 0) { - let trackInt, selectInt; - for (let i=0; i 0 ? int[0].point : null; + if (platformBelt) { + for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { + context.fillText((i * factor).round(1).toString(), ruler.x2 - i + xPadding / 2, 0); } } else { - viewControl.enabled = false; + for (let i = 0; i >= ruler.x1; i -= grid.unitMajor) { + context.fillText((i * factor).round(1).toString(), ruler.xo + i + xPadding / 2, 0); + } + for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { + context.fillText((i * factor).round(1).toString(), ruler.xo + i + xPadding / 2, 0); + } } - mouseStart = { - x: (event.clientX / width()) * 2 - 1, - y: -(event.clientY / height()) * 2 + 1}; + + context.font = (labelSize * 0.75) + 'px sans-serif'; + context.fillText(xlabel, (x + xPadding) / 2, labelSize * 1.5); + mesh.position.set(0, - h - labelSize * 2, zp); + view.add(mesh); } - function onMouseUp(event) { - updateLastAction(); - if (!viewControl.enabled) { - viewControl.enabled = true; - viewControl.onMouseUp(event); + if (yon && axesOn) { + let yPadding = labelSize, + canvas = canvasInMesh(labelSize * 4, y + yPadding, 'end', 'middle', rulerColor, labelSize), + context = canvas.context, + mesh = canvas.mesh; + if (!(context && mesh)) return; + + for (let i = 0; i >= ruler.y1; i -= grid.unitMajor) { + context.fillText((i * factor).round(1), labelSize * 4, y - (ruler.yo + i) + yPadding / 2); } - let mouseEnd = { - x: (event.clientX / width()) * 2 - 1, - y: -(event.clientY / height()) * 2 + 1}; - // only fire on mouse move between mouseStart (down) and up - if (mouseStart && mouseEnd.x - mouseStart.x + mouseEnd.y - mouseStart.y === 0) { - event.preventDefault(); - let refresh = false, - selection = null; - if (mouseUpSelect) { - selection = mouseUpSelect(); - } - if (selection && selection.length > 0) { - let int = intersect(selection, selectRecurse); - if (mouseUp) { - if (int.length) { - mouseUp(event, int); - } else { - mouseUp(event, intersect([platform], selectRecurse)); - } - if (event.button === 2) { - return; - } - } - if (int.length > 0) { - mouseUpSelect(int[0], event, int); - refresh = true; - } else { - mouseUpSelect(null, event); - } - } - if (!refresh && platformClickAt) { - platformClick(platformClickAt); - } - if (refresh) { - requestRefresh(); - } - mouseStart = null; - } else if (mouseDrag && mouseDragStart) { - // fired on mouse drag end - mouseDrag(null,null,true); + for (let i = 0; i <= ruler.y2; i += grid.unitMajor) { + context.fillText((i * factor).round(1), labelSize * 4, y - (ruler.yo + i) + yPadding / 2); } - mouseDragPoint = null; - mouseDragStart = null; + + context.font = (labelSize * 0.75) + 'px sans-serif'; + context.fillText(ylabel, labelSize * 1.25, (y + yPadding) / 2); + mesh.position.set(-w - labelSize * 2 - 5, 0, zp); + view.add(mesh); } - function onMouseMove(event) { - updateLastAction(); - let int, vis; + Space.scene.remove(oldView); + Space.scene.add(view); + requestRefresh(); +} - const mv = new THREE.Vector2(); - mv.x = ( event.clientX / window.innerWidth ) * 2 - 1; - mv.y = - ( event.clientY / window.innerHeight ) * 2 + 1; - raycaster.setFromCamera( mv, camera ); - - if (viewControl.enabled) { - event.preventDefault(); - let selection = mouseHover ? mouseHover() : null; - if (selection && selection.length > 0) { - int = intersect(selection, selectRecurse); - if (int.length > 0) mouseHover(int[0], event, int); - } - if ((!int || int.length == 0) && platformHover) { - vis = platform.visible; - platform.visible = true; - int = intersect([platform], false); - platform.visible = vis; - if (int && int.length > 0) platformHover(int[0].point); - } - } else if (mouseDragPoint && mouseDrag && mouseDrag()) { - event.preventDefault(); - let trackTo = alignedTracking ? trackPlane : platform; - let vis = trackTo.visible; - trackTo.visible = true; - int = intersect([trackTo], false); - trackTo.visible = vis; - if (int.length > 0 && int[0].object === trackTo) { - let delta = mouseDragPoint.clone().sub(int[0].point); - let offset = mouseDragStart.clone().sub(int[0].point); - mouseDragPoint = int[0].point; - mouseDrag({ - x: -delta.x, - y: delta.z, - z: 0, - event - }, { - x: -offset.x, - y: offset.z, - z: 0 - }); - requestRefresh(); - } - } - mouse = { - x: (event.clientX / width()) * 2 - 1, - y: -(event.clientY / height()) * 2 + 1}; +function setGrid( + unitMajor = grid.unitMajor, + unitMinor = grid.unitMinor, + colorMajor = grid.colorMajor, + colorMinor = grid.colorMinor) +{ + if (!unitMajor) { + return; } + if ( + unitMajor !== grid.unitMajor || unitMinor !== grid.unitMinor || + colorMajor !== grid.colorMajor || colorMinor !== grid.colorMinor + ) { + grid.unitMajor = unitMajor; + grid.unitMinor = unitMinor; + grid.colorMajor = colorMajor || grid.colorMajor; + grid.colorMinor = colorMinor || grid.colorMinor; + updateGrid(); + } +} - /** ****************************************************************** - * Space Object - ******************************************************************* */ +function setGridColor(opt = {}) { + grid.colorMajor = valueOr(opt.major || opt.colorMajor, grid.colorMajor); + grid.colorMinor = valueOr(opt.minor || opt.colorMinor, grid.colorMinor); + grid.colorX = valueOr(opt.colorX, grid.colorX); + grid.colorY = valueOr(opt.colorY, grid.colorY); + updateGrid(); +} - function updateFocus() { - if (focalPoint) { - Space.scene.remove(focalPoint); +function numOrDef(v, dv) { + return v !== undefined ? v : dv; +} + +function modMatch(val, mod) { + let mv = Math.abs(val) % mod; + return (mv < 1) || ((mod - mv) < 1); +} + +function updateGrid() { + let { view, unitMinor, unitMajor, colorMajor, colorMinor, colorX, colorY } = grid; + let oldView = view; + let axes = grid.axes = new THREE.Group(); + let lines = grid.lines = new THREE.Group(); + + view = grid.view = new THREE.Group(); + view.visible = oldView ? oldView.visible : true; + view.add(lines); + view.add(axes); + + let majors = [], + minors = [], + x = platform.scale.x, + y = isRound ? platform.scale.z : platform.scale.y, + z = isRound ? platform.scale.y : platform.scale.z, + zp = -(z / 2) - platformZOff + numOrDef(gridZOff, (z/2-0.1)), + xh = x / 2, + yh = y / 2, + x1 = -xh - origin.x, + x2 = xh - origin.x, + y1 = -yh + origin.y, + y2 = yh + origin.y, + xo = x1 + xh, + yo = y1 + yh; + + ruler.x1 = x1; + ruler.x2 = x2; + ruler.y1 = y1; + ruler.y2 = y2; + ruler.xo = xh - xo; + ruler.yo = yh - yo; + + for (let x=-unitMinor; x>x1; x -= unitMinor) { + let oh = isRound ? Math.sqrt(1-(x/xh)*(x/xh)) * yh : yh; + let arr = modMatch(x, unitMajor) ? majors : minors; + arr.append({x:x-xo, y:-oh, z:zp}).append({x:x-xo, y:oh, z:zp}); + } + for (let x=unitMinor; xy1; y -= unitMinor) { + let ow = isRound ? Math.sqrt(1-(y/yh)*(y/yh)) * xh : xh; + let arr = modMatch(y, unitMajor) ? majors : minors; + arr.append({x:-ow, y:y-yo, z:zp}).append({x:ow, y:y-yo, z:zp}); + } + for (let y=unitMinor; y { - showSkyGrid = b; - }, - - setColor: (c) => { - SCENE.background = new THREE.Color(c); - }, - - setGridColor: (c) => { - skyGridColor = c; - if (skyGridMaterial) skyGridMaterial.color = new THREE.Color(c); - } - }, - - scene: { - add: function (o) { - o.rotation.x = WORLD.rotation.x; - return SCENE.add(o); - }, - - remove: function (o) { - THREE.dispose(o); - return SCENE.remove(o); - }, - - active: updateLastAction, - - setFog: function(mult, color) { - if (mult) { - SCENE.fog = new THREE.Fog(color, 100, 1000); - SCENE.fog.mult = mult > 0 ? mult : 3; - } else { - SCENE.fog = undefined; - } - Space.scene.updateFog(); - }, - - updateFog: function() { - const { fog } = SCENE; - if (fog) { - const dist = camera.position.distanceTo(viewControl.target); - fog.near = dist; - fog.far = dist * fog.mult; - } - // todo: option to clip for close views - // camera.near = dist / 2; - // camera.updateProjectionMatrix(); - }, - - lightInfo: () => { - setTimeout(updateLights, 0); - return lightInfo; - } - }, - - world: { - add: function(o) { - return WORLD.add(o); - }, - - remove: function(o) { - THREE.dispose(o); - return WORLD.remove(o); - }, - - newGroup: function() { - return WORLD.newGroup(); - } - }, - - platform: { - set: setPlatform, - update: updateDraws, - setSize: setPlatformSizeUpdateGrid, - setColor: setPlatformColor, - setOrigin, - setRulers, - setGrid, - setGridColor, - setFont, - setRound, - showAxes, - showVolume, - showGridBelow: (b) => { hideGridBelow = !b }, - showGrid: (b) => { showGrid = grid.view.visible = b }, - showGrid2: (b) => { showGridLines = grid.lines.visible = b }, - setMaxZ: (z) => { panY = z / 2 }, - setCenter: (x,y,z) => { panX = x; panY = z, panZ = y }, - setHidden: (b) => { showPlatform = !b; platform.visible = !b }, - setVisible: (b) => { showPlatform = b; platform.visible = b }, - setHiding: (b) => { hidePlatformBelow = b }, - setZOff: (z) => { platformZOff = z; updatePlatformPosition() }, - setGridZOff:(z) => { gridZOff = z; updatePlatformPosition() }, - setBelt: (b) => { platformBelt = b }, - isHidden: () => { return !showPlatform }, - isVisible: () => { return platform.visible }, - isGridVisible() { return grid.view.visible }, - opacity: (o) => { platform.material.opacity = o; Space.platform.setVisible(o > 0) }, - onMove: (f,t) => { platformOnMove = f, platformOnMoveTime = t || platformOnMoveTime }, - onHover: (f) => { platformHover = f }, - onClick: (f) => { platformClick = f}, - size: () => { return platform.scale }, - get world() { throw "platform.world deprecated" } - }, - - preset: { - top: {left: home, up: 0, panX, panY, panZ}, - back: {left: PI, up: PI2, panX, panY, panZ}, - home: {left: home, up, panX, panY, panZ}, - front: {left: 0, up: PI2, panX, panY, panZ}, - right: {left: PI2, up: PI2, panX, panY, panZ}, - left: {left: -PI2, up: PI2, panX, panY, panZ}, - }, - - view: { - top: (then) => { tweenCam({left: home, up: 0, panX, panY, panZ, then}) }, - back: (then) => { tweenCam({left: PI, up: PI2, panX, panY, panZ, then}) }, - home: (then) => { tweenCam({left: home, up, panX, panY, panZ, then}) }, - front: (then) => { tweenCam({left: 0, up: PI2, panX, panY, panZ, then}) }, - right: (then) => { tweenCam({left: PI2, up: PI2, panX, panY, panZ, then}) }, - left: (then) => { tweenCam({left: -PI2, up: PI2, panX, panY, panZ, then}) }, - reset: () => { viewControl.reset(); requestRefresh() }, - load: (cam) => { viewControl.setPosition(cam); requestRefresh() }, - save: () => { return viewControl.getPosition(true) }, - panTo: (x,y,z,l,u) => { tweenCamPan(x,y,z,l,u) }, - setZoom: (r,v) => { viewControl.setZoom(r,v) }, - setCtrl: (name) => { - if (name === 'onshape') { - viewControl.setMouse(viewControl.mouseOnshape); - } else { - viewControl.setMouse(viewControl.mouseDefault); - } - }, - getFPS () { return fps }, - getRMS() { return renderTime }, - getFocus() { return viewControl.getTarget() }, - setFocus(v) { - viewControl.setTarget(v); - updateFocus(); - refresh() - }, - showFocus(ms) { - showFocus = ms; - updateFocus(); - }, - setHome(r,u) { - home = r || 0; - up = u || PI4; - }, - spin(then, count) { - Space.view.front(() => { - Space.view.right(() => { - Space.view.back(() => { - Space.view.left(() => { - if (--count > 0) { - Space.view.spin(then, count); - } else { - Space.view.front(then); - } - }); - }); - }); - }); - }, - get ctrl() { - return viewControl; - } - }, - - mouse: { - up: (f) => { mouseUp = f }, - down: (f) => { mouseDown = f }, - downSelect: (f) => { mouseDownSelect = f }, - upSelect: (f) => { mouseUpSelect = f }, - onDrag: (f) => { mouseDrag = f }, - onHover: (f) => { mouseHover = f } - }, - - tween: { - setTime: (t) => { tweenTime = t || 500 }, - setDelay: (d) => { tweenDelay = d || 20 } - }, - - useDefaultKeys (b) { defaultKeys = b }, - selectRecurse (b) { selectRecurse = b }, - renderInfo () { return renderer.info }, - objects () { return WORLD.children }, - - screenshot(format, options) { - return renderer.domElement.toDataURL(format || "image/png", options); - }, - - screenshot2(param = {}) { - let oco = renderer.domElement; - let oWidth = oco.offsetWidth; - let oHeight = oco.offsetHeight; - let oRatio = oHeight / oWidth; - let width = param.width || 240; - let ncv = document.createElement('canvas'); - ncv.width = width; - ncv.height = width * oRatio; - let nco = ncv.getContext('2d'); - nco.drawImage(oco, 0, 0, ncv.width, ncv.height); - return { - url: ncv.toDataURL(param.format || "image/png", param.options), - width: ncv.width, - height: ncv.height, - }; - }, - - screenshot3(param = {}) { - let oco = renderer.domElement; - let oWidth = oco.offsetWidth; - let oHeight = oco.offsetHeight; - let oRatio = oWidth / oHeight; - let width = param.width || 512; - let height = param.height || width; - let nRatio = width / height; - let ncv = document.createElement('canvas'); - ncv.width = width; - ncv.height = height; - let nco = ncv.getContext('2d'); - let ox = 0, oy = 0; - if (oRatio > nRatio) { - let tmp = oWidth; - oWidth = oHeight * nRatio; - ox = (tmp - oWidth) / 2; - } else { - let tmp = oHeight; - oHeight = oWidth * nRatio; - oy = (tmp - oHeight) / 2; - } - nco.drawImage(oco, ox, oy, oWidth, oHeight, 0, 0, width, height); - if (param.out) { - ncv.toBlob(blob => blob.arrayBuffer().then(png => param.out({ png, width, height }))); - } - return ncv; - }, - - internals: () => { - return { renderer, camera, platform }; - }, - - init: (domelement, slider, ortho) => { - container = domelement; - - WORLD.rotation.x = -PI2; - SCENE.add(WORLD); - - domelement.style.width = width(); - domelement.style.height = height(); - - renderer = new WebGLRenderer({ - antialias: antiAlias, - preserveDrawingBuffer: true, - logarithmicDepthBuffer: true - }); - - // THREE.ColorManagement.enabled = false; - // renderer.outputColorSpace = THREE.LinearSRGBColorSpace; - - renderer.localClippingEnabled = true; - camera = ortho ? - new THREE.OrthographicCamera(-100 * aspect(), 100 * aspect(), 100, -100, 0.1, 100000) : - new THREE.PerspectiveCamera(perspective, aspect(), 0.1, 100000); - - camera.position.set(0, 200, 340); - renderer.setSize(width(), height()); - domelement.appendChild(renderer.domElement); - - raycaster = new THREE.Raycaster(); - - viewControl = new moto.Orbit(camera, domelement, (position, moved) => { - if (platform) { - platform.visible = hidePlatformBelow ? - initialized && position.y >= 0 && showPlatform : showPlatform; - volume.visible = showVolume && platform.visible; - } - if (grid.view) { - grid.view.visible = hideGridBelow ? platform.visible : showGrid; - } - if (cameraLight) { - cameraLight.position.copy(camera.position); - } - if (moved && platformOnMove) { - clearTimeout(platformMoveTimer); - platformMoveTimer = setTimeout(platformOnMove, 500); - Space.scene.updateFog(); - } - updateLastAction(); - updateFocus(); - }, (val) => { - if (camera && grid?.origin?.scale) { - grid.origin.scale(); - } - if (camera && viewControl) { - // increase intersect line precision on zoom - const dist = camera.position.distanceTo(viewControl.target); - raycaster.params.Line.threshold = Math.min(1, dist / 100); - } - moto.broker.publish("space.view.zoom", camera); - updateLastAction(); - if (slider) slider(val); - }); - - viewControl.noKeys = true; - viewControl.maxDistance = 1000; - - SCENE.add(skyAmbient = new THREE.AmbientLight(0x707070)); - - platform = new THREE.Mesh( - new THREE.BoxGeometry(1, 1, 1), - platformMaterial - ); - - platform.position.y = platformZOff; - platform.rotation.x = -PI2; - platform.visible = showPlatform; - - trackPlane = new THREE.Mesh( - new THREE.PlaneGeometry(100000, 100000, 1, 1), - new THREE.MeshBasicMaterial( { color: 0x777777, opacity: 0, transparent: true } ) - ); - trackPlane.visible = false; - trackPlane.rotation.x = PI2; - - let skygrid = new THREE.Mesh( - new THREE.BoxGeometry(10000, 10000, 10000, 10, 10, 10), - skyGridMaterial = - new THREE.MeshBasicMaterial({ color: skyGridColor, side: THREE.DoubleSide }) - ); - - SCENE.add(platform); - SCENE.add(trackPlane); - - if (showSkyGrid) { - skygrid.material.wireframe = true; - SCENE.add(skygrid); - } - - addEventHandlers(WIN, [ - 'resize', onResize, - 'mousemove', onMouseMove, - 'mousedown', onMouseDown, - 'mouseup', onMouseUp, - 'keypress', keyHandler, - 'touchstart', updateLastAction, - 'touchmove', updateLastAction, - 'touchend', updateLastAction - ]); - - let animates = 0; - let rateStart = Date.now(); - let renderStart; - let renders = []; - - function animate() { - animates++; - const now = Date.now(); - if (now - rateStart > 1000) { - // compute stats roughly every second - const delta = now - rateStart; - fps = 1000 * animates / delta; - animates = 0; - rateStart = now; - // look for slowest rendered frame - renderTime = Math.max(0, ...renders); - renders.length = 0; - } - requestAnimationFrame(animate); - if (docVisible && !freeze && Date.now() - lastAction < 1500) { - renderStart = Date.now(); - renderer.render(SCENE, camera); - // track frame render times - renders.push(Date.now() - renderStart); - } else { - fps = 0; - } - } - - animate(); - - const ctx = renderer.getContext(); - - Space.info = { - ver: ctx.getParameter(ctx.VERSION), - ven: ctx.getParameter(ctx.VENDOR), - glr: ctx.getParameter(ctx.RENDERER), - pla: nav.platform, - mob: nav.maxTouchPoints > 1 || (/android/i.test(nav.userAgent)) - }; - - initialized = true; - } + let cmat = new THREE.MeshPhongMaterial({ + color: 0xcceeff, + specular: 0xcceeff, + shininess: 5, + transparent: true, + opacity: 0.5, + // side: THREE.DoubleSide }); + let rmat = new THREE.MeshPhongMaterial({ + color: 0x88aadd, + transparent: true, + opacity: 0.5, + // side: THREE.DoubleSide + }); + let PIP = Math.PI/2; + let pi1, pi2, pi3, pi4; + let group = new THREE.Group(); + grid.origin = {x, y, z, group}; + group.add(pi1 = new THREE.Mesh( + new THREE.CircleGeometry(4.6, 50, PIP*0, PIP*1), + cmat + )); + pi1.position.x = 0.25; + pi1.position.y = 0.25; + group.add(pi2 = new THREE.Mesh( + new THREE.CircleGeometry(4.6, 50, PIP*1, PIP*1), + cmat + )); + pi2.position.x = -0.25; + pi2.position.y = 0.25; + group.add(pi3 = new THREE.Mesh( + new THREE.CircleGeometry(4.6, 50, PIP*2, PIP*1), + cmat + )); + pi3.position.x = -0.25; + pi3.position.y = -0.25; + group.add(pi4 = new THREE.Mesh( + new THREE.CircleGeometry(4.6, 50, PIP*3, PIP*1), + cmat + )); + pi4.position.x = 0.25; + pi4.position.y = -0.25; + let aa, bb, cc; + group.add(aa = new THREE.Mesh( + new THREE.RingGeometry(5, 5.5, 50), + rmat + )); + group.add(bb = new THREE.Mesh( + new THREE.PlaneGeometry(0.5, 10), + rmat + )); + group.add(cc = new THREE.Mesh( + new THREE.PlaneGeometry(10, 0.5), + rmat + )); + group.rotation.x = -PI2; + group.position.x = x; + group.position.y = z; + group.position.z = y; + [bb,cc].forEach(m => { + m.renderOrder = 3; + }); + Space.scene.add(group); + const scale = grid.origin.scale = () => { + const dist = camera.position.distanceTo(group.position); + const scale = Math.min(dist / 100, 0.5); + group.scale.set(scale, scale, scale); + }; + scale(); + updateDraws(); +} - let cycle = [ - Space.view.front, - Space.view.right, - Space.view.back, - Space.view.left, - ]; +function setRound(bool) { + let current = platform; + isRound = bool; + if (bool) { + platform = new THREE.Mesh( + new THREE.CylinderGeometry(.5, .5, 1, 60), + platformMaterial + ); + platform.rotation.x = 0; + } else { + platform = new THREE.Mesh( + new THREE.BoxGeometry(1, 1, 1), + platformMaterial + ); + platform.rotation.x = -PI2; + } - let cycleInd = 0; + platform.position.y = current.position.y; + platform.visible = current.visible; -}); + SCENE.remove(current); + SCENE.add(platform); + THREE.dispose(current); +} + +function refresh() { + refreshRequested = false; + clearTimeout(refreshTimeout); + updateLastAction(); + viewControl.update(); +} + +/** deferred refresh that collapses multiple requests */ +function requestRefresh(timeout) { + if (refreshRequested === false) { + refreshRequested = true; + clearTimeout(refreshTimeout); + refreshTimeout = setTimeout(refresh, timeout || 10); + } +} + +function onResize() { + updateLastAction(); + camera.aspect = aspect(); + camera.updateProjectionMatrix(); + renderer.setSize(width(), height()); + container.style.width = width(); + container.style.height = height(); + requestRefresh(); +} + +function cca(c) { + return c.charCodeAt(0); +} + +function inputHasFocus() { + return DOC.activeElement && (DOC.activeElement != DOC.body); +} + +function keyHandler(evt) { + updateLastAction(); + if (!defaultKeys || inputHasFocus()) return false; + if (evt.metaKey) return false; + let handled = true; + switch (evt.charCode) { + case cca('z'): + Space.view.reset(); + break; + case cca('h'): + Space.view.home(); + break; + case cca('t'): + Space.view.top(); + break; + case cca('f'): + Space.view.front(); + cycleInd = 0; + break; + case cca('b'): + Space.view.back(); + cycleInd = 2; + break; + case cca('>'): + cycleInd = (++cycleInd % cycle.length); + cycle[cycleInd](); + break; + case cca('<'): + cycleInd--; + if (cycleInd < 0) { + cycleInd += cycle.length; + } + cycle[cycleInd](); + break; + default: + handled = false; + break; + } + if (handled) evt.preventDefault(); + return false; +} + +/** ****************************************************************** + * ThreeJS Helper Functions + ******************************************************************* */ + +function makeLinesFromPoints(points, color) { + if (points.length % 2 != 0) { + throw "invalid line : "+points.length; + } + const geo = new THREE.BufferGeometry(); + const vrt = new Float32Array(points.length * 3); + let vi = 0; + for (let p of points) { + vrt[vi++] = p.x; + vrt[vi++] = p.y; + vrt[vi++] = p.z; + } + geo.setAttribute('position', new THREE.BufferAttribute(vrt, 3)); + return new THREE.LineSegments(geo, new THREE.LineBasicMaterial({ color })); +} + +function intersect(objects, recurse) { + // console.log(({int: objects})); + let ints = raycaster.intersectObjects(objects, recurse); + // console.trace({ints}); + return ints; +} + +/** ****************************************************************** + * Mouse Functions + ******************************************************************* */ + +function onMouseDown(event) { + updateLastAction(); + if (event.target === renderer.domElement) { + DOC.activeElement.blur(); + event.preventDefault(); + let selection = null, + trackTo = alignedTracking ? trackPlane : platform, + isVis = trackTo.visible; + if (mouseDownSelect) { + selection = mouseDownSelect(undefined, event); + } + if (selection && selection.length > 0) { + // selection = selection.map(o => o.isGroup ? o.children : o).flat(); + // console.log({ selection }); + trackTo.visible = true; + let int = intersect(selection.slice().append(trackTo), false); + trackTo.visible = isVis; + if (int.length > 0) { + let trackInt, selectInt; + for (let i=0; i 0 ? int[0].point : null; + } + } else { + viewControl.enabled = false; + } + mouseStart = { + x: (event.clientX / width()) * 2 - 1, + y: -(event.clientY / height()) * 2 + 1}; +} + +function onMouseUp(event) { + updateLastAction(); + if (!viewControl.enabled) { + viewControl.enabled = true; + viewControl.onMouseUp(event); + } + let mouseEnd = { + x: (event.clientX / width()) * 2 - 1, + y: -(event.clientY / height()) * 2 + 1}; + // only fire on mouse move between mouseStart (down) and up + if (mouseStart && mouseEnd.x - mouseStart.x + mouseEnd.y - mouseStart.y === 0) { + event.preventDefault(); + let refresh = false, + selection = null; + if (mouseUpSelect) { + selection = mouseUpSelect(); + } + if (selection && selection.length > 0) { + let int = intersect(selection, selectRecurse); + if (mouseUp) { + if (int.length) { + mouseUp(event, int); + } else { + mouseUp(event, intersect([platform], selectRecurse)); + } + if (event.button === 2) { + return; + } + } + if (int.length > 0) { + mouseUpSelect(int[0], event, int); + refresh = true; + } else { + mouseUpSelect(null, event); + } + } + if (!refresh && platformClickAt) { + platformClick(platformClickAt); + } + if (refresh) { + requestRefresh(); + } + mouseStart = null; + } else if (mouseDrag && mouseDragStart) { + // fired on mouse drag end + mouseDrag(null,null,true); + } + mouseDragPoint = null; + mouseDragStart = null; +} + +function onMouseMove(event) { + updateLastAction(); + let int, vis; + + const mv = new THREE.Vector2(); + mv.x = ( event.clientX / window.innerWidth ) * 2 - 1; + mv.y = - ( event.clientY / window.innerHeight ) * 2 + 1; + raycaster.setFromCamera( mv, camera ); + + if (viewControl.enabled) { + event.preventDefault(); + let selection = mouseHover ? mouseHover() : null; + if (selection && selection.length > 0) { + int = intersect(selection, selectRecurse); + if (int.length > 0) mouseHover(int[0], event, int); + } + if ((!int || int.length == 0) && platformHover) { + vis = platform.visible; + platform.visible = true; + int = intersect([platform], false); + platform.visible = vis; + if (int && int.length > 0) platformHover(int[0].point); + } + } else if (mouseDragPoint && mouseDrag && mouseDrag()) { + event.preventDefault(); + let trackTo = alignedTracking ? trackPlane : platform; + let vis = trackTo.visible; + trackTo.visible = true; + int = intersect([trackTo], false); + trackTo.visible = vis; + if (int.length > 0 && int[0].object === trackTo) { + let delta = mouseDragPoint.clone().sub(int[0].point); + let offset = mouseDragStart.clone().sub(int[0].point); + mouseDragPoint = int[0].point; + mouseDrag({ + x: -delta.x, + y: delta.z, + z: 0, + event + }, { + x: -offset.x, + y: offset.z, + z: 0 + }); + requestRefresh(); + } + } + mouse = { + x: (event.clientX / width()) * 2 - 1, + y: -(event.clientY / height()) * 2 + 1}; +} + +/** ****************************************************************** + * Space Object + ******************************************************************* */ + +function updateFocus() { + if (focalPoint) { + Space.scene.remove(focalPoint); + } + if (showFocus) { + let mesh = focalPoint = new THREE.Mesh( + new THREE.SphereGeometry(1, 16, 16), + new THREE.MeshPhongMaterial({ + side: THREE.DoubleSide, + specular: 0x202020, + color: 0xff0000, + shininess: 125 + }) + ); + Space.scene.add(mesh); + mesh.position.copy(viewControl.target); + } +} + +function setSky(opt = {}) { + let { grid, color, gridColor } = opt; + if (grid) Space.sky.showGrid(grid); + if (color !== undefined) Space.sky.setColor(color); + if (gridColor !== undefined) Space.sky.setGridColor(gridColor); + if (skyAmbient && opt.ambient) { + let { color, intensity } = opt.ambient; + if (color) skyAmbient.color.set(color); + if (intensity) skyAmbient.intensity = intensity; + } +} + +function setPlatform(opt = {}) { + let platform = Space.platform; + let { color, round, size, grid, opacity } = opt; + let { visible, volume, zOffset, origin, light } = opt; + if (light) { + lightInfo.intensity = light; + } + if (color) { + platform.setColor(color); + } + if (round !== undefined) { + platform.setRound(round); + } + if (size) { + let { width = 300, depth = 300, height = 2.5, maxz = 300 } = size; + platform.setSize(width, depth, height, maxz); + } + if (grid) { + let { zOffset } = grid; + let { major = 25, minor = 5 } = grid; + let { colorX, colorY, colorMajor, colorMinor } = grid; + platform.setGrid(major, minor); + platform.setGridColor({ colorX, colorY, colorMajor, colorMinor }); + if (zOffset !== undefined) platform.setGridZOff(zOffset); + } + if (origin) { + let { x, y, z, show } = origin; + platform.setOrigin(x || 0, y || 0, z || 0, show); + } + if (opacity !== undefined) { + platform.opacity(opacity); + } + if (volume !== undefined) { + platform.showVolume(volume); + } + if (zOffset !== undefined) { + platform.setZOff(zOffset); + } + if (visible !== undefined) { + platform.setVisible(visible); + } +} + +let Space = { + refresh: refresh, + update: requestRefresh, + + setAntiAlias(b) { antiAlias = b ? true : false }, + raycast: intersect, + + event: { + addHandlers: addEventHandlers, + onEnterKey: onEnterKey, + onResize: onResize + }, + + sky: { + set: setSky, + + showGrid: (b) => { + showSkyGrid = b; + }, + + setColor: (c) => { + SCENE.background = new THREE.Color(c); + }, + + setGridColor: (c) => { + skyGridColor = c; + if (skyGridMaterial) skyGridMaterial.color = new THREE.Color(c); + } + }, + + scene: { + add: function (o) { + o.rotation.x = WORLD.rotation.x; + return SCENE.add(o); + }, + + remove: function (o) { + THREE.dispose(o); + return SCENE.remove(o); + }, + + active: updateLastAction, + + setFog: function(mult, color) { + if (mult) { + SCENE.fog = new THREE.Fog(color, 100, 1000); + SCENE.fog.mult = mult > 0 ? mult : 3; + } else { + SCENE.fog = undefined; + } + Space.scene.updateFog(); + }, + + updateFog: function() { + const { fog } = SCENE; + if (fog) { + const dist = camera.position.distanceTo(viewControl.target); + fog.near = dist; + fog.far = dist * fog.mult; + } + // todo: option to clip for close views + // camera.near = dist / 2; + // camera.updateProjectionMatrix(); + }, + + lightInfo: () => { + setTimeout(updateLights, 0); + return lightInfo; + } + }, + + world: { + add: function(o) { + return WORLD.add(o); + }, + + remove: function(o) { + THREE.dispose(o); + return WORLD.remove(o); + }, + + newGroup: function() { + return WORLD.newGroup(); + } + }, + + platform: { + set: setPlatform, + update: updateDraws, + setSize: setPlatformSizeUpdateGrid, + setColor: setPlatformColor, + setOrigin, + setRulers, + setGrid, + setGridColor, + setFont, + setRound, + showAxes, + showVolume, + showGridBelow: (b) => { hideGridBelow = !b }, + showGrid: (b) => { showGrid = grid.view.visible = b }, + showGrid2: (b) => { showGridLines = grid.lines.visible = b }, + setMaxZ: (z) => { panY = z / 2 }, + setCenter: (x,y,z) => { panX = x; panY = z, panZ = y }, + setHidden: (b) => { showPlatform = !b; platform.visible = !b }, + setVisible: (b) => { showPlatform = b; platform.visible = b }, + setHiding: (b) => { hidePlatformBelow = b }, + setZOff: (z) => { platformZOff = z; updatePlatformPosition() }, + setGridZOff:(z) => { gridZOff = z; updatePlatformPosition() }, + setBelt: (b) => { platformBelt = b }, + isHidden: () => { return !showPlatform }, + isVisible: () => { return platform.visible }, + isGridVisible() { return grid.view.visible }, + opacity: (o) => { platform.material.opacity = o; Space.platform.setVisible(o > 0) }, + onMove: (f,t) => { platformOnMove = f, platformOnMoveTime = t || platformOnMoveTime }, + onHover: (f) => { platformHover = f }, + onClick: (f) => { platformClick = f}, + size: () => { return platform.scale }, + get world() { throw "platform.world deprecated" } + }, + + preset: { + top: {left: home, up: 0, panX, panY, panZ}, + back: {left: PI, up: PI2, panX, panY, panZ}, + home: {left: home, up, panX, panY, panZ}, + front: {left: 0, up: PI2, panX, panY, panZ}, + right: {left: PI2, up: PI2, panX, panY, panZ}, + left: {left: -PI2, up: PI2, panX, panY, panZ}, + }, + + view: { + top: (then) => { tweenCam({left: home, up: 0, panX, panY, panZ, then}) }, + back: (then) => { tweenCam({left: PI, up: PI2, panX, panY, panZ, then}) }, + home: (then) => { tweenCam({left: home, up, panX, panY, panZ, then}) }, + front: (then) => { tweenCam({left: 0, up: PI2, panX, panY, panZ, then}) }, + right: (then) => { tweenCam({left: PI2, up: PI2, panX, panY, panZ, then}) }, + left: (then) => { tweenCam({left: -PI2, up: PI2, panX, panY, panZ, then}) }, + reset: () => { viewControl.reset(); requestRefresh() }, + load: (cam) => { viewControl.setPosition(cam); requestRefresh() }, + save: () => { return viewControl.getPosition(true) }, + panTo: (x,y,z,l,u) => { tweenCamPan(x,y,z,l,u) }, + setZoom: (r,v) => { viewControl.setZoom(r,v) }, + setCtrl: (name) => { + if (name === 'onshape') { + viewControl.setMouse(viewControl.mouseOnshape); + } else { + viewControl.setMouse(viewControl.mouseDefault); + } + }, + getFPS () { return fps }, + getRMS() { return renderTime }, + getFocus() { return viewControl.getTarget() }, + setFocus(v) { + viewControl.setTarget(v); + updateFocus(); + refresh() + }, + showFocus(ms) { + showFocus = ms; + updateFocus(); + }, + setHome(r,u) { + home = r || 0; + up = u || PI4; + }, + spin(then, count) { + Space.view.front(() => { + Space.view.right(() => { + Space.view.back(() => { + Space.view.left(() => { + if (--count > 0) { + Space.view.spin(then, count); + } else { + Space.view.front(then); + } + }); + }); + }); + }); + }, + get ctrl() { + return viewControl; + } + }, + + mouse: { + up: (f) => { mouseUp = f }, + down: (f) => { mouseDown = f }, + downSelect: (f) => { mouseDownSelect = f }, + upSelect: (f) => { mouseUpSelect = f }, + onDrag: (f) => { mouseDrag = f }, + onHover: (f) => { mouseHover = f } + }, + + tween: { + setTime: (t) => { tweenTime = t || 500 }, + setDelay: (d) => { tweenDelay = d || 20 } + }, + + useDefaultKeys (b) { defaultKeys = b }, + selectRecurse (b) { selectRecurse = b }, + renderInfo () { return renderer.info }, + objects () { return WORLD.children }, + + screenshot(format, options) { + return renderer.domElement.toDataURL(format || "image/png", options); + }, + + screenshot2(param = {}) { + let oco = renderer.domElement; + let oWidth = oco.offsetWidth; + let oHeight = oco.offsetHeight; + let oRatio = oHeight / oWidth; + let width = param.width || 240; + let ncv = document.createElement('canvas'); + ncv.width = width; + ncv.height = width * oRatio; + let nco = ncv.getContext('2d'); + nco.drawImage(oco, 0, 0, ncv.width, ncv.height); + return { + url: ncv.toDataURL(param.format || "image/png", param.options), + width: ncv.width, + height: ncv.height, + }; + }, + + screenshot3(param = {}) { + let oco = renderer.domElement; + let oWidth = oco.offsetWidth; + let oHeight = oco.offsetHeight; + let oRatio = oWidth / oHeight; + let width = param.width || 512; + let height = param.height || width; + let nRatio = width / height; + let ncv = document.createElement('canvas'); + ncv.width = width; + ncv.height = height; + let nco = ncv.getContext('2d'); + let ox = 0, oy = 0; + if (oRatio > nRatio) { + let tmp = oWidth; + oWidth = oHeight * nRatio; + ox = (tmp - oWidth) / 2; + } else { + let tmp = oHeight; + oHeight = oWidth * nRatio; + oy = (tmp - oHeight) / 2; + } + nco.drawImage(oco, ox, oy, oWidth, oHeight, 0, 0, width, height); + if (param.out) { + ncv.toBlob(blob => blob.arrayBuffer().then(png => param.out({ png, width, height }))); + } + return ncv; + }, + + internals: () => { + return { renderer, camera, platform }; + }, + + init: (domelement, slider, ortho) => { + container = domelement; + + WORLD.rotation.x = -PI2; + SCENE.add(WORLD); + + domelement.style.width = width(); + domelement.style.height = height(); + + renderer = new WebGLRenderer({ + antialias: antiAlias, + preserveDrawingBuffer: true, + logarithmicDepthBuffer: true + }); + + // THREE.ColorManagement.enabled = false; + // renderer.outputColorSpace = THREE.LinearSRGBColorSpace; + + renderer.localClippingEnabled = true; + camera = ortho ? + new THREE.OrthographicCamera(-100 * aspect(), 100 * aspect(), 100, -100, 0.1, 100000) : + new THREE.PerspectiveCamera(perspective, aspect(), 0.1, 100000); + + camera.position.set(0, 200, 340); + renderer.setSize(width(), height()); + domelement.appendChild(renderer.domElement); + + raycaster = new THREE.Raycaster(); + + viewControl = new Orbit(camera, domelement, (position, moved) => { + if (platform) { + platform.visible = hidePlatformBelow ? + initialized && position.y >= 0 && showPlatform : showPlatform; + volume.visible = showVolume && platform.visible; + } + if (grid.view) { + grid.view.visible = hideGridBelow ? platform.visible : showGrid; + } + if (cameraLight) { + cameraLight.position.copy(camera.position); + } + if (moved && platformOnMove) { + clearTimeout(platformMoveTimer); + platformMoveTimer = setTimeout(platformOnMove, 500); + Space.scene.updateFog(); + } + updateLastAction(); + updateFocus(); + }, (val) => { + if (camera && grid?.origin?.scale) { + grid.origin.scale(); + } + if (camera && viewControl) { + // increase intersect line precision on zoom + const dist = camera.position.distanceTo(viewControl.target); + raycaster.params.Line.threshold = Math.min(1, dist / 100); + } + // broker.publish("space.view.zoom", camera); + updateLastAction(); + if (slider) slider(val); + }); + + viewControl.noKeys = true; + viewControl.maxDistance = 1000; + + SCENE.add(skyAmbient = new THREE.AmbientLight(0x707070)); + + platform = new THREE.Mesh( + new THREE.BoxGeometry(1, 1, 1), + platformMaterial + ); + + platform.position.y = platformZOff; + platform.rotation.x = -PI2; + platform.visible = showPlatform; + + trackPlane = new THREE.Mesh( + new THREE.PlaneGeometry(100000, 100000, 1, 1), + new THREE.MeshBasicMaterial( { color: 0x777777, opacity: 0, transparent: true } ) + ); + trackPlane.visible = false; + trackPlane.rotation.x = PI2; + + let skygrid = new THREE.Mesh( + new THREE.BoxGeometry(10000, 10000, 10000, 10, 10, 10), + skyGridMaterial = + new THREE.MeshBasicMaterial({ color: skyGridColor, side: THREE.DoubleSide }) + ); + + SCENE.add(platform); + SCENE.add(trackPlane); + + if (showSkyGrid) { + skygrid.material.wireframe = true; + SCENE.add(skygrid); + } + + addEventHandlers(WIN, [ + 'resize', onResize, + 'mousemove', onMouseMove, + 'mousedown', onMouseDown, + 'mouseup', onMouseUp, + 'keypress', keyHandler, + 'touchstart', updateLastAction, + 'touchmove', updateLastAction, + 'touchend', updateLastAction + ]); + + let animates = 0; + let rateStart = Date.now(); + let renderStart; + let renders = []; + + function animate() { + animates++; + const now = Date.now(); + if (now - rateStart > 1000) { + // compute stats roughly every second + const delta = now - rateStart; + fps = 1000 * animates / delta; + animates = 0; + rateStart = now; + // look for slowest rendered frame + renderTime = Math.max(0, ...renders); + renders.length = 0; + } + requestAnimationFrame(animate); + if (docVisible && !freeze && Date.now() - lastAction < 1500) { + renderStart = Date.now(); + renderer.render(SCENE, camera); + // track frame render times + renders.push(Date.now() - renderStart); + } else { + fps = 0; + } + } + + animate(); + + const ctx = renderer.getContext(); + + Space.info = { + ver: ctx.getParameter(ctx.VERSION), + ven: ctx.getParameter(ctx.VENDOR), + glr: ctx.getParameter(ctx.RENDERER), + pla: nav.platform, + mob: nav.maxTouchPoints > 1 || (/android/i.test(nav.userAgent)) + }; + + initialized = true; + } +}; + +let cycle = [ + Space.view.front, + Space.view.right, + Space.view.back, + Space.view.left, +]; + +let cycleInd = 0; + +export const space = Space; diff --git a/src/moto/webui.js b/src/moto/webui.js index bc77f910..6bb0704c 100644 --- a/src/moto/webui.js +++ b/src/moto/webui.js @@ -1,10 +1,5 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -// dep: moto.license -// dep: add.array -gapp.register("moto.webui", [], (root, exports) => { let nextid = 1; @@ -47,8 +42,11 @@ function build(data, context) { func[key] = val; elid = elid || `_${nextid++}`; } else if (tov === 'object') { - if (val.length) - html.push(` ${key}="${val.filter(v => v !== undefined).join(' ')}"`); + if (Array.isArray(val)) { + html.push(` ${key}="${val.filter(v => v !== undefined).join(' ')}"`); + } else { + html.push(` ${key}="${val}"`); + } } else if (tov !== 'undefined') { html.push(` ${key}="${val}"`); } @@ -77,7 +75,7 @@ function build(data, context) { } // core html builder funtions -let h = exports({ +const h = { bind: (el, data, opt = {}) => { let ctx = []; let html = build(data, ctx).join(''); @@ -118,43 +116,39 @@ let h = exports({ build: (data) => { return build(data, []).join(''); } -}); +}; // window ui helpers -gapp.overlay(root, { - $: (id) => { - return document.getElementById(id); - }, +const $ = (id) => { + return document.getElementById(id); +}; - $d: (id, v) => { - $(id).style.display = v; - }, +const $d = (id, v) => { + $(id).style.display = v; +}; - $h: (id, h) => { - $(id).innerHTML = h; - }, +const $h = (id, h) => { + $(id).innerHTML = h; +}; - $c: (id, add, del) => { - if (add) $(id).classList.add(add); - if (del) $(id).classList.remove(del); - }, +const $c = (id, add, del) => { + if (add) $(id).classList.add(add); + if (del) $(id).classList.remove(del); +}; - $C: (clazz, add, del) => { - let el = [...document.getElementsByClassName(clazz)]; - for (let e of el) { - if (add) e.classList.add(add); - if (del) e.classList.remove(del); - } - return el; - }, - - h, - - estop: (evt) => { - evt.stopPropagation(); - evt.preventDefault(); +const $C = (clazz, add, del) => { + let el = [...document.getElementsByClassName(clazz)]; + for (let e of el) { + if (add) e.classList.add(add); + if (del) e.classList.remove(del); } -}); + return el; +}; + +const estop = (evt) => { + evt.stopPropagation(); + evt.preventDefault(); +}; // add common element types [ @@ -166,4 +160,12 @@ gapp.overlay(root, { } }); -}); +export { + $, + $d, + $h, + $c, + $C, + h, + estop +}; diff --git a/src/moto/worker.js b/src/moto/worker.js index 245815e5..b421f2e7 100644 --- a/src/moto/worker.js +++ b/src/moto/worker.js @@ -1,14 +1,9 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -gapp.register("moto.worker", [], (root, exports) => { - let endpoints = {}; // code is running in the worker / server context -const dispatch = exports({ - +const dispatch = { // bind an endpoint name to a function bind(name, fn) { endpoints[name] = fn; @@ -95,10 +90,9 @@ const dispatch = exports({ send.error(`no registered endpoint: ${msg.task}`); } } - -}); +}; // attach onmessage hanler self.onmessage = dispatch.onmessage; -}); +export const worker = dispatch; diff --git a/src2/webpack/esbuild.config.mjs b/src/webpack/esbuild.config.mjs similarity index 88% rename from src2/webpack/esbuild.config.mjs rename to src/webpack/esbuild.config.mjs index 3d82dc8d..5d7f420d 100644 --- a/src2/webpack/esbuild.config.mjs +++ b/src/webpack/esbuild.config.mjs @@ -8,14 +8,14 @@ const isProd = mode === 'prod'; console.log(`Building in ${mode} mode...`); -const MESH_OUTFILE = 'src2/pack/mesh-main.js'; +const MESH_OUTFILE = 'src/pack/mesh-main.js'; const MESH_EXTRAS = [ // 'mod/*/electron.js', // 'mod/*/tra0m.js', // 'mod/*/src/space.js' ]; -const KIRI_OUTFILE = 'src2/pack/kiri-main.js'; +const KIRI_OUTFILE = 'src/pack/kiri-main.js'; const KIRI_EXTRAS = [ // 'mod/*/electron.js', // 'mod/*/sync.js', @@ -51,7 +51,7 @@ async function buildApp() { try { // Bundle mesh main app await build({ - entryPoints: [ 'src2/main/mesh.js' ], + entryPoints: [ 'src/main/mesh.js' ], bundle: true, outfile: MESH_OUTFILE, format: 'esm', @@ -69,9 +69,9 @@ async function buildApp() { // Bundle mesh worker await build({ - entryPoints: [ 'src2/mesh/work.js' ], + entryPoints: [ 'src/mesh/work.js' ], bundle: true, - outfile: 'src2/pack/mesh-work.js', + outfile: 'src/pack/mesh-work.js', format: 'esm', external: ['module'], sourcemap: !isProd, // true for dev, false for prod @@ -85,7 +85,7 @@ async function buildApp() { // Bundle kiri main app await build({ - entryPoints: [ 'src2/main/kiri.js' ], + entryPoints: [ 'src/main/kiri.js' ], bundle: true, outfile: KIRI_OUTFILE, format: 'esm', @@ -103,9 +103,9 @@ async function buildApp() { // Bundle kiri worker await build({ - entryPoints: [ 'src2/kiri-run/worker.js' ], + entryPoints: [ 'src/kiri-run/worker.js' ], bundle: true, - outfile: 'src2/pack/kiri-work.js', + outfile: 'src/pack/kiri-work.js', format: 'esm', external: ['module'], sourcemap: !isProd, // true for dev, false for prod @@ -119,9 +119,9 @@ async function buildApp() { // Bundle kiri minion await build({ - entryPoints: [ 'src2/kiri-run/minion.js' ], + entryPoints: [ 'src/kiri-run/minion.js' ], bundle: true, - outfile: 'src2/pack/kiri-pool.js', + outfile: 'src/pack/kiri-pool.js', format: 'esm', external: ['module'], sourcemap: !isProd, // true for dev, false for prod diff --git a/src2/webpack/webpack-three-bundle.js b/src/webpack/webpack-three-bundle.js similarity index 100% rename from src2/webpack/webpack-three-bundle.js rename to src/webpack/webpack-three-bundle.js diff --git a/src2/webpack/webpack-three-esm.js b/src/webpack/webpack-three-esm.js similarity index 100% rename from src2/webpack/webpack-three-esm.js rename to src/webpack/webpack-three-esm.js diff --git a/src2/data/local.js b/src2/data/local.js deleted file mode 100644 index 41116f9c..00000000 --- a/src2/data/local.js +++ /dev/null @@ -1,41 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -class Local { - __data__ = {}; - __mem__ = true; - - getItem(key) { - return this[key]; - } - - setItem(key, val) { - this.__data__[key] = val; - this[key] = val; - } - - removeItem(key) { - delete this.__data__[key]; - } - - clear() { - this.__data__ = {}; - } -} - -let setLocal; - -try { - // deprecate 'Local' at some point - setLocal = self.localStorage; - let testkey = '__test'; - setLocal.setItem(testkey, 1); - setLocal.getItem(testkey); - setLocal.removeItem(testkey); -} catch (e) { - setLocal = new Local(); - let msg = "localStorage disabled: application may not function properly"; - console.log(msg); - // alert(msg); -} - -export const local = setLocal; diff --git a/src2/kiri-mode/cam/client.js b/src2/kiri-mode/cam/client.js deleted file mode 100644 index efa1e8f1..00000000 --- a/src2/kiri-mode/cam/client.js +++ /dev/null @@ -1,890 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import { $, h } from '../../moto/webui.js'; -import { api } from '../../kiri/api.js'; -import { load } from '../../load/file.js'; -import { animate as anim_2d, animate_clear as anim_2d_clear } from './anim-2d-fe.js'; -import { animate2 as anim_3d, animate_clear2 as anim_3d_clear } from './anim-3d-fe.js'; -import { space as SPACE } from '../../moto/space.js'; -import { Layers } from '../../kiri/layers.js'; -import { Stack } from '../../kiri/stack.js'; -import { updateStock } from './cl-stock.js'; -import { createPopOps } from './cl-ops.js'; -import { tabAdd, tabDone, tabClear, restoreTabs, updateTabs } from './cl-tab.js'; -import { traceOn, traceDone, unselectTraces } from './cl-trace.js'; -import { holeSelOn, selectHolesDone } from './cl-hole.js'; -import { surfaceOn, surfaceDone } from './cl-surface.js'; - -const DEG2RAD = Math.PI / 180; -const RAD2DEG = 180 / Math.PI; -const hasSharedArrays = self.SharedArrayBuffer ? true : false; - -const { VIEWS, STACKS } = api.const; -const { noop, space } = api; -const { ui: UI, uc: UC } = api; -const { widgets: WIDGETS } = api; - -class Client { - animVer = 0; - camStock; - current; - currentIndex; - flipping; - func = {}; - hoveredOp; - hover = noop; - hoverUp = noop; - isAnimate; - isArrange; - isPreview; - isCamMode; - isIndexed; - isParsed; - lastMode; - popOp = {}; - poppedRec; - zaxis = { x: 0, y: 0, z: 1 }; -} - -export const env = new Client(); - -export function isDark() { - return api.space.is_dark() -} - -export function clearPops() { - if (env.func.unpop) env.func.unpop(); - tabDone(); - traceDone(); - surfaceDone(); - selectHolesDone(); -} - -function animFn() { - return [{ - animate: anim_2d, - animate_clear: anim_2d_clear - }, { - animate: anim_3d, - animate_clear: anim_3d_clear - }][env.animVer]; -} - -function updateIndex() { - let oplist = env.current.process.ops; - if (!(env.isCamMode && oplist) || env.lastMode === VIEWS.ANIMATE) { - return; - } - let index = 0; - for (let op of oplist) { - if (op.type === '|') { - break; - } - if (op.type === 'index' && !op.disabled) { - if (op.absolute) { - index = op.degrees - } else { - index += op.degrees; - } - } - } - WIDGETS.setAxisIndex(env.isPreview || !env.isIndexed ? 0 : -index); - env.currentIndex = env.isIndexed && !env.isPreview ? index * DEG2RAD : 0; -} - -function updateAxisMode(refresh) { - const { camStockIndexGrid, camStockIndexed } = env.current.process; - let newIndexed = camStockIndexed; - let changed = refresh || env.isIndexed !== newIndexed; - env.isIndexed = newIndexed; - if (!env.isIndexed || !env.isCamMode) { - WIDGETS.setAxisIndex(0); - } - if (!env.isCamMode) { - return; - } - if (env.isIndexed) { - env.current.process.camZAnchor = "middle"; - } - env.animVer = env.isIndexed ? 1 : 0; - SPACE.platform.setVisible(!env.isIndexed); - SPACE.platform.showGrid2(!env.isIndexed || camStockIndexGrid); - const showIndexed = env.isIndexed ? '' : 'none'; - const showNonIndexed = env.isIndexed ? 'none' : ''; - $('cam-index').style.display = showIndexed; - $('cam-lathe').style.display = showIndexed; - $('cam-flip').style.display = showNonIndexed; - $('cam-reg').style.display = showNonIndexed; - if (!changed) { - return; - } - WIDGETS.setIndexed(env.isIndexed ? true : false); - api.platform.update_bounds(); - // add or remove clock op depending on indexing - const cp = env.current.process; - if (!cp.ops) { - return; - } - const clockOp = cp.ops.filter(op => op.type === '|')[0]; - if (!clockOp) { - opAdd(env.popOp['|'].new()); - } else { - opRender(); - } - updateStock(); -} - -const opAddLaserOn = () => { - opAdd(env.popOp['laser on'].new()); -}; - -const opAddLaserOff = () => { - opAdd(env.popOp['laser off'].new()); -}; - -const opAddGCode = () => { - opAdd(env.popOp.gcode.new()); -}; - -const opAddIndex = () => { - opAdd(env.popOp.index.new()); -}; - -const opAddLevel = () => { - opAdd(env.popOp.level.new()); -}; - -const opAddRough = () => { - opAdd(env.popOp.rough.new()); -}; - -const opAddOutline = () => { - opAdd(env.popOp.outline.new()); -}; - -const opAddPocket = () => { - traceDone(); - surfaceDone(); - let rec = env.popOp.pocket.new(); - rec.surfaces = { /* widget.id: [ faces... ] */ }; - opAdd(rec); -}; - -const opAddContour = (axis) => { - let rec = env.popOp.contour.new(); - rec.axis = axis.toUpperCase(); - opAdd(rec); -}; - -const opAddLathe = (axis) => { - let rec = env.popOp.lathe.new(); - rec.axis = axis.toUpperCase(); - opAdd(rec); -}; - -const opAddTrace = () => { - let rec = env.popOp.trace.new(); - rec.areas = { /* widget.id: [ polygons... ] */ }; - opAdd(rec); -}; - -const opAddDrill = () => { - let rec = env.popOp.drill.new(); - rec.drills = {}; - opAdd(rec); -}; - -const opAddRegister = (axis, points) => { - let rec = env.popOp.register.new(); - rec.axis = axis.toUpperCase(); - rec.points = points; - opAdd(rec); -}; - -const opAddFlip = () => { - opAdd(env.popOp.flip.new()); -}; - -const traceClear = () => { - traceDone(); - api.widgets.all().forEach(widget => { - unselectTraces(widget); - }); - api.conf.save(); -}; - -const opAdd = (rec) => { - if (!env.isCamMode) return; - clearPops(); - let oplist = env.current.process.ops; - if (oplist.indexOf(rec) < 0) { - if (oplist.length && oplist[oplist.length - 1].type === '|') { - oplist.splice(oplist.length - 1, 0, rec); - } else { - oplist.push(rec); - } - let fpos = oplist.findWith(rec => rec.type === 'flip'); - if (fpos >= 0 && oplist.length > 1) { - let oprec = oplist.splice(fpos, 1); - oplist.push(oprec[0]); - } - api.conf.save(); - opRender(); - } -}; - -const opDel = (rec) => { - if (!env.isCamMode) return; - clearPops(); - let oplist = env.current.process.ops; - let pos = oplist.indexOf(rec); - if (pos >= 0) { - oplist.splice(pos, 1); - api.conf.save(); - opRender(); - } -}; - -export function opRender() { - let oplist = env.current.process.ops; - if (!(env.isCamMode && oplist)) { - return; - } - oplist = oplist.filter(rec => !Array.isArray(rec)); - let mark = Date.now(); - let html = []; - let bind = {}; - let scale = api.view.unit_scale(); - let notime = false; - oplist.forEach((rec, i) => { - let title = ''; - let clock = rec.type === '|'; - let label = clock ? `` : rec.type; - let clazz = notime ? ["draggable", "notime"] : ["draggable"]; - let notable = rec.note ? rec.note.split(' ').filter(v => v.charAt(0) === '#') : undefined; - if (clock) { clazz.push('clock'); title = ` title="end of ops chain\ndrag/drop like an op\nops after this are disabled"` } - if (notable?.length) label += ` (${notable[0].slice(1)})`; - html.appendAll([ - `
`, - ``, - clock ? '' : - ``, - `
` - ]); - bind[mark + i] = rec; - notime = notime || clock; - }); - let listel = $('oplist'); - listel.innerHTML = html.join(''); - let bounds = []; - let unpop = null; - let index = 0; - let indexing = true; - // drag and drop re-ordering - for (let [id, rec] of Object.entries(bind)) { - let type = rec.type; - let clock = type === '|'; - if (!clock) { - $(`${id}-x`).onmousedown = (ev) => { - ev.stopPropagation(); - ev.preventDefault(); - surfaceDone(); - traceDone(); - tabDone(); - opDel(rec); - }; - } else { - indexing = false; - } - let el = $(id); - if (!env.isIndexed && type === 'lathe') { - rec.disabled = true; - } - if (!hasSharedArrays && (type === 'contour' || type === 'lathe')) { - rec.disabled = true; - } - if (rec.disabled) { - el.classList.add("disabled"); - } - bounds.push(el); - let timer = null; - let inside = true; - let popped = false; - let poprec = env.popOp[rec.type]; - if (type === 'index' && indexing && !rec.disabled) { - index = rec.absolute ? rec.degrees : index + rec.degrees; - } - el.rec = rec; - el.unpop = () => { - let pos = [...el.childNodes].indexOf(poprec.div); - if (pos >= 0) { - el.removeChild(poprec.div); - } - popped = false; - }; - function onEnter(ev) { - if ((surfaceOn || traceOn) && env.poppedRec != rec) { - return; - } - if (popped && env.poppedRec != rec) { - surfaceDone(); - traceDone(); - } - if (unpop) unpop(); - env.func.unpop = unpop = el.unpop; - inside = true; - // pointer to current rec for trace editing - env.poppedRec = rec; - popped = true; - poprec.use(rec); - env.hoveredOp = el; - if (!clock) { - // offset Y position of pop div by % of Y screen location of button - el.appendChild(poprec.div); - poprec.addNote(); - const { innerHeight } = window; - const brect = ev.target.getBoundingClientRect(); - const prect = poprec.div.getBoundingClientRect(); - const pcty = (brect.top / innerHeight) * 0.9; - const offpx = -pcty * prect.height; - poprec.div.style.transform = `translateY(${offpx}px)`; - } - // option click event appears latent - // and overides the sticky settings - setTimeout(() => { - UC.setSticky(false); - }, 0); - } - function onLeave(ev) { - inside = false; - clearTimeout(timer); - timer = setTimeout(() => { - if (!inside && poprec.using(rec) && !UC.isSticky()) { - el.unpop(); - } - }, 250); - } - function onDown(ev) { - if (!ev.target.rec) { - // only trigger on operation buttons bound to recs - return; - } - let mobile = ev.touches; - surfaceDone(); - traceDone(); - let target = ev.target, clist = target.classList; - if (!clist.contains("draggable")) { - return; - } - // toggle enable / disable - if (!clock && (ev.ctrlKey || ev.metaKey)) { - ev.preventDefault(); - ev.stopPropagation(); - rec.disabled = !rec.disabled; - for (let op of ev.shiftKey ? oplist : [rec]) { - if (op !== rec) { - op.disabled = op.type !== '|' ? !rec.disabled : false; - } - } - for (let el of bounds) { - if (el.rec.disabled) { - el.classList.add("disabled"); - } else { - el.classList.remove("disabled"); - } - } - if (env.isIndexed) { - updateIndex(); - } - return true; - } - // duplicate op - if (!clock && ev.shiftKey) { - ev.preventDefault(); - ev.stopPropagation(); - oplist = env.current.process.ops; - oplist.push(Object.clone(rec)); - api.conf.save(); - opRender(); - return true; - } - clist.add("drag"); - ev.stopPropagation(); - ev.preventDefault(); - let tracker = UI.tracker; - tracker.style.display = 'block'; - let cancel = tracker.onmouseup = (ev) => { - oplist = env.current.process.ops; - clist.remove("drag"); - tracker.style.display = 'none'; - if (ev) { - ev.stopPropagation(); - ev.preventDefault(); - } - oplist.length = 0; - for (let child of listel.childNodes) { - oplist.push(child.rec); - } - api.conf.save(); - opRender(); - if (mobile) { - el.ontouchmove = onDown; - el.ontouchend = undefined; - } - }; - function onMove(ev) { - ev.stopPropagation(); - ev.preventDefault(); - if (ev.buttons === 0) { - return cancel(); - } - for (let el of bounds) { - if (el === target) continue; - let rect = el.getBoundingClientRect(); - let top = rect.top; - let bottom = rect.bottom;// + rect.height; - let tar = mobile ? ev.touches[0] : ev; - if (tar.pageY >= top && tar.pageY <= bottom) { - let mid = (top + bottom) / 2; - try { listel.removeChild(target); } catch (e) { } - el.insertAdjacentElement(tar.pageY < mid ? "beforebegin" : "afterend", target); - } - } - } - tracker.onmousemove = onMove; - if (mobile) { - el.ontouchmove = onMove; - el.ontouchend = cancel; - } - } - if (SPACE.info.mob) { - let touched = false; - el.ontouchstart = (ev) => { - touched = true; - if (env.poppedRec === rec && popped) { - onLeave(ev); - } else { - onEnter(ev); - } - }; - el.ontouchmove = onDown; - el.onmouseenter = (ev) => { - if (touched) { - // touches block mouse events on touchscreen PCs - // which are often sent along with touch events - // but when not touched, allow the mouse to work - return; - } - el.onmousedown = onDown; - el.onmouseleave = onLeave; - onEnter(ev); - }; - } else { - el.onmousedown = onDown; - el.onmouseenter = onEnter; - el.onmouseleave = onLeave; - } - } - if (env.lastMode !== VIEWS.ANIMATE) { - // update widget rotations from timeline marker - WIDGETS.setAxisIndex(env.isPreview || !env.isIndexed ? 0 : -index); - } - env.currentIndex = env.isIndexed && !env.isPreview ? index * DEG2RAD : 0; - updateStock(); -} - -export function init() { - - api.event.on('tool.mesh.face-normal', normal => { - // console.log({ env.poppedRec }); - env.poppedRec.degrees = (Math.atan2(normal.y, normal.z) * RAD2DEG).round(2); - env.poppedRec.absolute = true; - opRender(); - updateStock(); - }); - - api.event.on("cam.trace.clear", traceClear); - - api.event.on("cam.parse.gerber", opts => { - const { data, mesh } = opts; - const { open, closed, circs, rects } = load.GBR.parse(data); - const stack = new Stack(mesh || space.world.newGroup()); - const layers = new Layers(); - for (let poly of open) { - layers.setLayer("open", { line: 0xff8800 }, false).addPoly(poly); - let diam = poly.tool?.shape?.diameter; - if (diam) { - const exp = poly.offset_open(diam, 'round'); - layers.setLayer("open-exp", { line: 0xff5555 }, false).addPolys(exp); - } - } - for (let poly of closed) { - layers.setLayer("close", { line: 0xff0000 }, false).addPoly(poly); - } - for (let poly of circs) { - layers.setLayer("circs", { line: 0x008800 }, false).addPoly(poly); - } - for (let poly of rects) { - layers.setLayer("rects", { line: 0x0000ff }, false).addPoly(poly); - } - stack.addLayers(layers); - }); - - api.event.on("widget.add", widget => { - if (env.isCamMode && !Array.isArray(widget)) { - updateAxisMode(true); - widget.setIndexed(env.isIndexed ? true : false); - api.platform.update_bounds(); - } - }); - - // wire up animate button in ui - api.event.on("function.animate", (mode) => { - if (env.isAnimate || !env.isCamMode) { - return; - } - api.function.prepare(() => { - if (env.isCamMode) { - animate(); - } - }); - }); - - api.event.on("function.export", (mode) => { - if (env.isAnimate) { - env.isAnimate = false; - animFn().animate_clear(api); - } - }); - - api.event.on("mode.set", (mode) => { - env.isIndexed = undefined; - env.isCamMode = mode === 'CAM'; - SPACE.platform.setColor(env.isCamMode ? 0xeeeeee : 0xcccccc); - api.uc.setVisible(UI.func.animate, env.isCamMode); - // hide/show cam mode elements - for (let el of [...document.getElementsByClassName('mode-cam')]) { - api.uc.setClass(el, 'hide', !env.isCamMode); - } - if (!env.isCamMode) { - clearPops(); - tabClear(); - } - // do not persist traces across page reloads - traceClear(); - opRender(); - updateStock(); - }); - - api.event.on("view.set", (mode) => { - env.lastMode = mode; - env.isArrange = (mode === VIEWS.ARRANGE); - env.isPreview = (mode === VIEWS.PREVIEW); - env.isAnimate = (mode === VIEWS.ANIMATE); - animFn().animate_clear(api); - clearPops(); - if (env.isCamMode && env.isPreview) { - WIDGETS.setAxisIndex(0); - } - updateStock(); - opRender(); - api.uc.setVisible($('layer-animate'), env.isAnimate && env.isCamMode); - }); - - api.event.on("settings.saved", (settings) => { - validateTools(settings.tools); - env.current = settings; - let proc = settings.process; - let hasTabs = false; - let hasTraces = false; - if (env.isCamMode && proc.ops) { - proc.ops = proc.ops.filter(v => v); - } - // for any tabs or traces to set markers - for (let widget of api.widgets.all()) { - let wannot = widget.anno; - if (wannot.tab && wannot.tab.length) hasTabs = true; - if (wannot.trace && wannot.trace.length) hasTraces = true; - } - api.platform.update_bounds(); - updateIndex(); - updateStock(); - updateAxisMode(); - if (!env.poppedRec) { - opRender(); - } - }); - - api.event.on("settings.load", (settings) => { - opRender(); - if (!env.isCamMode) return; - validateTools(settings.tools); - restoreTabs(api.widgets.all()); - updateAxisMode(); - }); - - api.event.on("cam.stock.toggle", (bool) => { - env.camStock && (env.camStock.visible = bool ?? !env.camStock.visible); - }); - - api.event.on("boolean.click", api.platform.update_bounds); - - api.event.on([ - "init-done", - "view.set", - // update stock when modes change? - "slice.end", - "preview.end", - // update stock when preferences change - // "boolean.click", - "boolean.update", - // update stock when objects move - "platform.layout", - "selection.drag", - "selection.move", - // force bounds update, too - "selection.scale", - "selection.rotate" - ], updateStock); - - // invalidate trace and drill ops on scale or rotate - api.event.on([ - "selection.scale", - "selection.rotate" - ], () => { - if (!env.isCamMode) return; - for (let op of env.current.process.ops) { - if (op.type === 'trace' && !env.flipping) { - op.areas = {}; - } - else if (op.type === 'drill' && !env.flipping) { - op.drills = {}; - } - } - }); - - // invalidate tabs when scaleds - api.event.on([ - "selection.scale", - ], () => { - tabClear(); - }); - - api.event.on([ - // update tab color/opacity on dark/light change - "boolean.update" - ], updateTabs); - - api.event.on("preview.end", () => { - env.isParsed = false; - if (env.isCamMode) { - let bounds = STACKS.getStack("bounds"); - if (bounds) bounds.button("animate", animate); - } - }); - - api.event.on("code.loaded", (info) => { - if (env.isCamMode) { - env.isParsed = true; - let parse = STACKS.getStack("parse", SPACE.world); - if (parse) parse.button("animate", animate); - } - }); - - $('op-add').onmouseenter = () => { - if (env.func.unpop) env.func.unpop(); - }; - - $('op-add-list').onclick = (ev) => { - let settings = api.conf.get(); - let { process, device } = settings; - switch (ev.target.innerText.toLowerCase()) { - case "index": return opAddIndex(); - case "laser on": return opAddLaserOn(); - case "laser off": return opAddLaserOff(); - case "gcode": return opAddGCode(); - case "level": return opAddLevel(); - case "rough": return opAddRough(); - case "outline": return opAddOutline(); - case "contour": - let caxis = "X"; - for (let op of env.current.process.ops) { - if (op.type === "contour" && op.axis === "X") { - caxis = "Y"; - } - } - return opAddContour(caxis); - case "lathe": - let laxis = "X"; - for (let op of env.current.process.ops) { - if (op.type === "lathe" && op.axis === "X") { - laxis = "Y"; - } - } - return opAddLathe(laxis); - case "register": return opAddRegister('X', 2); - case "drill": return opAddDrill(); - case "trace": return opAddTrace(); - case "pocket": return opAddPocket(); - case "flip": - // only one flip op permitted - for (let op of env.current.process.ops) { - if (op.type === 'flip') { - return; - } - } - return opAddFlip(); - } - }; - - // TAB/TRACE BUTTON HANDLERS - api.event.on("button.click", target => { - let process = api.conf.get().process; - switch (target) { - case api.ui.tabAdd: - return tabAdd(); - case api.ui.tabDun: - return tabDone(); - case api.ui.tabClr: - api.uc.confirm("clear tabs?").then(ok => { - if (ok) tabClear(); - }); - break; - } - }); - - api.event.on("cam.op.add", opAdd); - - api.event.on("cam.op.del", opDel); - - api.event.on("cam.op.render", opRender); - - api.event.on("cam.tabs.add", tabAdd); - - api.event.on("cam.tabs.done", tabDone); - - api.event.on("cam.tabs.clear", tabClear); - - // COMMON TAB/TRACE EVENT HANDLERS - api.event.on("slice.begin", () => { - if (env.isCamMode) { - clearPops(); - } - }); - - api.event.on("key.esc", () => { - if (env.isCamMode) { - clearPops(); - } - }); - - api.event.on("selection.scale", () => { - if (env.isCamMode) { - clearPops(); - } - }); - - api.event.on("widget.duplicate", (widget, oldwidget) => { - if (!env.isCamMode) { - return; - } - if (traceOn) { - traceDone(); - } - unselectTraces(widget); - if (env.flipping) { - return; - } - let ann = api.widgets.annotate(widget.id); - if (ann.tab) { - ann.tab.forEach((tab, i) => { - tab.id = Date.now() + i; - }); - restoreTabs([widget]); - } - }); - - api.event.on("widget.mirror", widget => { - if (!env.isCamMode) { - return; - } - if (traceOn) { - traceDone(); - } - if (holeSelOn) { - selectHolesDone(); - } - clearHolesRec(widget) - unselectTraces(widget); - if (env.flipping) { - return; - } - mirrorTabs(widget); - }); - - api.event.on("widget.rotate", rot => { - if (!env.isCamMode) { - return; - } - let { widget, x, y, z } = rot; - if (traceOn) { - traceDone(); - } - unselectTraces(widget); - if (holeSelOn) { - selectHolesDone(); - } - if (env.flipping) { - return; - } - clearHolesRec(widget) - if (x || y) { - clearTabs(widget); - } else { - rotateTabs(widget, x, y, z); - } - }); - - api.event.on("mouse.hover.up", rec => { - if (!env.isCamMode) { - return; - } - let { object, event } = rec; - env.hoverUp(object, event); - }); - - api.event.on("mouse.hover", data => { - if (!env.isCamMode) { - return; - } - env.hover(data); - }); - - createPopOps(); -} - -function validateTools(tools) { - if (tools) { - let max = 0; - for (let t of tools) { - if (Number.isInteger(t.number)) { - max = Math.max(max, t.number); - } - } - for (let t of tools) { - if (!Number.isInteger(t.number)) { - t.number = ++max; - console.log('added tool #', t); - } - } - } -} - -function animate() { - env.isAnimate = true; - api.widgets.opacity(env.isParsed ? 0 : 0.75); - api.hide.slider(); - STACKS.clear(); - animFn().animate(api); - api.view.set_animate(); -} diff --git a/src2/kiri-mode/cam/ops.js b/src2/kiri-mode/cam/ops.js deleted file mode 100644 index 2eb1dbd8..00000000 --- a/src2/kiri-mode/cam/ops.js +++ /dev/null @@ -1,37 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import { OpXRay } from './op-xray.js'; -import { OpShadow } from './op-shadow.js'; -import { OpLevel } from './op-level.js'; -import { OpRough } from './op-rough.js'; -import { OpOutline } from './op-outline.js'; -import { OpContour } from './op-contour.js'; -import { OpPocket } from './op-pocket.js'; -import { OpLathe } from './op-lathe.js'; -import { OpTrace } from './op-trace.js'; -import { OpDrill } from './op-drill.js'; -import { OpRegister } from './op-register.js'; -import { OpLaserOn } from './op-laser-on.js'; -import { OpLaserOff } from './op-laser-off.js'; -import { OpGCode } from './op-gcode.js'; -import { OpIndex } from './op-index.js'; -import { CamOp } from './op.js'; - -export const ops = { - "xray": OpXRay, - "shadow": OpShadow, - "level": OpLevel, - "rough": OpRough, - "outline": OpOutline, - "contour": OpContour, - "pocket": OpPocket, - "lathe": OpLathe, - "trace": OpTrace, - "drill": OpDrill, - "register": OpRegister, - "laser on": OpLaserOn, - "laser off": OpLaserOff, - "gcode": OpGCode, - "index": OpIndex, - "flip": CamOp -}; diff --git a/src2/kiri-mode/cam/tools.js b/src2/kiri-mode/cam/tools.js deleted file mode 100644 index 33fc3aab..00000000 --- a/src2/kiri-mode/cam/tools.js +++ /dev/null @@ -1,344 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import { $ } from '../../moto/webui.js'; -import { api } from '../../kiri/api.js'; -import { consts } from '../../kiri/consts.js'; -import { settings as setconf } from '../../kiri/settings.js'; - -const DEG2RAD = Math.PI / 180; - -let { MODES } = consts, - DOC = document, - selectedTool = null, - editTools = null, - maxTool = 0, - toolNames = ['endmill','ballmill','tapermill','drill']; - -function settings() { - return api.conf.get(); -} - -function renderTools() { - api.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) }; - api.ui.toolSelect.appendChild(opt); - }); -} - -function selectTool(tool) { - const { ui } = api; - 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(' ')}>`); - }); - return tags.join(''); -} - -function renderTool(tool) { - const { ui } = api; - 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 = ''; - 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); -} - -export function updateTool(ev) { - const { ui } = api; - 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; - api.ui.toolsSave.disabled = !changed; -} - -export function showTools() { - if (api.mode.get_id() !== MODES.CAM) return; - setconf.sync.get().then(_showTools); -} - -function _showTools() { - const { ui } = api; - - 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(); - setconf.sync.put(); - }; - ui.toolsImport.onclick = (ev) => api.event.import(ev); - ui.toolsExport.onclick = () => { - api.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(); -} - -export const tools = { - updateTool, - showTools -}; diff --git a/src2/kiri-mode/drag/driver.js b/src2/kiri-mode/drag/driver.js deleted file mode 100644 index a6b5c853..00000000 --- a/src2/kiri-mode/drag/driver.js +++ /dev/null @@ -1,13 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import { LASER, TYPE } from '../laser/driver.js'; - -function init(worker) { - // console.log({ DRAGKNIFE_INIT: worker }); -} - -export const DRAG = Object.assign({}, LASER, { - type: TYPE.DRAG, - name: 'DragKnife', - init -}); diff --git a/src2/kiri-mode/fdm/driver.js b/src2/kiri-mode/fdm/driver.js deleted file mode 100644 index ea0deabd..00000000 --- a/src2/kiri-mode/fdm/driver.js +++ /dev/null @@ -1,22 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -// shared by client and worker contexts -export 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; -} diff --git a/src2/kiri-mode/sla/client.js b/src2/kiri-mode/sla/client.js deleted file mode 100644 index c613c88c..00000000 --- a/src2/kiri-mode/sla/client.js +++ /dev/null @@ -1,86 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import { $ } from '../../moto/webui.js'; -import { api } from '../../kiri/api.js'; - -export const SLA = { - init, - printDownload -} - -export function init() { - api.event.on("mode.set", (mode) => { - if (mode === 'SLA') { - api.ui.func.preview.classList.add('hide'); - } else { - api.ui.func.preview.classList.remove('hide'); - } - }); -} - -export 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'); - - 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; - } -} - -function saveFile(api, file, ext) { - api.util.download(file, $('print-filename-sla').value + ext); - api.modal.hide(); -} diff --git a/src2/kiri-mode/wedm/driver.js b/src2/kiri-mode/wedm/driver.js deleted file mode 100644 index 3342abfd..00000000 --- a/src2/kiri-mode/wedm/driver.js +++ /dev/null @@ -1,21 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import { LASER, TYPE } from '../laser/driver.js'; - -const DEG2RAD = Math.PI / 180; - -function init(worker) { - // console.log({ WIREEDM_INIT: worker }); -} - -async function prepare(widgets, settings, update) { - widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); - LASER.prepare(widgets, settings, update); -} - -export const WEDM = Object.assign({}, LASER, { - type: TYPE.WEDM, - name: 'WireEDM', - init, - prepare -}); diff --git a/src2/kiri-mode/wjet/driver.js b/src2/kiri-mode/wjet/driver.js deleted file mode 100644 index b11c54fd..00000000 --- a/src2/kiri-mode/wjet/driver.js +++ /dev/null @@ -1,13 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import { LASER, TYPE } from '../laser/driver.js'; - -function init(worker) { - // console.log({ WATERJET_INIT: worker }); -} - -export const WJET = Object.assign({}, LASER, { - type: TYPE.WJET, - name: 'WaterJet', - init -}); diff --git a/src2/kiri/api.js b/src2/kiri/api.js deleted file mode 100644 index f3fed59b..00000000 --- a/src2/kiri/api.js +++ /dev/null @@ -1,175 +0,0 @@ -/** Copyright Stewart Allen -- 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 } 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 '../kiri/stats.js'; -import { widgets } from './widgets.js'; -import { updateTool } from '../kiri-mode/cam/tools.js'; - -import { LANG } from './lang.js'; -import { LOCAL, SETUP, SECURE } from './main.js'; -import { UI } from './ui.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 busy }, - 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), - 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 } }) } - }; - -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, - alerts, - busy, - catalog, - client: work, - clip, - clone, - color, - conf: settings.conf, - const: { LANG, LISTS: lists, LOCAL, MODES, SETUP, SECURE, STACKS, VIEWS }, - devel, - device, - devices, - dialog, - doit, - event, - feature, - function: functions, - group, - help, - hide, - image, - js2o, - language: LANG, - lists, - local, - modal, - mode, - noop, - o2js, - onkey, - platform, - probe, - process, - sdb: dataLocal, - selection, - settings, - show, - space, - stacks: STACKS, - stats, - tool: { - update: updateTool - }, - tweak, - uc: UC, - ui: {}, - util, - var: { - layer_lo: 0, - layer_hi: 0, - layer_max: 0 - }, - view, - widgets, - work, -}; - -// allow widget to straddle client / worker FOR NOW -self.kiri_api = api; diff --git a/src2/kiri/devices.js b/src2/kiri/devices.js deleted file mode 100644 index 419386cb..00000000 --- a/src2/kiri/devices.js +++ /dev/null @@ -1,382 +0,0 @@ -/** Copyright Stewart Allen -- 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/devices.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 { - 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); -} - diff --git a/src2/kiri/init.js b/src2/kiri/init.js deleted file mode 100644 index 517d4d09..00000000 --- a/src2/kiri/init.js +++ /dev/null @@ -1,2166 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import { $, $c } from '../moto/webui.js'; -import { api } from '../kiri/api.js'; -import { base } from '../geo/base.js'; -import { local as sdb } from '../data/local.js'; -import { space } from '../moto/space.js'; -import { LOCAL, SETUP } from './main.js'; -import { VIEWS, MODES, SEED } from '../kiri/consts.js'; -import { beta, version } from '../moto/license.js'; -import { settings as set_ctrl } from './settings.js'; -import { init as initCAM } from '../kiri-mode/cam/client.js'; -import { init as initDRAG } from '../kiri-mode/drag/client.js'; -import { init as initFDM } from '../kiri-mode/fdm/client.js'; -import { init as initLaser } from '../kiri-mode/laser/client.js'; -import { init as initSLA } from '../kiri-mode/sla/client.js'; -import { init as initWEDM } from '../kiri-mode/wedm/client.js'; -import { init as initWJET } from '../kiri-mode/wjet/client.js'; - -let { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, - { consts, client, catalog, platform, selection } = api, - LANG = api.language.current, - WIN = self.window, - DOC = self.document, - STARTMODE = SETUP.sm && SETUP.sm.length === 1 ? SETUP.sm[0] : null, - TWOD = [ LASER, DRAG, WJET, WEDM ], - TWONED = [ LASER, DRAG, WJET ], - THREED = [ FDM, CAM, SLA ], - GCODE = [ FDM, CAM, ...TWOD ], - CAM_LZR = [ CAM, ...TWOD ], - FDM_LZN = [ FDM, ...TWONED ], - NO_WEDM = [ FDM, CAM, SLA, LASER, DRAG, WJET ], - FDM_CAM = [ FDM, CAM ], - proto = location.protocol, - platformColor, - separator = true, - hideable = true, - inline = true, - driven = true, - trigger = true, - units = true, - ui = api.ui, - uc = api.uc; - -function settings() { - return api.conf.get(); -} - -function updateTool(ev) { - api.tool.update(ev); -} - -function checkSeed(then) { - // skip sample object load in onshape (or any script postload) - if (!sdb[SEED]) { - sdb[SEED] = new Date().getTime(); - if (!SETUP.s && api.feature.seed) { - if (SETUP.debug) { - return then(); - } - platform.load_stl("/obj/cube.stl", function(vert) { - catalog.putFile("sample cube.stl", vert); - platform.update_bounds(); - space.view.home(); - setTimeout(() => { api.space.save(true) },500); - then(); - api.help.show(); - }); - return true; - } - } - return false; -} - -function unitsSave() { - api.conf.update({ controller: true }); - platform.update_size(); -} - -function aniMeshSave() { - api.conf.update({ controller: true }); - api.conf.save(); -} - -function lineTypeSave() { - const sel = ui.lineType.options[ui.lineType.selectedIndex]; - if (sel) { - settings().controller.lineType = sel.value; - api.conf.save(); - } -} - -function thinWallSave() { - let opt = ui.sliceDetectThin; - let level = opt.options[opt.selectedIndex]; - if (level) { - settings().process.sliceDetectThin = level.value; - api.conf.save(); - } -} - -function detailSave() { - let level = ui.detail.options[ui.detail.selectedIndex]; - if (level) { - level = level.value; - let rez = base.config.clipperClean; - switch (level) { - case '100': rez = 50; break; - case '75': rez = base.config.clipperClean; break; - case '50': rez = 500; break; - case '25': rez = 1000; break; - } - client.config({ - base: { clipperClean: rez } - }); - settings().controller.detail = level; - api.conf.save(); - } -} - -function speedSave() { - settings().controller.showSpeeds = ui.showSpeeds.checked; - api.view.update_speeds(); -} - -function zAnchorSave() { - api.conf.update(); - api.platform.update_top_z(); -} - -function setThreaded(bool) { - if (bool) { - client.pool.start(); - } else { - client.pool.stop(); - } - return bool; -} - -// upon restore, seed presets -api.event.emit('preset', api.conf.dbo()); - -api.event.on("set.threaded", bool => setThreaded(bool)); - -function booleanSave() { - let control = settings().controller; - let doAlert = ui.ortho.checked !== control.ortho; - if (control.assembly != ui.assembly.checked) { - client.wasm(ui.assembly.checked); - } - if (control.antiAlias != ui.antiAlias.checked) { - api.show.alert('Page Reload Required to Change Aliasing'); - } - control.shiny = ui.shiny.checked; - control.drawer = ui.drawer.checked; - control.scrolls = ui.scrolls.checked; - control.showOrigin = ui.showOrigin.checked; - control.showRulers = ui.showRulers.checked; - control.autoLayout = ui.autoLayout.checked; - control.freeLayout = ui.freeLayout.checked; - control.spaceRandoX = ui.spaceRandoX.checked; - control.autoSave = ui.autoSave.checked; - control.antiAlias = ui.antiAlias.checked; - control.reverseZoom = ui.reverseZoom.checked; - control.dark = ui.dark.checked; - control.exportOcto = ui.exportOcto.checked; - control.exportGhost = ui.exportGhost.checked; - control.exportLocal = ui.exportLocal.checked; - control.exportThumb = ui.exportThumb.checked; - control.exportPreview = ui.exportPreview.checked; - control.healMesh = ui.healMesh.checked; - control.threaded = setThreaded(ui.threaded.checked); - control.assembly = ui.assembly.checked; - control.ortho = ui.ortho.checked; - control.devel = ui.devel.checked; - space.view.setZoom(control.reverseZoom, control.zoomSpeed); - // platform.layout(); - api.conf.save(); - api.platform.update_size(); - updateStats(); - updateDrawer(); - api.event.emit('boolean.update'); - if (doAlert) { - api.show.alert("change requires page refresh"); - } -} - -function updateDrawer() { - const { drawer, scrolls } = settings().controller; - $c('app', drawer ? 'slideshow' : '', drawer ? '' : 'slideshow'); - $c('app', scrolls ? '' : 'hide-scroll', scrolls ? 'hide-scroll' : ''); -} - -function updateStats() { - if (self.debug !== true) { - return; - } - let { div, fps, rms, rnfo } = ui.stats; - div.style.display = 'flex'; - setInterval(() => { - const nrms = space.view.getRMS().toFixed(1); - const nfps = space.view.getFPS().toFixed(1); - const rend = space.renderInfo(); - const { memory, render } = rend; - if (nfps !== fps.innerText) { - fps.innerText = nfps; - } - if (nrms !== rms.innerText) { - rms.innerText = nrms; - } - if (rnfo.offsetParent !== null) { - rnfo.innerHTML = Object.entries({ ...memory, ...render, render_ms: nrms, frames_sec: nfps }).map(row => { - return `
${row[0]}
` - }).join(''); - } - }, 100); -} - -function onBooleanClick(el) { - // copy some ui elements to target settings - let settings = api.conf.get(); - settings.device.bedBelt = ui.deviceBelt.checked && api.mode.is_fdm(); - settings.device.bedRound = ui.deviceRound.checked && api.mode.is_fdm(); - settings.device.originCenter = ui.deviceOrigin.checked || ui.deviceRound.checked; - settings.device.fwRetract = ui.fwRetract.checked; - // refresh vars and other ui elements - uc.refresh(); - if (el === ui.camStockIndexed) { - api.view.set_arrange(); - } - api.conf.update(); - DOC.activeElement.blur(); - api.event.emit("boolean.click"); - api.devices.update_laser_state(); -} - -function onButtonClick(ev) { - let target = ev.target; - while (target && target.tagName !== 'BUTTON') { - target = target.parentNode; - } - api.event.emit("button.click", target); -} - -function inputHasFocus() { - let active = DOC.activeElement; - return active && (active.nodeName === "INPUT" || active.nodeName === "TEXTAREA"); -} - -function cca(c) { - return c.charCodeAt(0); -} - -function keyUpHandler(evt) { - if (api.feature.on_key) { - if (api.feature.on_key({up:evt})) return; - } - for (let handler of api.feature.on_key2) { - if (handler({up:evt})) return; - } - switch (evt.keyCode) { - // escape - case 27: - // blur text input focus - DOC.activeElement.blur(); - // dismiss modals - api.modal.hide(); - // deselect widgets - platform.deselect(); - // hide all dialogs - api.dialog.hide(); - // cancel slicing - api.function.cancel(); - // and send an event (used by FDM client) - api.event.emit("key.esc"); - break; - } - return false; -} - -function keyDownHandler(evt) { - if (api.modal.visible()) { - return false; - } - if (api.feature.on_key) { - if (api.feature.on_key({down:evt})) return; - } - for (let handler of api.feature.on_key2) { - if (handler({down:evt})) return; - } - let move = evt.altKey ? 5 : 0, - deg = move ? 0 : -Math.PI / (evt.shiftKey ? 36 : 2); - switch (evt.keyCode) { - case 8: // apple: delete/backspace - case 46: // others: delete - if (inputHasFocus()) return false; - platform.delete(selection.meshes()); - evt.preventDefault(); - break; - case 37: // left arrow - if (inputHasFocus()) return false; - if (deg) selection.rotate(0, 0, -deg); - if (move > 0) selection.move(-move, 0, 0); - evt.preventDefault(); - break; - case 39: // right arrow - if (inputHasFocus()) return false; - if (deg) selection.rotate(0, 0, deg); - if (move > 0) selection.move(move, 0, 0); - evt.preventDefault(); - break; - case 38: // up arrow - if (inputHasFocus()) return false; - if (evt.metaKey) return api.show.layer(api.var.layer_at+1); - if (deg) selection.rotate(deg, 0, 0); - if (move > 0) selection.move(0, move, 0); - evt.preventDefault(); - break; - case 40: // down arrow - if (inputHasFocus()) return false; - if (evt.metaKey) return api.show.layer(api.var.layer_at-1); - if (deg) selection.rotate(-deg, 0, 0); - if (move > 0) selection.move(0, -move, 0); - evt.preventDefault(); - break; - case 65: // 'a' for select all - if (evt.metaKey || evt.ctrlKey) { - if (inputHasFocus()) return false; - evt.preventDefault(); - platform.deselect(); - platform.select_all(); - } - break; - case 83: // 's' for save workspace - if (evt.ctrlKey) { - evt.preventDefault(); - api.conf.save(); - console.log("settings saved"); - } else - if (evt.metaKey) { - evt.preventDefault(); - api.space.save(); - set_ctrl.sync.put(); - } - break; - case 76: // 'l' for restore workspace - if (evt.metaKey) { - evt.preventDefault(); - api.space.restore(); - } - break; - } -} - -function keyHandler(evt) { - let handled = true; - if (api.modal.visible() || inputHasFocus()) { - return false; - } - if (api.feature.on_key) { - if (api.feature.on_key({key:evt})) return; - } - for (let handler of api.feature.on_key2) { - if (handler({key:evt})) return; - } - if (evt.ctrlKey) { - switch (evt.key) { - case 'g': return api.group.merge(); - case 'u': return api.group.split(); - } - } - switch (evt.charCode) { - case cca('`'): api.show.slices(0); break; - case cca('0'): api.show.slices(api.var.layer_max); break; - case cca('1'): api.show.slices(api.var.layer_max/10); break; - case cca('2'): api.show.slices(api.var.layer_max*2/10); break; - case cca('3'): api.show.slices(api.var.layer_max*3/10); break; - case cca('4'): api.show.slices(api.var.layer_max*4/10); break; - case cca('5'): api.show.slices(api.var.layer_max*5/10); break; - case cca('6'): api.show.slices(api.var.layer_max*6/10); break; - case cca('7'): api.show.slices(api.var.layer_max*7/10); break; - case cca('8'): api.show.slices(api.var.layer_max*8/10); break; - case cca('9'): api.show.slices(api.var.layer_max*9/10); break; - case cca('?'): - api.help.show(); - break; - case cca('Z'): // reset stored state - uc.confirm('clear all settings and preferences?').then(yes => { - if (yes) { - sdb.clear(); - WIN.location.reload(); - } - }); - break; - case cca('C'): // refresh catalog - catalog.refresh(); - break; - case cca('i'): // file import - api.event.import(); - break; - case cca('S'): // slice - case cca('s'): // slice - api.function.slice(); - break; - case cca('P'): // prepare - case cca('p'): // prepare - if (api.mode.get() !== 'SLA') { - // hidden in SLA mode - api.function.prepare(); - } - break; - case cca('X'): // export - case cca('x'): // export - api.function.export(); - break; - case cca('g'): // CAM animate - api.function.animate(); - break; - case cca('O'): // manual rotation - rotateInputSelection(); - break; - case cca('r'): // recent files - api.modal.show('files'); - break; - case cca('q'): // preferences - api.modal.show('prefs'); - break; - case cca('l'): // device - settingsLoad(); - break; - case cca('e'): // device - api.show.devices(); - break; - case cca('o'): // tools - api.show.tools(); - break; - case cca('c'): // local devices - api.show.local(); - break; - case cca('v'): // toggle single slice view mode - if (api.view.get() === VIEWS.ARRANGE) { - api.space.set_focus(selection.widgets()); - } - if (api.var.layer_hi == api.var.layer_lo) { - api.var.layer_lo = 0; - } else { - api.var.layer_lo = api.var.layer_hi; - } - api.show.slices(); - break; - case cca('d'): // duplicate object - duplicateSelection(); - break; - case cca('m'): // mirror object - mirrorSelection(); - break; - case cca('a'): - if (api.view.get() === VIEWS.ARRANGE) { - // auto arrange items on platform - platform.layout(); - if (!api.conf.get().controller.spaceRandoX) { - api.space.set_focus(selection.widgets()); - } - } else { - // go to arrange view - api.view.set(VIEWS.ARRANGE); - } - break; - default: - api.event.emit('keypress', evt); - handled = false; - break; - } - if (handled) { - evt.preventDefault(); - evt.stopPropagation(); - } - return false; -} - -function duplicateSelection() { - selection.duplicate(); -} - -function mirrorSelection() { - selection.mirror(); -} - -function keys(o) { - let key, list = []; - for (key in o) { if (o.hasOwnProperty(key)) list.push(key) } - return list.sort(); -} - -function clearSelected(children) { - for (let i=0; i { - coord = (coord || '').split(','); - let prod = Math.PI / 180, - x = parseFloat(coord[0] || 0.0) * prod, - y = parseFloat(coord[1] || 0.0) * prod, - z = parseFloat(coord[2] || 0.0) * prod; - selection.rotate(x, y, z); - }); -} - -function positionSelection() { - if (selection.meshes().length === 0) { - api.show.alert("select object to position"); - return; - } - let current = settings(), - { device, process} = current, - center = process.ctOriginCenter || process.camOriginCenter || device.bedRound || device.originCenter, - bounds = boundsSelection(); - - api.uc.prompt("Enter X,Y coordinates for selection","").then(coord => { - coord = (coord || '').split(','); - let x = parseFloat(coord[0] || 0.0), - y = parseFloat(coord[1] || 0.0), - z = parseFloat(coord[2] || 0.0); - - if (!center) { - x = x - device.bedWidth/2 + (bounds.max.x - bounds.min.x)/2; - y = y - device.bedDepth/2 + (bounds.max.y - bounds.min.y)/2 - } - - selection.move(x, y, z, true); - }); -} - -function deviceExport(exp, name) { - name = (name || "device") - .toLowerCase() - .replace(/ /g,'_') - .replace(/\./g,'_'); - uc.prompt("Export Device Filename", name).then(name => { - if (name) { - api.util.download(exp, `${name}.km`); - } - }); -} - -function objectsExport(format = "stl") { - // return selection.export(); - uc.confirm("Export Filename", {ok:true, cancel: false}, `selected.${format}`).then(name => { - if (!name) return; - if (name.toLowerCase().indexOf(`.${format}`) < 0) { - name = `${name}.${format}`; - } - api.util.download(selection.export(format), name); - }); -} - -function workspaceNew() { - uc.confirm("Clear Workspace?", {ok:true, cancel: false}).then(value => { - if (value === true) { - let proc = set_ctrl.proc(); - proc.ops && (proc.ops.length = 0); - proc.op2 && (proc.op2.length = 0); - api.platform.clear(); - } - }); -} - -function profileExport() { - const opt = {pre: [ - "
", - "

Workspace Export

", - " ", - " ", - "  Exclude meshes", - "
" - ]}; - let suggestion = "workspace"; - let file = api.widgets.all()[0]?.meta.file || ''; - if (file) { - suggestion = `${suggestion}_${file.split('.')[0]}`.replaceAll(' ','_'); - }; - uc.confirm("Filename", {ok:true, cancel: false}, suggestion, opt).then(name => { - if (!name) return; - - let work = !$('excwork').checked; - let json = api.conf.export({work, clear:true}); - - client.zip([ - {name:"workspace.json", data:JSON.stringify(json)} - ], progress => { - api.show.progress(progress.percent/100, "compressing workspace"); - }, output => { - api.show.progress(0); - api.util.download(output, `${name}.kmz`); - }); - }); -} - -function settingsSave(ev, name) { - if (ev) { - ev.stopPropagation(); - ev.preventDefault(); - } - - api.dialog.hide(); - let mode = api.mode.get(), - s = settings(), - def = "default", - cp = s.process, - pl = s.sproc[mode], - lp = s.cproc[mode], - saveAs = (name) => { - if (!name) { - return; - } - let np = pl[name] = {}; - cp.processName = name; - pl[name] = Object.clone(cp); - for (let k in cp) { - if (!cp.hasOwnProperty(k)) continue; - np[k] = cp[k]; - } - s.cproc[mode] = name; - s.devproc[s.device.deviceName] = name; - api.conf.save(); - api.conf.update(); - api.event.settings(); - sync_put(); - }; - - if (name) { - saveAs(name); - } else { - uc.prompt("Save Settings As", cp ? lp || def : def).then(saveAs); - } -} - -function settingsLoad() { - api.conf.show(); -} - -function updateDeviceSize() { - api.conf.update(); - platform.update_size(); - platform.update_origin(); -} - -async function sync_put() { - await set_ctrl.sync.put(); -} - -function dragOverHandler(evt) { - evt.stopPropagation(); - evt.preventDefault(); - - // prevent drop actions when a dialog is open - if (api.modal.visible()) { - return; - } - - evt.dataTransfer.dropEffect = 'copy'; - let oldcolor = space.platform.setColor(0x00ff00); - if (oldcolor !== 0x00ff00) platformColor = oldcolor; -} - -function dragLeave() { - space.platform.setColor(platformColor); -} - -function dropHandler(evt) { - evt.stopPropagation(); - evt.preventDefault(); - - // prevent drop actions when a dialog is open - if (api.modal.visible()) { - return; - } - - space.platform.setColor(platformColor); - - let files = evt.dataTransfer.files; - - switch (api.feature.drop_group) { - case true: - return api.platform.load_files(files, []); - case false: - return api.platform.load_files(files, undefined); - } - - if (files.length === 1) { - api.platform.load_files(files); - } else if (files.length > 1) { - uc.confirm(`group ${files.length} files?`).then(yes => { - api.platform.load_files(files, yes ? [] : undefined); - }); - } -} - -function loadCatalogFile(e) { - api.widgets.load(e.target.getAttribute('load'), function(widget) { - platform.add(widget); - api.dialog.hide(); - }); -} - -function updateCatalog(files) { - let table = ui.catalogList, - list = []; - table.innerHTML = ''; - for (let name in files) { - list.push({n:name, ln:name.toLowerCase(), v:files[name].vertices, t:files[name].updated}); - } - list.sort(function(a,b) { - return a.ln < b.ln ? -1 : 1; - }); - for (let i=0; i { - api.uc.prompt(`rename file`, short).then(newname => { - if (newname && newname !== short) { - catalog.rename(name, `${newname}${ext}`, then => { - api.modal.show('files'); - }); - } - }); - }; - - load.setAttribute('load', name); - load.setAttribute('title', `file: ${name}\nvertices: ${file.v}\ndate: ${date}`); - load.onclick = loadCatalogFile; - load.appendChild(DOC.createTextNode(short)); - - del.setAttribute('del', name); - del.setAttribute('title', "remove '"+name+"'"); - del.onclick = () => { catalog.deleteFile(name) }; - del.innerHTML = ''; - - size.setAttribute("disabled", true); - size.setAttribute("class", "label"); - size.appendChild(DOC.createTextNode(base.util.comma(file.v))); - - row.setAttribute("class", "f-row a-center"); - row.appendChild(renm); - row.appendChild(load); - row.appendChild(size); - row.appendChild(del); - table.appendChild(row); - } -} - -function isMultiHead() { - let dev = api.conf.get().device; - return isNotBelt() && dev.extruders && dev.extruders.length > 1; -} - -function isBelt() { - return api.device.isBelt(); -} - -function isNotBelt() { - return !isBelt(); -} - -// MAIN INITIALIZATION FUNCTION -function init_one() { - let { event, conf, view, show } = api, - { bound, toInt, toFloat } = uc, - { newBlank, newButton, newBoolean, newGroup, newInput } = uc, - { newSelect, newLabel, newValue, newRow, newGCode, newDiv } = uc; - - event.emit('init.one'); - - // ensure we have settings from last session - conf.restore(); - - let container = $('container'), - gcode = $('dev-gcode'), - tracker = $('tracker'), - controller = settings().controller; - - WIN.addEventListener("resize", () => { - event.emit('resize'); - }); - - event.on('resize', () => { - if (WIN.innerHeight < 800) { - ui.modalBox.classList.add('mh85'); - } else { - ui.modalBox.classList.remove('mh85'); - } - view.update_slider(); - }); - - space.sky.showGrid(false); - space.sky.setColor(controller.dark ? 0 : 0xffffff); - space.setAntiAlias(controller.antiAlias); - space.init(container, function (delta) { - let vars = api.var; - if (vars.layer_max === 0 || !delta) return; - if (controller.reverseZoom) delta = -delta; - let same = vars.layer_hi === vars.layer_lo; - let track = vars.layer_lo > 0; - if (delta > 0) { - vars.layer_hi = Math.max(same ? 0 : vars.layer_lo, vars.layer_hi - 1); - if (track) { - vars.layer_lo = Math.max(0, vars.layer_lo - 1); - } - } else if (delta < 0) { - vars.layer_hi = Math.min(vars.layer_max, vars.layer_hi + 1); - if (track) { - vars.layer_lo = Math.min(vars.layer_hi, vars.layer_lo + 1); - } - } - if (same) { - vars.layer_lo = vars.layer_hi; - } - view.update_slider(); - show.slices(); - }, controller.ortho); - space.platform.onMove(conf.save); - space.platform.setRound(true); - space.useDefaultKeys(api.feature.on_key === undefined || api.feature.on_key_defaults); - updateDrawer(); - - // api augmentation with local functions - api.device.export = deviceExport; - let anim = ui.anim = {}; - - Object.assign(ui, { - tracker: tracker, - container: container, - - alert: { - dialog: $('alert-area'), - text: $('alert-text') - }, - func: { - slice: $('act-slice'), - preview: $('act-preview'), - animate: $('act-animate'), - export: $('act-export') - }, - label: { - slice: $('label-slice'), - preview: $('label-preview'), - animate: $('label-animate'), - export: $('label-export'), - }, - acct: { - help: $('app-help'), - don8: $('app-don8'), - mesh: $('app-mesh'), - export: $('app-export') - }, - dev: { - header: $('dev-header'), - search: $('dev-search'), - filter: $('dev-filter') - }, - mesh: { - name: $('mesh-name'), - points: $('mesh-points'), - faces: $('mesh-faces'), - }, - - stats: { - fps: $('fps'), - rms: $('rms'), - div: $('stats'), - rnfo: $('rnfo'), - }, - - load: $('load-file'), - speeds: $('speeds'), - speedbar: $('speedbar'), - context: $('context-menu'), - - ltsetup: $('lt-setup'), - ltfile: $('lt-file'), - ltview: $('lt-view'), - ltact: $('lt-start'), - edit: $('lt-tools'), - nozzle: $('menu-nozzle'), - - modal: $('modal'), - modalBox: $('modal-box'), - modals: { - help: $('mod-help'), - setup: $('mod-setup'), - prefs: $('mod-prefs'), - files: $('mod-files'), - saves: $('mod-saves'), - tools: $('mod-tools'), - xany: $('mod-x-any'), - xsla: $('mod-x-sla'), - xlaser: $('mod-x-laser'), - local: $('mod-local'), - don8: $('mod-don8'), - any: $('mod-any'), - }, - - catalogBody: $('catalogBody'), - catalogList: $('catalogList'), - - devices: $('devices'), - deviceAdd: $('device-add'), - deviceDelete: $('device-del'), - deviceRename: $('device-ren'), - deviceExport: $('device-exp'), - deviceSave: $('device-save'), - - toolsSave: $('tools-save'), - toolsClose: $('tools-close'), - toolsImport: $('tools-import'), - toolsExport: $('tools-export'), - toolSelect: $('tool-select'), - toolAdd: $('tool-add'), - toolCopy: $('tool-dup'), - toolDelete: $('tool-del'), - toolType: $('tool-type'), - toolName: $('tool-name'), - toolNum: $('tool-num'), - toolFluteDiam: $('tool-fdiam'), - toolFluteLen: $('tool-flen'), - toolShaftDiam: $('tool-sdiam'), - toolShaftLen: $('tool-slen'), - toolTaperAngle: $('tool-tangle'), - toolTaperTip: $('tool-ttip'), - toolMetric: $('tool-metric'), - - setMenu: $('set-menu'), - settings: $('settings'), - settingsBody: $('settingsBody'), - settingsList: $('settingsList'), - - slider: $('slider'), - sliderMax: $('slider-max'), - sliderMin: $('slider-zero'), - sliderLo: $('slider-lo'), - sliderMid: $('slider-mid'), - sliderHi: $('slider-hi'), - sliderHold: $('slider-hold'), - sliderRange: $('slider-center'), - - loading: $('progress').style, - progress: $('progbar').style, - prostatus: $('progtxt'), - - selection: $('selection'), - sizeX: $('size_x'), - sizeY: $('size_y'), - sizeZ: $('size_z'), - scaleX: $('scale_x'), - scaleY: $('scale_y'), - scaleZ: $('scale_z'), - lockX: $('lock_x'), - lockY: $('lock_y'), - lockZ: $('lock_z'), - stock: $('stock'), - stockWidth: $('stock-width'), - stockDepth: $('stock-width'), - stockHeight: $('stock-width'), - - /** CAM Animation Bar */ - - _____: { - _____: newDiv({ addto: $('layer-animate'), set:true }), - row: newRow([ - anim.replay = newButton(null,"anim.replay",{icon:'',title:"restart"}), - anim.play = newButton(null,"anim.play",{icon:'',title:"play"}), - anim.pause = newButton(null,"anim.pause",{icon:'',title:"pause"}), - anim.step = newButton(null,"anim.step",{icon:'',title:"single step"}), - anim.speed = newButton(null,"anim.fast",{icon:'',title:"toggle speed"}), - anim.labspd = newValue(3, {class:"center padleft"}), - anim.labx = newLabel("X", {class:"padleft"}), - anim.valx = newValue(7, {class:"center"}), - anim.laby = newLabel("Y", {class:"padleft"}), - anim.valy = newValue(7, {class:"center"}), - anim.labz = newLabel("Z", {class:"padleft"}), - anim.valz = newValue(6, {class:"center"}), - anim.laba = newLabel("A", {class:"padleft hide"}), - anim.vala = newValue(6, {class:"center hide"}), - anim.labpro = newLabel("%", {class:"padleft"}), - anim.progress = newValue(5, {class:"center"}), - anim.trans = newButton(null,"anim.trans",{icon:'',title:"transparency",class:"padleft"}), - anim.model = newButton(null,"anim.model",{icon:'',title:"show model"}), - anim.shade = newButton(null,"anim.stock",{icon:'',title:"stock box"}), - ]) - }, - - /** Device Browser / Editor */ - - _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true }), - device: newGroup(LANG.dv_gr_dev, null, {group:"ddev", inline, class:"noshow"}), - - _____: newGroup("workspace", null, {group:"dext", inline}), - bedWidth: newInput('X (width)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), - bedDepth: newInput('Y (depth)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), - maxHeight: newInput('Z (height)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), - resolutionX: newInput(LANG.dv_rezx_s, {title:LANG.dv_rezx_l, convert:toInt, size:6, modes:SLA}), - resolutionY: newInput(LANG.dv_rezy_s, {title:LANG.dv_rezy_l, convert:toInt, size:6, modes:SLA}), - _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:NO_WEDM }), - _____: newGroup("firmware", null, {group:"dext", inline, modes:NO_WEDM}), - fwRetract: newBoolean(LANG.dv_retr_s, onBooleanClick, {title:LANG.dv_retr_l, modes:FDM}), - deviceOrigin: newBoolean(LANG.dv_orgc_s, onBooleanClick, {title:LANG.dv_orgc_l, modes:FDM_LZN, show:() => !ui.deviceRound.checked}), - deviceRound: newBoolean(LANG.dv_bedc_s, onBooleanClick, {title:LANG.dv_bedc_l, modes:FDM, trigger, show:isNotBelt}), - deviceBelt: newBoolean(LANG.dv_belt_s, onBooleanClick, {title:LANG.dv_belt_l, modes:FDM, trigger, show:() => !ui.deviceRound.checked}), - separator: newBlank({class:"pop-sep", modes:FDM, driven}), - spindleMax: newInput(LANG.dv_spmx_s, {title:LANG.dv_spmx_l, convert:toInt, size:5, modes:CAM, trigger}), - deviceZMax: newInput(LANG.dv_zmax_s, {title:LANG.dv_zmax_l, convert:toInt, size:5, modes:FDM}), - gcodeTime: newInput(LANG.dv_time_s, {title:LANG.dv_time_l, convert:toFloat, size:5, modes:FDM}), - _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:FDM }), - extruder: newGroup(LANG.dv_gr_ext, null, {group:"dext", inline}), - extFilament: newInput(LANG.dv_fila_s, {title:LANG.dv_fila_l, convert:toFloat, modes:FDM}), - extNozzle: newInput(LANG.dv_nozl_s, {title:LANG.dv_nozl_l, convert:toFloat, modes:FDM}), - extOffsetX: newInput(LANG.dv_exox_s, {title:LANG.dv_exox_l, convert:toFloat, modes:FDM}), - extOffsetY: newInput(LANG.dv_exoy_s, {title:LANG.dv_exoy_l, convert:toFloat, modes:FDM}), - extPad: newBlank({class:"grow", modes:FDM}), - separator: newBlank({class:"pop-sep", modes:FDM, driven}), - extActions: newRow([ - ui.extPrev = newButton(undefined, undefined, {icon:''}), - ui.extAdd = newButton(undefined, undefined, {icon:''}), - ui.extDel = newButton(undefined, undefined, {icon:''}), - ui.extNext = newButton(undefined, undefined, {icon:''}) - ], {class:"dev-buttons ext-buttons var-row", modes:FDM}), - _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:CAM_LZR }), - _____: newGroup(LANG.dv_gr_out, null, {group:"dgco", inline}), - gcodeStrip: newBoolean(LANG.dv_strc_s, onBooleanClick, {title:LANG.dv_strc_l, modes:CAM}), - gcodeSpace: newBoolean(LANG.dv_tksp_s, onBooleanClick, {title:LANG.dv_tksp_l, modes:CAM_LZR}), - laserMaxPower: newInput(LANG.ou_maxp_s, {title:LANG.ou_maxp_l, modes:LASER, size:7, text:true}), - useLaser: newBoolean(LANG.dv_lazr_s, onBooleanClick, {title:LANG.dv_lazr_l, modes:CAM}), - gcodeFExt: newInput(LANG.dv_fext_s, {title:LANG.dv_fext_l, modes:CAM_LZR, size:7, text:true}), - gcodeEd: newGroup(LANG.dv_gr_gco, $('dg'), {group:"dgcp", inline, modes:GCODE}), - gcodeMacros: newRow([ - (ui.gcodePre = newGCode(LANG.dv_head_s, {title:LANG.dv_head_l, modes:GCODE, area:gcode})).button, - (ui.gcodePost = newGCode(LANG.dv_foot_s, {title:LANG.dv_foot_l, modes:GCODE, area:gcode})).button, - (ui.gcodeLayer = newGCode(LANG.dv_layr_s, {title:LANG.dv_layr_l, modes:FDM, area:gcode})).button, - (ui.gcodeTrack = newGCode(LANG.dv_prog_s, {title:LANG.dv_prog_l, modes:FDM, area:gcode})).button, - (ui.gcodeFan = newGCode(LANG.dv_fanp_s, {title:LANG.dv_fanp_l, modes:FDM, area:gcode})).button, - (ui.gcodeFeature = newGCode(LANG.dv_feat_s, {title:LANG.dv_feat_l, modes:FDM, area:gcode})).button, - (ui.gcodeLaserOn = newGCode(LANG.dv_lzon_s, {title:LANG.dv_lzon_l, modes:LASER, area:gcode})).button, - (ui.gcodeLaserOff = newGCode(LANG.dv_lzof_s, {title:LANG.dv_lzof_l, modes:LASER, area:gcode})).button, - (ui.gcodeWaterOn = newGCode(LANG.dv_waon_s, {title:LANG.dv_waon_l, modes:WJET, area:gcode})).button, - (ui.gcodeWaterOff = newGCode(LANG.dv_waof_s, {title:LANG.dv_waof_l, modes:WJET, area:gcode})).button, - (ui.gcodeKnifeDn = newGCode(LANG.dv_dkon_s, {title:LANG.dv_dkon_l, modes:DRAG, area:gcode})).button, - (ui.gcodeKnifeUp = newGCode(LANG.dv_dkof_s, {title:LANG.dv_dkof_l, modes:DRAG, area:gcode})).button, - (ui.gcodeChange = newGCode(LANG.dv_tool_s, {title:LANG.dv_tool_l, modes:FDM_CAM, area:gcode})).button, - (ui.gcodeDwell = newGCode(LANG.dv_dwll_s, {title:LANG.dv_dwll_l, modes:CAM, area:gcode})).button, - (ui.gcodeSpindle = newGCode(LANG.dv_sspd_s, {title:LANG.dv_sspd_l, modes:CAM, area:gcode, show:() => ui.spindleMax.value > 0})).button - ], {class:"ext-buttons f-row gcode-macros"}), - - /** Preferences Menu */ - - _____: newGroup(LANG.op_menu, $('prefs-gen1'), {inline}), - antiAlias: newBoolean(LANG.op_anta_s, booleanSave, {title:LANG.op_anta_l}), - reverseZoom: newBoolean(LANG.op_invr_s, booleanSave, {title:LANG.op_invr_l}), - ortho: newBoolean(LANG.op_orth_s, booleanSave, {title:LANG.op_orth_l}), - dark: newBoolean(LANG.op_dark_s, booleanSave, {title:LANG.op_dark_l}), - drawer: newBoolean('slide out', booleanSave, {title:'slide out settings drawer'}), - scrolls: newBoolean('scrollbars', booleanSave, {title:'show scrollbars'}), - devel: newBoolean(LANG.op_devl_s, booleanSave, {title:LANG.op_devl_l}), - _____: newGroup(LANG.op_disp, $('prefs-gen2'), {inline}), - showOrigin: newBoolean(LANG.op_shor_s, booleanSave, {title:LANG.op_shor_l}), - showRulers: newBoolean(LANG.op_shru_s, booleanSave, {title:LANG.op_shru_l}), - showSpeeds: newBoolean(LANG.op_sped_s, speedSave, {title:LANG.op_sped_l}), - shiny: newBoolean(LANG.op_shny_s, booleanSave, {title:LANG.op_shny_l, modes:FDM}), - lineType: newSelect(LANG.op_line_s, {title: LANG.op_line_l, action: lineTypeSave, modes:FDM}, "linetype"), - animesh: newSelect(LANG.op_anim_s, {title: LANG.op_anim_l, action: aniMeshSave, modes:CAM}, "animesh"), - units: newSelect(LANG.op_unit_s, {title: LANG.op_unit_l, action: unitsSave, modes:CAM}, "units"), - edgeangle: newInput(LANG.op_spoa_s, {title:LANG.op_spoa_l, convert:toFloat, size:3}), - _____: newGroup(LANG.lo_menu, $('prefs-lay'), {inline}), - autoSave: newBoolean(LANG.op_save_s, booleanSave, {title:LANG.op_save_l}), - autoLayout: newBoolean(LANG.op_auto_s, booleanSave, {title:LANG.op_auto_l}), - freeLayout: newBoolean(LANG.op_free_s, booleanSave, {title:LANG.op_free_l}), - spaceRandoX: newBoolean(LANG.op_spcx_s, booleanSave, {title:LANG.op_spcx_l, show:isBelt}), - spaceLayout: newInput(LANG.op_spcr_s, {title:LANG.op_spcr_l, convert:toFloat, size:3, units}), - _____: newGroup(LANG.xp_menu, $('prefs-xpo'), {inline: true}), - exportLocal: newBoolean(`Grid:Local`, booleanSave, {title:LANG.op_exgl_l}), - exportGhost: newBoolean(`Grid:Host`, booleanSave, {title:LANG.op_exgh_l}), - exportOcto: newBoolean(`OctoPrint`, booleanSave, {title:LANG.op_exop_l}), - exportThumb: newBoolean(`Thumbnail`, booleanSave, {modes:FDM}), - exportPreview: newBoolean(`Code Preview`, booleanSave), - _____: newGroup(LANG.pt_menu, $('prefs-prt'), {inline}), - detail: newSelect(LANG.pt_qual_s, {title: LANG.pt_qual_l, action: detailSave}, "detail"), - healMesh: newBoolean(LANG.pt_heal_s, booleanSave, {title: LANG.pt_heal_l}), - threaded: newBoolean(LANG.pt_thrd_s, booleanSave, {title: LANG.pt_thrd_l, modes:THREED}), - assembly: newBoolean(LANG.pt_assy_s, booleanSave, {title: LANG.pt_assy_l, modes:THREED}), - - prefadd: uc.checkpoint($('prefs-add')), - - /** FDM Settings */ - - _____: newGroup(LANG.sl_menu, $('fdm-layers'), { modes:FDM, driven, hideable, separator, group:"fdm-layers" }), - sliceHeight: newInput(LANG.sl_lahi_s, { title:LANG.sl_lahi_l, convert:toFloat }), - sliceMinHeight: newInput(LANG.ad_minl_s, { title:LANG.ad_minl_l, convert:toFloat, bound:bound(0,3.0), show:() => ui.sliceAdaptive.checked }), - sliceTopLayers: newInput(LANG.sl_ltop_s, { title:LANG.sl_ltop_l, convert:toInt }), - sliceBottomLayers: newInput(LANG.sl_lbot_s, { title:LANG.sl_lbot_l, convert:toInt }), - separator: newBlank({ class:"set-sep", driven }), - sliceAdaptive: newBoolean(LANG.ad_adap_s, onBooleanClick, { title: LANG.ad_adap_l }), - _____: newGroup(LANG.sw_menu, $('fdm-walls'), { modes:FDM, driven, hideable, separator, group:"fdm-walls" }), - sliceShells: newInput(LANG.sl_shel_s, { title:LANG.sl_shel_l, convert:toFloat }), - sliceLineWidth: newInput(LANG.sl_line_s, { title:LANG.sl_line_l, convert:toFloat, bound:bound(0,5) }), - separator: newBlank({ class:"set-sep", driven }), - sliceShellOrder: newSelect(LANG.sl_ordr_s, { title:LANG.sl_ordr_l}, "shell"), - sliceDetectThin: newSelect(LANG.ad_thin_s, { title: LANG.ad_thin_l, action: thinWallSave }, "thin"), - outputAlternating: newBoolean(LANG.ad_altr_s, onBooleanClick, {title:LANG.ad_altr_l}), - sliceZInterleave: newBoolean(LANG.ad_zint_s, onBooleanClick, {title:LANG.ad_zint_l, show:zIntShow}), - _____: newGroup(LANG.fi_menu, $('fdm-fill'), { modes:FDM, driven, hideable, separator, group:"fdm-fill" }), - sliceFillType: newSelect(LANG.fi_type, {trigger}, "infill"), - sliceFillSparse: newInput(LANG.fi_pcnt_s, {title:LANG.fi_pcnt_l, convert:toFloat, bound:bound(0.0,1.0), show:hasInfill}), - sliceFillRepeat: newInput(LANG.fi_rept_s, {title:LANG.fi_rept_l, convert:toInt, bound:bound(1,10), show:fillIsLinear}), - sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,2.0), show:hasInfill}), - separator: newBlank({ class:"set-sep", driven }), - sliceFillRate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt, bound:bound(0,500)}), - sliceSolidRate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt, bound:bound(0,500)}), - separator: newBlank({ class:"set-sep", driven }), - sliceFillGrow: newInput(LANG.fi_grow_s, {title:LANG.fi_grow_l, convert:toFloat}), - sliceFillAngle: newInput(LANG.fi_angl_s, {title:LANG.fi_angl_l, convert:toFloat}), - _____: newGroup(LANG.fh_menu, $('fdm-heat'), { modes:FDM, driven, hideable, separator, group:"fdm-heat" }), - outputTemp: newInput(LANG.ou_nozl_s, {title:LANG.ou_nozl_l, convert:toInt}), - outputBedTemp: newInput(LANG.ou_bedd_s, {title:LANG.ou_bedd_l, convert:toInt}), - _____: newGroup(LANG.fc_menu, $('fdm-cool'), { modes:FDM, driven, hideable, separator, group:"fdm-cool" }), - outputFanLayer: newInput(LANG.ou_fanl_s, { title:LANG.ou_fanl_l, convert:toInt, bound:bound(0,255) }), - outputFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255)}), - _____: newGroup(LANG.sp_menu, $('fdm-support'), { modes:FDM, driven, hideable, separator, group:"fdm-supp" }), - sliceSupportNozzle: newSelect(LANG.sp_nozl_s, {title:LANG.sp_nozl_l, show:isMultiHead}, "extruders"), - sliceSupportDensity: newInput(LANG.sp_dens_s, {title:LANG.sp_dens_l, convert:toFloat, bound:bound(0.0,1.0)}), - sliceSupportSize: newInput(LANG.sp_size_s, {title:LANG.sp_size_l, convert:toFloat, bound:bound(1.0,200.0)}), - sliceSupportOffset: newInput(LANG.sp_offs_s, {title:LANG.sp_offs_l, convert:toFloat, bound:bound(0.0,200.0)}), - sliceSupportGap: newInput(LANG.sp_gaps_s, {title:LANG.sp_gaps_l, convert:toInt, bound:bound(0,5)}), - sliceSupportArea: newInput(LANG.sp_area_s, {title:LANG.sp_area_l, convert:toFloat, bound:bound(0.0,200.0)}), - sliceSupportExtra: newInput(LANG.sp_xpnd_s, {title:LANG.sp_xpnd_l, convert:toFloat, bound:bound(0.0,10.0)}), - sliceSupportGrow: newInput(LANG.sp_grow_s, {title:LANG.sp_grow_l, convert:toFloat, bound:bound(0.0,10.0)}), - sliceSupportAngle: newInput(LANG.sp_angl_s, {title:LANG.sp_angl_l, convert:toFloat, bound:bound(0.0,90.0)}), - sliceSupportSpan: newInput(LANG.sp_span_s, {title:LANG.sp_span_l, convert:toFloat, bound:bound(0.0,200.0), show:() => ui.sliceSupportEnable.checked }), - separator: newBlank({ class:"set-sep", driven }), - sliceSupportEnable: newBoolean(LANG.sp_auto_s, onBooleanClick, {title:LANG.sp_auto_l, show:isNotBelt}), - sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l}), - separator: newBlank({ class:"set-sep", driven }), - sliceSupportGen: newRow([ - ui.ssaGen = newButton(LANG.sp_detect, onButtonClick, {class: "f-col grow a-center"}) - ], { modes: FDM, class: "ext-buttons f-row grow" }), - separator: newBlank({ class:"set-sep", driven }), - sliceSupportManual: newRow([ - (ui.ssmAdd = newButton(undefined, onButtonClick, {icon:''})), - (ui.ssmDun = newButton(undefined, onButtonClick, {icon:''})), - (ui.ssmClr = newButton(undefined, onButtonClick, {icon:''})) - ], {class:"ext-buttons f-row"}), - _____: newGroup(LANG.fl_menu, $('fdm-base'), { modes:FDM, driven, hideable, separator, group:"fdm-base" }), - firstSliceHeight: newInput(LANG.fl_lahi_s, {title:LANG.fl_lahi_l, convert:toFloat, show:isNotBelt}), - firstLayerNozzleTemp:newInput(LANG.fl_nozl_s, {title:LANG.fl_nozl_l, convert:toInt, show:isNotBelt}), - firstLayerBedTemp: newInput(LANG.fl_bedd_s, {title:LANG.fl_bedd_l, convert:toInt, show:isNotBelt}), - separator: newBlank({ class:"set-sep", driven }), - firstLayerFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255), show:isBelt}), - firstLayerYOffset: newInput(LANG.fl_zoff_s, {title:LANG.fl_zoff_l, convert:toFloat, show:isBelt}), - firstLayerFlatten: newInput(LANG.fl_flat_s, {title:LANG.fl_flat_l, convert:toFloat, show:isBelt}), - firstLayerRate: newInput(LANG.fl_rate_s, {title:LANG.fl_rate_l, convert:toFloat}), - firstLayerFillRate: newInput(LANG.fl_frat_s, {title:LANG.fl_frat_l, convert:toFloat, show:isNotBelt}), - separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), - firstLayerLineMult: newInput(LANG.fl_sfac_s, {title:LANG.fl_sfac_l, convert:toFloat, bound:bound(0.5,2), show:isNotBelt}), - firstLayerPrintMult: newInput(LANG.fl_mult_s, {title:LANG.fl_mult_l, convert:toFloat}), - separator: newBlank({ class:"set-sep", driven, show:isBelt }), - firstLayerBrim: newInput(LANG.fl_brim_s, {title:LANG.fl_brim_l, convert:toInt, show:isBelt}), - firstLayerBrimIn: newInput(LANG.fl_brin_s, {title:LANG.fl_brin_l, convert:toInt, show:isBelt}), - firstLayerBrimTrig: newInput(LANG.fl_brmn_s, {title:LANG.fl_brmn_l, convert:toInt, show:isBelt}), - firstLayerBrimGap: newInput(LANG.fl_brgp_s, {title:LANG.fl_brgp_l, convert:toFloat, show:isBelt}), - separator: newBlank({ class:"set-sep", driven, show:isBelt }), - firstLayerBeltLead: newInput(LANG.fl_bled_s, {title:LANG.fl_bled_l, convert:toFloat, show:isBelt}), - firstLayerBeltBump: newInput(LANG.fl_blmp_s, {title:LANG.fl_blmp_l, convert:toFloat, bound:bound(0, 10), show:isBelt}), - separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), - outputBrimCount: newInput(LANG.fl_skrt_s, {title:LANG.fl_skrt_l, convert:toInt, show:isNotBelt}), - outputBrimOffset: newInput(LANG.fl_skro_s, {title:LANG.fl_skro_l, convert:toFloat, show:isNotBelt}), - outputRaftSpacing: newInput(LANG.fr_spac_s, {title:LANG.fr_spac_l, convert:toFloat, bound:bound(0.0,3.0), show: () => ui.outputRaft.checked && isNotBelt() }), - separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), - outputRaft: newBoolean(LANG.fr_nabl_s, onBooleanClick, {title:LANG.fr_nabl_l, trigger, show:() => isNotBelt()}), - outputDraftShield: newBoolean(LANG.fr_draf_s, onBooleanClick, {title:LANG.fr_draf_l, trigger, show:() => isNotBelt()}), - _____: newGroup(LANG.ou_menu, $('fdm-output'), { modes:FDM, driven, hideable, separator, group:"fdm-out" }), - outputFeedrate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt}), - outputFinishrate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt}), - outputSeekrate: newInput(LANG.ou_move_s, {title:LANG.ou_move_l, convert:toInt}), - separator: newBlank({ class:"set-sep", driven }), - outputShellMult: newInput(LANG.ou_shml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), - outputFillMult: newInput(LANG.ou_flml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), - outputSparseMult: newInput(LANG.ou_spml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), - separator: newBlank({ class:"set-sep", driven }), - outputRetractDist: newInput(LANG.ad_rdst_s, {title:LANG.ad_rdst_l, convert:toFloat}), - outputRetractSpeed: newInput(LANG.ad_rrat_s, {title:LANG.ad_rrat_l, convert:toInt}), - outputRetractWipe: newInput(LANG.ad_wpln_s, {title:LANG.ad_wpln_l, convert:toFloat, bound:bound(0.0,10)}), - separator: newBlank({ class:"set-sep", driven }), - sliceLayerStart: newSelect(LANG.sl_strt_s, {title:LANG.sl_strt_l}, "start"), - outputLayerRetract: newBoolean(LANG.ad_lret_s, onBooleanClick, {title:LANG.ad_lret_l}), - outputAvoidGaps: newBoolean(LANG.ad_agap_s, onBooleanClick, {title:LANG.ad_agap_l}), - separator: newBlank({ class:"set-sep", driven, show:isBelt }), - outputBeltFirst: newBoolean(LANG.ad_lbir_s, onBooleanClick, {title:LANG.ad_lbir_l, show:isBelt}), - _____: newGroup(LANG.ad_menu, $('fdm-expert'), { modes:FDM, driven, hideable, separator, group:"fdm-xprt" }), - sliceAngle: newInput(LANG.sl_angl_s, {title:LANG.sl_angl_l, convert:toFloat, show:isBelt}), - outputRetractDwell: newInput(LANG.ad_rdwl_s, {title:LANG.ad_rdwl_l, convert:toInt}), - sliceSolidMinArea: newInput(LANG.ad_msol_s, {title:LANG.ad_msol_l, convert:toFloat}), - outputMinSpeed: newInput(LANG.ad_mins_s, {title:LANG.ad_mins_l, convert:toFloat, bound:bound(1,200)}), - outputShortPoly: newInput(LANG.ad_spol_s, {title:LANG.ad_spol_l, convert:toFloat, bound:bound(0,10000)}), - outputCoastDist: newInput(LANG.ad_scst_s, {title:LANG.ad_scst_l, convert:toFloat, bound:bound(0.0,10)}), - zHopDistance: newInput(LANG.ad_zhop_s, {title:LANG.ad_zhop_l, convert:toFloat, bound:bound(0,3.0)}), - arcTolerance: newInput(LANG.ad_arct_s, {title:LANG.ad_arct_l, convert:toFloat, bound:bound(0,1.0), show:() => { return isNotBelt() }}), - antiBacklash: newInput(LANG.ad_abkl_s, {title:LANG.ad_abkl_l, convert:toInt, bound:bound(0,3)}), - outputLoops: newInput(LANG.ag_loop_s, {title:LANG.ag_loop_l, convert:toInt, bound:bound(-1,1000), show:isBelt}), - outputPurgeTower: newInput(LANG.ad_purg_s, {title:LANG.ad_purg_l, convert:toInt, bound:bound(0,1000)}), - - fdmRanges: $('fdm-ranges'), - - /** CAM Settings */ - - _____: newGroup(LANG.ct_menu, $('cam-tabs'), { modes:CAM, marker:true, driven, separator }), - camTabsWidth: newInput(LANG.ct_wdth_s, {title:LANG.ct_wdth_l, convert:toFloat, bound:bound(0.005,100), units}), - camTabsHeight: newInput(LANG.ct_hght_s, {title:LANG.ct_hght_l, convert:toFloat, bound:bound(0.005,100), units}), - camTabsDepth: newInput(LANG.ct_dpth_s, {title:LANG.ct_dpth_l, convert:toFloat, bound:bound(0.005,100), units}), - separator: newBlank({ class:"set-sep", driven }), - camTabsMidline: newBoolean(LANG.ct_midl_s, onBooleanClick, {title:LANG.ct_midl_l}), - separator: newBlank({ class:"set-sep", driven }), - camTabsManual: newRow([ - (ui.tabAdd = newButton(undefined, onButtonClick, {icon:''})), - (ui.tabDun = newButton(undefined, onButtonClick, {icon:''})), - (ui.tabClr = newButton(undefined, onButtonClick, {icon:''})) - ], {class:"ext-buttons f-row"}), - _____: newGroup(LANG.cs_menu, $('cam-stock'), { modes:CAM, driven, separator }), - camStockX: newInput(LANG.cs_wdth_s, {title:LANG.cs_wdth_l, convert:toFloat, bound:bound(0,9999), units}), - camStockY: newInput(LANG.cs_dpth_s, {title:LANG.cs_dpth_l, convert:toFloat, bound:bound(0,9999), units}), - camStockZ: newInput(LANG.cs_hght_s, {title:LANG.cs_hght_l, convert:toFloat, bound:bound(0,9999), units}), - separator: newBlank({ class:"set-sep", driven }), - camStockOffset: newBoolean(LANG.cs_offs_s, onBooleanClick, {title:LANG.cs_offs_l}), - camStockClipTo: newBoolean(LANG.cs_clip_s, onBooleanClick, {title:LANG.cs_clip_l}), - camStockIndexed: newBoolean(LANG.cs_indx_s, onBooleanClick, {title:LANG.cs_indx_l}), - camStockIndexGrid: newBoolean(LANG.cs_ishg_s, onBooleanClick, {title:LANG.cs_ishg_l, show:() => ui.camStockIndexed.checked}), - _____: newGroup(LANG.cc_menu, $('cam-limits'), { modes:CAM, driven, separator }), - camZAnchor: newSelect(LANG.ou_zanc_s, {title: LANG.ou_zanc_l, action:zAnchorSave, show:() => !ui.camStockIndexed.checked}, "zanchor"), - camZOffset: newInput(LANG.ou_ztof_s, {title:LANG.ou_ztof_l, convert:toFloat, units}), - camZTop: newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:toFloat, units, trigger}), - camZBottom: newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:toFloat, units, trigger}), - camZThru: newInput(LANG.ou_ztru_s, {title:LANG.ou_ztru_l, convert:toFloat, bound:bound(0.0,100), units }), - camZClearance: newInput(LANG.ou_zclr_s, {title:LANG.ou_zclr_l, convert:toFloat, bound:bound(0.01,100), units }), - camFastFeedZ: newInput(LANG.cc_rzpd_s, {title:LANG.cc_rzpd_l, convert:toFloat, units}), - camFastFeed: newInput(LANG.cc_rapd_s, {title:LANG.cc_rapd_l, convert:toFloat, units}), - _____: newGroup(LANG.ou_menu, $('cam-output'), { modes:CAM, driven, separator, group:"cam-output" }), - camConventional: newBoolean(LANG.ou_conv_s, onBooleanClick, {title:LANG.ou_conv_l}), - camEaseDown: newBoolean(LANG.cr_ease_s, onBooleanClick, {title:LANG.cr_ease_l}), - camDepthFirst: newBoolean(LANG.ou_depf_s, onBooleanClick, {title:LANG.ou_depf_l}), - camToolInit: newBoolean(LANG.ou_toin_s, onBooleanClick, {title:LANG.ou_toin_l}), - separator: newBlank({ class:"set-sep", driven }), - camFirstZMax: newBoolean(LANG.ou_z1st_s, onBooleanClick, {title:LANG.ou_z1st_l}), - camForceZMax: newBoolean(LANG.ou_forz_s, onBooleanClick, {title:LANG.ou_forz_l}), - separator: newBlank({ class:"set-sep", driven }), - camEaseAngle: newInput(LANG.ou_eang_s, {title:LANG.ou_eang_l, convert:toFloat, bound:bound(0.1,85), show:() => ui.camEaseDown.checked}), - camFullEngage: newInput(LANG.ou_feng_s, {title:LANG.ou_feng_l, convert:toFloat, bound:bound(0.1,1.0)}), - _____: newGroup(LANG.co_menu, $('cam-origin'), { modes:CAM, driven, separator }), - camOriginTop: newBoolean(LANG.or_topp_s, onBooleanClick, {title:LANG.or_topp_l}), - camOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, {title:LANG.or_cntr_l}), - separator: newBlank({ class:"set-sep", driven }), - camOriginOffX: newInput(LANG.co_offx_s, {title:LANG.co_offx_l, convert:toFloat, units}), - camOriginOffY: newInput(LANG.co_offy_s, {title:LANG.co_offy_l, convert:toFloat, units}), - camOriginOffZ: newInput(LANG.co_offz_s, {title:LANG.co_offz_l, convert:toFloat, units}), - _____: newGroup(LANG.op_xprt_s, $('cam-expert'), { group:"cam_expert", modes:CAM, marker: false, driven, separator }), - camExpertFast: newBoolean(LANG.cx_fast_s, onBooleanClick, {title:LANG.cx_fast_l, show: () => !ui.camTrueShadow.checked }), - camTrueShadow: newBoolean(LANG.cx_true_s, onBooleanClick, {title:LANG.cx_true_l, show: () => !ui.camExpertFast.checked }), - separator: newBlank({ class:"set-sep", driven }), - camArcTolerance: newInput(LANG.cx_arct_s, {title:LANG.cx_arct_l, convert:toFloat, units, bound:bound(0,100)}), - camArcResolution: newInput(LANG.cx_arcr_s, {title:LANG.cx_arcr_l, convert:toFloat, bound:bound(0,180), show:() => ui.camArcTolerance.value > 0}), - - /** LASER/DRAG/WJET/WEDM cut tool Settings */ - - _____: newGroup(LANG.sl_menu, $('lzr-slice'), { modes:TWOD, driven, separator }), - ctSliceKerf: newInput(LANG.ls_offs_s, {title:LANG.ls_offs_l, convert:toFloat}), - ctSliceHeight: newInput(LANG.ls_lahi_s, {title:LANG.ls_lahi_l, convert:toFloat, trigger}), - ctSliceHeightMin: newInput(LANG.ls_lahm_s, {title:LANG.ls_lahm_l, convert:toFloat, show:() => ui.ctSliceHeight.value == 0 && !ui.ctSliceSingle.checked }), - separator: newBlank({ class:"set-sep", driven }), - ctSliceSingle: newBoolean(LANG.ls_sngl_s, onBooleanClick, {title:LANG.ls_sngl_l}), - ctOmitInner: newBoolean(LANG.we_omit_s, onBooleanClick, {title:LANG.we_omit_l, modes:WEDM}), - _____: newGroup('surfaces', $('lzr-surface'), { modes:[-1], driven, separator }), - ctSurfaces: newRow([ - (ui.faceAdd = newButton(undefined, onButtonClick, {icon:''})), - (ui.faceDun = newButton(undefined, onButtonClick, {icon:''})), - (ui.faceClr = newButton(undefined, onButtonClick, {icon:''})) - ], {class:"ext-buttons f-row", modes:WEDM}), - _____: newGroup(LANG.dk_menu, $('lzr-knife'), { modes:DRAG, marker:true, driven, separator }), - ctOutKnifeDepth: newInput(LANG.dk_dpth_s, { title:LANG.dk_dpth_l, convert:toFloat, bound:bound(0.0,5.0) }), - ctOutKnifePasses: newInput(LANG.dk_pass_s, { title:LANG.dk_pass_l, convert:toInt, bound:bound(0,5) }), - ctOutKnifeTip: newInput(LANG.dk_offs_s, { title:LANG.dk_offs_l, convert:toFloat, bound:bound(0.0,10.0) }), - _____: newGroup(LANG.lo_menu, $('lzr-layout'), { modes:TWOD, driven, separator }), - ctOutTileSpacing: newInput(LANG.ou_spac_s, { title:LANG.ou_spac_l, convert:toInt }), - ctOutMerged: newBoolean(LANG.ou_mrgd_s, onBooleanClick, {title:LANG.ou_mrgd_l, modes:TWONED, show:() => !ui.ctOutStack.checked }), - ctOutGroup: newBoolean(LANG.ou_grpd_s, onBooleanClick, {title:LANG.ou_grpd_l, show:() => !(ui.ctOutMark.checked || ui.ctOutStack.checked) }), - _____: newGroup(LANG.ou_menu, $('lzr-output'), { modes:TWOD, driven, separator, group:"lzr-output" }), - ctOutPower: newInput(LANG.ou_powr_s, {title:LANG.ou_powr_l, convert:toInt, bound:bound(1,100), modes:TWONED }), - ctOutSpeed: newInput(LANG.ou_sped_s, {title:LANG.ou_sped_l, convert:toInt }), - ctAdaptive: newBoolean('adaptive speed', onBooleanClick, {modes:WEDM, title:'controller determines best cutting speed based on material feedback at runtime'}), - separator: newBlank({ class:"set-sep", driven }), - ctOriginBounds: newBoolean(LANG.or_bnds_s, onBooleanClick, { title:LANG.or_bnds_l, show:() => !ui.ctOriginCenter.checked }), - ctOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, { title:LANG.or_cntr_l, show:() => !ui.ctOriginBounds.checked }), - separator: newBlank({ class:"set-sep", driven, modes:WEDM, show:() => ui.ctOriginBounds.checked }), - ctOriginOffX: newInput(LANG.or_offx_s, { title:LANG.or_offx_l, convert:toFloat, modes:WEDM, show:() => ui.ctOriginBounds.checked }), - ctOriginOffY: newInput(LANG.or_offy_s, { title:LANG.or_offy_l, convert:toFloat, modes:WEDM, show:() => ui.ctOriginBounds.checked }), - separator: newBlank({ class:"set-sep", driven, modes:TWONED }), - ctOutZColor: newBoolean(LANG.ou_layo_s, onBooleanClick, { title:LANG.ou_layo_l, modes:TWONED, show:() => !ui.ctOutMerged.checked }), - ctOutLayer: newBoolean(LANG.ou_layr_s, onBooleanClick, { title:LANG.ou_layr_l, modes:TWONED, show:() => !ui.ctOutStack.checked }), - ctOutMark: newBoolean(LANG.ou_lays_s, onBooleanClick, { title:LANG.ou_lays_l, modes:TWONED, show:() => !ui.ctOutStack.checked }), - separator: newBlank({ class:"set-sep", driven, modes:LASER }), - ctOutInches: newBoolean(LANG.ou_inch_s, onBooleanClick, { title:LANG.ou_inch_l, modes:LASER }), - ctOutStack: newBoolean(LANG.ou_stak_s, onBooleanClick, { title:LANG.ou_stak_l, modes:LASER }), - ctOutShaper: newBoolean(LANG.ou_shap_s, onBooleanClick, { title:LANG.ou_shap_l, modes:LASER, show:() => ui.ctOutStack.checked }), - - /** SLA SETTINGS */ - - slaProc: newGroup(LANG.sa_menu, $('sla-slice'), { modes:SLA, group:"sla-slice", driven, separator }), - slaSlice: newInput(LANG.sa_lahe_s, {title:LANG.sa_lahe_l, convert:toFloat}), - slaShell: newInput(LANG.sa_shel_s, {title:LANG.sa_shel_l, convert:toFloat}), - slaOpenTop: newBoolean(LANG.sa_otop_s, onBooleanClick, {title:LANG.sa_otop_l}), - slaOpenBase: newBoolean(LANG.sa_obas_s, onBooleanClick, {title:LANG.sa_obas_l}), - slaLayers: newGroup(LANG.sa_layr_m, $('sla-layers'), { modes:SLA, group:"sla-layers", driven, separator }), - slaLayerOn: newInput(LANG.sa_lton_s, {title:LANG.sa_lton_l, convert:toFloat}), - slaLayerOff: newInput(LANG.sa_ltof_s, {title:LANG.sa_ltof_l, convert:toFloat}), - slaPeelDist: newInput(LANG.sa_pldi_s, {title:LANG.sa_pldi_l, convert:toFloat}), - slaPeelLiftRate: newInput(LANG.sa_pllr_s, {title:LANG.sa_pllr_l, convert:toFloat}), - slaPeelDropRate: newInput(LANG.sa_pldr_s, {title:LANG.sa_pldr_l, convert:toFloat}), - slaBase: newGroup(LANG.sa_base_m, $('sla-base'), { modes:SLA, group:"sla-base", driven, separator }), - slaBaseLayers: newInput(LANG.sa_balc_s, {title:LANG.sa_balc_l, convert:toInt}), - slaBaseOn: newInput(LANG.sa_lton_s, {title:LANG.sa_bltn_l, convert:toFloat}), - slaBaseOff: newInput(LANG.sa_ltof_s, {title:LANG.sa_bltf_l, convert:toFloat}), - slaBasePeelDist: newInput(LANG.sa_pldi_s, {title:LANG.sa_pldi_l, convert:toFloat}), - slaBasePeelLiftRate: newInput(LANG.sa_pllr_s, {title:LANG.sa_pllr_l, convert:toFloat}), - slaFill: newGroup(LANG.sa_infl_m, $('sla-fill'), { modes:SLA, group:"sla-infill", driven, separator }), - slaFillDensity: newInput(LANG.sa_ifdn_s, {title:LANG.sa_ifdn_l, convert:toFloat, bound:bound(0,1)}), - slaFillLine: newInput(LANG.sa_iflw_s, {title:LANG.sa_iflw_l, convert:toFloat, bound:bound(0,5)}), - slaSupport: newGroup(LANG.sa_supp_m, $('sla-support'), { modes:SLA, group:"sla-support", driven, separator }), - slaSupportLayers: newInput(LANG.sa_slyr_s, {title:LANG.sa_slyr_l, convert:toInt, bound:bound(5,100)}), - slaSupportGap: newInput(LANG.sa_slgp_s, {title:LANG.sa_slgp_l, convert:toInt, bound:bound(3,30)}), - slaSupportDensity: newInput(LANG.sa_sldn_s, {title:LANG.sa_sldn_l, convert:toFloat, bound:bound(0.01,0.9)}), - slaSupportSize: newInput(LANG.sa_slsz_s, {title:LANG.sa_slsz_l, convert:toFloat, bound:bound(0.1,1)}), - slaSupportPoints: newInput(LANG.sa_slpt_s, {title:LANG.sa_slpt_l, convert:toInt, bound:bound(3,10)}), - slaSupportEnable: newBoolean(LANG.enable, onBooleanClick, {title:LANG.sl_slen_l}), - slaOutput: newGroup(LANG.sa_outp_m, $('sla-output'), { modes:SLA, driven, separator, group:"sla-output" }), - slaFirstOffset: newInput(LANG.sa_opzo_s, {title:LANG.sa_opzo_l, convert:toFloat, bound:bound(0,1)}), - slaAntiAlias: newSelect(LANG.sa_opaa_s, {title:LANG.sa_opaa_l}, "antialias"), - - layers: uc.setGroup($("layers")), - - settingsName: $('settingsName'), - settingsSave: $('settingsSave'), - }); - - // run client mode initializations right after UI is bound - initCAM(); - initDRAG(); - initFDM(); - initLaser(); - initSLA(); - initWEDM(); - initWJET(); - - // override old style settings two-button menu - ui.settingsSave.onclick = () => { - settingsSave(undefined, ui.settingsName.value); - }; - - function optSelected(sel) { - let opt = sel.options[sel.selectedIndex]; - return opt ? opt.value : undefined; - } - - function hasInfill() { - return optSelected(ui.sliceFillType) !== 'none' - } - - function fillIsLinear() { - return hasInfill() && optSelected(ui.sliceFillType) === 'linear'; - } - - function spindleShow() { - return settings().device.spindleMax > 0; - } - - function zIntShow() { - return settings().controller.devel; - } - - // slider setup - const mobile = space.info.mob; - const slbar = mobile ? 80 : 30; - const slbar2 = slbar * 2; - const slider = ui.sliderRange; - const drag = { }; - - if (mobile) { - ui.slider.classList.add('slider-mobile'); - ui.sliderLo.classList.add('slider-mobile'); - ui.sliderHi.classList.add('slider-mobile'); - // add css style for mobile devices - DOC.body.classList.add('mobile'); - } - - function pxToInt(txt) { - return txt ? parseInt(txt.substring(0,txt.length-2)) : 0; - } - - function sliderUpdate() { - let start = drag.low / drag.maxval; - let end = (drag.low + drag.mid - slbar) / drag.maxval; - api.event.emit('slider.pos', { start, end }); - api.var.layer_lo = Math.round(start * api.var.layer_max); - api.var.layer_hi = Math.round(end * api.var.layer_max); - api.show.layer(); - space.scene.active(); - } - - function dragit(el, delta) { - el.ontouchstart = el.onmousedown = (ev) => { - // el.classList.add('sli-drag-el'); - tracker.style.display = 'block'; - ev.stopPropagation(); - let obj = (ev.touches ? ev.touches[0] : ev); - drag.width = slider.clientWidth; - drag.maxval = drag.width - slbar2; - drag.start = obj.screenX; - drag.loat = drag.low = pxToInt(ui.sliderHold.style.marginLeft); - drag.mdat = drag.mid = ui.sliderMid.clientWidth; - drag.hiat = pxToInt(ui.sliderHold.style.marginRight); - drag.mdmax = drag.width - slbar - drag.loat; - drag.himax = drag.width - slbar - drag.mdat; - let cancel_drag = tracker.ontouchend = tracker.onmouseup = (ev) => { - // el.classList.remove('sli-drag-el'); - if (ev) { - ev.stopPropagation(); - ev.preventDefault(); - } - slider.onmousemove = undefined; - tracker.style.display = 'none'; - }; - el.ontouchend = cancel_drag; - el.ontouchmove = tracker.ontouchmove = tracker.onmousemove = (ev) => { - ev.stopPropagation(); - ev.preventDefault(); - if (ev.buttons === 0) { - return cancel_drag(); - } - if (delta) { - let obj = (ev.touches ? ev.touches[0] : ev); - delta(obj.screenX - drag.start); - } - }; - }; - } - - dragit(ui.sliderLo, (delta) => { - let midval = drag.mdat - delta; - let lowval = drag.loat + delta; - if (midval < slbar || lowval < 0) { - return; - } - ui.sliderHold.style.marginLeft = `${lowval}px`; - ui.sliderMid.style.width = `${midval}px`; - drag.low = lowval; - drag.mid = midval; - sliderUpdate(); - }); - dragit(ui.sliderMid, (delta) => { - let loval = drag.loat + delta; - let hival = drag.hiat - delta; - if (loval < 0 || hival < 0) return; - ui.sliderHold.style.marginLeft = `${loval}px`; - ui.sliderHold.style.marginRight = `${hival}px`; - drag.low = loval; - sliderUpdate(); - }); - dragit(ui.sliderHi, (delta) => { - let midval = drag.mdat + delta; - let hival = drag.hiat - delta; - if (midval < slbar || midval > drag.mdmax || hival < 0) return; - ui.sliderMid.style.width = `${midval}px`; - ui.sliderHold.style.marginRight = `${hival}px`; - drag.mid = midval; - sliderUpdate(); - }); - - ui.load.onchange = function(event) { - api.platform.load_files(event.target.files); - ui.load.value = ''; // reset so you can re-import the same filee - }; - - ui.sliderMin.onclick = () => { - api.show.layer(0,0); - } - - ui.sliderMax.onclick = () => { - api.show.layer(api.var.layer_max,0); - } - - ui.slider.onmouseover = (ev) => { - api.event.emit('slider.label'); - }; - - ui.slider.onmouseleave = (ev) => { - if (!ev.buttons) api.event.emit('slider.unlabel'); - }; - - api.event.on('slider.unlabel', (values) => { - }); - - api.event.on('slider.label', (values) => { - let digits = api.var.layer_max.toString().length; - $('slider-zero').style.width = `${digits}em`; - $('slider-max').style.width = `${digits}em`; - $('slider-zero').innerText = api.var.layer_lo; - $('slider-max').innerText = api.var.layer_hi; - }); - - api.event.on('slider.set', (values) => { - let width = slider.clientWidth; - let maxval = width - slbar2; - let start = Math.max(0, Math.min(1, values.start)); - let end = Math.max(start, Math.min(1, values.end)); - let lowval = start * maxval; - let midval = ((end - start) * maxval) + slbar; - let hival = maxval - end * maxval; - ui.sliderHold.style.marginLeft = `${lowval}px`; - ui.sliderMid.style.width = `${midval}px`; - ui.sliderHold.style.marginRight = `${hival}px`; - }); - - // store layer preferences - api.event.on('stack.show', label => { - let mode = api.mode.get(); - let view = api.view.get(); - api.conf.get().labels[`${mode}-${view}-${label}`] = true; - }); - - api.event.on('stack.hide', label => { - let mode = api.mode.get(); - let view = api.view.get(); - api.conf.get().labels[`${mode}-${view}-${label}`] = false; - }); - - // bind language choices - $('lset-en').onclick = function() { - sdb.setItem('kiri-lang', 'en-us'); - api.space.reload(); - }; - $('lset-da').onclick = function() { - sdb.setItem('kiri-lang', 'da-dk'); - api.space.reload(); - }; - $('lset-de').onclick = function() { - sdb.setItem('kiri-lang', 'de-de'); - api.space.reload(); - }; - $('lset-fr').onclick = function() { - sdb.setItem('kiri-lang', 'fr-fr'); - api.space.reload(); - }; - $('lset-pl').onclick = function() { - sdb.setItem('kiri-lang', 'pl-pl'); - api.space.reload(); - }; - $('lset-pt').onclick = function() { - sdb.setItem('kiri-lang', 'pt-pt'); - api.space.reload(); - }; - $('lset-es').onclick = function() { - sdb.setItem('kiri-lang', 'es-es'); - api.space.reload(); - }; - $('lset-zh').onclick = function() { - sdb.setItem('kiri-lang', 'zh'); - api.space.reload(); - }; - - space.event.addHandlers(self, [ - 'keyup', keyUpHandler, - 'keydown', keyDownHandler, - 'keypress', keyHandler, - 'dragover', dragOverHandler, - 'dragleave', dragLeave, - 'drop', dropHandler - ]); - - function selectionSize(e) { - let dv = parseFloat(e.target.value || 1), - pv = parseFloat(e.target.was || 1), - ra = dv / pv, - xv = parseFloat(ui.sizeX.was ?? ui.scaleX.value) || 1, - yv = parseFloat(ui.sizeY.was ?? ui.scaleY.value) || 1, - zv = parseFloat(ui.sizeZ.was ?? ui.scaleZ.value) || 1, - ta = e.target, - xc = ui.lockX.checked, - yc = ui.lockY.checked, - zc = ui.lockZ.checked, - xt = ta === ui.sizeX, - yt = ta === ui.sizeY, - zt = ta === ui.sizeZ, - tl = (xt && xc) || (yt && yc) || (zt && zc), - xr = ((tl && xc) || (!tl && xt) ? ra : 1), - yr = ((tl && yc) || (!tl && yt) ? ra : 1), - zr = ((tl && zc) || (!tl && zt) ? ra : 1); - // prevent null scale - if (xr * yr * zr === 0) { - return; - } - selection.scale(xr,yr,zr); - ui.sizeX.was = ui.sizeX.value = xv * xr; - ui.sizeY.was = ui.sizeY.value = yv * yr; - ui.sizeZ.was = ui.sizeZ.value = zv * zr; - } - - function selectionScale(e) { - let dv = parseFloat(e.target.value || 1), - pv = parseFloat(e.target.was || 1), - ra = dv / pv, - xv = parseFloat(ui.scaleX.was ?? ui.scaleX.value) || 1, - yv = parseFloat(ui.scaleY.was ?? ui.scaleY.value) || 1, - zv = parseFloat(ui.scaleZ.was ?? ui.scaleY.value) || 1, - ta = e.target, - xc = ui.lockX.checked, - yc = ui.lockY.checked, - zc = ui.lockZ.checked, - xt = ta === ui.scaleX, - yt = ta === ui.scaleY, - zt = ta === ui.scaleZ, - tl = (xt && xc) || (yt && yc) || (zt && zc), - xr = ((tl && xc) || (!tl && xt) ? ra : 1), - yr = ((tl && yc) || (!tl && yt) ? ra : 1), - zr = ((tl && zc) || (!tl && zt) ? ra : 1); - - selection.scale(xr,yr,zr); - ui.scaleX.was = ui.scaleX.value = xv * xr; - ui.scaleY.was = ui.scaleY.value = yv * yr; - ui.scaleZ.was = ui.scaleZ.value = zv * zr; - } - - function selectionRotate(e) { - let val = parseFloat(e.target.value) || 0; - e.target.value = val; - } - - space.event.onEnterKey([ - ui.scaleX, selectionScale, - ui.scaleY, selectionScale, - ui.scaleZ, selectionScale, - ui.sizeX, selectionSize, - ui.sizeY, selectionSize, - ui.sizeZ, selectionSize, - ui.toolName, updateTool, - ui.toolNum, updateTool, - ui.toolFluteDiam, updateTool, - ui.toolFluteLen, updateTool, - ui.toolShaftDiam, updateTool, - ui.toolShaftLen, updateTool, - ui.toolTaperTip, updateTool, - ui.toolTaperAngle, updateTool, - $('rot_x'), selectionRotate, - $('rot_y'), selectionRotate, - $('rot_z'), selectionRotate - ], true); - - $('lab-axis').onclick = () => { - ui.lockX.checked = - ui.lockY.checked = - ui.lockZ.checked = !( - ui.lockX.checked || - ui.lockY.checked || - ui.lockZ.checked - ); - }; - - $('scale-reset').onclick = $('lab-scale').onclick = () => { - selection.scale(1 / ui.scaleX.was, 1 / ui.scaleY.was, 1 / ui.scaleZ.was); - ui.scaleX.value = ui.scaleY.value = ui.scaleZ.value = - ui.scaleX.was = ui.scaleY.was = ui.scaleZ.was = 1; - }; - - $('app-xpnd').onclick = () => { - try { - DOC.body.requestFullscreen(); - } catch (e) { - event.emit('resize'); - moto.space.event.onResize(); - } - }; - - if (!DOC.body.requestFullscreen) { - $('app-xpnd').style.display = 'none'; - } - - uc.onBlur([ - ui.toolName, - ui.toolNum, - ui.toolFluteDiam, - ui.toolFluteLen, - ui.toolShaftDiam, - ui.toolShaftLen, - ui.toolTaperTip, - ], updateTool); - - ui.toolMetric.onclick = updateTool; - ui.toolType.onchange = updateTool; - // default show gcode pre - ui.gcodePre.button.click(); - - function mksvg(src) { - let svg = DOC.createElement('svg'); - svg.innerHTML = src; - return svg; - } - - function mklbl(src) { - let lbl = DOC.createElement('label'); - lbl.innerText = src; - return lbl; - } - - api.platform.update_size(); - - function mouseOnHover(int, event, ints) { - if (!api.feature.hover) return; - if (!int) return api.feature.hovers || api.widgets.meshes(); - api.event.emit('mouse.hover', {int, ints, event, point: int.point, type: 'widget'}); - } - - function platformOnHover(int, event) { - if (!api.feature.hover) return; - if (int) api.event.emit('mouse.hover', {point: int, event, type: 'platform'}); - } - - api.event.on("feature.hover", enable => { - space.mouse.onHover(enable ? mouseOnHover : undefined); - space.platform.onHover(enable ? platformOnHover : undefined); - }); - - // block standard browser context menu - DOC.oncontextmenu = (event) => { - let et = event.target; - if (et.tagName === 'CANVAS' || et.id === 'context-menu' || et.classList.contains('draggable')) { - event.preventDefault(); - event.stopPropagation(); - return false; - } - }; - - space.mouse.downSelect((int, event) => { - if (api.feature.on_mouse_down) { - if (int) { - api.feature.on_mouse_down(int, event); - return; - } - } - if (api.feature.hover) { - if (int) { - return api.event.emit('mouse.hover.down', {int, point: int.point}); - } else { - return selection.meshes(); - } - } - // lay flat with meta or ctrl clicking a selected face - if (int && (event.ctrlKey || event.metaKey || api.feature.on_face_select)) { - let q = new THREE.Quaternion(); - // find intersecting point, look "up" on Z and rotate to face that - q.setFromUnitVectors(int.face.normal, new THREE.Vector3(0,0,-1)); - selection.rotate(q); - } - if (api.view.get() !== VIEWS.ARRANGE) { - // return no selection in modes other than arrange - return null; - } else { - // return selected meshes for further mouse processing - return api.feature.hovers || selection.meshes(); - } - }); - - space.mouse.upSelect((object, event) => { - if (api.feature.on_mouse_up) { - if (event && object) { - return api.feature.on_mouse_up(object, event); - } else { - return api.widgets.meshes(); - } - } - if (event && api.feature.hover) { - api.event.emit('mouse.hover.up', { object, event }); - return; - } - if (event && event.target.nodeName === "CANVAS") { - if (object && object.object) { - if (object.object.widget) { - platform.select(object.object.widget, event.shiftKey, false); - } - } else { - platform.deselect(); - } - } else { - return api.feature.hovers || api.widgets.meshes(); - } - }); - - space.mouse.onDrag(function(delta, offset, up = false) { - if (api.feature.hover) { - return; - } - if (up) { - api.event.emit('mouse.drag.done', offset); - } - if (delta && ui.freeLayout.checked) { - let set = settings(); - let dev = set.device; - let bound = set.bounds_sel; - let width = dev.bedWidth/2; - let depth = dev.bedDepth/2; - let isout = ( - bound.min.x <= -width || - bound.min.y <= -depth || - bound.max.x >= width || - bound.max.y >= depth - ); - if (!isout) { - if (bound.min.x + delta.x <= -width) return; - if (bound.min.y + delta.y <= -depth) return; - if (bound.max.x + delta.x >= width) return; - if (bound.max.y + delta.y >= depth) return; - } - selection.move(delta.x, delta.y, 0); - api.event.emit('selection.drag', delta); - } else { - return selection.meshes().length > 0; - } - }); - - api.space.restore(init_two) || checkSeed(init_two) || init_two(); -}; - -// SECOND STAGE INIT AFTER UI RESTORED -function init_two() { - api.event.emit('init.two'); - - // load script extensions - if (SETUP.s) SETUP.s.forEach(function(lib) { - let scr = DOC.createElement('script'); - scr.setAttribute('defer',true); - scr.setAttribute('src',`/code/${lib}.js?${version}`); - DOC.body.appendChild(scr); - stats.add('load_'+lib); - api.event.emit('load.lib', lib); - }); - - // load CSS extensions - if (SETUP.ss) SETUP.ss.forEach(function(style) { - style = style.charAt(0) === '/' ? style : `/kiri/style-${style}`; - let ss = DOC.createElement('link'); - ss.setAttribute("type", "text/css"); - ss.setAttribute("rel", "stylesheet"); - ss.setAttribute("href", `${style}.css?${version}`); - DOC.body.appendChild(ss); - }); - - // override stored settings - if (SETUP.v) SETUP.v.forEach(function(kv) { - kv = kv.split('='); - sdb.setItem(kv[0],kv[1]); - }); - - // import octoprint settings - if (SETUP.ophost) { - let ohost = api.const.OCTO = { - host: SETUP.ophost[0], - apik: SETUP.opkey ? SETUP.opkey[0] : '' - }; - sdb['octo-host'] = ohost.host; - sdb['octo-apik'] = ohost.apik; - console.log({octoprint:ohost}); - } - - // load workspace from url - if (SETUP.wrk) { - set_ctrl.import_url(`${proto}//${SETUP.wrk[0]}`, false); - } - - // load an object from url - if (SETUP.load) { - console.log({load:SETUP}); - api.platform.load_url(`${proto}//${SETUP.load[0]}`); - } - - // bind this to UI so main can call it on settings import - ui.sync = function() { - const current = settings(); - const control = current.controller; - - if (!control.devel) { - // TODO: hide thin type 3 during development - consts.LISTS.thin.length = 3; - } - - platform.deselect(); - catalog.addFileListener(updateCatalog); - space.view.setZoom(control.reverseZoom, control.zoomSpeed); - space.platform.setGridZOff(undefined); - space.platform.setZOff(0.05); - - // restore UI state from settings - ui.showOrigin.checked = control.showOrigin; - ui.showRulers.checked = control.showRulers; - ui.showSpeeds.checked = control.showSpeeds; - ui.freeLayout.checked = control.freeLayout; - ui.autoLayout.checked = control.autoLayout; - ui.spaceRandoX.checked = control.spaceRandoX; - ui.antiAlias.checked = control.antiAlias; - ui.reverseZoom.checked = control.reverseZoom; - ui.autoSave.checked = control.autoSave; - ui.healMesh.checked = control.healMesh; - ui.threaded.checked = setThreaded(control.threaded); - ui.assembly.checked = control.assembly; - ui.ortho.checked = control.ortho; - ui.devel.checked = control.devel; - lineTypeSave(); - detailSave(); - updateStats(); - - // optional set-and-lock mode (hides mode menu) - let SETMODE = SETUP.mode ? SETUP.mode[0] : null; - - // optional set-and-lock device (hides device menu) - let DEVNAME = SETUP.dev ? SETUP.dev[0] : null; - - // setup default mode and enable mode locking, if set - api.mode.set(SETMODE || STARTMODE || current.mode, SETMODE); - - // fill device list - api.devices.refresh(); - - // update ui fields from settings - api.conf.update_fields(); - - // default to ARRANGE view mode - api.view.set(VIEWS.ARRANGE); - - // add ability to override - api.show.controls(api.feature.controls); - - // update everything dependent on the platform size - platform.update_size(); - - // load wasm if indicated - client.wasm(control.assembly === true); - }; - - ui.sync(); - - // clear alerts as they build up - setInterval(api.event.alerts, 1000); - - // add hide-alerts-on-alert-click - ui.alert.dialog.onclick = function() { - api.event.alerts(true); - }; - - // enable modal hiding - $('mod-x').onclick = api.modal.hide; - - if (!SETUP.s) console.log(`kiri | init main | ${version}`); - - // send init-done event - api.event.emit('init-done', stats); - - // show gdpr if it's never been seen and we're not iframed - const isLocal = LOCAL || WIN.location.host.split(':')[0] === 'localhost'; - if (!sdb.gdpr && WIN.self === WIN.top && !SETUP.debug && !isLocal) { - $('gdpr').style.display = 'flex'; - } - - // warn of degraded functionality when SharedArrayBuffers are missing - if (api.feature.work_alerts && !window.SharedArrayBuffer) { - api.alerts.show("The security context of this", 10); - api.alerts.show("Window blocks important functionality.", 10); - api.alerts.show("Try a Chromium-base Browser instead", 10); - } - - // add keyboard focus handler (must use for iframes) - WIN.addEventListener('load', function () { - WIN.focus(); - DOC.body.addEventListener('click', function() { - WIN.focus(); - },false); - }); - - // dismiss gdpr alert - $('gotit').onclick = () => { - $('gdpr').style.display = 'none'; - sdb.gdpr = Date.now(); - }; - - // bind interface action elements - $('mode-device').onclick = api.show.devices; - $('mode-profile').onclick = settingsLoad; - $('mode-fdm').onclick = () => api.mode.set('FDM'); - $('mode-cam').onclick = () => api.mode.set('CAM'); - $('mode-sla').onclick = () => api.mode.set('SLA'); - $('mode-laser').onclick = () => api.mode.set('LASER'); - $('mode-drag').onclick = () => api.mode.set('DRAG'); - $('mode-wjet').onclick = () => api.mode.set('WJET'); - $('mode-wedm').onclick = () => api.mode.set('WEDM'); - $('set-device').onclick = (ev) => { ev.stopPropagation(); api.show.devices() }; - $('set-profs').onclick = (ev) => { ev.stopPropagation(); api.conf.show() }; - $('set-tools').onclick = (ev) => { ev.stopPropagation(); api.show.tools() }; - $('set-prefs').onclick = (ev) => { ev.stopPropagation(); api.modal.show('prefs') }; - ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() }; - ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; - ui.acct.mesh.onclick = (ev) => { ev.stopPropagation(); WIN.location = "/mesh" }; - ui.acct.export.onclick = (ev) => { ev.stopPropagation(); profileExport() }; - ui.acct.export.title = LANG.acct_xpo; - $('file-new').onclick = (ev) => { ev.stopPropagation(); workspaceNew() }; - $('file-recent').onclick = () => { api.modal.show('files') }; - $('file-import').onclick = (ev) => { api.event.import(ev); }; - ui.func.slice.onclick = (ev) => { ev.stopPropagation(); api.function.slice() }; - ui.func.preview.onclick = (ev) => { ev.stopPropagation(); api.function.print() }; - ui.func.animate.onclick = (ev) => { ev.stopPropagation(); api.function.animate() }; - ui.func.export.onclick = (ev) => { ev.stopPropagation(); api.function.export() }; - $('view-arrange').onclick = api.platform.layout; - $('view-top').onclick = space.view.top; - $('view-home').onclick = space.view.home; - $('view-front').onclick = space.view.front; - $('view-back').onclick = space.view.back; - $('view-left').onclick = space.view.left; - $('view-right').onclick = space.view.right; - $('unrotate').onclick = () => { - api.widgets.for(w => w.unrotate()); - selection.update_info(); - }; - // attach button handlers to support targets - for (let btn of ["don8pt","don8gh","don8pp"]) { - $(btn).onclick = (ev) => { - window.open(ev.target.children[0].href); - } - } - // rotation buttons - let d = (Math.PI / 180); - $('rot_x_lt').onclick = () => { selection.rotate(-d * $('rot_x').value,0,0) }; - $('rot_x_gt').onclick = () => { selection.rotate( d * $('rot_x').value,0,0) }; - $('rot_y_lt').onclick = () => { selection.rotate(0,-d * $('rot_y').value,0) }; - $('rot_y_gt').onclick = () => { selection.rotate(0, d * $('rot_y').value,0) }; - $('rot_z_lt').onclick = () => { selection.rotate(0,0, d * $('rot_z').value) }; - $('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) }; - // rendering options - $('render-edges').onclick = () => { api.view.edges({ toggle: true }); api.conf.save() }; - $('render-ghost').onclick = () => { api.view.wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); }; - $('render-wire').onclick = () => { api.view.wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); }; - $('render-solid').onclick = () => { api.view.wireframe(false, 0, 1); }; - $('mesh-export-stl').onclick = () => { objectsExport('stl') }; - $('mesh-export-obj').onclick = () => { objectsExport('obj') }; - $('mesh-merge').onclick = selection.merge; - $('context-duplicate').onclick = duplicateSelection; - $('context-mirror').onclick = mirrorSelection; - $('context-layflat').onclick = () => { api.event.emit("tool.mesh.lay-flat") }; - $('context-setfocus').onclick = () => { - api.event.emit( - "tool.camera.focus", - ev => api.space.set_focus(undefined, ev.object.point) - ); - }; - - // ui.modal.onclick = api.modal.hide; - ui.modalBox.onclick = (ev) => { ev.stopPropagation() }; - - // add app name hover info - $('app-info').innerText = version; - - // show topline separator when iframed - try { if (WIN.self !== WIN.top) $('top-sep').style.display = 'flex' } catch (e) { console.log(e) } - - // warn users they are running a beta release - if (beta && beta > 0 && sdb.kiri_beta != beta) { - api.show.alert("CAUTION"); - api.show.alert("this is a development release"); - api.show.alert("and may not function properly"); - sdb.kiri_beta = beta; - } - - // lift curtain - $('curtain').style.display = 'none'; -} - -// update static html elements with language overrides -function lang_rewrite() { - // lk attribute causes inner text to be replaced with lang value - for (let el of [...DOC.querySelectorAll("[lk]")]) { - let key = el.getAttribute('lk'); - let val = LANG[key]; - if (val) { - el.innerText = val; - } else { - console.log({missing_ln: key}); - } - } - // lkt attribute causes a title attribute to be set from lang value - for (let el of [...DOC.querySelectorAll("[lkt]")]) { - let key = el.getAttribute('lkt'); - let val = LANG[key]; - if (val) { - el.setAttribute("title", val); - } else { - console.log({missing_ln: key}); - } - } -} - -// init lang must happen before all other init functions -function init_lang() { - // if a language needs to load, the script is injected and loaded - // first. once this loads, or doesn't, the initialization begins - let lang = SETUP.ln ? SETUP.ln[0] : sdb.getItem('kiri-lang') || api.language.get(); - api.event.emit('init.lang', lang); - // inject language script if not english - if (lang && lang !== 'en' && lang !== 'en-us') { - let map = api.language.map(lang); - let scr = DOC.createElement('script'); - // scr.setAttribute('defer',true); - scr.setAttribute('src',`/kiri/lang/${map}.js?${version}`); - (DOC.body || DOC.head).appendChild(scr); - stats.set('ll',lang); - scr.onload = function() { - api.language.set(map); - lang_rewrite(); - init_one(); - }; - scr.onerror = function(err) { - console.log({language_load_error: err, lang}) - api.language.set(); - lang_rewrite(); - init_one(); - } - } else { - // set to browser default which will be overridden - // by any future script loads (above) - api.language.set(); - lang_rewrite(); - init_one(); - } -} - -export function run() { - init_lang(); - return api; -} diff --git a/src2/kiri/lang-en.js b/src2/kiri/lang-en.js deleted file mode 100644 index 85179cdd..00000000 --- a/src2/kiri/lang-en.js +++ /dev/null @@ -1,932 +0,0 @@ -// English language map. Other localization maps will -// defer to English map for any missing key/value pairs -self.lang['en'] = -self.lang['en-us'] = { - // common keys and menus - animate: "animate", // CAM animate button - arrange: "arrange", // layout workspace objects - axis: "axis", // left object scale pop menu - back: "back", - clear: "clear", // clear workspace (remove all objects) - copy: "copy", - clone: "clone", // create duplicate - delete: "delete", - detail: "detail", - done: "done", - edit: "edit", - enable: "enable", - disable: "disable", - donate: "donate", - export: "export", - files: "files", - filter: "filter", - front: "front", - fullscreen: "fullscreen", - help: "help", - ghost: "ghost", // left render pop menu (wireframe) - hide: "hide", // left render pop menu (invisible) - home: "home", - import: "import", - language: "language", - left: "left", - machines: "machines", // device or machine - mesh: "mesh", // mesh editing - metric: "metric", - name: "name", - new: "new", // file new - off: "off", // turn something off (like a lightbulb) - on: "on", // turn something on (like a lightbulb) - profs: "profiles", // right menu "profiles" - prefs: "preferences", // right menu "preferences" - preview: "preview", - recent: "recent", - rename: "rename", - render: "render", // left render pop menu - reset: "reset", - right: "right", - rotate: "rotate", // left rotate pop menu - save: "save", - scale: "scale", // left object scale pop menu - select: "select", - setup: "setup", - settings: "settings", - size: "size", - slice: "slice", - solid: "solid", // view type pop menu - start: "start", - tool: "tool", - tools: "tools", // CAM tool menu button - top: "top", - type: "type", // CAM tool type - version: "version", - view: "view", // left view pop menu - wire: "wire", // left render pop menu - volume: "volume", // device build area - - acct_xpo: ["make a backup of your device","and device profiles with the","option to include workspace","objects and positions"], - - // RIGHT-CLICK CONTEXT MENU - rc_clws: "clear workspace", - rc_xpws: "export workspace", - rc_lafl: "lay flat", - rc_mirr: "mirror", - rc_dupl: "duplicate", - rc_focs: "set focus", - rc_heal: "heal meshes", - rc_swap: "replace vertices", - rc_xobj: "export OBJ", - rc_xstl: "export STL", - sb_info: ["print speed","in mm/s"], - rc_merg: "merge object meshes", - - // DEVICE MENU and related dialogs - dm_sldt: "select a device type", - dm_stdd: "standard devices", - dm_mydd: "my devices", - dm_seld: "selected device", - dm_rcnt: "recent files", - dm_savs: "settings", - dm_appp: "Application Preferences", - - // CAM Tool Dialog - td_tyem: "end", // end mill - td_tybm: "ball", // ball mill - td_tytm: "taper", // taper mill - td_tydr: "drill", // drill - td_tonm: "tool #", - td_shft: "shaft", // endmill shaft specs - td_flut: "flute", // endmill flute specs - td_tapr: "taper", // endmill taper specs - - // DEVICE dialog groups - dv_gr_dev: "device", - dv_gr_ext: "extruder", - dv_gr_out: "output", - dv_gr_gco: "gcode macros", - - // DEVICE dialog (_s = label, _l = hover help) - dv_name_s: "name", - dv_name_l: "device name", - dv_fila_s: "filament", - dv_fila_l: "diameter in millimeters", - dv_nozl_s: "nozzle", - dv_nozl_l: "diameter in millimeters", - dv_bedw_s: "width", - dv_bedw_l: "workspace units", - dv_bedd_s: "depth", - dv_bedd_l: "workspace units", - dv_bedh_s: "height", - dv_bedh_l: ["max build height","in workspace units"], - dv_rezx_s: "resolution X", - dv_rezx_l: ["x resolution"], - dv_rezy_s: "resolution Y", - dv_rezy_l: ["y resolution"], - dv_zmax_s: "Z axis speed limit", - dv_zmax_l: ["maximum Z axis speed","in millimeters / second","0 to disable"], - dv_time_s: "output time factor", - dv_time_l: ["modifier to print time estimates","in the gcode export dialog.","raise or lower value based on your","predicted vs observed print times"], - dv_spmx_s: "max spindle", - dv_spmx_l: ["max spindle rpm speed","0 to disable"], - dv_xtab_s: "absolute positioning", - dv_xtab_l: "extrusion moves absolute", - dv_orgc_s: "origin center", - dv_orgc_l: "bed origin center", - // dv_orgt_s: "origin top", - // dv_orgt_l: "part z origin top", - dv_bedc_s: "circular bed", - dv_bedc_l: "device bed is circular", - dv_belt_s: "belt bed", - dv_belt_l: "continuous printing bed", - dv_fsrc_s: "filament source", - dv_fsrc_l: ["how filament is fed\ninto the extruder"], - dv_retr_s: "firmware retract", - dv_retr_l: ["device fimware supports G10/G11"], - dv_fanp_s: "cooling", - dv_fanp_l: "set cooling fan power", - dv_feat_s: "feature", - dv_feat_l: "inject code for specified features", - dv_prog_s: "progress", - dv_prog_l: "output on each % progress", - dv_layr_s: "layer", - dv_layr_l: ["output at each","layer change"], - dv_tksp_s: "token spacer", - dv_tksp_l: ["add a space between","gcode axis parameters","G0X0Y0X0","vs","G0 X0 Y0 Z0"], - dv_strc_s: "strip comments", - dv_strc_l: ["strip gcode comments","comments begin with ;"], - dv_fext_s: "file extension", - dv_fext_l: "file name extension", - dv_lazr_s: "enable laser", - dv_lazr_l: ["enable laser operations"], - - dv_dwll_s: "dwell", - dv_dwll_l: "gcode dwell script", - dv_tool_s: "tool", - dv_tool_l: "tool change script", - dv_sspd_s: "spindle", - dv_sspd_l: "set spindle speed", - dv_paus_s: "pause", - dv_paus_l: "gcode pause script", - dv_head_s: "header", - dv_head_l: "gcode header script", - dv_foot_s: "footer", - dv_foot_l: "gcode footer script", - dv_pext_s: "exterior", - dv_pext_l: ["gcode script run before","printing exterior lines"], - dv_pint_s: "interior", - dv_pint_l: ["gcode script run before","printing interior lines"], - dv_lzon_s: "laser on", - dv_lzon_l: "gcode laser on script", - dv_lzof_s: "laser off", - dv_lzof_l: "gcode laser off script", - - dv_waon_s: "water on", - dv_waon_l: "gcode water pump on script", - dv_waof_s: "water off", - dv_waof_l: "gcode water pump off script", - - dv_dkon_s: "knife down", - dv_dkon_l: "gcode knife down script", - dv_dkof_s: "knife up", - dv_dkof_l: "gcode knife up script", - - dv_exts_s: "select", - dv_exts_l: "gcode run to enable this extruder", - dv_dext_s: "deselect", - dv_dext_l: "gcode run before enabling another extruder", - dv_extd_s: "deselect", - dv_extd_l: "gcode to deselect this extruder", - dv_exox_s: "offset x", - dv_exox_l: "nozzle offset x", - dv_exoy_s: "offset y", - dv_exoy_l: "nozzle offset y", - - dv_paid_s: "printer id", - dv_paid_l: ["unique printer identifier","may be made up"], - dv_paps_s: "ping spacing", - dv_paps_l: ["minimum filament consumption","between pings in millimeters","recommended 500 - 1000","0 = disable pings"], - dv_pafe_s: "feed length", - dv_pafe_l: ["feeder tube length in millimeters","short=570, medium=800, long=1100"], - dv_papl_s: "push length", - dv_papl_l: ["distance between extruder gear","and nozzle tip in millimeters"], - dv_paof_s: "offset length", - dv_paof_l: ["length to add or remove from","the first segment in millimeters","usually 0"], - dv_pahe_s: "heating time", - dv_pahe_l: ["offset from default"], - dv_paco_s: "cooling time", - dv_paco_l: ["offset from default"], - dv_pacm_s: "compression time", - dv_pacm_l: ["offset from default"], - - // MODE - mo_menu: "mode", - mo_fdmp: "FDM Print", - mo_slap: "SLA Print", - mo_lazr: "Laser Cut", - mo_cncm: "CNC Mill", - - // SETUP - su_menu: "setup", - su_devi: "Devices", - su_tool: "Tools", - su_locl: "Local", - su_xprt: "Export", - su_help: "Help", - - // LOAD - fe_menu: "file", - fn_recn: "Recent", - fn_impo: "Import", - - // FUNCTION - fn_menu: "action", - fn_arra: "Arrange", - fn_slic: "Slice", - fn_prev: "Preview", - fn_expo: "Export", - - // VIEW - vu_menu: "view", - vu_home: "Home", - vu_rset: "Reset", - vu_sptp: "Top", - vu_spfr: "Front", - vu_splt: "Left", - vu_sprt: "Right", - - // WORKSPACE - ws_menu: "view", - ws_save: "Save", - ws_cler: "Clear", - - // OPTIONS - op_menu: "interface", - op_disp: "display", - op_xprt_s: "expert", - op_xprt_l: "show more settings options", - op_decl_s: "show decals", - op_decl_l: "show device decals and logos", - op_shny_s: "detail render", - op_shny_l: "choose a slice render method that is slower and uses specular highlighting. this makes it easier to see detail in some models at the expense of render speed", - op_devl_s: "developer", - op_devl_l: ["show developer options","enable developer debugging"], - op_hopo_s: "hover pop", - op_hopo_l: ["enable menu hover","to activate"], - op_dark_s: "dark mode", - op_dark_l: "dark mode interface", - op_comp_s: "compact ui", - op_comp_l: ["compact user interface","better for small screens","and tablets"], - op_shor_s: "show origin", - op_shor_l: "show device or process origin", - op_shru_s: "show rulers", - op_shru_l: ["show axes rulers","on major gridlines"], - op_sped_s: "show speeds", - op_sped_l: ["show speed to color bar","in preview mode"], - op_auto_s: "auto layout", - op_auto_l: ["automatically layout platform","when new items added"], - op_free_s: "free layout", - op_free_l: ["permit dragable layout","no effect in laser mode"], - op_spcr_s: "spacing", - op_spcr_l: ["spacing between objects","during auto layout","in workspace units","in belt mode, use 0","to auto-space by height"], - op_spoa_s: "edge angle", - op_spoa_l: ["surface angle difference triggering an edge trace"], - op_spcx_s: "random X", - op_spcx_l: ["in belt mode, randomly","layout the part on X","to even belt wear"], - op_orth_s: "orthographic", - op_orth_l: ["orthographic display","requires page refresh"], - op_anta_s: "anti-alias", - op_anta_l: ["smooth display. better but slower","requires a page refresh or reload"], - op_invr_s: "invert zoom", - op_invr_l: ["invert mouse wheel","scroll zoom"], - op_save_s: "auto save", - op_save_l: ["preserve objects in workspace","between application reloads"], - op_line_s: "line type", - op_line_l: ["line style for path rendering","impacts 3d performance","path: 3d best","flat: 2d good","line = 1d fast"], - op_unit_s: "units", - op_unit_l: ["workspace units affects","speeds and distances"], - op_anim_s: "animate", - op_anim_l: ["animation mesh density","higher is more dense","takes more memory","and is slower"], - op_exgl_l: ["show Grid:Local devices in export dialog","includes Grid:Bots and OctoPrint plugins"], - op_exgh_l: ["legacy Grid:Host local network support"], - op_exop_l: ["legacy OctoPrint local network support","Grid:Space OctoPrint plugin recommended"], - - lo_menu: "layout", - - pt_menu: "slicer", - pt_deci_s: "decimate", - pt_deci_l: ["enable or disable point decimation","during port import. for faster slicing","and lower memory usage"], - pt_qual_s: "detail", - pt_qual_l: ["level of detail to retain during slicing operations. lower is faster and uses less memory. large scale models often do no benefit from higher detail. instead it my only result in memory and processing problems. reduce this value if you are getting out of memory errors."], - pt_heal_s: "heal mesh", - pt_heal_l: ["attempt to heal non-manifold meshes during slicing. may increase slicing time. enables a warning when non-manifold geometries are detected."], - pt_thrd_s: "threaded", - pt_thrd_l: ["use parallel processing when browser supports nested workers. may increase memory pressure on larger models. disable if you are getting out of memory errors"], - pt_assy_s: "assembly", - pt_assy_l: ["use web assembly when supported by browser. if you experience artifacts or memory errors, try disabling this."], - - xp_menu: "exports", - - // SETTINGS - se_menu: "profile", - se_load: "load", - se_save: "save", - - // FDM LAYERS - sl_menu: "paths", - sl_angl_s: "slice angle", - sl_angl_l: ["slice angle bias in degrees"], - sl_lahi_s: "layer height", - sl_lahi_l: ["height of each slice","layer in millimeters"], - ad_minl_s: "layer minimum", - ad_minl_l: ["adaptive min layer height","in millimeters","must be non-zero"], - ad_adap_s: "adaptive", - ad_adap_l: ["use adaptive layer heights","with 'layer height' as max","and 'layer min' as the min"], - sl_ltop_s: "top layers", - sl_ltop_l: ["number of solid layers at the top of the print or on flat areas facing up"], - sl_lsld_s: "solid layers", - sl_lsld_l: ["solid fill areas computed","from bridges and flat areas","detected betwen sliced layers"], - sl_lbot_s: "base layers", - sl_lbot_l: ["number of solid layers at bottom of the print or under bridge spans"], - - // FDM WALLS - sw_menu: "shells", - sl_shel_s: "shell count", - sl_shel_l: ["number of perimeter","walls to generate"], - ad_thin_s: "thin walls", - ad_thin_l: ["detect and fill gaps","between shell walls"], - - // FDM SHELLS - sl_line_s: "shell width", - sl_line_l: ["extrusion width in millimeters","0 = nozzle diameter"], - sl_ordr_s: "shell order", - sl_ordr_l: ["output shell order","inside to outside","or outside to inside","affects surface quality"], - sl_strt_s: "layer start", - sl_strt_l: ["layer starting point","last = last layer end","center = part center","origin = device origin"], - - // FDM FILL - fi_menu: "sparse fill", - fi_type: "fill type", - fi_pcnt_s: "fill amount", - fi_pcnt_l: ["fill density values","0.0 - 1.0"], - fi_rept_s: "fill repeat", - fi_rept_l: ["number of times to repeat","unchanged fill layers"], - fi_angl_s: "solid angle", - fi_angl_l: ["starting angle in degrees","90 degrees added to ","each following layer","applies only to solid layers"], - fi_grow_s: "solid expand", - fi_grow_l: ["expand projected solids","can help with solid areas","over sparse infill areas","units in millimeters"], - fi_over_s: "fill overlap", - fi_over_l: ["overlap with shell and other fill","for improved extrusion bonding","as fraction of nozzle diameter","0.0 - 2.0"], - // fi_rate_s: "print speed", - fi_rate_l: ["extrusion speed for infill","set to 0 to use default","output print speeds"], - - // FDM HEATING - fh_menu: "heating", - - // FDM COOLING - fc_menu: "cooling", - - // FDM FIRST LAYER - fl_menu: "base", - fl_lahi_s: "layer height", - fl_lahi_l: ["height of each slice","in millimeters","should be >= slice height"], - fl_rate_s: "shell speed", - fl_rate_l: ["shell printing max speed","in millimeters / second"], - fl_frat_s: "fill speed", - fl_frat_l: ["fill printing max speed","in millimeters / second"], - fl_mult_s: "flow factor", - fl_mult_l: ["extrusion multiplier","0.0 - 2.0"], - fl_sfac_s: "width factor", - fl_sfac_l: ["multiplier on nozzle size","changes line spacing"], - fl_skrt_s: "skirt count", - fl_skrt_l: ["number of first-layer offset","brims to generate"], - fl_skro_s: "skirt offset", - fl_skro_l: ["skirt offset from part","in millimeters","when rafts are used","this is the raft offset"], - fl_nozl_s: "nozzle temp", - fl_nozl_l: ["degrees in celsius","output setting used","when this is zero"], - fl_bedd_s: "bed temp", - fl_bedd_l: ["degrees in celsius","output setting used","when this is zero"], - fr_spac_s: "raft gap", - fr_spac_l: ["additional layer spacing","between 1st layer and raft","in millimeters"], - fr_nabl_s: "raft enable", - fr_nabl_l: ["create a raft under the","model for better adhesion","uses skirt offset and","disables skirt output"], - fr_draf_s: "draft shield", - fr_draf_l: ["extend skirt to height of part","which acts as a shield against","drafts and traps hot air"], - - // FDM BELT ONLY - fl_zoff_s: "belt offset", - fl_zoff_l: ["height offset from belt","of lowest extrusion","in millimeters"], - fl_flat_s: "flatten", - fl_flat_l: ["flatten to the belt any point","height less than this value","in millimeters"], - fl_brim_s: "brim sides", - fl_brim_l: ["add brim to part perimeter","size is width in millimeters","0 to disable"], - fl_brin_s: "brim interior", - fl_brin_l: ["add brim to part interior","when interior voids exist","size is width in millimeters","0 to disable"], - fl_brmn_s: "brim trigger", - fl_brmn_l: ["add brims only when segment","facing belt is shorter than this","value in millimeters","0 = Infinity"], - fl_brco_s: "brim comb", - fl_brco_l: ["add brim gaps","every n lines","0 to disable"], - fl_brgp_s: "brim gap", - fl_brgp_l: ["spacing between brim and part","in millimeters"], - fl_bled_s: "part anchor", - fl_bled_l: ["belt part anchor","at start of print","in millimeters"], - fl_blmp_s: "anchor bump", - fl_blmp_l: ["add a bump to the start of","an anchor to help it peel","off at the end of the belt","in millimeters"], - - // FDM SUPPORT - sp_menu: "support", - sp_detect: "detect", - sp_dens_s: "density", - sp_dens_l: ["fraction 0.0 - 1.0","recommended 0.15","0 to disable"], - sp_size_s: "pillar size", - sp_size_l: ["pillar width in millimeters","smaller values exponentially","increase detection times"], - sp_offs_s: "part offset", - sp_offs_l: ["offset from part","in millimeters"], - sp_gaps_s: "gap layers", - sp_gaps_l: ["number of layers","offset from part"], - sp_span_s: "max span", - sp_span_l: ["unsupported span that causes","a new support to be generated","in millimeters"], - sp_angl_s: "max angle", - sp_angl_l: ["max overhang angle before","supporting pillar is generated","normal values 45-60","belt values 0-10"], - sp_area_s: "min area", - sp_area_l: ["minimum area for","a support column","in millimeters","smaller values will","slow detection"], - sp_xpnd_s: "expand", - sp_xpnd_l: ["expand support area","beyond part boundary","in millimeters"], - sp_grow_s: "grow", - sp_grow_l: ["grow support column to fill small gaps","in millimeters"], - sp_nozl_s: "extruder", - sp_nozl_l: ["in multi-extruder systems","the extruder to use for","support material"], - sp_outl_s: "enclosed", - sp_outl_l: ["enclose support pattern","inside a perimeter shell"], - sp_auto_s: "automatic", - sp_auto_l: ["enable generated supports","using at-slice-time geometry","supports will only appear","after slicing completes"], - - // CNC COMMON terms - cc_tool: "tool", - cc_offs_s: "offset", - cc_offs_l: ["offset tool center","from chosen path"], - cc_spnd_s: "spindle rpm", - cc_spnd_l: ["spindle speed in","revolutions / minute"], - cc_sovr_s: "step over", - cc_sovr_l: ["a fraction of tool diameter between 0 and 1. values greater than 0.49 may result in uncut areas or undesirable artifacts."], - cc_sovc_s: "wide steps", - cc_sovc_l: ["number of wide cutout steps"], - cc_sang_s: "step angle", - cc_sang_l: ["rotation in degrees between lathe axial points"], - cc_sdwn_s: "step down", - cc_sdwn_l: ["step down depth","for each pass","in workspace units","0 to disable"], - cc_thru_s: "cut thru", - cc_thru_l: ["additional cut depth below trace bottom"], - cc_offd_s: "offset", - cc_offd_l: ["overrides default offset distance (which is the tool radius) when set to any value but zero"], - cc_feed_s: "feed rate", - cc_feed_l: ["max cutting speed in","workspace units / minute"], - cc_plng_s: "plunge rate", - cc_plng_l: ["max z axis speed in","workspace units / minute"], - cc_sngl_s: "select lines only", - cc_sngl_l: ["select only single edges","instead of connected polylines"], - cc_sele_s: "select", - cc_sele_l: ["select lines for follow","select loops for clear","select surface for pocket"], - - // CNC COMMON - cc_menu: "limits", - cc_rapd_s: "xy feed", - cc_rapd_l: ["max xy moves feedrate","in workspace units / minute"], - cc_rzpd_s: "z feed", - cc_rzpd_l: ["max z moves feedrate","in workspace units / minute"], - - // CNC LEVELING - cc_loff_s: "z offset", - cc_loff_l: ["distance from stock face","in workspace units"], - cc_lxyo_s: "xy offset", - cc_lxyo_l: ["xy offset from part boundary as a fraction of the tool diameter (0 - 1.0)"], - cc_lsto_s: "stock", - cc_lsto_l: ["use stock area instead of part area"], - - // CNC ROUGHING - cr_menu: "rough", - cr_lsto_s: "leave stock", - cr_lsto_l: ["horizontal offset from vertical faces","stock to leave for finishing pass","in workspace units"], - cr_lstz_s: "leave stock z", - cr_lstz_l: ["vertical offset for roughing passes in workspace units. best for roughing curved surfaces."], - cr_ease_s: "ease down", - cr_ease_l: ["plunge cuts will","spiral down or ease","along a linear path"], - cr_clst_s: "clear stock", - cr_clst_l: ["in indexed mode, clear entire stock area, not just to part periemter"], - cr_clrt_s: "clear top", - cr_clrt_l: ["run a clearing pass over","the bounding area of the part","at z = 0"], - cr_clrp_s: "clear voids", - cr_clrp_l: ["mill out through pockets","instead of just the outline"], - cr_clrf_s: "clear faces", - cr_clrf_l: ["interpolate step down to","clear any detected flat areas"], - cr_olin_s: "inside only", - cr_olin_l: ["limit cutting to","inside part boundaries"], - - // CNC OUTLINE - co_menu: "outline", - co_merg_s: "merge overlap", - co_merg_l: ["merge overlapping lines to prevent overcutting into adjacent solids"], - co_dogb_s: "dogbones", - co_dogb_l: ["insert dogbone cuts","into inside corners"], - co_dogr_s: "reverse bones", - co_dogr_l: ["reverse dogbone direction"], - co_clrt_s: "clear top", - co_clrt_l: ["cut starting at the top of stock","when stock is enabled"], - co_wide_s: "wide cutout", - co_wide_l: ["widen outside cutout paths","for deep cuts in hard material"], - co_olin_s: "inside only", - co_olin_l: ["limit cutting to","inside part boundaries"], - co_olot_s: "outside only", - co_olot_l: ["limit cutting to","exterior part boundaries","which can be thought of","as the shadow outline"], - co_omit_s: "omit through", - co_omit_l: "eliminate thru holes", - co_omvd_s: "omit pocket", - co_omvd_l: "eliminate interior pockets", - co_olen_s: "enable", - co_olen_l: "enabled outline cutting", - - // CNC CONTOUR - cn_menu: "contour", - cf_angl_s: "max angle", - cf_angl_l: ["changes in z that result in angles greater than this are considered vertical and omitted from curves"], - cf_leav_s: "leave stock", - cf_leav_l: ["amount of stock to leave above part surface"], - cf_botm_s: "z bottom", - cf_botm_l: ["obey z bottom limit"], - cf_curv_s: "curves only", - cf_curv_l: ["limit linear cleanup","to curved surfaces"], - cf_olin_s: "inside only", - cf_olin_l: ["limit cutting to","inside part boundaries"], - cf_linx_s: "enable y pass", - cf_linx_l: "linear y-axis finishing", - cf_liny_s: "enable x pass", - cf_liny_l: "linear x-axis finishing", - - // CNC TRACE - cu_menu: "trace", - cu_type_s: "type", - cu_type_l: ["clear = clear a closed-line pocket","follow = tool tip follows line","with offset option when closed"], - - // CNC POCKET - cp_xpnd_s: "expand", - cp_xpnd_l: ["grow (+) or shrink (-) selected areas","contour uses entire selected area","non-contour defaults to tool radius","in workspace units"], - cp_smoo_s: "smooth", - cp_smoo_l: ["clean up jagged edges caused by angled walls. can speedup and improve pocketing for detailed geometries. recommend starting with values around 1 if slicing takes too long or the contours look jagged"], - cp_foll_s: "selection", - cp_foll_l: ["curvature selection limit in degrees. values closer to 0 will restrict selections to flatter surfaces"], - cp_refi_s: "refine", - cp_refi_l: ["number of refining passes to perform on contoured polylines. meant to address Z delta sawtoothing caused by faced geometries whereas smoothing handles XY. values > 10 work well for gently contoured geometries with significant Z movement."], - cp_cont_s: "contour", - cp_cont_l: ["ignore interior voids and features"], - cp_engr_s: "engrave", - cp_engr_l: ["sets up a single pass around the perimeter of the selected area. also useful for 3D laser marking"], - cp_outl_s: "outline only", - cp_outl_l: ["ignore interior voids and features"], - - // CNC DRILLING - cd_menu: "drill", - cd_axis: "axis", - cd_points: "points", - // cd_plpr_s: "plunge per", - // cd_plpr_l: ["max plunge between","dwell periods","in workspace units","0 to disable"], - cd_dwll_s: "dwell time", - cd_dwll_l: ["dwell time","between plunges in","in milliseconds"], - cd_lift_s: "drill lift", - cd_lift_l: ["lift between plunges","after dwell period","in workspace units","0 to disable"], - cd_regi_s: "register", - cd_regi_l: ["drill registration holes","for double-sided parts","independent of enable","drilling but uses same","tool and settings"], - cd_thru_s: "z extend", - cd_thru_l: ["extend downward cut through stock","in workspace units"], - cd_dtru_s: "drill through", - cd_dtru_l: ["additional drill depth below part bottom","overrides global z thru","0 to disable"], - cd_mark_s: "marking", - cd_mark_l: ["only mark holes, do not drill out","the step down setting is used","to determine how deep to mark"], - cd_ftop_s: "from stock top", - cd_ftop_l: ["drill from stock top","instead of part surface"], - cd_prcn_s: "precision", - cd_prcn_l: ["distance between slices","to search for holes","in workspace units","lower is slower and","uses more memory","0 = only flats"], - - cd_seli_l: ["individually select holes","to drill"], - cd_sela_s: "Select Matching Diameter", - cd_sela_l: ["select all holes that", "match selected tool diameter"], - - // CNC FLIP - cf_menu: "flip", - cf_nvrt_s: "z bottom", - cf_nvrt_l: "invert z bottom setting to follow stock", - - // CNC INDEX AXIS - ci_degr_s: "degrees", - ci_degr_l: "degrees to rotate the A (indexed) axis", - ci_abso_s: "absolute", - ci_abso_l: "degree rotation is absolute", - ci_face_s: "face", - ci_face_l: "select face to rotate facing up", - ci_line_s: "linear", - ci_line_l: "make linear passes along X then rotate Y", - - // CNC LASER On/Off Operations (Carvera) - cl_powr_s: "power", - cl_powr_l: "laser power from 0 to 1.0", - cl_adap_s: "adaptive", - cl_adap_l: "use adaptive power based on Z height", - cl_adrp_s: "repeating", - cl_adrp_l: "apply a moduluse of Z height using the given band to make repeating patterns", - cl_flat_s: "flatten", - cl_flat_l: "flatten output to a given Z height", - cl_flaz_s: "flat height", - cl_flaz_l: ["flatten output to this Z height", "in workspace units"], - cl_minp_s: "z min power", - cl_minp_l: ["power setting at the lowest part of the Z band. may be higher or lower than the other end of the Z band."], - cl_maxp_s: "z max power", - cl_maxp_l: ["power setting at the highest part of the Z band. may be higher or lower than the other end of the Z band."], - cl_minz_s: "min z height", - cl_minz_l: ["lowest Z band position"], - cl_maxz_s: "max z height", - cl_maxz_l: ["highest Z band position. when set to 0, uses the highest Z point of the workspace"], - - // CNC CUTOUT TABS - ct_menu: "tabs", - ct_angl_s: "angle", - ct_angl_l: ["starting angle for tab spacing","in degrees (0-360)"], - ct_numb_s: "count", - ct_numb_l: ["number of tabs to use","will be spaced evenly","around the part"], - ct_wdth_s: "width", - ct_wdth_l: "width in workspace units", - ct_hght_s: "height", - ct_hght_l: "height in workspace units", - ct_dpth_s: "depth", - ct_dpth_l: ["distance in workspace units","that the tab projects from","the part surface"], - ct_midl_s: "midline", - ct_midl_l: ["use midline of tab","instead of z bottom","for double-sided work"], - ct_nabl_s: "auto", - ct_nabl_l: ["auto generate radial tabs","projected from part center","using count and angle offset"], - - // OUTPUT - ou_menu: "output", - - // LASER SLICING - ls_offs_s: "offset / kerf", - ls_offs_l: ["distance between the center of the cutting","path and the edge of remaining material","in millimeters"], - ls_lahi_s: "slice height", - ls_lahi_l: ["layer height","in millimeters","0 = auto/detect"], - ls_lahm_s: "slice minimum", - ls_lahm_l: ["layer height minimum","will merge auto slices","under this thickness","in millimeters"], - ls_sngl_s: "single slice", - ls_sngl_l: ["perform only one slice","at specified layer height"], - - // LASER KNIFE - dk_menu: "drag knife", - dk_dpth_s: "cut depth", - dk_dpth_l: ["final cut depth","in millimeters"], - dk_pass_s: "cut passes", - dk_pass_l: ["number of passes","down to cut depth"], - dk_offs_s: "tip offset", - dk_offs_l: ["distance from blade tip","to center of tool","in millimeters"], - - // WIRE EDM - we_omit_s: "omit inner", - we_omit_l: "omit inner polygons", - - // LASER OUTPUT - ou_spac_s: "spacing", - ou_spac_l: ["distance between layer output","in millimeters"], - ou_scal_s: "scaling", - ou_scal_l: "multiplier (0.1 to 100)", - ou_powr_s: "power", - ou_powr_l: ["0 - 100","represents %"], - ou_sped_s: "speed", - ou_sped_l: "millimeters / second", - ou_mrgd_s: "merged", - ou_mrgd_l: ["merge all layers using","color coding to denote","stacking depth"], - ou_grpd_s: "grouped", - ou_grpd_l: ["retain each layer as","a unified grouping","instead of separated","polygons"], - ou_layr_s: "layer order", - ou_layr_l: ["output layer order","from top right to","bottom left"], - ou_layo_s: "layer color", - ou_layo_l: ["output layer colors","for each z index","overridden by merged"], - ou_lays_s: "layer marking", - ou_lays_l: ["mark layers for stacking. the layer above will be output in a different color which the laser can mark with a lower power. turns on layer grouping."], - ou_drkn_s: "drag knife", - ou_drkn_l: ["enable drag knife","output in gcode","cut radii are added","to corners with","cut down passes"], - ou_stak_s: "3d stack", - ou_stak_l: ["output is a 3D stack of 2D paths rather than packing layers flat in 2D"], - ou_maxp_s: "max power", - ou_maxp_l: ["max power value in gcode. power % from settings will scale from 0 to this value"], - ou_inch_s: "imperial", - ou_inch_l: ["output values in inches"], - ou_shap_s: "shaper tool", - ou_shap_l: ["annotate SVG with Shaper Tool custom attributes"], - - // OUTPUT FDM - ou_nozl_s: "nozzle temp", - ou_nozl_l: "degrees in celsius", - ou_bedd_s: "bed temp", - ou_bedd_l: "degrees in celsius", - ou_feed_s: "print speed", - ou_feed_l: ["max printing speed","set to 0 to use default","millimeters / second"], - ou_fini_s: "finish speed", - ou_fini_l: ["outermost shell speed","bridge/flat solid layers","set to 0 to use default","millimeters / second"], - ou_move_s: "move speed", - ou_move_l: ["non-print move speed","millimeters / second","0 = enable G0 moves"], - ou_shml_s: "shell factor", - ou_flml_s: "solid factor", - ou_spml_s: "infill factor", - ou_exml_l: ["extrusion multiplier","0.0 - 2.0"], - ou_fanl_s: "fan on layer", - ou_fanl_l: ["layer # to turn on fan","layers start at 0"], - ou_fans_s: "fan speed", - ou_fans_l: "0 - 255", - - // OUTPUT CAM - ou_toll_s: "precision", - ou_toll_l: ["surface precision","in workspace units","lower is slower and","uses more memory","0 = automatic based","on animate preference"], - ou_flat_s: "flatness", - ou_flat_l: ["delta z below which two adjacent points are considered co-linear. 0 = compute using tolerance"], - ou_redu_s: "reduction", - ou_redu_l: ["number of axis steps to skip for each unit of precision. higher precision may require reduction to produce reasonable file sizes. start at 0 and test in increments of 1."], - ou_brdg_s: "bridging", - ou_brdg_l: ["flat distance between curved","areas to bridge with a cut"], - ou_zanc_s: "z anchor", - ou_zanc_l: ["controls the position of the part","when stock Z exceeds part Z"], - ou_ztof_s: "z offset", - ou_ztof_l: ["offset z anchor","in workspace units"], - ou_ztop_s: "z top", - ou_ztop_l: ["offset from stock bottom","to set start of cutting depth","in workspace units","* drill/contour ignore this *"], - ou_zbot_s: "z bottom", - ou_zbot_l: ["offset from stock bottom","to limit cutting depth","in workspace units"], - ou_zclr_s: "z clearance", - ou_zclr_l: ["safe travel offset","from top of part","in workspace units"], - ou_ztru_s: "z thru", - ou_ztru_l: ["extend cutout pass down","in workspace units"], - ou_conv_s: "conventional", - ou_conv_l: ["milling direction","uncheck for 'climb'"], - ou_depf_s: "depth first", - ou_depf_l: ["optimize pocket cuts","with depth priority"], - ou_toin_s: "tool init", - ou_toin_l: "add tool change with first operation. when disabled, the gcode assumes the first tool is loaded and offset probed", - ou_forz_s: "force z max", - ou_forz_l: "all moves between pockets will go to z max (above stock). this is usually required if you are not performing a roughing operation before pocketing operations.", - ou_z1st_s: "first z max", - ou_z1st_l: "first move in the job will be to Z max (travel distance above stock)", - ou_feng_s: "engage factor", - ou_feng_l: "speed scale (0.1-1) when endmill is fully engaged on a new cut. stepped over cuts when the endmill is less than fully engaged run at a factor of 1 relative to output speed. typically this relates to roughing or any operation with step over.", - ou_eang_s: "ease angle", - ou_eang_l: "descent angle for ease down in degrees", - - // CAM STOCK - cs_menu: "stock", - cs_wdth_s: "width", - cs_wdth_l: ["width (x) in workspace units","0 defaults to part size"], - cs_dpth_s: "depth", - cs_dpth_l: ["depth (y) in workspace units","0 defaults to part size"], - cs_hght_s: "height", - cs_hght_l: ["height (z) in workspace units","0 defaults to part size"], - cs_offs_s: "offset", - cs_offs_l: ["use width, depth, height","as offsets from max","part size on platform"], - cs_clip_s: "clip to", - cs_clip_l: ["rough and outline","clip cutting paths","to defined stock"], - cs_ishg_s: "show grid", - cs_ishg_l: ["show platform grid in indexed mode"], - cs_indx_s: "indexed", - cs_indx_l: ["stock is mounted to a rotatary indexer"], - cs_offe_s: "enable", - cs_offe_l: "enable milling stock", - - // ORIGIN (CAM & LASER) - or_bnds_s: "origin bounds", - or_bnds_l: ["origin is relative to","boundary of all objects"], - or_cntr_s: "origin center", - or_cntr_l: "origin is referenced from the center", - or_topp_s: "origin top", - or_topp_l: "origin is references from the top of objects", - or_offx_s: "origin offset X", - or_offx_l: "origin offset X", - or_offy_s: "origin offset Y", - or_offy_l: "origin offset Y", - - // CAM ORIGIN - co_menu: "origin", - co_offx_s: "offset x", - co_offx_l: ["offset origin along x axis","in workspace units"], - co_offy_s: "offset y", - co_offt_l: ["offset origin along y axis","in workspace units"], - co_offz_s: "offset z", - co_offz_l: ["offset origin along z axis","in workspace units"], - - // FDM ADVANCED - ad_menu: "expert", - ad_rdst_s: "retract dist", - ad_rdst_l: ["amount to retract filament","for long moves. in millimeters"], - ad_rrat_s: "retract rate", - ad_rrat_l: ["speed of filament","retraction in mm/s"], - ad_rdwl_s: "engage dwell", - ad_wpln_s: "retract wipe", - ad_wpln_l: ["non-printing move","after retraction","in millimeters"], - ad_rdwl_l: ["time between re-engaging","filament and movement","in milliseconds"], - ad_scst_s: "shell coast", - ad_scst_l: ["non-printing end","of perimeter shells","in millimeters"], - ad_msol_s: "min solid", - ad_msol_l: ["minimum area (mm^2)","required to keep solid","must be > 0.1"], - ad_mins_s: "min speed", - ad_mins_l: ["minimum speed","for short segments"], - ad_spol_s: "short path", - ad_spol_l: ["polygons shorter than this","will have their print speed","scaled down to min speed","in millimeters"], - ad_arct_s: "arc tolerance", - ad_arct_l: ["convert faceted lines to arcs","center point drift tolerance","when matching arc points","consider values around 0.15","in millimeters","0 to disable"], - ad_zhop_s: "z hop dist", - ad_zhop_l: ["amount to raise z","on retraction moves","in millimeters","0 to disable"], - ad_abkl_s: "anti-backlash", - ad_abkl_l: ["for better flat surface finish","use micro-movements to cancel","backlash in solid layer output","in millimeters","0 to disable","if your firmware has M425","put that in the gcode header","and leave this as 0"], - ad_lbir_s: "belt first", - ad_lbir_l: ["always print shells touching the belt before any other shells. this is no longer a recommended setting based on extensive testing."], - ad_altr_s: "alternating", - ad_altr_l: ["alternate shell winding order","clockwise / counter-clockwise","may reduce warping in thin areas","and help with belt edge adhesion"], - ad_zint_s: "interleave z", - ad_zint_l: ["interleave Z heights with odd number shells"], - ad_lret_s: "layer retract", - ad_lret_l: ["force filament retraction","between layers"], - ad_agap_s: "avoid gaps", - ad_agap_l: ["route around gaps rather","than perform a retract","when possible"], - ad_play_s: "polish layers", - ad_play_l: ["polish up to specified","# of layers at a time"], - ad_pspd_s: "polish speed", - ad_pspd_l: ["polishing speed","in millimeters / second"], - ad_purg_s: "purge tower", - ad_purg_l: ["purge tower area in mm^2","for multi extruder setups","0 = disabled"], - - // CAM EXPERT - cx_fast_s: "skip shadow", - cx_fast_l: ["disable overhang detection","can be faster and use less","memory with complex models","but fails with overhangs","try enabling if slicing","hangs during shadowing"], - cx_true_s: "true shadow", - cx_true_l: ["computationally correct shadow","will be slower but produce","better cuts for complex parts"], - cx_arct_s: "arc tolerance", - cx_arct_l: ["convert cicrular paths to arcs;","center point drift tolerance","when matching arc points","consider values around 0.15","in mm, or 0.006 inches;","High values may cause","unexpected behavior","0 to disable"], - cx_arcr_s: "arc resolution", - cx_arcr_l: ["change in line direction","aceptible in an arc","consider values around 5","High values may cause","unexpected behavior",], - - // FDM GCODE - ag_menu: "gcode", - ag_nozl_s: "nozzle", - ag_nozl_l: "select output nozzle or head", - ag_peel_s: "peel guard", - ag_peel_l: ["starting at this belt z position","periodically roll the print off and","back on to the belt to unstick it","and prevent rolling deflection"], - // ag_paws_s: "pause layers", - // ag_paws_l: ["comma-separated list of layers","to inject pause commands before"], - // ag_loop_s: "loop layers", - // ag_loop_l: ["layer ranges to repeat in the format","first-last-count,first-last-count,...","omitted count = 1"], - ag_loop_s: "loop count", - ag_loop_l: ["number of times to repeat","the selected range or the","entire print if no range"], - - // SLA MENU - sa_menu: "slicing", - sa_lahe_s: "layer height", - sa_lahe_l: ["layer height","in millimeters"], - sa_shel_s: "hollow shell", - sa_shel_l: ["shell thickness in mm","use multiple of layer height","use 0 for solid (disabled)"], - sa_otop_s: "open top", - sa_otop_l: ["if shell is enabled","results in an open top"], - sa_obas_s: "open base", - sa_obas_l: ["if shell is enabled","results in an open base","disabled with supports"], - - sa_layr_m: "layers", - sa_lton_s: "light on time", - sa_lton_l: ["layer light on","time in seconds"], - sa_ltof_s: "light off time", - sa_ltof_l: ["layer light off","time in seconds"], - sa_pldi_s: "peel distance", - sa_pldi_l: ["peel distance","in millimeters"], - sa_pllr_s: "peel lift rate", - sa_pllr_l: ["peel lift speed","in mm/sec"], - sa_pldr_s: "peel drop rate", - sa_pldr_l: ["peel drop speed","in mm/sec"], - - sa_base_m: "base", - sa_balc_s: "layer count", - sa_balc_l: ["number of","base layers"], - sa_bltn_l: ["base layer light on","time in seconds"], - sa_bltf_l: ["base layer light off","time in seconds"], - - sa_infl_m: "infill", - sa_ifdn_s: "density", - sa_ifdn_l: ["percent infill","requires shell","0 = disabled","valid 0.0 - 1.0"], - sa_iflw_s: "line width", - sa_iflw_l: ["hatch line width","in millimeters"], - - sa_supp_m: "support", - sa_slyr_s: "base layers", - sa_slyr_l: ["base support layers","value range 0-10"], - sa_slgp_s: "gap layers", - sa_slgp_l: ["number of layers between","raft and bottom of object"], - sa_sldn_s: "density", - sa_sldn_l: ["used to compute the","number of support pillars","0.0-1.0 (0 = disable)"], - sa_slsz_s: "size", - sa_slsz_l: ["max size of a","support pillar","in millimeters"], - sa_slpt_s: "points", - sa_slpt_l: ["number of points in","each support pillar","in millimeters"], - sl_slen_l: "enable supports", - - sa_outp_m: "output", - sa_opzo_s: "z offset", - sa_opzo_l: ["z layer offset","almost always 0.0","0.0-1.0 in millimeters"], - sa_opaa_s: "anti alias", - sa_opaa_l: ["enable anti-aliasing","produces larger files","can blur details"] -}; diff --git a/src2/kiri/main.js b/src2/kiri/main.js deleted file mode 100644 index 13987d4e..00000000 --- a/src2/kiri/main.js +++ /dev/null @@ -1,956 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import lang from './lang.js'; -import STACKS from './stacks.js'; - -import { $ } from '../moto/webui.js'; -import { api } from '../kiri/api.js'; -import { broker } from '../moto/broker.js'; -import { consts } from '../kiri/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 '../kiri/stats.js'; -import { noop, utils } from '../kiri/utils.js'; -import { version } from '../moto/license.js'; -import { Widget } from './widget.js'; -import { showTools } from '../kiri-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 (busy === 0) { - LOC.reload(); - } else { - console.log(`reload deferred on busy=${busy}`); - 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(``); - }); - 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
Consider resizing under 1000 x 1000
Proceed with import?").then(ok => { - if (ok) { - loadImageDialog(image, name, true); - } - }); - } - const opt = {pre: [ - "
", - "

Image Conversion

", - "

", - " 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.", - "

", - "
", - " ", - " ", - " ", - " ", - " ", - " ", - "
blur value invert image
base size invert alpha
border size
", - "
" - ]}; - 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 = ``; - - 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 = ''; - del.onclick = deleteSettings; - - edit.setAttribute('name', sk); - edit.setAttribute('title', 'edit'); - edit.innerHTML = ''; - edit.onclick = editSettings; - - xprt.setAttribute('name', sk); - xprt.setAttribute('title', 'export'); - xprt.innerHTML = ''; - 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; - } - $('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 = ['']; - html.push(``); - html.push(``); - 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(``); - html.push(``); - html.push(``); - html.push(``); - html.push(``); - html.push(``); - devc++; - } - html.push(``); - html.push(`
devicetypestatus
${r.device.name}${r.device.mode}${r.state}
`); - if (devc) { - $('mod-local').innerHTML = html.join(''); - } else { - $('mod-local').innerHTML = `
no local devices`; - } - 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 = `x`; - $('_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 }; diff --git a/src2/kiri/ui.js b/src2/kiri/ui.js deleted file mode 100644 index 1166ec98..00000000 --- a/src2/kiri/ui.js +++ /dev/null @@ -1,846 +0,0 @@ -/** Copyright Stewart Allen -- 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, - 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 = [ - `
` - ]; - if (message) { - html.push(``); - } - if (opt.pre) { - html = opt.pre.appendAll(html); - } - let iid; - if (Array.isArray(input)) { - iid = `confirm-input-${rnd}`; - html.append(`
`); - } else if (input !== undefined) { - iid = `confirm-input-${rnd}`; - html.append(`
`); - } - html.append(`
`); - Object.entries(btns).forEach((row,i) => { - html.append(``); - }); - html.append(`
`); - 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 = ''; - row.classList.add('hidden'); - } else { - arr.innerHTML = ''; - 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 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 = ``; - - 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; -} diff --git a/src2/load/stl.js b/src2/load/stl.js deleted file mode 100644 index 155b8c90..00000000 --- a/src2/load/stl.js +++ /dev/null @@ -1,250 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -'use strict'; - -// Polyfill for appendAll if not present -if (!Array.prototype.appendAll) { - Object.defineProperty(Array.prototype, 'appendAll', { - value: function(arr) { - for (let v of arr) this.push(v); - }, - enumerable: false - }); -} - -export class STL { - constructor() { - this.vertices = null; - this.normals = null; - this.colors = null; - } - load(url, callback, formdata, scale, credentials, headers) { - const stl = this; - - fetch(url, { - method: formdata ? 'POST' : 'GET', - credentials: credentials ? credentials : 'same-origin', - body: formdata, - ...(headers ? {headers: headers} : {}) - }).then(response => { - if (response.status === 200 || response.status === 0) { - response.arrayBuffer().then(buffer => { - stl.parse(buffer, scale); - - let cdhVal = response.headers.get(CDH); - if (typeof cdhVal === "string") { - cdhVal = cdhVal.split(';').map(v => v.trim()).filter(v => { - return v.indexOf('filename=') === 0; - }).map(v => { - return v.substring(10,v.length-1); - })[0]; - } - if (callback) callback(stl.vertices, cdhVal); - }); - } else { - if (callback) callback(null, response.statusText); - } - }).catch(err => { - if (callback) callback(null, err) - }); - } - encode(vertices, normals) { - if (!(vertices && vertices.length % 3 === 0)) throw "invalid vertices"; - - let vc = vertices.length / 3, - bs = (vc * 16) + (vc * (2/3)) + 84, - bin = new ArrayBuffer(bs), - writer = new DataView(bin), - i = 0, - j = 0, - pos = 80; - - function writeInt16(val) { - writer.setUint16(pos, val, true); - pos += 2; - } - - function writeInt32(val) { - writer.setUint32(pos, val, true); - pos += 4; - } - - function writeFloat(val) { - writer.setFloat32(pos, val, true); - pos += 4; - } - - function writeVertex() { - writeFloat(vertices[i++]); // x - writeFloat(vertices[i++]); // y - writeFloat(vertices[i++]); // z - } - - writeInt32(vc / 3); - while (i < vertices.length) { - writeFloat(normals ? normals[j++] : 0); // norm x - writeFloat(normals ? normals[j++] : 0); // norm y - writeFloat(normals ? normals[j++] : 0); // norm z - writeVertex(); // p1 - writeVertex(); // p2 - writeVertex(); // p3 - writeInt16(0); // attributes - } - - return bin; - } - parse(data, scale) { - let binData = this.convertToBinary(data); - - let isBinary = function () { - let expect, face_size, n_faces, reader; - reader = new DataView(binData); - face_size = (32 / 8 * 3) + ((32 / 8 * 3) * 3) + (16 / 8); - n_faces = reader.getUint32(80,true); - expect = 80 + (32 / 8) + (n_faces * face_size); - return expect === reader.byteLength; - }; - - return isBinary() - ? this.parseBinary(binData, scale) - : this.parseASCII(this.convertToString(data), scale); - } - parseAsync(data, scale) { - return new Promise((resolve, reject) => { - resolve(this.parse(data, scale)); - }); - } - parseBinary(data, scale = 1) { - let reader = new DataView(data), - faces = reader.getUint32 (80, true), - r, g, b, hasColors = false, colors, - defaultR, defaultG, defaultB, alpha; - - // check for default color in STL header ("COLOR=rgba" sequence). - for (let index = 0; index < 80 - 10; index++) { - if ((reader.getUint32(index, false) == 0x434F4C4F /*COLO*/) && - (reader.getUint8(index + 4) == 0x52 /*'R'*/) && - (reader.getUint8(index + 5) == 0x3D /*'='*/)) { - hasColors = true; - colors = new Float32Array(faces * 3 * 3); - defaultR = reader.getUint8(index + 6) / 255; - defaultG = reader.getUint8(index + 7) / 255; - defaultB = reader.getUint8(index + 8) / 255; - alpha = reader.getUint8(index + 9) / 255; - } - } - - let offset = 0, - dataOffset = 84, - faceLength = 12 * 4 + 2, - vertices = new Float32Array(faces * 3 * 3), - normals = new Float32Array(faces * 3 * 3); - - colors = hasColors ? new Uint16Array(faces * 3 * 3) : null; - - for (let face = 0; face < faces; face ++) { - - let start = dataOffset + face * faceLength, - normalX = reader.getFloat32(start, true), - normalY = reader.getFloat32(start + 4, true), - normalZ = reader.getFloat32(start + 8, true); - - if (hasColors) { - let packedColor = reader.getUint16(start + 48, true); - if ((packedColor & 0x8000) === 0) { // facet has its own unique color - r = (packedColor & 0x1F) / 31; - g = ((packedColor >> 5) & 0x1F) / 31; - b = ((packedColor >> 10) & 0x1F) / 31; - } else { - r = defaultR; - g = defaultG; - b = defaultB; - } - } - - let i = 1, vertexstart; - - while (i <= 3) { - vertexstart = start + (i++) * 12; - vertices[offset ] = reader.getFloat32 (vertexstart, true) * scale; - vertices[offset + 1] = reader.getFloat32 (vertexstart + 4, true) * scale; - vertices[offset + 2] = reader.getFloat32 (vertexstart + 8, true) * scale; - normals[offset ] = normalX; - normals[offset + 1] = normalY; - normals[offset + 2] = normalZ; - if (hasColors) { - colors[offset ] = r; - colors[offset + 1] = g; - colors[offset + 2] = b; - } - offset += 3; - } - } - - this.vertices = vertices; - this.normals = normals; - this.colors = colors; - - return vertices; - } - parseASCII(data, scale = 1) { - let result, - resultText, - patternNormal, - patternVertex, - vertices = [], - normals = [], - patternFace = /facet([\s\S]*?)endfacet/g; - - while ((result = patternFace.exec(data)) !== null) { - resultText = result[0]; - patternNormal = /normal[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g; - patternVertex = /vertex[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g; - while ((result = patternNormal.exec(resultText)) !== null) { - normals.push(parseFloat(result[1])); - normals.push(parseFloat(result[3])); - normals.push(parseFloat(result[5])); - } - while ((result = patternVertex.exec(resultText)) !== null) { - vertices.push(parseFloat(result[1]) * scale); - vertices.push(parseFloat(result[3]) * scale); - vertices.push(parseFloat(result[5]) * scale); - } - } - - let vToFloat32 = new Float32Array(vertices.length), - nToFloat32 = new Float32Array(normals.length), - i; - - for (i=0; i -- All Rights Reserved */ - -import '../add/array.js'; -import '../add/class.js'; -import '../add/three.js'; -import '../kiri/lang.js'; -import '../kiri/lang-en.js'; - -import { run } from '../kiri/init.js'; - -const load = []; - -function checkReady() { - if (document.readyState === 'complete') { - let api = run(); - kiri.api = api; - for (let fn of load) { - try { - fn(api); - } catch (error) { - console.log('load error', fn, error); - } - } - } -} - -self.kiri = { - load(fn) { - console.log('KIRI LOAD', [...arguments]); - load.push(fn); - } -}; - -self.moto = { }; - -// when dom + scripts complete -document.onreadystatechange = checkReady; - -checkReady(); diff --git a/src2/mesh/pool.js b/src2/mesh/pool.js deleted file mode 100644 index d4c79733..00000000 --- a/src2/mesh/pool.js +++ /dev/null @@ -1,7 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import { license as motoLicense } from '../moto/license.js'; -import { worker as motoWorker } from '../moto/worker.js'; - -// todo -export const pool = {}; diff --git a/src2/moto/license.js b/src2/moto/license.js deleted file mode 100644 index c15f957f..00000000 --- a/src2/moto/license.js +++ /dev/null @@ -1,11 +0,0 @@ -const is_self = typeof(self) !== 'undefined'; - -const terms = { - COPYRIGHT: "Copyright (C) Stewart Allen - All Rights Reserved", - LICENSE: "See the license.md file included with the source distribution", - VERSION: (is_self ? self : this).debug_version || "4.3.0" -}; - -export const beta = 1; -export const license = terms; -export const version = terms.VERSION; diff --git a/src2/moto/orbit.js b/src2/moto/orbit.js deleted file mode 100644 index 893fcc34..00000000 --- a/src2/moto/orbit.js +++ /dev/null @@ -1,642 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -"use strict"; - -import { THREE } from '../ext/three.js'; - -const { - Vector3, - Vector2, - Quaternion, - EventDispatcher, - MOUSE -} = THREE; - -/** - * Adapted from THREE.OrbitControls - */ -class Orbit extends EventDispatcher { - constructor(object, domElement, notify, slider) { - super(); - - this.object = object; - this.domElement = ( domElement !== undefined ) ? domElement : document; - - // Set to false to disable this control - this.enabled = true; - - // "target" sets the location of focus, where the control orbits around - // and where it pans with respect to. - this.target = new Vector3(); - - // center is old, deprecated; use "target" instead - this.center = this.target; - - // This option actually enables dollying in and out; left as "zoom" for - // backwards compatibility - this.noZoom = false; - this.zoomSpeed = 1.0; - this.reverseZoom = false; - - // Limits to how far you can dolly in and out - this.minDistance = 0; - this.maxDistance = Infinity; - - // Set to true to disable this control - this.noRotate = false; - this.rotateSpeed = 1.0; - - // Set to true to disable this control - this.noPan = false; - this.keyPanSpeed = 7.0; // pixels moved per arrow key push - - // How far you can orbit vertically, upper and lower limits. - // Range is 0 to Math.PI radians. - this.minPolarAngle = 0; // radians - this.maxPolarAngle = Math.PI; // radians - - // How far you can orbit horizontally, upper and lower limits. - // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. - this.minAzimuthAngle = -Infinity; // radians - this.maxAzimuthAngle = Infinity; // radians - - // Set to true to disable use of the keys - this.noKeys = false; - - // The four arrow keys - this.keys = { - UP: 38, - LEFT: 37, - RIGHT: 39, - BOTTOM: 40 - }; - - // default mouse buttons - this.mouseDefault = { - ORBIT: MOUSE.LEFT, - ZOOM: MOUSE.MIDDLE, - PAN: MOUSE.RIGHT - }; - - // Onshape - this.mouseOnshape = { - ORBIT: MOUSE.RIGHT, - ZOOM: MOUSE.LEFT, - PAN: MOUSE.MIDDLE - }; - - this.mouseButtons = this.mouseDefault; - - this.setMouse = function(bindings) { - this.mouseButtons = bindings; - }; - - let scope = this, - domEl = scope.domElement, - EPS = 0.000001, - rotateStart = new Vector2(), - rotateEnd = new Vector2(), - rotateDelta = new Vector2(), - panStart = new Vector2(), - panEnd = new Vector2(), - panDelta = new Vector2(), - panOffset = new Vector3(), - offset = new Vector3(), - dollyStart = new Vector2(), - dollyEnd = new Vector2(), - dollyDelta = new Vector2(), - theta, - thetaDelta = 0, - thetaSet = null, - phi, - phiDelta = 0, - phiSet = null, - scale = 1, - scaleSave = 1, - pan = new Vector3(), - lastPosition = new Vector3(), - lastQuaternion = new Quaternion(), - // so camera.up is the orbit axis - quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0)), - quatInverse = quat.clone().invert(), - // touch tracking - firstTouch = [], - touchSlide = false, - lastDY = 0, - // events - changeEvent = { type: 'change'}, - startEvent = { type: 'start'}, - endEvent = { type: 'end'}; - - let STATE = { NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5}, - state = STATE.NONE, - MODE = { PERSPECTIVE: 1, ORTHOGRAPHIC: 2, UNKNOWN: 3}, - mode = isValue(object.fov) ? MODE.PERSPECTIVE : isValue(object.top) ? MODE.ORTHOGRAPHIC : MODE.UNKNOWN; - - // for reset - this.target0 = this.target.clone(); - this.position0 = this.object.position.clone(); - - this.rotateLeft = function (angle) { - thetaDelta -= angle; - }; - - this.rotateUp = function (angle) { - phiDelta -= angle; - }; - - // pass in distance in world space to move left - this.panLeft = function (distance) { - let te = this.object.matrix.elements; - - // get X column of matrix - panOffset.set(te[ 0 ], te[ 1 ], te[ 2 ]); - panOffset.multiplyScalar(-distance); - pan.add(panOffset); - }; - - // pass in distance in world space to move up - this.panUp = function (distance) { - let te = this.object.matrix.elements; - - // get Y column of matrix - panOffset.set(te[ 4 ], te[ 5 ], te[ 6 ]); - panOffset.multiplyScalar(distance); - pan.add(panOffset); - }; - - // pass in distance in world space to move backward - this.panBack = function (distance) { - let te = this.object.matrix.elements; - - // get Y column of matrix - panOffset.set(te[ 8 ], te[ 9 ], te[ 10 ]); - panOffset.multiplyScalar(distance); - pan.add(panOffset); - }; - - // pass in x,y of change desired in pixel space, - // right and down are positive - this.pan = function (deltaX, deltaY) { - let element = scope.domElement === document ? scope.domElement.body : scope.domElement; - - switch (mode) { - case MODE.PERSPECTIVE: - let position = scope.object.position; - let offset = position.clone().sub(scope.target); - let targetDistance = offset.length(); - - // half of the fov is center to top of screen - targetDistance *= Math.tan(( scope.object.fov / 2 ) * Math.PI / 180.0); - - // we actually don't use screenWidth, since perspective camera is fixed to screen height - scope.panLeft(2 * deltaX * targetDistance / element.clientHeight); - scope.panUp(2 * deltaY * targetDistance / element.clientHeight); - break; - case MODE.ORTHOGRAPHIC: - scope.panLeft(deltaX * (scope.object.right - scope.object.left) / element.clientWidth); - scope.panUp(deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight); - break; - } - }; - - this.getTarget = function() { - return this.target; - }; - - this.setTarget = function(t) { - this.target.copy(t); - }; - - function isValue(v) { - return v !== undefined && v !== null; - } - - function firstValue(choices) { - for (let v of choices) { - if (isValue(v)) return v; - } - return null; - } - - this.setZoom = function(reverse, speed) { - scope.reverseZoom = reverse; - scope.zoomSpeed = speed || 1.0; - }; - - this.setPosition = function(set) { - thetaSet = firstValue([set.left, set.theta, thetaSet]); - phiSet = firstValue([set.up, set.phi, phiSet]); - if (set.panX) this.target.x = set.panX; - if (set.panY) this.target.y = set.panY; - if (set.panZ) this.target.z = set.panZ; - if (set.scale) scale = set.scale; - }; - - this.getPosition = function(scaled) { - let t = this.target, - pos = { left:theta, up:phi, panX:t.x, panY:t.y, panZ:t.z, scale:scaled ? scaleSave : 1 }; - return pos; - }; - - this.dollyIn = function (factor = 1) { - let dollyScale = getZoomScale(factor); - scale = scale * dollyScale; - }; - - this.dollyOut = function (factor = 1) { - let dollyScale = getZoomScale(factor); - scale = scale / dollyScale; - }; - - this.update = function () { - let position = this.object.position; - - offset.copy(position).sub(this.target); - - // rotate offset to "y-axis-is-up" space - offset.applyQuaternion(quat); - - // angle from z-axis around y-axis - theta = isValue(thetaSet) ? thetaSet : Math.atan2(offset.x, offset.z); - - // angle from y-axis - phi = isValue(phiSet) ? phiSet : Math.atan2(Math.sqrt(offset.x * offset.x + offset.z * offset.z), offset.y); - - theta += thetaDelta; - phi += phiDelta; - - // restrict theta to be between desired limits - theta = Math.max(this.minAzimuthAngle, Math.min(this.maxAzimuthAngle, theta)); - - // restrict phi to be between desired limits - phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, phi)); - - // restrict phi to be betwee EPS and PI-EPS - phi = Math.max(EPS, Math.min(Math.PI - EPS, phi)); - - let radius = offset.length() * (mode === MODE.PERSPECTIVE ? scale : 1); - - // restrict radius to be between desired limits - radius = Math.max(this.minDistance, Math.min(this.maxDistance, radius)); - - // move target to panned location - this.target.add(pan); - - offset.x = radius * Math.sin(phi) * Math.sin(theta); - offset.y = radius * Math.cos(phi); - offset.z = radius * Math.sin(phi) * Math.cos(theta); - - // rotate offset back to "camera-up-vector-is-up" space - offset.applyQuaternion(quatInverse); - - position.copy(this.target).add(offset); - this.object.lookAt(this.target); - - thetaDelta = 0; - thetaSet = null; - phiDelta = 0; - phiSet = null; - pan.set(0, 0, 0); - - scaleSave *= scale; - - if (mode === MODE.ORTHOGRAPHIC) { - scope.object.zoom = 1/scale; - scope.object.updateProjectionMatrix(); - } else { - scale = 1; - } - - // update condition is: - // min(camera displacement, camera rotation in radians)^2 > EPS - // using small-angle approximation cos(x/2) = 1 - x^2 / 8 - if (lastPosition.distanceToSquared(this.object.position) > EPS - || 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS) { - - this.dispatchEvent(changeEvent); - lastPosition.copy(this.object.position); - lastQuaternion.copy(this.object.quaternion); - if (notify) notify(position, true); - } else { - if (notify) notify(position, false); - } - }; - - this.reset = function () { - state = STATE.NONE; - scale = 1; - scaleSave = 1; - this.target.copy(this.target0); - this.object.position.copy(this.position0); - }; - - this.getPolarAngle = function () { - return phi; - }; - - this.getAzimuthalAngle = function () { - return theta - }; - - function getZoomScale(factor = 1) { - return Math.pow(0.95, factor); - } - - function onMouseDown(event) { - if (scope.enabled === false) return; - event.preventDefault(); - - switch (event.button) { - case scope.mouseButtons.ORBIT: - state = event.metaKey ? STATE.PAN : STATE.ROTATE; - break; - case scope.mouseButtons.ZOOM: - state = STATE.DOLLY; - break; - case scope.mouseButtons.PAN: - state = STATE.PAN; - break; - } - - switch (state) { - case STATE.ROTATE: - if (scope.noRotate === true) return state = STATE.NONE; - rotateStart.set(event.clientX, event.clientY); - break; - case STATE.DOLLY: - if (scope.noZoom === true) return state = STATE.NONE; - dollyStart.set(event.clientX, event.clientY); - break; - case STATE.PAN: - if (scope.noPan === true) return state = STATE.NONE; - panStart.set(event.clientX, event.clientY); - break; - } - - document.addEventListener('mousemove', onMouseMove, false); - document.addEventListener('mouseup', onMouseUp, false); - scope.dispatchEvent(startEvent); - } - - function onMouseMove(event) { - if (scope.enabled === false) return; - event.preventDefault(); - - let element = scope.domElement === document ? scope.domElement.body : scope.domElement; - - if (state === STATE.ROTATE) { - if (scope.noRotate === true) return; - - rotateEnd.set(event.clientX, event.clientY); - rotateDelta.subVectors(rotateEnd, rotateStart); - - // rotating across whole screen goes 360 degrees around - scope.rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed); - // rotating up and down along whole screen attempts to go 360, but limited to 180 - scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed); - - rotateStart.copy(rotateEnd); - - } else if (state === STATE.DOLLY) { - if (scope.noZoom === true) return; - - dollyEnd.set(event.clientX, event.clientY); - dollyDelta.subVectors(dollyEnd, dollyStart); - - if (dollyDelta.y > 0) { - scope.dollyIn(); - } else { - scope.dollyOut(); - } - - dollyStart.copy(dollyEnd); - - } else if (state === STATE.PAN) { - if (scope.noPan === true) return; - - panEnd.set(event.clientX, event.clientY); - panDelta.subVectors(panEnd, panStart); - scope.pan(panDelta.x, panDelta.y); - panStart.copy(panEnd); - } - - scope.update(); - } - - function onMouseUp(event) { - if (scope.enabled === false) return; - - document.removeEventListener('mousemove', onMouseMove, false); - document.removeEventListener('mouseup', onMouseUp, false); - scope.dispatchEvent(endEvent); - state = STATE.NONE; - } - - function onMouseWheel(event) { - if (scope.enabled === false || scope.noZoom === true) return; - - event.preventDefault(); - event.stopPropagation(); - - if (event.shiftKey && slider) { - slider(event.deltaX || event.wheelDelta || event.detail); - return; - } - - slider(null); - - let delta = -(event.deltaY || event.wheelDelta || event.detail || 0); - - if (delta === 0) return; - - if (scope.reverseZoom) { - delta = -delta; - } - - if (delta > 0) { - scope.dollyOut(); - } else if (delta < 0) { - scope.dollyIn(); - } - - scope.update(); - scope.dispatchEvent(startEvent); - scope.dispatchEvent(endEvent); - } - - function onKeyDown(event) { - if (scope.enabled === false || scope.noKeys === true || scope.noPan === true) return; - - switch (event.keyCode) { - - case scope.keys.UP: - scope.pan(0, scope.keyPanSpeed); - scope.update(); - break; - - case scope.keys.BOTTOM: - scope.pan(0, -scope.keyPanSpeed); - scope.update(); - break; - - case scope.keys.LEFT: - scope.pan(scope.keyPanSpeed, 0); - scope.update(); - break; - - case scope.keys.RIGHT: - scope.pan(-scope.keyPanSpeed, 0); - scope.update(); - break; - } - } - - function touchstart(event) { - if (scope.enabled === false) return; - - const touches = [...event.touches]; - - switch (event.touches.length) { - case 1: // one-fingered touch: rotate - if (scope.noRotate === true) return; - - state = STATE.TOUCH_ROTATE; - - rotateStart.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); - break; - - case 2: // two-fingered touch: dolly - if (scope.noZoom === true) return; - - state = STATE.TOUCH_DOLLY; - - let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; - let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; - let distance = Math.sqrt(dx * dx + dy * dy); - - firstTouch = [...event.touches].map(v => { return {x:v.pageX, y:v.pageY} }); - touchSlide = false; - lastDY = 0; - - dollyStart.set(0, distance); - break; - - case 3: // three-fingered touch: pan - case 4: // four-fingered touch: pan - if (scope.noPan === true) return; - - state = STATE.TOUCH_PAN; - const px = touches.map(t => t.pageX).reduce((a,b) => a+b) / touches.length; - const py = touches.map(t => t.pageY).reduce((a,b) => a+b) / touches.length; - - panStart.set(px, py); - break; - - default: - state = STATE.NONE; - } - scope.dispatchEvent(startEvent); - } - - function touchmove(event) { - if (scope.enabled === false) return; - - event.preventDefault(); - event.stopPropagation(); - - const element = scope.domElement === document ? scope.domElement.body : scope.domElement; - - switch (event.touches.length) { - case 1: // one-fingered touch: rotate - if (scope.noRotate === true) return; - if (state !== STATE.TOUCH_ROTATE) return; - - rotateEnd.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY); - rotateDelta.subVectors(rotateEnd, rotateStart); - - // rotating across whole screen goes 360 degrees around - scope.rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed); - // rotating up and down along whole screen attempts to go 360, but limited to 180 - scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed); - - rotateStart.copy(rotateEnd); - scope.update(); - break; - - case 2: // two-fingered touch: dolly - if (scope.noZoom === true) return; - if (state !== STATE.TOUCH_DOLLY) return; - - let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; - let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; - let distance = Math.sqrt(dx * dx + dy * dy); - - let touches = [...event.touches].map(v => { return {x:v.pageX, y:v.pageY} }); - let ddx = ( (touches[0].x - firstTouch[0].x) + (touches[1].x - firstTouch[1].x) ) / 2; - let ddy = ( (touches[0].y - firstTouch[0].y) + (touches[1].y - firstTouch[1].y) ) / 2; - - if (scope.reverseZoom) distance = -distance; - - dollyEnd.set(0, distance); - dollyDelta.subVectors(dollyEnd, dollyStart); - - if (touchSlide || (Math.abs(dy/dx) < 1 && slider && dx < 200)) { - touchSlide = true; - slider(lastDY ? (lastDY - ddy) : ddy); - lastDY = ddy; - } else { - if (dollyDelta.y > 0) { - scope.dollyOut(0.5); - } else if (dollyDelta.y < 0) { - scope.dollyIn(0.5); - } - } - - dollyStart.copy(dollyEnd); - scope.update(); - break; - - case 3: // three-fingered touch: pan - case 4: // four-fingered touch: pan - if (scope.noPan === true) return; - if (state !== STATE.TOUCH_PAN) return; - - const tlist = [...event.touches]; - const px = tlist.map(t => t.pageX).reduce((a,b) => a+b) / tlist.length; - const py = tlist.map(t => t.pageY).reduce((a,b) => a+b) / tlist.length; - - panEnd.set(px, py); - panDelta.subVectors(panEnd, panStart); - scope.pan(panDelta.x, panDelta.y); - panStart.copy(panEnd); - - scope.update(); - break; - - default: - state = STATE.NONE; - } - } - - function touchend(/* event */) { - if (scope.enabled === false) return; - scope.dispatchEvent(endEvent); - state = STATE.NONE; - } - - this.onMouseUp = onMouseUp; - - domEl.addEventListener('contextmenu', function (event) { event.preventDefault() }, false); - domEl.addEventListener('mousedown', onMouseDown, false); - domEl.addEventListener('mousewheel', onMouseWheel, false); - domEl.addEventListener('DOMMouseScroll', onMouseWheel, false); // firefox - domEl.addEventListener('touchstart', touchstart, false); - domEl.addEventListener('touchend', touchend, false); - domEl.addEventListener('touchmove', touchmove, false); - - window.addEventListener('keydown', onKeyDown, false); - } -} - -export { Orbit }; diff --git a/src2/moto/space.js b/src2/moto/space.js deleted file mode 100644 index 54a7c4d4..00000000 --- a/src2/moto/space.js +++ /dev/null @@ -1,1517 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import { broker } from './broker.js'; -import { THREE } from '../ext/three.js'; -import { Orbit } from './orbit.js'; -import '../ext/tween.js'; - -const { - WebGLRenderer, - Scene, - Group, - MeshPhongMaterial, - DoubleSide -} = THREE; - -const nav = navigator; - -let WIN = self.window || {}, - DOC = self.document, - SCENE = new Scene(), - WORLD = new Group(), - PI = Math.PI, - PI2 = PI / 2, - PI4 = PI / 4, - panX = 0, - panY = 0, - panZ = 0, - home = 0, - up = PI4, - gridZOff = 0, - tweenTime = 500, - tweenDelay = 20, - platformZOff = 0, - perspective = 35, - refreshTimeout = null, - refreshRequested = false, - selectRecurse = false, - defaultKeys = true, - initialized = false, - alignedTracking = false, - skyAmbient, - skyGridColor = 0xcccccc, - skyGridMaterial = undefined, - showSkyGrid = false, - showPlatform = true, - hidePlatformBelow = true, - hideGridBelow = false, - showGrid = true, - showGridLines = true, - showFocus = 0, - focalPoint = undefined, - lightInfo = { - mode: 2, - array: [], - intensity: 0.09, - debug: false - }, - cameraLight = addLight(0, 0, 0, lightInfo.intensity), - origin = {x:0, y:0, z: 0}, - mouse = {x: 0, y: 0}, - mouseStart = null, - mouseDragPoint = null, - mouseDragStart = null, - mouseDownSelect, - mouseUpSelect, - mouseUp, - mouseDown, - mouseHover, - mouseDrag, - grid = { - origin: origin, - unitMinor: 0, - unitMajor: 0, - colorMinor: 0xeeeeee, - colorMajor: 0xcccccc, - colorX: 0xff6666, - colorY: 0x6666ff, - zoff: 0, - opacity: 1, - view: undefined, - axes: undefined, - lines: undefined - }, - ruler = { - x1: 0, - x2: 0, - y1: 0, - y2: 0, - xlabel: 'X', - ylabel: 'Y', - xon: undefined, - yon: undefined, - factor: undefined, - view: undefined - }, - psize = {}, - timers = {}, - fontColor = '#333333', - fontScale = 1.4, // computed relative to grid size - rulerColor, - axisColor, - axesOn = true, - volumeOn = true, - viewControl, - trackPlane, - platform, - platformHover, - platformClick, - platformClickAt, - platformOnMove, - platformOnMoveTime = 500, - platformMoveTimer, - platformBelt = false, - volume, - camera, - renderer, - container, - raycaster, - freezeTo, - freeze, - isRound = false, - platformMaterial = new MeshPhongMaterial({ - color: 0xeeeeee, - specular: 0xeeeeee, - shininess: 0, - transparent: true, - opacity: 0.6, - side: DoubleSide - }), - hiddenKey, - vizChange, - docVisible = true, - antiAlias = WIN.devicePixelRatio <= 1, - lastAction = Date.now(), - renderTime = 0, - fps = 0; - -if (DOC) { - if (typeof DOC.hidden !== "undefined") { - hiddenKey = "hidden"; - vizChange = "visibilitychange"; - } else if (typeof DOC.msHidden !== "undefined") { - hiddenKey = "msHidden"; - vizChange = "msvisibilitychange"; - } else if (typeof DOC.webkitHidden !== "undefined") { - hiddenKey = "webkitHidden"; - vizChange = "webkitvisibilitychange"; - } - - DOC.addEventListener(vizChange, () => { - docVisible = DOC[hiddenKey] ? false : true; - }, false); -} - -function updateLastAction() { - lastAction = Date.now(); -} - -function delayed(key, time, fn) { - clearTimeout(timers[key]); - timers[key] = setTimeout(fn, time); -} - -function valueOr(val, def) { - return val !== undefined ? val : def; -} - -WORLD.contains = (obj) => { - return WORLD.children.contains(obj); -}; - -/** ****************************************************************** - * TWEENing Functions - ******************************************************************* */ - -function tweenit() { - self.TWEEN?.update(); - setTimeout(tweenit, tweenDelay); -} - -tweenit(); - -function tweenCamPan(x,y,z,left,up) { - updateLastAction(); - let pos = viewControl.getPosition(); - pos.panX = x; - pos.panY = y; - pos.panZ = z; - if (left !== undefined) pos.left = left; - if (up !== undefined) pos.up = up; - tweenCam(pos); -} - -function tweenCam(pos) { - let tf = function () { - viewControl.setPosition(this); - updateLastAction(); - refresh(); - }; - let from = Object.clone(viewControl.getPosition()); - let to = Object.clone(pos); - let dist = Math.abs(from.left - to.left); - if (dist > Math.PI) { - if (from.left < to.left) { - from.left += Math.PI * 2; - } else { - from.left -= Math.PI * 2; - } - } - new TWEEN.Tween(from). - to(to, tweenTime). - onUpdate(tf). - onComplete(() => { - viewControl.setPosition(pos); - updateLastAction(); - refresh(); - let { then } = pos; - if (typeof then === 'function') { - then(); - } - }). - start(); -} - -/** ****************************************************************** - * Utility Functions - ******************************************************************* */ - -function width() { return WIN.innerWidth } - -function height() { return WIN.innerHeight } - -function aspect() { return width() / height() } - -function addEventListener(el, key, fn) { - el.addEventListener(key, fn); -} - -function addEventHandlers(el, pairs) { - for (let i=0; i { freeze = undefined }, 10); -} - -function setPlatformColor(color) { - let was = platform.material.color.getHex(); - platform.material.color.set(color); - requestRefresh(); - return was; -} - -function setFont(options) { - if (options.color) fontColor = options.color; - if (options.scale) fontScale = options.scale; - if (options.axisColor) axisColor = options.axisColor; - if (options.rulerColor) rulerColor = options.rulerColor; - updateRulers(); -} - -function showAxes(bool) { - axesOn = bool; - updateRulers(); -} - -function showVolume(bool) { - volumeOn = bool; - if (volume) volume.visible = bool; - requestRefresh(); -} - -function canvasInMesh(w, h, textAlign, textBaseline, color, size) { - let canvas = document.createElement('canvas'), - canvasTexture = new THREE.CanvasTexture(canvas), - plane = new THREE.PlaneGeometry(w, h), - context = canvas.getContext('2d'), - scale = 8; - - canvas.width = w * scale; - canvas.height = h * scale; - - canvas.addEventListener("webglcontextlost", event => { - console.log('WEB GL CONTEXT LOST'); - event.preventDefault(); - }, false); - - canvas.addEventListener("webglcontextrestored", event => { - console.log('WEB GL CONTEXT RESTORED'); - }, false); - - try { - context.scale(scale, scale); - } catch (e) { - console.log('unable to create label canvas', e ? e.name : 'unknown'); - return {}; - } - context.fillStyle = color || fontColor; - context.font = `${size}px sans-serif`; - context.textAlign = textAlign; - context.textBaseline = textBaseline; - canvasTexture.minFilter = THREE.LinearFilter; - - // set 'transparent' to false to debug mesh bounds - let material = new THREE.MeshBasicMaterial({transparent: true, map: canvasTexture}); - let mesh = new THREE.Mesh(plane, material); - - return { context, mesh }; -} - - -function setRulers( - xon = ruler.xon, - yon = ruler.yon, - factor = ruler.factor || 1, - xl = ruler.xlabel || 'X', - yl = ruler.ylabel || 'Y') -{ - if (xon !== ruler.xon || yon !== ruler.yon || factor !== ruler.factor || xl !== ruler.xlabel || yl !== ruler.ylabel) { - ruler.factor = factor; - ruler.xon = xon; - ruler.yon = yon; - ruler.xlabel = xl; - ruler.ylabel = yl; - updateRulers(); - } -} - -function updateRulers() { - let { xon, yon, factor, xlabel, ylabel } = ruler; - let x = platform.scale.x, - y = isRound ? platform.scale.z : platform.scale.y, - z = isRound ? platform.scale.y : platform.scale.z, - w = x / 2, - h = y / 2, - d = z / 2, - zp = -d - platformZOff + numOrDef(gridZOff, (z/2-0.1)), - labelSize = grid.unitMinor * fontScale, - oldView = ruler.view, - view = ruler.view = new THREE.Group(); - - if (xon && axesOn) { - let xPadding = labelSize * 4, - canvas = canvasInMesh(x + xPadding, labelSize * 3, 'center', 'top', rulerColor, labelSize), - context = canvas.context, - mesh = canvas.mesh; - if (!(context && mesh)) return; - - if (platformBelt) { - for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { - context.fillText((i * factor).round(1).toString(), ruler.x2 - i + xPadding / 2, 0); - } - } else { - for (let i = 0; i >= ruler.x1; i -= grid.unitMajor) { - context.fillText((i * factor).round(1).toString(), ruler.xo + i + xPadding / 2, 0); - } - for (let i = 0; i <= ruler.x2; i += grid.unitMajor) { - context.fillText((i * factor).round(1).toString(), ruler.xo + i + xPadding / 2, 0); - } - } - - context.font = (labelSize * 0.75) + 'px sans-serif'; - context.fillText(xlabel, (x + xPadding) / 2, labelSize * 1.5); - mesh.position.set(0, - h - labelSize * 2, zp); - view.add(mesh); - } - - if (yon && axesOn) { - let yPadding = labelSize, - canvas = canvasInMesh(labelSize * 4, y + yPadding, 'end', 'middle', rulerColor, labelSize), - context = canvas.context, - mesh = canvas.mesh; - if (!(context && mesh)) return; - - for (let i = 0; i >= ruler.y1; i -= grid.unitMajor) { - context.fillText((i * factor).round(1), labelSize * 4, y - (ruler.yo + i) + yPadding / 2); - } - for (let i = 0; i <= ruler.y2; i += grid.unitMajor) { - context.fillText((i * factor).round(1), labelSize * 4, y - (ruler.yo + i) + yPadding / 2); - } - - context.font = (labelSize * 0.75) + 'px sans-serif'; - context.fillText(ylabel, labelSize * 1.25, (y + yPadding) / 2); - mesh.position.set(-w - labelSize * 2 - 5, 0, zp); - view.add(mesh); - } - - Space.scene.remove(oldView); - Space.scene.add(view); - requestRefresh(); -} - -function setGrid( - unitMajor = grid.unitMajor, - unitMinor = grid.unitMinor, - colorMajor = grid.colorMajor, - colorMinor = grid.colorMinor) -{ - if (!unitMajor) { - return; - } - if ( - unitMajor !== grid.unitMajor || unitMinor !== grid.unitMinor || - colorMajor !== grid.colorMajor || colorMinor !== grid.colorMinor - ) { - grid.unitMajor = unitMajor; - grid.unitMinor = unitMinor; - grid.colorMajor = colorMajor || grid.colorMajor; - grid.colorMinor = colorMinor || grid.colorMinor; - updateGrid(); - } -} - -function setGridColor(opt = {}) { - grid.colorMajor = valueOr(opt.major || opt.colorMajor, grid.colorMajor); - grid.colorMinor = valueOr(opt.minor || opt.colorMinor, grid.colorMinor); - grid.colorX = valueOr(opt.colorX, grid.colorX); - grid.colorY = valueOr(opt.colorY, grid.colorY); - updateGrid(); -} - -function numOrDef(v, dv) { - return v !== undefined ? v : dv; -} - -function modMatch(val, mod) { - let mv = Math.abs(val) % mod; - return (mv < 1) || ((mod - mv) < 1); -} - -function updateGrid() { - let { view, unitMinor, unitMajor, colorMajor, colorMinor, colorX, colorY } = grid; - let oldView = view; - let axes = grid.axes = new THREE.Group(); - let lines = grid.lines = new THREE.Group(); - - view = grid.view = new THREE.Group(); - view.visible = oldView ? oldView.visible : true; - view.add(lines); - view.add(axes); - - let majors = [], - minors = [], - x = platform.scale.x, - y = isRound ? platform.scale.z : platform.scale.y, - z = isRound ? platform.scale.y : platform.scale.z, - zp = -(z / 2) - platformZOff + numOrDef(gridZOff, (z/2-0.1)), - xh = x / 2, - yh = y / 2, - x1 = -xh - origin.x, - x2 = xh - origin.x, - y1 = -yh + origin.y, - y2 = yh + origin.y, - xo = x1 + xh, - yo = y1 + yh; - - ruler.x1 = x1; - ruler.x2 = x2; - ruler.y1 = y1; - ruler.y2 = y2; - ruler.xo = xh - xo; - ruler.yo = yh - yo; - - for (let x=-unitMinor; x>x1; x -= unitMinor) { - let oh = isRound ? Math.sqrt(1-(x/xh)*(x/xh)) * yh : yh; - let arr = modMatch(x, unitMajor) ? majors : minors; - arr.append({x:x-xo, y:-oh, z:zp}).append({x:x-xo, y:oh, z:zp}); - } - for (let x=unitMinor; xy1; y -= unitMinor) { - let ow = isRound ? Math.sqrt(1-(y/yh)*(y/yh)) * xh : xh; - let arr = modMatch(y, unitMajor) ? majors : minors; - arr.append({x:-ow, y:y-yo, z:zp}).append({x:ow, y:y-yo, z:zp}); - } - for (let y=unitMinor; y { - m.renderOrder = 3; - }); - Space.scene.add(group); - const scale = grid.origin.scale = () => { - const dist = camera.position.distanceTo(group.position); - const scale = Math.min(dist / 100, 0.5); - group.scale.set(scale, scale, scale); - }; - scale(); - updateDraws(); -} - -function setRound(bool) { - let current = platform; - isRound = bool; - if (bool) { - platform = new THREE.Mesh( - new THREE.CylinderGeometry(.5, .5, 1, 60), - platformMaterial - ); - platform.rotation.x = 0; - } else { - platform = new THREE.Mesh( - new THREE.BoxGeometry(1, 1, 1), - platformMaterial - ); - platform.rotation.x = -PI2; - } - - platform.position.y = current.position.y; - platform.visible = current.visible; - - SCENE.remove(current); - SCENE.add(platform); - THREE.dispose(current); -} - -function refresh() { - refreshRequested = false; - clearTimeout(refreshTimeout); - updateLastAction(); - viewControl.update(); -} - -/** deferred refresh that collapses multiple requests */ -function requestRefresh(timeout) { - if (refreshRequested === false) { - refreshRequested = true; - clearTimeout(refreshTimeout); - refreshTimeout = setTimeout(refresh, timeout || 10); - } -} - -function onResize() { - updateLastAction(); - camera.aspect = aspect(); - camera.updateProjectionMatrix(); - renderer.setSize(width(), height()); - container.style.width = width(); - container.style.height = height(); - requestRefresh(); -} - -function cca(c) { - return c.charCodeAt(0); -} - -function inputHasFocus() { - return DOC.activeElement && (DOC.activeElement != DOC.body); -} - -function keyHandler(evt) { - updateLastAction(); - if (!defaultKeys || inputHasFocus()) return false; - if (evt.metaKey) return false; - let handled = true; - switch (evt.charCode) { - case cca('z'): - Space.view.reset(); - break; - case cca('h'): - Space.view.home(); - break; - case cca('t'): - Space.view.top(); - break; - case cca('f'): - Space.view.front(); - cycleInd = 0; - break; - case cca('b'): - Space.view.back(); - cycleInd = 2; - break; - case cca('>'): - cycleInd = (++cycleInd % cycle.length); - cycle[cycleInd](); - break; - case cca('<'): - cycleInd--; - if (cycleInd < 0) { - cycleInd += cycle.length; - } - cycle[cycleInd](); - break; - default: - handled = false; - break; - } - if (handled) evt.preventDefault(); - return false; -} - -/** ****************************************************************** - * ThreeJS Helper Functions - ******************************************************************* */ - -function makeLinesFromPoints(points, color) { - if (points.length % 2 != 0) { - throw "invalid line : "+points.length; - } - const geo = new THREE.BufferGeometry(); - const vrt = new Float32Array(points.length * 3); - let vi = 0; - for (let p of points) { - vrt[vi++] = p.x; - vrt[vi++] = p.y; - vrt[vi++] = p.z; - } - geo.setAttribute('position', new THREE.BufferAttribute(vrt, 3)); - return new THREE.LineSegments(geo, new THREE.LineBasicMaterial({ color })); -} - -function intersect(objects, recurse) { - // console.log(({int: objects})); - let ints = raycaster.intersectObjects(objects, recurse); - // console.trace({ints}); - return ints; -} - -/** ****************************************************************** - * Mouse Functions - ******************************************************************* */ - -function onMouseDown(event) { - updateLastAction(); - if (event.target === renderer.domElement) { - DOC.activeElement.blur(); - event.preventDefault(); - let selection = null, - trackTo = alignedTracking ? trackPlane : platform, - isVis = trackTo.visible; - if (mouseDownSelect) { - selection = mouseDownSelect(undefined, event); - } - if (selection && selection.length > 0) { - // selection = selection.map(o => o.isGroup ? o.children : o).flat(); - // console.log({ selection }); - trackTo.visible = true; - let int = intersect(selection.slice().append(trackTo), false); - trackTo.visible = isVis; - if (int.length > 0) { - let trackInt, selectInt; - for (let i=0; i 0 ? int[0].point : null; - } - } else { - viewControl.enabled = false; - } - mouseStart = { - x: (event.clientX / width()) * 2 - 1, - y: -(event.clientY / height()) * 2 + 1}; -} - -function onMouseUp(event) { - updateLastAction(); - if (!viewControl.enabled) { - viewControl.enabled = true; - viewControl.onMouseUp(event); - } - let mouseEnd = { - x: (event.clientX / width()) * 2 - 1, - y: -(event.clientY / height()) * 2 + 1}; - // only fire on mouse move between mouseStart (down) and up - if (mouseStart && mouseEnd.x - mouseStart.x + mouseEnd.y - mouseStart.y === 0) { - event.preventDefault(); - let refresh = false, - selection = null; - if (mouseUpSelect) { - selection = mouseUpSelect(); - } - if (selection && selection.length > 0) { - let int = intersect(selection, selectRecurse); - if (mouseUp) { - if (int.length) { - mouseUp(event, int); - } else { - mouseUp(event, intersect([platform], selectRecurse)); - } - if (event.button === 2) { - return; - } - } - if (int.length > 0) { - mouseUpSelect(int[0], event, int); - refresh = true; - } else { - mouseUpSelect(null, event); - } - } - if (!refresh && platformClickAt) { - platformClick(platformClickAt); - } - if (refresh) { - requestRefresh(); - } - mouseStart = null; - } else if (mouseDrag && mouseDragStart) { - // fired on mouse drag end - mouseDrag(null,null,true); - } - mouseDragPoint = null; - mouseDragStart = null; -} - -function onMouseMove(event) { - updateLastAction(); - let int, vis; - - const mv = new THREE.Vector2(); - mv.x = ( event.clientX / window.innerWidth ) * 2 - 1; - mv.y = - ( event.clientY / window.innerHeight ) * 2 + 1; - raycaster.setFromCamera( mv, camera ); - - if (viewControl.enabled) { - event.preventDefault(); - let selection = mouseHover ? mouseHover() : null; - if (selection && selection.length > 0) { - int = intersect(selection, selectRecurse); - if (int.length > 0) mouseHover(int[0], event, int); - } - if ((!int || int.length == 0) && platformHover) { - vis = platform.visible; - platform.visible = true; - int = intersect([platform], false); - platform.visible = vis; - if (int && int.length > 0) platformHover(int[0].point); - } - } else if (mouseDragPoint && mouseDrag && mouseDrag()) { - event.preventDefault(); - let trackTo = alignedTracking ? trackPlane : platform; - let vis = trackTo.visible; - trackTo.visible = true; - int = intersect([trackTo], false); - trackTo.visible = vis; - if (int.length > 0 && int[0].object === trackTo) { - let delta = mouseDragPoint.clone().sub(int[0].point); - let offset = mouseDragStart.clone().sub(int[0].point); - mouseDragPoint = int[0].point; - mouseDrag({ - x: -delta.x, - y: delta.z, - z: 0, - event - }, { - x: -offset.x, - y: offset.z, - z: 0 - }); - requestRefresh(); - } - } - mouse = { - x: (event.clientX / width()) * 2 - 1, - y: -(event.clientY / height()) * 2 + 1}; -} - -/** ****************************************************************** - * Space Object - ******************************************************************* */ - -function updateFocus() { - if (focalPoint) { - Space.scene.remove(focalPoint); - } - if (showFocus) { - let mesh = focalPoint = new THREE.Mesh( - new THREE.SphereGeometry(1, 16, 16), - new THREE.MeshPhongMaterial({ - side: THREE.DoubleSide, - specular: 0x202020, - color: 0xff0000, - shininess: 125 - }) - ); - Space.scene.add(mesh); - mesh.position.copy(viewControl.target); - } -} - -function setSky(opt = {}) { - let { grid, color, gridColor } = opt; - if (grid) Space.sky.showGrid(grid); - if (color !== undefined) Space.sky.setColor(color); - if (gridColor !== undefined) Space.sky.setGridColor(gridColor); - if (skyAmbient && opt.ambient) { - let { color, intensity } = opt.ambient; - if (color) skyAmbient.color.set(color); - if (intensity) skyAmbient.intensity = intensity; - } -} - -function setPlatform(opt = {}) { - let platform = Space.platform; - let { color, round, size, grid, opacity } = opt; - let { visible, volume, zOffset, origin, light } = opt; - if (light) { - lightInfo.intensity = light; - } - if (color) { - platform.setColor(color); - } - if (round !== undefined) { - platform.setRound(round); - } - if (size) { - let { width = 300, depth = 300, height = 2.5, maxz = 300 } = size; - platform.setSize(width, depth, height, maxz); - } - if (grid) { - let { zOffset } = grid; - let { major = 25, minor = 5 } = grid; - let { colorX, colorY, colorMajor, colorMinor } = grid; - platform.setGrid(major, minor); - platform.setGridColor({ colorX, colorY, colorMajor, colorMinor }); - if (zOffset !== undefined) platform.setGridZOff(zOffset); - } - if (origin) { - let { x, y, z, show } = origin; - platform.setOrigin(x || 0, y || 0, z || 0, show); - } - if (opacity !== undefined) { - platform.opacity(opacity); - } - if (volume !== undefined) { - platform.showVolume(volume); - } - if (zOffset !== undefined) { - platform.setZOff(zOffset); - } - if (visible !== undefined) { - platform.setVisible(visible); - } -} - -let Space = { - refresh: refresh, - update: requestRefresh, - - setAntiAlias(b) { antiAlias = b ? true : false }, - raycast: intersect, - - event: { - addHandlers: addEventHandlers, - onEnterKey: onEnterKey, - onResize: onResize - }, - - sky: { - set: setSky, - - showGrid: (b) => { - showSkyGrid = b; - }, - - setColor: (c) => { - SCENE.background = new THREE.Color(c); - }, - - setGridColor: (c) => { - skyGridColor = c; - if (skyGridMaterial) skyGridMaterial.color = new THREE.Color(c); - } - }, - - scene: { - add: function (o) { - o.rotation.x = WORLD.rotation.x; - return SCENE.add(o); - }, - - remove: function (o) { - THREE.dispose(o); - return SCENE.remove(o); - }, - - active: updateLastAction, - - setFog: function(mult, color) { - if (mult) { - SCENE.fog = new THREE.Fog(color, 100, 1000); - SCENE.fog.mult = mult > 0 ? mult : 3; - } else { - SCENE.fog = undefined; - } - Space.scene.updateFog(); - }, - - updateFog: function() { - const { fog } = SCENE; - if (fog) { - const dist = camera.position.distanceTo(viewControl.target); - fog.near = dist; - fog.far = dist * fog.mult; - } - // todo: option to clip for close views - // camera.near = dist / 2; - // camera.updateProjectionMatrix(); - }, - - lightInfo: () => { - setTimeout(updateLights, 0); - return lightInfo; - } - }, - - world: { - add: function(o) { - return WORLD.add(o); - }, - - remove: function(o) { - THREE.dispose(o); - return WORLD.remove(o); - }, - - newGroup: function() { - return WORLD.newGroup(); - } - }, - - platform: { - set: setPlatform, - update: updateDraws, - setSize: setPlatformSizeUpdateGrid, - setColor: setPlatformColor, - setOrigin, - setRulers, - setGrid, - setGridColor, - setFont, - setRound, - showAxes, - showVolume, - showGridBelow: (b) => { hideGridBelow = !b }, - showGrid: (b) => { showGrid = grid.view.visible = b }, - showGrid2: (b) => { showGridLines = grid.lines.visible = b }, - setMaxZ: (z) => { panY = z / 2 }, - setCenter: (x,y,z) => { panX = x; panY = z, panZ = y }, - setHidden: (b) => { showPlatform = !b; platform.visible = !b }, - setVisible: (b) => { showPlatform = b; platform.visible = b }, - setHiding: (b) => { hidePlatformBelow = b }, - setZOff: (z) => { platformZOff = z; updatePlatformPosition() }, - setGridZOff:(z) => { gridZOff = z; updatePlatformPosition() }, - setBelt: (b) => { platformBelt = b }, - isHidden: () => { return !showPlatform }, - isVisible: () => { return platform.visible }, - isGridVisible() { return grid.view.visible }, - opacity: (o) => { platform.material.opacity = o; Space.platform.setVisible(o > 0) }, - onMove: (f,t) => { platformOnMove = f, platformOnMoveTime = t || platformOnMoveTime }, - onHover: (f) => { platformHover = f }, - onClick: (f) => { platformClick = f}, - size: () => { return platform.scale }, - get world() { throw "platform.world deprecated" } - }, - - preset: { - top: {left: home, up: 0, panX, panY, panZ}, - back: {left: PI, up: PI2, panX, panY, panZ}, - home: {left: home, up, panX, panY, panZ}, - front: {left: 0, up: PI2, panX, panY, panZ}, - right: {left: PI2, up: PI2, panX, panY, panZ}, - left: {left: -PI2, up: PI2, panX, panY, panZ}, - }, - - view: { - top: (then) => { tweenCam({left: home, up: 0, panX, panY, panZ, then}) }, - back: (then) => { tweenCam({left: PI, up: PI2, panX, panY, panZ, then}) }, - home: (then) => { tweenCam({left: home, up, panX, panY, panZ, then}) }, - front: (then) => { tweenCam({left: 0, up: PI2, panX, panY, panZ, then}) }, - right: (then) => { tweenCam({left: PI2, up: PI2, panX, panY, panZ, then}) }, - left: (then) => { tweenCam({left: -PI2, up: PI2, panX, panY, panZ, then}) }, - reset: () => { viewControl.reset(); requestRefresh() }, - load: (cam) => { viewControl.setPosition(cam); requestRefresh() }, - save: () => { return viewControl.getPosition(true) }, - panTo: (x,y,z,l,u) => { tweenCamPan(x,y,z,l,u) }, - setZoom: (r,v) => { viewControl.setZoom(r,v) }, - setCtrl: (name) => { - if (name === 'onshape') { - viewControl.setMouse(viewControl.mouseOnshape); - } else { - viewControl.setMouse(viewControl.mouseDefault); - } - }, - getFPS () { return fps }, - getRMS() { return renderTime }, - getFocus() { return viewControl.getTarget() }, - setFocus(v) { - viewControl.setTarget(v); - updateFocus(); - refresh() - }, - showFocus(ms) { - showFocus = ms; - updateFocus(); - }, - setHome(r,u) { - home = r || 0; - up = u || PI4; - }, - spin(then, count) { - Space.view.front(() => { - Space.view.right(() => { - Space.view.back(() => { - Space.view.left(() => { - if (--count > 0) { - Space.view.spin(then, count); - } else { - Space.view.front(then); - } - }); - }); - }); - }); - }, - get ctrl() { - return viewControl; - } - }, - - mouse: { - up: (f) => { mouseUp = f }, - down: (f) => { mouseDown = f }, - downSelect: (f) => { mouseDownSelect = f }, - upSelect: (f) => { mouseUpSelect = f }, - onDrag: (f) => { mouseDrag = f }, - onHover: (f) => { mouseHover = f } - }, - - tween: { - setTime: (t) => { tweenTime = t || 500 }, - setDelay: (d) => { tweenDelay = d || 20 } - }, - - useDefaultKeys (b) { defaultKeys = b }, - selectRecurse (b) { selectRecurse = b }, - renderInfo () { return renderer.info }, - objects () { return WORLD.children }, - - screenshot(format, options) { - return renderer.domElement.toDataURL(format || "image/png", options); - }, - - screenshot2(param = {}) { - let oco = renderer.domElement; - let oWidth = oco.offsetWidth; - let oHeight = oco.offsetHeight; - let oRatio = oHeight / oWidth; - let width = param.width || 240; - let ncv = document.createElement('canvas'); - ncv.width = width; - ncv.height = width * oRatio; - let nco = ncv.getContext('2d'); - nco.drawImage(oco, 0, 0, ncv.width, ncv.height); - return { - url: ncv.toDataURL(param.format || "image/png", param.options), - width: ncv.width, - height: ncv.height, - }; - }, - - screenshot3(param = {}) { - let oco = renderer.domElement; - let oWidth = oco.offsetWidth; - let oHeight = oco.offsetHeight; - let oRatio = oWidth / oHeight; - let width = param.width || 512; - let height = param.height || width; - let nRatio = width / height; - let ncv = document.createElement('canvas'); - ncv.width = width; - ncv.height = height; - let nco = ncv.getContext('2d'); - let ox = 0, oy = 0; - if (oRatio > nRatio) { - let tmp = oWidth; - oWidth = oHeight * nRatio; - ox = (tmp - oWidth) / 2; - } else { - let tmp = oHeight; - oHeight = oWidth * nRatio; - oy = (tmp - oHeight) / 2; - } - nco.drawImage(oco, ox, oy, oWidth, oHeight, 0, 0, width, height); - if (param.out) { - ncv.toBlob(blob => blob.arrayBuffer().then(png => param.out({ png, width, height }))); - } - return ncv; - }, - - internals: () => { - return { renderer, camera, platform }; - }, - - init: (domelement, slider, ortho) => { - container = domelement; - - WORLD.rotation.x = -PI2; - SCENE.add(WORLD); - - domelement.style.width = width(); - domelement.style.height = height(); - - renderer = new WebGLRenderer({ - antialias: antiAlias, - preserveDrawingBuffer: true, - logarithmicDepthBuffer: true - }); - - // THREE.ColorManagement.enabled = false; - // renderer.outputColorSpace = THREE.LinearSRGBColorSpace; - - renderer.localClippingEnabled = true; - camera = ortho ? - new THREE.OrthographicCamera(-100 * aspect(), 100 * aspect(), 100, -100, 0.1, 100000) : - new THREE.PerspectiveCamera(perspective, aspect(), 0.1, 100000); - - camera.position.set(0, 200, 340); - renderer.setSize(width(), height()); - domelement.appendChild(renderer.domElement); - - raycaster = new THREE.Raycaster(); - - viewControl = new Orbit(camera, domelement, (position, moved) => { - if (platform) { - platform.visible = hidePlatformBelow ? - initialized && position.y >= 0 && showPlatform : showPlatform; - volume.visible = showVolume && platform.visible; - } - if (grid.view) { - grid.view.visible = hideGridBelow ? platform.visible : showGrid; - } - if (cameraLight) { - cameraLight.position.copy(camera.position); - } - if (moved && platformOnMove) { - clearTimeout(platformMoveTimer); - platformMoveTimer = setTimeout(platformOnMove, 500); - Space.scene.updateFog(); - } - updateLastAction(); - updateFocus(); - }, (val) => { - if (camera && grid?.origin?.scale) { - grid.origin.scale(); - } - if (camera && viewControl) { - // increase intersect line precision on zoom - const dist = camera.position.distanceTo(viewControl.target); - raycaster.params.Line.threshold = Math.min(1, dist / 100); - } - // broker.publish("space.view.zoom", camera); - updateLastAction(); - if (slider) slider(val); - }); - - viewControl.noKeys = true; - viewControl.maxDistance = 1000; - - SCENE.add(skyAmbient = new THREE.AmbientLight(0x707070)); - - platform = new THREE.Mesh( - new THREE.BoxGeometry(1, 1, 1), - platformMaterial - ); - - platform.position.y = platformZOff; - platform.rotation.x = -PI2; - platform.visible = showPlatform; - - trackPlane = new THREE.Mesh( - new THREE.PlaneGeometry(100000, 100000, 1, 1), - new THREE.MeshBasicMaterial( { color: 0x777777, opacity: 0, transparent: true } ) - ); - trackPlane.visible = false; - trackPlane.rotation.x = PI2; - - let skygrid = new THREE.Mesh( - new THREE.BoxGeometry(10000, 10000, 10000, 10, 10, 10), - skyGridMaterial = - new THREE.MeshBasicMaterial({ color: skyGridColor, side: THREE.DoubleSide }) - ); - - SCENE.add(platform); - SCENE.add(trackPlane); - - if (showSkyGrid) { - skygrid.material.wireframe = true; - SCENE.add(skygrid); - } - - addEventHandlers(WIN, [ - 'resize', onResize, - 'mousemove', onMouseMove, - 'mousedown', onMouseDown, - 'mouseup', onMouseUp, - 'keypress', keyHandler, - 'touchstart', updateLastAction, - 'touchmove', updateLastAction, - 'touchend', updateLastAction - ]); - - let animates = 0; - let rateStart = Date.now(); - let renderStart; - let renders = []; - - function animate() { - animates++; - const now = Date.now(); - if (now - rateStart > 1000) { - // compute stats roughly every second - const delta = now - rateStart; - fps = 1000 * animates / delta; - animates = 0; - rateStart = now; - // look for slowest rendered frame - renderTime = Math.max(0, ...renders); - renders.length = 0; - } - requestAnimationFrame(animate); - if (docVisible && !freeze && Date.now() - lastAction < 1500) { - renderStart = Date.now(); - renderer.render(SCENE, camera); - // track frame render times - renders.push(Date.now() - renderStart); - } else { - fps = 0; - } - } - - animate(); - - const ctx = renderer.getContext(); - - Space.info = { - ver: ctx.getParameter(ctx.VERSION), - ven: ctx.getParameter(ctx.VENDOR), - glr: ctx.getParameter(ctx.RENDERER), - pla: nav.platform, - mob: nav.maxTouchPoints > 1 || (/android/i.test(nav.userAgent)) - }; - - initialized = true; - } -}; - -let cycle = [ - Space.view.front, - Space.view.right, - Space.view.back, - Space.view.left, -]; - -let cycleInd = 0; - -export const space = Space; diff --git a/web2/fon2 b/web.old/fon2 similarity index 100% rename from web2/fon2 rename to web.old/fon2 diff --git a/web2/font b/web.old/font similarity index 100% rename from web2/font rename to web.old/font diff --git a/web2/kiri/cxdlp.html b/web.old/kiri/cxdlp.html similarity index 100% rename from web2/kiri/cxdlp.html rename to web.old/kiri/cxdlp.html diff --git a/web2/kiri/engine.html b/web.old/kiri/engine.html similarity index 100% rename from web2/kiri/engine.html rename to web.old/kiri/engine.html diff --git a/web2/kiri/favicon-mobile.png b/web.old/kiri/favicon-mobile.png similarity index 100% rename from web2/kiri/favicon-mobile.png rename to web.old/kiri/favicon-mobile.png diff --git a/web2/kiri/favicon.ico b/web.old/kiri/favicon.ico similarity index 100% rename from web2/kiri/favicon.ico rename to web.old/kiri/favicon.ico diff --git a/web2/kiri/frame.html b/web.old/kiri/frame.html similarity index 100% rename from web2/kiri/frame.html rename to web.old/kiri/frame.html diff --git a/web2/kiri/gsbox.png b/web.old/kiri/gsbox.png similarity index 100% rename from web2/kiri/gsbox.png rename to web.old/kiri/gsbox.png diff --git a/web2/kiri/gyroid-test.html b/web.old/kiri/gyroid-test.html similarity index 100% rename from web2/kiri/gyroid-test.html rename to web.old/kiri/gyroid-test.html diff --git a/web2/kiri/index.css b/web.old/kiri/index.css similarity index 99% rename from web2/kiri/index.css rename to web.old/kiri/index.css index a1f75cbe..a35f0b7f 100644 --- a/web2/kiri/index.css +++ b/web.old/kiri/index.css @@ -251,9 +251,6 @@ details[open] summary::after { background-color: #333; color: #fff; } -.dark option:checked { - background-color: var(--blue-1); -} .dark hr { border-color: #555; } @@ -2189,13 +2186,11 @@ details[open] summary::after { } #slider-zero { z-index: 10; - min-width: 3em; background-color: #bbb; border-top-left-radius: 5px; border-bottom-left-radius: 5px; } #slider-max { - min-width: 3em; background-color: #bbb; border-top-right-radius: 5px; border-bottom-right-radius: 5px; diff --git a/web2/kiri/index.html b/web.old/kiri/index.html similarity index 98% rename from web2/kiri/index.html rename to web.old/kiri/index.html index c6d3fe27..3eb610ab 100644 --- a/web2/kiri/index.html +++ b/web.old/kiri/index.html @@ -17,16 +17,14 @@ Kiri:Moto - - - - - - - - - - + + + + + + + +
diff --git a/web2/kiri/lang/da-dk.js b/web.old/kiri/lang/da-dk.js similarity index 100% rename from web2/kiri/lang/da-dk.js rename to web.old/kiri/lang/da-dk.js diff --git a/web2/kiri/lang/da.js b/web.old/kiri/lang/da.js similarity index 100% rename from web2/kiri/lang/da.js rename to web.old/kiri/lang/da.js diff --git a/web2/kiri/lang/de-de.js b/web.old/kiri/lang/de-de.js similarity index 100% rename from web2/kiri/lang/de-de.js rename to web.old/kiri/lang/de-de.js diff --git a/web2/kiri/lang/de.js b/web.old/kiri/lang/de.js similarity index 100% rename from web2/kiri/lang/de.js rename to web.old/kiri/lang/de.js diff --git a/web2/kiri/lang/en-us.js b/web.old/kiri/lang/en-us.js similarity index 100% rename from web2/kiri/lang/en-us.js rename to web.old/kiri/lang/en-us.js diff --git a/web2/kiri/lang/en.js b/web.old/kiri/lang/en.js similarity index 100% rename from web2/kiri/lang/en.js rename to web.old/kiri/lang/en.js diff --git a/web2/kiri/lang/es-es.js b/web.old/kiri/lang/es-es.js similarity index 100% rename from web2/kiri/lang/es-es.js rename to web.old/kiri/lang/es-es.js diff --git a/web2/kiri/lang/es.js b/web.old/kiri/lang/es.js similarity index 100% rename from web2/kiri/lang/es.js rename to web.old/kiri/lang/es.js diff --git a/web2/kiri/lang/fr-fr.js b/web.old/kiri/lang/fr-fr.js similarity index 100% rename from web2/kiri/lang/fr-fr.js rename to web.old/kiri/lang/fr-fr.js diff --git a/web2/kiri/lang/fr.js b/web.old/kiri/lang/fr.js similarity index 100% rename from web2/kiri/lang/fr.js rename to web.old/kiri/lang/fr.js diff --git a/web2/kiri/lang/pl-pl.js b/web.old/kiri/lang/pl-pl.js similarity index 100% rename from web2/kiri/lang/pl-pl.js rename to web.old/kiri/lang/pl-pl.js diff --git a/web2/kiri/lang/pl.js b/web.old/kiri/lang/pl.js similarity index 100% rename from web2/kiri/lang/pl.js rename to web.old/kiri/lang/pl.js diff --git a/web2/kiri/lang/pt-pt.js b/web.old/kiri/lang/pt-pt.js similarity index 100% rename from web2/kiri/lang/pt-pt.js rename to web.old/kiri/lang/pt-pt.js diff --git a/web2/kiri/lang/pt.js b/web.old/kiri/lang/pt.js similarity index 100% rename from web2/kiri/lang/pt.js rename to web.old/kiri/lang/pt.js diff --git a/web2/kiri/lang/zh.js b/web.old/kiri/lang/zh.js similarity index 100% rename from web2/kiri/lang/zh.js rename to web.old/kiri/lang/zh.js diff --git a/web2/kiri/logo-cube-128.png b/web.old/kiri/logo-cube-128.png similarity index 100% rename from web2/kiri/logo-cube-128.png rename to web.old/kiri/logo-cube-128.png diff --git a/web2/kiri/logo-cube-144.png b/web.old/kiri/logo-cube-144.png similarity index 100% rename from web2/kiri/logo-cube-144.png rename to web.old/kiri/logo-cube-144.png diff --git a/web2/kiri/logo-cube-256.png b/web.old/kiri/logo-cube-256.png similarity index 100% rename from web2/kiri/logo-cube-256.png rename to web.old/kiri/logo-cube-256.png diff --git a/web2/kiri/logo-cube-512.png b/web.old/kiri/logo-cube-512.png similarity index 100% rename from web2/kiri/logo-cube-512.png rename to web.old/kiri/logo-cube-512.png diff --git a/web2/kiri/logo-cube-64.png b/web.old/kiri/logo-cube-64.png similarity index 100% rename from web2/kiri/logo-cube-64.png rename to web.old/kiri/logo-cube-64.png diff --git a/web2/kiri/manifest.json b/web.old/kiri/manifest.json similarity index 100% rename from web2/kiri/manifest.json rename to web.old/kiri/manifest.json diff --git a/web2/kiri/style-alt.css b/web.old/kiri/style-alt.css similarity index 100% rename from web2/kiri/style-alt.css rename to web.old/kiri/style-alt.css diff --git a/web2/kiri/style-atom3dp.css b/web.old/kiri/style-atom3dp.css similarity index 100% rename from web2/kiri/style-atom3dp.css rename to web.old/kiri/style-atom3dp.css diff --git a/web2/mesh/favicon.ico b/web.old/mesh/favicon.ico similarity index 100% rename from web2/mesh/favicon.ico rename to web.old/mesh/favicon.ico diff --git a/web/mesh/fiddle.html b/web.old/mesh/fiddle.html similarity index 100% rename from web/mesh/fiddle.html rename to web.old/mesh/fiddle.html diff --git a/web2/mesh/index.css b/web.old/mesh/index.css similarity index 100% rename from web2/mesh/index.css rename to web.old/mesh/index.css diff --git a/web2/mesh/index.html b/web.old/mesh/index.html similarity index 68% rename from web2/mesh/index.html rename to web.old/mesh/index.html index b11ee937..79b8c42b 100644 --- a/web2/mesh/index.html +++ b/web.old/mesh/index.html @@ -11,14 +11,12 @@ Mesh:Tool - - - - - - - - + + + + + +
diff --git a/web/meta/favicon-mobile.png b/web.old/meta/favicon-mobile.png similarity index 100% rename from web/meta/favicon-mobile.png rename to web.old/meta/favicon-mobile.png diff --git a/web/meta/favicon.ico b/web.old/meta/favicon.ico similarity index 100% rename from web/meta/favicon.ico rename to web.old/meta/favicon.ico diff --git a/web/meta/index.css b/web.old/meta/index.css similarity index 100% rename from web/meta/index.css rename to web.old/meta/index.css diff --git a/web/meta/index.html b/web.old/meta/index.html similarity index 100% rename from web/meta/index.html rename to web.old/meta/index.html diff --git a/web2/moto/russo-one.ttf b/web.old/moto/russo-one.ttf similarity index 100% rename from web2/moto/russo-one.ttf rename to web.old/moto/russo-one.ttf diff --git a/web2/obj/box.stl b/web.old/obj/box.stl similarity index 100% rename from web2/obj/box.stl rename to web.old/obj/box.stl diff --git a/web2/obj/cube.stl b/web.old/obj/cube.stl similarity index 100% rename from web2/obj/cube.stl rename to web.old/obj/cube.stl diff --git a/web/kiri/index.css b/web/kiri/index.css index a35f0b7f..a1f75cbe 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -251,6 +251,9 @@ details[open] summary::after { background-color: #333; color: #fff; } +.dark option:checked { + background-color: var(--blue-1); +} .dark hr { border-color: #555; } @@ -2186,11 +2189,13 @@ details[open] summary::after { } #slider-zero { z-index: 10; + min-width: 3em; background-color: #bbb; border-top-left-radius: 5px; border-bottom-left-radius: 5px; } #slider-max { + min-width: 3em; background-color: #bbb; border-top-right-radius: 5px; border-bottom-right-radius: 5px; diff --git a/web/kiri/index.html b/web/kiri/index.html index 3eb610ab..b51031dc 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -17,14 +17,16 @@ Kiri:Moto - - - - - - - - + + + + + + + + + +
diff --git a/web/mesh/index.html b/web/mesh/index.html index 79b8c42b..2df11e36 100644 --- a/web/mesh/index.html +++ b/web/mesh/index.html @@ -11,12 +11,15 @@ Mesh:Tool - - - - - - + + + + + + + + +
diff --git a/web2/obj/pocket-test.stl b/web/obj/pocket-test.stl similarity index 100% rename from web2/obj/pocket-test.stl rename to web/obj/pocket-test.stl diff --git a/web2/lib b/web2/lib deleted file mode 120000 index d487e974..00000000 --- a/web2/lib +++ /dev/null @@ -1 +0,0 @@ -../src2/ \ No newline at end of file